Skip to content

Commit

Permalink
enhance(native): Reduce chance of frame drops with InteractionManager (
Browse files Browse the repository at this point in the history
  • Loading branch information
ntucker committed Jun 22, 2024
1 parent da255ba commit c18fbf7
Show file tree
Hide file tree
Showing 17 changed files with 117 additions and 37 deletions.
7 changes: 7 additions & 0 deletions .changeset/twelve-rockets-divide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@data-client/react': patch
---

React Native calls fetches in InteractionManager.runAfterInteractions callback

This reduces the chance of frame drops.
5 changes: 5 additions & 0 deletions .changeset/twelve-socks-hope.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@data-client/core': patch
---

Remove RIC export
20 changes: 20 additions & 0 deletions .changeset/young-needles-hide.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
---
'@data-client/core': patch
---

Add NetworkManager.idleCallback overridable method

This allows platform specific implementations by overriding the method.
For instance, on web:

```ts
import { NetworkManager } from '@data-client/core';

export default class WebNetworkManager extends NetworkManager {
static {
if (typeof requestIdleCallback === 'function') {
WebNetworkManager.prototype.idleCallback = requestIdleCallback;
}
}
}
```
1 change: 0 additions & 1 deletion packages/core/src/internal.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
export { MemoCache, INVALID } from '@data-client/normalizr';
export { default as RIC } from './state/RIC.js';
export { initialState } from './state/reducer/createReducer.js';
20 changes: 15 additions & 5 deletions packages/core/src/manager/NetworkManager.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SET_RESPONSE_TYPE, FETCH_TYPE, RESET_TYPE } from '../actionTypes.js';
import Controller from '../controller/Controller.js';
import createSetResponse from '../controller/createSetResponse.js';
import RIC from '../state/RIC.js';
import type {
FetchAction,
Manager,
Expand Down Expand Up @@ -282,16 +281,27 @@ export default class NetworkManager implements Manager {
});
this.fetchedAt[key] = createdAt;

// since our real promise is resolved via the wrapReducer(),
// we should just stop all errors here.
// TODO: decouple this from useFetcher() (that's what's dispatching the error the resolves in here)
RIC(
this.idleCallback(
() => {
// since our real promise is resolved via the wrapReducer(),
// we should just stop all errors here.
// TODO: decouple this from useFetcher() (that's what's dispatching the error the resolves in here)
fetch().catch(() => null);
},
{ timeout: 500 },
);

return this.fetched[key];
}

/** 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,
) {
callback();
}
}
5 changes: 0 additions & 5 deletions packages/core/src/state/RIC.ts

This file was deleted.

2 changes: 1 addition & 1 deletion packages/react/src/components/DataProvider.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
'use client';
import {
initialState as defaultState,
NetworkManager,
Controller as DataController,
applyManager,
SubscriptionManager,
Expand All @@ -17,6 +16,7 @@ import type { DevToolsPosition } from './DevToolsButton.js';
import { SSR } from './LegacyReact.js';
import { renderDevButton } from './renderDevButton.js';
import { ControllerContext } from '../context.js';
import { NetworkManager } from '../managers/index.js';

export interface ProviderProps {
children: React.ReactNode;
Expand Down
13 changes: 2 additions & 11 deletions packages/react/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,7 @@ Object.hasOwn =
/* istanbul ignore next */ function hasOwn(it, key) {
return Object.prototype.hasOwnProperty.call(it, key);
};
export {
PollingSubscription,
DevToolsManager,
SubscriptionManager,
DefaultConnectionListener,
NetworkManager,
LogoutManager,
Controller,
ExpiryStatus,
actionTypes,
} from '@data-client/core';
export { Controller, ExpiryStatus, actionTypes } from '@data-client/core';
export type {
EndpointExtraOptions,
FetchFunction,
Expand Down Expand Up @@ -47,6 +37,7 @@ export type {
DataClientDispatch,
GenericDispatch,
} from '@data-client/core';
export * from './managers/index.js';
export * from './components/index.js';
export * from './hooks/index.js';
export { StateContext, ControllerContext, StoreContext } from './context.js';
Expand Down
18 changes: 18 additions & 0 deletions packages/react/src/managers/NetworkManager.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { NetworkManager } from '@data-client/core';
import { InteractionManager } from 'react-native';

export default class NativeNetworkManager extends NetworkManager {
/** 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,
) {
InteractionManager.runAfterInteractions(callback);
if (options?.timeout) {
InteractionManager.setDeadline(options.timeout);
}
}
}
9 changes: 9 additions & 0 deletions packages/react/src/managers/NetworkManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { NetworkManager } from '@data-client/core';

export default class WebNetworkManager extends NetworkManager {
static {
if (typeof requestIdleCallback === 'function') {
WebNetworkManager.prototype.idleCallback = requestIdleCallback;
}
}
}
12 changes: 12 additions & 0 deletions packages/react/src/managers/__tests__/RIC.native.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { NetworkManager } 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, {});
jest.runAllTimers();
expect(fn).toHaveBeenCalled();
jest.useRealTimers();
});
});
Original file line number Diff line number Diff line change
@@ -1,15 +1,16 @@
describe('RequestIdleCallback', () => {
it('should still run when requestIdleCallback is not available', () => {
it('should still run when requestIdleCallback is not available', async () => {
const requestIdle = (global as any).requestIdleCallback;
(global as any).requestIdleCallback = undefined;
jest.resetModules();
// eslint-disable-next-line @typescript-eslint/no-var-requires
const RIC = require('../RIC').default;
const { NetworkManager } = await import('..');
const fn = jest.fn();
jest.useFakeTimers();
RIC(fn, {});
// @ts-expect-error
new NetworkManager().idleCallback(fn, {});
jest.runAllTimers();
expect(fn).toBeCalled();
expect(fn).toHaveBeenCalled();
(global as any).requestIdleCallback = requestIdle;
jest.useRealTimers();
});
Expand Down
8 changes: 8 additions & 0 deletions packages/react/src/managers/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export {
PollingSubscription,
DevToolsManager,
SubscriptionManager,
DefaultConnectionListener,
LogoutManager,
} from '@data-client/core';
export { default as NetworkManager } from './NetworkManager.js';
3 changes: 2 additions & 1 deletion packages/react/src/server/createPersistedStore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,14 @@ import { useSyncExternalStore } from 'react';
import { PromiseifyMiddleware } from './redux/index.js';
import { createStore, applyMiddleware } from './redux/redux.js';
import SSRDataProvider from './SSRDataProvider.js';
import { NetworkManager as ReactNetworkManager } from '../managers/index.js';

export default function createPersistedStore(
managers?: Manager[],
hasDevManager: boolean = true,
) {
const controller = new Controller();
managers = managers ?? [new NetworkManager()];
managers = managers ?? [new ReactNetworkManager()];
const nm: NetworkManager = managers.find(
m => m instanceof NetworkManager,
) as any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ import {
import type { ComponentProps } from 'react';

import type DataProvider from '../../../components/DataProvider.js';
import { NetworkManager as ReactNetworkManager } from '../../../managers/index.js';
import { PromiseifyMiddleware } from '../../redux/index.js';
import { createStore, applyMiddleware } from '../../redux/redux.js';
import SSRDataProvider from '../../SSRDataProvider.js';

export default function createPersistedStore(managers?: Manager[]) {
const controller = new Controller();
managers = managers ?? [new NetworkManager()];
managers = managers ?? [new ReactNetworkManager()];
const nm: NetworkManager = managers.find(
m => m instanceof NetworkManager,
) as any;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -233,9 +233,6 @@ type FetchFunction<A extends readonly any[] = any, R = any> = (...args: A) => Pr

declare const INVALID: unique symbol;

declare const RIC: (cb: (...args: any[]) => void, options: any) => void;
//# sourceMappingURL=RIC.d.ts.map

type ResultEntry<E extends EndpointInterface> = E['schema'] extends undefined | null ? ResolveType<E> : Normalize<E['schema']>;
type EndpointUpdateFunction<Source extends EndpointInterface, Updaters extends Record<string, any> = Record<string, any>> = (source: ResultEntry<Source>, ...args: any) => {
[K in keyof Updaters]: (result: Updaters[K]) => Updaters[K];
Expand Down Expand Up @@ -602,13 +599,11 @@ type ReducerType = (state: State<unknown> | undefined, action: ActionTypes) => S
type internal_d_MemoCache = MemoCache;
declare const internal_d_MemoCache: typeof MemoCache;
declare const internal_d_INVALID: typeof INVALID;
declare const internal_d_RIC: typeof RIC;
declare const internal_d_initialState: typeof initialState;
declare namespace internal_d {
export {
internal_d_MemoCache as MemoCache,
internal_d_INVALID as INVALID,
internal_d_RIC as RIC,
internal_d_initialState as initialState,
};
}
Expand Down Expand Up @@ -690,6 +685,11 @@ declare class NetworkManager implements Manager {
* by the reducer.
*/
protected throttle(key: string, fetch: () => Promise<any>, createdAt: number): Promise<any>;
/** 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): void;
}

declare function applyManager(managers: Manager[], controller: Controller): Middleware$1[];
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import * as _data_client_core from '@data-client/core';
import { 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 } 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 { 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 } 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, 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';

declare class WebNetworkManager extends NetworkManager {
}

declare function BackupLoading(): react_jsx_runtime.JSX.Element;

type DevToolsPosition = 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
Expand Down Expand Up @@ -415,4 +418,4 @@ declare namespace internal_d {
/** Turns a dispatch function into one that resolves once its been commited */
declare function usePromisifiedDispatch<R extends React.Reducer<any, any>>(dispatch: React.Dispatch<React.ReducerAction<R>>, state: React.ReducerState<R>): (action: React.ReducerAction<R>) => Promise<void>;

export { _default as AsyncBoundary, BackupLoading, DataProvider as CacheProvider, ControllerContext, DataProvider, DevToolsPosition, ErrorBoundary, ErrorBoundary as NetworkErrorBoundary, ProviderProps, StateContext, Store, StoreContext, UniversalSuspense, internal_d as __INTERNAL__, getDefaultManagers, useCache, useCancelling, useController, useDLE, useDebounce, useError, useFetch, useLive, useLoading, usePromisifiedDispatch, useQuery, useSubscription, useSuspense };
export { _default as AsyncBoundary, BackupLoading, DataProvider as CacheProvider, ControllerContext, DataProvider, DevToolsPosition, ErrorBoundary, ErrorBoundary as NetworkErrorBoundary, WebNetworkManager as NetworkManager, ProviderProps, StateContext, Store, StoreContext, UniversalSuspense, internal_d as __INTERNAL__, getDefaultManagers, useCache, useCancelling, useController, useDLE, useDebounce, useError, useFetch, useLive, useLoading, usePromisifiedDispatch, useQuery, useSubscription, useSuspense };

0 comments on commit c18fbf7

Please sign in to comment.