From e933cbd26d6549d620d42881fbe876f63bc26bba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mi=C5=A1ko=20Hevery?= Date: Wed, 26 Apr 2017 15:52:07 -0700 Subject: [PATCH] feat: make codebase more modular so that only parts of it can be loaded (#748) --- karma-build.conf.js | 2 + lib/browser/browser.ts | 318 ++++++------ lib/browser/rollup-main.ts | 4 +- lib/browser/websocket.ts | 2 +- lib/common/error-rewrite.ts | 306 +++++++++++ lib/common/promise.ts | 367 ++++++++++++++ lib/common/utils.ts | 20 +- lib/node/node.ts | 26 +- lib/zone.ts | 761 ++++------------------------ scripts/closure/closure_compiler.sh | 8 +- test/browser/browser.spec.ts | 1 + test/common/microtasks.spec.ts | 2 +- test/common/util.spec.ts | 21 - 13 files changed, 959 insertions(+), 879 deletions(-) create mode 100644 lib/common/error-rewrite.ts create mode 100644 lib/common/promise.ts diff --git a/karma-build.conf.js b/karma-build.conf.js index 746826a87..33ab1fea2 100644 --- a/karma-build.conf.js +++ b/karma-build.conf.js @@ -11,5 +11,7 @@ module.exports = function (config) { config.files.push('build/test/wtf_mock.js'); config.files.push('build/test/custom_error.js'); config.files.push('build/lib/zone.js'); + config.files.push('build/lib/common/promise.js'); + config.files.push('build/lib/common/error-rewrite.js'); config.files.push('build/test/main.js'); }; diff --git a/lib/browser/browser.ts b/lib/browser/browser.ts index 7a445b7a9..aca876ebe 100644 --- a/lib/browser/browser.ts +++ b/lib/browser/browser.ts @@ -15,168 +15,184 @@ import {eventTargetPatch} from './event-target'; import {propertyDescriptorPatch} from './property-descriptor'; import {registerElementPatch} from './register-element'; -const set = 'set'; -const clear = 'clear'; -const blockingMethods = ['alert', 'prompt', 'confirm']; -const _global: any = - typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global; - -patchTimer(_global, set, clear, 'Timeout'); -patchTimer(_global, set, clear, 'Interval'); -patchTimer(_global, set, clear, 'Immediate'); -patchTimer(_global, 'request', 'cancel', 'AnimationFrame'); -patchTimer(_global, 'mozRequest', 'mozCancel', 'AnimationFrame'); -patchTimer(_global, 'webkitRequest', 'webkitCancel', 'AnimationFrame'); - -for (let i = 0; i < blockingMethods.length; i++) { - const name = blockingMethods[i]; - patchMethod(_global, name, (delegate, symbol, name) => { - return function(s: any, args: any[]) { - return Zone.current.run(delegate, _global, args, name); - }; - }); -} - -eventTargetPatch(_global); -// patch XMLHttpRequestEventTarget's addEventListener/removeEventListener -const XMLHttpRequestEventTarget = (_global as any)['XMLHttpRequestEventTarget']; -if (XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype) { - patchEventTargetMethods(XMLHttpRequestEventTarget.prototype); -} -propertyDescriptorPatch(_global); -patchClass('MutationObserver'); -patchClass('WebKitMutationObserver'); -patchClass('FileReader'); -propertyPatch(); -registerElementPatch(_global); - -// Treat XMLHTTPRequest as a macrotask. -patchXHR(_global); - -const XHR_TASK = zoneSymbol('xhrTask'); -const XHR_SYNC = zoneSymbol('xhrSync'); -const XHR_LISTENER = zoneSymbol('xhrListener'); -const XHR_SCHEDULED = zoneSymbol('xhrScheduled'); - -interface XHROptions extends TaskData { - target: any; - args: any[]; - aborted: boolean; -} - -function patchXHR(window: any) { - function findPendingTask(target: any) { - const pendingTask: Task = target[XHR_TASK]; - return pendingTask; +Zone.__load_patch('timers', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + const set = 'set'; + const clear = 'clear'; + patchTimer(global, set, clear, 'Timeout'); + patchTimer(global, set, clear, 'Interval'); + patchTimer(global, set, clear, 'Immediate'); + patchTimer(global, 'request', 'cancel', 'AnimationFrame'); + patchTimer(global, 'mozRequest', 'mozCancel', 'AnimationFrame'); + patchTimer(global, 'webkitRequest', 'webkitCancel', 'AnimationFrame'); +}); + +Zone.__load_patch('blocking', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + const blockingMethods = ['alert', 'prompt', 'confirm']; + for (let i = 0; i < blockingMethods.length; i++) { + const name = blockingMethods[i]; + patchMethod(global, name, (delegate, symbol, name) => { + return function(s: any, args: any[]) { + return Zone.current.run(delegate, global, args, name); + }; + }); + } +}); + +Zone.__load_patch('EventTarget', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + eventTargetPatch(global); + // patch XMLHttpRequestEventTarget's addEventListener/removeEventListener + const XMLHttpRequestEventTarget = (global as any)['XMLHttpRequestEventTarget']; + if (XMLHttpRequestEventTarget && XMLHttpRequestEventTarget.prototype) { + patchEventTargetMethods(XMLHttpRequestEventTarget.prototype); + } + patchClass('MutationObserver'); + patchClass('WebKitMutationObserver'); + patchClass('FileReader'); +}); + +Zone.__load_patch('on_property', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + propertyDescriptorPatch(global); + propertyPatch(); + registerElementPatch(global); +}); + +Zone.__load_patch('XHR', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + // Treat XMLHTTPRequest as a macrotask. + patchXHR(global); + + const XHR_TASK = zoneSymbol('xhrTask'); + const XHR_SYNC = zoneSymbol('xhrSync'); + const XHR_LISTENER = zoneSymbol('xhrListener'); + const XHR_SCHEDULED = zoneSymbol('xhrScheduled'); + + interface XHROptions extends TaskData { + target: any; + args: any[]; + aborted: boolean; } - function scheduleTask(task: Task) { - (XMLHttpRequest as any)[XHR_SCHEDULED] = false; - const data = task.data; - // remove existing event listener - const listener = data.target[XHR_LISTENER]; - if (listener) { - data.target.removeEventListener('readystatechange', listener); + function patchXHR(window: any) { + function findPendingTask(target: any) { + const pendingTask: Task = target[XHR_TASK]; + return pendingTask; } - const newListener = data.target[XHR_LISTENER] = () => { - if (data.target.readyState === data.target.DONE) { - // sometimes on some browsers XMLHttpRequest will fire onreadystatechange with - // readyState=4 multiple times, so we need to check task state here - if (!data.aborted && (XMLHttpRequest as any)[XHR_SCHEDULED] && task.state === 'scheduled') { - task.invoke(); - } + + function scheduleTask(task: Task) { + (XMLHttpRequest as any)[XHR_SCHEDULED] = false; + const data = task.data; + // remove existing event listener + const listener = data.target[XHR_LISTENER]; + if (listener) { + data.target.removeEventListener('readystatechange', listener); } - }; - data.target.addEventListener('readystatechange', newListener); + const newListener = data.target[XHR_LISTENER] = () => { + if (data.target.readyState === data.target.DONE) { + // sometimes on some browsers XMLHttpRequest will fire onreadystatechange with + // readyState=4 multiple times, so we need to check task state here + if (!data.aborted && (XMLHttpRequest as any)[XHR_SCHEDULED] && + task.state === 'scheduled') { + task.invoke(); + } + } + }; + data.target.addEventListener('readystatechange', newListener); - const storedTask: Task = data.target[XHR_TASK]; - if (!storedTask) { - data.target[XHR_TASK] = task; + const storedTask: Task = data.target[XHR_TASK]; + if (!storedTask) { + data.target[XHR_TASK] = task; + } + sendNative.apply(data.target, data.args); + (XMLHttpRequest as any)[XHR_SCHEDULED] = true; + return task; } - sendNative.apply(data.target, data.args); - (XMLHttpRequest as any)[XHR_SCHEDULED] = true; - return task; - } - function placeholderCallback() {} + function placeholderCallback() {} - function clearTask(task: Task) { - const data = task.data; - // Note - ideally, we would call data.target.removeEventListener here, but it's too late - // to prevent it from firing. So instead, we store info for the event listener. - data.aborted = true; - return abortNative.apply(data.target, data.args); - } - - const openNative: Function = - patchMethod(window.XMLHttpRequest.prototype, 'open', () => function(self: any, args: any[]) { - self[XHR_SYNC] = args[2] == false; - return openNative.apply(self, args); - }); - - const sendNative: Function = - patchMethod(window.XMLHttpRequest.prototype, 'send', () => function(self: any, args: any[]) { - const zone = Zone.current; - if (self[XHR_SYNC]) { - // if the XHR is sync there is no task to schedule, just execute the code. - return sendNative.apply(self, args); - } else { - const options: XHROptions = - {target: self, isPeriodic: false, delay: null, args: args, aborted: false}; - return zone.scheduleMacroTask( - 'XMLHttpRequest.send', placeholderCallback, options, scheduleTask, clearTask); - } - }); + function clearTask(task: Task) { + const data = task.data; + // Note - ideally, we would call data.target.removeEventListener here, but it's too late + // to prevent it from firing. So instead, we store info for the event listener. + data.aborted = true; + return abortNative.apply(data.target, data.args); + } - const abortNative = patchMethod( - window.XMLHttpRequest.prototype, 'abort', - (delegate: Function) => function(self: any, args: any[]) { - const task: Task = findPendingTask(self); - if (task && typeof task.type == 'string') { - // If the XHR has already completed, do nothing. - // If the XHR has already been aborted, do nothing. - // Fix #569, call abort multiple times before done will cause - // macroTask task count be negative number - if (task.cancelFn == null || (task.data && (task.data).aborted)) { - return; + const openNative: Function = patchMethod( + window.XMLHttpRequest.prototype, 'open', () => function(self: any, args: any[]) { + self[XHR_SYNC] = args[2] == false; + return openNative.apply(self, args); + }); + + const sendNative: Function = patchMethod( + window.XMLHttpRequest.prototype, 'send', () => function(self: any, args: any[]) { + const zone = Zone.current; + if (self[XHR_SYNC]) { + // if the XHR is sync there is no task to schedule, just execute the code. + return sendNative.apply(self, args); + } else { + const options: XHROptions = + {target: self, isPeriodic: false, delay: null, args: args, aborted: false}; + return zone.scheduleMacroTask( + 'XMLHttpRequest.send', placeholderCallback, options, scheduleTask, clearTask); } - task.zone.cancelTask(task); + }); + + const abortNative = patchMethod( + window.XMLHttpRequest.prototype, 'abort', + (delegate: Function) => function(self: any, args: any[]) { + const task: Task = findPendingTask(self); + if (task && typeof task.type == 'string') { + // If the XHR has already completed, do nothing. + // If the XHR has already been aborted, do nothing. + // Fix #569, call abort multiple times before done will cause + // macroTask task count be negative number + if (task.cancelFn == null || (task.data && (task.data).aborted)) { + return; + } + task.zone.cancelTask(task); + } + // Otherwise, we are trying to abort an XHR which has not yet been sent, so there is no + // task + // to cancel. Do nothing. + }); + } +}); + +Zone.__load_patch('geo', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + /// GEO_LOCATION + if (global['navigator'] && global['navigator'].geolocation) { + patchPrototype(global['navigator'].geolocation, ['getCurrentPosition', 'watchPosition']); + } +}); + +Zone.__load_patch('toString', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + // patch Func.prototype.toString to let them look like native + patchFuncToString(); + // patch Object.prototype.toString to let them look like native + patchObjectToString(); +}); + +Zone.__load_patch('promiseRejectionHandler', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + // handle unhandled promise rejection + function findPromiseRejectionHandler(evtName: string) { + return function(e: any) { + const eventTasks = findEventTask(global, evtName); + eventTasks.forEach(eventTask => { + // windows has added unhandledrejection event listener + // trigger the event listener + const PromiseRejectionEvent = global['PromiseRejectionEvent']; + if (PromiseRejectionEvent) { + const evt = new PromiseRejectionEvent(evtName, {promise: e.promise, reason: e.rejection}); + eventTask.invoke(evt); } - // Otherwise, we are trying to abort an XHR which has not yet been sent, so there is no task - // to cancel. Do nothing. }); -} - -/// GEO_LOCATION -if (_global['navigator'] && _global['navigator'].geolocation) { - patchPrototype(_global['navigator'].geolocation, ['getCurrentPosition', 'watchPosition']); -} - -// patch Func.prototype.toString to let them look like native -patchFuncToString(); -// patch Object.prototype.toString to let them look like native -patchObjectToString(); - -// handle unhandled promise rejection -function findPromiseRejectionHandler(evtName: string) { - return function(e: any) { - const eventTasks = findEventTask(_global, evtName); - eventTasks.forEach(eventTask => { - // windows has added unhandledrejection event listener - // trigger the event listener - const PromiseRejectionEvent = _global['PromiseRejectionEvent']; - if (PromiseRejectionEvent) { - const evt = new PromiseRejectionEvent(evtName, {promise: e.promise, reason: e.rejection}); - eventTask.invoke(evt); - } - }); - }; -} + }; + } -if (_global['PromiseRejectionEvent']) { - (Zone as any)[zoneSymbol('unhandledPromiseRejectionHandler')] = - findPromiseRejectionHandler('unhandledrejection'); + if (global['PromiseRejectionEvent']) { + (Zone as any)[zoneSymbol('unhandledPromiseRejectionHandler')] = + findPromiseRejectionHandler('unhandledrejection'); - (Zone as any)[zoneSymbol('rejectionHandledHandler')] = - findPromiseRejectionHandler('rejectionhandled'); -} \ No newline at end of file + (Zone as any)[zoneSymbol('rejectionHandledHandler')] = + findPromiseRejectionHandler('rejectionhandled'); + } +}); diff --git a/lib/browser/rollup-main.ts b/lib/browser/rollup-main.ts index 2eb7d99a4..8832d80ac 100644 --- a/lib/browser/rollup-main.ts +++ b/lib/browser/rollup-main.ts @@ -8,4 +8,6 @@ import '../zone'; -import './browser'; \ No newline at end of file +import '../common/promise'; +import '../common/error-rewrite'; +import './browser'; diff --git a/lib/browser/websocket.ts b/lib/browser/websocket.ts index aaac869ef..5b8d4f64b 100644 --- a/lib/browser/websocket.ts +++ b/lib/browser/websocket.ts @@ -39,6 +39,6 @@ export function apply(_global: any) { return proxySocket; }; for (const prop in WS) { - _global.WebSocket[prop] = WS[prop]; + _global['WebSocket'][prop] = WS[prop]; } } diff --git a/lib/common/error-rewrite.ts b/lib/common/error-rewrite.ts new file mode 100644 index 000000000..f1a4437f8 --- /dev/null +++ b/lib/common/error-rewrite.ts @@ -0,0 +1,306 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +/** + * Extend the Error with additional fields for rewritten stack frames + */ +interface Error { + /** + * Stack trace where extra frames have been removed and zone names added. + */ + zoneAwareStack?: string; + + /** + * Original stack trace with no modifications + */ + originalStack?: string; +} + +Zone.__load_patch('Error', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + /* + * This code patches Error so that: + * - It ignores un-needed stack frames. + * - It Shows the associated Zone for reach frame. + */ + + const enum FrameType { + /// Skip this frame when printing out stack + blackList, + /// This frame marks zone transition + transition + } + + const blacklistedStackFramesSymbol = api.symbol('blacklistedStackFrames'); + const NativeError = global[api.symbol('Error')] = global['Error']; + // Store the frames which should be removed from the stack frames + const blackListedStackFrames: {[frame: string]: FrameType} = {}; + // We must find the frame where Error was created, otherwise we assume we don't understand stack + let zoneAwareFrame1: string; + let zoneAwareFrame2: string; + + global['Error'] = ZoneAwareError; + const stackRewrite = 'stackRewrite'; + + /** + * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as + * adds zone information to it. + */ + function ZoneAwareError(): Error { + // We always have to return native error otherwise the browser console will not work. + let error: Error = NativeError.apply(this, arguments); + // Save original stack trace + const originalStack = (error as any)['originalStack'] = error.stack; + + // Process the stack trace and rewrite the frames. + if ((ZoneAwareError as any)[stackRewrite] && originalStack) { + let frames: string[] = originalStack.split('\n'); + let zoneFrame = api.currentZoneFrame(); + let i = 0; + // Find the first frame + while (!(frames[i] === zoneAwareFrame1 || frames[i] === zoneAwareFrame2) && + i < frames.length) { + i++; + } + for (; i < frames.length && zoneFrame; i++) { + let frame = frames[i]; + if (frame.trim()) { + switch (blackListedStackFrames[frame]) { + case FrameType.blackList: + frames.splice(i, 1); + i--; + break; + case FrameType.transition: + if (zoneFrame.parent) { + // This is the special frame where zone changed. Print and process it accordingly + zoneFrame = zoneFrame.parent; + } else { + zoneFrame = null; + } + frames.splice(i, 1); + i--; + break; + default: + frames[i] += ` [${zoneFrame.zone.name}]`; + } + } + } + try { + error.stack = error.zoneAwareStack = frames.join('\n'); + } catch (e) { + // ignore as some browsers don't allow overriding of stack + } + } + + if (this instanceof NativeError && this.constructor != NativeError) { + // We got called with a `new` operator AND we are subclass of ZoneAwareError + // in that case we have to copy all of our properties to `this`. + Object.keys(error).concat('stack', 'message').forEach((key) => { + if ((error as any)[key] !== undefined) { + try { + this[key] = (error as any)[key]; + } catch (e) { + // ignore the assignment in case it is a setter and it throws. + } + } + }); + return this; + } + return error; + } + + // Copy the prototype so that instanceof operator works as expected + ZoneAwareError.prototype = NativeError.prototype; + (ZoneAwareError as any)[blacklistedStackFramesSymbol] = blackListedStackFrames; + (ZoneAwareError as any)[stackRewrite] = false; + + // those properties need special handling + const specialPropertyNames = ['stackTraceLimit', 'captureStackTrace', 'prepareStackTrace']; + // those properties of NativeError should be set to ZoneAwareError + const nativeErrorProperties = Object.keys(NativeError); + if (nativeErrorProperties) { + nativeErrorProperties.forEach(prop => { + if (specialPropertyNames.filter(sp => sp === prop).length === 0) { + Object.defineProperty(ZoneAwareError, prop, { + get: function() { + return NativeError[prop]; + }, + set: function(value) { + NativeError[prop] = value; + } + }); + } + }); + } + + if (NativeError.hasOwnProperty('stackTraceLimit')) { + // Extend default stack limit as we will be removing few frames. + NativeError.stackTraceLimit = Math.max(NativeError.stackTraceLimit, 15); + + // make sure that ZoneAwareError has the same property which forwards to NativeError. + Object.defineProperty(ZoneAwareError, 'stackTraceLimit', { + get: function() { + return NativeError.stackTraceLimit; + }, + set: function(value) { + return NativeError.stackTraceLimit = value; + } + }); + } + + if (NativeError.hasOwnProperty('captureStackTrace')) { + Object.defineProperty(ZoneAwareError, 'captureStackTrace', { + // add named function here because we need to remove this + // stack frame when prepareStackTrace below + value: function zoneCaptureStackTrace(targetObject: Object, constructorOpt?: Function) { + NativeError.captureStackTrace(targetObject, constructorOpt); + } + }); + } + + Object.defineProperty(ZoneAwareError, 'prepareStackTrace', { + get: function() { + return NativeError.prepareStackTrace; + }, + set: function(value) { + if (!value || typeof value !== 'function') { + return NativeError.prepareStackTrace = value; + } + return NativeError.prepareStackTrace = function( + error: Error, structuredStackTrace: {getFunctionName: Function}[]) { + // remove additional stack information from ZoneAwareError.captureStackTrace + if (structuredStackTrace) { + for (let i = 0; i < structuredStackTrace.length; i++) { + const st = structuredStackTrace[i]; + // remove the first function which name is zoneCaptureStackTrace + if (st.getFunctionName() === 'zoneCaptureStackTrace') { + structuredStackTrace.splice(i, 1); + break; + } + } + } + return value.apply(this, [error, structuredStackTrace]); + }; + } + }); + + // Now we need to populate the `blacklistedStackFrames` as well as find the + // run/runGuarded/runTask frames. This is done by creating a detect zone and then threading + // the execution through all of the above methods so that we can look at the stack trace and + // find the frames of interest. + let detectZone: Zone = Zone.current.fork({ + name: 'detect', + onHandleError: function(parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): + boolean { + if (error.originalStack && Error === ZoneAwareError) { + let frames = error.originalStack.split(/\n/); + let runFrame = false, runGuardedFrame = false, runTaskFrame = false; + while (frames.length) { + let frame = frames.shift(); + // On safari it is possible to have stack frame with no line number. + // This check makes sure that we don't filter frames on name only (must have + // linenumber) + if (/:\d+:\d+/.test(frame)) { + // Get rid of the path so that we don't accidentally find function name in path. + // In chrome the separator is `(` and `@` in FF and safari + // Chrome: at Zone.run (zone.js:100) + // Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24) + // FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24 + // Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24 + let fnName: string = frame.split('(')[0].split('@')[0]; + let frameType = FrameType.transition; + if (fnName.indexOf('ZoneAwareError') !== -1) { + zoneAwareFrame1 = frame; + zoneAwareFrame2 = frame.replace('Error.', ''); + blackListedStackFrames[zoneAwareFrame2] = FrameType.blackList; + } + if (fnName.indexOf('runGuarded') !== -1) { + runGuardedFrame = true; + } else if (fnName.indexOf('runTask') !== -1) { + runTaskFrame = true; + } else if (fnName.indexOf('run') !== -1) { + runFrame = true; + } else { + frameType = FrameType.blackList; + } + blackListedStackFrames[frame] = frameType; + // Once we find all of the frames we can stop looking. + if (runFrame && runGuardedFrame && runTaskFrame) { + (ZoneAwareError as any)[stackRewrite] = true; + break; + } + } + } + } + return false; + } + }) as Zone; + // carefully constructor a stack frame which contains all of the frames of interest which + // need to be detected and blacklisted. + + const childDetectZone = detectZone.fork({ + name: 'child', + onScheduleTask: function(delegate, curr, target, task) { + return delegate.scheduleTask(target, task); + }, + onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) { + return delegate.invokeTask(target, task, applyThis, applyArgs); + }, + onCancelTask: function(delegate, curr, target, task) { + return delegate.cancelTask(target, task); + }, + onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs, source) { + return delegate.invoke(target, callback, applyThis, applyArgs, source); + } + }); + + // we need to detect all zone related frames, it will + // exceed default stackTraceLimit, so we set it to + // larger number here, and restore it after detect finish. + const originalStackTraceLimit = Error.stackTraceLimit; + Error.stackTraceLimit = 100; + // we schedule event/micro/macro task, and invoke them + // when onSchedule, so we can get all stack traces for + // all kinds of tasks with one error thrown. + childDetectZone.run(() => { + childDetectZone.runGuarded(() => { + const fakeTransitionTo = + (toState: TaskState, fromState1: TaskState, fromState2: TaskState) => {}; + childDetectZone.scheduleEventTask( + blacklistedStackFramesSymbol, + () => { + childDetectZone.scheduleMacroTask( + blacklistedStackFramesSymbol, + () => { + childDetectZone.scheduleMicroTask( + blacklistedStackFramesSymbol, + () => { + throw new (ZoneAwareError as any)(ZoneAwareError, NativeError); + }, + null, + (t: Task) => { + (t as any)._transitionTo = fakeTransitionTo; + t.invoke(); + }); + }, + null, + (t) => { + (t as any)._transitionTo = fakeTransitionTo; + t.invoke(); + }, + () => {}); + }, + null, + (t) => { + (t as any)._transitionTo = fakeTransitionTo; + t.invoke(); + }, + () => {}); + }); + }); + Error.stackTraceLimit = originalStackTraceLimit; +}); diff --git a/lib/common/promise.ts b/lib/common/promise.ts new file mode 100644 index 000000000..feff99b10 --- /dev/null +++ b/lib/common/promise.ts @@ -0,0 +1,367 @@ +/** + * @license + * Copyright Google Inc. All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ +Zone.__load_patch('ZoneAwarePromise', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + interface UncaughtPromiseError extends Error { + zone: AmbientZone; + task: Task; + promise: ZoneAwarePromise; + rejection: any; + } + + const __symbol__ = api.symbol; + const _uncaughtPromiseErrors: UncaughtPromiseError[] = []; + const symbolPromise = __symbol__('Promise'); + const symbolThen = __symbol__('then'); + + api.onUnhandledError = (showError: boolean, e: any) => { + if (showError) { + const rejection = e && e.rejection; + if (rejection) { + console.error( + 'Unhandled Promise rejection:', + rejection instanceof Error ? rejection.message : rejection, '; Zone:', + (e.zone).name, '; Task:', e.task && (e.task).source, '; Value:', rejection, + rejection instanceof Error ? rejection.stack : undefined); + } + console.error(e); + } + }; + + api.microtaskDrainDone = (showError: boolean) => { + while (_uncaughtPromiseErrors.length) { + while (_uncaughtPromiseErrors.length) { + const uncaughtPromiseError: UncaughtPromiseError = _uncaughtPromiseErrors.shift(); + try { + uncaughtPromiseError.zone.runGuarded(() => { + throw uncaughtPromiseError; + }); + } catch (error) { + handleUnhandledRejection(showError, error); + } + } + } + }; + + function handleUnhandledRejection(ignoreError: boolean, e: any) { + api.onUnhandledError(ignoreError, e); + try { + const handler = (Zone as any)[__symbol__('unhandledPromiseRejectionHandler')]; + if (handler && typeof handler === 'function') { + handler.apply(this, [e]); + } + } catch (err) { + } + } + + function isThenable(value: any): boolean { + return value && value.then; + } + + function forwardResolution(value: any): any { + return value; + } + + function forwardRejection(rejection: any): any { + return ZoneAwarePromise.reject(rejection); + } + + const symbolState: string = __symbol__('state'); + const symbolValue: string = __symbol__('value'); + const source: string = 'Promise.then'; + const UNRESOLVED: null = null; + const RESOLVED = true; + const REJECTED = false; + const REJECTED_NO_CATCH = 0; + + function makeResolver(promise: ZoneAwarePromise, state: boolean): (value: any) => void { + return (v) => { + try { + resolvePromise(promise, state, v); + } catch (err) { + resolvePromise(promise, false, err); + } + // Do not return value or you will break the Promise spec. + }; + } + + const once = function() { + let wasCalled = false; + + return function wrapper(wrappedFunction: Function) { + return function() { + if (wasCalled) { + return; + } + wasCalled = true; + wrappedFunction.apply(null, arguments); + }; + }; + }; + + // Promise Resolution + function resolvePromise( + promise: ZoneAwarePromise, state: boolean, value: any): ZoneAwarePromise { + const onceWrapper = once(); + if (promise === value) { + throw new TypeError('Promise resolved with itself'); + } + if ((promise as any)[symbolState] === UNRESOLVED) { + // should only get value.then once based on promise spec. + let then: any = null; + try { + if (typeof value === 'object' || typeof value === 'function') { + then = value && value.then; + } + } catch (err) { + onceWrapper(() => { + resolvePromise(promise, false, err); + })(); + return promise; + } + // if (value instanceof ZoneAwarePromise) { + if (state !== REJECTED && value instanceof ZoneAwarePromise && + value.hasOwnProperty(symbolState) && value.hasOwnProperty(symbolValue) && + (value as any)[symbolState] !== UNRESOLVED) { + clearRejectedNoCatch(>value); + resolvePromise(promise, (value as any)[symbolState], (value as any)[symbolValue]); + } else if (state !== REJECTED && typeof then === 'function') { + try { + then.apply(value, [ + onceWrapper(makeResolver(promise, state)), onceWrapper(makeResolver(promise, false)) + ]); + } catch (err) { + onceWrapper(() => { + resolvePromise(promise, false, err); + })(); + } + } else { + (promise as any)[symbolState] = state; + const queue = (promise as any)[symbolValue]; + (promise as any)[symbolValue] = value; + + // record task information in value when error occurs, so we can + // do some additional work such as render longStackTrace + if (state === REJECTED && value instanceof Error) { + (value as any)[__symbol__('currentTask')] = Zone.currentTask; + } + + for (let i = 0; i < queue.length;) { + scheduleResolveOrReject(promise, queue[i++], queue[i++], queue[i++], queue[i++]); + } + if (queue.length == 0 && state == REJECTED) { + (promise as any)[symbolState] = REJECTED_NO_CATCH; + try { + throw new Error( + 'Uncaught (in promise): ' + value + + (value && value.stack ? '\n' + value.stack : '')); + } catch (err) { + const error: UncaughtPromiseError = err; + error.rejection = value; + error.promise = promise; + error.zone = Zone.current; + error.task = Zone.currentTask; + _uncaughtPromiseErrors.push(error); + api.scheduleMicroTask(); // to make sure that it is running + } + } + } + } + // Resolving an already resolved promise is a noop. + return promise; + } + + function clearRejectedNoCatch(promise: ZoneAwarePromise): void { + if ((promise as any)[symbolState] === REJECTED_NO_CATCH) { + // if the promise is rejected no catch status + // and queue.length > 0, means there is a error handler + // here to handle the rejected promise, we should trigger + // windows.rejectionhandled eventHandler or nodejs rejectionHandled + // eventHandler + try { + const handler = (Zone as any)[__symbol__('rejectionHandledHandler')]; + if (handler && typeof handler === 'function') { + handler.apply(this, [{rejection: (promise as any)[symbolValue], promise: promise}]); + } + } catch (err) { + } + (promise as any)[symbolState] = REJECTED; + for (let i = 0; i < _uncaughtPromiseErrors.length; i++) { + if (promise === _uncaughtPromiseErrors[i].promise) { + _uncaughtPromiseErrors.splice(i, 1); + } + } + } + } + + function scheduleResolveOrReject( + promise: ZoneAwarePromise, zone: AmbientZone, chainPromise: ZoneAwarePromise, + onFulfilled?: (value: R) => U, onRejected?: (error: any) => U): void { + clearRejectedNoCatch(promise); + const delegate = (promise as any)[symbolState] ? + (typeof onFulfilled === 'function') ? onFulfilled : forwardResolution : + (typeof onRejected === 'function') ? onRejected : forwardRejection; + zone.scheduleMicroTask(source, () => { + try { + resolvePromise( + chainPromise, true, zone.run(delegate, undefined, [(promise as any)[symbolValue]])); + } catch (error) { + resolvePromise(chainPromise, false, error); + } + }); + } + + class ZoneAwarePromise implements Promise { + static toString() { + return 'function ZoneAwarePromise() { [native code] }'; + } + + static resolve(value: R): Promise { + return resolvePromise(>new this(null), RESOLVED, value); + } + + static reject(error: U): Promise { + return resolvePromise(>new this(null), REJECTED, error); + } + + static race(values: PromiseLike[]): Promise { + let resolve: (v: any) => void; + let reject: (v: any) => void; + let promise: any = new this((res, rej) => { + [resolve, reject] = [res, rej]; + }); + function onResolve(value: any) { + promise && (promise = null || resolve(value)); + } + function onReject(error: any) { + promise && (promise = null || reject(error)); + } + + for (let value of values) { + if (!isThenable(value)) { + value = this.resolve(value); + } + value.then(onResolve, onReject); + } + return promise; + } + + static all(values: any): Promise { + let resolve: (v: any) => void; + let reject: (v: any) => void; + let promise = new this((res, rej) => { + resolve = res; + reject = rej; + }); + let count = 0; + const resolvedValues: any[] = []; + for (let value of values) { + if (!isThenable(value)) { + value = this.resolve(value); + } + value.then( + ((index) => (value: any) => { + resolvedValues[index] = value; + count--; + if (!count) { + resolve(resolvedValues); + } + })(count), + reject); + count++; + } + if (!count) resolve(resolvedValues); + return promise; + } + + constructor( + executor: + (resolve: (value?: R|PromiseLike) => void, reject: (error?: any) => void) => void) { + const promise: ZoneAwarePromise = this; + if (!(promise instanceof ZoneAwarePromise)) { + throw new Error('Must be an instanceof Promise.'); + } + (promise as any)[symbolState] = UNRESOLVED; + (promise as any)[symbolValue] = []; // queue; + try { + executor && executor(makeResolver(promise, RESOLVED), makeResolver(promise, REJECTED)); + } catch (error) { + resolvePromise(promise, false, error); + } + } + + then( + onFulfilled?: (value: R) => U | PromiseLike, + onRejected?: (error: any) => U | PromiseLike): Promise { + const chainPromise: Promise = new (this.constructor as typeof ZoneAwarePromise)(null); + const zone = Zone.current; + if ((this as any)[symbolState] == UNRESOLVED) { + ((this as any)[symbolValue]).push(zone, chainPromise, onFulfilled, onRejected); + } else { + scheduleResolveOrReject(this, zone, chainPromise, onFulfilled, onRejected); + } + return chainPromise; + } + + catch(onRejected?: (error: any) => U | PromiseLike): Promise { + return this.then(null, onRejected); + } + } + // Protect against aggressive optimizers dropping seemingly unused properties. + // E.g. Closure Compiler in advanced mode. + ZoneAwarePromise['resolve'] = ZoneAwarePromise.resolve; + ZoneAwarePromise['reject'] = ZoneAwarePromise.reject; + ZoneAwarePromise['race'] = ZoneAwarePromise.race; + ZoneAwarePromise['all'] = ZoneAwarePromise.all; + + const NativePromise = global[symbolPromise] = global['Promise']; + global['Promise'] = ZoneAwarePromise; + + const symbolThenPatched = __symbol__('thenPatched'); + + function patchThen(Ctor: Function) { + const proto = Ctor.prototype; + const originalThen = proto.then; + // Keep a reference to the original method. + proto[symbolThen] = originalThen; + + Ctor.prototype.then = function(onResolve: any, onReject: any) { + const wrapped = new ZoneAwarePromise((resolve, reject) => { + originalThen.call(this, resolve, reject); + }); + return wrapped.then(onResolve, onReject); + }; + (Ctor as any)[symbolThenPatched] = true; + } + + function zoneify(fn: Function) { + return function() { + let resultPromise = fn.apply(this, arguments); + if (resultPromise instanceof ZoneAwarePromise) { + return resultPromise; + } + let ctor = resultPromise.constructor; + if (!ctor[symbolThenPatched]) { + patchThen(ctor); + } + return resultPromise; + }; + } + + if (NativePromise) { + patchThen(NativePromise); + + let fetch = global['fetch']; + if (typeof fetch == 'function') { + global['fetch'] = zoneify(fetch); + } + } + + // This is not part of public API, but it is useful for tests, so we expose it. + (Promise as any)[Zone.__symbol__('uncaughtPromiseErrors')] = _uncaughtPromiseErrors; + return ZoneAwarePromise; +}); diff --git a/lib/common/utils.ts b/lib/common/utils.ts index 499e11294..b1bc53a26 100644 --- a/lib/common/utils.ts +++ b/lib/common/utils.ts @@ -133,7 +133,7 @@ export function patchProperty(obj: any, prop: string) { // the onclick will be evaluated when first time event was triggered or // the property is accessed, https://github.com/angular/zone.js/issues/525 // so we should use original native get to retrieve the handler - let value = originalDescGet.apply(this); + let value = originalDescGet && originalDescGet.apply(this); if (value) { desc.set.apply(this, [value]); if (typeof target['removeAttribute'] === 'function') { @@ -553,23 +553,12 @@ export function patchClass(className: string) { } } -export function createNamedFn(name: string, delegate: (self: any, args: any[]) => any): Function { - try { - return (Function('f', `return function ${name}(){return f(this, arguments)}`))(delegate); - } catch (error) { - // if we fail, we must be CSP, just return delegate. - return function() { - return delegate(this, arguments); - }; - } -} - export function patchMethod( target: any, name: string, patchFn: (delegate: Function, delegateName: string, name: string) => (self: any, args: any[]) => any): Function { let proto = target; - while (proto && Object.getOwnPropertyNames(proto).indexOf(name) === -1) { + while (proto && !proto.hasOwnProperty(name)) { proto = Object.getPrototypeOf(proto); } if (!proto && target[name]) { @@ -580,7 +569,10 @@ export function patchMethod( let delegate: Function; if (proto && !(delegate = proto[delegateName])) { delegate = proto[delegateName] = proto[name]; - proto[name] = createNamedFn(name, patchFn(delegate, delegateName, name)); + const patchDelegate = patchFn(delegate, delegateName, name); + proto[name] = function() { + return patchDelegate(this, arguments as any); + }; attachOriginToPatched(proto[name], delegate); } return delegate; diff --git a/lib/node/node.ts b/lib/node/node.ts index 24a78ecf6..da7c4569f 100644 --- a/lib/node/node.ts +++ b/lib/node/node.ts @@ -7,6 +7,8 @@ */ import '../zone'; +import '../common/promise'; +import '../common/error-rewrite'; import './events'; import './fs'; @@ -18,19 +20,21 @@ const set = 'set'; const clear = 'clear'; const _global = typeof window === 'object' && window || typeof self === 'object' && self || global; -// Timers -const timers = require('timers'); -patchTimer(timers, set, clear, 'Timeout'); -patchTimer(timers, set, clear, 'Interval'); -patchTimer(timers, set, clear, 'Immediate'); +Zone.__load_patch('timers', (global: any, Zone: ZoneType, api: _ZonePrivate) => { + // Timers + const timers = require('timers'); + patchTimer(timers, set, clear, 'Timeout'); + patchTimer(timers, set, clear, 'Interval'); + patchTimer(timers, set, clear, 'Immediate'); -const shouldPatchGlobalTimers = global.setTimeout !== timers.setTimeout; + const shouldPatchGlobalTimers = global['setTimeout'] !== timers.setTimeout; -if (shouldPatchGlobalTimers) { - patchTimer(_global, set, clear, 'Timeout'); - patchTimer(_global, set, clear, 'Interval'); - patchTimer(_global, set, clear, 'Immediate'); -} + if (shouldPatchGlobalTimers) { + patchTimer(_global, set, clear, 'Timeout'); + patchTimer(_global, set, clear, 'Interval'); + patchTimer(_global, set, clear, 'Immediate'); + } +}); // patch process related methods patchProcess(); diff --git a/lib/zone.ts b/lib/zone.ts index 0454e71cb..32ab0f8e3 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -301,6 +301,30 @@ interface ZoneType { * Return the root zone. */ root: Zone; + + /** @internal */ + __load_patch(name: string, fn: _PatchFn): void; + + /** @internal */ + __symbol__(name: string): string; +} + +/** @internal */ +type _PatchFn = (global: Window, Zone: ZoneType, api: _ZonePrivate) => void; + +/** @internal */ +interface _ZonePrivate { + currentZoneFrame(): _ZoneFrame; + symbol(name: string): string; + scheduleMicroTask(task?: MicroTask): void; + onUnhandledError: (showError: boolean, error: Error) => void; + microtaskDrainDone: (showError: boolean) => void; +} + +/** @internal */ +interface _ZoneFrame { + parent: _ZoneFrame; + zone: Zone; } /** @@ -587,43 +611,30 @@ interface EventTask extends Task { type: 'eventTask'; } -/** - * Extend the Error with additional fields for rewritten stack frames - */ -interface Error { - /** - * Stack trace where extra frames have been removed and zone names added. - */ - zoneAwareStack?: string; - - /** - * Original stack trace with no modifications - */ - originalStack?: string; -} - /** @internal */ type AmbientZone = Zone; /** @internal */ type AmbientZoneDelegate = ZoneDelegate; const Zone: ZoneType = (function(global: any) { + const performance: {mark(name: string): void; measure(name: string, label: string): void;} = + global['performance']; + function mark(name: string) { + performance && performance['mark'] && performance['mark'](name); + } + function performanceMeasure(name: string, label: string) { + performance && performance['measure'] && performance['measure'](name, label); + } + mark('Zone'); if (global['Zone']) { throw new Error('Zone already loaded.'); } - const NO_ZONE = {name: 'NO ZONE'}; - const notScheduled: 'notScheduled' = 'notScheduled', scheduling: 'scheduling' = 'scheduling', - scheduled: 'scheduled' = 'scheduled', running: 'running' = 'running', - canceling: 'canceling' = 'canceling', unknown: 'unknown' = 'unknown'; - const microTask: 'microTask' = 'microTask', macroTask: 'macroTask' = 'macroTask', - eventTask: 'eventTask' = 'eventTask'; - class Zone implements AmbientZone { static __symbol__: (name: string) => string = __symbol__; static assertZonePatched() { - if (global.Promise !== ZoneAwarePromise) { + if (global['Promise'] !== patches['ZoneAwarePromise']) { throw new Error( 'Zone.js has detected that ZoneAwarePromise `(window|global).Promise` ' + 'has been overwritten.\n' + @@ -648,6 +659,17 @@ const Zone: ZoneType = (function(global: any) { return _currentTask; }; + static __load_patch(name: string, fn: _PatchFn): void { + if (patches.hasOwnProperty(name)) { + throw Error('Already loaded patch: ' + name); + } else { + const perfName = 'Zone:' + name; + mark(perfName); + patches[name] = fn(global, Zone, _api); + performanceMeasure(perfName, perfName); + } + } + public get parent(): AmbientZone { return this._parent; }; @@ -705,7 +727,7 @@ const Zone: ZoneType = (function(global: any) { public run( callback: (...args: any[]) => T, applyThis: any = undefined, applyArgs: any[] = null, source: string = null): T { - _currentZoneFrame = new ZoneFrame(_currentZoneFrame, this); + _currentZoneFrame = {parent: _currentZoneFrame, zone: this}; try { return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source); } finally { @@ -717,7 +739,7 @@ const Zone: ZoneType = (function(global: any) { public runGuarded( callback: (...args: any[]) => T, applyThis: any = null, applyArgs: any[] = null, source: string = null) { - _currentZoneFrame = new ZoneFrame(_currentZoneFrame, this); + _currentZoneFrame = {parent: _currentZoneFrame, zone: this}; try { try { return this._zoneDelegate.invoke(this, callback, applyThis, applyArgs, source); @@ -742,7 +764,7 @@ const Zone: ZoneType = (function(global: any) { task.runCount++; const previousTask = _currentTask; _currentTask = task; - _currentZoneFrame = new ZoneFrame(_currentZoneFrame, this); + _currentZoneFrame = {parent: _currentZoneFrame, zone: this}; try { if (task.type == macroTask && task.data && !task.data.isPeriodic) { task.cancelFn = null; @@ -1097,7 +1119,6 @@ const Zone: ZoneType = (function(global: any) { } } - class ZoneTask implements Task { public type: T; public source: string; @@ -1188,37 +1209,18 @@ const Zone: ZoneType = (function(global: any) { } } - interface UncaughtPromiseError extends Error { - zone: AmbientZone; - task: Task; - promise: ZoneAwarePromise; - rejection: any; - } - - class ZoneFrame { - public parent: ZoneFrame; - public zone: Zone; - constructor(parent: ZoneFrame, zone: Zone) { - this.parent = parent; - this.zone = zone; - } - } - - function __symbol__(name: string) { - return '__zone_symbol__' + name; - } + ////////////////////////////////////////////////////// + ////////////////////////////////////////////////////// + /// MICROTASK QUEUE + ////////////////////////////////////////////////////// + ////////////////////////////////////////////////////// const symbolSetTimeout = __symbol__('setTimeout'); const symbolPromise = __symbol__('Promise'); const symbolThen = __symbol__('then'); - - let _currentZoneFrame = new ZoneFrame(null, new Zone(null, null)); - let _currentTask: Task = null; let _microTaskQueue: Task[] = []; let _isDrainingMicrotaskQueue: boolean = false; - const _uncaughtPromiseErrors: UncaughtPromiseError[] = []; - let _numberOfNestedTaskFrames = 0; - function scheduleQueueDrain() { + 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) { @@ -1229,40 +1231,11 @@ const Zone: ZoneType = (function(global: any) { global[symbolSetTimeout](drainMicroTaskQueue, 0); } } - } - - function scheduleMicroTask(task: MicroTask) { - scheduleQueueDrain(); - _microTaskQueue.push(task); - } - - function consoleError(e: any) { - if ((Zone as any)[__symbol__('ignoreConsoleErrorUncaughtError')]) { - return; - } - const rejection = e && e.rejection; - if (rejection) { - console.error( - 'Unhandled Promise rejection:', - rejection instanceof Error ? rejection.message : rejection, '; Zone:', - (e.zone).name, '; Task:', e.task && (e.task).source, '; Value:', rejection, - rejection instanceof Error ? rejection.stack : undefined); - } - console.error(e); - } - - function handleUnhandledRejection(e: any) { - consoleError(e); - try { - const handler = (Zone as any)[__symbol__('unhandledPromiseRejectionHandler')]; - if (handler && typeof handler === 'function') { - handler.apply(this, [e]); - } - } catch (err) { - } + task && _microTaskQueue.push(task); } function drainMicroTaskQueue() { + const showError: boolean = !(Zone as any)[__symbol__('ignoreConsoleErrorUncaughtError')]; if (!_isDrainingMicrotaskQueue) { _isDrainingMicrotaskQueue = true; while (_microTaskQueue.length) { @@ -1273,613 +1246,51 @@ const Zone: ZoneType = (function(global: any) { try { task.zone.runTask(task, null, null); } catch (error) { - consoleError(error); - } - } - } - while (_uncaughtPromiseErrors.length) { - while (_uncaughtPromiseErrors.length) { - const uncaughtPromiseError: UncaughtPromiseError = _uncaughtPromiseErrors.shift(); - try { - uncaughtPromiseError.zone.runGuarded(() => { - throw uncaughtPromiseError; - }); - } catch (error) { - handleUnhandledRejection(error); + if ((Zone as any)[__symbol__('ignoreConsoleErrorUncaughtError')]) { + return; + } + _api.onUnhandledError(showError, error); } } } + _api.microtaskDrainDone(showError); _isDrainingMicrotaskQueue = false; } } - function isThenable(value: any): boolean { - return value && value.then; - } - - function forwardResolution(value: any): any { - return value; - } - - function forwardRejection(rejection: any): any { - return ZoneAwarePromise.reject(rejection); - } - - const symbolState: string = __symbol__('state'); - const symbolValue: string = __symbol__('value'); - const source: string = 'Promise.then'; - const UNRESOLVED: null = null; - const RESOLVED = true; - const REJECTED = false; - const REJECTED_NO_CATCH = 0; + ////////////////////////////////////////////////////// + ////////////////////////////////////////////////////// + /// BOOTSTRAP + ////////////////////////////////////////////////////// + ////////////////////////////////////////////////////// - function makeResolver(promise: ZoneAwarePromise, state: boolean): (value: any) => void { - return (v) => { - try { - resolvePromise(promise, state, v); - } catch (err) { - resolvePromise(promise, false, err); - } - // Do not return value or you will break the Promise spec. - }; - } - const once = function() { - let wasCalled = false; + const NO_ZONE = {name: 'NO ZONE'}; + const notScheduled: 'notScheduled' = 'notScheduled', scheduling: 'scheduling' = 'scheduling', + scheduled: 'scheduled' = 'scheduled', running: 'running' = 'running', + canceling: 'canceling' = 'canceling', unknown: 'unknown' = 'unknown'; + const microTask: 'microTask' = 'microTask', macroTask: 'macroTask' = 'macroTask', + eventTask: 'eventTask' = 'eventTask'; - return function wrapper(wrappedFunction: Function) { - return function() { - if (wasCalled) { - return; - } - wasCalled = true; - wrappedFunction.apply(null, arguments); - }; - }; + const patches: {[key: string]: any} = {}; + const _api: _ZonePrivate = { + symbol: __symbol__, + currentZoneFrame: () => _currentZoneFrame, + onUnhandledError: noop, + microtaskDrainDone: noop, + scheduleMicroTask: scheduleMicroTask }; + let _currentZoneFrame: _ZoneFrame = {parent: null, zone: new Zone(null, null)}; + let _currentTask: Task = null; + let _numberOfNestedTaskFrames = 0; - // Promise Resolution - function resolvePromise( - promise: ZoneAwarePromise, state: boolean, value: any): ZoneAwarePromise { - const onceWrapper = once(); - if (promise === value) { - throw new TypeError('Promise resolved with itself'); - } - if ((promise as any)[symbolState] === UNRESOLVED) { - // should only get value.then once based on promise spec. - let then: any = null; - try { - if (typeof value === 'object' || typeof value === 'function') { - then = value && value.then; - } - } catch (err) { - onceWrapper(() => { - resolvePromise(promise, false, err); - })(); - return promise; - } - // if (value instanceof ZoneAwarePromise) { - if (state !== REJECTED && value instanceof ZoneAwarePromise && - value.hasOwnProperty(symbolState) && value.hasOwnProperty(symbolValue) && - (value as any)[symbolState] !== UNRESOLVED) { - clearRejectedNoCatch(>value); - resolvePromise(promise, (value as any)[symbolState], (value as any)[symbolValue]); - } else if (state !== REJECTED && typeof then === 'function') { - try { - then.apply(value, [ - onceWrapper(makeResolver(promise, state)), onceWrapper(makeResolver(promise, false)) - ]); - } catch (err) { - onceWrapper(() => { - resolvePromise(promise, false, err); - })(); - } - } else { - (promise as any)[symbolState] = state; - const queue = (promise as any)[symbolValue]; - (promise as any)[symbolValue] = value; - - // record task information in value when error occurs, so we can - // do some additional work such as render longStackTrace - if (state === REJECTED && value instanceof Error) { - (value as any)[__symbol__('currentTask')] = Zone.currentTask; - } - - for (let i = 0; i < queue.length;) { - scheduleResolveOrReject(promise, queue[i++], queue[i++], queue[i++], queue[i++]); - } - if (queue.length == 0 && state == REJECTED) { - (promise as any)[symbolState] = REJECTED_NO_CATCH; - try { - throw new Error( - 'Uncaught (in promise): ' + value + - (value && value.stack ? '\n' + value.stack : '')); - } catch (err) { - const error: UncaughtPromiseError = err; - error.rejection = value; - error.promise = promise; - error.zone = Zone.current; - error.task = Zone.currentTask; - _uncaughtPromiseErrors.push(error); - scheduleQueueDrain(); - } - } - } - } - // Resolving an already resolved promise is a noop. - return promise; - } - - function clearRejectedNoCatch(promise: ZoneAwarePromise): void { - if ((promise as any)[symbolState] === REJECTED_NO_CATCH) { - // if the promise is rejected no catch status - // and queue.length > 0, means there is a error handler - // here to handle the rejected promise, we should trigger - // windows.rejectionhandled eventHandler or nodejs rejectionHandled - // eventHandler - try { - const handler = (Zone as any)[__symbol__('rejectionHandledHandler')]; - if (handler && typeof handler === 'function') { - handler.apply(this, [{rejection: (promise as any)[symbolValue], promise: promise}]); - } - } catch (err) { - } - (promise as any)[symbolState] = REJECTED; - for (let i = 0; i < _uncaughtPromiseErrors.length; i++) { - if (promise === _uncaughtPromiseErrors[i].promise) { - _uncaughtPromiseErrors.splice(i, 1); - } - } - } - } - - function scheduleResolveOrReject( - promise: ZoneAwarePromise, zone: AmbientZone, chainPromise: ZoneAwarePromise, - onFulfilled?: (value: R) => U, onRejected?: (error: any) => U): void { - clearRejectedNoCatch(promise); - const delegate = (promise as any)[symbolState] ? - (typeof onFulfilled === 'function') ? onFulfilled : forwardResolution : - (typeof onRejected === 'function') ? onRejected : forwardRejection; - zone.scheduleMicroTask(source, () => { - try { - resolvePromise( - chainPromise, true, zone.run(delegate, undefined, [(promise as any)[symbolValue]])); - } catch (error) { - resolvePromise(chainPromise, false, error); - } - }); - } - - class ZoneAwarePromise implements Promise { - static toString() { - return 'function ZoneAwarePromise() { [native code] }'; - } - - static resolve(value: R): Promise { - return resolvePromise(>new this(null), RESOLVED, value); - } - - static reject(error: U): Promise { - return resolvePromise(>new this(null), REJECTED, error); - } - - static race(values: PromiseLike[]): Promise { - let resolve: (v: any) => void; - let reject: (v: any) => void; - let promise: any = new this((res, rej) => { - [resolve, reject] = [res, rej]; - }); - function onResolve(value: any) { - promise && (promise = null || resolve(value)); - } - function onReject(error: any) { - promise && (promise = null || reject(error)); - } - - for (let value of values) { - if (!isThenable(value)) { - value = this.resolve(value); - } - value.then(onResolve, onReject); - } - return promise; - } - - static all(values: any): Promise { - let resolve: (v: any) => void; - let reject: (v: any) => void; - let promise = new this((res, rej) => { - resolve = res; - reject = rej; - }); - let count = 0; - const resolvedValues: any[] = []; - for (let value of values) { - if (!isThenable(value)) { - value = this.resolve(value); - } - value.then( - ((index) => (value: any) => { - resolvedValues[index] = value; - count--; - if (!count) { - resolve(resolvedValues); - } - })(count), - reject); - count++; - } - if (!count) resolve(resolvedValues); - return promise; - } - - constructor( - executor: - (resolve: (value?: R|PromiseLike) => void, reject: (error?: any) => void) => void) { - const promise: ZoneAwarePromise = this; - if (!(promise instanceof ZoneAwarePromise)) { - throw new Error('Must be an instanceof Promise.'); - } - (promise as any)[symbolState] = UNRESOLVED; - (promise as any)[symbolValue] = []; // queue; - try { - executor && executor(makeResolver(promise, RESOLVED), makeResolver(promise, REJECTED)); - } catch (error) { - resolvePromise(promise, false, error); - } - } - - then( - onFulfilled?: (value: R) => U | PromiseLike, - onRejected?: (error: any) => U | PromiseLike): Promise { - const chainPromise: Promise = new (this.constructor as typeof ZoneAwarePromise)(null); - const zone = Zone.current; - if ((this as any)[symbolState] == UNRESOLVED) { - ((this as any)[symbolValue]).push(zone, chainPromise, onFulfilled, onRejected); - } else { - scheduleResolveOrReject(this, zone, chainPromise, onFulfilled, onRejected); - } - return chainPromise; - } - - catch(onRejected?: (error: any) => U | PromiseLike): Promise { - return this.then(null, onRejected); - } - } - // Protect against aggressive optimizers dropping seemingly unused properties. - // E.g. Closure Compiler in advanced mode. - ZoneAwarePromise['resolve'] = ZoneAwarePromise.resolve; - ZoneAwarePromise['reject'] = ZoneAwarePromise.reject; - ZoneAwarePromise['race'] = ZoneAwarePromise.race; - ZoneAwarePromise['all'] = ZoneAwarePromise.all; - - const NativePromise = global[symbolPromise] = global['Promise']; - global['Promise'] = ZoneAwarePromise; - - const symbolThenPatched = __symbol__('thenPatched'); - - function patchThen(Ctor: Function) { - const proto = Ctor.prototype; - const originalThen = proto.then; - // Keep a reference to the original method. - proto[symbolThen] = originalThen; - - Ctor.prototype.then = function(onResolve: any, onReject: any) { - const wrapped = new ZoneAwarePromise((resolve, reject) => { - originalThen.call(this, resolve, reject); - }); - return wrapped.then(onResolve, onReject); - }; - (Ctor as any)[symbolThenPatched] = true; - } - - function zoneify(fn: Function) { - return function() { - let resultPromise = fn.apply(this, arguments); - if (resultPromise instanceof ZoneAwarePromise) { - return resultPromise; - } - let ctor = resultPromise.constructor; - if (!ctor[symbolThenPatched]) { - patchThen(ctor); - } - return resultPromise; - }; - } - - if (NativePromise) { - patchThen(NativePromise); - - let fetch = global['fetch']; - if (typeof fetch == 'function') { - global['fetch'] = zoneify(fetch); - } - } - - // This is not part of public API, but it is useful for tests, so we expose it. - (Promise as any)[Zone.__symbol__('uncaughtPromiseErrors')] = _uncaughtPromiseErrors; - - /* - * This code patches Error so that: - * - It ignores un-needed stack frames. - * - It Shows the associated Zone for reach frame. - */ - - const enum FrameType { - /// Skip this frame when printing out stack - blackList, - /// This frame marks zone transition - transition - } - - const blacklistedStackFramesSymbol = Zone.__symbol__('blacklistedStackFrames'); - const NativeError = global[__symbol__('Error')] = global.Error; - // Store the frames which should be removed from the stack frames - const blackListedStackFrames: {[frame: string]: FrameType} = {}; - // We must find the frame where Error was created, otherwise we assume we don't understand stack - let zoneAwareFrame1: string; - let zoneAwareFrame2: string; - - global.Error = ZoneAwareError; - const stackRewrite = 'stackRewrite'; - - /** - * This is ZoneAwareError which processes the stack frame and cleans up extra frames as well as - * adds zone information to it. - */ - function ZoneAwareError(): Error { - // We always have to return native error otherwise the browser console will not work. - let error: Error = NativeError.apply(this, arguments); - // Save original stack trace - const originalStack = (error as any)['originalStack'] = error.stack; - - // Process the stack trace and rewrite the frames. - if ((ZoneAwareError as any)[stackRewrite] && originalStack) { - let frames: string[] = originalStack.split('\n'); - let zoneFrame = _currentZoneFrame; - let i = 0; - // Find the first frame - while (!(frames[i] === zoneAwareFrame1 || frames[i] === zoneAwareFrame2) && - i < frames.length) { - i++; - } - for (; i < frames.length && zoneFrame; i++) { - let frame = frames[i]; - if (frame.trim()) { - switch (blackListedStackFrames[frame]) { - case FrameType.blackList: - frames.splice(i, 1); - i--; - break; - case FrameType.transition: - if (zoneFrame.parent) { - // This is the special frame where zone changed. Print and process it accordingly - zoneFrame = zoneFrame.parent; - } else { - zoneFrame = null; - } - frames.splice(i, 1); - i--; - break; - default: - frames[i] += ` [${zoneFrame.zone.name}]`; - } - } - } - try { - error.stack = error.zoneAwareStack = frames.join('\n'); - } catch (e) { - // ignore as some browsers don't allow overriding of stack - } - } - - if (this instanceof NativeError && this.constructor != NativeError) { - // We got called with a `new` operator AND we are subclass of ZoneAwareError - // in that case we have to copy all of our properties to `this`. - Object.keys(error).concat('stack', 'message').forEach((key) => { - if ((error as any)[key] !== undefined) { - try { - this[key] = (error as any)[key]; - } catch (e) { - // ignore the assignment in case it is a setter and it throws. - } - } - }); - return this; - } - return error; - } - - // Copy the prototype so that instanceof operator works as expected - ZoneAwareError.prototype = NativeError.prototype; - (ZoneAwareError as any)[blacklistedStackFramesSymbol] = blackListedStackFrames; - (ZoneAwareError as any)[stackRewrite] = false; - - // those properties need special handling - const specialPropertyNames = ['stackTraceLimit', 'captureStackTrace', 'prepareStackTrace']; - // those properties of NativeError should be set to ZoneAwareError - const nativeErrorProperties = Object.keys(NativeError); - if (nativeErrorProperties) { - nativeErrorProperties.forEach(prop => { - if (specialPropertyNames.filter(sp => sp === prop).length === 0) { - Object.defineProperty(ZoneAwareError, prop, { - get: function() { - return NativeError[prop]; - }, - set: function(value) { - NativeError[prop] = value; - } - }); - } - }); - } - - if (NativeError.hasOwnProperty('stackTraceLimit')) { - // Extend default stack limit as we will be removing few frames. - NativeError.stackTraceLimit = Math.max(NativeError.stackTraceLimit, 15); - - // make sure that ZoneAwareError has the same property which forwards to NativeError. - Object.defineProperty(ZoneAwareError, 'stackTraceLimit', { - get: function() { - return NativeError.stackTraceLimit; - }, - set: function(value) { - return NativeError.stackTraceLimit = value; - } - }); - } + function noop() {} - if (NativeError.hasOwnProperty('captureStackTrace')) { - Object.defineProperty(ZoneAwareError, 'captureStackTrace', { - // add named function here because we need to remove this - // stack frame when prepareStackTrace below - value: function zoneCaptureStackTrace(targetObject: Object, constructorOpt?: Function) { - NativeError.captureStackTrace(targetObject, constructorOpt); - } - }); + function __symbol__(name: string) { + return '__zone_symbol__' + name; } - Object.defineProperty(ZoneAwareError, 'prepareStackTrace', { - get: function() { - return NativeError.prepareStackTrace; - }, - set: function(value) { - if (!value || typeof value !== 'function') { - return NativeError.prepareStackTrace = value; - } - return NativeError.prepareStackTrace = function( - error: Error, structuredStackTrace: {getFunctionName: Function}[]) { - // remove additional stack information from ZoneAwareError.captureStackTrace - if (structuredStackTrace) { - for (let i = 0; i < structuredStackTrace.length; i++) { - const st = structuredStackTrace[i]; - // remove the first function which name is zoneCaptureStackTrace - if (st.getFunctionName() === 'zoneCaptureStackTrace') { - structuredStackTrace.splice(i, 1); - break; - } - } - } - return value.apply(this, [error, structuredStackTrace]); - }; - } - }); - - // Now we need to populate the `blacklistedStackFrames` as well as find the - // run/runGuarded/runTask frames. This is done by creating a detect zone and then threading - // the execution through all of the above methods so that we can look at the stack trace and - // find the frames of interest. - let detectZone: Zone = Zone.current.fork({ - name: 'detect', - onHandleError: function(parentZD: ZoneDelegate, current: Zone, target: Zone, error: any): - boolean { - if (error.originalStack && Error === ZoneAwareError) { - let frames = error.originalStack.split(/\n/); - let runFrame = false, runGuardedFrame = false, runTaskFrame = false; - while (frames.length) { - let frame = frames.shift(); - // On safari it is possible to have stack frame with no line number. - // This check makes sure that we don't filter frames on name only (must have - // linenumber) - if (/:\d+:\d+/.test(frame)) { - // Get rid of the path so that we don't accidentally find function name in path. - // In chrome the separator is `(` and `@` in FF and safari - // Chrome: at Zone.run (zone.js:100) - // Chrome: at Zone.run (http://localhost:9876/base/build/lib/zone.js:100:24) - // FireFox: Zone.prototype.run@http://localhost:9876/base/build/lib/zone.js:101:24 - // Safari: run@http://localhost:9876/base/build/lib/zone.js:101:24 - let fnName: string = frame.split('(')[0].split('@')[0]; - let frameType = FrameType.transition; - if (fnName.indexOf('ZoneAwareError') !== -1) { - zoneAwareFrame1 = frame; - zoneAwareFrame2 = frame.replace('Error.', ''); - blackListedStackFrames[zoneAwareFrame2] = FrameType.blackList; - } - if (fnName.indexOf('runGuarded') !== -1) { - runGuardedFrame = true; - } else if (fnName.indexOf('runTask') !== -1) { - runTaskFrame = true; - } else if (fnName.indexOf('run') !== -1) { - runFrame = true; - } else { - frameType = FrameType.blackList; - } - blackListedStackFrames[frame] = frameType; - // Once we find all of the frames we can stop looking. - if (runFrame && runGuardedFrame && runTaskFrame) { - (ZoneAwareError as any)[stackRewrite] = true; - break; - } - } - } - } - return false; - } - }) as Zone; - // carefully constructor a stack frame which contains all of the frames of interest which - // need to be detected and blacklisted. - - const childDetectZone = detectZone.fork({ - name: 'child', - onScheduleTask: function(delegate, curr, target, task) { - return delegate.scheduleTask(target, task); - }, - onInvokeTask: function(delegate, curr, target, task, applyThis, applyArgs) { - return delegate.invokeTask(target, task, applyThis, applyArgs); - }, - onCancelTask: function(delegate, curr, target, task) { - return delegate.cancelTask(target, task); - }, - onInvoke: function(delegate, curr, target, callback, applyThis, applyArgs, source) { - return delegate.invoke(target, callback, applyThis, applyArgs, source); - } - }); - - // we need to detect all zone related frames, it will - // exceed default stackTraceLimit, so we set it to - // larger number here, and restore it after detect finish. - const originalStackTraceLimit = Error.stackTraceLimit; - Error.stackTraceLimit = 100; - // we schedule event/micro/macro task, and invoke them - // when onSchedule, so we can get all stack traces for - // all kinds of tasks with one error thrown. - childDetectZone.run(() => { - childDetectZone.runGuarded(() => { - const fakeTransitionTo = - (toState: TaskState, fromState1: TaskState, fromState2: TaskState) => {}; - childDetectZone.scheduleEventTask( - blacklistedStackFramesSymbol, - () => { - childDetectZone.scheduleMacroTask( - blacklistedStackFramesSymbol, - () => { - childDetectZone.scheduleMicroTask( - blacklistedStackFramesSymbol, - () => { - throw new (ZoneAwareError as any)(ZoneAwareError, NativeError); - }, - null, - (t: Task) => { - (t as any)._transitionTo = fakeTransitionTo; - t.invoke(); - }); - }, - null, - (t) => { - (t as any)._transitionTo = fakeTransitionTo; - t.invoke(); - }, - () => {}); - }, - null, - (t) => { - (t as any)._transitionTo = fakeTransitionTo; - t.invoke(); - }, - () => {}); - }); - }); - Error.stackTraceLimit = originalStackTraceLimit; + performanceMeasure('Zone', 'Zone'); return global['Zone'] = Zone; })(typeof window !== 'undefined' && window || typeof self !== 'undefined' && self || global); diff --git a/scripts/closure/closure_compiler.sh b/scripts/closure/closure_compiler.sh index 2f4b27dd5..563c83814 100755 --- a/scripts/closure/closure_compiler.sh +++ b/scripts/closure/closure_compiler.sh @@ -1,5 +1,5 @@ # compile closure test source file -tsc -p . +$(npm bin)/tsc -p . # Run the Google Closure compiler java runnable with zone externs java -jar node_modules/google-closure-compiler/compiler.jar --flagfile 'scripts/closure/closure_flagfile' --externs './dist/zone_externs.js' @@ -10,7 +10,7 @@ if [ $? -eq 0 ] then echo "Successfully pass closure compiler with zone externs" else - echo "failed to pass closure compiler with zone externs" + echo "failed to pass closure compiler with zone externs" exit 1 fi @@ -24,8 +24,8 @@ if [ $? -eq 1 ] then echo "Successfully detect closure compiler error without zone externs" else - echo "failed to detect closure compiler error without zone externs" + echo "failed to detect closure compiler error without zone externs" exit 1 fi -exit 0 \ No newline at end of file +exit 0 diff --git a/test/browser/browser.spec.ts b/test/browser/browser.spec.ts index c2ccdfe18..056494886 100644 --- a/test/browser/browser.spec.ts +++ b/test/browser/browser.spec.ts @@ -56,6 +56,7 @@ function supportEventListenerOptions() { describe('Zone', function() { const rootZone = Zone.current; + (Zone as any)[zoneSymbol('ignoreConsoleErrorUncaughtError')] = true; describe('hooks', function() { it('should allow you to override alert/prompt/confirm', function() { diff --git a/test/common/microtasks.spec.ts b/test/common/microtasks.spec.ts index 7357470cf..87f512012 100644 --- a/test/common/microtasks.spec.ts +++ b/test/common/microtasks.spec.ts @@ -56,7 +56,7 @@ describe('Microtasks', function() { testZone.run(function() { resolvedPromise.then(function() { - expect(Zone.current).toBe(testZone); + expect(Zone.current.name).toBe(testZone.name); done(); }); }); diff --git a/test/common/util.spec.ts b/test/common/util.spec.ts index b56ac7efa..3b4465d37 100644 --- a/test/common/util.spec.ts +++ b/test/common/util.spec.ts @@ -78,26 +78,5 @@ describe('utils', function() { expect(desc.writable).toBeTruthy(); expect(!desc.get).toBeTruthy(); }); - - - it('should have a method name in the stacktrace', () => { - const fn = function someOtherName() { - throw new Error('MyError'); - }; - const target = {mySpecialMethodName: fn}; - patchMethod(target, 'mySpecialMethodName', (delegate: Function) => { - return function(self, args) { - return delegate(); - }; - }); - try { - target.mySpecialMethodName(); - } catch (e) { - if (e.stack) { - expect(e.stack).toContain('mySpecialMethodName'); - } - } - }); }); - });