@tanstack/react-db
Version:
React integration for @tanstack/db
273 lines (270 loc) • 11.4 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`);
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 useLiveQueryImpl(configOrQueryOrCollection, suspense) {
const forSuspense = suspense !== void 0;
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;
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 || !db.deepEquals(instance.identityDeps, identityDeps));
if (needsNewCollection && !inputIsCollection && preparedQueryValue === unpreparedQueryValue) {
preparedQueryValue = db.prepareLiveQueryValue(
configOrQueryOrCollection,
dbClient,
instance.deferredCollections
);
}
let queryHash = instance.queryHash;
let commitWindow;
let compile;
if (needsNewCollection) {
queryHash = queryKey ? db.getLiveQueryHash(preparedQueryValue, queryKey) : db.getStableValueHash(
inputIsCollection ? [`collection`, configOrQueryOrCollection.id] : identityDeps,
`queryKey`
);
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 {
compile = (ownsCollection, windowOwner) => {
if (!dbClient && queryKey !== void 0) {
sourceIdBindings.getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`);
}
const collection = db.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 && !db.isCollection(preparedQueryValue)) {
instance.cancelReclaimRelease?.();
instance.cancelReclaimRelease = db.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 = db.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 = react.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 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.useLiveQueryImpl = useLiveQueryImpl;
//# sourceMappingURL=useLiveQuery.cjs.map