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()