UNPKG

react-suspense-fetch

Version:

A low-level library for React Suspense for Data Fetching

200 lines (190 loc) 5.09 kB
type FetchFunc<Result, Input> = ( input: Input, options: { signal: AbortSignal }, ) => Promise<Result>; type GetOptions = { forcePrefetch?: boolean; }; /** * fetch store * * `prefetch` will start fetching. * `get` will return a result or throw a promise when a result is not ready. * `preset` will set a result without fetching. * `evict` will remove a result. * `abort` will cancel fetching. * * There are three cache types: * - WeakMap: `input` has to be an object in this case * - Map: you need to call evict to remove from cache * - Map with areEqual: you can specify a custom comparator */ export type FetchStore<Result, Input> = { prefetch: (input: Input) => void; get: (input: Input, option?: GetOptions) => Result; preset: (input: Input, result: Result) => void; evict: (input: Input) => void; abort: (input: Input) => void; }; const isObject = (x: unknown): x is object => typeof x === 'object' && x !== null; const createMapLikeWithComparator = <K, V>(areEqual: (a: K, b: K) => boolean) => { const map = new Map<K, V>(); const has = (key: K) => { for (const [k] of map) { if (areEqual(k, key)) { return true; } } return false; }; const get = (key: K) => { for (const [k, v] of map) { if (areEqual(k, key)) { return v; } } return undefined; }; const remove = (key: K) => { for (const [k] of map) { if (areEqual(k, key)) { map.delete(k); } } }; return { set: (key: K, value: V) => { map.set(key, value); }, has, get, delete: remove, }; }; type CacheType<Input> = | { type: 'WeakMap'} | { type: 'Map'; areEqual?: ((a: Input, b: Input) => boolean); }; const createCache = <Input, Instance>( cacheType?: CacheType<Input>, ) => { if (cacheType?.type === 'WeakMap') { return new WeakMap<object, Instance>() as unknown as Map<Input, Instance>; } const areEqual = cacheType?.type === 'Map' && cacheType.areEqual; if (areEqual) { return createMapLikeWithComparator<Input, Instance>(areEqual); } return new Map<Input, Instance>(); }; export function createFetchStore<Result, Input extends object>( fetchFunc: FetchFunc<Result, Input>, cacheType: { type: 'WeakMap' }, presets?: Iterable<readonly [Input, Result]>, ): FetchStore<Result, Input> export function createFetchStore<Result, Input>( fetchFunc: FetchFunc<Result, Input>, cacheType?: { type: 'Map'; areEqual?: ((a: Input, b: Input) => boolean); }, presets?: Iterable<readonly [Input, Result]>, ): FetchStore<Result, Input> /** * create fetch store * * @example * import { createFetchStore } from 'react-suspense-fetch'; * * const fetchFunc = async (userId) => (await fetch(`https://reqres.in/api/users/${userId}?delay=3`)).json(); * const store = createFetchStore(fetchFunc); * store.prefetch('1'); */ export function createFetchStore<Result, Input>( fetchFunc: FetchFunc<Result, Input>, cacheType?: CacheType<Input>, presets?: Iterable<readonly [Input, Result]>, ) { type Instance = { get: () => Result; abort: () => void; }; const cache = createCache<Input, Instance>(cacheType); const assertObjectInput = (input: Input) => { if (cacheType?.type === 'WeakMap' && !isObject(input)) { throw new Error('WeakMap requires object input'); } }; const preset = (input: Input, result: Result) => { assertObjectInput(input); cache.set(input, { get: () => result, abort: () => { // nothing }, }); }; if (presets) { for (const [input, result] of presets) { preset(input, result); } } const createInstance = (input: Input) => { let promise: Promise<void> | null = null; let result: Result | null = null; let error: unknown | null = null; const controller = new AbortController(); promise = (async () => { try { result = await fetchFunc(input, { signal: controller.signal }); } catch (e) { error = e; } finally { promise = null; } })(); return { get: () => { if (promise) throw promise; if (error !== null) throw error; return result as Result; }, abort: () => { controller.abort(); }, }; }; const prefetch = (input: Input) => { assertObjectInput(input); if (!cache.has(input)) { cache.set(input, createInstance(input)); } }; const get = (input: Input, options?: GetOptions) => { assertObjectInput(input); if (options?.forcePrefetch) { prefetch(input); } const instance = cache.get(input); if (!instance) { throw new Error('prefetch() must be called before get()'); } return instance.get(); }; const evict = (input: Input) => { assertObjectInput(input); cache.delete(input); }; const abort = (input: Input) => { assertObjectInput(input); cache.get(input)?.abort(); }; const store: FetchStore<Result, Input> = { prefetch, get, preset, evict, abort, }; return store; }