@tanstack/react-db
Version:
React integration for @tanstack/db
271 lines (268 loc) • 11.4 kB
JavaScript
"use client";
import { useRef, useEffect, useLayoutEffect, useSyncExternalStore } from "react";
import { prepareLiveQueryValue, identifyLiveQuery, isCollection, createLiveQuerySourceIdClaims, getLiveQueryKey, deepEquals, getLiveQueryHash, getStableValueHash, resolveLiveQueryValue, releaseDeferredOnReclaim, createLiveQueryObserver, rejectLiveQueryInput, resumeDeferredLiveQueryCollections } from "@tanstack/db";
import { prepareLiveQueryValue as prepareLiveQueryValue2 } from "@tanstack/db";
import { useOptionalDbClient } from "./DbProvider.js";
import { shouldWarnInDevelopment } from "./development.js";
import { getPreparedSources, getUnboundDescriptorReporter } from "./source-id-bindings.js";
const useWindowCommitEffect = typeof window === `undefined` ? useEffect : 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`);
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 || !(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: getUnboundDescriptorReporter(dbClient, hookName)
};
}
function prepareDerivedQuery(value, profiler, scope, deferredCollections) {
const shouldProfile = profiler !== void 0 && /* @__PURE__ */ shouldWarnInDevelopment(
`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`
);
const start = shouldProfile && process.env.NODE_ENV !== `production` ? getCurrentTime() : 0;
const preparedValue = prepareLiveQueryValue(
value,
scope.client,
deferredCollections
);
try {
return {
value: preparedValue,
identityDeps: [`derived`, 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 useLiveQueryImpl(configOrQueryOrCollection, suspense) {
const forSuspense = suspense !== void 0;
const contextDbClient = useOptionalDbClient();
const inputIsCollection = isCollection(configOrQueryOrCollection);
const dbClient = (inputIsCollection || typeof configOrQueryOrCollection !== `object` ? void 0 : configOrQueryOrCollection?.client) ?? contextDbClient;
const instanceRef = useRef(null);
const instance = instanceRef.current ??= {
collection: null,
identityDeps: null,
derivedIdentityProfiler: createDerivedIdentityProfiler(),
deferredCollections: /* @__PURE__ */ new Set(),
sourceIds: createLiveQuerySourceIdClaims()
};
const resumeDeferredCollections = () => {
instance.cancelReclaimRelease?.();
instance.cancelReclaimRelease = void 0;
resumeDeferredLiveQueryCollections(instance.deferredCollections);
};
try {
const queryKey = !inputIsCollection ? getLiveQueryKey(configOrQueryOrCollection) : void 0;
let preparedQueryValue = unpreparedQueryValue;
let identityDeps;
if (queryKey) {
identityDeps = queryKey;
} else if (inputIsCollection) {
identityDeps = [];
} else {
const preparation = prepareDerivedQuery(
configOrQueryOrCollection,
instance.derivedIdentityProfiler,
getIdentityScope(instance.sourceIds, dbClient, `useLiveQuery`),
instance.deferredCollections
);
preparedQueryValue = preparation.value;
identityDeps = preparation.identityDeps;
}
let needsNewCollection = suspense?.needsOwnCollection || !instance.collection || (inputIsCollection ? instance.config !== configOrQueryOrCollection : instance.client !== dbClient || !deepEquals(instance.identityDeps, identityDeps));
if (needsNewCollection && !inputIsCollection && preparedQueryValue === unpreparedQueryValue) {
preparedQueryValue = prepareLiveQueryValue(
configOrQueryOrCollection,
dbClient,
instance.deferredCollections
);
}
let queryHash = instance.queryHash;
let commitWindow;
let compile;
if (needsNewCollection) {
queryHash = queryKey ? getLiveQueryHash(preparedQueryValue, queryKey) : getStableValueHash(
inputIsCollection ? [`collection`, configOrQueryOrCollection.id] : identityDeps,
`queryKey`
);
if (inputIsCollection) {
if (/* @__PURE__ */ 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 {
compile = (ownsCollection, windowOwner) => {
if (!dbClient && queryKey !== void 0) {
getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`);
}
const collection = resolveLiveQueryValue(preparedQueryValue, {
gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS,
// Suspense and client observers have distinct preload/snapshot
// policies, so neither uses equality-partition pooling.
pool: forSuspense || dbClient ? false : void 0,
previousCollection: ownsCollection && instance.client === dbClient ? instance.collection : null,
windowOwner,
onWindowChange: (move) => {
commitWindow = move;
},
// Without a DbClient, Suspense starts it through a preload below.
startSync: !forSuspense || dbClient !== void 0
});
if (collection && !isCollection(preparedQueryValue)) {
instance.cancelReclaimRelease?.();
instance.cancelReclaimRelease = releaseDeferredOnReclaim(
collection,
instance.deferredCollections
);
}
return collection;
};
instance.collection = suspense ? suspense.resolve(preparedQueryValue, queryHash, dbClient, compile) : compile(true, instance);
if (!commitWindow) instance.identityDeps = [...identityDeps];
}
instance.config = configOrQueryOrCollection;
instance.client = dbClient;
if (!commitWindow) instance.queryHash = queryHash;
}
if (commitWindow && !dbClient) needsNewCollection = false;
let observer = instance.observer;
let subscribe = instance.subscribe;
const watchCollection = () => {
observer = createLiveQueryObserver(instance.collection, {
mode: `wholesale`,
client: dbClient,
queryHash,
onPreload: resumeDeferredCollections,
suspendUntilReady: forSuspense
});
subscribe = (onStoreChange) => {
const unsubscribe = observer.subscribe(onStoreChange);
resumeDeferredCollections();
return unsubscribe;
};
};
if (needsNewCollection) watchCollection();
useWindowCommitEffect(() => {
if (commitWindow) {
instance.identityDeps = [...identityDeps];
instance.queryHash = queryHash;
instance.observer = observer;
instance.subscribe = subscribe;
commitWindow();
}
claim?.();
});
if (forSuspense && !dbClient && (instance.collection?.status === `idle` || instance.collection?.status === `cleaned-up`)) {
observer.preload().catch(() => {
});
}
const privateCollection = compile && suspense?.forkReady(observer, compile);
if (privateCollection) {
instance.collection = privateCollection;
watchCollection();
}
if (!commitWindow) {
instance.observer = observer;
instance.subscribe = subscribe;
}
const returned = useSyncExternalStore(
subscribe,
() => {
const snapshot = observer.getSnapshot();
return suspense ? suspense.read(snapshot) : snapshot;
},
() => {
const snapshot = observer.getServerSnapshot(forSuspense);
return suspense ? suspense.read(snapshot) : snapshot;
}
);
const claim = returned.isReady ? suspense?.claim() : void 0;
if (forSuspense) {
return {
result: returned,
client: dbClient,
queryHash,
observer,
deferredCollections: instance.deferredCollections
};
}
return returned;
} catch (error) {
return rejectLiveQueryInput(instance.deferredCollections, error);
}
}
export {
createDerivedIdentityProfiler,
getIdentityScope,
prepareDerivedQuery,
prepareLiveQueryValue2 as prepareQueryValue,
useLiveQuery,
useLiveQueryImpl
};
//# sourceMappingURL=useLiveQuery.js.map