UNPKG

@tanstack/react-db

Version:

React integration for @tanstack/db

309 lines (306 loc) 11.9 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 liveQueryInternals = require("./live-query-internals.cjs"); const DEFAULT_GC_TIME_MS = 1; const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16; const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10; const DERIVED_IDENTITY_TOTAL_WARN_MS = 50; const warnedDepsCallsites = /* @__PURE__ */ new Set(); const warnedDerivedIdentityCallsites = /* @__PURE__ */ new Set(); const warnedUnhashableIdentityCallsites = /* @__PURE__ */ new Set(); const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`); function warnDeprecatedDepsArray(hookName = `useLiveQuery`) { if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) { return; } const callsite = getWarningCallsite(4); if (warnedDepsCallsites.has(callsite)) { return; } warnedDepsCallsites.add(callsite); const replacement = hookName === `useLiveQuery` ? `useLiveQuery({ query })` : `useLiveInfiniteQuery(query, { queryKey })`; console.warn( `[${hookName}] The dependency-array form is deprecated and will be removed in 1.0. Use ${replacement} instead. Provide queryKey only for functional/opaque queries or to avoid deriving identity from structured query IR on render.` ); } function shouldWarnInDevelopment(disableEnvVar) { if (typeof process === `undefined`) { return false; } return process.env.NODE_ENV !== `production` && process.env[disableEnvVar] !== `1`; } function getCurrentTime() { return typeof performance !== `undefined` && typeof performance.now === `function` ? performance.now() : Date.now(); } function getWarningCallsite(stackIndex) { const stack = new Error().stack ?? `unknown`; return stack.split(` `)[stackIndex]?.trim() ?? stack; } function warnDerivedIdentityHotPath(profiler, durationMs) { if (profiler.warned || !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) { 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 getExplicitQueryKey(value) { return value && typeof value === `object` && Array.isArray(value.queryKey) ? value.queryKey : void 0; } 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 db.prepareLiveQueryValue(value, dbClient, deferredCollections); } function prepareDerivedQuery(value, dbClient, profiler, deferredCollections) { const shouldProfile = shouldWarnInDevelopment( `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS` ); const start = shouldProfile ? getCurrentTime() : 0; const preparedValue = prepareQueryValue(value, dbClient, deferredCollections); try { const identity = db.getPreparedLiveQueryIdentity(preparedValue); return { status: `hashable`, value: preparedValue, identityDeps: [`derived`, identity] }; } catch (error) { if (error instanceof db.UnhashableQueryIRError) { return { status: `unhashable`, value: preparedValue, error }; } throw error; } finally { if (shouldProfile) { const durationMs = getCurrentTime() - start; profiler.renderCount += 1; profiler.totalMs += durationMs; profiler.maxMs = Math.max(profiler.maxMs, durationMs); warnDerivedIdentityHotPath(profiler, durationMs); } } } function warnUnhashableDerivedIdentity(error) { if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) { return; } const callsite = getWarningCallsite(4); if (warnedUnhashableIdentityCallsites.has(callsite)) { return; } warnedUnhashableIdentityCallsites.add(callsite); console.warn( `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. Unhashable queries without queryKey will throw in 1.0.` ); } function createCollectionFromPreparedQuery(value) { if (value === void 0 || value === null) { return null; } if (db.isCollection(value)) { value.startSyncImmediate(); return value; } if (value instanceof db.BaseQueryBuilder) { return db.createLiveQueryCollection({ query: value, startSync: true, gcTime: DEFAULT_GC_TIME_MS }); } if (typeof value === `object`) { return db.createLiveQueryCollection({ startSync: true, gcTime: DEFAULT_GC_TIME_MS, ...value }); } throw new Error( `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof value}` ); } function useLiveQuery(configOrQueryOrCollection, deps) { const contextDbClient = DbProvider.useOptionalDbClient(); const inputIsCollection = db.isCollection(configOrQueryOrCollection); const dbClient = inputIsCollection ? contextDbClient : getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient; const resolvedDeps = deps ?? []; const collectionRef = react.useRef( null ); const depsRef = react.useRef(null); const configRef = react.useRef(null); const clientRef = react.useRef(dbClient); const legacyUnhashableIdentityRef = react.useRef([ `legacy-unhashable` ]); const derivedIdentityProfilerRef = react.useRef({ renderCount: 0, totalMs: 0, maxMs: 0, warned: false }); const deferredCollectionsRef = react.useRef( /* @__PURE__ */ new Set() ); const observerRef = react.useRef( null ); const queryHashRef = react.useRef(void 0); const identityErrorRef = react.useRef(void 0); const queryKey = !inputIsCollection ? getExplicitQueryKey(configOrQueryOrCollection) : void 0; let preparedQueryValue = unpreparedQueryValue; let identityDeps; let streamIdentity = void 0; let identityError; if (queryKey) { identityDeps = queryKey; streamIdentity = [`queryKey`, queryKey]; } else if (deps !== void 0) { identityDeps = resolvedDeps; try { preparedQueryValue = prepareQueryValue( configOrQueryOrCollection, dbClient, deferredCollectionsRef.current ); streamIdentity = [ `deps`, resolvedDeps, db.getPreparedLiveQueryIdentity(preparedQueryValue) ]; } catch (error) { if (!(error instanceof db.UnhashableQueryIRError)) throw error; warnUnhashableDerivedIdentity(error); identityError = error; } } else if (inputIsCollection) { identityDeps = []; streamIdentity = [`collection`, configOrQueryOrCollection.id]; } else { const preparation = prepareDerivedQuery( configOrQueryOrCollection, dbClient, derivedIdentityProfilerRef.current, deferredCollectionsRef.current ); preparedQueryValue = preparation.value; if (preparation.status === `hashable`) { identityDeps = preparation.identityDeps; streamIdentity = preparation.identityDeps; } else { warnUnhashableDerivedIdentity(preparation.error); identityDeps = legacyUnhashableIdentityRef.current; identityError = preparation.error; } } let queryHash; if (streamIdentity !== void 0) { try { queryHash = db.getStableValueHash(streamIdentity, `queryKey`); } catch (error) { if (error instanceof db.UnhashableQueryIRError) { if (queryKey !== void 0) throw error; identityError = error; } else { throw error; } } } if (deps !== void 0) { warnDeprecatedDepsArray(); } const identityChanged = depsRef.current === null || (deps !== void 0 ? depsRef.current.length !== identityDeps.length || depsRef.current.some((dep, index) => dep !== identityDeps[index]) : !db.deepEquals(depsRef.current, identityDeps)); const needsNewCollection = !collectionRef.current || inputIsCollection && configRef.current !== configOrQueryOrCollection || !inputIsCollection && (clientRef.current !== dbClient || identityChanged); const resumeDeferredCollections = () => { for (const collection of deferredCollectionsRef.current) { collection._resumeSyncStart(); } deferredCollectionsRef.current.clear(); }; if (needsNewCollection) { if (inputIsCollection) { const syncMode = configOrQueryOrCollection.config?.syncMode; if (syncMode === `on-demand` && shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) { 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(); collectionRef.current = configOrQueryOrCollection; configRef.current = configOrQueryOrCollection; } else { if (preparedQueryValue === unpreparedQueryValue) { preparedQueryValue = prepareQueryValue( configOrQueryOrCollection, dbClient, deferredCollectionsRef.current ); } collectionRef.current = createCollectionFromPreparedQuery( preparedQueryValue ); configRef.current = configOrQueryOrCollection; depsRef.current = [...identityDeps]; } clientRef.current = dbClient; queryHashRef.current = queryHash; identityErrorRef.current = identityError; } if (needsNewCollection) { observerRef.current = db.createLiveQueryObserver(collectionRef.current, { mode: `wholesale`, client: dbClient, queryHash: queryHashRef.current, onPreload: resumeDeferredCollections }); } const observer = observerRef.current; const subscribeRef = react.useRef(null); if (!subscribeRef.current || needsNewCollection) { subscribeRef.current = (onStoreChange) => { const unsubscribe = observer.subscribe(() => onStoreChange()); resumeDeferredCollections(); return unsubscribe; }; } const returned = react.useSyncExternalStore( subscribeRef.current, () => observer.getSnapshot(), () => observer.getServerSnapshot() ); liveQueryInternals.setLiveQueryResultInfo(returned, { client: dbClient, queryHash: queryHashRef.current, identityError: identityErrorRef.current, observer }); return returned; } exports.prepareDerivedQuery = prepareDerivedQuery; exports.prepareQueryValue = prepareQueryValue; exports.useLiveQuery = useLiveQuery; exports.warnDeprecatedDepsArray = warnDeprecatedDepsArray; exports.warnUnhashableDerivedIdentity = warnUnhashableDerivedIdentity; //# sourceMappingURL=useLiveQuery.cjs.map