UNPKG

@dr.pogodin/react-global-state

Version:
354 lines (346 loc) 11.1 kB
import { c as _c } from "react/compiler-runtime"; /** * Loads and uses async data into the GlobalState path. */ import { useEffect, useRef, useState } from 'react'; import { MIN_MS } from '@dr.pogodin/js-utils'; import { useGlobalStateObject } from "./GlobalStateProvider.js"; import useGlobalState from "./useGlobalState.js"; import { cloneDeepForLog, isDebugMode } from "./utils.js"; export const DEFAULT_MAXAGE = 5 * MIN_MS; // 5 minutes. // NOTE: Here, and below it is important whether a loader and related // (re-)loading handlers return a promise or a value, as returning promises // mean the async mode, in which related global state values are updated // asynchronously (the new value comes into effect in a next rendering cycle), // while returning a non-promise value means a synchronous mode, in which // related global state values are updated immediately, within the current // rendering cycle. export function newAsyncDataEnvelope(initialData = null, { numRefs = 0, timestamp = 0 } = {}) { return { data: initialData, numRefs, operationId: '', timestamp }; } /** * Writes data into the global state, and prints console messages in the debug * mode. */ function setState(data, path, gs, prevState = gs.get(path)) { if (process.env.NODE_ENV !== 'production' && isDebugMode()) { /* eslint-disable no-console */ console.groupCollapsed(`ReactGlobalState: async data (re-)loaded. Path: "${path ?? ''}"`); console.log('Data:', cloneDeepForLog(data, path ?? '')); /* eslint-enable no-console */ } gs.set(path, { ...prevState, data, operationId: '', timestamp: Date.now() }); if (process.env.NODE_ENV !== 'production' && isDebugMode()) { /* eslint-disable no-console */ console.groupEnd(); /* eslint-enable no-console */ } } function finalizeLoad(data, path, gs, operationId) { // NOTE: We don't really mean that it hasn't been aborted, // the "false" flag rather says we don't need to trigger "on aborted" // callback for this operation, if any is registered - just drop it. // // Also, in the synchronous state update mode, we don't really need to set up // the abort callback at all (as there is no way to use it), but for now it is // set up, thus it should be cleaned out here. gs.asyncDataLoadDone(operationId, false); const state = gs.get(path); if (operationId === state?.operationId) setState(data, path, gs, state); } /** * Executes the data loading operation. * @param path Data segment path inside the global state. * @param loader Data loader. * @param globalState The global state instance. * @param oldData Optional. Previously fetched data, currently stored in * the state, if already fetched by the caller; otherwise, they will be fetched * by the load() function itself. * @param opIdPrefix operationId prefix to use, which should be * 'C' at the client-side (default), or 'S' at the server-side (within SSR * context). * @return Resolves once the operation is done. * @ignore */ export function loadAsyncData(path, loader, globalState, old, // TODO: Should this parameter be just a binary flag (client or server), // and UUID always generated inside this function? Or do we need it in // the caller methods as well, in some cases (see useAsyncCollection() // use case as well). operationId = `C${globalThis.crypto.randomUUID()}`) { if (process.env.NODE_ENV !== 'production' && isDebugMode()) { /* eslint-disable no-console */ console.log(`ReactGlobalState: async data (re-)loading. Path: "${path ?? ''}"`); /* eslint-enable no-console */ } const operationIdPath = path ? `${path}.operationId` : 'operationId'; { const prevOperationId = globalState.get(operationIdPath); if (prevOperationId) globalState.asyncDataLoadDone(prevOperationId, true); } globalState.set(operationIdPath, operationId); let definedOld = old; if (!definedOld) { // TODO: Can we improve the typing, to avoid ForceT? const e = globalState.get(path); definedOld = { data: e.data, timestamp: e.timestamp }; } const controller = new AbortController(); globalState.setAsyncDataAbortCallback(operationId, () => { controller.abort(); }); const dataOrPromise = loader(definedOld.data, { abortSignal: controller.signal, oldDataTimestamp: definedOld.timestamp }); if (dataOrPromise instanceof Promise) { return dataOrPromise.then(data => { finalizeLoad(data, path, globalState, operationId); }).finally(() => { // NOTE: We don't really mean that it hasn't been aborted, // the "false" flag rather says we don't need to trigger "on aborted" // callback for this operation, if any is registered - just drop it. globalState.asyncDataLoadDone(operationId, false); }); } finalizeLoad(dataOrPromise, path, globalState, operationId); return undefined; } /** * Resolves asynchronous data, and stores them at given `path` of global * state. */ // TODO: Perhaps split the heap management to a dedicated hook, // as it is done inside useAsyncCollection(). function useAsyncData(path, loader, t0) { const $ = _c(38); let t1; if ($[0] !== t0) { t1 = t0 === undefined ? {} : t0; $[0] = t0; $[1] = t1; } else { t1 = $[1]; } const options = t1; const maxage = options.maxage ?? DEFAULT_MAXAGE; const refreshAge = options.refreshAge ?? maxage; const garbageCollectAge = options.garbageCollectAge ?? maxage; const globalState = useGlobalStateObject(); const state = globalState.get(path, { initialValue: newAsyncDataEnvelope() }); let t2; if ($[2] !== globalState || $[3] !== loader || $[4] !== path) { t2 = { globalState, loader, path }; $[2] = globalState; $[3] = loader; $[4] = path; $[5] = t2; } else { t2 = $[5]; } const { current: heap } = useRef(t2); let t3; if ($[6] !== globalState || $[7] !== loader || $[8] !== path) { t3 = () => { heap.globalState = globalState; heap.path = path; heap.loader = loader; }; $[6] = globalState; $[7] = loader; $[8] = path; $[9] = t3; } else { t3 = $[9]; } useEffect(t3); let t4; if ($[10] === Symbol.for("react.memo_cache_sentinel")) { t4 = () => ({ reload: customLoader => { const localLoader = customLoader ?? heap.loader; return loadAsyncData(heap.path, localLoader, heap.globalState); }, set: data => { setState(data, heap.path, heap.globalState); } }); $[10] = t4; } else { t4 = $[10]; } const [stable] = useState(t4); if (globalState.ssrContext) { if (!options.disabled && !options.noSSR && !state.operationId && !state.timestamp) { const promiseOrVoid = loadAsyncData(path, loader, globalState, { data: state.data, timestamp: state.timestamp }, `S${globalThis.crypto.randomUUID()}`); if (promiseOrVoid instanceof Promise) { globalState.ssrContext.pending.push(promiseOrVoid); } } } const { disabled } = options; let t5; let t6; if ($[11] !== disabled || $[12] !== garbageCollectAge || $[13] !== globalState || $[14] !== path) { t5 = () => { const numRefsPath = path ? `${path}.numRefs` : "numRefs"; if (!disabled) { const numRefs = globalState.get(numRefsPath); globalState.set(numRefsPath, numRefs + 1); } return () => { if (!disabled) { const state2 = globalState.get(path); if (state2.numRefs === 1 && garbageCollectAge < Date.now() - state2.timestamp) { if (process.env.NODE_ENV !== "production" && isDebugMode()) { console.log(`ReactGlobalState - useAsyncData garbage collected at path ${path ?? ""}`); } globalState.dropDependencies(path ?? ""); globalState.set(path, { ...state2, data: null, numRefs: 0, timestamp: 0 }); } else { globalState.set(numRefsPath, state2.numRefs - 1); } } }; }; t6 = [disabled, garbageCollectAge, globalState, path]; $[11] = disabled; $[12] = garbageCollectAge; $[13] = globalState; $[14] = path; $[15] = t5; $[16] = t6; } else { t5 = $[15]; t6 = $[16]; } useEffect(t5, t6); let t7; if ($[17] !== disabled || $[18] !== globalState || $[19] !== loader || $[20] !== options || $[21] !== path || $[22] !== refreshAge) { t7 = () => { if (!disabled) { const state2_0 = globalState.get(path); const { deps } = options; if (deps && globalState.hasChangedDependencies(path ?? "", deps) || refreshAge < Date.now() - state2_0.timestamp && (!state2_0.operationId || state2_0.operationId.startsWith("S"))) { if (!deps) { globalState.dropDependencies(path ?? ""); } loadAsyncData(path, loader, globalState, { data: state2_0.data, timestamp: state2_0.timestamp }); } } }; $[17] = disabled; $[18] = globalState; $[19] = loader; $[20] = options; $[21] = path; $[22] = refreshAge; $[23] = t7; } else { t7 = $[23]; } useEffect(t7); let t8; if ($[24] === Symbol.for("react.memo_cache_sentinel")) { t8 = newAsyncDataEnvelope(); $[24] = t8; } else { t8 = $[24]; } const [localState] = useGlobalState(path, t8); let t9; if ($[25] !== localState.timestamp || $[26] !== maxage) { t9 = () => maxage < Date.now() - localState.timestamp; $[25] = localState.timestamp; $[26] = maxage; $[27] = t9; } else { t9 = $[27]; } const [stale, setStale] = useState(t9); let t10; if ($[28] !== localState.timestamp || $[29] !== maxage || $[30] !== stale) { t10 = () => { const nowStale = maxage < Date.now() - localState.timestamp; const id = stale === nowStale ? null : requestAnimationFrame(() => { setStale(nowStale); }); return () => { if (id !== null) { cancelAnimationFrame(id); } }; }; $[28] = localState.timestamp; $[29] = maxage; $[30] = stale; $[31] = t10; } else { t10 = $[31]; } useEffect(t10); const t11 = stale ? null : localState.data; const t12 = !!localState.operationId; let t13; if ($[32] !== localState.timestamp || $[33] !== stable.reload || $[34] !== stable.set || $[35] !== t11 || $[36] !== t12) { t13 = { data: t11, loading: t12, reload: stable.reload, set: stable.set, timestamp: localState.timestamp }; $[32] = localState.timestamp; $[33] = stable.reload; $[34] = stable.set; $[35] = t11; $[36] = t12; $[37] = t13; } else { t13 = $[37]; } return t13; } export { useAsyncData }; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions // eslint-disable-next-line @typescript-eslint/consistent-type-definitions //# sourceMappingURL=useAsyncData.js.map