import { AsyncAction } from './AsyncAction'; import { Subscription } from '../Subscription'; import { AsyncScheduler } from './AsyncScheduler'; import { SchedulerAction } from '../types'; import { TimerHandle } from './timerHandle'; export class VirtualTimeScheduler extends AsyncScheduler { /** @deprecated Not used in VirtualTimeScheduler directly. Will be removed in v8. */ static frameTimeFactor = 10; /** * The current frame for the state of the virtual scheduler instance. The difference * between two "frames" is synonymous with the passage of "virtual time units". So if * you record `scheduler.frame` to be `1`, then later, observe `scheduler.frame` to be at `11`, * that means `10` virtual time units have passed. */ public frame: number = 0; /** * Used internally to examine the current virtual action index being processed. * @deprecated Internal implementation detail, do not use directly. Will be made internal in v8. */ public index: number = -1; /** * This creates an instance of a `VirtualTimeScheduler`. Experts only. The signature of * this constructor is likely to change in the long run. * * @param schedulerActionCtor The type of Action to initialize when initializing actions during scheduling. * @param maxFrames The maximum number of frames to process before stopping. Used to prevent endless flush cycles. */ constructor(schedulerActionCtor: typeof AsyncAction = VirtualAction as any, public maxFrames: number = Infinity) { super(schedulerActionCtor, () => this.frame); } /** * Prompt the Scheduler to execute all of its queued actions, therefore * clearing its queue. * @return {void} */ public flush(): void { const { actions, maxFrames } = this; let error: any; let action: AsyncAction | undefined; while ((action = actions[0]) && action.delay <= maxFrames) { actions.shift(); this.frame = action.delay; if ((error = action.execute(action.state, action.delay))) { break; } } if (error) { while ((action = actions.shift())) { action.unsubscribe(); } throw error; } } } export class VirtualAction extends AsyncAction { protected active: boolean = true; constructor( protected scheduler: VirtualTimeScheduler, protected work: (this: SchedulerAction, state?: T) => void, protected index: number = (scheduler.index += 1) ) { super(scheduler, work); this.index = scheduler.index = index; } public schedule(state?: T, delay: number = 0): Subscription { if (Number.isFinite(delay)) { if (!this.id) { return super.schedule(state, delay); } this.active = false; // If an action is rescheduled, we save allocations by mutating its state, // pushing it to the end of the scheduler queue, and recycling the action. // But since the VirtualTimeScheduler is used for testing, VirtualActions // must be immutable so they can be inspected later. const action = new VirtualAction(this.scheduler, this.work); this.add(action); return action.schedule(state, delay); } else { // If someone schedules something with Infinity, it'll never happen. So we // don't even schedule it. return Subscription.EMPTY; } } protected requestAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay: number = 0): TimerHandle { this.delay = scheduler.frame + delay; const { actions } = scheduler; actions.push(this); (actions as Array>).sort(VirtualAction.sortActions); return 1; } protected recycleAsyncId(scheduler: VirtualTimeScheduler, id?: any, delay: number = 0): TimerHandle | undefined { return undefined; } protected _execute(state: T, delay: number): any { if (this.active === true) { return super._execute(state, delay); } } private static sortActions(a: VirtualAction, b: VirtualAction) { if (a.delay === b.delay) { if (a.index === b.index) { return 0; } else if (a.index > b.index) { return 1; } else { return -1; } } else if (a.delay > b.delay) { return 1; } else { return -1; } } }