Skip to content

Commit ef91862

Browse files
feat: max concurrent request usedataloader (#413)
* feat: max concurrent request usedataloader * fix: feedbacks and bugs
1 parent 4959dbe commit ef91862

File tree

8 files changed

+505
-462
lines changed

8 files changed

+505
-462
lines changed

packages/use-dataloader/README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,12 @@ ReactDOM.render(
6363
)
6464
```
6565

66+
#### `maxConcurrentRequests`
67+
68+
You can specify a `maxConcurrentRequests` which will prevent DataLoader to launch request simultaneously and wait some to finish before start next ones.
69+
70+
This can be useful if you want to limit the number of concurrent requests.
71+
6672
#### `onError(err: Error): void | Promise<void>`
6773

6874
This is a global `onError` handler. It will be overriden if you specify one in `useDataLoader`

packages/use-dataloader/src/DataLoaderProvider.tsx

Lines changed: 103 additions & 132 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,23 @@ import React, {
55
createContext,
66
useCallback,
77
useContext,
8+
useEffect,
89
useMemo,
910
useRef,
10-
useState,
1111
} from 'react'
12-
import { KEY_IS_NOT_STRING_ERROR, StatusEnum } from './constants'
12+
import {
13+
DEFAULT_MAX_CONCURRENT_REQUESTS,
14+
KEY_IS_NOT_STRING_ERROR,
15+
StatusEnum,
16+
} from './constants'
1317
import DataLoader from './dataloader'
1418
import { OnErrorFn, PromiseType } from './types'
1519

16-
type RequestQueue = Record<string, DataLoader>
1720
type CachedData = Record<string, unknown>
1821
type Reloads = Record<string, () => Promise<void | unknown>>
1922

2023
type UseDataLoaderInitializerArgs<T = unknown> = {
2124
enabled?: boolean
22-
key: string
2325
status?: StatusEnum
2426
method: () => PromiseType<T>
2527
pollingInterval?: number
@@ -30,226 +32,192 @@ type UseDataLoaderInitializerArgs<T = unknown> = {
3032
maxDataLifetime?: number
3133
}
3234

33-
interface Context {
34-
addCachedData: (key: string, newData: unknown) => void
35-
addReload: (key: string, method: () => Promise<void | unknown>) => void
35+
type GetCachedDataFn = {
36+
(): CachedData
37+
(key?: string): unknown | undefined
38+
}
39+
40+
type GetReloadsFn = {
41+
(): Reloads
42+
(key?: string): (() => Promise<void | unknown>) | undefined
43+
}
44+
45+
export interface IDataLoaderContext {
3646
addRequest: (key: string, args: UseDataLoaderInitializerArgs) => DataLoader
37-
cacheKeyPrefix: string
47+
getOrAddRequest: (
48+
key: string,
49+
args: UseDataLoaderInitializerArgs,
50+
) => DataLoader
51+
cacheKeyPrefix?: string
3852
onError?: (error: Error) => void | Promise<void>
3953
clearAllCachedData: () => void
40-
clearAllReloads: () => void
41-
clearCachedData: (key?: string) => void
42-
clearReload: (key?: string) => void
43-
getCachedData: (key?: string) => unknown | CachedData
44-
getReloads: (key?: string) => (() => Promise<void | unknown>) | Reloads
45-
getRequest: (key: string) => DataLoader | undefined
54+
clearCachedData: (key: string) => void
55+
getCachedData: GetCachedDataFn
56+
getReloads: GetReloadsFn
57+
getRequest: (key: string) => DataLoader
4658
reload: (key?: string) => Promise<void>
4759
reloadAll: () => Promise<void>
4860
}
4961

5062
// @ts-expect-error we force the context to undefined, should be corrected with default values
51-
export const DataLoaderContext = createContext<Context>(undefined)
63+
export const DataLoaderContext = createContext<IDataLoaderContext>(undefined)
5264

5365
const DataLoaderProvider = ({
5466
children,
5567
cacheKeyPrefix,
5668
onError,
69+
maxConcurrentRequests,
5770
}: {
5871
children: ReactNode
5972
cacheKeyPrefix: string
6073
onError: OnErrorFn
74+
maxConcurrentRequests?: number
6175
}): ReactElement => {
62-
const [requestQueue, setRequestQueue] = useState({} as RequestQueue)
63-
const [cachedData, setCachedDataPrivate] = useState<CachedData>({})
64-
const reloads = useRef<Reloads>({})
65-
76+
const requestsRef = useRef<Record<string, DataLoader>>({})
6677
const computeKey = useCallback(
6778
(key: string) => `${cacheKeyPrefix ? `${cacheKeyPrefix}-` : ''}${key}`,
6879
[cacheKeyPrefix],
6980
)
7081

71-
const setCachedData = useCallback(
72-
(compute: CachedData | ((data: CachedData) => CachedData)) => {
73-
if (typeof compute === 'function') {
74-
setCachedDataPrivate(current => compute(current))
75-
} else {
76-
setCachedDataPrivate(compute)
77-
}
78-
},
79-
[],
80-
)
81-
82-
const setReloads = useCallback(
83-
(compute: Reloads | ((data: Reloads) => Reloads)) => {
84-
if (typeof compute === 'function') {
85-
reloads.current = compute(reloads.current)
86-
} else {
87-
reloads.current = compute
88-
}
89-
},
90-
[],
91-
)
92-
93-
const addCachedData = useCallback(
94-
(key: string, newData: unknown) => {
95-
if (newData) {
96-
if (key && typeof key === 'string') {
97-
setCachedData(actualCachedData => ({
98-
...actualCachedData,
99-
[computeKey(key)]: newData,
100-
}))
101-
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
102-
}
103-
},
104-
[setCachedData, computeKey],
105-
)
106-
107-
const addReload = useCallback(
108-
(key: string, method: () => Promise<void | unknown>) => {
109-
if (method) {
110-
if (key && typeof key === 'string') {
111-
setReloads(actualReloads => ({
112-
...actualReloads,
113-
[computeKey(key)]: method,
114-
}))
115-
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
116-
}
117-
},
118-
[setReloads, computeKey],
82+
const getRequest = useCallback(
83+
(key: string) => requestsRef.current[computeKey(key)],
84+
[computeKey],
11985
)
12086

12187
const addRequest = useCallback(
12288
(key: string, args: UseDataLoaderInitializerArgs) => {
89+
if (DataLoader.maxConcurrent !== maxConcurrentRequests) {
90+
DataLoader.maxConcurrent = maxConcurrentRequests as number
91+
}
12392
if (key && typeof key === 'string') {
124-
const notifyChanges = (updatedRequest: DataLoader) => {
125-
setRequestQueue(current => ({
126-
...current,
127-
[computeKey(updatedRequest.key)]: updatedRequest,
128-
}))
129-
}
130-
const newRequest = new DataLoader({ ...args, notify: notifyChanges })
131-
newRequest.addOnSuccessListener(result => {
132-
if (result !== undefined && result !== null)
133-
addCachedData(key, result)
93+
const newRequest = new DataLoader({
94+
...args,
95+
key: computeKey(key),
13496
})
135-
setRequestQueue(current => ({
136-
...current,
137-
[computeKey(key)]: newRequest,
138-
}))
13997

140-
addReload(key, () => newRequest.load(true))
98+
requestsRef.current[newRequest.key] = newRequest
14199

142100
return newRequest
143101
}
144102
throw new Error(KEY_IS_NOT_STRING_ERROR)
145103
},
146-
[computeKey, addCachedData, addReload],
104+
[computeKey, maxConcurrentRequests],
147105
)
148106

149-
const getRequest = useCallback(
150-
(key: string) => requestQueue[computeKey(key)],
151-
[computeKey, requestQueue],
152-
)
153-
154-
const clearReload = useCallback(
155-
(key?: string) => {
156-
if (key && typeof key === 'string') {
157-
setReloads(actualReloads => {
158-
const tmp = actualReloads
159-
delete tmp[computeKey(key)]
107+
const getOrAddRequest = useCallback(
108+
(key: string, args: UseDataLoaderInitializerArgs) => {
109+
const requestFound = getRequest(key)
110+
if (!requestFound) {
111+
return addRequest(key, args)
112+
}
160113

161-
return tmp
162-
})
163-
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
114+
return requestFound
164115
},
165-
[setReloads, computeKey],
116+
[addRequest, getRequest],
166117
)
167118

168-
const clearAllReloads = useCallback(() => {
169-
setReloads({})
170-
}, [setReloads])
171-
172119
const clearCachedData = useCallback(
173-
(key?: string) => {
174-
if (key && typeof key === 'string') {
175-
setCachedData(actualCachedData => {
176-
const tmp = actualCachedData
177-
delete tmp[computeKey(key)]
178-
179-
return tmp
180-
})
120+
(key: string) => {
121+
if (typeof key === 'string') {
122+
if (requestsRef.current[computeKey(key)]) {
123+
requestsRef.current[computeKey(key)].clearData()
124+
}
181125
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
182126
},
183-
[setCachedData, computeKey],
127+
[computeKey],
184128
)
185129
const clearAllCachedData = useCallback(() => {
186-
setCachedData({})
187-
}, [setCachedData])
130+
Object.values(requestsRef.current).forEach(request => {
131+
request.clearData()
132+
})
133+
}, [])
188134

189135
const reload = useCallback(
190136
async (key?: string) => {
191137
if (key && typeof key === 'string') {
192-
await reloads.current[computeKey(key)]?.()
138+
await getRequest(key)?.load(true)
193139
} else throw new Error(KEY_IS_NOT_STRING_ERROR)
194140
},
195-
[computeKey],
141+
[getRequest],
196142
)
197143

198144
const reloadAll = useCallback(async () => {
199145
await Promise.all(
200-
Object.values(reloads.current).map(reloadFn => reloadFn()),
146+
Object.values(requestsRef.current).map(request => request.load(true)),
201147
)
202148
}, [])
203149

204150
const getCachedData = useCallback(
205151
(key?: string) => {
206152
if (key) {
207-
return cachedData[computeKey(key)]
153+
return getRequest(key)?.getData()
208154
}
209155

210-
return cachedData
156+
return Object.values(requestsRef.current).reduce(
157+
(acc, request) => ({
158+
...acc,
159+
[request.key]: request.getData(),
160+
}),
161+
{} as CachedData,
162+
)
211163
},
212-
[computeKey, cachedData],
164+
[getRequest],
213165
)
214166

215167
const getReloads = useCallback(
216168
(key?: string) => {
217169
if (key) {
218-
return reloads.current[computeKey(key)]
170+
return getRequest(key) ? () => getRequest(key).load(true) : undefined
219171
}
220172

221-
return reloads.current
173+
return Object.entries(requestsRef.current).reduce(
174+
(acc, [requestKey, { load }]) => ({
175+
...acc,
176+
[requestKey]: () => load(true),
177+
}),
178+
{} as Reloads,
179+
)
222180
},
223-
[computeKey],
181+
[getRequest],
224182
)
225183

184+
useEffect(() => {
185+
const cleanRequest = () => {
186+
setTimeout(() => {
187+
Object.keys(requestsRef.current).forEach(key => {
188+
if (requestsRef.current[key].getObserversCount() === 0) {
189+
requestsRef.current[key].destroy()
190+
delete requestsRef.current[key]
191+
}
192+
})
193+
cleanRequest()
194+
}, 300)
195+
}
196+
197+
cleanRequest()
198+
}, [])
199+
226200
const value = useMemo(
227201
() => ({
228-
addCachedData,
229-
addReload,
230202
addRequest,
231203
cacheKeyPrefix,
232204
clearAllCachedData,
233-
clearAllReloads,
234205
clearCachedData,
235-
clearReload,
236206
getCachedData,
207+
getOrAddRequest,
237208
getReloads,
238209
getRequest,
239210
onError,
240211
reload,
241212
reloadAll,
242213
}),
243214
[
244-
addCachedData,
245-
addReload,
246215
addRequest,
247216
cacheKeyPrefix,
248217
clearAllCachedData,
249-
clearAllReloads,
250218
clearCachedData,
251-
clearReload,
252219
getCachedData,
220+
getOrAddRequest,
253221
getRequest,
254222
getReloads,
255223
onError,
@@ -259,7 +227,7 @@ const DataLoaderProvider = ({
259227
)
260228

261229
return (
262-
<DataLoaderContext.Provider value={value}>
230+
<DataLoaderContext.Provider value={value as IDataLoaderContext}>
263231
{children}
264232
</DataLoaderContext.Provider>
265233
)
@@ -268,14 +236,17 @@ const DataLoaderProvider = ({
268236
DataLoaderProvider.propTypes = {
269237
cacheKeyPrefix: PropTypes.string,
270238
children: PropTypes.node.isRequired,
239+
maxConcurrentRequests: PropTypes.number,
271240
onError: PropTypes.func,
272241
}
273242

274243
DataLoaderProvider.defaultProps = {
275244
cacheKeyPrefix: undefined,
245+
maxConcurrentRequests: DEFAULT_MAX_CONCURRENT_REQUESTS,
276246
onError: undefined,
277247
}
278248

279-
export const useDataLoaderContext = (): Context => useContext(DataLoaderContext)
249+
export const useDataLoaderContext = (): IDataLoaderContext =>
250+
useContext(DataLoaderContext)
280251

281252
export default DataLoaderProvider

0 commit comments

Comments
 (0)