UNPKG

@tanstack/react-db

Version:

React integration for @tanstack/db

344 lines (341 loc) • 14.2 kB
"use client"; import { useRef, useSyncExternalStore } from "react"; import { identifyLiveQuery, prepareLiveQueryValue, isCollection, getLiveQueryKey, getStableValueHash, UnhashableQueryIRError, deepEquals, resolveLiveQueryValue, releaseDeferredOnReclaim, createLiveQueryObserver, rejectLiveQueryInput, createLiveQuerySourceIdClaims, getSourceObjectToken, resumeDeferredLiveQueryCollections } from "@tanstack/db"; import { useOptionalDbClient } from "./DbProvider.js"; import { setLiveQueryResultInfo } from "./live-query-internals.js"; import { shouldWarnInDevelopment } from "./development.js"; import { getPreparedSources, getUnboundDescriptorReporter } from "./source-id-bindings.js"; const DEFAULT_GC_TIME_MS = 1; const DEFAULT_SUSPENSE_GC_TIME_MS = 5e3; const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16; const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10; const DERIVED_IDENTITY_TOTAL_WARN_MS = 50; const warnedDerivedIdentityCallsites = /* @__PURE__ */ new Set(); const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`); const unscopedSuspenseCollections = /* @__PURE__ */ new Map(); const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap(); function getSourceQualifiedSuspenseKey(preparedValue, queryHash, client) { const sourceIds = getPreparedSources( preparedValue, client, `useLiveSuspenseQuery` ).map(getSourceObjectToken); if (sourceIds.length === 0) return queryHash; return `${sourceIds.join(`,`)}:${queryHash}`; } function getSuspenseCollections(client) { if (!client) return unscopedSuspenseCollections; let collections = suspenseCollectionsByClient.get(client); if (!collections) { collections = /* @__PURE__ */ new Map(); suspenseCollectionsByClient.set(client, collections); } return collections; } function releaseSuspenseCollection(collections, queryHash, collection) { const entry = collections.get(queryHash); if (entry?.collection !== collection) return; collections.delete(queryHash); entry.removeStatusListeners(); } function getCurrentTime() { return typeof performance !== `undefined` && typeof performance.now === `function` ? performance.now() : Date.now(); } function getWarningCallsite(stackIndex) { const stack = new Error().stack ?? `unknown`; const lines = stack.split(` `); const userFrame = lines.slice(1).find( (line) => !line.includes(`useLiveQuery.ts`) && !line.includes(`useLiveSuspenseQuery.ts`) && !line.includes(`useLiveInfiniteQuery.ts`) ); return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack; } function warnDerivedIdentityHotPath(profiler, durationMs) { if (profiler.warned || !(shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) && process.env.NODE_ENV !== `production`)) { return; } const isSlowSingleRender = durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS; const isHotRenderPath = profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD && profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS; if (!isSlowSingleRender && !isHotRenderPath) { return; } const callsite = getWarningCallsite(5); if (warnedDerivedIdentityCallsites.has(callsite)) { profiler.warned = true; return; } warnedDerivedIdentityCallsites.add(callsite); profiler.warned = true; const reason = isSlowSingleRender ? `one render took ${durationMs.toFixed(1)}ms` : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`; console.warn( `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).` ); } function getExplicitDbClient(value) { return value && typeof value === `object` && `client` in value && value.client !== void 0 ? value.client : void 0; } function prepareQueryValue(value, dbClient, deferredCollections) { return prepareLiveQueryValue(value, dbClient, deferredCollections); } function getIdentityScope(claims, dbClient, hookName) { return { claims, client: dbClient, bindingName: hookName, onUnboundDescriptor: getUnboundDescriptorReporter(dbClient, hookName) }; } function prepareDerivedQuery(value, profiler, scope, deferredCollections) { const shouldProfile = shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) && process.env.NODE_ENV !== `production`; const start = shouldProfile ? getCurrentTime() : 0; const preparedValue = prepareQueryValue( value, scope.client, deferredCollections ); try { return { value: preparedValue, identityDeps: [`derived`, identifyLiveQuery(preparedValue, scope)] }; } finally { if (shouldProfile) { const durationMs = getCurrentTime() - start; profiler.renderCount += 1; profiler.totalMs += durationMs; profiler.maxMs = Math.max(profiler.maxMs, durationMs); warnDerivedIdentityHotPath(profiler, durationMs); } } } function useLiveQuery(configOrQueryOrCollection) { return useLiveQueryImpl(configOrQueryOrCollection); } function useLiveQueryForSuspense(configOrQueryOrCollection) { return useLiveQueryImpl(configOrQueryOrCollection, true); } function createHookInstance(dbClient) { return { collection: null, identityDeps: null, config: null, client: dbClient, derivedIdentityProfiler: { renderCount: 0, totalMs: 0, maxMs: 0, warned: false }, deferredCollections: /* @__PURE__ */ new Set(), sourceIds: createLiveQuerySourceIdClaims(), cancelReclaimRelease: void 0, observer: null, queryHash: void 0, suspenseKey: void 0, identityError: void 0, subscribe: null }; } function useLiveQueryImpl(configOrQueryOrCollection, forSuspense = false) { const contextDbClient = useOptionalDbClient(); const inputIsCollection = isCollection(configOrQueryOrCollection); const dbClient = inputIsCollection ? contextDbClient : getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient; const instanceRef = useRef(null); const instance = instanceRef.current ??= createHookInstance(dbClient); const resumeDeferredCollections = () => { instance.cancelReclaimRelease?.(); instance.cancelReclaimRelease = void 0; resumeDeferredLiveQueryCollections(instance.deferredCollections); }; try { const queryKey = !inputIsCollection ? getLiveQueryKey(configOrQueryOrCollection) : void 0; let preparedQueryValue = unpreparedQueryValue; let identityDeps; let streamIdentity = void 0; let identityError; if (queryKey) { identityDeps = queryKey; streamIdentity = [`queryKey`, queryKey]; } else if (inputIsCollection) { identityDeps = []; streamIdentity = [`collection`, configOrQueryOrCollection.id]; } else { const preparation = prepareDerivedQuery( configOrQueryOrCollection, instance.derivedIdentityProfiler, getIdentityScope(instance.sourceIds, dbClient, `useLiveQuery`), instance.deferredCollections ); preparedQueryValue = preparation.value; identityDeps = preparation.identityDeps; streamIdentity = preparation.identityDeps; } let queryHash; if (streamIdentity !== void 0) { try { queryHash = getStableValueHash(streamIdentity, `queryKey`); } catch (error) { if (error instanceof UnhashableQueryIRError) { if (queryKey !== void 0) throw error; identityError = error; } else { throw error; } } } const canReuseSuspenseKey = forSuspense && !inputIsCollection && queryHash !== void 0 && instance.collection !== null && instance.client === dbClient && instance.queryHash === queryHash && instance.suspenseKey !== void 0; if (forSuspense && !inputIsCollection && queryHash && !canReuseSuspenseKey && preparedQueryValue === unpreparedQueryValue) { preparedQueryValue = prepareQueryValue( configOrQueryOrCollection, dbClient, instance.deferredCollections ); } const suspenseKey = queryHash && forSuspense && !inputIsCollection ? canReuseSuspenseKey ? instance.suspenseKey : getSourceQualifiedSuspenseKey( preparedQueryValue, queryHash, dbClient ) : queryHash; const suspenseCollections = forSuspense && !inputIsCollection && suspenseKey ? getSuspenseCollections(dbClient) : void 0; const suspenseEntry = suspenseKey ? suspenseCollections?.get(suspenseKey) : void 0; const suspenseCollection = suspenseEntry?.collection; const identityChanged = instance.identityDeps === null || !deepEquals(instance.identityDeps, identityDeps); const needsNewCollection = !instance.collection || inputIsCollection && instance.config !== configOrQueryOrCollection || !inputIsCollection && (instance.client !== dbClient || identityChanged); if (needsNewCollection) { if (inputIsCollection) { const syncMode = configOrQueryOrCollection.config?.syncMode; if (syncMode === `on-demand` && shouldWarnInDevelopment( `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS` ) && process.env.NODE_ENV !== `production`) { console.warn( `[useLiveQuery] Warning: Passing a collection with syncMode "on-demand" directly to useLiveQuery will not load any data. In on-demand mode, data is only loaded when queries with predicates request it. Instead, use a query builder function: const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) }) Or switch to syncMode "eager" if you want all data to sync automatically.` ); } configOrQueryOrCollection.startSyncImmediate(); instance.collection = configOrQueryOrCollection; instance.config = configOrQueryOrCollection; } else { if (preparedQueryValue === unpreparedQueryValue) { preparedQueryValue = prepareQueryValue( configOrQueryOrCollection, dbClient, instance.deferredCollections ); } if (suspenseCollection) { instance.collection = suspenseCollection; } else { if (!dbClient && queryKey !== void 0) { getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`); } instance.collection = resolveLiveQueryValue(preparedQueryValue, { gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS, // Hydration and Suspense key the live-query Collection by identity, // so they never pool, even when the config passes a pool. pool: forSuspense || dbClient ? false : void 0, // Without a DbClient, Suspense starts it through a preload below. startSync: !forSuspense || dbClient !== void 0 }); if (instance.collection && !isCollection(preparedQueryValue)) { instance.cancelReclaimRelease?.(); instance.cancelReclaimRelease = releaseDeferredOnReclaim( instance.collection, instance.deferredCollections ); } if (suspenseCollections && suspenseKey && instance.collection) { const collection = instance.collection; const removeCleanupListener = collection.on( `status:cleaned-up`, () => releaseSuspenseCollection( suspenseCollections, suspenseKey, collection ) ); const retireAfterRetry = () => setTimeout( () => releaseSuspenseCollection( suspenseCollections, suspenseKey, collection ), 0 ); const removeErrorListener = collection.on( `status:error`, retireAfterRetry ); const removeCrashListener = collection.on( `status:crashed`, retireAfterRetry ); const entry = { collection, removeStatusListeners: () => { removeCleanupListener(); removeErrorListener(); removeCrashListener(); } }; suspenseCollections.set(suspenseKey, entry); } } instance.config = configOrQueryOrCollection; instance.identityDeps = [...identityDeps]; } instance.client = dbClient; instance.queryHash = queryHash; instance.suspenseKey = suspenseKey; instance.identityError = identityError; } if (needsNewCollection) { instance.observer = createLiveQueryObserver(instance.collection, { mode: `wholesale`, client: dbClient, queryHash: instance.queryHash, onPreload: resumeDeferredCollections, suspendUntilReady: forSuspense }); } const observer = instance.observer; if (forSuspense && !dbClient && (instance.collection?.status === `idle` || instance.collection?.status === `cleaned-up`)) { observer.preload().catch(() => { }); } if (!instance.subscribe || needsNewCollection) { instance.subscribe = (onStoreChange) => { const unsubscribe = observer.subscribe(onStoreChange); resumeDeferredCollections(); return unsubscribe; }; } const returned = useSyncExternalStore( instance.subscribe, () => observer.getSnapshot(), () => observer.getServerSnapshot(forSuspense) ); if (forSuspense) { setLiveQueryResultInfo(returned, { client: dbClient, queryHash: instance.queryHash, identityError: instance.identityError, observer, deferredCollections: instance.deferredCollections }); } return returned; } catch (error) { return rejectLiveQueryInput(instance.deferredCollections, error); } } export { getIdentityScope, prepareDerivedQuery, prepareQueryValue, useLiveQuery, useLiveQueryForSuspense }; //# sourceMappingURL=useLiveQuery.js.map