diff --git a/.vscode/settings.json b/.vscode/settings.json index f83dad098228..38f726672216 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -48,6 +48,7 @@ "packages/**/index.d.ts": true, "packages/*/lib": true, "packages/*/legacy": true, + "packages/*/native": true, "yarn.lock": true, "**/versioned_docs": true, "**/*_versioned_docs": true, diff --git a/package.json b/package.json index a165ec473dd3..017bad3ed85f 100644 --- a/package.json +++ b/package.json @@ -67,6 +67,7 @@ "@react-navigation/native-stack": "^7.0.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^6.6.3", + "@testing-library/jest-native": "^5.4.3", "@testing-library/react": "16.1.0", "@testing-library/react-hooks": "8.0.1", "@testing-library/react-native": "13.0.0", diff --git a/packages/core/src/state/GCPolicy.ts b/packages/core/src/state/GCPolicy.ts index 9ed3dfb9fab4..9cc410b80eaf 100644 --- a/packages/core/src/state/GCPolicy.ts +++ b/packages/core/src/state/GCPolicy.ts @@ -24,18 +24,7 @@ export class GCPolicy implements GCInterface { this.controller = controller; this.intervalId = setInterval(() => { - if (typeof requestIdleCallback === 'function') { - requestIdleCallback(() => this.runSweep(), { timeout: 1000 }); - } else { - /* TODO: React native - import { InteractionManager } from 'react-native'; - InteractionManager.runAfterInteractions(callback); - if (options?.timeout) { - InteractionManager.setDeadline(options.timeout); - } - */ - this.runSweep(); - } + this.idleCallback(() => this.runSweep(), { timeout: 1000 }); }, this.options.intervalMS); } @@ -119,6 +108,21 @@ export class GCPolicy implements GCInterface { this.controller.dispatch({ type: GC, entities, endpoints }); } } + + /** Calls the callback when client is not 'busy' with high priority interaction tasks + * + * Override for platform-specific implementations + */ + protected idleCallback( + callback: (...args: any[]) => void, + options?: IdleRequestOptions, + ) { + if (typeof requestIdleCallback === 'function') { + requestIdleCallback(callback, options); + } else { + callback(); + } + } } export class ImmortalGCPolicy implements GCInterface { diff --git a/packages/core/src/state/reducer/createReducer.ts b/packages/core/src/state/reducer/createReducer.ts index e0c1b839d93b..c53c971290b0 100644 --- a/packages/core/src/state/reducer/createReducer.ts +++ b/packages/core/src/state/reducer/createReducer.ts @@ -25,6 +25,7 @@ export default function createReducer(controller: Controller): ReducerType { if (!state) state = initialState; switch (action.type) { case GC: + console.log('GC action', action.endpoints, action.entities); // inline deletes are fine as these should have 0 refcounts action.entities.forEach(({ key, pk }) => { delete (state as any).entities[key]?.[pk]; diff --git a/packages/react/src/__tests__/integration-garbage-collection.native.tsx b/packages/react/src/__tests__/integration-garbage-collection.native.tsx new file mode 100644 index 000000000000..9ff1b5dbbba8 --- /dev/null +++ b/packages/react/src/__tests__/integration-garbage-collection.native.tsx @@ -0,0 +1,150 @@ +import { + AsyncBoundary, + DataProvider, + GCPolicy, + useSuspense, +} from '@data-client/react'; +import { MockResolver } from '@data-client/test'; +import { render, screen, act, fireEvent } from '@testing-library/react-native'; +import { ArticleResource } from '__tests__/new'; +import { useState } from 'react'; +import '@testing-library/jest-native'; +import { View, Text, Button, TouchableOpacity } from 'react-native'; + +const mockGetList = jest.fn(); +const mockGet = jest.fn(); +const GC_INTERVAL = 5000; + +const ListView = ({ onSelect }: { onSelect: (id: number) => void }) => { + const articles = useSuspense(ArticleResource.getList); + return ( + + {articles.map(article => ( + onSelect(article.id ?? 0)} + > + {article.title} + + ))} + + ); +}; + +const DetailView = ({ id }: { id: number }) => { + const article = useSuspense(ArticleResource.get, { id }); + const [toggle, setToggle] = useState(false); + + return ( + + {article.title} + {article.content} +