UNPKG

@tanstack/react-db

Version:

React integration for @tanstack/db

323 lines (320 loc) • 13.3 kB
"use client"; "use strict"; Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); const react = require("react"); const db = require("@tanstack/db"); const DbProvider = require("./DbProvider.cjs"); const development = require("./development.cjs"); const sourceIdBindings = require("./source-id-bindings.cjs"); const useWindowCommitEffect = typeof window === `undefined` ? react.useEffect : react.useLayoutEffect; 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 unscopedSuspenseScope = {}; const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap(); function getSourceQualifiedSuspenseKey(preparedValue, queryHash, client) { const sourceIds = sourceIdBindings.getPreparedSources( preparedValue, client, `useLiveSuspenseQuery` ).map(db.getSourceObjectToken); const { query: _query, pool: _pool, ...options } = preparedValue?.query ? preparedValue : {}; options.autoIndex ??= `off`; for (const key in options) { const value = options[key]; if (value === void 0) delete options[key]; else if (value && (typeof value === `object` || typeof value === `function`)) options[key] = db.getSourceObjectToken(value); } return db.getStableValueHash([sourceIds, options, queryHash]); } function getSuspenseCollections(client) { const scope = client ?? unscopedSuspenseScope; let collections = suspenseCollectionsByClient.get(scope); if (!collections) { collections = /* @__PURE__ */ new Map(); suspenseCollectionsByClient.set(scope, collections); } return collections; } function createDerivedIdentityProfiler() { try { if (process.env.NODE_ENV !== `production`) { return { renderCount: 0, totalMs: 0, maxMs: 0, warned: false }; } } catch { } return void 0; } 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 || !(development.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 getIdentityScope(claims, dbClient, hookName) { return { claims, client: dbClient, bindingName: hookName, onUnboundDescriptor: sourceIdBindings.getUnboundDescriptorReporter(dbClient, hookName) }; } function prepareDerivedQuery(value, profiler, scope, deferredCollections) { const shouldProfile = profiler !== void 0 && /* @__PURE__ */ development.shouldWarnInDevelopment( `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS` ); const start = shouldProfile && process.env.NODE_ENV !== `production` ? getCurrentTime() : 0; const preparedValue = db.prepareLiveQueryValue( value, scope.client, deferredCollections ); try { return { value: preparedValue, identityDeps: [`derived`, db.identifyLiveQuery(preparedValue, scope)] }; } finally { if (shouldProfile && process.env.NODE_ENV !== `production`) { 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 useLiveQueryImpl(configOrQueryOrCollection, forSuspense = false) { const contextDbClient = DbProvider.useOptionalDbClient(); const inputIsCollection = db.isCollection(configOrQueryOrCollection); const dbClient = (inputIsCollection || typeof configOrQueryOrCollection !== `object` ? void 0 : configOrQueryOrCollection?.client) ?? contextDbClient; const instanceRef = react.useRef(null); const instance = instanceRef.current ??= { collection: null, identityDeps: null, derivedIdentityProfiler: createDerivedIdentityProfiler(), deferredCollections: /* @__PURE__ */ new Set(), sourceIds: db.createLiveQuerySourceIdClaims() }; const resumeDeferredCollections = () => { instance.cancelReclaimRelease?.(); instance.cancelReclaimRelease = void 0; db.resumeDeferredLiveQueryCollections(instance.deferredCollections); }; try { const queryKey = !inputIsCollection ? db.getLiveQueryKey(configOrQueryOrCollection) : void 0; let preparedQueryValue = unpreparedQueryValue; let identityDeps; let streamIdentity; 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; try { queryHash = db.getStableValueHash(streamIdentity, `queryKey`); } catch (error) { if (!(error instanceof db.UnhashableQueryIRError) || queryKey !== void 0) throw error; identityError = error; } let needsNewCollection = !instance.collection || (inputIsCollection ? instance.config !== configOrQueryOrCollection : instance.client !== dbClient || !db.deepEquals(instance.identityDeps, identityDeps)); if (needsNewCollection && !inputIsCollection && preparedQueryValue === unpreparedQueryValue) { preparedQueryValue = db.prepareLiveQueryValue( configOrQueryOrCollection, dbClient, instance.deferredCollections ); } let commitWindow; if (needsNewCollection) { if (inputIsCollection) { if (/* @__PURE__ */ development.shouldWarnInDevelopment( `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS` ) && process.env.NODE_ENV !== `production` && configOrQueryOrCollection.config?.syncMode === `on-demand`) { 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; } else { const suspenseKey = forSuspense && queryHash ? getSourceQualifiedSuspenseKey( preparedQueryValue, queryHash, dbClient ) : void 0; const suspenseCollections = suspenseKey ? getSuspenseCollections(dbClient) : void 0; const suspenseCollection = suspenseCollections?.get( suspenseKey )?.collection; if (suspenseCollection) { instance.collection = suspenseCollection; } else { if (!dbClient && queryKey !== void 0) { sourceIdBindings.getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`); } instance.collection = db.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, previousCollection: forSuspense || dbClient ? void 0 : instance.collection, windowOwner: instance, onWindowChange: (move) => { commitWindow = move; }, // Without a DbClient, Suspense starts it through a preload below. startSync: !forSuspense || dbClient !== void 0 }); if (instance.collection && !db.isCollection(preparedQueryValue)) { instance.cancelReclaimRelease?.(); instance.cancelReclaimRelease = db.releaseDeferredOnReclaim( instance.collection, instance.deferredCollections ); } if (suspenseCollections && instance.collection) { const collection = instance.collection; const releaseCollection = () => { const entry = suspenseCollections.get(suspenseKey); if (entry?.collection !== collection) return; suspenseCollections.delete(suspenseKey); entry.removeStatusListeners.forEach((remove) => remove()); }; const retireAfterRetry = () => setTimeout(releaseCollection, 0); suspenseCollections.set(suspenseKey, { collection, removeStatusListeners: [ collection.on(`status:cleaned-up`, releaseCollection), collection.on(`status:error`, retireAfterRetry), collection.on(`status:crashed`, retireAfterRetry) ] }); } } if (!commitWindow) instance.identityDeps = [...identityDeps]; } instance.config = configOrQueryOrCollection; instance.client = dbClient; instance.queryHash = queryHash; instance.identityError = identityError; } if (commitWindow) needsNewCollection = false; useWindowCommitEffect(() => { if (commitWindow) { instance.identityDeps = [...identityDeps]; commitWindow(); } }); if (needsNewCollection) { const observer2 = instance.observer = db.createLiveQueryObserver( instance.collection, { mode: `wholesale`, client: dbClient, queryHash: instance.queryHash, onPreload: resumeDeferredCollections, suspendUntilReady: forSuspense } ); instance.subscribe = (onStoreChange) => { const unsubscribe = observer2.subscribe(onStoreChange); resumeDeferredCollections(); return unsubscribe; }; } const observer = instance.observer; if (forSuspense && !dbClient && (instance.collection?.status === `idle` || instance.collection?.status === `cleaned-up`)) { observer.preload().catch(() => { }); } const returned = react.useSyncExternalStore( instance.subscribe, () => observer.getSnapshot(), () => observer.getServerSnapshot(forSuspense) ); if (forSuspense) { return { result: returned, client: dbClient, queryHash: instance.queryHash, identityError: instance.identityError, observer, deferredCollections: instance.deferredCollections }; } return returned; } catch (error) { return db.rejectLiveQueryInput(instance.deferredCollections, error); } } Object.defineProperty(exports, "prepareQueryValue", { enumerable: true, get: () => db.prepareLiveQueryValue }); exports.createDerivedIdentityProfiler = createDerivedIdentityProfiler; exports.getIdentityScope = getIdentityScope; exports.prepareDerivedQuery = prepareDerivedQuery; exports.useLiveQuery = useLiveQuery; exports.useLiveQueryForSuspense = useLiveQueryForSuspense; //# sourceMappingURL=useLiveQuery.cjs.map