-
Notifications
You must be signed in to change notification settings - Fork 18
/
useSingleFlight.ts
67 lines (65 loc) · 2.39 KB
/
useSingleFlight.ts
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
import { useCallback, useRef } from 'react';
/**
* Wraps a function to single-flight invocations, using the latest args.
*
* Generates a function that behaves like the passed in function,
* but only one execution runs at a time. If multiple calls are requested
* before the current call has finished, it will use the latest arguments
* for the next invocation.
*
* Note: some requests may never be made. If while a request is in-flight, N
* requests are made, N-1 of them will never resolve or reject the promise they
* returned. For most applications this is the desired behavior, but if you need
* all calls to eventually resolve, you can modify this code. Some behavior you
* could add, left as an exercise to the reader:
* 1. Resolve with the previous result when a request is about to be dropped.
* 2. Resolve all N requests with the result of the next request.
* 3. Do not return anything, and use this as a fire-and-forget library only.
*
* @param fn - Function to be called, with only one request in flight at a time.
* This must be a stable identifier, e.g. returned from useCallback.
* @returns Function that can be called whenever, returning a promise that will
* only resolve or throw if the underlying function gets called.
*/
export default function useSingleFlight<
F extends (...args: any[]) => Promise<any>
>(fn: F) {
const flightStatus = useRef({
inFlight: false,
upNext: null as null | {
fn: F;
resolve: any;
reject: any;
args: Parameters<F>;
},
});
return useCallback(
(...args: Parameters<F>): ReturnType<F> => {
if (flightStatus.current.inFlight) {
return new Promise((resolve, reject) => {
flightStatus.current.upNext = { fn, resolve, reject, args };
}) as ReturnType<F>;
}
flightStatus.current.inFlight = true;
const firstReq = fn(...args) as ReturnType<F>;
void (async () => {
try {
await firstReq;
} finally {
// If it failed, we naively just move on to the next request.
}
while (flightStatus.current.upNext) {
let cur = flightStatus.current.upNext;
flightStatus.current.upNext = null;
await cur
.fn(...cur.args)
.then(cur.resolve)
.catch(cur.reject);
}
flightStatus.current.inFlight = false;
})();
return firstReq;
},
[fn]
);
}