Skip to content

Commit 548ec2a

Browse files
trxcllntbenlesh
authored andcommitted
fix(schedulers): fix asap and animationFrame schedulers to execute across async boundaries. (#1820)
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 548ec2a

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} animationFrame 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)