@@ -5,7 +5,6 @@ import type {
5
5
FetchAction ,
6
6
Manager ,
7
7
ActionTypes ,
8
- MiddlewareAPI ,
9
8
Middleware ,
10
9
SetResponseAction ,
11
10
} from '../types.js' ;
@@ -18,6 +17,13 @@ export class ResetError extends Error {
18
17
}
19
18
}
20
19
20
+ export interface FetchingMeta {
21
+ promise : Promise < any > ;
22
+ resolve : ( value ?: any ) => void ;
23
+ reject : ( value ?: any ) => void ;
24
+ fetchedAt : number ;
25
+ }
26
+
21
27
/** Handles all async network dispatches
22
28
*
23
29
* Dedupes concurrent requests by keeping track of all fetches in flight
@@ -28,10 +34,7 @@ export class ResetError extends Error {
28
34
* @see https://dataclient.io/docs/api/NetworkManager
29
35
*/
30
36
export default class NetworkManager implements Manager {
31
- protected fetched : { [ k : string ] : Promise < any > } = Object . create ( null ) ;
32
- protected resolvers : { [ k : string ] : ( value ?: any ) => void } = { } ;
33
- protected rejectors : { [ k : string ] : ( value ?: any ) => void } = { } ;
34
- protected fetchedAt : { [ k : string ] : number } = { } ;
37
+ protected fetching : Map < string , FetchingMeta > = new Map ( ) ;
35
38
declare readonly dataExpiryLength : number ;
36
39
declare readonly errorExpiryLength : number ;
37
40
protected controller : Controller = new Controller ( ) ;
@@ -61,7 +64,7 @@ export default class NetworkManager implements Manager {
61
64
case SET_RESPONSE :
62
65
// only set after new state is computed
63
66
return next ( action ) . then ( ( ) => {
64
- if ( action . key in this . fetched ) {
67
+ if ( this . fetching . has ( action . key ) ) {
65
68
// Note: meta *must* be set by reducer so this should be safe
66
69
const error = controller . getState ( ) . meta [ action . key ] ?. error ;
67
70
// processing errors result in state meta having error, so we should reject the promise
@@ -80,14 +83,18 @@ export default class NetworkManager implements Manager {
80
83
}
81
84
} ) ;
82
85
case RESET : {
83
- const rejectors = { ...this . rejectors } ;
86
+ // take snapshot of rejectors at this point in time
87
+ // we must use Array.from since iteration does not freeze state at this point in time
88
+ const rejectors = Array . from (
89
+ this . fetching . values ( ) . map ( ( { reject } ) => reject ) ,
90
+ ) ;
84
91
85
92
this . clearAll ( ) ;
86
93
return next ( action ) . then ( ( ) => {
87
94
// there could be external listeners to the promise
88
95
// this must happen after commit so our own rejector knows not to dispatch an error based on this
89
- for ( const k in rejectors ) {
90
- rejectors [ k ] ( new ResetError ( ) ) ;
96
+ for ( const rejector of rejectors ) {
97
+ rejector ( new ResetError ( ) ) ;
91
98
}
92
99
} ) ;
93
100
}
@@ -112,28 +119,29 @@ export default class NetworkManager implements Manager {
112
119
/** Used by DevtoolsManager to determine whether to log an action */
113
120
skipLogging ( action : ActionTypes ) {
114
121
/* istanbul ignore next */
115
- return action . type === FETCH && action . key in this . fetched ;
122
+ return action . type === FETCH && this . fetching . has ( action . key ) ;
116
123
}
117
124
118
125
allSettled ( ) {
119
- const fetches = Object . values ( this . fetched ) ;
120
- if ( fetches . length ) return Promise . allSettled ( fetches ) ;
126
+ if ( this . fetching . size )
127
+ return Promise . allSettled (
128
+ this . fetching . values ( ) . map ( ( { promise } ) => promise ) ,
129
+ ) ;
121
130
}
122
131
123
132
/** Clear all promise state */
124
133
protected clearAll ( ) {
125
- for ( const k in this . rejectors ) {
134
+ for ( const k of this . fetching . keys ( ) ) {
126
135
this . clear ( k ) ;
127
136
}
128
137
}
129
138
130
139
/** Clear promise state for a given key */
131
140
protected clear ( key : string ) {
132
- this . fetched [ key ] . catch ( ( ) => { } ) ;
133
- delete this . resolvers [ key ] ;
134
- delete this . rejectors [ key ] ;
135
- delete this . fetched [ key ] ;
136
- delete this . fetchedAt [ key ] ;
141
+ if ( this . fetching . has ( key ) ) {
142
+ ( this . fetching . get ( key ) as FetchingMeta ) . promise . catch ( ( ) => { } ) ;
143
+ this . fetching . delete ( key ) ;
144
+ }
137
145
}
138
146
139
147
protected getLastReset ( ) {
@@ -226,14 +234,14 @@ export default class NetworkManager implements Manager {
226
234
*/
227
235
protected handleSet ( action : SetResponseAction ) {
228
236
// this can still turn out to be untrue since this is async
229
- if ( action . key in this . fetched ) {
230
- let promiseHandler : ( value ?: any ) => void ;
237
+ if ( this . fetching . has ( action . key ) ) {
238
+ const fetchMeta = this . fetching . get ( action . key ) as FetchingMeta ;
231
239
if ( action . error ) {
232
- promiseHandler = this . rejectors [ action . key ] ;
240
+ fetchMeta . reject ( action . response ) ;
233
241
} else {
234
- promiseHandler = this . resolvers [ action . key ] ;
242
+ fetchMeta . resolve ( action . response ) ;
235
243
}
236
- promiseHandler ( action . response ) ;
244
+
237
245
// since we're resolved we no longer need to keep track of this promise
238
246
this . clear ( action . key ) ;
239
247
}
@@ -253,31 +261,54 @@ export default class NetworkManager implements Manager {
253
261
key : string ,
254
262
fetch : ( ) => Promise < any > ,
255
263
fetchedAt : number ,
256
- ) {
264
+ ) : Promise < any > {
257
265
const lastReset = this . getLastReset ( ) ;
266
+ let fetchMeta : FetchingMeta ;
267
+
258
268
// we're already fetching so reuse the promise
259
269
// fetches after reset do not count
260
- if ( key in this . fetched && this . fetchedAt [ key ] > lastReset ) {
261
- return this . fetched [ key ] ;
262
- }
270
+ if (
271
+ ! this . fetching . has ( key ) ||
272
+ ( fetchMeta = this . fetching . get ( key ) as FetchingMeta ) . fetchedAt <=
273
+ lastReset
274
+ ) {
275
+ const promiseHandlers : {
276
+ resolve ?: ( value : any ) => void ;
277
+ reject ?: ( value : any ) => void ;
278
+ } = { } ;
279
+ fetchMeta = {
280
+ promise : new Promise ( ( resolve , reject ) => {
281
+ promiseHandlers . resolve = resolve ;
282
+ promiseHandlers . reject = reject ;
283
+ } ) ,
284
+ resolve ( value ) {
285
+ // if this is called before the promise callback runs it might not be set yet
286
+ if ( promiseHandlers . resolve ) return promiseHandlers . resolve ( value ) ;
287
+ // this should be safe (no infinite recursion), since our new Promise is not conditional
288
+ setTimeout ( fetchMeta . resolve , 0 ) ;
289
+ } ,
290
+ reject ( value ) {
291
+ // if this is called before the promise callback runs it might not be set yet
292
+ if ( promiseHandlers . reject ) return promiseHandlers . reject ( value ) ;
293
+ // this should be safe (no infinite recursion), since our new Promise is not conditional
294
+ setTimeout ( fetchMeta . reject , 0 ) ;
295
+ } ,
296
+ fetchedAt,
297
+ } as FetchingMeta ;
298
+ this . fetching . set ( key , fetchMeta ) ;
263
299
264
- this . fetched [ key ] = new Promise ( ( resolve , reject ) => {
265
- this . resolvers [ key ] = resolve ;
266
- this . rejectors [ key ] = reject ;
267
- } ) ;
268
- this . fetchedAt [ key ] = fetchedAt ;
269
-
270
- this . idleCallback (
271
- ( ) => {
272
- // since our real promise is resolved via the wrapReducer(),
273
- // we should just stop all errors here.
274
- // TODO: decouple this from useFetcher() (that's what's dispatching the error the resolves in here)
275
- fetch ( ) . catch ( ( ) => null ) ;
276
- } ,
277
- { timeout : 500 } ,
278
- ) ;
300
+ this . idleCallback (
301
+ ( ) => {
302
+ // since our real promise is resolved via the wrapReducer(),
303
+ // we should just stop all errors here.
304
+ // TODO: decouple this from useFetcher() (that's what's dispatching the error the resolves in here)
305
+ fetch ( ) . catch ( ( ) => null ) ;
306
+ } ,
307
+ { timeout : 500 } ,
308
+ ) ;
309
+ }
279
310
280
- return this . fetched [ key ] ;
311
+ return fetchMeta . promise ;
281
312
}
282
313
283
314
/** Calls the callback when client is not 'busy' with high priority interaction tasks
0 commit comments