-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy pathmergebounce.js
221 lines (200 loc) · 7.37 KB
/
mergebounce.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
import isObject from 'lodash-es/isObject.js';
import merge from 'lodash-es/merge.js';
import mergeWith from 'lodash-es/mergeWith.js';
import now from 'lodash-es/now.js';
import toNumber from 'lodash-es/toNumber.js';
import { getOpenPromise } from '@converse/openpromise/openpromise.js';
/** Error message constants. */
const FUNC_ERROR_TEXT = 'Expected a function';
/* Built-in method references for those with the same name as other `lodash` methods. */
const nativeMax = Math.max;
const nativeMin = Math.min;
/**
* Creates a debounced function that delays invoking `func` until after `wait`
* milliseconds have elapsed since the last time the debounced function was
* invoked. The debounced function comes with a `cancel` method to cancel
* delayed `func` invocations and a `flush` method to immediately invoke them.
*
* This function differs from lodash's debounce by merging all passed objects
* before passing them to the final invoked function.
*
* Because of this, invoking can only happen on the trailing edge, since
* passed-in data would be discarded if invoking happened on the leading edge.
*
* If `wait` is `0`, `func` invocation is deferred until to the next tick,
* similar to `setTimeout` with a timeout of `0`.
*
* @static
* @category Function
* @param {Function} func The function to mergebounce.
* @param {number} [wait=0] The number of milliseconds to delay.
* @param {Object} [options={}] The options object.
* @param {number} [options.maxWait]
* The maximum time `func` is allowed to be delayed before it's invoked.
* @param {boolean} [options.concatArrays=false]
* By default arrays will be treated as objects when being merged. When
* merging two arrays, the values in the 2nd arrray will replace the
* corresponding values (i.e. those with the same indexes) in the first array.
* When `concatArrays` is set to `true`, arrays will be concatenated instead.
* @param {boolean} [options.dedupeArrays=false]
* This option is similar to `concatArrays`, except that the concatenated
* array will also be deduplicated. Thus any entries that are concatenated to the
* existing array, which are already contained in the existing array, will
* first be removed.
* @param {boolean} [options.promise=false]
* By default, when calling a merge-debounced function that doesn't execute
* immediately, you'll receive the result from its previous execution, or
* `undefined` if it has never executed before. By setting the `promise`
* option to `true`, a promise will be returned instead of the previous
* execution result when the function is debounced. The promise will resolve
* with the result of the next execution, as soon as it happens.
* @returns {Function} Returns the new debounced function.
* @example
*
* // Avoid costly calculations while the window size is in flux.
* window.addEventListener('resize', mergebounce(calculateLayout, 150));
*
* // Invoke `sendMail` when clicked, debouncing subsequent calls.
* element.addEventListner('click', mergebounce(sendMail, 300));
*
* // Ensure `batchLog` is invoked once after 1 second of debounced calls.
* const mergebounced = mergebounce(batchLog, 250, { 'maxWait': 1000 });
* const source = new EventSource('/stream');
* jQuery(source).on('message', mergebounced);
*
* // Cancel the trailing debounced invocation.
* window.addEventListener('popstate', mergebounced.cancel);
*/
function mergebounce(func, wait, options={}) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
maxing = false;
let promise = options.promise ? getOpenPromise() : null;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
wait = toNumber(wait) || 0;
if (isObject(options)) {
maxing = 'maxWait' in options;
maxWait = maxing ? nativeMax(toNumber(options.maxWait) || 0, wait) : maxWait;
}
function invokeFunc(time) {
const args = lastArgs;
const thisArg = lastThis;
const existingPromise = promise;
lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
if (options.promise) {
existingPromise.resolve(result);
promise = getOpenPromise();
}
return options.promise ? existingPromise : result;
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time;
// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
return options.promise ? promise : result;
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
const timeWaiting = wait - timeSinceLastCall;
return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime;
const timeSinceLastInvoke = time - lastInvokeTime;
// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait));
}
function timerExpired() {
const time = now();
if (shouldInvoke(time)) {
return trailingEdge(time);
}
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}
function trailingEdge(time) {
timerId = undefined;
// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return options.promise ? promise : result;
}
function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}
function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
function concatArrays(objValue, srcValue) {
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
if (options?.dedupeArrays) {
return objValue.concat(srcValue.filter(i => objValue.indexOf(i) === -1));
} else {
return objValue.concat(srcValue);
}
}
}
function mergeArguments(args) {
if (lastArgs?.length) {
if (!args.length) {
return lastArgs;
}
if (options?.concatArrays || options?.dedupeArrays) {
return mergeWith(lastArgs, args, concatArrays);
} else {
return merge(lastArgs, args);
}
} else {
return args || [];
}
}
function debounced() {
const time = now();
const isInvoking = shouldInvoke(time);
lastArgs = mergeArguments(Array.from(arguments));
lastThis = this;
lastCallTime = time;
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}
if (maxing) {
// Handle invocations in a tight loop.
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return options.promise ? promise : result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}
export default mergebounce;