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