-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathdebounce.js
276 lines (250 loc) · 7.84 KB
/
debounce.js
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
const DEFAULT_WAIT = 300;
const DEFAULT_WAIT_BETWEEN = 0;
const DEFAULT_PROP = '$wasyncDebounce';
const INACTIVE = 1;
const WAITING = 2;
const RUNNING = 3;
/**
* Implements the Async Debounce pattern.
*
* It helps you call asynchronous functions only once at a time.
*
* See the README
*
* @param wait {number} Milliseconds to wait before running
* @param waitBetween {number} Milliseconds to wait between two consecutive
* runs
* @constructor
*/
export function Debounce(
{
wait = DEFAULT_WAIT,
waitBetween = DEFAULT_WAIT_BETWEEN,
} = {},
) {
const self = this;
self.state = INACTIVE;
self.next = undefined;
/**
* Generates an asynchronously debounced function
*
* @param validate {function} Validation function. If the return value is
* false-ish then the operation is canceled,
* otherwise the return value is passed as first
* parameter of run()
* @param prepare {function} Immediately invoked in order to prepare the
* UI for the imminent load (like enable the
* loader)
* @param run {function} The function that does the action. It can return
* a promise. It will be called with the output of
* validate() after wait milliseconds
* @param success {function} Called with the value returned/resolved by
* run() in case of success
* @param failure {function} Called with the exception/rejection value
* raised by run()
* @param cleanup {function} Does the opposite of prepare(), when all
* is finished
* @returns {Function} Returned function is a composition of all provided
* hooks, in such a way that each of the hook is called
* exactly when it should be (according to the logic
* explained in the README).
*/
self.func = function ({
validate,
prepare,
run,
success,
failure,
cleanup,
}) {
if (!run) {
throw new Error(
'"run" function is not defined! That is the point of this '
+ 'Debounce class, so you probably are doing something wrong',
);
}
return function () {
const stack = {
this: this,
args: arguments,
hooks: {
validate,
prepare,
run,
success,
failure,
cleanup,
},
};
stack.params = self.validate(stack);
if (!stack.params) {
return;
}
if (self.state === INACTIVE) {
self.prepare(stack);
}
self.next = stack;
if (self.state === INACTIVE) {
self.wait();
}
};
};
/**
* Runs the validation hook. If no hook is provided then just return an
* empty object.
*
* @private
* @param validate {function|undefined} Validation hook
* @param this_ {object} Object to bind the validation hook to
* @param args {array} Arguments to call the function with
*/
self.validate = function ({hooks: {validate}, this: this_, args}) {
let v = {};
if (validate) {
v = validate.apply(this_, args);
}
return v;
};
/**
* Runs the prepare hook, if defined
*
* @private
* @param prepare {function|undefined} Prepare hook
* @param this_ {object} Object to bind the prepare hook to
*/
self.prepare = function ({hooks: {prepare}, this: this_}) {
if (prepare) {
prepare.apply(this_);
}
};
/**
* Go into waiting state and call the run function after the given amount
* of time. Expects to be called from the "INACTIVE" state.
*
* @private
*/
self.wait = function () {
self.state = WAITING;
setTimeout(function () {
self.run();
}, wait);
};
/**
* Runs the run hook, wait for the result and then trigger the finishing
* sequence (re-run if new run available, otherwise success/failure hooks
* then cleanup).
*
* It is expected that the run hook might not always return a Promise, or
* might just return a Promise-like but with not the exact API we need. For
* this reason, it passes through Promise.resolve()/Promise.reject().
*
* @private
*/
self.run = function () {
self.state = RUNNING;
const stack = this.next;
const {
this: this_,
params,
hooks: {
run,
success,
failure,
cleanup,
},
} = stack;
let prom;
try {
prom = Promise.resolve(run.call(this_, params));
} catch (e) {
prom = Promise.reject(e);
}
prom
.then(function () {
if (success) {
success.apply(this_, arguments);
}
})
.catch(function () {
if (failure) {
failure.apply(this_, arguments);
}
})
.finally(() => {
if (self.next === stack) {
try {
if (cleanup) {
cleanup.apply(this_);
}
} finally {
self.state = INACTIVE;
}
} else {
setTimeout(() => self.run(), waitBetween);
}
});
};
}
/**
* Independently of how you define your "classes" in JS ('cause you got a hell
* lot of options there), if you use Debounce.func() directly you'll only get
* one instance of Debounce. Which means that if you have two instances of your
* "object" at once then they are going to conflict and this is going to cause
* trouble.
*
* This is then a convenience class that will automatically create a new
* instance every time the calling "this" changes. The instance will be added
* as a property to "this" so it can be retrieved by further calls.
*
* @param wait {number} Milliseconds to wait before running
* @param waitBetween {number} Milliseconds to wait between two consecutive
* runs
* @param prop {string} Name of the property to store the Debounce instance
* into
* @constructor
*/
export function ObjectDebounce(
{
wait = DEFAULT_WAIT,
waitBetween = DEFAULT_WAIT_BETWEEN,
prop = DEFAULT_PROP,
} = {},
) {
const self = this;
/**
* Wrapper around Debounce.func() that will automatically handle Debounce
* instances.
*
* @param validate {function}
* @param prepare {function}
* @param run {function}
* @param success {function}
* @param failure {function}
* @param cleanup {function}
* @returns {function(): *}
*/
self.func = function ({
validate,
prepare,
run,
success,
failure,
cleanup,
}) {
return function () {
if (!this[prop]) {
this[prop] = new Debounce({wait, waitBetween});
}
const deb = this[prop];
const func = deb.func({
validate,
prepare,
run,
success,
failure,
cleanup,
});
return func.apply(this, arguments);
};
};
}