@@ -5,7 +5,6 @@ import type {
55 FetchAction ,
66 Manager ,
77 ActionTypes ,
8- MiddlewareAPI ,
98 Middleware ,
109 SetResponseAction ,
1110} from '../types.js' ;
@@ -18,6 +17,13 @@ export class ResetError extends Error {
1817 }
1918}
2019
20+ export interface FetchingMeta {
21+ promise : Promise < any > ;
22+ resolve : ( value ?: any ) => void ;
23+ reject : ( value ?: any ) => void ;
24+ fetchedAt : number ;
25+ }
26+
2127/** Handles all async network dispatches
2228 *
2329 * Dedupes concurrent requests by keeping track of all fetches in flight
@@ -28,10 +34,7 @@ export class ResetError extends Error {
2834 * @see https://dataclient.io/docs/api/NetworkManager
2935 */
3036export 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 ( ) ;
3538 declare readonly dataExpiryLength : number ;
3639 declare readonly errorExpiryLength : number ;
3740 protected controller : Controller = new Controller ( ) ;
@@ -61,7 +64,7 @@ export default class NetworkManager implements Manager {
6164 case SET_RESPONSE :
6265 // only set after new state is computed
6366 return next ( action ) . then ( ( ) => {
64- if ( action . key in this . fetched ) {
67+ if ( this . fetching . has ( action . key ) ) {
6568 // Note: meta *must* be set by reducer so this should be safe
6669 const error = controller . getState ( ) . meta [ action . key ] ?. error ;
6770 // processing errors result in state meta having error, so we should reject the promise
@@ -80,14 +83,18 @@ export default class NetworkManager implements Manager {
8083 }
8184 } ) ;
8285 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+ ) ;
8491
8592 this . clearAll ( ) ;
8693 return next ( action ) . then ( ( ) => {
8794 // there could be external listeners to the promise
8895 // 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 ( ) ) ;
9198 }
9299 } ) ;
93100 }
@@ -112,28 +119,29 @@ export default class NetworkManager implements Manager {
112119 /** Used by DevtoolsManager to determine whether to log an action */
113120 skipLogging ( action : ActionTypes ) {
114121 /* istanbul ignore next */
115- return action . type === FETCH && action . key in this . fetched ;
122+ return action . type === FETCH && this . fetching . has ( action . key ) ;
116123 }
117124
118125 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+ ) ;
121130 }
122131
123132 /** Clear all promise state */
124133 protected clearAll ( ) {
125- for ( const k in this . rejectors ) {
134+ for ( const k of this . fetching . keys ( ) ) {
126135 this . clear ( k ) ;
127136 }
128137 }
129138
130139 /** Clear promise state for a given key */
131140 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+ }
137145 }
138146
139147 protected getLastReset ( ) {
@@ -226,14 +234,14 @@ export default class NetworkManager implements Manager {
226234 */
227235 protected handleSet ( action : SetResponseAction ) {
228236 // 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 ;
231239 if ( action . error ) {
232- promiseHandler = this . rejectors [ action . key ] ;
240+ fetchMeta . reject ( action . response ) ;
233241 } else {
234- promiseHandler = this . resolvers [ action . key ] ;
242+ fetchMeta . resolve ( action . response ) ;
235243 }
236- promiseHandler ( action . response ) ;
244+
237245 // since we're resolved we no longer need to keep track of this promise
238246 this . clear ( action . key ) ;
239247 }
@@ -253,31 +261,54 @@ export default class NetworkManager implements Manager {
253261 key : string ,
254262 fetch : ( ) => Promise < any > ,
255263 fetchedAt : number ,
256- ) {
264+ ) : Promise < any > {
257265 const lastReset = this . getLastReset ( ) ;
266+ let fetchMeta : FetchingMeta ;
267+
258268 // we're already fetching so reuse the promise
259269 // 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 ) ;
263299
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+ }
279310
280- return this . fetched [ key ] ;
311+ return fetchMeta . promise ;
281312 }
282313
283314 /** Calls the callback when client is not 'busy' with high priority interaction tasks
0 commit comments