1 | import { Action } from './Action';
|
---|
2 | import { SchedulerAction } from '../types';
|
---|
3 | import { Subscription } from '../Subscription';
|
---|
4 | import { AsyncScheduler } from './AsyncScheduler';
|
---|
5 |
|
---|
6 | /**
|
---|
7 | * We need this JSDoc comment for affecting ESDoc.
|
---|
8 | * @ignore
|
---|
9 | * @extends {Ignored}
|
---|
10 | */
|
---|
11 | export class AsyncAction<T> extends Action<T> {
|
---|
12 |
|
---|
13 | public id: any;
|
---|
14 | public state: T;
|
---|
15 | public delay: number;
|
---|
16 | protected pending: boolean = false;
|
---|
17 |
|
---|
18 | constructor(protected scheduler: AsyncScheduler,
|
---|
19 | protected work: (this: SchedulerAction<T>, state?: T) => void) {
|
---|
20 | super(scheduler, work);
|
---|
21 | }
|
---|
22 |
|
---|
23 | public schedule(state?: T, delay: number = 0): Subscription {
|
---|
24 |
|
---|
25 | if (this.closed) {
|
---|
26 | return this;
|
---|
27 | }
|
---|
28 |
|
---|
29 | // Always replace the current state with the new state.
|
---|
30 | this.state = state;
|
---|
31 |
|
---|
32 | const id = this.id;
|
---|
33 | const scheduler = this.scheduler;
|
---|
34 |
|
---|
35 | //
|
---|
36 | // Important implementation note:
|
---|
37 | //
|
---|
38 | // Actions only execute once by default, unless rescheduled from within the
|
---|
39 | // scheduled callback. This allows us to implement single and repeat
|
---|
40 | // actions via the same code path, without adding API surface area, as well
|
---|
41 | // as mimic traditional recursion but across asynchronous boundaries.
|
---|
42 | //
|
---|
43 | // However, JS runtimes and timers distinguish between intervals achieved by
|
---|
44 | // serial `setTimeout` calls vs. a single `setInterval` call. An interval of
|
---|
45 | // serial `setTimeout` calls can be individually delayed, which delays
|
---|
46 | // scheduling the next `setTimeout`, and so on. `setInterval` attempts to
|
---|
47 | // guarantee the interval callback will be invoked more precisely to the
|
---|
48 | // interval period, regardless of load.
|
---|
49 | //
|
---|
50 | // Therefore, we use `setInterval` to schedule single and repeat actions.
|
---|
51 | // If the action reschedules itself with the same delay, the interval is not
|
---|
52 | // canceled. If the action doesn't reschedule, or reschedules with a
|
---|
53 | // different delay, the interval will be canceled after scheduled callback
|
---|
54 | // execution.
|
---|
55 | //
|
---|
56 | if (id != null) {
|
---|
57 | this.id = this.recycleAsyncId(scheduler, id, delay);
|
---|
58 | }
|
---|
59 |
|
---|
60 | // Set the pending flag indicating that this action has been scheduled, or
|
---|
61 | // has recursively rescheduled itself.
|
---|
62 | this.pending = true;
|
---|
63 |
|
---|
64 | this.delay = delay;
|
---|
65 | // If this action has already an async Id, don't request a new one.
|
---|
66 | this.id = this.id || this.requestAsyncId(scheduler, this.id, delay);
|
---|
67 |
|
---|
68 | return this;
|
---|
69 | }
|
---|
70 |
|
---|
71 | protected requestAsyncId(scheduler: AsyncScheduler, id?: any, delay: number = 0): any {
|
---|
72 | return setInterval(scheduler.flush.bind(scheduler, this), delay);
|
---|
73 | }
|
---|
74 |
|
---|
75 | protected recycleAsyncId(scheduler: AsyncScheduler, id: any, delay: number = 0): any {
|
---|
76 | // If this action is rescheduled with the same delay time, don't clear the interval id.
|
---|
77 | if (delay !== null && this.delay === delay && this.pending === false) {
|
---|
78 | return id;
|
---|
79 | }
|
---|
80 | // Otherwise, if the action's delay time is different from the current delay,
|
---|
81 | // or the action has been rescheduled before it's executed, clear the interval id
|
---|
82 | clearInterval(id);
|
---|
83 | return undefined;
|
---|
84 | }
|
---|
85 |
|
---|
86 | /**
|
---|
87 | * Immediately executes this action and the `work` it contains.
|
---|
88 | * @return {any}
|
---|
89 | */
|
---|
90 | public execute(state: T, delay: number): any {
|
---|
91 |
|
---|
92 | if (this.closed) {
|
---|
93 | return new Error('executing a cancelled action');
|
---|
94 | }
|
---|
95 |
|
---|
96 | this.pending = false;
|
---|
97 | const error = this._execute(state, delay);
|
---|
98 | if (error) {
|
---|
99 | return error;
|
---|
100 | } else if (this.pending === false && this.id != null) {
|
---|
101 | // Dequeue if the action didn't reschedule itself. Don't call
|
---|
102 | // unsubscribe(), because the action could reschedule later.
|
---|
103 | // For example:
|
---|
104 | // ```
|
---|
105 | // scheduler.schedule(function doWork(counter) {
|
---|
106 | // /* ... I'm a busy worker bee ... */
|
---|
107 | // var originalAction = this;
|
---|
108 | // /* wait 100ms before rescheduling the action */
|
---|
109 | // setTimeout(function () {
|
---|
110 | // originalAction.schedule(counter + 1);
|
---|
111 | // }, 100);
|
---|
112 | // }, 1000);
|
---|
113 | // ```
|
---|
114 | this.id = this.recycleAsyncId(this.scheduler, this.id, null);
|
---|
115 | }
|
---|
116 | }
|
---|
117 |
|
---|
118 | protected _execute(state: T, delay: number): any {
|
---|
119 | let errored: boolean = false;
|
---|
120 | let errorValue: any = undefined;
|
---|
121 | try {
|
---|
122 | this.work(state);
|
---|
123 | } catch (e) {
|
---|
124 | errored = true;
|
---|
125 | errorValue = !!e && e || new Error(e);
|
---|
126 | }
|
---|
127 | if (errored) {
|
---|
128 | this.unsubscribe();
|
---|
129 | return errorValue;
|
---|
130 | }
|
---|
131 | }
|
---|
132 |
|
---|
133 | /** @deprecated This is an internal implementation detail, do not use. */
|
---|
134 | _unsubscribe() {
|
---|
135 |
|
---|
136 | const id = this.id;
|
---|
137 | const scheduler = this.scheduler;
|
---|
138 | const actions = scheduler.actions;
|
---|
139 | const index = actions.indexOf(this);
|
---|
140 |
|
---|
141 | this.work = null;
|
---|
142 | this.state = null;
|
---|
143 | this.pending = false;
|
---|
144 | this.scheduler = null;
|
---|
145 |
|
---|
146 | if (index !== -1) {
|
---|
147 | actions.splice(index, 1);
|
---|
148 | }
|
---|
149 |
|
---|
150 | if (id != null) {
|
---|
151 | this.id = this.recycleAsyncId(scheduler, id, null);
|
---|
152 | }
|
---|
153 |
|
---|
154 | this.delay = null;
|
---|
155 | }
|
---|
156 | }
|
---|