Skip to content
This repository was archived by the owner on Feb 26, 2024. It is now read-only.

fix(timer): fix #314, setTimeout/interval should return original timerId #894

Merged
merged 1 commit into from
Sep 13, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 40 additions & 13 deletions lib/common/timers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
* @suppress {missingRequire}
*/

import {patchMethod} from './utils';
import {patchMethod, zoneSymbol} from './utils';

const taskSymbol = zoneSymbol('zoneTask');

interface TimerOptions extends TaskData {
handleId: number;
Expand Down Expand Up @@ -38,27 +40,22 @@ export function patchTimer(window: any, setName: string, cancelName: string, nam
task.invoke.apply(this, arguments);
} finally {
if (typeof data.handleId === NUMBER) {
// Node returns complex objects as handleIds
// in non-nodejs env, we remove timerId
// from local cache
delete tasksByHandleId[data.handleId];
} else if (data.handleId) {
// Node returns complex objects as handleIds
// we remove task reference from timer object
(data.handleId as any)[taskSymbol] = null;
}
}
}
data.args[0] = timer;
data.handleId = setNative.apply(window, data.args);
if (typeof data.handleId === NUMBER) {
// Node returns complex objects as handleIds -> no need to keep them around. Additionally,
// this throws an
// exception in older node versions and has no effect there, because of the stringified key.
tasksByHandleId[data.handleId] = task;
}
return task;
}

function clearTask(task: Task) {
if (typeof(<TimerOptions>task.data).handleId === NUMBER) {
// Node returns complex objects as handleIds
delete tasksByHandleId[(<TimerOptions>task.data).handleId];
}
return clearNative((<TimerOptions>task.data).handleId);
}

Expand All @@ -78,13 +75,26 @@ export function patchTimer(window: any, setName: string, cancelName: string, nam
}
// Node.js must additionally support the ref and unref functions.
const handle: any = (<TimerOptions>task.data).handleId;
if (typeof handle === NUMBER) {
// for non nodejs env, we save handleId: task
// mapping in local cache for clearTimeout
tasksByHandleId[handle] = task;
} else if (handle) {
// for nodejs env, we save task
// reference in timerId Object for clearTimeout
handle[taskSymbol] = task;
}

// check whether handle is null, because some polyfill or browser
// may return undefined from setTimeout/setInterval/setImmediate/requestAnimationFrame
if (handle && handle.ref && handle.unref && typeof handle.ref === FUNCTION &&
typeof handle.unref === FUNCTION) {
(<any>task).ref = (<any>handle).ref.bind(handle);
(<any>task).unref = (<any>handle).unref.bind(handle);
}
if (typeof handle === NUMBER || handle) {
return handle;
}
return task;
} else {
// cause an error by calling it directly.
Expand All @@ -94,10 +104,27 @@ export function patchTimer(window: any, setName: string, cancelName: string, nam

clearNative =
patchMethod(window, cancelName, (delegate: Function) => function(self: any, args: any[]) {
const task: Task = typeof args[0] === NUMBER ? tasksByHandleId[args[0]] : args[0];
const id = args[0];
let task: Task;
if (typeof id === NUMBER) {
// non nodejs env.
task = tasksByHandleId[id];
} else {
// nodejs env.
task = id && id[taskSymbol];
// other environments.
if (!task) {
task = id;
}
}
if (task && typeof task.type === STRING) {
if (task.state !== NOT_SCHEDULED &&
(task.cancelFn && task.data.isPeriodic || task.runCount === 0)) {
if (typeof id === NUMBER) {
delete tasksByHandleId[id];
} else if (id) {
id[taskSymbol] = null;
}
// Do not cancel already canceled functions
task.zone.cancelTask(task);
}
Expand Down
31 changes: 12 additions & 19 deletions test/common/setInterval.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@ describe('setInterval', function() {
let cancelId: any;
const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
testZone.run(() => {
let id: number;
let intervalCount = 0;
let timeoutRunning = false;
const intervalFn = function() {
Expand All @@ -35,12 +34,14 @@ describe('setInterval', function() {
for (let i = 0; i < intervalCount; i++) {
intervalLog = intervalLog.concat(intervalUnitLog);
}
expect(wtfMock.log).toEqual([
'# Zone:fork("<root>::ProxyZone::WTF", "TestZone")',
'> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")',
'# Zone:schedule:macroTask:setInterval("<root>::ProxyZone::WTF::TestZone", ' + id + ')',
'< Zone:invoke:unit-test'
].concat(intervalLog));
expect(wtfMock.log[0]).toEqual('# Zone:fork("<root>::ProxyZone::WTF", "TestZone")');
expect(wtfMock.log[1])
.toEqual('> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")');
expect(wtfMock.log[2])
.toContain(
'# Zone:schedule:macroTask:setInterval("<root>::ProxyZone::WTF::TestZone"');
expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test');
expect(wtfMock.log.splice(4)).toEqual(intervalLog);
clearInterval(cancelId);
done();
});
Expand All @@ -52,18 +53,10 @@ describe('setInterval', function() {
expect(typeof cancelId.unref).toEqual(('function'));
}

// This icky replacer is to deal with Timers in node.js. The data.handleId contains timers in
// node.js. They do not stringify properly since they contain circular references.
id = JSON.stringify((<MacroTask>cancelId).data, function replaceTimer(key, value) {
if (key == 'handleId' && typeof value == 'object') return value.constructor.name;
if (typeof value === 'function') return value.name;
return value;
}) as any as number;
expect(wtfMock.log).toEqual([
'# Zone:fork("<root>::ProxyZone::WTF", "TestZone")',
'> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")',
'# Zone:schedule:macroTask:setInterval("<root>::ProxyZone::WTF::TestZone", ' + id + ')'
]);
expect(wtfMock.log[0]).toEqual('# Zone:fork("<root>::ProxyZone::WTF", "TestZone")');
expect(wtfMock.log[1]).toEqual('> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")');
expect(wtfMock.log[2])
.toContain('# Zone:schedule:macroTask:setInterval("<root>::ProxyZone::WTF::TestZone"');
}, null, null, 'unit-test');
});

Expand Down
42 changes: 16 additions & 26 deletions test/common/setTimeout.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,18 +14,18 @@ describe('setTimeout', function() {
let cancelId: any;
const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
testZone.run(() => {
let id: number;
const timeoutFn = function() {
expect(Zone.current.name).toEqual(('TestZone'));
global[zoneSymbol('setTimeout')](function() {
expect(wtfMock.log).toEqual([
'# Zone:fork("<root>::ProxyZone::WTF", "TestZone")',
'> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")',
'# Zone:schedule:macroTask:setTimeout("<root>::ProxyZone::WTF::TestZone", ' + id + ')',
'< Zone:invoke:unit-test',
'> Zone:invokeTask:setTimeout("<root>::ProxyZone::WTF::TestZone")',
'< Zone:invokeTask:setTimeout'
]);
expect(wtfMock.log[0]).toEqual('# Zone:fork("<root>::ProxyZone::WTF", "TestZone")');
expect(wtfMock.log[1])
.toEqual('> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")');
expect(wtfMock.log[2])
.toContain('# Zone:schedule:macroTask:setTimeout("<root>::ProxyZone::WTF::TestZone"');
expect(wtfMock.log[3]).toEqual('< Zone:invoke:unit-test');
expect(wtfMock.log[4])
.toEqual('> Zone:invokeTask:setTimeout("<root>::ProxyZone::WTF::TestZone")');
expect(wtfMock.log[5]).toEqual('< Zone:invokeTask:setTimeout');
done();
});
};
Expand All @@ -35,18 +35,10 @@ describe('setTimeout', function() {
expect(typeof cancelId.ref).toEqual(('function'));
expect(typeof cancelId.unref).toEqual(('function'));
}
// This icky replacer is to deal with Timers in node.js. The data.handleId contains timers in
// node.js. They do not stringify properly since they contain circular references.
id = JSON.stringify((<MacroTask>cancelId).data, function replaceTimer(key, value) {
if (key == 'handleId' && typeof value == 'object') return value.constructor.name;
if (typeof value === 'function') return value.name;
return value;
}) as any as number;
expect(wtfMock.log).toEqual([
'# Zone:fork("<root>::ProxyZone::WTF", "TestZone")',
'> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")',
'# Zone:schedule:macroTask:setTimeout("<root>::ProxyZone::WTF::TestZone", ' + id + ')'
]);
expect(wtfMock.log[0]).toEqual('# Zone:fork("<root>::ProxyZone::WTF", "TestZone")');
expect(wtfMock.log[1]).toEqual('> Zone:invoke:unit-test("<root>::ProxyZone::WTF::TestZone")');
expect(wtfMock.log[2])
.toContain('# Zone:schedule:macroTask:setTimeout("<root>::ProxyZone::WTF::TestZone"');
}, null, null, 'unit-test');
});

Expand Down Expand Up @@ -97,11 +89,11 @@ describe('setTimeout', function() {
});
});

it('should return the timeout Id through toString', function() {
it('should return the original timeout Id', function() {
// Node returns complex object from setTimeout, ignore this test.
if (isNode) return;
const cancelId = setTimeout(() => {}, 0);
expect(typeof(cancelId.toString())).toBe('number');
expect(typeof cancelId).toEqual('number');
});

it('should allow cancelation by numeric timeout Id', function(done) {
Expand All @@ -114,12 +106,10 @@ describe('setTimeout', function() {
const testZone = Zone.current.fork((Zone as any)['wtfZoneSpec']).fork({name: 'TestZone'});
testZone.run(() => {
const spy = jasmine.createSpy('spy');
const task: Task = <any>setTimeout(spy, 0);
const cancelId: number = <any>task;
const cancelId = setTimeout(spy, 0);
clearTimeout(cancelId);
setTimeout(function() {
expect(spy).not.toHaveBeenCalled();
expect(task.runCount).toEqual(0);
done();
}, 1);
});
Expand Down
2 changes: 1 addition & 1 deletion test/zone-spec/fake-async-test.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -193,7 +193,7 @@ describe('FakeAsyncTestZoneSpec', () => {
it('should not run cancelled timer', () => {
fakeAsyncTestZone.run(() => {
let ran = false;
let id = setTimeout(() => {
let id: any = setTimeout(() => {
ran = true;
}, 10);
clearTimeout(id);
Expand Down
12 changes: 6 additions & 6 deletions test/zone-spec/task-tracking.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,14 @@ describe('TaskTrackingZone', function() {

it('should track tasks', (done: Function) => {
taskTrackingZone.run(() => {
const microTask = taskTrackingZone.scheduleMicroTask('test1', () => {});
taskTrackingZone.scheduleMicroTask('test1', () => {});
expect(taskTrackingZoneSpec.microTasks.length).toBe(1);
expect(taskTrackingZoneSpec.microTasks[0].source).toBe('test1');

const macroTask = setTimeout(() => {}) as any as Task;
setTimeout(() => {});
expect(taskTrackingZoneSpec.macroTasks.length).toBe(1);
expect(taskTrackingZoneSpec.macroTasks[0].source).toBe('setTimeout');
taskTrackingZone.cancelTask(macroTask);
taskTrackingZone.cancelTask(taskTrackingZoneSpec.macroTasks[0]);
expect(taskTrackingZoneSpec.macroTasks.length).toBe(0);

setTimeout(() => {
Expand Down Expand Up @@ -71,10 +71,10 @@ describe('TaskTrackingZone', function() {

it('should capture task creation stacktrace', (done) => {
taskTrackingZone.run(() => {
const task: any = setTimeout(() => {
setTimeout(() => {
done();
}) as any as Task;
expect(task['creationLocation']).toBeTruthy();
});
expect((taskTrackingZoneSpec.macroTasks[0] as any)['creationLocation']).toBeTruthy();
});
});
});