UNPKG

@baqhub/sdk-react

Version:

The official React SDK for the BAQ federated app platform.

1,127 lines (1,126 loc) 55 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createStore = createStore; const tslib_1 = require("tslib"); const jsx_runtime_1 = require("react/jsx-runtime"); const sdk_1 = require("@baqhub/sdk"); const isEqual_js_1 = tslib_1.__importDefault(require("lodash/isEqual.js")); const memoize_js_1 = tslib_1.__importDefault(require("lodash/memoize.js")); const orderBy_js_1 = tslib_1.__importDefault(require("lodash/orderBy.js")); const uniq_js_1 = tslib_1.__importDefault(require("lodash/uniq.js")); const uniqBy_js_1 = tslib_1.__importDefault(require("lodash/uniqBy.js")); const react_1 = require("react"); const with_selector_js_1 = require("use-sync-external-store/with-selector.js"); const equality_js_1 = require("../../helpers/equality.js"); const hooks_js_1 = require("../../helpers/hooks.js"); const proxyStore_js_1 = require("./proxyStore.js"); const storeContext_js_1 = require("./storeContext.js"); const storeIdentity_js_1 = require("./storeIdentity.js"); const storeMutation_js_1 = require("./storeMutation.js"); const storeQuery_js_1 = require("./storeQuery.js"); function createStore(...types) { const RIntermediate = sdk_1.IO.union([sdk_1.EntityRecord, sdk_1.EntityRecord, ...types]); const RKnownRecord = sdk_1.IO.union([ RIntermediate, sdk_1.StandingRecord, sdk_1.SubscriptionRecord, ]); const RKnownEventRecord = sdk_1.IO.union([RKnownRecord, sdk_1.RNoContentRecord]); // // Store. // const { StoreContext, useStoreContext } = (0, storeContext_js_1.buildStoreContext)(); const { ProxyStoreContext, useProxyStoreContext } = (0, proxyStore_js_1.buildProxyStoreContext)(); const ProxyStore = props => { const { entity, children } = props; const storeContext = useStoreContext(); const accessors = (0, react_1.useMemo)(() => (0, proxyStore_js_1.buildAccessors)(storeContext.entity, entity), [storeContext.entity, entity]); const context = (0, react_1.useMemo)(() => ({ proxyEntity: entity, accessors: accessors, helpers: (0, proxyStore_js_1.buildHelpers)(storeContext, accessors, entity), }), [entity, accessors, storeContext]); return ((0, jsx_runtime_1.jsx)(ProxyStoreContext.Provider, { value: context, children: children })); }; function wrapInProxyStore(entity) { return (children) => { return (0, jsx_runtime_1.jsx)(ProxyStore, { entity: entity, children: children }); }; } const Store = props => { const { onDisconnectRequest, children } = props; const identity = (0, react_1.useMemo)(() => { return props.identity || storeIdentity_js_1.StoreIdentity.newUnauthenticated(); }, [props.identity]); const { entityRecord, findClient, blobUrlBuilder } = identity; const { entity } = entityRecord.author; const onDisconnectRequestStable = (0, hooks_js_1.useStable)(onDisconnectRequest); const { isMountedRef } = (0, hooks_js_1.useIsMounted)(); const stateRef = (0, react_1.useRef)({ versions: { [entityRecord.version.hash]: entityRecord, }, state: { [entity]: { dictionary: { [sdk_1.Record.toKey(entityRecord)]: entityRecord }, list: [entityRecord], }, }, stateSubscriptions: [], queries: {}, queriesSubscriptions: [], liveQueries: [], liveQueriesSubscriptions: [], lastQueryId: 0, mutations: [], mutationsSubscriptions: [], blobUrls: new Map(), isUpdating: false, }); const updateRecordsInState = (0, react_1.useCallback)((proxyEntity, updater) => { const { state, stateSubscriptions, isUpdating } = stateRef.current; if (isUpdating) { throw new Error("State is already updating."); } stateRef.current.isUpdating = true; const entityState = state[proxyEntity] || { dictionary: {}, list: [] }; const newRecords = updater(entityState.dictionary); if (newRecords === entityState.dictionary) { stateRef.current.isUpdating = false; return; } stateRef.current.state = { ...state, [proxyEntity]: { dictionary: newRecords, list: Object.values(newRecords), }, }; stateRef.current.isUpdating = false; stateSubscriptions.forEach(s => s()); }, []); const subscribeToState = (0, react_1.useCallback)((callback) => { stateRef.current.stateSubscriptions = [ ...stateRef.current.stateSubscriptions, callback, ]; return () => { stateRef.current.stateSubscriptions = stateRef.current.stateSubscriptions.filter(s => s !== callback); }; }, []); const getStateSnapshot = (0, react_1.useCallback)(() => { return stateRef.current.state; }, []); (0, react_1.useEffect)(() => { updateRecordsInState(entity, records => { const key = sdk_1.Record.toKey(entityRecord); if (records[key] === entityRecord) { return records; } return { ...records, [key]: entityRecord, }; }); }, [entity, updateRecordsInState, entityRecord]); // // Mutations. // const updateRecords = (0, react_1.useCallback)((updates, proxyEntity = entity) => { if (stateRef.current.isUpdating) { throw new Error("State is already updating."); } stateRef.current.isUpdating = true; const { state, stateSubscriptions } = stateRef.current; const { mutations, mutationsSubscriptions } = stateRef.current; updates.forEach(r => { if ("noContent" in r || !r.version?.hash) { return; } stateRef.current.versions[r.version.hash] = r; }); const updateState = (updateEntity, updater, mutations, updates) => { const entityState = state[updateEntity] || { dictionary: {}, list: [] }; const reduced = updater(entityState.dictionary, mutations, updates); if (reduced.state !== entityState.dictionary) { stateRef.current.state = { ...state, [updateEntity]: { dictionary: reduced.state, list: Object.values(reduced.state), }, }; return [true, reduced.mutations]; } return [false, reduced.mutations]; }; const [wasUpdated, newMutations] = (() => { if (proxyEntity === entity) { return updateState(entity, storeMutation_js_1.applyUpdates, mutations, updates); } const [wasUpdated1, mutations1] = updateState(proxyEntity, storeMutation_js_1.applyProxyUpdates, mutations, updates.filter(u => u.source === sdk_1.RecordSource.PROXY)); const [wasUpdated2, mutations2] = updateState(entity, storeMutation_js_1.applyUpdates, mutations1, updates.filter(u => u.source !== sdk_1.RecordSource.PROXY)); return [wasUpdated1 || wasUpdated2, mutations2]; })(); stateRef.current.isUpdating = false; if (wasUpdated) { stateSubscriptions.forEach(s => s()); } if (newMutations !== mutations) { stateRef.current.mutations = newMutations; mutationsSubscriptions.forEach(s => s()); } }, [entity]); const subscribeToMutations = (0, react_1.useCallback)((callback) => { stateRef.current.mutationsSubscriptions = [ ...stateRef.current.mutationsSubscriptions, callback, ]; return () => { stateRef.current.mutationsSubscriptions = stateRef.current.mutationsSubscriptions.filter(s => s !== callback); }; }, []); (0, react_1.useEffect)(() => { const abortController = new AbortController(); const { signal } = abortController; let isRunning = false; const performMutation = async (mutation) => { // Perform the request. const client = findClient(entity); const result = await (0, storeMutation_js_1.performMutationRequest)(RKnownRecord, RKnownEventRecord, entity, client, mutation, signal); // Re-apply following mutations. const { mutations } = stateRef.current; const m = mutations.slice(1); const updates = m.flatMap(m => [m.record, ...m.followingUpdates]); const reduced = (0, storeMutation_js_1.applyUpdates)(result.state, result.mutations, updates); // Update the state. if (stateRef.current.isUpdating) { throw new Error("State is already updating."); } updates.map(r => { if ("noContent" in r || !r.version?.hash) { return; } stateRef.current.versions[r.version.hash] = r; }); const { state, stateSubscriptions } = stateRef.current; const entityState = state[entity] || { dictionary: {}, list: [] }; if (reduced.state !== entityState.dictionary) { stateRef.current.state = { ...state, [entity]: { dictionary: reduced.state, list: Object.values(reduced.state), }, }; stateSubscriptions.forEach(s => s()); } return reduced.mutations; }; const performNextMutation = async () => { while (stateRef.current.mutations.length > 0) { const activeMutation = stateRef.current.mutations[0]; try { const updatedMutations = await performMutation(activeMutation); stateRef.current.mutations = updatedMutations; } catch (error) { // Aborted: nothing to do. if (error instanceof sdk_1.AbortedError) { return; } // Unauthorized or Forbidden: disconnect. if (sdk_1.Http.isError(error, [ sdk_1.HttpStatusCode.UNAUTHORIZED, sdk_1.HttpStatusCode.FORBIDDEN, ])) { onDisconnectRequestStable(); return; } // Other http error: retry. if (sdk_1.Http.isError(error)) { await sdk_1.Async.delay(1000, signal); // TODO: Evaluate exponential backoff. continue; } // Other error, unexpected. throw error; } } stop(); }; const start = () => { if (isRunning) { return; } isRunning = true; performNextMutation(); }; const stop = () => { isRunning = false; }; const unsubscribe = subscribeToMutations(() => { start(); }); start(); return () => { abortController.abort(); unsubscribe(); stop(); }; }, [subscribeToMutations, entity, findClient, onDisconnectRequestStable]); // // Queries. // const updateQueries = (0, react_1.useCallback)((updater) => { const { queries, isUpdating, queriesSubscriptions } = stateRef.current; if (isUpdating) { throw new Error("State is already updating."); } stateRef.current.isUpdating = true; const newQueries = updater(queries); if (newQueries === queries) { stateRef.current.isUpdating = false; return; } stateRef.current.queries = newQueries; stateRef.current.isUpdating = false; queriesSubscriptions.forEach(s => s()); }, []); const subscribeToQueries = (0, react_1.useCallback)((callback) => { stateRef.current.queriesSubscriptions = [ ...stateRef.current.queriesSubscriptions, callback, ]; return () => { stateRef.current.queriesSubscriptions = stateRef.current.queriesSubscriptions.filter(s => s !== callback); }; }, []); const getQueriesSnapshot = (0, react_1.useCallback)(() => { return stateRef.current.queries; }, []); const updateLiveQueries = (0, react_1.useCallback)((updater) => { if (stateRef.current.isUpdating) { throw new Error("State is already updating."); } const { liveQueries, liveQueriesSubscriptions } = stateRef.current; stateRef.current.isUpdating = true; const newLiveQueries = updater(liveQueries); if (newLiveQueries === liveQueries) { stateRef.current.isUpdating = false; return; } stateRef.current.liveQueries = newLiveQueries; stateRef.current.isUpdating = false; liveQueriesSubscriptions.forEach(s => s()); }, []); const subscribeToLiveQueries = (0, react_1.useCallback)((callback) => { stateRef.current.liveQueriesSubscriptions = [ ...stateRef.current.liveQueriesSubscriptions, callback, ]; return () => { stateRef.current.liveQueriesSubscriptions = stateRef.current.liveQueriesSubscriptions.filter(s => s !== callback); }; }, []); const getLiveQueriesSnapshot = (0, react_1.useCallback)(() => { return stateRef.current.liveQueries; }, []); const registerQuery = (0, react_1.useCallback)((query, options) => { const { isFetch, isSync, isLocalTracked } = options; const { refreshSpec, loadMorePageSize } = options; const { queries, lastQueryId, liveQueries } = stateRef.current; const equal = (() => { for (let i = lastQueryId; i > 0; i--) { const q = queries[i]; if (!q || q.isDisplayed || !(0, isEqual_js_1.default)(q.refreshSpec, refreshSpec)) { continue; } if (sdk_1.Query.isMatch(q.query, query)) { return q; } } return undefined; })(); if (equal) { return equal; } const match = (() => { if (isLocalTracked) { return undefined; } for (let i = lastQueryId; i > 0; i--) { const q = queries[i]; if (!q) { continue; } // If this is a complete query, it only needs to be a superset. if (q.isComplete && sdk_1.Query.isSuperset(q.query, query)) { return q; } // Otherwise, it needs to be a match. if (sdk_1.Query.isMatch(q.query, query)) { return q; } } return undefined; })(); const syncMatch = match && findLiveQueryMatch(liveQueries, match); const queryId = ++stateRef.current.lastQueryId; const makeLoadMore = (loadMoreQuery) => { // Cannot load more in full refresh mode. if (refreshSpec?.mode === "full") { return undefined; } return () => { updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } if (currentQuery.loadMorePromise) { return value; } const loadMorePromise = performLoadMoreQuery(loadMoreQuery); return { ...value, [queryId]: { ...currentQuery, loadMorePromise, }, }; }); }; }; const performQuery = async () => { const queryState1 = stateRef.current.queries[queryId]; if (queryState1?.loadMorePromise) { await queryState1.loadMorePromise; } let keepGoing = true; while (keepGoing) { keepGoing = false; try { const client = findClient(query.proxyTo || entity); const response = await client.getRecords(RKnownRecord, RKnownRecord, query); if (!isMountedRef.current) { return; } const responseRecords = [ ...response.linkedRecords, ...response.records, ]; updateRecords(responseRecords, query.proxyTo || entity); updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } const loadMore = response.nextPage ? makeLoadMore(response.nextPage) : undefined; // Loaded boundary. const last = sdk_1.Array.last(response.records); const loadedBoundary = response.nextPage && last ? sdk_1.Query.findBoundary(query, last) : undefined; // Refresh boundary. const max = (0, orderBy_js_1.default)(response.records, t => t.version.receivedAt, "desc")[0]; const refreshBoundary = max && sdk_1.Query.findBoundary(query, max); return { ...value, [queryId]: { ...currentQuery, promise: undefined, error: undefined, refreshCount: currentQuery.refreshCount + 1, refreshBoundary, loadMorePromise: undefined, loadMoreError: undefined, loadMoreQuery: response.nextPage, loadMore, isComplete: !response.nextPage, loadedBoundary, recordVersions: response.records.map(sdk_1.Record.toVersionHash), }, }; }); } catch (error) { // Unmounted: nothing to do. if (!isMountedRef.current) { return; } // Permanent error: mark as failed. if (sdk_1.Http.isError(error, [ sdk_1.HttpStatusCode.BAD_REQUEST, sdk_1.HttpStatusCode.NOT_FOUND, sdk_1.HttpStatusCode.INTERNAL_SERVER_ERROR, ])) { updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } return { ...value, [queryId]: { ...currentQuery, promise: undefined, error: error, }, }; }); return; } // Transient error: retry. await sdk_1.Async.delay(2000); keepGoing = true; } } }; const performRefreshSyncQuery = async () => { const queryState1 = stateRef.current.queries[queryId]; if (queryState1.loadMorePromise) { await queryState1.loadMorePromise; } // // Find the upper boundary we currently have. // const queryState2 = stateRef.current.queries[queryId]; if (!queryState2.refreshBoundary) { return performQuery(); } const refreshQuery = sdk_1.Query.toSync(query, queryState2.refreshBoundary); // // Perform the refresh query. // let keepGoing = true; while (keepGoing) { keepGoing = false; try { const client = findClient(query.proxyTo || entity); const response = await client.getRecords(RKnownRecord, RKnownRecord, refreshQuery); if (!isMountedRef.current) { return; } // If there are too many items to sync, full refresh. if (response.nextPage) { return performQuery(); } const responseRecords = [ ...response.linkedRecords, ...response.records, ]; updateRecords(responseRecords, query.proxyTo || entity); updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } const queryRecords = (currentQuery.recordVersions || []).map(version => stateRef.current.versions[version]); const recordVersions = sdk_1.Query.filter(currentQuery.query, (0, uniqBy_js_1.default)(response.records.concat(queryRecords), r => r.id), { ignorePageSize: true, boundary: currentQuery.loadedBoundary }).map(sdk_1.Record.toVersionHash); const max = (0, orderBy_js_1.default)(response.records, t => t.version.receivedAt, "desc")[0]; const refreshBoundary = (max && [max.version.receivedAt, max.id]) || currentQuery.refreshBoundary; return { ...value, [queryId]: { ...currentQuery, promise: undefined, error: undefined, refreshCount: currentQuery.refreshCount + 1, refreshBoundary, recordVersions, }, }; }); } catch (error) { // Unmounted: nothing to do. if (!isMountedRef.current) { return; } // Permanent error: mark as failed. if (sdk_1.Http.isError(error, [ sdk_1.HttpStatusCode.BAD_REQUEST, sdk_1.HttpStatusCode.NOT_FOUND, sdk_1.HttpStatusCode.INTERNAL_SERVER_ERROR, ])) { updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } return { ...value, [queryId]: { ...currentQuery, promise: undefined, error: error, }, }; }); return; } // Transient error: retry. await sdk_1.Async.delay(2000); keepGoing = true; } } }; const performLoadMoreQuery = async (loadMoreQuery) => { const queryState1 = stateRef.current.queries[queryId]; if (queryState1.promise) { await queryState1.promise; } await null; const queryState2 = stateRef.current.queries[queryId]; if (!queryState2.loadMorePromise) { return; } let keepGoing = true; while (keepGoing) { keepGoing = false; try { const client = findClient(query.proxyTo || entity); const patchedLoadMoreQuery = loadMorePageSize ? sdk_1.Str.buildQuery([ ["page_size", loadMorePageSize.toString()], ...sdk_1.Str.parseQuery(loadMoreQuery).filter(([key]) => key !== "page_size"), ]) : loadMoreQuery; const response = await client.getMoreRecords(RKnownRecord, RKnownRecord, patchedLoadMoreQuery); if (!isMountedRef.current) { return; } const responseRecords = [ ...response.linkedRecords, ...response.records, ]; updateRecords(responseRecords, query.proxyTo || entity); updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } const loadMore = response.nextPage ? makeLoadMore(response.nextPage) : undefined; const recordVersions = (0, uniq_js_1.default)((currentQuery.recordVersions || []).concat(response.records.map(sdk_1.Record.toVersionHash))); const last = sdk_1.Array.last(response.records); const boundary = last && sdk_1.Query.findBoundary(query, last); return { ...value, [queryId]: { ...currentQuery, loadMorePromise: undefined, loadMoreError: undefined, loadMoreQuery: response.nextPage, loadMore, isComplete: !response.nextPage, loadedBoundary: response.nextPage ? boundary : undefined, recordVersions, }, }; }); } catch (error) { // Unmounted: nothing to do. if (!isMountedRef.current) { return; } // Permanent error: mark as failed. if (sdk_1.Http.isError(error, [ sdk_1.HttpStatusCode.BAD_REQUEST, sdk_1.HttpStatusCode.NOT_FOUND, sdk_1.HttpStatusCode.INTERNAL_SERVER_ERROR, ])) { updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } return { ...value, [queryId]: { ...currentQuery, loadMorePromise: undefined, loadMoreError: error, }, }; }); return; } // Transient error: retry. await sdk_1.Async.delay(2000); keepGoing = true; } } }; const refresh = (refreshCount) => { updateQueries(value => { const currentQuery = value[queryId]; if (!currentQuery) { throw new Error("Query not found."); } if (!refreshSpec || currentQuery.promise || !currentQuery.recordVersions || refreshCount !== currentQuery.refreshCount) { return value; } const promise = refreshSpec.mode === "full" ? performQuery() : performRefreshSyncQuery(); return { ...value, [queryId]: { ...currentQuery, promise, }, }; }); }; const newQuery = { id: queryId, query, promise: syncMatch || !isFetch ? undefined : performQuery(), refreshSpec, refresh, refreshCount: 0, refreshBoundary: undefined, loadMorePromise: undefined, loadMoreError: undefined, loadMoreQuery: match?.loadMoreQuery, loadMore: match?.loadMoreQuery ? makeLoadMore(match.loadMoreQuery) : undefined, isSync, isComplete: match?.isComplete || isLocalTracked, isDisplayed: false, error: undefined, loadedBoundary: match?.loadedBoundary, recordVersions: match?.recordVersions, }; updateQueries(value => ({ ...value, [queryId]: newQuery, })); return newQuery; }, [updateQueries, entity, findClient, isMountedRef, updateRecords]); const registerLiveQuery = (0, react_1.useCallback)((query) => { if (!query.isSync) { return undefined; } if (findLiveQueryMatch(stateRef.current.liveQueries, query)) { return undefined; } updateLiveQueries(value => (0, uniqBy_js_1.default)([query, ...value], q => q.id)); return () => { updateLiveQueries(value => value.filter(q => q !== query)); }; }, [updateLiveQueries]); // // Helpers. // const uploadBlob = (0, react_1.useCallback)(async (blob, signal) => { const client = findClient(entity); const blobResponse = await client.uploadBlob(blob, signal); // TODO: Memory management. // TODO: React Native compatibility. const blobUrl = URL.createObjectURL(blob); stateRef.current.blobUrls.set(blobResponse.hash, blobUrl); return blobResponse; }, [findClient, entity]); const buildBlobUrl = (0, react_1.useCallback)((record, blob, expiresInSeconds) => { const blobUrl = stateRef.current.blobUrls.get(blob.hash); if (blobUrl) { return blobUrl; } return blobUrlBuilder(record, blob, expiresInSeconds); }, [blobUrlBuilder]); // // Context. // const { versions } = stateRef.current; const context = (0, react_1.useMemo)(() => { const result = { isAuthenticated: identity.isAuthenticated, entity: identity.entityRecord.author.entity, findClient: identity.findClient, discover: identity.discover, downloadBlob: identity.downloadBlob, versions, updateRecords, uploadBlob, buildBlobUrl, onDisconnectRequest: onDisconnectRequestStable, subscribeToState, getStateSnapshot, subscribeToQueries, getQueriesSnapshot, subscribeToLiveQueries, getLiveQueriesSnapshot, registerQuery, registerLiveQuery, }; return result; }, [ identity, versions, updateRecords, uploadBlob, buildBlobUrl, onDisconnectRequestStable, subscribeToState, getStateSnapshot, subscribeToQueries, getQueriesSnapshot, subscribeToLiveQueries, getLiveQueriesSnapshot, registerQuery, registerLiveQuery, ]); return ((0, jsx_runtime_1.jsx)(StoreContext.Provider, { value: context, children: (0, jsx_runtime_1.jsx)(ProxyStore, { entity: entity, children: children }) })); }; function useReferenceStateSelector(selector) { const { subscribeToState, getStateSnapshot } = useStoreContext(); return (0, with_selector_js_1.useSyncExternalStoreWithSelector)(subscribeToState, getStateSnapshot, null, selector, equality_js_1.isReferenceEqual); } function useShallowStateSelector(selector) { const { subscribeToState, getStateSnapshot } = useStoreContext(); return (0, with_selector_js_1.useSyncExternalStoreWithSelector)(subscribeToState, getStateSnapshot, null, selector, equality_js_1.isShallowEqual); } function useQuery(queryId) { const selector = (0, react_1.useCallback)((queries) => { return queries[queryId]; }, [queryId]); const { subscribeToQueries, getQueriesSnapshot } = useStoreContext(); return (0, with_selector_js_1.useSyncExternalStoreWithSelector)(subscribeToQueries, getQueriesSnapshot, null, selector, equality_js_1.isReferenceEqual); } function useShouldSync(query) { const selector = (0, react_1.useCallback)((liveQueries) => { if (!query.isSync) { return false; } return !findLiveQueryMatch(liveQueries, query); }, [query]); const { subscribeToLiveQueries, getLiveQueriesSnapshot } = useStoreContext(); return (0, with_selector_js_1.useSyncExternalStoreWithSelector)(subscribeToLiveQueries, getLiveQueriesSnapshot, null, selector, equality_js_1.isReferenceEqual); } // // Queries. // function useRecordsQuery(requestedQuery, options = {}) { const { loadMorePageSize } = options; const mode = options.mode || "fetch"; const storeContext = useStoreContext(); const { isAuthenticated, entity, findClient, updateRecords } = storeContext; const { registerQuery, registerLiveQuery } = storeContext; if (!isAuthenticated) { throw new Error("useRecordsQuery() requires an authenticated Store."); } // // State. // const isFetch = mode === "fetch"; const isTracked = mode !== "local"; const isLocalTracked = mode === "local-tracked"; const isSync = mode !== "local" && mode !== "local-tracked"; const initialStoreQuery = (0, hooks_js_1.useDeepMemo)(() => { if (isTracked) { return registerQuery(requestedQuery, { isFetch, isSync, isLocalTracked, refreshSpec: undefined, loadMorePageSize, }); } return { id: -1, query: requestedQuery, promise: undefined, error: undefined, refreshSpec: undefined, refreshCount: 0, refreshBoundary: undefined, refresh: () => { }, loadMorePromise: undefined, loadMoreError: undefined, loadMoreQuery: undefined, loadMore: undefined, isSync: false, isComplete: isLocalTracked, isDisplayed: true, loadedBoundary: undefined, recordVersions: undefined, }; }, [ isFetch, isTracked, isSync, isLocalTracked, loadMorePageSize, requestedQuery, ]); const trackedQuery = useQuery(initialStoreQuery.id); const storeQuery = trackedQuery || initialStoreQuery; const { query, promise, error, loadedBoundary } = storeQuery; const { loadMorePromise, loadMoreError, loadMore } = storeQuery; (0, react_1.useEffect)(() => { storeQuery.isDisplayed = true; }, [storeQuery]); // // Result. // const recordsSelector = (0, react_1.useCallback)((state) => { if (promise) { return []; } return sdk_1.Query.filter(query, state[entity]?.list || [], { ignorePageSize: true, boundary: loadedBoundary, }); }, [promise, entity, query, loadedBoundary]); const records = useShallowStateSelector(recordsSelector); const lastQueryRef = (0, react_1.useRef)(undefined); (0, react_1.useEffect)(() => { if (storeQuery.promise) { return; } lastQueryRef.current = storeQuery; }, [storeQuery]); const lastStoreQuery = lastQueryRef.current; const deferredStoreQuery = (promise && lastStoreQuery) || storeQuery; const deferredQuery = deferredStoreQuery.query; const deferredRecordsSelector = (0, react_1.useCallback)((state) => { if (query === deferredQuery) { return records; } return sdk_1.Query.filter(deferredQuery, state[entity]?.list || [], { ignorePageSize: true, boundary: loadedBoundary, }); }, [entity, query, deferredQuery, loadedBoundary, records]); const deferredRecords = useShallowStateSelector(deferredRecordsSelector); // // Suspense getters. // const getRecords = (0, react_1.useCallback)(() => { if (promise) { throw promise; } return records; }, [promise, records]); const getDeferredRecords = (0, react_1.useCallback)(() => { if (!lastStoreQuery) { return getRecords(); } return deferredRecords; }, [lastStoreQuery, getRecords, deferredRecords]); // // Sync. // const shouldSync = useShouldSync(storeQuery); const getMaxRecordDateRef = (0, react_1.useRef)(undefined); (0, react_1.useEffect)(() => { getMaxRecordDateRef.current = () => { if (!isFetch || !isSync || promise || !shouldSync) { return undefined; } return (0, orderBy_js_1.default)(getRecords(), t => t.version?.receivedAt, "desc") .map(t => t.version?.receivedAt) .filter(sdk_1.isDefined)[0]; }; }, [isFetch, isSync, promise, shouldSync, getRecords, query]); (0, react_1.useEffect)(() => { const currentGetMaxRecordDate = getMaxRecordDateRef.current; if (promise || !currentGetMaxRecordDate) { return undefined; } const unregister = registerLiveQuery(storeQuery); if (!unregister) { return undefined; } const abort = new AbortController(); const maxRecordDate = currentGetMaxRecordDate(); const onRecord = (record) => { console.log("Received record:", record); updateRecords([record]); }; const sseQuery = { ...storeQuery.query, min: maxRecordDate, includeDeleted: true, }; const client = findClient(entity); client.recordEventSource(RKnownEventRecord, onRecord, sseQuery, abort.signal); return () => { abort.abort(); unregister(); }; }, [ promise, registerLiveQuery, storeQuery, entity, findClient, updateRecords, ]); return { isLoading: Boolean(promise), error, isLoadingMore: Boolean(loadMorePromise), loadMoreError, loadMore, records, deferredRecords, getRecords, getDeferredRecords, query, deferredQuery, }; } function useStaticRecordsQuery(requestedQuery, options = {}) { const storeContext = useStoreContext(); const { isAuthenticated, versions } = storeContext; const { registerQuery } = storeContext; if (!isAuthenticated && !requestedQuery.proxyTo) { throw new Error("useStaticRecordsQuery() requires an authenticated Store for non-proxied queries."); } // // State. // const initialStoreQuery = (0, hooks_js_1.useDeepMemo)(() => { return registerQuery(requestedQuery, { isFetch: true, isSync: false, isLocalTracked: false, refreshSpec: (0, storeQuery_js_1.staticRecordQueryOptionsToRefreshSpec)(options), loadMorePageSize: options.loadMorePageSize, }); }, [requestedQuery, options]); const trackedQuery = useQuery(initialStoreQuery.id); const storeQuery = trackedQuery || initialStoreQuery; const { query, promise, error, recordVersions } = storeQuery; const { refreshSpec, refreshCount, refresh } = storeQuery; const { loadMorePromise, loadMoreError, loadMore } = storeQuery; (0, react_1.useEffect)(() => { storeQuery.isDisplayed = true; }, [storeQuery]); const records = (0, react_1.useMemo)(() => { if (!recordVersions) { return []; } return recordVersions.map(v => versions[v]); }, [recordVersions, versions]); const lastQueryRef = (0, react_1.useRef)(undefined); (0, react_1.useEffect)(() => { if (storeQuery.promise) { return; } lastQueryRef.current = storeQuery; }, [storeQuery]); const lastStoreQuery = lastQueryRef.current; const deferredStoreQuery = (promise && lastStoreQuery) || storeQuery; const deferredQuery = deferredStoreQuery.query; const deferredRecordVersions = deferredStoreQuery.recordVersions; const isSameDeferred = deferredQuery === query; const deferredRecords = (0, react_1.useMemo)(() => { if (isSameDeferred) { return records; } if (!deferredRecordVersions) { return []; } return deferredRecordVersions.map(v => versions[v]); }, [isSameDeferred, deferredRecordVersions, records, versions]); // // Suspense getters. // const getRecords = (0, react_1.useCallback)(() => { if (promise && !recordVersions) { throw promise; } return records; }, [promise, recordVersions, records]); const getDeferredRecords = (0, react_1.useCallback)(() => { if (!lastStoreQuery) { return getRecords(); } return deferredRecords; }, [lastStoreQuery, getRecords, deferredRecords]); // // Refresh. // (0, react_1.useEffect)(() => { if (!refreshSpec || refreshCount === 0) { return; } return (0, hooks_js_1.abortable)(async (abort) => { await sdk_1.Async.delay(refreshSpec.interval, abort); refresh(refreshCount); }); }, [refreshSpec, refreshCount, refresh]); // // Context. // return { isLoading: Boolean(promise) && refreshCount === 0, isRefreshing: Boolean(promise) && refreshCount > 0, error, isLoadingMore: Boolean(loadMorePromise), loadMoreError, loadMore, hasResults: Boolean(recordVersions), records, deferredRecords, getRecords, getDeferredRecords, query, deferredQuery, }; } function useRecordQueryBase(queryResult) { const { isLoading, records, deferredRecords } = queryResult; const { query, deferredQuery, getRecords, getDeferredRecords } = queryResult; const localRecord = useFindRecordByQuery(query); const isInitialQuery = query === deferredQuery; const record = (0, react_1.useMemo)(() => { if (isLoading) { return localRecord; } const firstRecord = records[0]; if (!firstRecord || records.length > 1) { return undefined; } return firstRecord; }, [isLoading, localRecord, records]); const deferredRecord = (0, react_1.useMemo)(() => { if (!isLoading || isInitialQuery) { return record; } const firstRecord = deferredRecords[0]; if (!firstRecord || deferredRecords.length > 1) { return undefined; } return firstRecord; }, [isLoading, isInitialQuery, record, deferredRecords]); // // Suspense getters. // const getRecord = (0, react_1.useMemo)(() => (0, memoize_js_1.default)(() => { try { const records = getRecords(); const firstRecord = records[0]; if (!firstRecord || records.length > 1) { return undefined; } return firstRecord; } catch (err) {