From d66fb9f256e2745c0408bd800a3c1ad41f26f500 Mon Sep 17 00:00:00 2001 From: Nathaniel Tucker Date: Sat, 4 Jan 2025 14:48:19 +0000 Subject: [PATCH] enhance: Control gcpolicy above controller Fix code scanning alert no. 81: Prototype-polluting assignment Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Revert "Fix code scanning alert no. 81: Prototype-polluting assignment" This reverts commit 848b4981b3c563c2bb0e5e7bcc47d39001bf083d. --- packages/core/src/controller/Controller.ts | 8 +- packages/core/src/index.ts | 1 + .../core/src/{manager => state}/GCPolicy.ts | 62 ++++++++------- .../__tests__/integration-endpoint.web.tsx | 77 ++++++++++--------- .../react/src/components/DataProvider.tsx | 11 ++- .../src/hooks/__tests__/useSuspense.web.tsx | 28 ++++--- .../react/src/server/redux/DataProvider.tsx | 6 +- .../react/src/server/redux/prepareStore.tsx | 6 +- .../test/src/makeRenderDataClient/index.tsx | 2 +- 9 files changed, 108 insertions(+), 93 deletions(-) rename packages/core/src/{manager => state}/GCPolicy.ts (68%) diff --git a/packages/core/src/controller/Controller.ts b/packages/core/src/controller/Controller.ts index 74f9c426d9e3..bc70ac13c7d6 100644 --- a/packages/core/src/controller/Controller.ts +++ b/packages/core/src/controller/Controller.ts @@ -35,7 +35,7 @@ import { } from './actions/index.js'; import ensurePojo from './ensurePojo.js'; import type { EndpointUpdateFunction } from './types.js'; -import GCPolicy from '../manager/GCPolicy.js'; +import type { GCInterface } from '../state/GCPolicy.js'; import { initialState } from '../state/reducer/createReducer.js'; import selectMeta from '../state/selectMeta.js'; import type { ActionTypes, State } from '../types.js'; @@ -47,6 +47,7 @@ interface ConstructorProps { dispatch?: D; getState?: () => State; memo?: Pick; + gcPolicy?: GCInterface; } const unsetDispatch = (action: unknown): Promise => { @@ -93,17 +94,18 @@ export default class Controller< /** * Handles garbage collection */ - declare readonly gcPolicy: GCPolicy; + declare readonly gcPolicy: GCInterface; constructor({ dispatch = unsetDispatch as any, getState = unsetState, memo = new MemoCache(), + gcPolicy = { createCountRef: () => () => () => undefined }, }: ConstructorProps = {}) { this.dispatch = dispatch; this.getState = getState; this.memo = memo; - this.gcPolicy = new GCPolicy(this); + this.gcPolicy = gcPolicy; } /*************** Action Dispatchers ***************/ diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index e5ae50ab5a6a..26a0d3e8b5ef 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -29,6 +29,7 @@ export { default as NetworkManager, ResetError, } from './manager/NetworkManager.js'; +export * from './state/GCPolicy.js'; export { default as createReducer, initialState, diff --git a/packages/core/src/manager/GCPolicy.ts b/packages/core/src/state/GCPolicy.ts similarity index 68% rename from packages/core/src/manager/GCPolicy.ts rename to packages/core/src/state/GCPolicy.ts index 9be00314e70a..ca50602f5d25 100644 --- a/packages/core/src/manager/GCPolicy.ts +++ b/packages/core/src/state/GCPolicy.ts @@ -3,9 +3,11 @@ import type { EntityPath } from '@data-client/normalizr'; import { GC } from '../actionTypes.js'; import Controller from '../controller/Controller.js'; -export default class GCPolicy { - protected endpointCount: Record = {}; - protected entityCount: Record> = {}; +export class GCPolicy implements GCInterface { + protected endpointCount: Record = Object.create(null); + protected entityCount: Record> = + Object.create(null); + protected endpoints = new Set(); protected entities: EntityPath[] = []; declare protected intervalId: ReturnType; @@ -13,22 +15,35 @@ export default class GCPolicy { declare protected options: GCOptions; constructor( - controller: Controller, // every 5 min { intervalMS = 60 * 1000 * 5 }: GCOptions = {}, ) { - this.controller = controller; this.options = { intervalMS }; } + init(controller: Controller) { + this.controller = controller; + + this.intervalId = setInterval(() => { + if (typeof requestIdleCallback === 'function') { + requestIdleCallback(() => this.runSweep(), { timeout: 1000 }); + } else { + this.runSweep(); + } + }, this.options.intervalMS); + } + + cleanup() { + clearInterval(this.intervalId); + } + createCountRef({ key, paths = [] }: { key: string; paths?: EntityPath[] }) { - if (!ENV_DYNAMIC) return () => () => undefined; // increment return () => { this.endpointCount[key]++; paths.forEach(path => { - if (!(path.key in this.endpointCount)) { - this.entityCount[path.key] = {}; + if (!(path.key in this.entityCount)) { + this.entityCount[path.key] = Object.create(null); } this.entityCount[path.key][path.pk]++; }); @@ -38,34 +53,20 @@ export default class GCPolicy { if (this.endpointCount[key]-- <= 0) { // queue for cleanup this.endpoints.add(key); - this.entities.concat(...paths); } paths.forEach(path => { if (!(path.key in this.endpointCount)) { return; } - this.entityCount[path.key][path.pk]--; + if (this.entityCount[path.key][path.pk]-- <= 0) { + // queue for cleanup + this.entities.concat(...paths); + } }); }; }; } - init() { - // don't run this in nodejs env - if (ENV_DYNAMIC) - this.intervalId = setInterval(() => { - if (typeof requestIdleCallback === 'function') { - requestIdleCallback(() => this.runSweep(), { timeout: 1000 }); - } else { - this.runSweep(); - } - }, this.options.intervalMS); - } - - cleanup() { - clearInterval(this.intervalId); - } - protected runSweep() { const state = this.controller.getState(); const entities: EntityPath[] = []; @@ -94,6 +95,9 @@ export default class GCPolicy { export interface GCOptions { intervalMS?: number; } - -const ENV_DYNAMIC = - typeof navigator !== 'undefined' || typeof window !== 'undefined'; +export interface CreateCountRef { + ({ key, paths }: { key: string; paths?: EntityPath[] }): () => () => void; +} +export interface GCInterface { + createCountRef: CreateCountRef; +} diff --git a/packages/react/src/__tests__/integration-endpoint.web.tsx b/packages/react/src/__tests__/integration-endpoint.web.tsx index 5858e65f8794..cfb91c1239a0 100644 --- a/packages/react/src/__tests__/integration-endpoint.web.tsx +++ b/packages/react/src/__tests__/integration-endpoint.web.tsx @@ -17,7 +17,7 @@ import { import nock from 'nock'; // relative imports to avoid circular dependency in tsconfig references -import { makeRenderDataClient, act } from '../../../test'; +import { makeRenderDataHook, act } from '../../../test'; import { useCache, useController, @@ -50,7 +50,7 @@ describe.each([ ['ExternalDataProvider', ExternalDataProvider], ] as const)(`%s`, (_, makeProvider) => { // TODO: add nested resource test case that has multiple partials to test merge functionality - let renderDataClient: ReturnType; + let renderDataHook: ReturnType; let mynock: nock.Scope; beforeEach(() => { @@ -100,7 +100,7 @@ describe.each([ }); beforeEach(() => { - renderDataClient = makeRenderDataClient(makeProvider); + renderDataHook = makeRenderDataHook(makeProvider); }); describe('Endpoint', () => { @@ -112,8 +112,9 @@ describe.each([ }); it('should resolve useSuspense()', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { - return useSuspense(CoolerArticleDetail, payload); + const { result, waitForNextUpdate } = renderDataHook(() => { + const a = useSuspense(CoolerArticleDetail, payload); + return a; }); expect(result.current).toBeUndefined(); await waitForNextUpdate(); @@ -126,7 +127,7 @@ describe.each([ 'should resolve useSuspense() with Interceptors', async ArticleResource => { nock.cleanAll(); - const { result, waitFor, controller } = renderDataClient( + const { result, waitFor, controller } = renderDataHook( () => { return useSuspense(ArticleResource.get, { id: 'abc123' }); }, @@ -159,7 +160,7 @@ describe.each([ ); it('should maintain global referential equality', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return [ useSuspense(CoolerArticleDetail, payload), useCache(CoolerArticleDetail, payload), @@ -177,7 +178,7 @@ describe.each([ signal: abort.signal, }); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return { data: useSuspense(AbortableArticle, { id: payload.id }), fetch: useController().fetch, @@ -206,7 +207,7 @@ describe.each([ }); it('should resolve useSuspense()', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(CoolerArticleResource.get, { id: payload.id }); }); expect(result.current).toBeUndefined(); @@ -225,7 +226,7 @@ describe.each([ path: `${CoolerArticleResource.getList.path}/values` as const, }); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(GetValues); }); expect(result.current).toBeUndefined(); @@ -244,7 +245,7 @@ describe.each([ }); const allArticles = new schema.All(CoolerArticle); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { useFetch(getList); return useQuery(allArticles); }); @@ -268,14 +269,13 @@ describe.each([ }, ); - const { result, waitForNextUpdate, rerender, controller } = - renderDataClient( - ({ tags }: { tags: string }) => { - useFetch(CoolerArticleResource.getList); - return useQuery(queryArticle, { tags }); - }, - { initialProps: { tags: 'a' } }, - ); + const { result, waitForNextUpdate, rerender, controller } = renderDataHook( + ({ tags }: { tags: string }) => { + useFetch(CoolerArticleResource.getList); + return useQuery(queryArticle, { tags }); + }, + { initialProps: { tags: 'a' } }, + ); expect(result.current).toBeUndefined(); await waitForNextUpdate(); expect(result.current).toBeDefined(); @@ -322,7 +322,7 @@ describe.each([ schema: queryArticle, }); - const { result, waitForNextUpdate, controller } = renderDataClient( + const { result, waitForNextUpdate, controller } = renderDataHook( ({ tags }: { tags: string }) => { return useSuspense(getList, { tags }); }, @@ -385,7 +385,7 @@ describe.each([ ], }); - const { result } = renderDataClient( + const { result } = renderDataHook( () => { return useSuspense(unionEndpoint, {}); }, @@ -431,7 +431,7 @@ describe.each([ { id: '5', body: 'hi', type: 'another' }, { id: '5', body: 'hi' }, ]; - const { result } = renderDataClient( + const { result } = renderDataHook( () => { return useSuspense(UnionResource.getList); }, @@ -470,8 +470,8 @@ describe.each([ .delete(`/article-cooler/${temppayload.id}`) .reply(204, ''); const throws: Promise[] = []; - const { result, waitForNextUpdate, waitFor, controller } = - renderDataClient(() => { + const { result, waitForNextUpdate, waitFor, controller } = renderDataHook( + () => { try { return useSuspense(ArticleResource.get, { id: temppayload.id, @@ -484,7 +484,8 @@ describe.each([ } throw e; } - }); + }, + ); expect(result.current).toBeUndefined(); await waitForNextUpdate(); let data = result.current; @@ -522,7 +523,7 @@ describe.each([ .delete(`/article-cooler/${temppayload.id}`) .reply(204, ''); const throws: Promise[] = []; - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { try { return [ useSuspense(CoolerArticleResource.get, { @@ -563,7 +564,7 @@ describe.each([ }); it('should throw when retrieving an empty string', async () => { - const { result } = renderDataClient(() => { + const { result } = renderDataHook(() => { return useController().fetch; }); @@ -576,7 +577,7 @@ describe.each([ ['CoolerArticleResource', CoolerArticleResource.delete], ['ArticleResource', ArticleResource.delete], ] as const)(`should not throw on delete [%s]`, async (_, endpoint) => { - const { result } = renderDataClient(() => { + const { result } = renderDataHook(() => { return useController().fetch; }); await expect( @@ -585,7 +586,7 @@ describe.each([ }); it('useSuspense() should throw errors on bad network', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(CoolerArticleResource.get, { title: '0', }); @@ -614,7 +615,7 @@ describe.each([ });*/ it('useSuspense() should throw 500 errors', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(TypedArticleResource.get, { id: 500, }); @@ -626,7 +627,7 @@ describe.each([ }); it('useSuspense() should not throw 500 if data already available', async () => { - const { result, waitForNextUpdate } = renderDataClient( + const { result, waitForNextUpdate } = renderDataHook( () => { return [ useSuspense(TypedArticleResource.get, { @@ -686,7 +687,7 @@ describe.each([ it('useSuspense() should throw errors on malformed response', async () => { const response = [1]; mynock.get(`/article-cooler/${878}`).reply(200, response); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(CoolerArticleResource.get, { id: 878, }); @@ -731,7 +732,7 @@ describe.each([ `should not suspend with no params to useSuspense() [%s]`, (_, endpoint) => { let article: any; - const { result } = renderDataClient(() => { + const { result } = renderDataHook(() => { article = useSuspense(endpoint, null); return 'done'; }); @@ -741,7 +742,7 @@ describe.each([ ); it('should update on create (legacy)', async () => { - const { result, waitForNextUpdate, controller } = renderDataClient(() => { + const { result, waitForNextUpdate, controller } = renderDataHook(() => { const articles = useSuspense( CoolerArticleResource.getList.extend({ schema: [CoolerArticle] }), ); @@ -767,7 +768,7 @@ describe.each([ }); it('should update on create', async () => { - const { result, waitForNextUpdate, controller } = renderDataClient(() => { + const { result, waitForNextUpdate, controller } = renderDataHook(() => { const articles = useSuspense(CoolerArticleResource.getList); return { articles }; }); @@ -790,7 +791,7 @@ describe.each([ }), }), }); - const { result, waitForNextUpdate, controller } = renderDataClient(() => { + const { result, waitForNextUpdate, controller } = renderDataHook(() => { const articles = useSuspense(getArticles); return articles; }); @@ -816,7 +817,7 @@ describe.each([ mynock.get(`/article-paginated`).reply(200, paginatedFirstPage); mynock.get(`/article-paginated?cursor=2`).reply(200, paginatedSecondPage); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { const { results: articles } = useSuspense( PaginatedArticleResource.getList, ); @@ -840,7 +841,7 @@ describe.each([ }); describe("a parent resource endpoint returns an attribute NOT in its own schema but used in a child's schemas", () => { it('should not error when fetching the child entity from cache', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { // CoolerArticleResource does NOT have editor in its schema, but return editor from the server const articleWithoutEditorSchema = useSuspense( CoolerArticleResource.get, diff --git a/packages/react/src/components/DataProvider.tsx b/packages/react/src/components/DataProvider.tsx index 608564fccd37..daf32d8b4e2c 100644 --- a/packages/react/src/components/DataProvider.tsx +++ b/packages/react/src/components/DataProvider.tsx @@ -3,6 +3,7 @@ import { initialState as defaultState, Controller as DataController, applyManager, + GCPolicy, } from '@data-client/core'; import type { State, Manager } from '@data-client/core'; import React, { useCallback, useMemo, useRef } from 'react'; @@ -50,9 +51,13 @@ export default function DataProvider({ See https://dataclient.io/docs/guides/ssr.`, ); } + const gcRef: React.RefObject = useRef(undefined); + if (!gcRef.current) gcRef.current = new GCPolicy(); + // contents of this component expected to be relatively stable const controllerRef: React.RefObject = useRef(undefined); - if (!controllerRef.current) controllerRef.current = new Controller(); + if (!controllerRef.current) + controllerRef.current = new Controller({ gcPolicy: gcRef.current }); //TODO: bind all methods so destructuring works const managersRef: React.RefObject = useRef(managers); @@ -63,12 +68,12 @@ See https://dataclient.io/docs/guides/ssr.`, managersRef.current.forEach(manager => { manager.init?.(initialState); }); - controllerRef.current.gcPolicy.init(); + gcRef.current.init(controllerRef.current); return () => { managersRef.current.forEach(manager => { manager.cleanup(); }); - controllerRef.current.gcPolicy.cleanup(); + gcRef.current.cleanup(); }; // we don't support initialState changes // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/react/src/hooks/__tests__/useSuspense.web.tsx b/packages/react/src/hooks/__tests__/useSuspense.web.tsx index 3fd246f38123..8b08f24c71ae 100644 --- a/packages/react/src/hooks/__tests__/useSuspense.web.tsx +++ b/packages/react/src/hooks/__tests__/useSuspense.web.tsx @@ -44,7 +44,7 @@ import { ControllerContext, StateContext, } from '../..'; -import { makeRenderDataClient, mockInitialState, act } from '../../../../test'; +import { renderDataHook, mockInitialState, act } from '../../../../test'; import { articlesPages, payload, users, nested } from '../test-fixtures'; import useSuspense from '../useSuspense'; @@ -103,7 +103,6 @@ function ArticleComponentTester({ invalidIfStale = false, schema = true }) { } describe('useSuspense()', () => { - let renderDataClient: ReturnType; const fbmock = jest.fn(); async function testMalformedResponse( @@ -120,7 +119,7 @@ describe('useSuspense()', () => { .get(`/article-cooler/400`) .reply(200, payload); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(endpoint, { id: 400, }); @@ -170,7 +169,6 @@ describe('useSuspense()', () => { }); beforeEach(() => { - renderDataClient = makeRenderDataClient(CacheProvider); fbmock.mockReset(); }); @@ -371,7 +369,7 @@ describe('useSuspense()', () => { // taken from integration it('should throw errors on bad network', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(CoolerArticleResource.get, { id: '0', }); @@ -426,7 +424,7 @@ describe('useSuspense()', () => { /*it('should not suspend with null params to useSuspense()', () => { let article: CoolerArticle | undefined; - const { result } = renderDataClient(() => { + const { result } = renderDataHook(() => { const a = useSuspense(CoolerArticleResource.get, null); a.tags; article = a; @@ -440,7 +438,7 @@ describe('useSuspense()', () => { it('should maintain schema structure even with null params', () => { let articles: PaginatedArticle[] | undefined; - const { result } = renderDataClient( + const { result } = renderDataHook( () => { const { results, nextPage } = useSuspense( PaginatedArticleResource.getList, @@ -467,7 +465,7 @@ describe('useSuspense()', () => { it('should suspend with no params to useSuspense()', async () => { const List = CoolerArticleResource.getList; - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(List); }); expect(result.current).toBeUndefined(); @@ -482,7 +480,7 @@ describe('useSuspense()', () => { it('should read with id params Endpoint', async () => { const Detail = FutureArticleResource.get; - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(Detail, 5); }); expect(result.current).toBeUndefined(); @@ -506,7 +504,7 @@ describe('useSuspense()', () => { .defaultReplyHeaders({ 'access-control-allow-origin': '*' }) .get(`/users/${userId}/simple`) .reply(200, response); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(GetNoEntities, { userId }); }); // undefined means it threw @@ -522,7 +520,7 @@ describe('useSuspense()', () => { .defaultReplyHeaders({ 'access-control-allow-origin': '*' }) .get(`/users/${userId}/photo`) .reply(200, response); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(GetPhoto, { userId }); }); // undefined means it threw @@ -540,7 +538,7 @@ describe('useSuspense()', () => { }) .get(`/users/${userId}/photo2`) .reply(200, response); - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(GetPhotoUndefined, { userId }); }); // undefined means it threw @@ -550,7 +548,7 @@ describe('useSuspense()', () => { }); it('should work with Serializable shapes', async () => { - const { result, waitForNextUpdate } = renderDataClient(() => { + const { result, waitForNextUpdate } = renderDataHook(() => { return useSuspense(ArticleTimedResource.get, { id: payload.id }); }); // undefined means it threw @@ -578,7 +576,7 @@ describe('useSuspense()', () => { return 'MyEndpoint'; }, }); - const { result, unmount } = renderDataClient(() => { + const { result, unmount } = renderDataHook(() => { return useSuspense(MyEndpoint); }); expect(result.current).toBeUndefined(); @@ -620,7 +618,7 @@ describe('useSuspense()', () => { {children} ); - const { result, waitForNextUpdate, rerender } = renderDataClient( + const { result, waitForNextUpdate, rerender } = renderDataHook( () => { return { data: useSuspense(ContextAuthdArticleResource.useGet(), { diff --git a/packages/react/src/server/redux/DataProvider.tsx b/packages/react/src/server/redux/DataProvider.tsx index a487f500d42a..ae56fdd202c0 100644 --- a/packages/react/src/server/redux/DataProvider.tsx +++ b/packages/react/src/server/redux/DataProvider.tsx @@ -12,14 +12,14 @@ import { prepareStore } from './prepareStore.js'; import { DevToolsPosition } from '../../components/DevToolsButton.js'; /** For usage with https://dataclient.io/docs/api/makeRenderDataHook */ -export default function ExternalDataProvider({ +export default function TestExternalDataProvider({ children, managers, initialState, Controller, devButton = 'bottom-right', }: Props) { - const { selector, store, controller } = useMemo( + const { selector, store, controller, gcPolicy } = useMemo( () => prepareStore(initialState, managers, Controller), // eslint-disable-next-line react-hooks/exhaustive-deps [Controller, ...managers], @@ -31,10 +31,12 @@ export default function ExternalDataProvider({ for (let i = 0; i < managers.length; ++i) { managers[i].init?.(selector(store.getState())); } + gcPolicy.init(controller); return () => { for (let i = 0; i < managers.length; ++i) { managers[i].cleanup(); } + gcPolicy.cleanup(); }; // we're ignoring state here, because it shouldn't trigger inits // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/packages/react/src/server/redux/prepareStore.tsx b/packages/react/src/server/redux/prepareStore.tsx index 9c42d2483298..d7edd7252eeb 100644 --- a/packages/react/src/server/redux/prepareStore.tsx +++ b/packages/react/src/server/redux/prepareStore.tsx @@ -5,6 +5,7 @@ import { ActionTypes, createReducer, applyManager, + GCPolicy, } from '@data-client/core'; import { combineReducers } from './combineReducers.js'; @@ -24,7 +25,8 @@ export function prepareStore< middlewares: Middleware[] = [] as any, ) { const selector = (s: { dataclient: State }) => s.dataclient; - const controller = new Ctrl(); + const gcPolicy = new GCPolicy(); + const controller = new Ctrl({ gcPolicy }); const reducer = createReducer(controller); const store: Store< StateFromReducersMapObject & { dataclient: State } @@ -39,7 +41,7 @@ export function prepareStore< ...middlewares, ), ) as any; - return { selector, store, controller }; + return { selector, store, controller, gcPolicy }; } // Extension of the DeepPartial type defined by Redux which handles unknown diff --git a/packages/test/src/makeRenderDataClient/index.tsx b/packages/test/src/makeRenderDataClient/index.tsx index c2971e079033..3d1f52b1b78b 100644 --- a/packages/test/src/makeRenderDataClient/index.tsx +++ b/packages/test/src/makeRenderDataClient/index.tsx @@ -18,7 +18,7 @@ import mockInitialState from '../mockState.js'; export type { RenderHookOptions } from './renderHook.cjs'; /** @see https://dataclient.io/docs/api/makeRenderDataHook */ -export default function makeRenderDataClient( +export default function makeRenderDataHook( Provider: React.ComponentType, ) { /** Wraps dispatches that are typically called declaratively in act() */