diff --git a/.changeset/five-apes-pretend.md b/.changeset/five-apes-pretend.md
new file mode 100644
index 000000000000..9ad31153e866
--- /dev/null
+++ b/.changeset/five-apes-pretend.md
@@ -0,0 +1,8 @@
+---
+'@data-client/react': patch
+'@data-client/core': patch
+---
+
+Manager.getMiddleware() -> Manager.middleware
+
+`getMiddleware()` is still supported to make this change non-breaking
\ No newline at end of file
diff --git a/.changeset/new-garlics-sort.md b/.changeset/new-garlics-sort.md
new file mode 100644
index 000000000000..ad6c4adfb780
--- /dev/null
+++ b/.changeset/new-garlics-sort.md
@@ -0,0 +1,7 @@
+---
+'@data-client/react': patch
+---
+
+Move manager lifecycle logic from DataStore to DataProvider
+
+This has no behavioral change, but creates a better seperation of concerns.
\ No newline at end of file
diff --git a/README.md b/README.md
index efd0c4091eec..c3afd77ce929 100644
--- a/README.md
+++ b/README.md
@@ -162,7 +162,7 @@ const totalVotesForUser = useQuery(queryTotalVotes, { userId });
```ts
class LoggingManager implements Manager {
- getMiddleware = (): Middleware => controller => next => async action => {
+ middleware: Middleware => controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
diff --git a/docs/core/README.md b/docs/core/README.md
index 8da883180215..e0efc86f8ee3 100644
--- a/docs/core/README.md
+++ b/docs/core/README.md
@@ -455,29 +455,29 @@ export default class StreamManager implements Manager {
) {
this.evtSource = evtSource;
this.endpoints = endpoints;
+ }
- this.middleware = controller => {
- this.evtSource.onmessage = event => {
- try {
- const msg = JSON.parse(event.data);
- if (msg.type in this.endpoints)
- controller.setResponse(this.endpoints[msg.type], ...msg.args, msg.data);
- } catch (e) {
- console.error('Failed to handle message');
- console.error(e);
- }
- };
- return next => async action => next(action);
+ middleware: Middleware = controller => {
+ this.evtSource.onmessage = event => {
+ try {
+ const msg = JSON.parse(event.data);
+ if (msg.type in this.endpoints)
+ controller.setResponse(
+ this.endpoints[msg.type],
+ ...msg.args,
+ msg.data,
+ );
+ } catch (e) {
+ console.error('Failed to handle message');
+ console.error(e);
+ }
};
- }
+ return next => async action => next(action);
+ };
cleanup() {
this.evtSource.close();
}
-
- getMiddleware() {
- return this.middleware;
- }
}
```
@@ -581,10 +581,10 @@ const currentTimeInterceptor: Interceptor = {
path: '/api/currentTime/:id',
}),
response({ id }) {
- return ({
+ return {
id,
updatedAt: new Date().toISOString(),
- });
+ };
},
delay: () => 150,
};
@@ -621,15 +621,15 @@ const incrementInterceptor: Interceptor = {
## Demo
-
+defaultValue="todo"
+values={[
+{ label: 'Todo', value: 'todo' },
+{ label: 'GitHub', value: 'github' },
+{ label: 'NextJS SSR', value: 'nextjs' },
+]}
+groupId="Demos"
+
+>
\ No newline at end of file
+
diff --git a/docs/core/api/Controller.md b/docs/core/api/Controller.md
index f46221d1287a..e56a882eb819 100644
--- a/docs/core/api/Controller.md
+++ b/docs/core/api/Controller.md
@@ -20,9 +20,9 @@ and retrieval performance.
`Controller` is provided:
- - [Managers](./Manager.md) as the first argument in [Manager.getMiddleware()](./Manager.md#getmiddleware)
- - React with [useController()](./useController.md)
- - [Unit testing hooks](../guides/unit-testing-hooks.md) with [renderDataClient()](./makeRenderDataClient.md#controller)
+- [Managers](./Manager.md) as the first argument in [Manager.middleware](./Manager.md#middleware)
+- React with [useController()](./useController.md)
+- [Unit testing hooks](../guides/unit-testing-hooks.md) with [renderDataClient()](./makeRenderDataClient.md#controller)
```ts
class Controller {
@@ -366,7 +366,11 @@ function UserName() {
Updates any [Queryable](/rest/api/schema#queryable) [Schema](/rest/api/schema#schema-overview).
```ts
-ctrl.set(Todo, { id: '5' }, { id: '5', title: 'tell me friends how great Data Client is' });
+ctrl.set(
+ Todo,
+ { id: '5' },
+ { id: '5', title: 'tell me friends how great Data Client is' },
+);
```
Functions can be used in the value when derived data is used. This [prevents race conditions](https://react.dev/reference/react/useState#updating-state-based-on-the-previous-state).
@@ -546,32 +550,25 @@ import type { Manager, Middleware, actionTypes } from '@data-client/core';
import type { EndpointInterface } from '@data-client/endpoint';
export default class MyManager implements Manager {
- protected declare middleware: Middleware;
- constructor() {
- this.middleware = controller => {
- return next => async action => {
- if (action.type === actionTypes.FETCH_TYPE) {
- console.log('The existing response of the requested fetch');
- console.log(
- controller.getResponse(
- action.endpoint,
- ...(action.meta.args as Parameters),
- controller.getState(),
- ).data,
- );
- }
- next(action);
- };
+ middleware: Middleware = controller => {
+ return next => async action => {
+ if (action.type === actionTypes.FETCH_TYPE) {
+ console.log('The existing response of the requested fetch');
+ console.log(
+ controller.getResponse(
+ action.endpoint,
+ ...(action.meta.args as Parameters),
+ controller.getState(),
+ ).data,
+ );
+ }
+ next(action);
};
- }
+ };
cleanup() {
this.websocket.close();
}
-
- getMiddleware(this: T) {
- return this.middleware;
- }
}
```
diff --git a/docs/core/api/Manager.md b/docs/core/api/Manager.md
index de8bb291d0ae..ad51e38bbae6 100644
--- a/docs/core/api/Manager.md
+++ b/docs/core/api/Manager.md
@@ -22,7 +22,7 @@ The default managers orchestrate the complex asynchronous behavior that Promise;
type Middleware = (controller: Controller) => (next: Dispatch) => Dispatch;
interface Manager {
- getMiddleware(): Middleware;
+ middleware: Middleware;
cleanup(): void;
init?: (state: State) => void;
}
@@ -40,9 +40,9 @@ interface Manager {
## Lifecycle
-### getMiddleware()
+### middleware
-getMiddleware() returns a function that very similar to a [redux middleware](https://redux.js.org/advanced/middleware).
+`middleware` is very similar to a [redux middleware](https://redux.js.org/advanced/middleware).
The only differences is that the `next()` function returns a `Promise`. This promise resolves when the reducer update is
[committed](https://indepth.dev/inside-fiber-in-depth-overview-of-the-new-reconciliation-algorithm-in-react/#general-algorithm)
when using <DataProvider /\>. This is necessary since the commit phase is asynchronously scheduled. This enables building
@@ -246,7 +246,7 @@ import CurrentTime from './CurrentTime';
export default class TimeManager implements Manager {
protected declare intervalID?: ReturnType;
- getMiddleware = (): Middleware => controller => {
+ middleware: Middleware => controller => {
this.intervalID = setInterval(() => {
controller.set(CurrentTime, { id: 1 }, { id: 1, time: Date.now() });
}, 1000);
@@ -273,7 +273,7 @@ import type { Manager, Middleware } from '@data-client/react';
import { actionTypes } from '@data-client/react';
export default class LoggingManager implements Manager {
- getMiddleware = (): Middleware => controller => next => async action => {
+ middleware: Middleware => controller => next => async action => {
switch (action.type) {
case actionTypes.SET_RESPONSE_TYPE:
if (action.endpoint.sideEffect) {
@@ -326,7 +326,7 @@ import isEntity from './isEntity';
export default class CustomSubsManager implements Manager {
protected declare entities: Record;
- getMiddleware = (): Middleware => controller => next => async action => {
+ middleware: Middleware => controller => next => async action => {
switch (action.type) {
case actionTypes.SUBSCRIBE_TYPE:
case actionTypes.UNSUBSCRIBE_TYPE:
diff --git a/docs/core/api/types.md b/docs/core/api/types.md
index ccdc895570b4..12d53639301c 100644
--- a/docs/core/api/types.md
+++ b/docs/core/api/types.md
@@ -6,7 +6,7 @@ title: TypeScript Types
```typescript
interface Manager {
- getMiddleware(): Middleware;
+ middleware: Middleware;
cleanup(): void;
init?: (state: State) => void;
}
diff --git a/docs/core/concepts/managers.md b/docs/core/concepts/managers.md
index 52914cc1a7c0..00df9b62c1e1 100644
--- a/docs/core/concepts/managers.md
+++ b/docs/core/concepts/managers.md
@@ -19,8 +19,8 @@ Reactive Data Client uses the [flux store](https://facebookarchive.github.io/flu
characterized by an easy to [understand and debug](../getting-started/debugging.md) the store's [undirectional data flow](). State updates are performed by a [reducer function](https://github.com/reactive/data-client/blob/master/packages/core/src/state/reducer/createReducer.ts#L19).
controller => next => async action => {
+ middleware: Middleware => controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
@@ -80,7 +79,6 @@ import { Controller, actionTypes } from '@data-client/react';
import type { Entity } from '@data-client/rest';
export default class StreamManager implements Manager {
- protected declare middleware: Middleware;
protected declare evtSource: WebSocket | EventSource;
protected declare entities: Record;
@@ -90,35 +88,27 @@ export default class StreamManager implements Manager {
) {
this.evtSource = evtSource;
this.entities = entities;
+ }
- // highlight-start
- this.middleware = controller => {
- this.evtSource.onmessage = event => {
- try {
- const msg = JSON.parse(event.data);
- if (msg.type in this.endpoints)
- controller.set(
- this.entities[msg.type],
- ...msg.args,
- msg.data,
- );
- } catch (e) {
- console.error('Failed to handle message');
- console.error(e);
- }
- };
- return next => async action => next(action);
+ // highlight-start
+ middleware: Middleware = controller => {
+ this.evtSource.onmessage = event => {
+ try {
+ const msg = JSON.parse(event.data);
+ if (msg.type in this.endpoints)
+ controller.set(this.entities[msg.type], ...msg.args, msg.data);
+ } catch (e) {
+ console.error('Failed to handle message');
+ console.error(e);
+ }
};
- // highlight-end
- }
+ return next => async action => next(action);
+ };
+ // highlight-end
cleanup() {
this.evtSource.close();
}
-
- getMiddleware() {
- return this.middleware;
- }
}
```
diff --git a/docs/core/guides/redux.md b/docs/core/guides/redux.md
index 997ac1b94a1e..2866c83708dd 100644
--- a/docs/core/guides/redux.md
+++ b/docs/core/guides/redux.md
@@ -110,7 +110,7 @@ You should only use ONE provider; nested another provider will override the prev
:::info Note
-Because `Reactive Data Client` [manager middlewares](../api/Manager.md#getmiddleware) return promises,
+Because `Reactive Data Client` [manager middlewares](../api/Manager.md#middleware) return promises,
all redux middlewares are placed after the [Managers](../concepts/managers.md).
If you need a middlware to run before the managers, you will need to wrap it in a [manager](../api/Manager.md).
diff --git a/examples/coin-app/src/resources/StreamManager.ts b/examples/coin-app/src/resources/StreamManager.ts
index d6b3631e50ee..d02207d7893d 100644
--- a/examples/coin-app/src/resources/StreamManager.ts
+++ b/examples/coin-app/src/resources/StreamManager.ts
@@ -7,93 +7,96 @@ import type { Entity } from '@data-client/rest';
* https://docs.cloud.coinbase.com/advanced-trade-api/docs/ws-overview
*/
export default class StreamManager implements Manager {
- protected declare middleware: Middleware;
protected declare evtSource: WebSocket; // | EventSource;
+ protected declare createEventSource: () => WebSocket; // | EventSource;
protected declare entities: Record;
protected msgQueue: (string | ArrayBufferLike | Blob | ArrayBufferView)[] =
[];
protected product_ids: string[] = [];
private attempts = 0;
- protected declare connect: () => void;
+ protected declare controller: Controller;
constructor(
evtSource: () => WebSocket, // | EventSource,
entities: Record,
) {
this.entities = entities;
+ this.createEventSource = evtSource;
+ }
- this.middleware = controller => {
- this.connect = () => {
- this.evtSource = evtSource();
- this.evtSource.onmessage = event => {
- try {
- const msg = JSON.parse(event.data);
- this.handleMessage(controller, msg);
- } catch (e) {
- console.error('Failed to handle message');
- console.error(e);
- }
- };
- this.evtSource.onopen = () => {
- console.info('WebSocket connected');
- // Reset reconnection attempts after a successful connection
- this.attempts = 0;
- };
- this.evtSource.onclose = () => {
- console.info('WebSocket disconnected');
- this.reconnect();
- };
- this.evtSource.onerror = error => {
- console.error('WebSocket error:', error);
- // Ensures that the onclose handler gets triggered for reconnection
- this.evtSource.close();
- };
- };
- return next => async action => {
- switch (action.type) {
- case actionTypes.SUBSCRIBE_TYPE:
- // only process registered endpoints
- if (
- !Object.values(this.entities).find(
- // @ts-expect-error
- entity => entity.key === action.endpoint.schema?.key,
- )
+ middleware: Middleware = controller => {
+ this.controller = controller;
+ return next => async action => {
+ switch (action.type) {
+ case actionTypes.SUBSCRIBE_TYPE:
+ // only process registered endpoints
+ if (
+ !Object.values(this.entities).find(
+ // @ts-expect-error
+ entity => entity.key === action.endpoint.schema?.key,
)
- break;
- if ('channel' in action.endpoint) {
- this.subscribe(action.args[0]?.product_id);
- // consume subscription if we use it
- return Promise.resolve();
- }
+ )
+ break;
+ if ('channel' in action.endpoint) {
+ this.subscribe(action.args[0]?.product_id);
+ // consume subscription if we use it
+ return Promise.resolve();
+ }
- return next(action);
- case actionTypes.UNSUBSCRIBE_TYPE:
- // only process registered endpoints
- if (
- !Object.values(this.entities).find(
- // @ts-expect-error
- entity => entity.key === action.endpoint.schema?.key,
- )
+ return next(action);
+ case actionTypes.UNSUBSCRIBE_TYPE:
+ // only process registered endpoints
+ if (
+ !Object.values(this.entities).find(
+ // @ts-expect-error
+ entity => entity.key === action.endpoint.schema?.key,
)
- break;
- if ('channel' in action.endpoint) {
- this.send(
- JSON.stringify({
- type: 'unsubscribe',
- product_ids: [action.args[0]?.product_id],
- channels: [action.endpoint.channel],
- }),
- );
- return Promise.resolve();
- }
- return next(action);
- default:
- return next(action);
- }
- };
+ )
+ break;
+ if ('channel' in action.endpoint) {
+ this.send(
+ JSON.stringify({
+ type: 'unsubscribe',
+ product_ids: [action.args[0]?.product_id],
+ channels: [action.endpoint.channel],
+ }),
+ );
+ return Promise.resolve();
+ }
+ return next(action);
+ default:
+ return next(action);
+ }
};
- }
+ };
+
+ connect = () => {
+ this.evtSource = this.createEventSource();
+ this.evtSource.onmessage = event => {
+ try {
+ const msg = JSON.parse(event.data);
+ this.handleMessage(this.controller, msg);
+ } catch (e) {
+ console.error('Failed to handle message');
+ console.error(e);
+ }
+ };
+ this.evtSource.onopen = () => {
+ console.info('WebSocket connected');
+ // Reset reconnection attempts after a successful connection
+ this.attempts = 0;
+ };
+ this.evtSource.onclose = () => {
+ console.info('WebSocket disconnected');
+ this.reconnect();
+ };
+ this.evtSource.onerror = error => {
+ console.error('WebSocket error:', error);
+ // Ensures that the onclose handler gets triggered for reconnection
+ this.evtSource.close();
+ };
+ };
send(data: string | ArrayBufferLike | Blob | ArrayBufferView): void {
if (this.evtSource.readyState === this.evtSource.OPEN) {
diff --git a/packages/core/src/manager/DevtoolsManager.ts b/packages/core/src/manager/DevtoolsManager.ts
index 1bd5cf44ef56..721d01ca6384 100644
--- a/packages/core/src/manager/DevtoolsManager.ts
+++ b/packages/core/src/manager/DevtoolsManager.ts
@@ -1,7 +1,7 @@
/* eslint-disable no-inner-declarations */
import type { DevToolsConfig } from './devtoolsTypes.js';
-import type { Middleware } from './LogoutManager.js';
import type { Controller, EndpointInterface } from '../index.js';
+import type { Middleware } from '../middlewareTypes.js';
import createReducer from '../state/reducer/createReducer.js';
import type { Manager, State, ActionTypes } from '../types.js';
@@ -89,11 +89,12 @@ if (process.env.NODE_ENV !== 'production') {
* @see https://dataclient.io/docs/api/DevToolsManager
*/
export default class DevToolsManager implements Manager {
- protected declare middleware: Middleware;
+ declare middleware: Middleware;
protected declare devTools: undefined | any;
protected started = false;
protected actions: [ActionTypes, State][] = [];
protected declare controller: Controller;
+ declare skipLogging?: (action: ActionTypes) => boolean;
maxBufferLength = 100;
constructor(
@@ -110,40 +111,20 @@ export default class DevToolsManager implements Manager {
});
// we cut it in half so we should double so we don't lose
if (config?.maxAge) this.maxBufferLength = config.maxAge * 2;
- if (process.env.NODE_ENV !== 'production' && this.devTools) {
- this.devTools.subscribe((msg: any) => {
- switch (msg.type) {
- case 'START':
- this.started = true;
-
- if (this.actions.length) {
- this.actions.forEach(([action, state]) => {
- this.handleAction(action, state);
- });
- this.actions = [];
- }
- break;
- case 'STOP':
- this.started = false;
- break;
- case 'DISPATCH':
- if (msg.payload.type === 'RESET') {
- this.controller.resetEntireStore();
- }
- break;
- }
- });
- }
+ if (skipLogging) this.skipLogging = skipLogging;
+ }
+ static {
/* istanbul ignore if */
/* istanbul ignore next */
- if (process.env.NODE_ENV !== 'production' && this.devTools) {
- this.middleware = controller => {
+ if (process.env.NODE_ENV !== 'production') {
+ this.prototype.middleware = function (controller) {
+ if (!this.devTools) return next => action => next(action);
this.controller = controller;
const reducer = createReducer(controller as any);
let state = controller.getState();
return next => action => {
- const shouldSkip = skipLogging?.(action);
+ const shouldSkip = this.skipLogging?.(action);
const ret = next(action);
if (this.started) {
// we track state changes here since getState() will only update after a batch commit
@@ -159,7 +140,7 @@ export default class DevToolsManager implements Manager {
};
};
} else {
- this.middleware = () => next => action => next(action);
+ this.prototype.middleware = () => next => action => next(action);
}
}
@@ -178,17 +159,33 @@ export default class DevToolsManager implements Manager {
/** Called when initial state is ready */
init(state: State) {
- /* istanbul ignore if */
- if (this.devTools) this.devTools.init(state);
+ if (process.env.NODE_ENV !== 'production' && this.devTools) {
+ this.devTools.init(state);
+ this.devTools.subscribe((msg: any) => {
+ switch (msg.type) {
+ case 'START':
+ this.started = true;
+
+ if (this.actions.length) {
+ this.actions.forEach(([action, state]) => {
+ this.handleAction(action, state);
+ });
+ this.actions = [];
+ }
+ break;
+ case 'STOP':
+ this.started = false;
+ break;
+ case 'DISPATCH':
+ if (msg.payload.type === 'RESET') {
+ this.controller.resetEntireStore();
+ }
+ break;
+ }
+ });
+ }
}
/** Ensures all subscriptions are cleaned up. */
cleanup() {}
-
- /** Attaches Manager to store
- *
- */
- getMiddleware() {
- return this.middleware;
- }
}
diff --git a/packages/core/src/manager/LogoutManager.ts b/packages/core/src/manager/LogoutManager.ts
index 050002e5a015..5d4e5b461abd 100644
--- a/packages/core/src/manager/LogoutManager.ts
+++ b/packages/core/src/manager/LogoutManager.ts
@@ -1,54 +1,42 @@
import { SET_RESPONSE_TYPE } from '../actionTypes.js';
-import Controller from '../controller/Controller.js';
+import type Controller from '../controller/Controller.js';
import { UnknownError } from '../index.js';
-import { ActionTypes, Manager } from '../types.js';
+import type { Manager, Middleware } from '../types.js';
/** Handling network unauthorized indicators like HTTP 401
*
* @see https://dataclient.io/docs/api/LogoutManager
*/
export default class LogoutManager implements Manager {
- protected declare middleware: Middleware;
-
constructor({ handleLogout, shouldLogout }: Props = {}) {
if (handleLogout) this.handleLogout = handleLogout;
if (shouldLogout) this.shouldLogout = shouldLogout;
- this.middleware = controller => next => async action => {
- await next(action);
- if (
- action.type === SET_RESPONSE_TYPE &&
- action.error &&
- this.shouldLogout(action.response)
- ) {
- this.handleLogout(controller);
- }
- };
}
- cleanup() {}
+ middleware: Middleware = controller => next => async action => {
+ await next(action);
+ if (
+ action.type === SET_RESPONSE_TYPE &&
+ action.error &&
+ this.shouldLogout(action.response)
+ ) {
+ this.handleLogout(controller);
+ }
+ };
- getMiddleware() {
- return this.middleware;
- }
+ cleanup() {}
protected shouldLogout(error: UnknownError) {
// 401 indicates reauthorization is needed
return error.status === 401;
}
- handleLogout(controller: Controller) {
+ handleLogout(controller: Controller) {
controller.resetEntireStore();
}
}
-type Dispatch = (value: ActionTypes) => Promise;
-
-// this further restricts the types to be future compatible
-export type Middleware = >(
- controller: C,
-) => (next: C['dispatch']) => C['dispatch'];
-
-type HandleLogout = (controller: Controller) => void;
+type HandleLogout = (controller: Controller) => void;
interface Props {
handleLogout?: HandleLogout;
diff --git a/packages/core/src/manager/NetworkManager.ts b/packages/core/src/manager/NetworkManager.ts
index 4edfa215ead2..7d13706ebd15 100644
--- a/packages/core/src/manager/NetworkManager.ts
+++ b/packages/core/src/manager/NetworkManager.ts
@@ -34,76 +34,68 @@ export default class NetworkManager implements Manager {
protected fetchedAt: { [k: string]: number } = {};
declare readonly dataExpiryLength: number;
declare readonly errorExpiryLength: number;
- protected declare middleware: Middleware;
protected controller: Controller = new Controller();
declare cleanupDate?: number;
constructor({ dataExpiryLength = 60000, errorExpiryLength = 1000 } = {}) {
this.dataExpiryLength = dataExpiryLength;
this.errorExpiryLength = errorExpiryLength;
+ }
- this.middleware = (controller: C) => {
- this.controller = controller;
- return (next: C['dispatch']): C['dispatch'] =>
- (action): Promise => {
- switch (action.type) {
- case FETCH_TYPE:
- this.handleFetch(action);
- // This is the only case that causes any state change
- // It's important to intercept other fetches as we don't want to trigger reducers during
- // render - so we need to stop 'readonly' fetches which can be triggered in render
- if (
- action.endpoint.getOptimisticResponse !== undefined &&
- action.endpoint.sideEffect
- ) {
- return next(action);
+ middleware: Middleware = controller => {
+ this.controller = controller;
+ return next => action => {
+ switch (action.type) {
+ case FETCH_TYPE:
+ this.handleFetch(action);
+ // This is the only case that causes any state change
+ // It's important to intercept other fetches as we don't want to trigger reducers during
+ // render - so we need to stop 'readonly' fetches which can be triggered in render
+ if (
+ action.endpoint.getOptimisticResponse !== undefined &&
+ action.endpoint.sideEffect
+ ) {
+ return next(action);
+ }
+ return Promise.resolve();
+ case SET_RESPONSE_TYPE:
+ // only set after new state is computed
+ return next(action).then(() => {
+ if (action.key in this.fetched) {
+ // Note: meta *must* be set by reducer so this should be safe
+ const error = controller.getState().meta[action.key]?.error;
+ // processing errors result in state meta having error, so we should reject the promise
+ if (error) {
+ this.handleSet(
+ createSetResponse(action.endpoint, {
+ args: action.args,
+ response: error,
+ fetchedAt: action.meta.fetchedAt,
+ error: true,
+ }),
+ );
+ } else {
+ this.handleSet(action);
}
- return Promise.resolve();
- case SET_RESPONSE_TYPE:
- // only set after new state is computed
- return next(action).then(() => {
- if (action.key in this.fetched) {
- // Note: meta *must* be set by reducer so this should be safe
- const error = controller.getState().meta[action.key]?.error;
- // processing errors result in state meta having error, so we should reject the promise
- if (error) {
- this.handleSet(
- createSetResponse(action.endpoint, {
- args: action.args,
- response: error,
- fetchedAt: action.meta.fetchedAt,
- error: true,
- }),
- );
- } else {
- this.handleSet(action);
- }
- }
- });
- case RESET_TYPE: {
- const rejectors = { ...this.rejectors };
+ }
+ });
+ case RESET_TYPE: {
+ const rejectors = { ...this.rejectors };
- this.clearAll();
- return next(action).then(() => {
- // there could be external listeners to the promise
- // this must happen after commit so our own rejector knows not to dispatch an error based on this
- for (const k in rejectors) {
- rejectors[k](new ResetError());
- }
- });
+ this.clearAll();
+ return next(action).then(() => {
+ // there could be external listeners to the promise
+ // this must happen after commit so our own rejector knows not to dispatch an error based on this
+ for (const k in rejectors) {
+ rejectors[k](new ResetError());
}
- default:
- return next(action);
- }
- };
+ });
+ }
+ default:
+ return next(action);
+ }
};
- }
-
- /** Used by DevtoolsManager to determine whether to log an action */
- skipLogging(action: ActionTypes) {
- /* istanbul ignore next */
- return action.type === FETCH_TYPE && action.key in this.fetched;
- }
+ };
/** On mount */
init() {
@@ -117,6 +109,12 @@ export default class NetworkManager implements Manager {
this.cleanupDate = Date.now();
}
+ /** Used by DevtoolsManager to determine whether to log an action */
+ skipLogging(action: ActionTypes) {
+ /* istanbul ignore next */
+ return action.type === FETCH_TYPE && action.key in this.fetched;
+ }
+
allSettled() {
const fetches = Object.values(this.fetched);
if (fetches.length) return Promise.allSettled(fetches);
@@ -241,17 +239,6 @@ export default class NetworkManager implements Manager {
}
}
- /** Attaches NetworkManager to store
- *
- * Intercepts 'rdc/fetch' actions to start requests.
- *
- * Resolve/rejects a request when matching 'rdc/set' event
- * is seen.
- */
- getMiddleware() {
- return this.middleware;
- }
-
/** Ensures only one request for a given key is in flight at any time
*
* Uses key to either retrieve in-flight promise, or if not
diff --git a/packages/core/src/manager/SubscriptionManager.ts b/packages/core/src/manager/SubscriptionManager.ts
index 46d24b8be276..156c7ff8a4ad 100644
--- a/packages/core/src/manager/SubscriptionManager.ts
+++ b/packages/core/src/manager/SubscriptionManager.ts
@@ -41,33 +41,31 @@ export default class SubscriptionManager<
} = {};
protected declare readonly Subscription: S;
- protected declare middleware: Middleware;
protected controller: Controller = new Controller();
constructor(Subscription: S) {
this.Subscription = Subscription;
+ }
- this.middleware = (controller: C) => {
- this.controller = controller;
- return (next: C['dispatch']): C['dispatch'] =>
- action => {
- switch (action.type) {
- case SUBSCRIBE_TYPE:
- try {
- this.handleSubscribe(action);
- } catch (e) {
- console.error(e);
- }
- return Promise.resolve();
- case UNSUBSCRIBE_TYPE:
- this.handleUnsubscribe(action);
- return Promise.resolve();
- default:
- return next(action);
+ middleware: Middleware = controller => {
+ this.controller = controller;
+ return next => action => {
+ switch (action.type) {
+ case SUBSCRIBE_TYPE:
+ try {
+ this.handleSubscribe(action);
+ } catch (e) {
+ console.error(e);
}
- };
+ return Promise.resolve();
+ case UNSUBSCRIBE_TYPE:
+ this.handleUnsubscribe(action);
+ return Promise.resolve();
+ default:
+ return next(action);
+ }
};
- }
+ };
/** Ensures all subscriptions are cleaned up. */
cleanup() {
@@ -110,16 +108,4 @@ export default class SubscriptionManager<
console.error(`Mismatched unsubscribe: ${key} is not subscribed`);
}
}
-
- /** Attaches Manager to store
- *
- * Intercepts 'rdc/subscribe'/'rest-hordc/ribe' to register resources that
- * need to be kept up to date.
- *
- * Will possibly dispatch 'rdc/fetch' or 'rest-hordc/' to keep resources fresh
- *
- */
- getMiddleware() {
- return this.middleware;
- }
}
diff --git a/packages/core/src/manager/__tests__/applyManager.ts b/packages/core/src/manager/__tests__/applyManager.ts
index 333dd00657ea..c1b7984f320c 100644
--- a/packages/core/src/manager/__tests__/applyManager.ts
+++ b/packages/core/src/manager/__tests__/applyManager.ts
@@ -1,5 +1,13 @@
+import { Article } from '__tests__/new';
+
+import { createSet } from '../../controller/actions';
import Controller from '../../controller/Controller';
-import applyManager from '../applyManager';
+import { Dispatch, Middleware } from '../../middlewareTypes';
+import { Manager } from '../../types';
+import applyManager, {
+ ReduxMiddleware,
+ ReduxMiddlewareAPI,
+} from '../applyManager';
import NetworkManager from '../NetworkManager';
function onError(e: any) {
@@ -36,3 +44,55 @@ it('applyManagers should not console.warn() when NetworkManager is provided', ()
warnspy.mockRestore();
}
});
+it('applyManagers should handle legacy Manager.getMiddleware()', () => {
+ let initCount = 0;
+ let actionCount = 0;
+ class MyManager implements Manager {
+ getMiddleware = (): Middleware => ctrl => {
+ initCount++;
+ return next => action => {
+ actionCount++;
+ return next(action);
+ };
+ };
+
+ cleanup() {}
+ }
+ const middlewares = applyManager(
+ [new MyManager(), new NetworkManager()],
+ new Controller(),
+ );
+
+ const rootDispatch = jest.fn((action: any) => {
+ return Promise.resolve();
+ });
+
+ const dispatch = middlewareDispatch(middlewares, rootDispatch);
+
+ expect(initCount).toBe(1);
+ expect(actionCount).toBe(0);
+ expect(rootDispatch.mock.calls.length).toBe(0);
+ dispatch(
+ createSet(Article, { args: [{ id: 1 }], value: { id: 1, title: 'hi' } }),
+ );
+ expect(initCount).toBe(1);
+ expect(actionCount).toBe(1);
+ expect(rootDispatch.mock.calls.length).toBe(1);
+});
+
+function middlewareDispatch(
+ middlewares: ReduxMiddleware[],
+ rootDispatch: Dispatch,
+) {
+ const middlewareAPI: ReduxMiddlewareAPI = {
+ getState: () => ({}),
+ dispatch: action => rootDispatch(action),
+ };
+ const comp = compose(
+ middlewares.map(middleware => middleware(middlewareAPI)),
+ );
+ const dispatch = comp(rootDispatch);
+ return dispatch;
+}
+const compose = (fns: ((...args: any) => any)[]) => (initial: any) =>
+ fns.reduceRight((v, f) => f(v), initial);
diff --git a/packages/core/src/manager/__tests__/logoutManager.ts b/packages/core/src/manager/__tests__/logoutManager.ts
index 7ffefb11dccf..afee96909061 100644
--- a/packages/core/src/manager/__tests__/logoutManager.ts
+++ b/packages/core/src/manager/__tests__/logoutManager.ts
@@ -21,11 +21,10 @@ describe('LogoutManager', () => {
const manager = new LogoutManager();
const getState = () => initialState;
- describe('getMiddleware()', () => {
+ describe('middleware', () => {
it('should return the same value every call', () => {
- const a = manager.getMiddleware();
- expect(a).toBe(manager.getMiddleware());
- expect(a).toBe(manager.getMiddleware());
+ const a = manager.middleware;
+ expect(a).toBe(manager.middleware);
});
});
@@ -33,7 +32,6 @@ describe('LogoutManager', () => {
afterEach(() => {
jest.useRealTimers();
});
- const middleware = manager.getMiddleware();
const next = jest.fn();
const dispatch = jest.fn(action => Promise.resolve());
const controller = new Controller({ dispatch, getState });
@@ -48,7 +46,7 @@ describe('LogoutManager', () => {
args: [{ id: 5 }],
response: { id: 5, title: 'hi' },
});
- await middleware(API)(next)(action);
+ await manager.middleware(API)(next)(action);
expect(dispatch.mock.calls.length).toBe(0);
});
@@ -60,7 +58,7 @@ describe('LogoutManager', () => {
response: error,
error: true,
});
- await middleware(API)(next)(action);
+ await manager.middleware(API)(next)(action);
expect(dispatch.mock.calls.length).toBe(0);
});
@@ -72,7 +70,7 @@ describe('LogoutManager', () => {
response: error,
error: true,
});
- await middleware(API)(next)(action);
+ await manager.middleware(API)(next)(action);
expect(dispatch.mock.calls.length).toBe(1);
expect(dispatch.mock.calls[0][0]?.type).toBe(RESET_TYPE);
@@ -85,7 +83,7 @@ describe('LogoutManager', () => {
return error.status === 403;
},
handleLogout,
- }).getMiddleware();
+ }).middleware;
const error: any = new Error('network failed');
error.status = 403;
const action = createSetResponse(CoolerArticleResource.get, {
@@ -102,7 +100,7 @@ describe('LogoutManager', () => {
const action = { type: FETCH_TYPE };
next.mockReset();
- await middleware(API)(next)(action as any);
+ await manager.middleware(API)(next)(action as any);
expect(next.mock.calls.length).toBe(1);
});
diff --git a/packages/core/src/manager/__tests__/manager.ts b/packages/core/src/manager/__tests__/manager.ts
index 4de5fc3e1ed8..956a58ecdcd6 100644
--- a/packages/core/src/manager/__tests__/manager.ts
+++ b/packages/core/src/manager/__tests__/manager.ts
@@ -1,9 +1,8 @@
import Controller from '../../controller/Controller';
-import { Middleware } from '../../middlewareTypes';
import { ActionTypes } from '../../types';
import NetworkManager from '../NetworkManager';
-const middleware: Middleware = new NetworkManager().getMiddleware();
+const netMgr = new NetworkManager();
it('middlewares should compose with non-data-client middlewares', () => {
type AnotherAction = {
type: 'BOB';
@@ -30,7 +29,7 @@ it('middlewares should compose with non-data-client middlewares', () => {
counter++;
};
- const [a, b] = [middleware(API), nonRHMiddleware(API)];
+ const [a, b] = [netMgr.middleware(API), nonRHMiddleware(API)];
const dispA = a(b(dispatch));
const dispB = b(a(dispatch));
expect(dispatch.mock.calls.length).toBe(0);
diff --git a/packages/core/src/manager/__tests__/networkManager.ts b/packages/core/src/manager/__tests__/networkManager.ts
index 1817fdf7368d..bd6de8227ccc 100644
--- a/packages/core/src/manager/__tests__/networkManager.ts
+++ b/packages/core/src/manager/__tests__/networkManager.ts
@@ -33,18 +33,17 @@ describe('NetworkManager', () => {
expect(hacked.getHacked()).toEqual(initialState);
});
- describe('getMiddleware()', () => {
+ describe('middleware', () => {
it('should return the same value every call', () => {
- const a = manager.getMiddleware();
- expect(a).toBe(manager.getMiddleware());
- expect(a).toBe(manager.getMiddleware());
+ const a = manager.middleware;
+ expect(a).toBe(manager.middleware);
});
it('should return the different value for a different instance', () => {
- const a = manager.getMiddleware();
+ const a = manager.middleware;
const manager2 = new NetworkManager();
- const a2 = manager2.getMiddleware();
+ const a2 = manager2.middleware;
expect(a).not.toBe(a2);
- expect(a2).toBe(manager2.getMiddleware());
+ expect(a2).toBe(manager2.middleware);
manager2.cleanup();
});
});
@@ -132,10 +131,8 @@ describe('NetworkManager', () => {
(fetchRejectAction.meta.promise as any).catch((e: unknown) => {});
let NM: NetworkManager;
- let middleware: Middleware;
beforeEach(() => {
NM = new NetworkManager({ dataExpiryLength: 42, errorExpiryLength: 7 });
- middleware = NM.getMiddleware();
});
afterEach(() => {
NM.cleanup();
@@ -152,7 +149,7 @@ describe('NetworkManager', () => {
},
);
- middleware(API)(next)(fetchResolveAction);
+ NM.middleware(API)(next)(fetchResolveAction);
const response = await fetchResolveAction.endpoint(
...fetchResolveAction.args,
@@ -188,7 +185,7 @@ describe('NetworkManager', () => {
},
);
- middleware(API)(next)(fetchSetWithUpdatersAction);
+ NM.middleware(API)(next)(fetchSetWithUpdatersAction);
const response = await fetchSetWithUpdatersAction.endpoint(
...fetchSetWithUpdatersAction.args,
@@ -224,7 +221,7 @@ describe('NetworkManager', () => {
},
);
- middleware(API)(next)(fetchRpcWithUpdatersAction);
+ NM.middleware(API)(next)(fetchRpcWithUpdatersAction);
const response = await fetchRpcWithUpdatersAction.endpoint(
...fetchRpcWithUpdatersAction.args,
@@ -260,7 +257,7 @@ describe('NetworkManager', () => {
},
);
- middleware(API)(next)(fetchRpcWithUpdatersAndOptimisticAction);
+ NM.middleware(API)(next)(fetchRpcWithUpdatersAndOptimisticAction);
const response = await fetchRpcWithUpdatersAndOptimisticAction.endpoint(
...fetchRpcWithUpdatersAndOptimisticAction.args,
@@ -293,7 +290,7 @@ describe('NetworkManager', () => {
},
);
- middleware(API)(() => Promise.resolve())({
+ NM.middleware(API)(() => Promise.resolve())({
...fetchResolveAction,
endpoint: detailEndpoint.extend({ dataExpiryLength: 314 }),
});
@@ -314,7 +311,7 @@ describe('NetworkManager', () => {
},
);
- middleware(API)(() => Promise.resolve())({
+ NM.middleware(API)(() => Promise.resolve())({
...fetchResolveAction,
endpoint: detailEndpoint.extend({ dataExpiryLength: undefined }),
});
@@ -337,7 +334,7 @@ describe('NetworkManager', () => {
);
try {
- await middleware(API)(next)(fetchRejectAction);
+ await NM.middleware(API)(next)(fetchRejectAction);
} catch (error) {
expect(next).not.toHaveBeenCalled();
expect(dispatch).toHaveBeenCalledWith({
@@ -363,7 +360,7 @@ describe('NetworkManager', () => {
);
try {
- await middleware(API)(() => Promise.resolve())({
+ await NM.middleware(API)(() => Promise.resolve())({
...fetchRejectAction,
meta: {
...fetchRejectAction.meta,
@@ -386,7 +383,7 @@ describe('NetworkManager', () => {
);
try {
- await middleware(API)(() => Promise.resolve())({
+ await NM.middleware(API)(() => Promise.resolve())({
...fetchRejectAction,
meta: {
...fetchRejectAction.meta,
diff --git a/packages/core/src/manager/__tests__/subscriptionManager.ts b/packages/core/src/manager/__tests__/subscriptionManager.ts
index ac635783f2ee..fc1b132d3576 100644
--- a/packages/core/src/manager/__tests__/subscriptionManager.ts
+++ b/packages/core/src/manager/__tests__/subscriptionManager.ts
@@ -27,11 +27,10 @@ describe('SubscriptionManager', () => {
const manager = new SubscriptionManager(TestSubscription);
const getState = () => initialState;
- describe('getMiddleware()', () => {
+ describe('middleware', () => {
it('should return the same value every call', () => {
- const a = manager.getMiddleware();
- expect(a).toBe(manager.getMiddleware());
- expect(a).toBe(manager.getMiddleware());
+ const a = manager.middleware;
+ expect(a).toBe(manager.middleware);
});
});
@@ -74,7 +73,6 @@ describe('SubscriptionManager', () => {
}
const manager = new SubscriptionManager(TestSubscription);
- const middleware = manager.getMiddleware();
const next = jest.fn();
const dispatch = () => Promise.resolve();
const controller = new Controller({ dispatch, getState });
@@ -86,14 +84,14 @@ describe('SubscriptionManager', () => {
);
it('subscribe should add a subscription', () => {
const action = createSubscribeAction({ id: 5 });
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect(next).not.toHaveBeenCalled();
expect((manager as any).subscriptions[action.key]).toBeDefined();
});
it('subscribe should add a subscription (no frequency)', () => {
const action = createSubscribeAction({ id: 597 });
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect(next).not.toHaveBeenCalled();
expect((manager as any).subscriptions[action.key]).toBeDefined();
@@ -101,19 +99,19 @@ describe('SubscriptionManager', () => {
it('subscribe with same should call subscription.add', () => {
const action = createSubscribeAction({ id: 5, title: 'four' });
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect(
(manager as any).subscriptions[action.key].add.mock.calls.length,
).toBe(1);
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect(
(manager as any).subscriptions[action.key].add.mock.calls.length,
).toBe(2);
});
it('subscribe with another should create another', () => {
const action = createSubscribeAction({ id: 7, title: 'four cakes' });
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect((manager as any).subscriptions[action.key]).toBeDefined();
expect(
@@ -134,13 +132,13 @@ describe('SubscriptionManager', () => {
() => true,
);
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect((manager as any).subscriptions[action.key]).not.toBeDefined();
});
it('unsubscribe should delete when remove returns true (no frequency)', () => {
- middleware(API)(next)(
+ manager.middleware(API)(next)(
createSubscribeAction({ id: 50, title: 'four cakes' }),
);
@@ -149,7 +147,7 @@ describe('SubscriptionManager', () => {
() => true,
);
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect((manager as any).subscriptions[action.key]).not.toBeDefined();
});
@@ -160,7 +158,7 @@ describe('SubscriptionManager', () => {
() => false,
);
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect((manager as any).subscriptions[action.key]).toBeDefined();
expect(
@@ -174,7 +172,7 @@ describe('SubscriptionManager', () => {
const action = createUnsubscribeAction({ id: 25 });
- middleware(API)(next)(action);
+ manager.middleware(API)(next)(action);
expect((manager as any).subscriptions[action.key]).not.toBeDefined();
@@ -190,7 +188,7 @@ describe('SubscriptionManager', () => {
const action = { type: SET_RESPONSE_TYPE };
next.mockReset();
- middleware(API)(next)(action as any);
+ manager.middleware(API)(next)(action as any);
expect(next.mock.calls.length).toBe(1);
});
diff --git a/packages/core/src/manager/applyManager.ts b/packages/core/src/manager/applyManager.ts
index 4b0b697a6d8f..edfb1020d265 100644
--- a/packages/core/src/manager/applyManager.ts
+++ b/packages/core/src/manager/applyManager.ts
@@ -1,12 +1,11 @@
import NetworkManager from './NetworkManager.js';
import type Controller from '../controller/Controller.js';
-import type { Reducer, Dispatch, ReducerState } from '../middlewareTypes.js';
import { Manager } from '../types.js';
export default function applyManager(
managers: Manager[],
controller: Controller,
-): Middleware[] {
+): ReduxMiddleware[] {
/* istanbul ignore next */
if (
process.env.NODE_ENV !== 'production' &&
@@ -18,25 +17,38 @@ export default function applyManager(
);
}
return managers.map((manager, i) => {
- const middleware = manager.getMiddleware();
+ if (!manager.middleware) manager.middleware = manager.getMiddleware?.();
return ({ dispatch, getState }) => {
if (i === 0) {
(controller as any).dispatch = dispatch;
(controller as any).getState = getState;
}
// controller is a superset of the middleware API
- return middleware(controller as Controller);
+ return (manager as Manager & { middleware: ReduxMiddleware }).middleware(
+ controller as Controller,
+ );
};
});
}
/* These should be compatible with redux */
-export interface MiddlewareAPI<
+export interface ReduxMiddlewareAPI<
R extends Reducer = Reducer,
> {
getState: () => ReducerState;
- dispatch: Dispatch;
+ dispatch: ReactDispatch;
}
-export type Middleware = >({
+export type ReduxMiddleware = >({
dispatch,
-}: MiddlewareAPI) => (next: Dispatch) => Dispatch;
+}: ReduxMiddlewareAPI) => (next: ReactDispatch) => ReactDispatch;
+
+/* The next are types from React; but we don't want dependencies on it */
+export type ReactDispatch> = (
+ action: ReducerAction,
+) => Promise;
+
+export type Reducer = (prevState: S, action: A) => S;
+export type ReducerState> =
+ R extends Reducer ? S : never;
+export type ReducerAction> =
+ R extends Reducer ? A : never;
diff --git a/packages/core/src/middlewareTypes.ts b/packages/core/src/middlewareTypes.ts
index 5a25db4adfb9..6edd936dce56 100644
--- a/packages/core/src/middlewareTypes.ts
+++ b/packages/core/src/middlewareTypes.ts
@@ -1,15 +1,14 @@
import type Controller from './controller/Controller.js';
import type { ActionTypes, State } from './types.js';
-type ClientDispatch = (value: Actions) => Promise;
+export type Dispatch = (value: Actions) => Promise;
-export interface MiddlewareAPI
- extends Controller> {}
+export interface MiddlewareAPI extends Controller> {}
export interface MiddlewareController
- extends Controller> {}
+ extends Controller> {}
-/** @see https://dataclient.io/docs/api/Manager#getmiddleware */
+/** @see https://dataclient.io/docs/api/Manager#middleware */
export type Middleware = <
C extends MiddlewareController,
>(
@@ -20,14 +19,3 @@ export type DataClientReducer = (
prevState: State,
action: ActionTypes,
) => State;
-
-/* The next are types from React; but we don't want dependencies on it */
-export type Dispatch> = (
- action: ReducerAction,
-) => Promise;
-
-export type Reducer = (prevState: S, action: A) => S;
-export type ReducerState> =
- R extends Reducer ? S : never;
-export type ReducerAction> =
- R extends Reducer ? A : never;
diff --git a/packages/core/src/types.ts b/packages/core/src/types.ts
index 1a5e6af81eb1..b94885e14a6d 100644
--- a/packages/core/src/types.ts
+++ b/packages/core/src/types.ts
@@ -61,7 +61,9 @@ export interface State {
*/
export interface Manager {
/** @see https://dataclient.io/docs/api/Manager#getmiddleware */
- getMiddleware(): Middleware;
+ getMiddleware?(): Middleware;
+ /** @see https://dataclient.io/docs/api/Manager#middleware */
+ middleware?: Middleware;
/** @see https://dataclient.io/docs/api/Manager#cleanup */
cleanup(): void;
/** @see https://dataclient.io/docs/api/Manager#init */
diff --git a/packages/react/README.md b/packages/react/README.md
index 94e866970179..8aa11b409eff 100644
--- a/packages/react/README.md
+++ b/packages/react/README.md
@@ -159,7 +159,7 @@ const totalVotesForUser = useQuery(queryTotalVotes, { userId });
```ts
class LoggingManager implements Manager {
- getMiddleware = (): Middleware => controller => next => async action => {
+ middleware: Middleware => controller => next => async action => {
console.log('before', action, controller.getState());
await next(action);
console.log('after', action, controller.getState());
diff --git a/packages/react/src/components/DataProvider.tsx b/packages/react/src/components/DataProvider.tsx
index 91c145ac2166..e5b39d0c07b2 100644
--- a/packages/react/src/components/DataProvider.tsx
+++ b/packages/react/src/components/DataProvider.tsx
@@ -5,7 +5,7 @@ import {
applyManager,
} from '@data-client/core';
import type { State, Manager } from '@data-client/core';
-import React, { useMemo, useRef } from 'react';
+import React, { useCallback, useMemo, useRef } from 'react';
import type { JSX } from 'react';
import DataStore from './DataStore.js';
@@ -59,8 +59,19 @@ See https://dataclient.io/docs/guides/ssr.`,
const managersRef: React.MutableRefObject = useRef(managers);
if (!managersRef.current) managersRef.current = getDefaultManagers();
- // eslint-disable-next-line react-hooks/exhaustive-deps
- const memodManagers = useMemo(() => managersRef.current, managersRef.current);
+ // run in a useEffect in DataStore
+ const mgrEffect = useCallback(() => {
+ managersRef.current.forEach(manager => {
+ manager.init?.(initialState);
+ });
+ return () => {
+ managersRef.current.forEach(manager => {
+ manager.cleanup();
+ });
+ };
+ // we don't support initialState changes
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, managersRef.current);
// Makes manager middleware compatible with redux-style middleware (by a wrapper enhancement to provide controller API)
const middlewares = useMemo(
@@ -76,7 +87,7 @@ See https://dataclient.io/docs/guides/ssr.`,
return (
void;
+ middlewares: GenericMiddleware[];
initialState: State;
controller: Controller;
}
@@ -24,7 +24,7 @@ interface StoreProps {
*/
function DataStore({
children,
- managers,
+ mgrEffect,
middlewares,
initialState,
controller,
@@ -39,19 +39,8 @@ function DataStore({
[masterReducer, state],
);
- // if we change out the manager we need to make sure it has no hanging async
- useEffect(() => {
- for (let i = 0; i < managers.length; ++i) {
- managers[i].init?.(state);
- }
- return () => {
- for (let i = 0; i < managers.length; ++i) {
- managers[i].cleanup();
- }
- };
- // we're ignoring state here, because it shouldn't trigger inits
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [managers]);
+ // only run once everything is prepared
+ useEffect(mgrEffect, [mgrEffect]);
return (
diff --git a/packages/react/src/components/__tests__/provider.tsx b/packages/react/src/components/__tests__/provider.tsx
index 7b963b56b751..52f0bb1be27d 100644
--- a/packages/react/src/components/__tests__/provider.tsx
+++ b/packages/react/src/components/__tests__/provider.tsx
@@ -158,23 +158,16 @@ describe('', () => {
it('should ignore dispatches after unmount', async () => {
class InjectorManager implements Manager {
- protected declare middleware: Middleware;
declare controller: Controller;
- constructor() {
- this.middleware = controller => {
- this.controller = controller;
- return next => async action => {
- await next(action);
- };
- };
- }
-
cleanup() {}
- getMiddleware() {
- return this.middleware;
- }
+ middleware: Middleware = controller => {
+ this.controller = controller;
+ return next => async action => {
+ await next(action);
+ };
+ };
}
const injector = new InjectorManager();
const managers = [injector, ...getDefaultManagers()];
diff --git a/website/src/components/Playground/editor-types/@data-client/core.d.ts b/website/src/components/Playground/editor-types/@data-client/core.d.ts
index 7db05473f87e..fe7ec4bc32ad 100644
--- a/website/src/components/Playground/editor-types/@data-client/core.d.ts
+++ b/website/src/components/Playground/editor-types/@data-client/core.d.ts
@@ -378,17 +378,13 @@ interface GCAction {
}
type ActionTypes = FetchAction | OptimisticAction | SetAction | SetResponseAction | SubscribeAction | UnsubscribeAction | InvalidateAction | InvalidateAllAction | ExpireAllAction | ResetAction | GCAction;
-type ClientDispatch = (value: Actions) => Promise;
-interface MiddlewareAPI$1 extends Controller> {
+type Dispatch = (value: Actions) => Promise;
+interface MiddlewareAPI extends Controller> {
}
-interface MiddlewareController extends Controller> {
+interface MiddlewareController extends Controller> {
}
-/** @see https://dataclient.io/docs/api/Manager#getmiddleware */
-type Middleware$2 = >(controller: C) => (next: C['dispatch']) => C['dispatch'];
-type Dispatch$1> = (action: ReducerAction) => Promise;
-type Reducer = (prevState: S, action: A) => S;
-type ReducerState> = R extends Reducer ? S : never;
-type ReducerAction> = R extends Reducer ? A : never;
+/** @see https://dataclient.io/docs/api/Manager#middleware */
+type Middleware = >(controller: C) => (next: C['dispatch']) => C['dispatch'];
type PK = string;
/** Normalized state for Reactive Data Client
@@ -435,7 +431,9 @@ interface State {
*/
interface Manager {
/** @see https://dataclient.io/docs/api/Manager#getmiddleware */
- getMiddleware(): Middleware$2;
+ getMiddleware?(): Middleware;
+ /** @see https://dataclient.io/docs/api/Manager#middleware */
+ middleware?: Middleware;
/** @see https://dataclient.io/docs/api/Manager#cleanup */
cleanup(): void;
/** @see https://dataclient.io/docs/api/Manager#init */
@@ -647,19 +645,19 @@ declare class NetworkManager implements Manager {
};
readonly dataExpiryLength: number;
readonly errorExpiryLength: number;
- protected middleware: Middleware$2;
protected controller: Controller;
cleanupDate?: number;
constructor({ dataExpiryLength, errorExpiryLength }?: {
dataExpiryLength?: number | undefined;
errorExpiryLength?: number | undefined;
});
- /** Used by DevtoolsManager to determine whether to log an action */
- skipLogging(action: ActionTypes): boolean;
+ middleware: Middleware;
/** On mount */
init(): void;
/** Ensures all promises are completed by rejecting remaining. */
cleanup(): void;
+ /** Used by DevtoolsManager to determine whether to log an action */
+ skipLogging(action: ActionTypes): boolean;
allSettled(): Promise[]> | undefined;
/** Clear all promise state */
protected clearAll(): void;
@@ -680,14 +678,6 @@ declare class NetworkManager implements Manager {
* Will resolve the promise associated with set key.
*/
protected handleSet(action: SetResponseAction): void;
- /** Attaches NetworkManager to store
- *
- * Intercepts 'rdc/fetch' actions to start requests.
- *
- * Resolve/rejects a request when matching 'rdc/set' event
- * is seen.
- */
- getMiddleware(): Middleware$2;
/** Ensures only one request for a given key is in flight at any time
*
* Uses key to either retrieve in-flight promise, or if not
@@ -706,12 +696,16 @@ declare class NetworkManager implements Manager {
protected idleCallback(callback: (...args: any[]) => void, options?: IdleRequestOptions): void;
}
-declare function applyManager(managers: Manager[], controller: Controller): Middleware$1[];
-interface MiddlewareAPI = Reducer> {
+declare function applyManager(managers: Manager[], controller: Controller): ReduxMiddleware[];
+interface ReduxMiddlewareAPI = Reducer> {
getState: () => ReducerState;
- dispatch: Dispatch$1;
+ dispatch: ReactDispatch;
}
-type Middleware$1 = >({ dispatch, }: MiddlewareAPI) => (next: Dispatch$1) => Dispatch$1;
+type ReduxMiddleware = >({ dispatch, }: ReduxMiddlewareAPI) => (next: ReactDispatch) => ReactDispatch;
+type ReactDispatch> = (action: ReducerAction) => Promise;
+type Reducer = (prevState: S, action: A) => S;
+type ReducerState> = R extends Reducer ? S : never;
+type ReducerAction> = R extends Reducer ? A : never;
declare function createSubscription(endpoint: E, { args }: {
args: readonly [...Parameters];
@@ -832,9 +826,9 @@ declare class SubscriptionManager;
};
protected readonly Subscription: S;
- protected middleware: Middleware$2;
protected controller: Controller;
constructor(Subscription: S);
+ middleware: Middleware;
/** Ensures all subscriptions are cleaned up. */
cleanup(): void;
/** Called when middleware intercepts 'rdc/subscribe' action.
@@ -845,15 +839,6 @@ declare class SubscriptionManager): void;
-}
-type Dispatch = (value: ActionTypes) => Promise;
-type Middleware = >(controller: C) => (next: C['dispatch']) => C['dispatch'];
-type HandleLogout = (controller: Controller) => void;
-interface Props {
- handleLogout?: HandleLogout;
- shouldLogout?: (error: UnknownError) => boolean;
-}
-
/** Integrates with https://github.com/reduxjs/redux-devtools
*
* Options: https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md
@@ -1127,11 +1092,12 @@ interface Props {
* @see https://dataclient.io/docs/api/DevToolsManager
*/
declare class DevToolsManager implements Manager {
- protected middleware: Middleware;
+ middleware: Middleware;
protected devTools: undefined | any;
protected started: boolean;
protected actions: [ActionTypes, State][];
protected controller: Controller;
+ skipLogging?: (action: ActionTypes) => boolean;
maxBufferLength: number;
constructor(config?: DevToolsConfig, skipLogging?: (action: ActionTypes) => boolean);
handleAction(action: any, state: any): void;
@@ -1139,10 +1105,23 @@ declare class DevToolsManager implements Manager {
init(state: State): void;
/** Ensures all subscriptions are cleaned up. */
cleanup(): void;
- /** Attaches Manager to store
- *
- */
- getMiddleware(): Middleware;
}
-export { AbstractInstanceType, ActionMeta, ActionTypes, ConnectionListener, Controller, DataClientDispatch, DefaultConnectionListener, Denormalize, DenormalizeNullable, DevToolsConfig, DevToolsManager, Dispatch$1 as Dispatch, EndpointExtraOptions, EndpointInterface, EndpointUpdateFunction, EntityInterface, ErrorTypes, ExpireAllAction, ExpiryStatus, FetchAction, FetchFunction, FetchMeta, GCAction, GenericDispatch, InvalidateAction, InvalidateAllAction, LogoutManager, Manager, Middleware$2 as Middleware, MiddlewareAPI$1 as MiddlewareAPI, NI, NetworkError, NetworkManager, Normalize, NormalizeNullable, OptimisticAction, PK, PollingSubscription, Queryable, ResetAction, ResetError, ResolveType, ResultEntry, Schema, SchemaArgs, SchemaClass, SetAction, SetResponseAction, SetResponseActionBase, SetResponseActionError, SetResponseActionSuccess, State, SubscribeAction, SubscriptionManager, UnknownError, UnsubscribeAction, UpdateFunction, internal_d as __INTERNAL__, actionTypes_d as actionTypes, index_d as actions, applyManager, createReducer, initialState };
+/** Handling network unauthorized indicators like HTTP 401
+ *
+ * @see https://dataclient.io/docs/api/LogoutManager
+ */
+declare class LogoutManager implements Manager {
+ constructor({ handleLogout, shouldLogout }?: Props);
+ middleware: Middleware;
+ cleanup(): void;
+ protected shouldLogout(error: UnknownError): boolean;
+ handleLogout(controller: Controller): void;
+}
+type HandleLogout = (controller: Controller) => void;
+interface Props {
+ handleLogout?: HandleLogout;
+ shouldLogout?: (error: UnknownError) => boolean;
+}
+
+export { AbstractInstanceType, ActionMeta, ActionTypes, ConnectionListener, Controller, DataClientDispatch, DefaultConnectionListener, Denormalize, DenormalizeNullable, DevToolsConfig, DevToolsManager, Dispatch, EndpointExtraOptions, EndpointInterface, EndpointUpdateFunction, EntityInterface, ErrorTypes, ExpireAllAction, ExpiryStatus, FetchAction, FetchFunction, FetchMeta, GCAction, GenericDispatch, InvalidateAction, InvalidateAllAction, LogoutManager, Manager, Middleware, MiddlewareAPI, NI, NetworkError, NetworkManager, Normalize, NormalizeNullable, OptimisticAction, PK, PollingSubscription, Queryable, ResetAction, ResetError, ResolveType, ResultEntry, Schema, SchemaArgs, SchemaClass, SetAction, SetResponseAction, SetResponseActionBase, SetResponseActionError, SetResponseActionSuccess, State, SubscribeAction, SubscriptionManager, UnknownError, UnsubscribeAction, UpdateFunction, internal_d as __INTERNAL__, actionTypes_d as actionTypes, index_d as actions, applyManager, createReducer, initialState };