@tanstack/react-db
Version:
React integration for @tanstack/db
309 lines (306 loc) • 11.9 kB
JavaScript
"use client";
;
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