-
-
Notifications
You must be signed in to change notification settings - Fork 98
/
dedupe.ts
85 lines (73 loc) · 2.43 KB
/
dedupe.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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import type { ConfiguredMiddleware, WretchOptions } from "../types.js"
/* Types */
export type DedupeSkipFunction = (url: string, opts: WretchOptions) => boolean
export type DedupeKeyFunction = (url: string, opts: WretchOptions) => string
export type DedupeResolverFunction = (response: Response) => Response
export type DedupeOptions = {
skip?: DedupeSkipFunction,
key?: DedupeKeyFunction,
resolver?: DedupeResolverFunction
}
/**
* ## Dedupe middleware
*
* #### Prevents having multiple identical requests on the fly at the same time.
*
* **Options**
*
* - *skip* `(url, opts) => boolean`
*
* > If skip returns true, then the dedupe check is skipped.
*
* - *key* `(url, opts) => string`
*
* > Returns a key that is used to identify the request.
*
* - *resolver* `(response: Response) => Response`
*
* > This function is called when resolving the fetch response from duplicate calls.
* By default it clones the response to allow reading the body from multiple sources.
*/
export type DedupeMiddleware = (options?: DedupeOptions) => ConfiguredMiddleware
/* Defaults */
const defaultSkip: DedupeSkipFunction = (_, opts) => (
opts.skipDedupe || opts.method !== "GET"
)
const defaultKey: DedupeKeyFunction = (url: string, opts) => opts.method + "@" + url
const defaultResolver: DedupeResolverFunction = response => response.clone()
export const dedupe: DedupeMiddleware = ({ skip = defaultSkip, key = defaultKey, resolver = defaultResolver } = {}) => {
const inflight = new Map()
return next => (url, opts) => {
if (skip(url, opts)) {
return next(url, opts)
}
const _key = key(url, opts)
if (!inflight.has(_key)) {
inflight.set(_key, [])
} else {
return new Promise((resolve, reject) => {
inflight.get(_key).push([resolve, reject])
})
}
try {
return next(url, opts)
.then(response => {
// Resolve pending promises
inflight.get(_key).forEach(([resolve]) => resolve(resolver(response)))
// Remove the inflight pending promises
inflight.delete(_key)
// Return the original response
return response
})
.catch(error => {
// Reject pending promises on error
inflight.get(_key).forEach(([resolve, reject]) => reject(error))
inflight.delete(_key)
throw error
})
} catch (error) {
inflight.delete(_key)
return Promise.reject(error)
}
}
}