rxjs-zone-less
Version:
A set of wrappers for RxJS to avoid unnecessary change detection and zone interference in Angular.
214 lines (213 loc) • 9.63 kB
JavaScript
import { immediateProvider } from '../internals/immediateProvider';
import { dateTimestampProvider } from '../internals/date-time-stamp.provider';
import { intervalProvider } from '../internals/intervalProvider';
import { animationFrameProvider } from '../internals/animationFrameProvider';
import { TestScheduler } from 'rxjs/testing';
export class RxTestScheduler extends TestScheduler {
run(callback) {
try {
const ret = super.run((helpers) => {
const animator = this._createAnimator();
const delegates = this._createDelegates();
animationFrameProvider.delegate = animator.delegate;
intervalProvider.delegate = delegates.interval;
immediateProvider.delegate = delegates.immediate;
dateTimestampProvider.delegate = this;
const origAnimate = helpers.animate;
helpers.animate = (marbles) => {
animator.animate(marbles);
origAnimate(marbles);
};
return callback(helpers);
});
return ret;
}
finally {
animationFrameProvider.delegate = undefined;
intervalProvider.delegate = undefined;
immediateProvider.delegate = undefined;
dateTimestampProvider.delegate = undefined;
}
}
_createAnimator() {
if (!this.runMode) {
throw new Error('animate() must only be used in run mode');
}
// The TestScheduler assigns a delegate to the provider that's used for
// requestAnimationFrame (rAF). The delegate works in conjunction with the
// animate run helper to coordinate the invocation of any rAF callbacks,
// that are effected within tests, with the animation frames specified by
// the test's author - in the marbles that are passed to the animate run
// helper. This allows the test's author to write deterministic tests and
// gives the author full control over when - or if - animation frames are
// 'painted'.
let lastHandle = 0;
let map;
const delegate = {
requestAnimationFrame(callback) {
if (!map) {
throw new Error('animate() was not called within run()');
}
const handle = ++lastHandle;
map.set(handle, callback);
return handle;
},
cancelAnimationFrame(handle) {
if (!map) {
throw new Error('animate() was not called within run()');
}
map.delete(handle);
},
};
const animate = (marbles) => {
if (map) {
throw new Error('animate() must not be called more than once within run()');
}
if (/[|#]/.test(marbles)) {
throw new Error('animate() must not complete or error');
}
map = new Map();
const messages = TestScheduler.parseMarbles(marbles, undefined, undefined, undefined, true);
for (const message of messages) {
this.schedule(() => {
const now = this.now();
// Capture the callbacks within the queue and clear the queue
// before enumerating the callbacks, as callbacks might
// reschedule themselves. (And, yeah, we're using a Map to represent
// the queue, but the values are guaranteed to be returned in
// insertion order, so it's all good. Trust me, I've read the docs.)
const callbacks = Array.from(map.values());
map.clear();
for (const callback of callbacks) {
callback(now);
}
}, message.frame);
}
};
return { animate, delegate };
}
_createDelegates() {
// When in run mode, the TestScheduler provides alternate implementations
// of set/clearImmediate and set/clearInterval. These implementations are
// consumed by the scheduler implementations via the providers. This is
// done to effect deterministic asap and async scheduler behavior so that
// all of the schedulers are testable in 'run mode'. Prior to v7,
// delegation occurred at the scheduler level. That is, the asap and
// animation frame schedulers were identical in behavior to the async
// scheduler. Now, when in run mode, asap actions are prioritized over
// async actions and animation frame actions are coordinated using the
// animate run helper.
let lastHandle = 0;
const scheduleLookup = new Map();
const run = () => {
// Whenever a scheduled run is executed, it must run a single immediate
// or interval action - with immediate actions being prioritized over
// interval and timeout actions.
const now = this.now();
const scheduledRecords = Array.from(scheduleLookup.values());
const scheduledRecordsDue = scheduledRecords.filter(({ due }) => due <= now);
const dueImmediates = scheduledRecordsDue.filter(({ type }) => type === 'immediate');
if (dueImmediates.length > 0) {
const { handle, handler } = dueImmediates[0];
scheduleLookup.delete(handle);
handler();
return;
}
const dueIntervals = scheduledRecordsDue.filter(({ type }) => type === 'interval');
if (dueIntervals.length > 0) {
const firstDueInterval = dueIntervals[0];
const { duration, handler } = firstDueInterval;
firstDueInterval.due = now + duration;
// The interval delegate must behave like setInterval, so run needs to
// be rescheduled. This will continue until the clearInterval delegate
// unsubscribes and deletes the handle from the map.
firstDueInterval.subscription = this.schedule(run, duration);
handler();
return;
}
const dueTimeouts = scheduledRecordsDue.filter(({ type }) => type === 'timeout');
if (dueTimeouts.length > 0) {
const { handle, handler } = dueTimeouts[0];
scheduleLookup.delete(handle);
handler();
return;
}
throw new Error('Expected a due immediate or interval');
};
// The following objects are the delegates that replace conventional
// runtime implementations with TestScheduler implementations.
//
// The immediate delegate is depended upon by the asapScheduler.
//
// The interval delegate is depended upon by the asyncScheduler.
//
// The timeout delegate is not depended upon by any scheduler, but it's
// included here because the onUnhandledError and onStoppedNotification
// configuration points use setTimeout to avoid producer interference. It's
// inclusion allows for the testing of these configuration points.
const immediate = {
setImmediate: (handler) => {
const handle = ++lastHandle;
scheduleLookup.set(handle, {
due: this.now(),
duration: 0,
handle,
handler,
subscription: this.schedule(run, 0),
type: 'immediate',
});
return handle;
},
clearImmediate: (handle) => {
const value = scheduleLookup.get(handle);
if (value) {
value.subscription.unsubscribe();
scheduleLookup.delete(handle);
}
},
};
const interval = {
setInterval: (handler, duration = 0) => {
const handle = ++lastHandle;
scheduleLookup.set(handle, {
due: this.now() + duration,
duration,
handle,
handler,
subscription: this.schedule(run, duration),
type: 'interval',
});
return handle;
},
clearInterval: (handle) => {
const value = scheduleLookup.get(handle);
if (value) {
value.subscription.unsubscribe();
scheduleLookup.delete(handle);
}
},
};
const timeout = {
setTimeout: (handler, duration = 0) => {
const handle = ++lastHandle;
scheduleLookup.set(handle, {
due: this.now() + duration,
duration,
handle,
handler,
subscription: this.schedule(run, duration),
type: 'timeout',
});
return handle;
},
clearTimeout: (handle) => {
const value = scheduleLookup.get(handle);
if (value) {
value.subscription.unsubscribe();
scheduleLookup.delete(handle);
}
},
};
return { immediate, interval, timeout };
}
}