Skip to content

Commit ac19ecf

Browse files
committed
enhance: Change NetworkManager bookkeeping data structure for inflight fetches
1 parent 25b153a commit ac19ecf

File tree

5 files changed

+73
-47
lines changed

5 files changed

+73
-47
lines changed

.changeset/sour-horses-give.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
---
2+
'@data-client/core': minor
3+
'@data-client/test': minor
4+
---
5+
6+
Change NetworkManager bookkeeping data structure for inflight fetches
7+
8+
BREAKING CHANGE: NetworkManager.fetched, NetworkManager.rejectors, NetworkManager.resolvers, NetworkManager.fetchedAt
9+
-> NetworkManager.fetching
10+
11+
12+
#### Before
13+
14+
```ts
15+
if (action.key in this.fetched)
16+
```
17+
18+
#### After
19+
20+
```ts
21+
if (this.fetching.has(action.key))
22+
```

packages/core/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ export {
2626
default as NetworkManager,
2727
ResetError,
2828
} from './manager/NetworkManager.js';
29+
export type { FetchingMeta } from './manager/NetworkManager.js';
2930
export * from './state/GCPolicy.js';
3031
export {
3132
default as createReducer,

packages/core/src/manager/NetworkManager.ts

Lines changed: 41 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -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
*/
3036
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();
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,16 @@ 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 fetches = Array.from(this.fetching.values());
8489

8590
this.clearAll();
8691
return next(action).then(() => {
8792
// there could be external listeners to the promise
8893
// 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());
94+
for (const { reject } of fetches) {
95+
reject(new ResetError());
9196
}
9297
});
9398
}
@@ -112,28 +117,29 @@ export default class NetworkManager implements Manager {
112117
/** Used by DevtoolsManager to determine whether to log an action */
113118
skipLogging(action: ActionTypes) {
114119
/* istanbul ignore next */
115-
return action.type === FETCH && action.key in this.fetched;
120+
return action.type === FETCH && this.fetching.has(action.key);
116121
}
117122

118123
allSettled() {
119-
const fetches = Object.values(this.fetched);
120-
if (fetches.length) return Promise.allSettled(fetches);
124+
if (this.fetching.size)
125+
return Promise.allSettled(
126+
this.fetching.values().map(({ promise }) => promise),
127+
);
121128
}
122129

123130
/** Clear all promise state */
124131
protected clearAll() {
125-
for (const k in this.rejectors) {
132+
for (const k of this.fetching.keys()) {
126133
this.clear(k);
127134
}
128135
}
129136

130137
/** Clear promise state for a given key */
131138
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];
139+
if (this.fetching.has(key)) {
140+
(this.fetching.get(key) as FetchingMeta).promise.catch(() => {});
141+
this.fetching.delete(key);
142+
}
137143
}
138144

139145
protected getLastReset() {
@@ -226,14 +232,14 @@ export default class NetworkManager implements Manager {
226232
*/
227233
protected handleSet(action: SetResponseAction) {
228234
// 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;
235+
if (this.fetching.has(action.key)) {
236+
const { reject, resolve } = this.fetching.get(action.key) as FetchingMeta;
231237
if (action.error) {
232-
promiseHandler = this.rejectors[action.key];
238+
reject(action.response);
233239
} else {
234-
promiseHandler = this.resolvers[action.key];
240+
resolve(action.response);
235241
}
236-
promiseHandler(action.response);
242+
237243
// since we're resolved we no longer need to keep track of this promise
238244
this.clear(action.key);
239245
}
@@ -253,19 +259,23 @@ export default class NetworkManager implements Manager {
253259
key: string,
254260
fetch: () => Promise<any>,
255261
fetchedAt: number,
256-
) {
262+
): Promise<any> {
257263
const lastReset = this.getLastReset();
264+
let fetchMeta = this.fetching.get(key);
265+
258266
// we're already fetching so reuse the promise
259267
// fetches after reset do not count
260-
if (key in this.fetched && this.fetchedAt[key] > lastReset) {
261-
return this.fetched[key];
268+
if (fetchMeta && fetchMeta.fetchedAt > lastReset) {
269+
return fetchMeta.promise;
262270
}
263271

264-
this.fetched[key] = new Promise((resolve, reject) => {
265-
this.resolvers[key] = resolve;
266-
this.rejectors[key] = reject;
272+
fetchMeta = { fetchedAt } as FetchingMeta;
273+
fetchMeta.promise = new Promise((resolve, reject) => {
274+
fetchMeta.resolve = resolve;
275+
fetchMeta.reject = reject;
267276
});
268-
this.fetchedAt[key] = fetchedAt;
277+
278+
this.fetching.set(key, fetchMeta);
269279

270280
this.idleCallback(
271281
() => {
@@ -277,7 +287,7 @@ export default class NetworkManager implements Manager {
277287
{ timeout: 500 },
278288
);
279289

280-
return this.fetched[key];
290+
return fetchMeta.promise;
281291
}
282292

283293
/** Calls the callback when client is not 'busy' with high priority interaction tasks

packages/test/src/makeRenderDataClient/index.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,9 +83,7 @@ export default function makeRenderDataHook(
8383
// TODO: move to return value
8484
renderDataClient.cleanup = () => {
8585
nm.cleanupDate = Infinity;
86-
Object.values(nm['rejectors'] as Record<string, any>).forEach(rej => {
87-
rej();
88-
});
86+
nm['fetching'].forEach(({ reject }) => reject());
8987
nm['clearAll']();
9088
managers.forEach(manager => manager.cleanup());
9189
};

website/src/components/Playground/editor-types/@data-client/core.d.ts

Lines changed: 8 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -889,6 +889,12 @@ declare class ResetError extends Error {
889889
name: string;
890890
constructor();
891891
}
892+
interface FetchingMeta {
893+
promise: Promise<any>;
894+
resolve: (value?: any) => void;
895+
reject: (value?: any) => void;
896+
fetchedAt: number;
897+
}
892898
/** Handles all async network dispatches
893899
*
894900
* Dedupes concurrent requests by keeping track of all fetches in flight
@@ -899,18 +905,7 @@ declare class ResetError extends Error {
899905
* @see https://dataclient.io/docs/api/NetworkManager
900906
*/
901907
declare class NetworkManager implements Manager {
902-
protected fetched: {
903-
[k: string]: Promise<any>;
904-
};
905-
protected resolvers: {
906-
[k: string]: (value?: any) => void;
907-
};
908-
protected rejectors: {
909-
[k: string]: (value?: any) => void;
910-
};
911-
protected fetchedAt: {
912-
[k: string]: number;
913-
};
908+
protected fetching: Map<string, FetchingMeta>;
914909
readonly dataExpiryLength: number;
915910
readonly errorExpiryLength: number;
916911
protected controller: Controller;
@@ -1371,4 +1366,4 @@ interface Props {
13711366
shouldLogout?: (error: UnknownError) => boolean;
13721367
}
13731368

1374-
export { type AbstractInstanceType, type ActionMeta, type ActionTypes, type ConnectionListener, Controller, type CreateCountRef, type DataClientDispatch, DefaultConnectionListener, type Denormalize, type DenormalizeNullable, type DevToolsConfig, DevToolsManager, type Dispatch, type EndpointExtraOptions, type EndpointInterface, type EndpointUpdateFunction, type EntityInterface, type ErrorTypes, type ExpireAllAction, ExpiryStatus, type FetchAction, type FetchFunction, type FetchMeta, type GCAction, type GCInterface, type GCOptions, GCPolicy, type GenericDispatch, type INormalizeDelegate, type IQueryDelegate, ImmortalGCPolicy, type InvalidateAction, type InvalidateAllAction, LogoutManager, type Manager, type Mergeable, type Middleware, type MiddlewareAPI, type NI, type NetworkError, NetworkManager, type Normalize, type NormalizeNullable, type OptimisticAction, type PK, PollingSubscription, type Queryable, type ResetAction, ResetError, type ResolveType, type ResultEntry, type Schema, type SchemaArgs, type SchemaClass, type SetAction, type SetResponseAction, type SetResponseActionBase, type SetResponseActionError, type SetResponseActionSuccess, type State, type SubscribeAction, SubscriptionManager, type UnknownError, type UnsubscribeAction, type UpdateFunction, internal_d as __INTERNAL__, actionTypes_d as actionTypes, index_d as actions, applyManager, createReducer, initManager, initialState };
1369+
export { type AbstractInstanceType, type ActionMeta, type ActionTypes, type ConnectionListener, Controller, type CreateCountRef, type DataClientDispatch, DefaultConnectionListener, type Denormalize, type DenormalizeNullable, type DevToolsConfig, DevToolsManager, type Dispatch, type EndpointExtraOptions, type EndpointInterface, type EndpointUpdateFunction, type EntityInterface, type ErrorTypes, type ExpireAllAction, ExpiryStatus, type FetchAction, type FetchFunction, type FetchMeta, type FetchingMeta, type GCAction, type GCInterface, type GCOptions, GCPolicy, type GenericDispatch, type INormalizeDelegate, type IQueryDelegate, ImmortalGCPolicy, type InvalidateAction, type InvalidateAllAction, LogoutManager, type Manager, type Mergeable, type Middleware, type MiddlewareAPI, type NI, type NetworkError, NetworkManager, type Normalize, type NormalizeNullable, type OptimisticAction, type PK, PollingSubscription, type Queryable, type ResetAction, ResetError, type ResolveType, type ResultEntry, type Schema, type SchemaArgs, type SchemaClass, type SetAction, type SetResponseAction, type SetResponseActionBase, type SetResponseActionError, type SetResponseActionSuccess, type State, type SubscribeAction, SubscriptionManager, type UnknownError, type UnsubscribeAction, type UpdateFunction, internal_d as __INTERNAL__, actionTypes_d as actionTypes, index_d as actions, applyManager, createReducer, initManager, initialState };

0 commit comments

Comments
 (0)