From d58747de8a2a2dee4dcb6d444af2efe456ff8112 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Mon, 19 Apr 2021 11:45:20 -0700 Subject: [PATCH] Revert "fix(zone.js): should continue to executue listeners when throw error (#41562)" (#41707) This reverts commit 5c48cd30b5e0d871adb8e72d4defa643495aa4ca. Reason: that change introduces race conditions on CI. PR Close #41707 --- packages/zone.js/lib/browser/browser.ts | 2 +- .../lib/browser/event-target-legacy.ts | 2 +- packages/zone.js/lib/browser/event-target.ts | 2 +- packages/zone.js/lib/browser/shadydom.ts | 2 +- .../browser/webapis-rtc-peer-connection.ts | 2 +- packages/zone.js/lib/browser/websocket.ts | 2 +- packages/zone.js/lib/common/events.ts | 64 +++++++------ packages/zone.js/lib/extra/socket-io.ts | 2 +- packages/zone.js/lib/node/events.ts | 4 +- packages/zone.js/lib/zone.ts | 42 ++++----- packages/zone.js/test/browser/browser.spec.ts | 94 +------------------ 11 files changed, 64 insertions(+), 154 deletions(-) diff --git a/packages/zone.js/lib/browser/browser.ts b/packages/zone.js/lib/browser/browser.ts index 88521a71f6..a6190c42ef 100644 --- a/packages/zone.js/lib/browser/browser.ts +++ b/packages/zone.js/lib/browser/browser.ts @@ -66,7 +66,7 @@ Zone.__load_patch('EventTarget', (global: any, Zone: ZoneType, api: _ZonePrivate // patch XMLHttpRequestEventTarget's addEventListener/removeEventListener const XMLHttpRequestEventTarget = (global as any)['XMLHttpRequestEventTarget']; if (XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype) { - api.patchEventTarget(global, api, [XMLHttpRequestEventTarget.prototype]); + api.patchEventTarget(global, [XMLHttpRequestEventTarget.prototype]); } }); diff --git a/packages/zone.js/lib/browser/event-target-legacy.ts b/packages/zone.js/lib/browser/event-target-legacy.ts index 5bebb2c395..9cf7a939f9 100644 --- a/packages/zone.js/lib/browser/event-target-legacy.ts +++ b/packages/zone.js/lib/browser/event-target-legacy.ts @@ -112,7 +112,7 @@ export function eventTargetLegacyPatch(_global: any, api: _ZonePrivate) { } // vh is validateHandler to check event handler // is valid or not(for security check) - api.patchEventTarget(_global, api, apiTypes, { + api.patchEventTarget(_global, apiTypes, { vh: checkIEAndCrossContext, transferEventName: (eventName: string) => { const pointerEventName = pointerEventsMap[eventName]; diff --git a/packages/zone.js/lib/browser/event-target.ts b/packages/zone.js/lib/browser/event-target.ts index 215ac3d13e..8ef5418f2a 100644 --- a/packages/zone.js/lib/browser/event-target.ts +++ b/packages/zone.js/lib/browser/event-target.ts @@ -29,7 +29,7 @@ export function eventTargetPatch(_global: any, api: _ZonePrivate) { if (!EVENT_TARGET || !EVENT_TARGET.prototype) { return; } - api.patchEventTarget(_global, api, [EVENT_TARGET && EVENT_TARGET.prototype]); + api.patchEventTarget(_global, [EVENT_TARGET && EVENT_TARGET.prototype]); return true; } diff --git a/packages/zone.js/lib/browser/shadydom.ts b/packages/zone.js/lib/browser/shadydom.ts index ef3e0c9192..ca7d5ba86c 100644 --- a/packages/zone.js/lib/browser/shadydom.ts +++ b/packages/zone.js/lib/browser/shadydom.ts @@ -20,7 +20,7 @@ Zone.__load_patch('shadydom', (global: any, Zone: ZoneType, api: _ZonePrivate) = if (proto && proto.hasOwnProperty('addEventListener')) { proto[Zone.__symbol__('addEventListener')] = null; proto[Zone.__symbol__('removeEventListener')] = null; - api.patchEventTarget(global, api, [proto]); + api.patchEventTarget(global, [proto]); } }); }); diff --git a/packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts b/packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts index 9ee71dea5c..1b31e0c85c 100644 --- a/packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts +++ b/packages/zone.js/lib/browser/webapis-rtc-peer-connection.ts @@ -22,5 +22,5 @@ Zone.__load_patch('RTCPeerConnection', (global: any, Zone: ZoneType, api: _ZoneP RTCPeerConnection.prototype[addSymbol] = null; RTCPeerConnection.prototype[removeSymbol] = null; - api.patchEventTarget(global, api, [RTCPeerConnection.prototype], {useG: false}); + api.patchEventTarget(global, [RTCPeerConnection.prototype], {useG: false}); }); diff --git a/packages/zone.js/lib/browser/websocket.ts b/packages/zone.js/lib/browser/websocket.ts index fecb814966..d11432c821 100644 --- a/packages/zone.js/lib/browser/websocket.ts +++ b/packages/zone.js/lib/browser/websocket.ts @@ -13,7 +13,7 @@ export function apply(api: _ZonePrivate, _global: any) { // On Safari window.EventTarget doesn't exist so need to patch WS add/removeEventListener // On older Chrome, no need since EventTarget was already patched if (!(_global).EventTarget) { - api.patchEventTarget(_global, api, [WS.prototype]); + api.patchEventTarget(_global, [WS.prototype]); } (_global).WebSocket = function(x: any, y: any) { const socket = arguments.length > 1 ? new WS(x, y) : new WS(x); diff --git a/packages/zone.js/lib/common/events.ts b/packages/zone.js/lib/common/events.ts index d96100fb2a..7d70918eca 100644 --- a/packages/zone.js/lib/common/events.ts +++ b/packages/zone.js/lib/common/events.ts @@ -86,7 +86,7 @@ export interface PatchEventTargetOptions { } export function patchEventTarget( - _global: any, api: _ZonePrivate, apis: any[], patchOptions?: PatchEventTargetOptions) { + _global: any, apis: any[], patchOptions?: PatchEventTargetOptions) { const ADD_EVENT_LISTENER = (patchOptions && patchOptions.add) || ADD_EVENT_LISTENER_STR; const REMOVE_EVENT_LISTENER = (patchOptions && patchOptions.rm) || REMOVE_EVENT_LISTENER_STR; @@ -114,15 +114,7 @@ export function patchEventTarget( task.originalDelegate = delegate; } // invoke static task.invoke - // need to try/catch error here, otherwise, the error in one event listener - // will break the executions of the other event listeners. Also error will - // not remove the event listener when `once` options is true. - let error; - try { - task.invoke(task, target, [event]); - } catch (err) { - error = err; - } + task.invoke(task, target, [event]); const options = task.options; if (options && typeof options === 'object' && options.once) { // if options.once is true, after invoke once remove listener here @@ -131,10 +123,10 @@ export function patchEventTarget( const delegate = task.originalDelegate ? task.originalDelegate : task.callback; target[REMOVE_EVENT_LISTENER].call(target, event.type, delegate, options); } - return error; }; - function globalCallback(context: unknown, event: Event, isCapture: boolean) { + // global shared zoneAwareCallback to handle all event callback with capture = false + const globalZoneAwareCallback = function(this: unknown, event: Event) { // https://github.com/angular/zone.js/issues/911, in IE, sometimes // event will be undefined, so we need to use window.event event = event || _global.event; @@ -143,8 +135,8 @@ export function patchEventTarget( } // event.target is needed for Samsung TV and SourceBuffer // || global is needed https://github.com/angular/zone.js/issues/190 - const target: any = context || event.target || _global; - const tasks = target[zoneSymbolEventNames[event.type][isCapture ? TRUE_STR : FALSE_STR]]; + const target: any = this || event.target || _global; + const tasks = target[zoneSymbolEventNames[event.type][FALSE_STR]]; if (tasks) { // invoke all tasks which attached to current target with given event.type and capture = false // for performance concern, if task.length === 1, just invoke @@ -155,32 +147,46 @@ export function patchEventTarget( // copy the tasks array before invoke, to avoid // the callback will remove itself or other listener const copyTasks = tasks.slice(); - const errors = []; for (let i = 0; i < copyTasks.length; i++) { if (event && (event as any)[IMMEDIATE_PROPAGATION_SYMBOL] === true) { break; } - const err = invokeTask(copyTasks[i], target, event); - err && errors.push(err); - } - for (let i = 0; i < errors.length; i++) { - const err = errors[i]; - api.nativeScheduleMicroTask(() => { - throw err; - }); + invokeTask(copyTasks[i], target, event); } } } - } - - // global shared zoneAwareCallback to handle all event callback with capture = false - const globalZoneAwareCallback = function(this: unknown, event: Event) { - return globalCallback(this, event, false); }; // global shared zoneAwareCallback to handle all event callback with capture = true const globalZoneAwareCaptureCallback = function(this: unknown, event: Event) { - return globalCallback(this, event, true); + // https://github.com/angular/zone.js/issues/911, in IE, sometimes + // event will be undefined, so we need to use window.event + event = event || _global.event; + if (!event) { + return; + } + // event.target is needed for Samsung TV and SourceBuffer + // || global is needed https://github.com/angular/zone.js/issues/190 + const target: any = this || event.target || _global; + const tasks = target[zoneSymbolEventNames[event.type][TRUE_STR]]; + if (tasks) { + // invoke all tasks which attached to current target with given event.type and capture = false + // for performance concern, if task.length === 1, just invoke + if (tasks.length === 1) { + invokeTask(tasks[0], target, event); + } else { + // https://github.com/angular/zone.js/issues/836 + // copy the tasks array before invoke, to avoid + // the callback will remove itself or other listener + const copyTasks = tasks.slice(); + for (let i = 0; i < copyTasks.length; i++) { + if (event && (event as any)[IMMEDIATE_PROPAGATION_SYMBOL] === true) { + break; + } + invokeTask(copyTasks[i], target, event); + } + } + } }; function patchEventTargetMethods(obj: any, patchOptions?: PatchEventTargetOptions) { diff --git a/packages/zone.js/lib/extra/socket-io.ts b/packages/zone.js/lib/extra/socket-io.ts index 696065aeab..b3cbdd3104 100644 --- a/packages/zone.js/lib/extra/socket-io.ts +++ b/packages/zone.js/lib/extra/socket-io.ts @@ -8,7 +8,7 @@ Zone.__load_patch('socketio', (global: any, Zone: ZoneType, api: _ZonePrivate) => { (Zone as any)[Zone.__symbol__('socketio')] = function patchSocketIO(io: any) { // patch io.Socket.prototype event listener related method - api.patchEventTarget(global, api, [io.Socket.prototype], { + api.patchEventTarget(global, [io.Socket.prototype], { useG: false, chkDup: false, rt: true, diff --git a/packages/zone.js/lib/node/events.ts b/packages/zone.js/lib/node/events.ts index d2e6832bba..f09f8a4216 100644 --- a/packages/zone.js/lib/node/events.ts +++ b/packages/zone.js/lib/node/events.ts @@ -8,7 +8,7 @@ import {patchEventTarget} from '../common/events'; -Zone.__load_patch('EventEmitter', (global: any, Zone: ZoneType, api: _ZonePrivate) => { +Zone.__load_patch('EventEmitter', (global: any) => { // For EventEmitter const EE_ADD_LISTENER = 'addListener'; const EE_PREPEND_LISTENER = 'prependListener'; @@ -34,7 +34,7 @@ Zone.__load_patch('EventEmitter', (global: any, Zone: ZoneType, api: _ZonePrivat }; function patchEventEmitterMethods(obj: any) { - const result = patchEventTarget(global, api, [obj], { + const result = patchEventTarget(global, [obj], { useG: false, add: EE_ADD_LISTENER, rm: EE_REMOVE_LISTENER, diff --git a/packages/zone.js/lib/zone.ts b/packages/zone.js/lib/zone.ts index 59f23745b8..92bd2f2bbf 100644 --- a/packages/zone.js/lib/zone.ts +++ b/packages/zone.js/lib/zone.ts @@ -337,7 +337,7 @@ interface _ZonePrivate { onUnhandledError: (error: Error) => void; microtaskDrainDone: () => void; showUncaughtError: () => boolean; - patchEventTarget: (global: any, api: _ZonePrivate, apis: any[], options?: any) => boolean[]; + patchEventTarget: (global: any, apis: any[], options?: any) => boolean[]; patchOnProperties: (obj: any, properties: string[]|null, prototype?: any) => void; patchThen: (ctro: Function) => void; patchMethod: @@ -359,7 +359,6 @@ interface _ZonePrivate { filterProperties: (target: any, onProperties: string[], ignoreProperties: any[]) => string[]; attachOriginToPatched: (target: any, origin: any) => void; _redefineProperty: (target: any, callback: string, desc: any) => void; - nativeScheduleMicroTask: (func: Function) => void; patchCallbacks: (api: _ZonePrivate, target: any, targetName: string, method: string, callbacks: string[]) => void; @@ -1344,31 +1343,27 @@ const Zone: ZoneType = (function(global: any) { let _isDrainingMicrotaskQueue: boolean = false; let nativeMicroTaskQueuePromise: any; - function nativeScheduleMicroTask(func: Function) { - if (!nativeMicroTaskQueuePromise) { - if (global[symbolPromise]) { - nativeMicroTaskQueuePromise = global[symbolPromise].resolve(0); - } - } - if (nativeMicroTaskQueuePromise) { - let nativeThen = nativeMicroTaskQueuePromise[symbolThen]; - if (!nativeThen) { - // native Promise is not patchable, we need to use `then` directly - // issue 1078 - nativeThen = nativeMicroTaskQueuePromise['then']; - } - nativeThen.call(nativeMicroTaskQueuePromise, func); - } else { - global[symbolSetTimeout](func, 0); - } - } - function scheduleMicroTask(task?: MicroTask) { // if we are not running in any task, and there has not been anything scheduled // we must bootstrap the initial task creation by manually scheduling the drain if (_numberOfNestedTaskFrames === 0 && _microTaskQueue.length === 0) { // We are not running in Task, so we need to kickstart the microtask queue. - nativeScheduleMicroTask(drainMicroTaskQueue); + if (!nativeMicroTaskQueuePromise) { + if (global[symbolPromise]) { + nativeMicroTaskQueuePromise = global[symbolPromise].resolve(0); + } + } + if (nativeMicroTaskQueuePromise) { + let nativeThen = nativeMicroTaskQueuePromise[symbolThen]; + if (!nativeThen) { + // native Promise is not patchable, we need to use `then` directly + // issue 1078 + nativeThen = nativeMicroTaskQueuePromise['then']; + } + nativeThen.call(nativeMicroTaskQueuePromise, drainMicroTaskQueue); + } else { + global[symbolSetTimeout](drainMicroTaskQueue, 0); + } } task && _microTaskQueue.push(task); } @@ -1433,8 +1428,7 @@ const Zone: ZoneType = (function(global: any) { filterProperties: () => [], attachOriginToPatched: () => noop, _redefineProperty: () => noop, - patchCallbacks: () => noop, - nativeScheduleMicroTask: nativeScheduleMicroTask + patchCallbacks: () => noop }; let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)}; let _currentTask: Task|null = null; diff --git a/packages/zone.js/test/browser/browser.spec.ts b/packages/zone.js/test/browser/browser.spec.ts index 7eea07815f..127bc9a2fb 100644 --- a/packages/zone.js/test/browser/browser.spec.ts +++ b/packages/zone.js/test/browser/browser.spec.ts @@ -348,7 +348,7 @@ describe('Zone', function() { (HTMLSpanElement.prototype as any)[zoneSymbol('addEventListener')] = null; - patchEventTarget(window, null as any, [HTMLSpanElement.prototype]); + patchEventTarget(window, [HTMLSpanElement.prototype]); const span = document.createElement('span'); document.body.appendChild(span); @@ -1037,7 +1037,7 @@ describe('Zone', function() { })); it('should change options to boolean if not support passive', () => { - patchEventTarget(window, null as any, [TestEventListener.prototype]); + patchEventTarget(window, [TestEventListener.prototype]); const testEventListener = new TestEventListener(); const listener = function() {}; @@ -2490,96 +2490,6 @@ describe('Zone', function() { expect(hookSpy).not.toHaveBeenCalled(); expect(logs).toEqual([]); })); - - it('should be able to continue to invoke remaining listeners even some listener throw error', - function(done: DoneFn) { - let logs: string[] = []; - const listener1 = function() { - logs.push('listener1'); - }; - const listener2 = function() { - throw new Error('test1'); - }; - const listener3 = function() { - throw new Error('test2'); - }; - const listener4 = { - handleEvent: function() { - logs.push('listener2'); - } - }; - - button.addEventListener('click', listener1); - button.addEventListener('click', listener2); - button.addEventListener('click', listener3); - button.addEventListener('click', listener4); - - const mouseEvent = document.createEvent('MouseEvent'); - mouseEvent.initEvent('click', true, true); - - const unhandledRejection = (e: PromiseRejectionEvent) => { - logs.push(e.reason.message); - }; - window.addEventListener('unhandledrejection', unhandledRejection); - - button.dispatchEvent(mouseEvent); - expect(logs).toEqual(['listener1', 'listener2']); - - setTimeout(() => { - expect(logs).toEqual(['listener1', 'listener2', 'test1', 'test2']); - window.removeEventListener('unhandledrejection', unhandledRejection); - done() - }); - }); - - it('should be able to continue to invoke remaining listeners even some listener throw error in the different zones', - function(done: DoneFn) { - let logs: string[] = []; - const zone1 = Zone.current.fork({ - name: 'zone1', - onHandleError: (delegate, curr, target, error) => { - logs.push(error.message); - return false; - } - }); - const listener1 = function() { - logs.push('listener1'); - }; - const listener2 = function() { - throw new Error('test1'); - }; - const listener3 = function() { - throw new Error('test2'); - }; - const listener4 = { - handleEvent: function() { - logs.push('listener2'); - } - }; - - button.addEventListener('click', listener1); - zone1.run(() => { - button.addEventListener('click', listener2); - }); - button.addEventListener('click', listener3); - button.addEventListener('click', listener4); - - const mouseEvent = document.createEvent('MouseEvent'); - mouseEvent.initEvent('click', true, true); - - const unhandledRejection = (e: PromiseRejectionEvent) => { - logs.push(e.reason.message); - }; - window.addEventListener('unhandledrejection', unhandledRejection); - - button.dispatchEvent(mouseEvent); - expect(logs).toEqual(['listener1', 'test1', 'listener2']); - - setTimeout(() => { - expect(logs).toEqual(['listener1', 'test1', 'listener2', 'test2']); - done() - }); - }); }); // TODO: Re-enable via https://github.com/angular/angular/pull/41526