UNPKG

@dr.pogodin/react-global-state

Version:
270 lines (257 loc) 9.9 kB
"use strict"; var _interopRequireDefault = require("@babel/runtime/helpers/interopRequireDefault"); Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _react = require("react"); var _uuid = require("uuid"); var _GlobalStateProvider = require("./GlobalStateProvider"); var _useAsyncData = require("./useAsyncData"); var _useGlobalState = _interopRequireDefault(require("./useGlobalState")); var _utils = require("./utils"); /** * Loads and uses item(s) in an async collection. */ /** * GarbageCollector: the piece of logic executed on mounting of * an useAsyncCollection() hook, and on update of hook params, to update * the state according to the new param values. It increments by 1 `numRefs` * counters for the requested collection items. */ function gcOnWithhold(ids, path, gs) { const collection = { ...gs.get(path) }; for (const id of ids) { let envelope = collection[id]; if (envelope) envelope = { ...envelope, numRefs: 1 + envelope.numRefs };else envelope = (0, _useAsyncData.newAsyncDataEnvelope)(null, { numRefs: 1 }); collection[id] = envelope; } gs.set(path, collection); } function idsToStringSet(ids) { const res = new Set(); for (const id of ids) { res.add(id.toString()); } return res; } /** * GarbageCollector: the piece of logic executed on un-mounting of * an useAsyncCollection() hook, and on update of hook params, to clean-up * after the previous param values. It decrements by 1 `numRefs` counters * for previously requested collection items, and also drops from the state * stale records. */ function gcOnRelease(ids, path, gs, gcAge) { const entries = Object.entries(gs.get(path)); const now = Date.now(); const idSet = idsToStringSet(ids); const collection = {}; for (const [id, envelope] of entries) { if (envelope) { const toBeReleased = idSet.has(id); let { numRefs } = envelope; if (toBeReleased) --numRefs; if (gcAge > now - envelope.timestamp || numRefs > 0) { collection[id] = toBeReleased ? { ...envelope, numRefs } : envelope; } else if (process.env.NODE_ENV !== 'production' && (0, _utils.isDebugMode)()) { // eslint-disable-next-line no-console console.log(`useAsyncCollection(): Garbage collected at the path "${path}", ID = ${id}`); } } } gs.set(path, collection); } function normalizeIds(idOrIds) { if (Array.isArray(idOrIds)) { const res = [...idOrIds]; res.sort((a, b) => a.toString().localeCompare(b.toString())); return res; } return [idOrIds]; } /** * Inits/updates, and returns the heap. */ function useHeap(ids, path, loader, gs) { const ref = (0, _react.useRef)(undefined); let heap = ref.current; if (heap) { // Update. heap.ids = ids; heap.path = path; heap.loader = loader; heap.globalState = gs; } else { // Initialization. const reload = async customLoader => { const heap2 = ref.current; const localLoader = customLoader ?? heap2.loader; // TODO: Revise - not sure all related typing is 100% correct, // thus let's keep this runtime assertion. // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition if (!localLoader || !heap2.globalState || !heap2.ids) { throw Error('Internal error'); } for (const id of heap2.ids) { const itemPath = heap2.path ? `${heap2.path}.${id}` : `${id}`; const promiseOrVoid = (0, _useAsyncData.load)(itemPath, // TODO: Revise! Most probably we don't have fully correct loader // typing, as it may return either promise or value, and those two // cases call for different runtime behavior, which in turns only // happens if the outer function on the next line matches the same // async / sync signature. // eslint-disable-next-line @typescript-eslint/promise-function-async (oldData, meta) => localLoader(id, oldData, meta), heap2.globalState); if (promiseOrVoid instanceof Promise) await promiseOrVoid; } }; heap = { globalState: gs, ids, loader, path, reload, // TODO: Revise! Most probably we don't have fully correct loader // typing, as it may return either promise or value, and those two // cases call for different runtime behavior, which in turns only // happens if the outer function on the next line matches the same // async / sync signature. // eslint-disable-next-line @typescript-eslint/promise-function-async reloadSingle: customLoader => ref.current.reload( // TODO: Revise! Most probably we don't have fully correct loader // typing, as it may return either promise or value, and those two // cases call for different runtime behavior, which in turns only // happens if the outer function on the next line matches the same // async / sync signature. // eslint-disable-next-line @typescript-eslint/promise-function-async customLoader && ((id, ...args) => customLoader(...args))) }; ref.current = heap; } return heap; } /** * Resolves and stores at the given `path` of the global state elements of * an asynchronous data collection. */ // TODO: This is largely similar to useAsyncData() logic, just more generic. // Perhaps, a bunch of logic blocks can be split into stand-alone functions, // and reused in both hooks. // eslint-disable-next-line complexity function useAsyncCollection(idOrIds, path, loader, options = {}) { const ids = normalizeIds(idOrIds); const maxage = options.maxage ?? _useAsyncData.DEFAULT_MAXAGE; const refreshAge = options.refreshAge ?? maxage; const garbageCollectAge = options.garbageCollectAge ?? maxage; const globalState = (0, _GlobalStateProvider.getGlobalState)(); const heap = useHeap(ids, path, loader, globalState); // Server-side logic. if (globalState.ssrContext && !options.noSSR) { const operationId = `S${(0, _uuid.v4)()}`; for (const id of ids) { const itemPath = path ? `${path}.${id}` : `${id}`; const state = globalState.get(itemPath, { initialValue: (0, _useAsyncData.newAsyncDataEnvelope)() }); if (!state.timestamp && !state.operationId) { const promiseOrVoid = (0, _useAsyncData.load)(itemPath, (...args) => loader(id, ...args), globalState, { data: state.data, timestamp: state.timestamp }, operationId); if (promiseOrVoid instanceof Promise) { globalState.ssrContext.pending.push(promiseOrVoid); } } } // Client-side logic. } else { // Reference-counting & garbage collection. const idsHash = (0, _utils.hash)(ids); // TODO: Violation of rules of hooks is fine here, // but perhaps it can be refactored to avoid the need for it. (0, _react.useEffect)(() => { // eslint-disable-line react-hooks/rules-of-hooks gcOnWithhold(ids, path, globalState); return () => { gcOnRelease(ids, path, globalState, garbageCollectAge); }; // `ids` are represented in the dependencies array by `idsHash` value, // as useEffect() hook requires a constant size of dependencies array. // eslint-disable-next-line react-hooks/exhaustive-deps }, [garbageCollectAge, globalState, idsHash, path]); // NOTE: a bunch of Rules of Hooks ignored belows because in our very // special case the otherwise wrong behavior is actually what we need. // Data loading and refreshing. (0, _react.useEffect)(() => { // eslint-disable-line react-hooks/rules-of-hooks void (async () => { for (const id of ids) { const itemPath = path ? `${path}.${id}` : `${id}`; const state2 = globalState.get(itemPath); const { deps } = options; if (deps && globalState.hasChangedDependencies(itemPath, deps) || refreshAge < Date.now() - (state2?.timestamp ?? 0) && (!state2?.operationId || state2.operationId.startsWith('S'))) { if (!deps) globalState.dropDependencies(itemPath); await (0, _useAsyncData.load)(itemPath, // TODO: I guess, the loader is not correctly typed here - // it can be synchronous, and in that case the following method // should be kept synchronous to not alter the sync logic. // eslint-disable-next-line @typescript-eslint/promise-function-async (old, ...args) => loader(id, old, ...args), globalState, { data: state2?.data, timestamp: state2?.timestamp ?? 0 }); } } })(); }); } const [localState] = (0, _useGlobalState.default)(path, {}); if (!Array.isArray(idOrIds)) { // TODO: Revise related typings! const e = localState[idOrIds]; const timestamp = e?.timestamp ?? 0; return { data: maxage < Date.now() - timestamp ? null : e?.data ?? null, loading: !!e?.operationId, reload: heap.reloadSingle, timestamp }; } const res = { items: {}, loading: false, reload: heap.reload, timestamp: Number.MAX_VALUE }; for (const id of ids) { // TODO: Revise related typing. Should `localState` have a more specific type? const e = localState[id]; const loading = !!e?.operationId; const timestamp = e?.timestamp ?? 0; res.items[id] = { data: maxage < Date.now() - timestamp ? null : e?.data ?? null, loading, timestamp }; res.loading ||= loading; if (res.timestamp > timestamp) res.timestamp = timestamp; } return res; } var _default = exports.default = useAsyncCollection; // eslint-disable-next-line @typescript-eslint/consistent-type-definitions //# sourceMappingURL=useAsyncCollection.js.map