From 8eaac3b4c64255fda02253693297257eafcee4cd Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Mon, 22 Jul 2024 14:39:59 +0200 Subject: [PATCH] feat: Add configuration to getDefaultManagers() --- .changeset/pink-dodos-protect.md | 6 + .changeset/witty-papayas-battle.md | 30 ++++ docs/core/api/DataProvider.md | 4 +- docs/core/api/DevToolsManager.md | 19 +-- docs/core/api/Manager.md | 6 +- docs/core/api/NetworkManager.md | 2 +- docs/core/api/getDefaultManagers.md | 128 ++++++++++++++++++ docs/core/concepts/managers.md | 2 +- docs/core/getting-started/debugging.md | 17 +-- examples/coin-app/src/getManagers.ts | 21 +++ examples/coin-app/src/index.tsx | 48 +------ .../core/src/manager/SubscriptionManager.ts | 5 +- .../react/src/components/DataProvider.tsx | 29 +--- .../__snapshots__/getDefaultManagers.tsx.snap | 9 ++ .../__tests__/getDefaultManagers.tsx | 106 +++++++++++++++ .../components/__tests__/provider.native.tsx | 10 +- .../src/components/__tests__/provider.tsx | 11 +- .../src/components/getDefaultManagers.tsx | 76 +++++++++++ packages/react/src/components/index.ts | 3 +- ...tive.ts => IdlingNetworkManager.native.ts} | 1 + ...workManager.ts => IdlingNetworkManager.ts} | 1 + .../src/managers/__tests__/RIC.native.ts | 13 +- .../react/src/managers/__tests__/RIC.web.ts | 8 +- packages/react/src/managers/index.ts | 2 +- website/sidebars.json | 4 + .../editor-types/@data-client/core.d.ts | 7 +- .../editor-types/@data-client/react.d.ts | 21 ++- 27 files changed, 451 insertions(+), 138 deletions(-) create mode 100644 .changeset/pink-dodos-protect.md create mode 100644 .changeset/witty-papayas-battle.md create mode 100644 docs/core/api/getDefaultManagers.md create mode 100644 examples/coin-app/src/getManagers.ts create mode 100644 packages/react/src/components/__tests__/__snapshots__/getDefaultManagers.tsx.snap create mode 100644 packages/react/src/components/__tests__/getDefaultManagers.tsx create mode 100644 packages/react/src/components/getDefaultManagers.tsx rename packages/react/src/managers/{NetworkManager.native.ts => IdlingNetworkManager.native.ts} (87%) rename packages/react/src/managers/{NetworkManager.ts => IdlingNetworkManager.ts} (78%) diff --git a/.changeset/pink-dodos-protect.md b/.changeset/pink-dodos-protect.md new file mode 100644 index 000000000000..d1b7eb6ae3ed --- /dev/null +++ b/.changeset/pink-dodos-protect.md @@ -0,0 +1,6 @@ +--- +'@data-client/react': patch +'@data-client/core': patch +--- + +Add jsdocs to IdlingNetworkManager diff --git a/.changeset/witty-papayas-battle.md b/.changeset/witty-papayas-battle.md new file mode 100644 index 000000000000..49bbe58692ef --- /dev/null +++ b/.changeset/witty-papayas-battle.md @@ -0,0 +1,30 @@ +--- +'@data-client/react': patch +--- + +Add configuration to [getDefaultManagers()](https://dataclient.io/docs/api/getDefaultManagers) + +```ts +// completely remove DevToolsManager +const managers = getDefaultManagers({ devToolsManager: null }); +``` + +```ts +// easier configuration +const managers = getDefaultManagers({ + devToolsManager: { + // double latency to help with high frequency updates + latency: 1000, + // skip websocket updates as these are too spammy + predicate: (state, action) => + action.type !== actionTypes.SET_TYPE || action.schema !== Ticker, + } +}); +``` + +```ts +// passing instance allows us to use custom classes as well +const managers = getDefaultManagers({ + networkManager: new CustomNetworkManager(), +}); +``` \ No newline at end of file diff --git a/docs/core/api/DataProvider.md b/docs/core/api/DataProvider.md index 907e4f99992c..3473bf0cade2 100644 --- a/docs/core/api/DataProvider.md +++ b/docs/core/api/DataProvider.md @@ -73,9 +73,9 @@ be useful for testing, or rehydrating the cache state when using server side ren ### managers?: Manager[] {#managers} -List of [Manager](./Manager.md#provided-managers)s use. This is the main extensibility point of the provider. +List of [Manager](./Manager.md)s use. This is the main extensibility point of the provider. -`getDefaultManagers()` can be used to extend the default managers. +[getDefaultManagers()](./getDefaultManagers.md) can be used to extend the default managers. Default Production: diff --git a/docs/core/api/DevToolsManager.md b/docs/core/api/DevToolsManager.md index 29e597021686..4f2bcf55417c 100644 --- a/docs/core/api/DevToolsManager.md +++ b/docs/core/api/DevToolsManager.md @@ -27,7 +27,7 @@ browser to get started. [Arguments](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md) to send to redux devtools. -For example, we can enable the `trace` option to help track down where actions are dispatched from. +For example, we can enable the [trace](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#trace) option to help track down where actions are dispatched from. ```tsx title="index.tsx" import { @@ -37,19 +37,10 @@ import { } from '@data-client/react'; import ReactDOM from 'react-dom'; -const managers = - process.env.NODE_ENV !== 'production' - ? [ - // highlight-start - new DevToolsManager({ - trace: true, - }), - // highlight-end - ...getDefaultManagers().filter( - manager => manager.constructor.name !== 'DevToolsManager', - ), - ] - : getDefaultManagers(); +const managers = getDefaultManagers({ + // highlight-next-line + devToolsManager: { trace: true }, +}); ReactDOM.createRoot(document.body).render( diff --git a/docs/core/api/Manager.md b/docs/core/api/Manager.md index 6831dac1aa6b..a5d7ecfb6d75 100644 --- a/docs/core/api/Manager.md +++ b/docs/core/api/Manager.md @@ -14,14 +14,14 @@ import useBaseUrl from '@docusaurus/useBaseUrl'; # Manager -Managers are singletons that orchestrate the complex asynchronous behavior of `Reactive Data Client`. -Several managers are provided by `Reactive Data Client` and used by default; however there is nothing +`Managers` are singletons that orchestrate the complex asynchronous behavior of Data Client. +Several managers are provided by Data Client and used by default; however there is nothing stopping other compatible managers to be built that expand the functionality. We encourage PRs or complimentary libraries! While managers often have complex internal state and methods - the exposed interface is quite simple. Because of this, it is encouraged to keep any supporting state or methods marked at protected by -typescript. Managers have three exposed pieces - the constructor to build initial state and +typescript. `Managers` have three exposed pieces - the constructor to build initial state and take any parameters; a simple cleanup() method to tear down any dangling pieces like setIntervals() or unresolved Promises; and finally getMiddleware() - providing the mechanism to hook into the flux data flow. diff --git a/docs/core/api/NetworkManager.md b/docs/core/api/NetworkManager.md index f00751d8cfc1..e2dc17fe319d 100644 --- a/docs/core/api/NetworkManager.md +++ b/docs/core/api/NetworkManager.md @@ -16,7 +16,7 @@ it is able to dedupe identical requests if they are made using the throttle flag ## Members -### constructor(dataExpiryLength = 60000, errorExpiryLength = 1000) {#constructor} +### constructor(\{ dataExpiryLength = 60000, errorExpiryLength = 1000 }) {#constructor} Arguments represent the default time (in miliseconds) before a resource is considered 'stale'. diff --git a/docs/core/api/getDefaultManagers.md b/docs/core/api/getDefaultManagers.md new file mode 100644 index 000000000000..4a1bc2db2c52 --- /dev/null +++ b/docs/core/api/getDefaultManagers.md @@ -0,0 +1,128 @@ +--- +title: getDefaultManagers() - Configuring managers for DataProvider +sidebar_label: getDefaultManagers +--- + +import StackBlitz from '@site/src/components/StackBlitz'; + +# getDefaultManagers() + +`getDefaultManagers` returns an Array of [Managers](./Manager.md) to be sent to [<DataProvider />](./DataProvider.md). + +This makes it simple to configure and add custom [Managers](./Manager.md), while remaining robust against +any potential changes to the default managers. + +Currently returns \[[DevToolsManager](./DevToolsManager.md)\*, [NetworkManager](./NetworkManager.md), [SubscriptionManager](./SubscriptionManager.md)\]. + +\*(`DevToolsManager` is excluded in production builds.) + +## Usage + +```tsx +import { + DevToolsManager, + DataProvider, + getDefaultManagers, +} from '@data-client/react'; +import ReactDOM from 'react-dom'; + +// highlight-start +const managers = getDefaultManagers({ + // set fallback expiry time to an hour + networkManager: { dataExpiryLength: 1000 * 60 * 60 }, +}); +// highlight-end + +ReactDOM.createRoot(document.body).render( + + + , +); +``` + +See [DataProvider](./DataProvider.md) for details on usage in different environments. + +## Arguments + +Each argument represents a configuration of the manager. It can be of three possible types: + +- Any plain object is used as options to be sent to the manager's constructor. +- An instance of the manager to be used directly. +- `null`. When sent will exclude the manager. + +```ts +getDefaultManagers({ + devToolsManager: { trace: true }, + networkManager: new NetworkManager({ errorExpiryLength: 1 }), + subscriptionManager: null, +}); +``` + +### networkManager + +:::note + +`null` is not allowed here since NetworkManager is required + +::: + +`dataExpiryLength` is used as a fallback when an Endpoint does not have [dataExpiryLength](https://dataclient.io/docs/concepts/expiry-policy#endpointdataexpirylength) defined. + +`errorExpiryLength` is used as a fallback when an Endpoint does not have [errorExpiryLength](https://dataclient.io/docs/concepts/expiry-policy#endpointerrorexpirylength) defined. + +### devToolsManager + +[Arguments](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md) +to send to redux devtools. + +### subscriptionManager + +A class that implements `SubscriptionConstructable` like [PollingSubscription](./PollingSubscription.md) + +## Examples + +### Tracing actions + +For example, we can enable the [trace](https://github.com/reduxjs/redux-devtools/blob/main/extension/docs/API/Arguments.md#trace) option to help track down where actions are dispatched from. This has a large performance impact, so it is normally disabled. + +```ts +const managers = getDefaultManagers({ + // highlight-next-line + devToolsManager: { trace: true }, +}); +``` + +### Manager inheritance + +Sending manager instances allows us to customize managers using inheritance. + +```ts +import { IdlingNetworkManager } from '@data-client/react'; + +const managers = getDefaultManagers({ + networkManager: new IdlingNetworkManager(), +}); +``` + +`IdlingNetworkManager` can prevent stuttering by delaying [sideEffect](/rest/api/Endpoint#sideeffect)-free (read-only/GET) fetches +until animations are complete. This works in web using [requestIdleCallback](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback), and react native using InteractionManager.runAfterInteractions. + +### Disabling + +Using `null` will remove managers completely. [NetworkManager](./NetworkManager.md) cannot be removed this way. + +```ts +const managers = getDefaultManagers({ + devToolsManager: null, + subscriptionManager: null, +}); +``` + +Here we disable every manager except [NetworkManager](./NetworkManager.md). + +### Coin App + +New prices are streamed in many times a second; to reduce devtool spam, we set it +to ignore [SET](./Controller.md#set) actions for `Ticker`. + + diff --git a/docs/core/concepts/managers.md b/docs/core/concepts/managers.md index 6b617bab8c67..11bd3153fa51 100644 --- a/docs/core/concepts/managers.md +++ b/docs/core/concepts/managers.md @@ -126,4 +126,4 @@ with `event.data`. ### Coin App - + diff --git a/docs/core/getting-started/debugging.md b/docs/core/getting-started/debugging.md index 233b60529cc6..727641872426 100644 --- a/docs/core/getting-started/debugging.md +++ b/docs/core/getting-started/debugging.md @@ -97,19 +97,10 @@ import { } from '@data-client/react'; import ReactDOM from 'react-dom'; -const managers = - process.env.NODE_ENV !== 'production' - ? [ - // highlight-start - new DevToolsManager({ - trace: true, - }), - // highlight-end - ...getDefaultManagers().filter( - manager => manager.constructor.name !== 'DevToolsManager', - ), - ] - : getDefaultManagers(); +const managers = getDefaultManagers({ + // highlight-next-line + devToolsManager: { trace: true }, +}); ReactDOM.createRoot(document.body).render( diff --git a/examples/coin-app/src/getManagers.ts b/examples/coin-app/src/getManagers.ts new file mode 100644 index 000000000000..2693a2f75d25 --- /dev/null +++ b/examples/coin-app/src/getManagers.ts @@ -0,0 +1,21 @@ +import { getDefaultManagers, actionTypes } from '@data-client/react'; +import StreamManager from 'resources/StreamManager'; +import { Ticker } from 'resources/Ticker'; + +export default function getManagers() { + return [ + new StreamManager( + () => new WebSocket('wss://ws-feed.exchange.coinbase.com'), + { ticker: Ticker }, + ), + ...getDefaultManagers({ + devToolsManager: { + // double latency to help with high frequency updates + latency: 1000, + // skip websocket updates as these are too spammy + predicate: (state, action) => + action.type !== actionTypes.SET_TYPE || action.schema !== Ticker, + }, + }), + ]; +} diff --git a/examples/coin-app/src/index.tsx b/examples/coin-app/src/index.tsx index 72733ee1704e..1f1b6063c7e3 100644 --- a/examples/coin-app/src/index.tsx +++ b/examples/coin-app/src/index.tsx @@ -6,16 +6,8 @@ import { JSONSpout, appSpout, } from '@anansi/core'; -import { - useController, - AsyncBoundary, - getDefaultManagers, - DevToolsManager, - NetworkManager, - actionTypes, -} from '@data-client/react'; -import StreamManager from 'resources/StreamManager'; -import { Ticker } from 'resources/Ticker'; +import { useController, AsyncBoundary } from '@data-client/react'; +import getManagers from 'getManagers'; import App from './App'; import { createRouter } from './routing'; @@ -28,17 +20,7 @@ const app = ( const spouts = JSONSpout()( documentSpout({ title: 'Coin App' })( - dataClientSpout({ - getManagers: () => { - return [ - new StreamManager( - () => new WebSocket('wss://ws-feed.exchange.coinbase.com'), - { ticker: Ticker }, - ), - ...getManagers(), - ]; - }, - })( + dataClientSpout({ getManagers })( routerSpout({ useResolveWith: useController, createRouter, @@ -47,28 +29,4 @@ const spouts = JSONSpout()( ), ); -function getManagers() { - const managers = getDefaultManagers().filter( - manager => manager.constructor.name !== 'DevToolsManager', - ); - if (process.env.NODE_ENV !== 'production') { - const networkManager: NetworkManager | undefined = managers.find( - manager => manager instanceof NetworkManager, - ) as any; - managers.unshift( - new DevToolsManager( - { - // double latency to help with high frequency updates - latency: 1000, - // skip websocket updates as these are too spammy - predicate: (state, action) => - action.type !== actionTypes.SET_TYPE || action.schema !== Ticker, - }, - networkManager && (action => networkManager.skipLogging(action)), - ), - ); - } - return managers; -} - export default floodSpouts(spouts); diff --git a/packages/core/src/manager/SubscriptionManager.ts b/packages/core/src/manager/SubscriptionManager.ts index 09623bde6e2e..46d24b8be276 100644 --- a/packages/core/src/manager/SubscriptionManager.ts +++ b/packages/core/src/manager/SubscriptionManager.ts @@ -32,8 +32,9 @@ export interface SubscriptionConstructable { * * @see https://dataclient.io/docs/api/SubscriptionManager */ -export default class SubscriptionManager - implements Manager +export default class SubscriptionManager< + S extends SubscriptionConstructable = SubscriptionConstructable, +> implements Manager { protected subscriptions: { [key: string]: InstanceType; diff --git a/packages/react/src/components/DataProvider.tsx b/packages/react/src/components/DataProvider.tsx index 769adea391d0..91c145ac2166 100644 --- a/packages/react/src/components/DataProvider.tsx +++ b/packages/react/src/components/DataProvider.tsx @@ -3,9 +3,6 @@ import { initialState as defaultState, Controller as DataController, applyManager, - SubscriptionManager, - PollingSubscription, - DevToolsManager, } from '@data-client/core'; import type { State, Manager } from '@data-client/core'; import React, { useMemo, useRef } from 'react'; @@ -13,10 +10,11 @@ import type { JSX } from 'react'; import DataStore from './DataStore.js'; import type { DevToolsPosition } from './DevToolsButton.js'; +import { getDefaultManagers } from './getDefaultManagers.js'; import { SSR } from './LegacyReact.js'; import { renderDevButton } from './renderDevButton.js'; import { ControllerContext } from '../context.js'; -import { NetworkManager } from '../managers/index.js'; +import { DevToolsManager } from '../managers/index.js'; export interface ProviderProps { children: React.ReactNode; @@ -89,26 +87,3 @@ See https://dataclient.io/docs/guides/ssr.`, ); } - -/* istanbul ignore next */ -let getDefaultManagers = () => - [ - new NetworkManager(), - new SubscriptionManager(PollingSubscription), - ] as Manager[]; - -/* istanbul ignore else */ -if (process.env.NODE_ENV !== 'production') { - getDefaultManagers = () => { - const networkManager = new NetworkManager(); - return [ - new DevToolsManager( - undefined, - networkManager.skipLogging.bind(networkManager), - ), - networkManager, - new SubscriptionManager(PollingSubscription), - ] as Manager[]; - }; -} -export { getDefaultManagers }; diff --git a/packages/react/src/components/__tests__/__snapshots__/getDefaultManagers.tsx.snap b/packages/react/src/components/__tests__/__snapshots__/getDefaultManagers.tsx.snap new file mode 100644 index 000000000000..e66c18f9e493 --- /dev/null +++ b/packages/react/src/components/__tests__/__snapshots__/getDefaultManagers.tsx.snap @@ -0,0 +1,9 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`getDefaultManagers() networkManager should not be removable 1`] = ` +[ + [ + "Disabling NetworkManager is not allowed.", + ], +] +`; diff --git a/packages/react/src/components/__tests__/getDefaultManagers.tsx b/packages/react/src/components/__tests__/getDefaultManagers.tsx new file mode 100644 index 000000000000..2274b1d4c974 --- /dev/null +++ b/packages/react/src/components/__tests__/getDefaultManagers.tsx @@ -0,0 +1,106 @@ +// eslint-env jest +import { + NetworkManager, + SubscriptionManager, + DevToolsManager, +} from '@data-client/core'; + +import { getDefaultManagers } from '../getDefaultManagers'; + +describe('getDefaultManagers()', () => { + let warnspy: jest.SpyInstance; + let debugspy: jest.SpyInstance; + beforeEach(() => { + warnspy = jest.spyOn(global.console, 'warn').mockImplementation(() => {}); + debugspy = jest.spyOn(global.console, 'info').mockImplementation(() => {}); + }); + afterEach(() => { + warnspy.mockRestore(); + debugspy.mockRestore(); + }); + + let errorSpy: jest.SpyInstance; + afterEach(() => { + errorSpy.mockRestore(); + }); + beforeEach( + () => (errorSpy = jest.spyOn(console, 'error').mockImplementation()), + ); + it('should have SubscriptionManager in default managers', () => { + const subManagers = getDefaultManagers().find( + manager => manager instanceof SubscriptionManager, + ); + expect(subManagers).toBeDefined(); + }); + it('should have NetworkManager in default managers', () => { + const networkManager = getDefaultManagers().find( + manager => manager instanceof NetworkManager, + ); + expect(networkManager).toBeDefined(); + }); + it('should have DevToolsManager in default managers', () => { + const devtoolsMgr = getDefaultManagers().find( + manager => manager instanceof DevToolsManager, + ); + expect(devtoolsMgr).toBeDefined(); + }); + it('null option should disable a manager', () => { + expect( + getDefaultManagers({ devToolsManager: null }).find( + manager => manager instanceof DevToolsManager, + ), + ).toBeUndefined(); + expect( + getDefaultManagers({ subscriptionManager: null }).find( + manager => manager instanceof SubscriptionManager, + ), + ).toBeUndefined(); + }); + + it('networkManager should not be removable', () => { + expect( + // @ts-expect-error + getDefaultManagers({ networkManager: null }).find( + manager => manager instanceof NetworkManager, + ), + ).toBeDefined(); + expect(errorSpy.mock.calls).toMatchSnapshot(); + }); + + it('manager constructor options should work', () => { + const managers = getDefaultManagers({ + networkManager: { dataExpiryLength: 1 }, + devToolsManager: { + maxAge: 1000, + }, + }); + expect( + managers.find(manager => manager instanceof NetworkManager) + ?.dataExpiryLength, + ).toBe(1); + // check that the value set is not default + expect( + managers.find(manager => manager instanceof DevToolsManager) + ?.maxBufferLength, + ).not.toBe(DevToolsManager.prototype.maxBufferLength); + }); + + it('manager instance should work', () => { + class MyDevTool extends DevToolsManager { + maxBufferLength = 500; + } + const managers = getDefaultManagers({ + networkManager: new NetworkManager({ dataExpiryLength: 1 }), + devToolsManager: new MyDevTool(), + }); + expect( + managers.find(manager => manager instanceof NetworkManager) + ?.dataExpiryLength, + ).toBe(1); + // check that the value set is not default + expect( + managers.find(manager => manager instanceof DevToolsManager) + ?.maxBufferLength, + ).not.toBe(DevToolsManager.prototype.maxBufferLength); + }); +}); diff --git a/packages/react/src/components/__tests__/provider.native.tsx b/packages/react/src/components/__tests__/provider.native.tsx index fcd89478c714..c959adbacc9c 100644 --- a/packages/react/src/components/__tests__/provider.native.tsx +++ b/packages/react/src/components/__tests__/provider.native.tsx @@ -2,7 +2,6 @@ import { NetworkManager, actionTypes, - SubscriptionManager, Controller, SetResponseAction, } from '@data-client/core'; @@ -15,7 +14,7 @@ import { Text } from 'react-native'; import { ControllerContext, StateContext } from '../../context'; import { useController, useSuspense } from '../../hooks'; import { payload } from '../../test-fixtures'; -import DataProvider, { getDefaultManagers } from '../DataProvider'; +import DataProvider from '../DataProvider'; const { SET_RESPONSE_TYPE } = actionTypes; @@ -156,11 +155,4 @@ describe('', () => { expect(count).toBe(2); expect(state).toMatchSnapshot(); }); - - it('should have SubscriptionManager in default managers', () => { - const subManagers = getDefaultManagers().filter( - manager => manager instanceof SubscriptionManager, - ); - expect(subManagers.length).toBe(1); - }); }); diff --git a/packages/react/src/components/__tests__/provider.tsx b/packages/react/src/components/__tests__/provider.tsx index d7e23894b69d..7b963b56b751 100644 --- a/packages/react/src/components/__tests__/provider.tsx +++ b/packages/react/src/components/__tests__/provider.tsx @@ -2,7 +2,6 @@ import { NetworkManager, actionTypes, - SubscriptionManager, Manager, Middleware, Controller, @@ -16,7 +15,8 @@ import React, { useContext, Suspense, StrictMode } from 'react'; import { ControllerContext, StateContext } from '../../context'; import { useController, useSuspense } from '../../hooks'; import { payload } from '../../test-fixtures'; -import DataProvider, { getDefaultManagers } from '../DataProvider'; +import DataProvider from '../DataProvider'; +import { getDefaultManagers } from '../getDefaultManagers'; const { SET_RESPONSE_TYPE } = actionTypes; @@ -156,13 +156,6 @@ describe('', () => { expect(state).toMatchSnapshot(); }); - it('should have SubscriptionManager in default managers', () => { - const subManagers = getDefaultManagers().filter( - manager => manager instanceof SubscriptionManager, - ); - expect(subManagers.length).toBe(1); - }); - it('should ignore dispatches after unmount', async () => { class InjectorManager implements Manager { protected declare middleware: Middleware; diff --git a/packages/react/src/components/getDefaultManagers.tsx b/packages/react/src/components/getDefaultManagers.tsx new file mode 100644 index 000000000000..a0a7eb43a6e5 --- /dev/null +++ b/packages/react/src/components/getDefaultManagers.tsx @@ -0,0 +1,76 @@ +import type { DevToolsConfig, Manager } from '@data-client/core'; + +import { + NetworkManager, + SubscriptionManager, + PollingSubscription, + DevToolsManager, +} from '../managers/index.js'; + +/* istanbul ignore next */ +/** Returns the default Managers used by DataProvider. + * + * @see https://dataclient.io/docs/api/getDefaultManagers + */ +let getDefaultManagers: ({ + devToolsManager, + networkManager, + subscriptionManager, +}?: GetManagersOptions) => Manager[] = ({ + networkManager, + subscriptionManager = PollingSubscription, +} = {}) => + subscriptionManager === null ? + [constructManager(NetworkManager, networkManager)] + : [ + constructManager(NetworkManager, networkManager), + constructManager(SubscriptionManager, subscriptionManager), + ]; +/* istanbul ignore else */ +if (process.env.NODE_ENV !== 'production') { + getDefaultManagers = ({ + devToolsManager, + networkManager, + subscriptionManager = PollingSubscription, + }: GetManagersOptions = {}): Manager[] => { + if (networkManager === null) { + console.error('Disabling NetworkManager is not allowed.'); + networkManager = {}; + } + const nm = constructManager(NetworkManager, networkManager); + const managers: Manager[] = [nm]; + if (subscriptionManager !== null) { + managers.push(constructManager(SubscriptionManager, subscriptionManager)); + } + if (devToolsManager !== null) { + managers.unshift( + devToolsManager instanceof DevToolsManager ? devToolsManager : ( + new DevToolsManager(devToolsManager, nm.skipLogging.bind(nm)) + ), + ); + } + return managers; + }; +} +export { getDefaultManagers }; + +function constructManager< + M extends { new (...args: any): Manager }, + O extends InstanceType | ConstructorArgs, +>(Mgr: M, optionOrInstance: O): InstanceType { + return optionOrInstance instanceof Mgr ? optionOrInstance : ( + (new Mgr(optionOrInstance) as any) + ); +} + +export type GetManagersOptions = { + devToolsManager?: DevToolsManager | DevToolsConfig | null; + networkManager?: NetworkManager | ConstructorArgs; + subscriptionManager?: + | SubscriptionManager + | ConstructorArgs + | null; +}; + +export type ConstructorArgs = + T extends { new (options: infer O): any } ? O : never; diff --git a/packages/react/src/components/index.ts b/packages/react/src/components/index.ts index 2ed3cee88860..82bfe9e92156 100644 --- a/packages/react/src/components/index.ts +++ b/packages/react/src/components/index.ts @@ -1,5 +1,6 @@ import BackupLoading from './BackupLoading.js'; -import DataProvider, { getDefaultManagers } from './DataProvider.js'; +import DataProvider from './DataProvider.js'; +import { getDefaultManagers } from './getDefaultManagers.js'; import UniversalSuspense from './UniversalSuspense.js'; export type { ProviderProps } from './DataProvider.js'; export type { DevToolsPosition } from './DevToolsButton.js'; diff --git a/packages/react/src/managers/NetworkManager.native.ts b/packages/react/src/managers/IdlingNetworkManager.native.ts similarity index 87% rename from packages/react/src/managers/NetworkManager.native.ts rename to packages/react/src/managers/IdlingNetworkManager.native.ts index f5b8354f8ac9..1d2f830321df 100644 --- a/packages/react/src/managers/NetworkManager.native.ts +++ b/packages/react/src/managers/IdlingNetworkManager.native.ts @@ -1,6 +1,7 @@ import { NetworkManager } from '@data-client/core'; import { InteractionManager } from 'react-native'; +/** Can help prevent stuttering by waiting for idle for sideEffect free fetches */ export default class NativeIdlingNetworkManager extends NetworkManager { /** Calls the callback when client is not 'busy' with high priority interaction tasks * diff --git a/packages/react/src/managers/NetworkManager.ts b/packages/react/src/managers/IdlingNetworkManager.ts similarity index 78% rename from packages/react/src/managers/NetworkManager.ts rename to packages/react/src/managers/IdlingNetworkManager.ts index 8093373facda..5d90b81cb4d6 100644 --- a/packages/react/src/managers/NetworkManager.ts +++ b/packages/react/src/managers/IdlingNetworkManager.ts @@ -1,5 +1,6 @@ import { NetworkManager } from '@data-client/core'; +/** Can help prevent stuttering by waiting for idle for sideEffect free fetches */ export default class WebIdlingNetworkManager extends NetworkManager { static { if (typeof requestIdleCallback === 'function') { diff --git a/packages/react/src/managers/__tests__/RIC.native.ts b/packages/react/src/managers/__tests__/RIC.native.ts index 06231fef162c..a98d64dc3665 100644 --- a/packages/react/src/managers/__tests__/RIC.native.ts +++ b/packages/react/src/managers/__tests__/RIC.native.ts @@ -1,10 +1,19 @@ -import { NetworkManager } from '..'; +import { IdlingNetworkManager } from '..'; describe('RequestIdleCallback', () => { it('should run using InteractionManager', async () => { const fn = jest.fn(); jest.useFakeTimers(); // @ts-expect-error this is protected member - new NetworkManager().idleCallback(fn, {}); + new IdlingNetworkManager().idleCallback(fn, {}); + jest.runAllTimers(); + expect(fn).toHaveBeenCalled(); + jest.useRealTimers(); + }); + it('should run with timeout using InteractionManager', async () => { + const fn = jest.fn(); + jest.useFakeTimers(); + // @ts-expect-error this is protected member + new IdlingNetworkManager().idleCallback(fn, { timeout: 500 }); jest.runAllTimers(); expect(fn).toHaveBeenCalled(); jest.useRealTimers(); diff --git a/packages/react/src/managers/__tests__/RIC.web.ts b/packages/react/src/managers/__tests__/RIC.web.ts index cb1a1133526a..b35ddf3e4541 100644 --- a/packages/react/src/managers/__tests__/RIC.web.ts +++ b/packages/react/src/managers/__tests__/RIC.web.ts @@ -4,11 +4,11 @@ describe('RequestIdleCallback', () => { (global as any).requestIdleCallback = undefined; jest.resetModules(); // eslint-disable-next-line @typescript-eslint/no-var-requires - const { NetworkManager } = await import('..'); + const { IdlingNetworkManager } = await import('..'); const fn = jest.fn(); jest.useFakeTimers(); // @ts-expect-error - new NetworkManager().idleCallback(fn, {}); + new IdlingNetworkManager().idleCallback(fn, {}); jest.runAllTimers(); expect(fn).toHaveBeenCalled(); (global as any).requestIdleCallback = requestIdle; @@ -18,11 +18,11 @@ describe('RequestIdleCallback', () => { it('should run through requestIdleCallback', async () => { jest.resetModules(); // eslint-disable-next-line @typescript-eslint/no-var-requires - const { NetworkManager } = await import('..'); + const { IdlingNetworkManager } = await import('..'); const fn = jest.fn(); jest.useFakeTimers(); // @ts-expect-error - new NetworkManager().idleCallback(fn, {}); + new IdlingNetworkManager().idleCallback(fn, {}); jest.runAllTimers(); expect(fn).toHaveBeenCalled(); jest.useRealTimers(); diff --git a/packages/react/src/managers/index.ts b/packages/react/src/managers/index.ts index 0ecbc18bf34a..c7d9fa0543a4 100644 --- a/packages/react/src/managers/index.ts +++ b/packages/react/src/managers/index.ts @@ -6,4 +6,4 @@ export { LogoutManager, NetworkManager, } from '@data-client/core'; -export { default as IdlingNetworkManager } from './NetworkManager.js'; +export { default as IdlingNetworkManager } from './IdlingNetworkManager.js'; diff --git a/website/sidebars.json b/website/sidebars.json index 94778dc0e394..59b34e801261 100644 --- a/website/sidebars.json +++ b/website/sidebars.json @@ -235,6 +235,10 @@ "type": "doc", "id": "api/Manager" }, + { + "type": "doc", + "id": "api/getDefaultManagers" + }, { "type": "doc", "id": "api/NetworkManager" 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 c7d8e2ccb20c..6ae56f4b8d29 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 @@ -633,7 +633,10 @@ declare class NetworkManager implements Manager { protected middleware: Middleware$2; protected controller: Controller; cleanupDate?: number; - constructor(dataExpiryLength?: number, errorExpiryLength?: number); + constructor({ dataExpiryLength, errorExpiryLength }?: { + dataExpiryLength?: number | undefined; + errorExpiryLength?: number | undefined; + }); /** Used by DevtoolsManager to determine whether to log an action */ skipLogging(action: ActionTypes): boolean; /** On mount */ @@ -807,7 +810,7 @@ interface SubscriptionConstructable { * * @see https://dataclient.io/docs/api/SubscriptionManager */ -declare class SubscriptionManager implements Manager { +declare class SubscriptionManager implements Manager { protected subscriptions: { [key: string]: InstanceType; }; diff --git a/website/src/components/Playground/editor-types/@data-client/react.d.ts b/website/src/components/Playground/editor-types/@data-client/react.d.ts index 334a2327c496..d8719217b6ee 100644 --- a/website/src/components/Playground/editor-types/@data-client/react.d.ts +++ b/website/src/components/Playground/editor-types/@data-client/react.d.ts @@ -1,9 +1,10 @@ import * as _data_client_core from '@data-client/core'; -import { NetworkManager, Manager, State, Controller, EndpointInterface as EndpointInterface$1, FetchFunction as FetchFunction$1, Schema as Schema$1, ResolveType as ResolveType$1, Denormalize as Denormalize$1, DenormalizeNullable as DenormalizeNullable$1, Queryable as Queryable$1, NI, SchemaArgs, NetworkError as NetworkError$1, UnknownError as UnknownError$1, ErrorTypes as ErrorTypes$2, __INTERNAL__, createReducer, applyManager, actions } from '@data-client/core'; +import { NetworkManager, Manager, State, Controller, DevToolsManager, DevToolsConfig, SubscriptionManager, EndpointInterface as EndpointInterface$1, FetchFunction as FetchFunction$1, Schema as Schema$1, ResolveType as ResolveType$1, Denormalize as Denormalize$1, DenormalizeNullable as DenormalizeNullable$1, Queryable as Queryable$1, NI, SchemaArgs, NetworkError as NetworkError$1, UnknownError as UnknownError$1, ErrorTypes as ErrorTypes$2, __INTERNAL__, createReducer, applyManager, actions } from '@data-client/core'; export { AbstractInstanceType, ActionTypes, Controller, DataClientDispatch, DefaultConnectionListener, Denormalize, DenormalizeNullable, DevToolsManager, Dispatch, EndpointExtraOptions, EndpointInterface, ErrorTypes, ExpiryStatus, FetchAction, FetchFunction, GenericDispatch, InvalidateAction, LogoutManager, Manager, Middleware, MiddlewareAPI, NetworkError, NetworkManager, Normalize, NormalizeNullable, PK, PollingSubscription, ResetAction, ResolveType, Schema, SetAction, SetResponseAction, State, SubscribeAction, SubscriptionManager, UnknownError, UnsubscribeAction, UpdateFunction, actionTypes } from '@data-client/core'; import * as react_jsx_runtime from 'react/jsx-runtime'; import React, { JSX, Context } from 'react'; +/** Can help prevent stuttering by waiting for idle for sideEffect free fetches */ declare class WebIdlingNetworkManager extends NetworkManager { } @@ -30,7 +31,23 @@ interface Props$1 { * @see https://dataclient.io/docs/api/DataProvider */ declare function DataProvider({ children, managers, initialState, Controller, devButton, }: Props$1): JSX.Element; -declare let getDefaultManagers: () => Manager[]; + +/** Returns the default Managers used by DataProvider. + * + * @see https://dataclient.io/docs/api/getDefaultManagers + */ +declare let getDefaultManagers: ({ devToolsManager, networkManager, subscriptionManager, }?: GetManagersOptions) => Manager[]; + +type GetManagersOptions = { + devToolsManager?: DevToolsManager | DevToolsConfig | null; + networkManager?: NetworkManager | ConstructorArgs; + subscriptionManager?: SubscriptionManager | ConstructorArgs | null; +}; +type ConstructorArgs = T extends { + new (options: infer O): any; +} ? O : never; /** Suspense but compatible with 18 SSR, 17, 16 and native */ declare const UniversalSuspense: React.FunctionComponent<{