@baqhub/sdk-react
Version:
The official React SDK for the BAQ federated app platform.
1,127 lines (1,126 loc) • 55 kB
JavaScript
"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) {