@tanstack/react-db
Version:
React integration for @tanstack/db
323 lines (320 loc) • 13.3 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 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