main
ts 101 lines 4.83 KB
Raw
1 // This file is part of HFS - Copyright 2021-2023, Massimo Melina <a@rejetto.com> - License https://www.gnu.org/licenses/gpl-3.0.txt
2
3 // like lodash.debounce, but also avoids async invocations to overlap
4 export function debounceAsync<Cancelable extends boolean = false, A extends unknown[] = unknown[], R = unknown>(
5 // the function you want to not call too often, too soon
6 callback: (...args: A) => Promise<R>,
7 options: {
8 // time to wait after invocation of the debounced function. If you call again while waiting, the timer starts again.
9 wait?: number,
10 // since the wait-ing is renewed at each invocation, indefinitely, do you want to put a cap to it?
11 maxWait?: number,
12 // in a train of invocations, should we execute also the first one, or just the last one?
13 leading?: boolean,
14 // for how long do you want to cache last success value, and return that at next invocation?
15 retain?: number,
16 // for how long do you want to cache last failure value, and return that at next invocation?
17 retainFailure?: number,
18 // if a call is overlapping another, return the same promise, instead of queuing. It's automatically on if you have retain or retainFailure.
19 reuseRunning?: boolean,
20 // should we offer a cancel method to the returned function? if we do, the awaited-type will include undefined
21 cancelable?: Cancelable
22 } = {}
23 ) {
24 type MaybeUndefined<T> = Cancelable extends true ? undefined | T : T
25 type MaybeR = MaybeUndefined<R>
26 const { wait=0, leading=false, maxWait=Infinity, cancelable=false, retain=0, retainFailure, reuseRunning=Boolean(retain || retainFailure) } = options
27 let started = 0 // latest callback invocation
28 let runningCallback: Promise<R> | undefined // latest callback invocation result
29 let latestDebouncer: Promise<MaybeR | R> // latest wrapper invocation
30 let waitingSince = 0 // we are delaying invocation since
31 let whoIsWaiting: undefined | A // args object identifies the pending instance, and incidentally stores args
32 let latestCallback: typeof runningCallback
33 let latestHasFailed = false
34 let latestTimestamp = 0
35 const interceptingWrapper = (...args: A) => latestDebouncer = debouncer(...args)
36 return Object.assign(interceptingWrapper, {
37 clearRetain: () => latestCallback = undefined,
38 flush: () => runningCallback ?? exec(),
39 isWorking: () => runningCallback,
40 ...cancelable && {
41 cancel() {
42 waitingSince = 0
43 whoIsWaiting = undefined
44 }
45 }
46 })
47
48 async function debouncer(...args: A) {
49 if (reuseRunning && runningCallback)
50 return runningCallback as MaybeR
51 const now = Date.now()
52 if (latestCallback && now - latestTimestamp < (latestHasFailed ? retainFailure ?? retain : retain))
53 return await latestCallback
54 whoIsWaiting = args
55 waitingSince ||= now
56 const waitingCap = maxWait - (now - (waitingSince || started))
57 const waitFor = Math.min(waitingCap, leading ? wait - (now - started) : wait)
58 if (waitFor > 0)
59 await new Promise(resolve => setTimeout(resolve, waitFor))
60 if (!whoIsWaiting) { // canceled
61 waitingSince = 0
62 return undefined as MaybeR
63 }
64 if (whoIsWaiting !== args) // another fresher call is waiting
65 return latestDebouncer
66 await runningCallback // in case we don't reuseRunning
67 return exec()
68 }
69
70 async function exec() {
71 if (!whoIsWaiting) return undefined as MaybeR
72 waitingSince = 0
73 started = Date.now()
74 try {
75 const args = whoIsWaiting
76 whoIsWaiting = undefined
77 runningCallback = Promise.resolve(callback(...args)) // cast to promise, in case callback was not really async (or hybrid)
78 runningCallback.then(() => latestHasFailed = false, () => latestHasFailed = true)
79 return await runningCallback as MaybeUndefined<R> // await necessary to go-finally at the right time and even on exceptions
80 }
81 finally {
82 latestCallback = runningCallback
83 latestTimestamp = Date.now()
84 runningCallback = undefined
85 }
86 }
87 }
88
89 // given a function that works on a batch of requests, returns the function that works on a single request
90 export function singleWorkerFromBatchWorker<Args extends any[]>(batchWorker: (batch: Args[]) => unknown, { maxWait=Infinity }={}) {
91 let batch: Args[] = []
92 const debounced = debounceAsync(async () => {
93 const ret = batchWorker(batch)
94 batch = [] // this is reset as batchWorker starts, but without waiting
95 return ret
96 }, { wait: 100, maxWait })
97 return (...args: Args) => {
98 const idx = batch.push(args) - 1
99 return debounced().then((x: any) => x[idx])
100 }
101 }