sounder debounceAsync function
Massimo Melina committed
Mar 30, 2022 at 11:19 UTC
0a3463b70ef3c7371badb1718a45b57ad78f2e03
2 files changed
+39
-12
server/src/misc.ts
+38
-11
@@ -158,17 +158,44 @@ export function onOffMap(em: EventEmitter, events: { [eventName:string]: (...arg
158
}
159
}
160
161
-// avoid for an async function to be overlapped with another execution while awaiting
162
-export function debounceAsync(cb: any, ms: number=100, ...args:any[]) {
163
- const debounced = _.debounce(cb, ms, ...args)
164
- let running: false | number = false
165
- return async () => {
166
- if (running && Date.now() - running < ms) return
167
- while (running)
168
- await wait(ms)
169
- running = Date.now()
170
- try { return await debounced() }
171
- finally { running = false }
161
+// like lodash.debounce, but also avoids async invocations to overlap
162
+export function debounceAsync<CB extends (...args: any[]) => Promise<R>, R>(
163
+ callback: CB,
164
+ wait: number=100,
165
+ { leading=false, maxWait=Infinity }={}
166
+) {
167
+ let started = 0 // latest callback invocation
168
+ let runningCallback: Promise<R> | undefined // latest callback invocation result
169
+ let runningDebouncer: Promise<R | undefined> // latest wrapper invocation
170
+ let waitingSince = 0 // we are delaying invocation since
171
+ let whoIsWaiting: undefined | any[] // args' array object identifies the pending instance, and incidentally stores args
172
+ const interceptingWrapper = (...args:any[]) => runningDebouncer = debouncer.apply(null, args)
173
+ return Object.assign(interceptingWrapper, {
174
+ cancel: () => whoIsWaiting = undefined,
175
+ flush() {
176
+ return whoIsWaiting ? callback.apply(null, whoIsWaiting) : this.cancel()
177
+ },
178
+ })
179
+
180
+ async function debouncer(...args:any[]) {
181
+ whoIsWaiting = args
182
+ waitingSince ||= Date.now()
183
+ await runningCallback
184
+ const waitingCap = maxWait - (Date.now() - (waitingSince || started))
185
+ const waitFor = Math.min(waitingCap, leading ? wait - (Date.now() - started) : wait)
186
+ if (waitFor > 0)
187
+ await new Promise(resolve => setTimeout(resolve, waitFor))
188
+ if (!whoIsWaiting) // canceled
189
+ return void(waitingSince = 0)
190
+ if (whoIsWaiting !== args) return // another fresher call is waiting
191
+ waitingSince = 0
192
+ whoIsWaiting = undefined
193
+ started = Date.now()
194
+ try {
195
+ runningCallback = callback.apply(null, args)
196
+ return await runningCallback
197
+ }
198
+ finally { runningCallback = undefined }
199
}
200
}
201
server/src/watchLoad.ts
+1
-1
@@ -14,7 +14,7 @@ interface WatchLoadReturn { unwatch:WatchLoadCanceller, save:WriteFile }
14
export function watchLoad(path:string, parser:(data:any)=>void|Promise<void>, { failedOnFirstAttempt }:Options={}): WatchLoadReturn {
15
let doing = false
16
let watcher: FSWatcher | undefined
17
- const debounced = debounceAsync(load, 500)
17
+ const debounced = debounceAsync(load, 500, { leading: true })
18
let retry: NodeJS.Timeout
19
let saving: Promise<unknown> | undefined
20
init()