@tanstack/react-db
Version:
React integration for @tanstack/db
344 lines (341 loc) • 14.2 kB
JavaScript
"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