Skip to content

Commit 1bd2902

Browse files
committed
fix(schedulers): fix asap and animationFrame schedulers to execute across async boundaries.
The AsapScheduler and AnimationFrameSchedulers were totally busted. My bad. Now they execute their scheduled actions in batches. If actions reschedule while executing a batch, a new frame is requested for the rescheduled action to execute in. This PR also simplifies the public `Scheduler` and `Action` APIs. Implementation details like the `actions` queue and `active` boolean are now on the concrete implementations, so it's easier for people to implement the Scheduler API. This PR also renames `FutureAction` -> `AsyncAction` to conform to the same naming convention as the rest of the Action types. Fixes #1814
1 parent 4f65b03 commit 1bd2902

32 files changed

+735
-602
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@
9494
"prepublish": "shx rm -rf ./typings && typings install && npm run build_all",
9595
"publish_docs": "./publish_docs.sh",
9696
"test_mocha": "mocha --opts spec/support/default.opts spec-js",
97+
"debug_mocha": "node-debug _mocha --opts spec/support/debug.opts spec-js",
9798
"test_browser": "npm-run-all build_spec_browser && opn spec/support/mocha-browser-runner.html",
9899
"test": "npm-run-all clean_spec build_spec test_mocha clean_spec",
99100
"tests2png": "npm run build_spec && mkdirp tmp/docs/img && mkdirp spec-js/support && shx cp spec/support/*.opts spec-js/support/ && mocha --opts spec/support/tests2png.opts spec-js",

spec/Scheduler-spec.ts

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,21 @@ describe('Scheduler.queue', () => {
1919
expect(call2).to.be.true;
2020
});
2121

22+
it('should schedule things recursively via this.schedule', () => {
23+
let call1 = false;
24+
let call2 = false;
25+
Scheduler.queue.active = false;
26+
Scheduler.queue.schedule(function (state) {
27+
call1 = state.call1;
28+
call2 = state.call2;
29+
if (!call2) {
30+
this.schedule({ call1: true, call2: true });
31+
}
32+
}, 0, { call1: true, call2: false });
33+
expect(call1).to.be.true;
34+
expect(call2).to.be.true;
35+
});
36+
2237
it('should schedule things in the future too', (done: MochaDone) => {
2338
let called = false;
2439
Scheduler.queue.schedule(() => {
@@ -55,4 +70,4 @@ describe('Scheduler.queue', () => {
5570
});
5671
}, 0);
5772
});
58-
});
73+
});
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
import {expect} from 'chai';
2+
import * as Rx from '../../dist/cjs/Rx';
3+
4+
const animationFrame = Rx.Scheduler.animationFrame;
5+
6+
/** @test {Scheduler} */
7+
describe('Scheduler.animationFrame', () => {
8+
it('should exist', () => {
9+
expect(animationFrame).exist;
10+
});
11+
12+
it('should schedule an action to happen later', (done: MochaDone) => {
13+
let actionHappened = false;
14+
animationFrame.schedule(() => {
15+
actionHappened = true;
16+
done();
17+
});
18+
if (actionHappened) {
19+
done(new Error('Scheduled action happened synchronously'));
20+
}
21+
});
22+
23+
it('should execute recursively scheduled actions in separate asynchronous contexts', (done: MochaDone) => {
24+
let syncExec1 = true;
25+
let syncExec2 = true;
26+
animationFrame.schedule(function (index) {
27+
if (index === 0) {
28+
this.schedule(1);
29+
animationFrame.schedule(() => { syncExec1 = false; });
30+
} else if (index === 1) {
31+
this.schedule(2);
32+
animationFrame.schedule(() => { syncExec2 = false; });
33+
} else if (index === 2) {
34+
this.schedule(3);
35+
} else if (index === 3) {
36+
if (!syncExec1 && !syncExec2) {
37+
done();
38+
} else {
39+
done(new Error('Execution happened synchronously.'));
40+
}
41+
}
42+
}, 0, 0);
43+
});
44+
45+
it('should cancel the animation frame if all scheduled actions unsubscribe before it executes', (done: MochaDone) => {
46+
let animationFrameExec1 = false;
47+
let animationFrameExec2 = false;
48+
const action1 = animationFrame.schedule(() => { animationFrameExec1 = true; });
49+
const action2 = animationFrame.schedule(() => { animationFrameExec2 = true; });
50+
expect(animationFrame.scheduled).to.exist;
51+
expect(animationFrame.actions.length).to.equal(2);
52+
action1.unsubscribe();
53+
action2.unsubscribe();
54+
expect(animationFrame.actions.length).to.equal(0);
55+
expect(animationFrame.scheduled).to.equal(undefined);
56+
animationFrame.schedule(() => {
57+
expect(animationFrameExec1).to.equal(false);
58+
expect(animationFrameExec2).to.equal(false);
59+
done();
60+
});
61+
});
62+
63+
it('should execute the rest of the scheduled actions if the first action is canceled', (done: MochaDone) => {
64+
let actionHappened = false;
65+
let firstSubscription = null;
66+
let secondSubscription = null;
67+
68+
firstSubscription = animationFrame.schedule(() => {
69+
actionHappened = true;
70+
if (secondSubscription) {
71+
secondSubscription.unsubscribe();
72+
}
73+
done(new Error('The first action should not have executed.'));
74+
});
75+
76+
secondSubscription = animationFrame.schedule(() => {
77+
if (!actionHappened) {
78+
done();
79+
}
80+
});
81+
82+
if (actionHappened) {
83+
done(new Error('Scheduled action happened synchronously'));
84+
} else {
85+
firstSubscription.unsubscribe();
86+
}
87+
});
88+
});

spec/schedulers/AsapScheduler-spec.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,46 @@ describe('Scheduler.asap', () => {
2020
}
2121
});
2222

23+
it('should execute recursively scheduled actions in separate asynchronous contexts', (done: MochaDone) => {
24+
let syncExec1 = true;
25+
let syncExec2 = true;
26+
asap.schedule(function (index) {
27+
if (index === 0) {
28+
this.schedule(1);
29+
asap.schedule(() => { syncExec1 = false; });
30+
} else if (index === 1) {
31+
this.schedule(2);
32+
asap.schedule(() => { syncExec2 = false; });
33+
} else if (index === 2) {
34+
this.schedule(3);
35+
} else if (index === 3) {
36+
if (!syncExec1 && !syncExec2) {
37+
done();
38+
} else {
39+
done(new Error('Execution happened synchronously.'));
40+
}
41+
}
42+
}, 0, 0);
43+
});
44+
45+
it('should cancel the setImmediate if all scheduled actions unsubscribe before it executes', (done: MochaDone) => {
46+
let asapExec1 = false;
47+
let asapExec2 = false;
48+
const action1 = asap.schedule(() => { asapExec1 = true; });
49+
const action2 = asap.schedule(() => { asapExec2 = true; });
50+
expect(asap.scheduled).to.exist;
51+
expect(asap.actions.length).to.equal(2);
52+
action1.unsubscribe();
53+
action2.unsubscribe();
54+
expect(asap.actions.length).to.equal(0);
55+
expect(asap.scheduled).to.equal(undefined);
56+
asap.schedule(() => {
57+
expect(asapExec1).to.equal(false);
58+
expect(asapExec2).to.equal(false);
59+
done();
60+
});
61+
});
62+
2363
it('should execute the rest of the scheduled actions if the first action is canceled', (done: MochaDone) => {
2464
let actionHappened = false;
2565
let firstSubscription = null;
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import {expect} from 'chai';
2+
import * as Rx from '../../dist/cjs/Rx';
3+
4+
const Scheduler = Rx.Scheduler;
5+
const queue = Scheduler.queue;
6+
7+
/** @test {Scheduler} */
8+
describe('Scheduler.queue', () => {
9+
it('should switch from synchronous to asynchronous at will', (done: MochaDone) => {
10+
let lastExecTime = 0;
11+
let asyncExec = false;
12+
queue.schedule(function (index) {
13+
if (index === 0) {
14+
lastExecTime = queue.now();
15+
this.schedule(1, 100);
16+
} else if (index === 1) {
17+
if (queue.now() - lastExecTime < 100) {
18+
done(new Error('Execution happened synchronously.'));
19+
} else {
20+
asyncExec = true;
21+
lastExecTime = queue.now();
22+
this.schedule(2, 0);
23+
}
24+
} else if (index === 2) {
25+
if (asyncExec === false) {
26+
done(new Error('Execution happened synchronously.'));
27+
} else {
28+
done();
29+
}
30+
}
31+
}, 0, 0);
32+
asyncExec = false;
33+
});
34+
it('should unsubscribe the rest of the scheduled actions if an action throws an error', () => {
35+
const actions = [];
36+
let action2Exec = false;
37+
let action3Exec = false;
38+
let errorValue = undefined;
39+
try {
40+
queue.schedule(() => {
41+
actions.push(
42+
queue.schedule(() => { throw new Error('oops'); }),
43+
queue.schedule(() => { action2Exec = true; }),
44+
queue.schedule(() => { action3Exec = true; })
45+
);
46+
});
47+
} catch (e) {
48+
errorValue = e;
49+
}
50+
expect(actions.every((action) => action.isUnsubscribed)).to.be.true;
51+
expect(action2Exec).to.be.false;
52+
expect(action3Exec).to.be.false;
53+
expect(errorValue).exist;
54+
expect(errorValue.message).to.equal('oops');
55+
});
56+
});

spec/support/debug.opts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
--bail
99
--full-trace
1010
--check-leaks
11-
--globals WebSocket,FormData
11+
--globals WebSocket,FormData,XDomainRequest,ActiveXObject
1212

1313
--recursive
1414
--timeout 100000

src/MiscJSDoc.ts

Lines changed: 0 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,6 @@
88
import {Subscriber} from './Subscriber';
99
import {TeardownLogic} from './Subscription';
1010
import {Observable} from './Observable';
11-
import {Subscription} from './Subscription';
12-
import {Action} from './scheduler/Action';
1311
import './scheduler/MiscJSDoc';
1412
import './observable/dom/MiscJSDoc';
1513

@@ -130,90 +128,3 @@ export class ObserverDoc<T> {
130128
return void 0;
131129
}
132130
}
133-
134-
/**
135-
* An execution context and a data structure to order tasks and schedule their
136-
* execution. Provides a notion of (potentially virtual) time, through the
137-
* `now()` getter method.
138-
*
139-
* Each unit of work in a Scheduler is called an {@link Action}.
140-
*
141-
* ```ts
142-
* interface Scheduler {
143-
* now(): number;
144-
* schedule(work, delay?, state?): Subscription;
145-
* flush(): void;
146-
* active: boolean;
147-
* actions: Action[];
148-
* scheduledId: number;
149-
* }
150-
* ```
151-
*
152-
* @interface
153-
* @name Scheduler
154-
* @noimport true
155-
*/
156-
export class SchedulerDoc {
157-
/**
158-
* A getter method that returns a number representing the current time
159-
* (at the time this function was called) according to the scheduler's own
160-
* internal clock.
161-
* @return {number} A number that represents the current time. May or may not
162-
* have a relation to wall-clock time. May or may not refer to a time unit
163-
* (e.g. milliseconds).
164-
*/
165-
now(): number {
166-
return 0;
167-
}
168-
169-
/**
170-
* Schedules a function, `work`, for execution. May happen at some point in
171-
* the future, according to the `delay` parameter, if specified. May be passed
172-
* some context object, `state`, which will be passed to the `work` function.
173-
*
174-
* The given arguments will be processed an stored as an Action object in a
175-
* queue of actions.
176-
*
177-
* @param {function(state: ?T): ?Subscription} work A function representing a
178-
* task, or some unit of work to be executed by the Scheduler.
179-
* @param {number} [delay] Time to wait before executing the work, where the
180-
* time unit is implicit and defined by the Scheduler itself.
181-
* @param {T} [state] Some contextual data that the `work` function uses when
182-
* called by the Scheduler.
183-
* @return {Subscription} A subscription in order to be able to unsubscribe
184-
* the scheduled work.
185-
*/
186-
schedule<T>(work: (state?: T) => Subscription | void, delay?: number, state?: T): Subscription {
187-
return void 0;
188-
}
189-
190-
/**
191-
* Prompt the Scheduler to execute all of its queued actions, therefore
192-
* clearing its queue.
193-
* @return {void}
194-
*/
195-
flush(): void {
196-
return void 0;
197-
}
198-
199-
/**
200-
* A flag to indicate whether the Scheduler is currently executing a batch of
201-
* queued actions.
202-
* @type {boolean}
203-
*/
204-
active: boolean = false;
205-
206-
/**
207-
* The queue of scheduled actions as an array.
208-
* @type {Action[]}
209-
*/
210-
actions: Action<any>[] = [];
211-
212-
/**
213-
* An internal ID used to track the latest asynchronous task such as those
214-
* coming from `setTimeout`, `setInterval`, `requestAnimationFrame`, and
215-
* others.
216-
* @type {number}
217-
*/
218-
scheduledId: number = 0;
219-
}

src/Rx.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,8 @@ import observable from 'symbol-observable';
184184
* asynchronous conversions.
185185
* @property {Scheduler} async Schedules work with `setInterval`. Use this for
186186
* time-based operations.
187+
* @property {Scheduler} animation Schedules work with `requestAnimationFrame`.
188+
* Use this for synchronizing with the platform's painting
187189
*/
188190
let Scheduler = {
189191
asap,

0 commit comments

Comments
 (0)