diff --git a/doc/error.png b/doc/error.png index 6a155397e..e1344e25a 100644 Binary files a/doc/error.png and b/doc/error.png differ diff --git a/doc/error.puml b/doc/error.puml index 0aa5da3be..f3db46186 100644 --- a/doc/error.puml +++ b/doc/error.puml @@ -1,9 +1,9 @@ @startuml -scheduling --> throw: zoneSpec.onScheduleTask\nor task.scheduleFn\nthrow error +scheduling --> unknown: zoneSpec.onScheduleTask\nor task.scheduleFn\nthrow error running --> scheduled: error in \ntask.callback\nand task is\nperiodical\ntask running --> notScheduled: error in\ntask.callback\nand\ntask is not\nperiodical running: zoneSpec.onHandleError running --> throw: error in\n task.callback\n and \nzoneSpec.onHandleError\n return true -throw: throw to application -canceling --> throw: zoneSpec.onCancelTask\n or task.cancelFn\n throw error +canceling --> unknown: zoneSpec.onCancelTask\n or task.cancelFn\n throw error +unknown --> throw @enduml \ No newline at end of file diff --git a/doc/onetime-macrotask.png b/doc/non-periodical-macrotask.png similarity index 100% rename from doc/onetime-macrotask.png rename to doc/non-periodical-macrotask.png diff --git a/doc/onetime-macrotask.puml b/doc/non-periodical-macrotask.puml similarity index 100% rename from doc/onetime-macrotask.puml rename to doc/non-periodical-macrotask.puml diff --git a/doc/task.md b/doc/task.md index 53cecdfab..337a0ebf7 100644 --- a/doc/task.md +++ b/doc/task.md @@ -39,7 +39,7 @@ EventTask will go back to scheduled state after invoked(running state), and will Such as setTimeout/XMLHttpRequest, their lifecycle(state transition) looks like this. -![onetime-MacroTask](onetime-macrotask.png "onetime MacroTask") +![non-periodical-macroTask](non-periodical-macrotask.png "non periodical macroTask") ZoneSpec's onHasTask callback will be triggered when the first macroTask were scheduled or the last macroTask was invoked or cancelled. diff --git a/lib/zone.ts b/lib/zone.ts index aaeb47592..24064e663 100644 --- a/lib/zone.ts +++ b/lib/zone.ts @@ -466,9 +466,9 @@ type HasTaskState = { type TaskType = 'microTask'|'macroTask'|'eventTask'; /** - * Task type: `notScheduled`, `scheduling`, `scheduled`, `running`, `canceling`. + * Task type: `notScheduled`, `scheduling`, `scheduled`, `running`, `canceling`, 'unknown'. */ -type TaskState = 'notScheduled'|'scheduling'|'scheduled'|'running'|'canceling'; +type TaskState = 'notScheduled'|'scheduling'|'scheduled'|'running'|'canceling'|'unknown'; /** @@ -514,7 +514,7 @@ interface Task { type: TaskType; /** - * Task state: `notScheduled`, `scheduling`, `scheduled`, `running`, `canceling`. + * Task state: `notScheduled`, `scheduling`, `scheduled`, `running`, `canceling`, `unknown`. */ state: TaskState; @@ -559,7 +559,7 @@ interface Task { * @type {Zone} The zone which will be used to invoke the `callback`. The Zone is captured * at the time of Task creation. */ - zone: Zone; + readonly zone: Zone; /** * Number of times the task has been executed, or -1 if canceled. @@ -614,7 +614,7 @@ const Zone: ZoneType = (function(global: any) { const NO_ZONE = {name: 'NO ZONE'}; const notScheduled: 'notScheduled' = 'notScheduled', scheduling: 'scheduling' = 'scheduling', scheduled: 'scheduled' = 'scheduled', running: 'running' = 'running', - canceling: 'canceling' = 'canceling'; + canceling: 'canceling' = 'canceling', unknown: 'unknown' = 'unknown'; const microTask: 'microTask' = 'microTask', macroTask: 'macroTask' = 'macroTask', eventTask: 'eventTask' = 'eventTask'; @@ -754,17 +754,17 @@ const Zone: ZoneType = (function(global: any) { } } } finally { - if (task.type == eventTask || (task.data && task.data.isPeriodic)) { - // if the task's state is notScheduled, then it has already been cancelled - // we should not reset the state to scheduled - if (task.state !== notScheduled) { + // if the task's state is notScheduled or unknown, then it has already been cancelled + // we should not reset the state to scheduled + if (task.state !== notScheduled && task.state !== unknown) { + if (task.type == eventTask || (task.data && task.data.isPeriodic)) { reEntryGuard && (task as ZoneTask)._transitionTo(scheduled, running); + } else { + task.runCount = 0; + this._updateTaskCount(task as ZoneTask, -1); + reEntryGuard && + (task as ZoneTask)._transitionTo(notScheduled, running, notScheduled); } - } else { - task.runCount = 0; - this._updateTaskCount(task as ZoneTask, -1); - reEntryGuard && - (task as ZoneTask)._transitionTo(notScheduled, running, notScheduled); } _currentZoneFrame = _currentZoneFrame.parent; _currentTask = previousTask; @@ -772,11 +772,32 @@ const Zone: ZoneType = (function(global: any) { } scheduleTask(task: T): T { + if (task.zone && task.zone !== this) { + // check if the task was rescheduled, the newZone + // should not be the children of the original zone + let newZone: any = this; + while (newZone) { + if (newZone === task.zone) { + throw Error(`can not reschedule task to ${this + .name} which is descendants of the original zone ${task.zone.name}`); + } + newZone = newZone.parent; + } + } (task as any as ZoneTask)._transitionTo(scheduling, notScheduled); const zoneDelegates: ZoneDelegate[] = []; (task as any as ZoneTask)._zoneDelegates = zoneDelegates; - task.zone = this; - task = this._zoneDelegate.scheduleTask(this, task) as T; + (task as any as ZoneTask)._zone = this; + try { + task = this._zoneDelegate.scheduleTask(this, task) as T; + } catch (err) { + // should set task's state to unknown when scheduleTask throw error + // because the err may from reschedule, so the fromState maybe notScheduled + (task as any as ZoneTask)._transitionTo(unknown, scheduling, notScheduled); + // TODO: @JiaLiPassion, should we check the result from handleError? + this._zoneDelegate.handleError(this, err); + throw err; + } if ((task as any as ZoneTask)._zoneDelegates === zoneDelegates) { // we have to check because internally the delegate can reschedule the task. this._updateTaskCount(task as any as ZoneTask, 1); @@ -809,8 +830,22 @@ const Zone: ZoneType = (function(global: any) { } cancelTask(task: Task): any { + if (task.zone != this) + throw new Error( + 'A task can only be cancelled in the zone of creation! (Creation: ' + + (task.zone || NO_ZONE).name + '; Execution: ' + this.name + ')'); (task as ZoneTask)._transitionTo(canceling, scheduled, running); - this._zoneDelegate.cancelTask(this, task); + try { + this._zoneDelegate.cancelTask(this, task); + } catch (err) { + // if error occurs when cancelTask, transit the state to unknown + (task as ZoneTask)._transitionTo(unknown, canceling); + // TODO: @JiaLiPassion, should updateTaskCount when cancelTask throw error? + this._updateTaskCount(task as ZoneTask, -1); + this._zoneDelegate.handleError(this, err); + throw err; + } + // TODO: @JiaLiPassion, should updateTaskCount when cancelTask throw error? this._updateTaskCount(task as ZoneTask, -1); (task as ZoneTask)._transitionTo(notScheduled, canceling); task.runCount = 0; @@ -1025,14 +1060,23 @@ const Zone: ZoneType = (function(global: any) { value = this._cancelTaskZS.onCancelTask( this._cancelTaskDlgt, this._cancelTaskCurrZone, targetZone, task); } else { + if (!task.cancelFn) { + throw Error('Task is not cancelable'); + } value = task.cancelFn(task); } return value; } hasTask(targetZone: Zone, isEmpty: HasTaskState) { - return this._hasTaskZS && - this._hasTaskZS.onHasTask(this._hasTaskDlgt, this._hasTaskCurrZone, targetZone, isEmpty); + // hasTask should not throw error so other ZoneDelegate + // can still trigger hasTask callback + try { + return this._hasTaskZS && + this._hasTaskZS.onHasTask( + this._hasTaskDlgt, this._hasTaskCurrZone, targetZone, isEmpty); + } catch (err) { + } } _updateTaskCount(type: TaskType, count: number) { @@ -1064,7 +1108,7 @@ const Zone: ZoneType = (function(global: any) { public data: TaskData; public scheduleFn: (task: Task) => void; public cancelFn: (task: Task) => void; - public zone: Zone = null; + _zone: Zone = null; public runCount: number = 0; _zoneDelegates: ZoneDelegate[] = null; _state: TaskState = 'notScheduled'; @@ -1093,6 +1137,10 @@ const Zone: ZoneType = (function(global: any) { }; } + get zone(): Zone { + return this._zone; + } + get state(): TaskState { return this._state; } diff --git a/test/common/task.spec.ts b/test/common/task.spec.ts new file mode 100644 index 000000000..92afa4c90 --- /dev/null +++ b/test/common/task.spec.ts @@ -0,0 +1,1021 @@ +/** + * @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 + */ + +const noop = function() {}; +let log: {zone: string, taskZone: undefined | string, toState: TaskState, fromState: TaskState}[] = + []; +const detectTask = Zone.current.scheduleMacroTask('detectTask', noop, null, noop, noop); +const originalTransitionTo = detectTask.constructor.prototype._transitionTo; +// patch _transitionTo of ZoneTask to add log for test +detectTask.constructor.prototype._transitionTo = function( + toState: TaskState, fromState1: TaskState, fromState2?: TaskState) { + log.push({ + zone: Zone.current.name, + taskZone: this.zone && this.zone.name, + toState: toState, + fromState: this._state + }); + originalTransitionTo.apply(this, arguments); +}; + +describe('task lifecycle', () => { + afterAll(() => { + detectTask.constructor.prototype._transitionTo = originalTransitionTo; + }); + + describe('event task lifecycle', () => { + beforeEach(() => { + log = []; + }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + () => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + Zone.current.scheduleEventTask('testEventTask', noop, null, noop, noop); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + () => { + Zone.current + .fork({ + name: 'testEventTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + Zone.current.scheduleEventTask('testEventTask', noop, null, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduled to running when task is invoked then from running to scheduled after invoke', + () => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask('testEventTask', noop, null, noop, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + }); + + it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running', + () => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask('testEventTask', noop, null, noop, noop); + Zone.current.cancelTask(task); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + + it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', + () => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask('testEventTask', () => { + Zone.current.cancelTask(task); + }, null, noop, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'canceling', fromState: 'running'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + + it('task should transit from running to scheduled when task.callback throw error', () => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask('testEventTask', () => { + throw Error('invoke error'); + }, null, noop, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + }); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running', + () => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask('testEventTask', noop, null, noop, () => { + throw Error('cancel task'); + }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + }); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state', + () => { + Zone.current.fork({name: 'testEventTaskZone'}).run(() => { + const task = Zone.current.scheduleEventTask('testEventTask', noop, null, noop, () => { + throw Error('cancel task'); + }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + }); + + it('task should transit from notScheduled to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + () => { + Zone.current + .fork({ + name: 'testEventTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + Zone.current.scheduleEventTask('testEventTask', noop, null, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled', + () => { + let task: Task; + Zone.current + .fork({ + name: 'testEventTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'canceling') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = Zone.current.scheduleEventTask('testEventTask', noop, null, noop, noop); + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + }); + + describe('non periodical macroTask lifecycle', () => { + beforeEach(() => { + log = []; + }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + () => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, noop); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + () => { + Zone.current + .fork({ + name: 'testMacroTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduled to running when task is invoked then from running to noScheduled after invoke', + () => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + }); + + it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running', + () => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask('testMacrotask', noop, null, noop, noop); + Zone.current.cancelTask(task); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + + it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', + () => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask('testMacroTask', () => { + Zone.current.cancelTask(task); + }, null, noop, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'canceling', fromState: 'running'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + + it('task should transit from running to noScheduled when task.callback throw error', () => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask('testMacroTask', () => { + throw Error('invoke error'); + }, null, noop, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + }); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running', + () => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, () => { + throw Error('cancel task'); + }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + }); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state', + () => { + Zone.current.fork({name: 'testMacroTaskZone'}).run(() => { + const task = Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, () => { + throw Error('cancel task'); + }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + }); + + it('task should transit from notScheduled to scheduling then to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + () => { + Zone.current + .fork({ + name: 'testMacroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error after task.callback being invoked', + () => { + let task: Task; + Zone.current + .fork({ + name: 'testMacroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'running') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, noop); + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + }); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled before running', + () => { + let task: Task; + Zone.current + .fork({ + name: 'testMacroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'canceling') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask('testMacroTask', noop, null, noop, noop); + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + }); + + describe('periodical macroTask lifecycle', () => { + let task: Task; + beforeEach(() => { + log = []; + task = null; + }); + afterEach(() => { + task && task.state !== 'notScheduled' && task.state !== 'canceling' && + task.state !== 'unknown' && task.zone.cancelTask(task); + }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + () => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + () => { + Zone.current + .fork({ + name: 'testPeriodicalTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduled to running when task is invoked then from running to scheduled after invoke', + () => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + }); + + it('task should transit from scheduled to canceling then from canceling to notScheduled when task is canceled before running', + () => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + Zone.current.cancelTask(task); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + + it('task should transit from running to canceling then from canceling to notScheduled when task is canceled in running state', + () => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask('testPeriodicalTask', () => { + Zone.current.cancelTask(task); + }, {isPeriodic: true}, noop, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'canceling', fromState: 'running'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + + it('task should transit from running to scheduled when task.callback throw error', () => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask('testPeriodicalTask', () => { + throw Error('invoke error'); + }, {isPeriodic: true}, noop, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'scheduled', fromState: 'running'} + ]); + }); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error before task running', + () => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, () => { + throw Error('cancel task'); + }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + }); + + it('task should transit from canceling to unknown when zoneSpec.onCancelTask throw error in running state', + () => { + Zone.current.fork({name: 'testPeriodicalTaskZone'}).run(() => { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, () => { + throw Error('cancel task'); + }); + try { + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'unknown', fromState: 'canceling'} + ]); + }); + + it('task should transit from notScheduled to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + () => { + Zone.current + .fork({ + name: 'testPeriodicalTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error when task is canceled', + () => { + Zone.current + .fork({ + name: 'testPeriodicalTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'canceling') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = Zone.current.scheduleMacroTask( + 'testPeriodicalTask', noop, {isPeriodic: true}, noop, noop); + Zone.current.cancelTask(task); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'canceling', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'canceling'} + ]); + }); + }); + + describe('microTask lifecycle', () => { + beforeEach(() => { + log = []; + }); + + it('task should transit from notScheduled to scheduling then to scheduled state when scheduleTask', + () => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + Zone.current.scheduleMicroTask('testMicroTask', noop, null, noop); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduling to unknown when zoneSpec onScheduleTask callback throw error', + () => { + Zone.current + .fork({ + name: 'testMicroTaskZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + throw Error('error in onScheduleTask'); + } + }) + .run(() => { + try { + Zone.current.scheduleMicroTask('testMicroTask', noop, null, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'unknown', fromState: 'scheduling'} + ]); + }); + + it('task should transit from scheduled to running when task is invoked then from running to noScheduled after invoke', + () => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + const task = Zone.current.scheduleMicroTask('testMicroTask', noop, null, noop); + task.invoke(); + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + }); + + it('should throw error when try to cancel a microTask', () => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + const task = Zone.current.scheduleMicroTask('testMicroTask', () => {}, null, noop); + expect(() => { + Zone.current.cancelTask(task); + }).toThrowError('Task is not cancelable'); + }); + }); + + it('task should transit from running to notScheduled when task.callback throw error', () => { + Zone.current.fork({name: 'testMicroTaskZone'}).run(() => { + const task = Zone.current.scheduleMicroTask('testMicroTask', () => { + throw Error('invoke error'); + }, null, noop); + try { + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + }); + + it('task should transit from notScheduled to scheduling then to scheduled if zoneSpec.onHasTask throw error when scheduleTask', + () => { + Zone.current + .fork({ + name: 'testMicroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + throw Error('hasTask Error'); + } + }) + .run(() => { + try { + Zone.current.scheduleMicroTask('testMicroTask', noop, null, noop); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'} + ]); + }); + + it('task should transit to notScheduled state if zoneSpec.onHasTask throw error after task.callback being invoked', + () => { + let task: Task; + Zone.current + .fork({ + name: 'testMicroTaskZone', + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + if (task && task.state === 'running') { + throw Error('hasTask Error'); + } + } + }) + .run(() => { + try { + task = Zone.current.scheduleMicroTask('testMicroTask', noop, null, noop); + task.invoke(); + } catch (err) { + } + }); + expect(log.map(item => { + return {toState: item.toState, fromState: item.fromState}; + })) + .toEqual([ + {toState: 'scheduling', fromState: 'notScheduled'}, + {toState: 'scheduled', fromState: 'scheduling'}, + {toState: 'running', fromState: 'scheduled'}, + {toState: 'notScheduled', fromState: 'running'} + ]); + }); + + + }); + + describe('reschedule zone', () => { + let callbackLogs: ({pos: string, method: string, zone: string, task: string}|HasTaskState)[]; + const newZone = Zone.root.fork({ + name: 'new', + onScheduleTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + return delegate.scheduleTask(targetZone, task); + }, + onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => { + callbackLogs.push( + {pos: 'before', method: 'onInvokeTask', zone: currZone.name, task: task.zone.name}); + return delegate.invokeTask(targetZone, task, applyThis, applyArgs); + }, + onCancelTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onCancelTask', zone: currZone.name, task: task.zone.name}); + return delegate.cancelTask(targetZone, task); + }, + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + (hasTaskState as any)['zone'] = targetZone.name; + callbackLogs.push(hasTaskState); + return delegate.hasTask(targetZone, hasTaskState); + } + }); + const zone = Zone.root.fork({ + name: 'original', + onScheduleTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + task.cancelScheduleRequest(); + task = newZone.scheduleTask(task); + callbackLogs.push( + {pos: 'after', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + return task; + }, + onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => { + callbackLogs.push( + {pos: 'before', method: 'onInvokeTask', zone: currZone.name, task: task.zone.name}); + return delegate.invokeTask(targetZone, task, applyThis, applyArgs); + }, + onCancelTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onCancelTask', zone: currZone.name, task: task.zone.name}); + return delegate.cancelTask(targetZone, task); + }, + onHasTask: (delegate, currZone, targetZone, hasTaskState) => { + (hasTaskState)['zone'] = targetZone.name; + callbackLogs.push(hasTaskState); + return delegate.hasTask(targetZone, hasTaskState); + } + }); + + beforeEach(() => { + callbackLogs = []; + }); + + it('should be able to reschedule zone when in scheduling state, after that, task will completely go to new zone, has nothing to do with original one', + () => { + zone.run(() => { + const t = + Zone.current.scheduleMacroTask('testRescheduleZoneTask', noop, null, noop, noop); + t.invoke(); + }); + + expect(callbackLogs).toEqual([ + {pos: 'before', method: 'onScheduleTask', zone: 'original', task: 'original'}, + {pos: 'before', method: 'onScheduleTask', zone: 'new', task: 'new'}, + {microTask: false, macroTask: true, eventTask: false, change: 'macroTask', zone: 'new'}, + {pos: 'after', method: 'onScheduleTask', zone: 'original', task: 'new'}, + {pos: 'before', method: 'onInvokeTask', zone: 'new', task: 'new'}, { + microTask: false, + macroTask: false, + eventTask: false, + change: 'macroTask', + zone: 'new' + } + ]); + }); + + it('should not be able to reschedule task in notScheduled/running/canceling state', () => { + Zone.current.fork({name: 'rescheduleNotScheduled'}).run(() => { + const t = Zone.current.scheduleMacroTask('testRescheduleZoneTask', noop, null, noop, noop); + Zone.current.cancelTask(t); + expect(() => { + t.cancelScheduleRequest(); + }) + .toThrow(Error( + `macroTask 'testRescheduleZoneTask': can not transition to 'notScheduled', expecting state 'scheduling', was 'notScheduled'.`)); + }); + + Zone.current + .fork({ + name: 'rescheduleRunning', + onInvokeTask: (delegate, currZone, targetZone, task, applyThis, applyArgs) => { + expect(() => { + task.cancelScheduleRequest(); + }) + .toThrow(Error( + `macroTask 'testRescheduleZoneTask': can not transition to 'notScheduled', expecting state 'scheduling', was 'running'.`)); + } + }) + .run(() => { + const t = + Zone.current.scheduleMacroTask('testRescheduleZoneTask', noop, null, noop, noop); + t.invoke(); + }); + + Zone.current + .fork({ + name: 'rescheduleCanceling', + onCancelTask: (delegate, currZone, targetZone, task) => { + expect(() => { + task.cancelScheduleRequest(); + }) + .toThrow(Error( + `macroTask 'testRescheduleZoneTask': can not transition to 'notScheduled', expecting state 'scheduling', was 'canceling'.`)); + } + }) + .run(() => { + const t = + Zone.current.scheduleMacroTask('testRescheduleZoneTask', noop, null, noop, noop); + Zone.current.cancelTask(t); + }); + }); + + it('can not reschedule a task to a zone which is the descendants of the original zone', () => { + const originalZone = Zone.root.fork({ + name: 'originalZone', + onScheduleTask: (delegate, currZone, targetZone, task) => { + callbackLogs.push( + {pos: 'before', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + task.cancelScheduleRequest(); + task = rescheduleZone.scheduleTask(task); + callbackLogs.push( + {pos: 'after', method: 'onScheduleTask', zone: currZone.name, task: task.zone.name}); + return task; + } + }); + const rescheduleZone = originalZone.fork({name: 'rescheduleZone'}); + expect(() => { + originalZone.run(() => { + Zone.current.scheduleMacroTask('testRescheduleZoneTask', noop, null, noop, noop); + }); + }) + .toThrowError( + 'can not reschedule task to rescheduleZone which is descendants of the original zone originalZone'); + }); + }); +}); diff --git a/test/common/zone.spec.ts b/test/common/zone.spec.ts index ab11c2f21..e4533cbd4 100644 --- a/test/common/zone.spec.ts +++ b/test/common/zone.spec.ts @@ -140,6 +140,28 @@ describe('Zone', function() { log = []; }); + it('task can only run in the zone of creation', () => { + const task = + zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, null, noop, noop); + expect(() => { + Zone.current.fork({name: 'anotherZone'}).runTask(task); + }) + .toThrowError( + 'A task can only be run in the zone of creation! (Creation: createZone; Execution: anotherZone)'); + task.zone.cancelTask(task); + }); + + it('task can only cancel in the zone of creation', () => { + const task = + zone.fork({name: 'createZone'}).scheduleMacroTask('test', noop, null, noop, noop); + expect(() => { + Zone.current.fork({name: 'anotherZone'}).cancelTask(task); + }) + .toThrowError( + 'A task can only be cancelled in the zone of creation! (Creation: createZone; Execution: anotherZone)'); + task.zone.cancelTask(task); + }); + it('should prevent double cancellation', () => { const task = zone.scheduleMacroTask('test', () => log.push('macroTask'), null, noop, noop); zone.cancelTask(task); @@ -209,28 +231,6 @@ describe('Zone', function() { ]); }); - it('should allow overriding of the Zone in task', () => { - expect(Zone.current).not.toBe(zone); - let taskZone = null; - const callback = () => { - taskZone = Zone.current; - }; - const customSchedule = (task: Task) => {}; - const customCancel = (task: Task) => {}; - const testZone = Zone.current.fork({ - name: 'testZone', - onScheduleTask: (delegate: ZoneDelegate, currentZone: Zone, targetZone: Zone, task: Task): - Task => { - task.zone = zone; - return delegate.scheduleTask(targetZone, task); - } - }); - const task = - testZone.scheduleMacroTask('test1', callback, null, customSchedule, customCancel); - task.zone.runTask(task); - expect(taskZone).toBe(zone); - }); - it('should allow rescheduling a task on a separate zone', () => { const log: any[] = []; const zone = Zone.current.fork({ diff --git a/test/common_tests.ts b/test/common_tests.ts index 500c699f7..67716e2b1 100644 --- a/test/common_tests.ts +++ b/test/common_tests.ts @@ -8,6 +8,7 @@ import './common/microtasks.spec'; import './common/zone.spec'; +import './common/task.spec'; import './common/util.spec'; import './common/Promise.spec'; import './common/Error.spec';