@tanstack/react-db 0.5.8 → 0.5.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -11,12 +11,15 @@ function getSourceObjectToken(source) {
11
11
  }
12
12
  return id;
13
13
  }
14
- function getPreparedSources(preparedValue) {
15
- if (db.isCollection(preparedValue)) return [preparedValue];
16
- const query = preparedValue instanceof db.BaseQueryBuilder ? preparedValue : preparedValue && typeof preparedValue === `object` && `query` in preparedValue && preparedValue.query instanceof db.BaseQueryBuilder ? preparedValue.query : void 0;
17
- return query ? db.IR.collectCollectionSources(query._getQuery()).map(
18
- ({ collection }) => collection
19
- ) : [];
14
+ function getPreparedSources(preparedValue, client, hookName) {
15
+ return db.getPreparedLiveQuerySources(
16
+ preparedValue,
17
+ !client && hookName ? (alias) => {
18
+ throw new Error(
19
+ `[${hookName}] Collection descriptor "${alias}" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`
20
+ );
21
+ } : void 0
22
+ );
20
23
  }
21
24
  function createSourceIdBindings() {
22
25
  return { unscoped: /* @__PURE__ */ new Map(), byClient: /* @__PURE__ */ new WeakMap() };
@@ -24,7 +27,7 @@ function createSourceIdBindings() {
24
27
  function claimSourceIds(bindings, preparedValue, client, hookName, release) {
25
28
  const prior = client ? bindings.byClient.get(client) : bindings.unscoped;
26
29
  const seen = /* @__PURE__ */ new Map();
27
- for (const source of getPreparedSources(preparedValue)) {
30
+ for (const source of getPreparedSources(preparedValue, client, hookName)) {
28
31
  const token = getSourceObjectToken(source);
29
32
  const previous = seen.get(source.id) ?? prior?.get(source.id);
30
33
  if (previous !== void 0 && previous !== token) {
@@ -39,22 +42,12 @@ function claimSourceIds(bindings, preparedValue, client, hookName, release) {
39
42
  for (const [id, token] of seen) claimed.set(id, token);
40
43
  if (client) bindings.byClient.set(client, claimed);
41
44
  }
42
- function resumeSyncStarts(collections) {
43
- const pending = Array.from(collections);
44
- collections.clear();
45
- const errors = [];
46
- for (const collection of pending) {
47
- try {
48
- collection._resumeSyncStart();
49
- } catch (error) {
50
- errors.push(error);
51
- }
52
- }
53
- if (errors.length) throw errors[0];
54
- }
45
+ Object.defineProperty(exports, "resumeSyncStarts", {
46
+ enumerable: true,
47
+ get: () => db.resumeDeferredLiveQueryCollections
48
+ });
55
49
  exports.claimSourceIds = claimSourceIds;
56
50
  exports.createSourceIdBindings = createSourceIdBindings;
57
51
  exports.getPreparedSources = getPreparedSources;
58
52
  exports.getSourceObjectToken = getSourceObjectToken;
59
- exports.resumeSyncStarts = resumeSyncStarts;
60
53
  //# sourceMappingURL=source-id-bindings.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"source-id-bindings.cjs","sources":["../../src/source-id-bindings.ts"],"sourcesContent":["import { BaseQueryBuilder, IR, isCollection } from '@tanstack/db'\nimport type { DbClient } from '@tanstack/db'\n\nconst sourceObjectTokens = new WeakMap<object, number>()\nlet nextSourceObjectToken = 0\n\nexport function getSourceObjectToken(source: object): number {\n let id = sourceObjectTokens.get(source)\n if (id === undefined) {\n id = ++nextSourceObjectToken\n sourceObjectTokens.set(source, id)\n }\n return id\n}\n\nexport function getPreparedSources(\n preparedValue: unknown,\n): Array<{ id: string }> {\n if (isCollection(preparedValue)) return [preparedValue]\n const query =\n preparedValue instanceof BaseQueryBuilder\n ? preparedValue\n : preparedValue &&\n typeof preparedValue === `object` &&\n `query` in preparedValue &&\n preparedValue.query instanceof BaseQueryBuilder\n ? preparedValue.query\n : undefined\n return query\n ? IR.collectCollectionSources(query._getQuery()).map(\n ({ collection }) => collection,\n )\n : []\n}\n\n/** The source object each Collection ID named while one hook is mounted. */\nexport type SourceIdBindings = {\n unscoped: Map<string, number>\n byClient: WeakMap<DbClient, Map<string, number>>\n}\n\nexport function createSourceIdBindings(): SourceIdBindings {\n return { unscoped: new Map(), byClient: new WeakMap() }\n}\n\n/**\n * Derived query identity names each source by its Collection ID. While a hook\n * is mounted, an ID must keep naming one source object in each client scope,\n * or the hook could reuse a live query that reads the old source. Claims the\n * prepared value's sources, or calls `release` and throws on a collision so a\n * rejected render keeps no sync-start deferral.\n */\nexport function claimSourceIds(\n bindings: SourceIdBindings,\n preparedValue: unknown,\n client: DbClient | undefined,\n hookName: string,\n release: () => void,\n): void {\n const prior = client ? bindings.byClient.get(client) : bindings.unscoped\n const seen = new Map<string, number>()\n for (const source of getPreparedSources(preparedValue)) {\n const token = getSourceObjectToken(source)\n const previous = seen.get(source.id) ?? prior?.get(source.id)\n if (previous !== undefined && previous !== token) {\n release()\n throw new Error(\n `[${hookName}] Source Collection \"${source.id}\" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,\n )\n }\n seen.set(source.id, token)\n }\n const claimed = prior ?? new Map<string, number>()\n for (const [id, token] of seen) claimed.set(id, token)\n if (client) bindings.byClient.set(client, claimed)\n}\n\n/**\n * Resumes every deferred sync start, then rethrows the first startup error, so\n * one failing source cannot keep a later shared source deferred.\n */\nexport function resumeSyncStarts(\n collections: Set<{ _resumeSyncStart: () => void }>,\n): void {\n const pending = Array.from(collections)\n collections.clear()\n const errors: Array<unknown> = []\n for (const collection of pending) {\n try {\n collection._resumeSyncStart()\n } catch (error) {\n errors.push(error)\n }\n }\n if (errors.length) throw errors[0]\n}\n"],"names":["isCollection","BaseQueryBuilder","IR"],"mappings":";;;AAGA,MAAM,yCAAyB,QAAA;AAC/B,IAAI,wBAAwB;AAErB,SAAS,qBAAqB,QAAwB;AAC3D,MAAI,KAAK,mBAAmB,IAAI,MAAM;AACtC,MAAI,OAAO,QAAW;AACpB,SAAK,EAAE;AACP,uBAAmB,IAAI,QAAQ,EAAE;AAAA,EACnC;AACA,SAAO;AACT;AAEO,SAAS,mBACd,eACuB;AACvB,MAAIA,gBAAa,aAAa,EAAG,QAAO,CAAC,aAAa;AACtD,QAAM,QACJ,yBAAyBC,GAAAA,mBACrB,gBACA,iBACE,OAAO,kBAAkB,YACzB,WAAW,iBACX,cAAc,iBAAiBA,sBAC/B,cAAc,QACd;AACR,SAAO,QACHC,GAAAA,GAAG,yBAAyB,MAAM,UAAA,CAAW,EAAE;AAAA,IAC7C,CAAC,EAAE,WAAA,MAAiB;AAAA,EAAA,IAEtB,CAAA;AACN;AAQO,SAAS,yBAA2C;AACzD,SAAO,EAAE,UAAU,oBAAI,IAAA,GAAO,UAAU,oBAAI,UAAQ;AACtD;AASO,SAAS,eACd,UACA,eACA,QACA,UACA,SACM;AACN,QAAM,QAAQ,SAAS,SAAS,SAAS,IAAI,MAAM,IAAI,SAAS;AAChE,QAAM,2BAAW,IAAA;AACjB,aAAW,UAAU,mBAAmB,aAAa,GAAG;AACtD,UAAM,QAAQ,qBAAqB,MAAM;AACzC,UAAM,WAAW,KAAK,IAAI,OAAO,EAAE,KAAK,OAAO,IAAI,OAAO,EAAE;AAC5D,QAAI,aAAa,UAAa,aAAa,OAAO;AAChD,cAAA;AACA,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,wBAAwB,OAAO,EAAE;AAAA,MAAA;AAAA,IAEjD;AACA,SAAK,IAAI,OAAO,IAAI,KAAK;AAAA,EAC3B;AACA,QAAM,UAAU,SAAS,oBAAI,IAAA;AAC7B,aAAW,CAAC,IAAI,KAAK,KAAK,KAAM,SAAQ,IAAI,IAAI,KAAK;AACrD,MAAI,OAAQ,UAAS,SAAS,IAAI,QAAQ,OAAO;AACnD;AAMO,SAAS,iBACd,aACM;AACN,QAAM,UAAU,MAAM,KAAK,WAAW;AACtC,cAAY,MAAA;AACZ,QAAM,SAAyB,CAAA;AAC/B,aAAW,cAAc,SAAS;AAChC,QAAI;AACF,iBAAW,iBAAA;AAAA,IACb,SAAS,OAAO;AACd,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AACA,MAAI,OAAO,OAAQ,OAAM,OAAO,CAAC;AACnC;;;;;;"}
1
+ {"version":3,"file":"source-id-bindings.cjs","sources":["../../src/source-id-bindings.ts"],"sourcesContent":["import { getPreparedLiveQuerySources } from '@tanstack/db'\nimport type { DbClient } from '@tanstack/db'\n\nexport { resumeDeferredLiveQueryCollections as resumeSyncStarts } from '@tanstack/db'\n\nconst sourceObjectTokens = new WeakMap<object, number>()\nlet nextSourceObjectToken = 0\n\nexport function getSourceObjectToken(source: object): number {\n let id = sourceObjectTokens.get(source)\n if (id === undefined) {\n id = ++nextSourceObjectToken\n sourceObjectTokens.set(source, id)\n }\n return id\n}\n\nexport function getPreparedSources(\n preparedValue: unknown,\n client?: DbClient,\n hookName?: string,\n): Array<{ id: string }> {\n return getPreparedLiveQuerySources(\n preparedValue,\n !client && hookName\n ? (alias) => {\n throw new Error(\n `[${hookName}] Collection descriptor \"${alias}\" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`,\n )\n }\n : undefined,\n )\n}\n\n/** The source object each Collection ID named while one hook is mounted. */\nexport type SourceIdBindings = {\n unscoped: Map<string, number>\n byClient: WeakMap<DbClient, Map<string, number>>\n}\n\nexport function createSourceIdBindings(): SourceIdBindings {\n return { unscoped: new Map(), byClient: new WeakMap() }\n}\n\n/**\n * Derived query identity names each source by its Collection ID. While a hook\n * is mounted, an ID must keep naming one source object in each client scope,\n * or the hook could reuse a live query that reads the old source. Claims the\n * prepared value's sources, or calls `release` and throws on a collision so a\n * rejected render keeps no sync-start deferral.\n */\nexport function claimSourceIds(\n bindings: SourceIdBindings,\n preparedValue: unknown,\n client: DbClient | undefined,\n hookName: string,\n release: () => void,\n): void {\n const prior = client ? bindings.byClient.get(client) : bindings.unscoped\n const seen = new Map<string, number>()\n for (const source of getPreparedSources(preparedValue, client, hookName)) {\n const token = getSourceObjectToken(source)\n const previous = seen.get(source.id) ?? prior?.get(source.id)\n if (previous !== undefined && previous !== token) {\n release()\n throw new Error(\n `[${hookName}] Source Collection \"${source.id}\" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,\n )\n }\n seen.set(source.id, token)\n }\n const claimed = prior ?? new Map<string, number>()\n for (const [id, token] of seen) claimed.set(id, token)\n if (client) bindings.byClient.set(client, claimed)\n}\n"],"names":["getPreparedLiveQuerySources"],"mappings":";;;AAKA,MAAM,yCAAyB,QAAA;AAC/B,IAAI,wBAAwB;AAErB,SAAS,qBAAqB,QAAwB;AAC3D,MAAI,KAAK,mBAAmB,IAAI,MAAM;AACtC,MAAI,OAAO,QAAW;AACpB,SAAK,EAAE;AACP,uBAAmB,IAAI,QAAQ,EAAE;AAAA,EACnC;AACA,SAAO;AACT;AAEO,SAAS,mBACd,eACA,QACA,UACuB;AACvB,SAAOA,GAAAA;AAAAA,IACL;AAAA,IACA,CAAC,UAAU,WACP,CAAC,UAAU;AACT,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,4BAA4B,KAAK;AAAA,MAAA;AAAA,IAEjD,IACA;AAAA,EAAA;AAER;AAQO,SAAS,yBAA2C;AACzD,SAAO,EAAE,UAAU,oBAAI,IAAA,GAAO,UAAU,oBAAI,UAAQ;AACtD;AASO,SAAS,eACd,UACA,eACA,QACA,UACA,SACM;AACN,QAAM,QAAQ,SAAS,SAAS,SAAS,IAAI,MAAM,IAAI,SAAS;AAChE,QAAM,2BAAW,IAAA;AACjB,aAAW,UAAU,mBAAmB,eAAe,QAAQ,QAAQ,GAAG;AACxE,UAAM,QAAQ,qBAAqB,MAAM;AACzC,UAAM,WAAW,KAAK,IAAI,OAAO,EAAE,KAAK,OAAO,IAAI,OAAO,EAAE;AAC5D,QAAI,aAAa,UAAa,aAAa,OAAO;AAChD,cAAA;AACA,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,wBAAwB,OAAO,EAAE;AAAA,MAAA;AAAA,IAEjD;AACA,SAAK,IAAI,OAAO,IAAI,KAAK;AAAA,EAC3B;AACA,QAAM,UAAU,SAAS,oBAAI,IAAA;AAC7B,aAAW,CAAC,IAAI,KAAK,KAAK,KAAM,SAAQ,IAAI,IAAI,KAAK;AACrD,MAAI,OAAQ,UAAS,SAAS,IAAI,QAAQ,OAAO;AACnD;;;;;;;;;"}
@@ -1,6 +1,7 @@
1
1
  import { DbClient } from '@tanstack/db';
2
+ export { resumeDeferredLiveQueryCollections as resumeSyncStarts } from '@tanstack/db';
2
3
  export declare function getSourceObjectToken(source: object): number;
3
- export declare function getPreparedSources(preparedValue: unknown): Array<{
4
+ export declare function getPreparedSources(preparedValue: unknown, client?: DbClient, hookName?: string): Array<{
4
5
  id: string;
5
6
  }>;
6
7
  /** The source object each Collection ID named while one hook is mounted. */
@@ -17,10 +18,3 @@ export declare function createSourceIdBindings(): SourceIdBindings;
17
18
  * rejected render keeps no sync-start deferral.
18
19
  */
19
20
  export declare function claimSourceIds(bindings: SourceIdBindings, preparedValue: unknown, client: DbClient | undefined, hookName: string, release: () => void): void;
20
- /**
21
- * Resumes every deferred sync start, then rethrows the first startup error, so
22
- * one failing source cannot keep a later shared source deferred.
23
- */
24
- export declare function resumeSyncStarts(collections: Set<{
25
- _resumeSyncStart: () => void;
26
- }>): void;
@@ -65,7 +65,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
65
65
  preparation.value,
66
66
  dbClient,
67
67
  `useLiveInfiniteQuery`,
68
- () => sourceIdBindings.resumeSyncStarts(deferredCollections)
68
+ () => db.resumeDeferredLiveQueryCollections(deferredCollections)
69
69
  );
70
70
  if (preparation.status === `hashable`) {
71
71
  identityDeps = preparation.identityDeps;
@@ -196,7 +196,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
196
196
  currentRenderState.warned = true;
197
197
  console.warn(currentRenderState.warning);
198
198
  }
199
- sourceIdBindings.resumeSyncStarts(currentRenderState.deferredCollections);
199
+ db.resumeDeferredLiveQueryCollections(currentRenderState.deferredCollections);
200
200
  return unsubscribe;
201
201
  },
202
202
  [controller, currentRenderState]
@@ -18,8 +18,12 @@ const warnedUnhashableIdentityCallsites = /* @__PURE__ */ new Set();
18
18
  const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
19
19
  const unscopedSuspenseCollections = /* @__PURE__ */ new Map();
20
20
  const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap();
21
- function getSourceQualifiedSuspenseKey(preparedValue, queryHash) {
22
- const sourceIds = sourceIdBindings.getPreparedSources(preparedValue).map(sourceIdBindings.getSourceObjectToken);
21
+ function getSourceQualifiedSuspenseKey(preparedValue, queryHash, client) {
22
+ const sourceIds = sourceIdBindings.getPreparedSources(
23
+ preparedValue,
24
+ client,
25
+ `useLiveSuspenseQuery`
26
+ ).map(sourceIdBindings.getSourceObjectToken);
23
27
  if (sourceIds.length === 0) return queryHash;
24
28
  return `${sourceIds.join(`,`)}:${queryHash}`;
25
29
  }
@@ -170,7 +174,7 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
170
174
  const resolvedDeps = deps ?? [];
171
175
  const instanceRef = react.useRef(null);
172
176
  const instance = instanceRef.current ??= createHookInstance(dbClient);
173
- const resumeDeferredCollections = () => sourceIdBindings.resumeSyncStarts(instance.deferredCollections);
177
+ const resumeDeferredCollections = () => db.resumeDeferredLiveQueryCollections(instance.deferredCollections);
174
178
  const queryKey = !inputIsCollection ? getExplicitQueryKey(configOrQueryOrCollection) : void 0;
175
179
  let preparedQueryValue = unpreparedQueryValue;
176
180
  let identityDeps;
@@ -251,7 +255,7 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
251
255
  instance.deferredCollections
252
256
  );
253
257
  }
254
- const suspenseKey = queryHash && forSuspense && !inputIsCollection ? canReuseSuspenseKey ? instance.suspenseKey : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash) : queryHash;
258
+ const suspenseKey = queryHash && forSuspense && !inputIsCollection ? canReuseSuspenseKey ? instance.suspenseKey : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient) : queryHash;
255
259
  const suspenseCollections = forSuspense && !inputIsCollection && suspenseKey ? getSuspenseCollections(dbClient) : void 0;
256
260
  const suspenseEntry = suspenseKey ? suspenseCollections?.get(suspenseKey) : void 0;
257
261
  const suspenseCollection = suspenseEntry?.collection;
@@ -284,6 +288,9 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
284
288
  instance.deferredCollections
285
289
  );
286
290
  }
291
+ if (!dbClient) {
292
+ sourceIdBindings.getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`);
293
+ }
287
294
  instance.collection = db.resolveLiveQueryValue(preparedQueryValue, {
288
295
  gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS,
289
296
  // Hydration and Suspense key the live-query Collection by identity.
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef, useSyncExternalStore } from 'react'\nimport {\n UnhashableQueryIRError,\n createLiveQueryObserver,\n deepEquals,\n getPreparedLiveQueryIdentity,\n getStableValueHash,\n isCollection,\n prepareLiveQueryValue,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { setLiveQueryResultInfo } from './live-query-internals'\nimport { shouldWarnInDevelopment } from './development'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n getPreparedSources,\n getSourceObjectToken,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport type {\n Collection,\n CollectionImpl,\n CollectionStatus,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n// Suspense renders can be abandoned before React subscribes. Keep their\n// collection briefly so a nearby retry can commit without starting over.\nconst DEFAULT_SUSPENSE_GC_TIME_MS = 5000\nconst DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16\nconst DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10\nconst DERIVED_IDENTITY_TOTAL_WARN_MS = 50\nconst warnedDepsCallsites = new Set<string>()\nconst warnedDerivedIdentityCallsites = new Set<string>()\nconst warnedUnhashableIdentityCallsites = new Set<string>()\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\ntype SuspenseCollection = Collection<object, string | number, {}>\ntype SuspenseCollectionEntry = {\n collection: SuspenseCollection\n removeStatusListeners: () => void\n}\n\nconst unscopedSuspenseCollections = new Map<string, SuspenseCollectionEntry>()\nconst suspenseCollectionsByClient = new WeakMap<\n DbClient,\n Map<string, SuspenseCollectionEntry>\n>()\nfunction getSourceQualifiedSuspenseKey(\n preparedValue: unknown,\n queryHash: string,\n): string {\n const sourceIds = getPreparedSources(preparedValue).map(getSourceObjectToken)\n if (sourceIds.length === 0) return queryHash\n return `${sourceIds.join(`,`)}:${queryHash}`\n}\n\nfunction getSuspenseCollections(\n client: DbClient | undefined,\n): Map<string, SuspenseCollectionEntry> {\n if (!client) return unscopedSuspenseCollections\n let collections = suspenseCollectionsByClient.get(client)\n if (!collections) {\n collections = new Map()\n suspenseCollectionsByClient.set(client, collections)\n }\n return collections\n}\n\nfunction releaseSuspenseCollection(\n collections: Map<string, SuspenseCollectionEntry>,\n queryHash: string,\n collection: SuspenseCollection,\n): void {\n const entry = collections.get(queryHash)\n if (entry?.collection !== collection) return\n collections.delete(queryHash)\n entry.removeStatusListeners()\n}\n\nexport type DerivedIdentityProfiler = {\n renderCount: number\n totalMs: number\n maxMs: number\n warned: boolean\n}\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\nexport type LiveQueryKey = ReadonlyArray<unknown>\ntype UseLiveQueryConfigOptions<TContext extends Context> = Omit<\n LiveQueryCollectionConfig<TContext>,\n `query`\n> & {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n}\n\ntype ConfiguredQueryBuilder<TContext extends Context> = Extract<\n LiveQueryCollectionConfig<TContext>[`query`],\n QueryBuilder<TContext>\n>\n\nexport type UseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> &\n Pick<LiveQueryCollectionConfig<TContext>, `query`>\n\nexport type ConditionalUseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> & {\n query:\n | ConfiguredQueryBuilder<TContext>\n | ((\n q: InitialQueryBuilder,\n ) => ConfiguredQueryBuilder<TContext> | undefined | null)\n }\n\nexport function warnDeprecatedDepsArray(\n hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedDepsCallsites.has(callsite)) {\n return\n }\n warnedDepsCallsites.add(callsite)\n const replacement =\n hookName === `useLiveQuery`\n ? `useLiveQuery({ query })`\n : `useLiveInfiniteQuery(query, { queryKey })`\n console.warn(\n `[${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.`,\n )\n}\n\nfunction getCurrentTime(): number {\n return typeof performance !== `undefined` &&\n typeof performance.now === `function`\n ? performance.now()\n : Date.now()\n}\n\nfunction getWarningCallsite(stackIndex: number): string {\n const stack = new Error().stack ?? `unknown`\n const lines = stack.split(`\\n`)\n const userFrame = lines\n .slice(1)\n .find(\n (line) =>\n !line.includes(`useLiveQuery.ts`) &&\n !line.includes(`useLiveSuspenseQuery.ts`) &&\n !line.includes(`useLiveInfiniteQuery.ts`),\n )\n return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack\n}\n\nfunction warnDerivedIdentityHotPath(\n profiler: DerivedIdentityProfiler,\n durationMs: number,\n): void {\n if (\n profiler.warned ||\n !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n return\n }\n\n const isSlowSingleRender =\n durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS\n const isHotRenderPath =\n profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&\n profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS\n\n if (!isSlowSingleRender && !isHotRenderPath) {\n return\n }\n\n const callsite = getWarningCallsite(5)\n if (warnedDerivedIdentityCallsites.has(callsite)) {\n profiler.warned = true\n return\n }\n\n warnedDerivedIdentityCallsites.add(callsite)\n profiler.warned = true\n\n const reason = isSlowSingleRender\n ? `one render took ${durationMs.toFixed(1)}ms`\n : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`\n\n console.warn(\n `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +\n `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,\n )\n}\n\nfunction getExplicitQueryKey(value: unknown): LiveQueryKey | undefined {\n return value &&\n typeof value === `object` &&\n Array.isArray((value as { queryKey?: unknown }).queryKey)\n ? (value as { queryKey: LiveQueryKey }).queryKey\n : undefined\n}\n\nfunction getExplicitDbClient(value: unknown): DbClient | undefined {\n return value &&\n typeof value === `object` &&\n `client` in value &&\n (value as { client?: unknown }).client !== undefined\n ? (value as { client: DbClient }).client\n : undefined\n}\n\nexport function prepareQueryValue(\n value: unknown,\n dbClient: DbClient | undefined,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): unknown {\n return prepareLiveQueryValue(value, dbClient, deferredCollections)\n}\n\ntype DerivedQueryPreparation =\n | {\n status: `hashable`\n value: unknown\n identityDeps: Array<unknown>\n }\n | {\n status: `unhashable`\n value: unknown\n error: UnhashableQueryIRError\n }\n\nexport function prepareDerivedQuery(\n value: unknown,\n dbClient: DbClient | undefined,\n profiler: DerivedIdentityProfiler,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): DerivedQueryPreparation {\n const shouldProfile = shouldWarnInDevelopment(\n `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,\n )\n const start = shouldProfile ? getCurrentTime() : 0\n const preparedValue = prepareQueryValue(value, dbClient, deferredCollections)\n\n try {\n const identity = getPreparedLiveQueryIdentity(preparedValue)\n return {\n status: `hashable`,\n value: preparedValue,\n identityDeps: [`derived`, identity],\n }\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n return { status: `unhashable`, value: preparedValue, error }\n }\n\n throw error\n } finally {\n if (shouldProfile) {\n const durationMs = getCurrentTime() - start\n profiler.renderCount += 1\n profiler.totalMs += durationMs\n profiler.maxMs = Math.max(profiler.maxMs, durationMs)\n warnDerivedIdentityHotPath(profiler, durationMs)\n }\n }\n}\n\nexport function warnUnhashableDerivedIdentity(\n error: UnhashableQueryIRError,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedUnhashableIdentityCallsites.has(callsite)) {\n return\n }\n warnedUnhashableIdentityCallsites.add(callsite)\n\n console.warn(\n `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` +\n `It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` +\n `Unhashable queries without queryKey will throw in 1.0.`,\n )\n}\n\n/**\n * Create a live query using a query function.\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Prefer config object syntax\n * const { data, isLoading } = useLiveQuery({\n * query: (q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * })\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * })\n *\n * @example\n * // Structured captured values are included in derived query identity\n * const { data, state } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // Return undefined or null to disable a query\n * const { data, isEnabled } = useLiveQuery({\n * query: (q) => {\n * if (!userId) return undefined\n * return q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.userId, userId))\n * },\n * })\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery({\n * query: (q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * })\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todoCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({\n * query: queryBuilder,\n * })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 7: Accept config object with a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 8: Accept config object with legacy deps\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 9: Accept config object with legacy deps and a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n deps: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 10: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 10: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps?: Array<unknown>,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps)\n}\n\n/** @internal Shared implementation for the Suspense wrapper. */\nexport function useLiveQueryForSuspense(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps, true)\n}\n\n// What one hook instance keeps across renders. It lives in a single ref\n// slot, so a render neither looks up nor allocates more.\nfunction createHookInstance(dbClient: DbClient | undefined) {\n return {\n collection: null as Collection<object, string | number, {}> | null,\n deps: null as Array<unknown> | null,\n config: null as unknown,\n client: dbClient,\n legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,\n derivedIdentityProfiler: {\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n } as DerivedIdentityProfiler,\n deferredCollections: new Set<\n CollectionImpl<any, string | number, any, any, any>\n >(),\n sourceIds: createSourceIdBindings(),\n observer: null as LiveQueryObserver<object, string | number> | null,\n queryHash: undefined as string | undefined,\n suspenseKey: undefined as string | undefined,\n identityError: undefined as UnhashableQueryIRError | undefined,\n subscribe: null as ((onStoreChange: () => void) => () => void) | null,\n }\n}\n\nfunction useLiveQueryImpl(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n forSuspense = false,\n) {\n const contextDbClient = useOptionalDbClient()\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n const dbClient = inputIsCollection\n ? contextDbClient\n : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)\n const resolvedDeps = deps ?? []\n\n const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)\n const instance = (instanceRef.current ??= createHookInstance(dbClient))\n const resumeDeferredCollections = () =>\n resumeSyncStarts(instance.deferredCollections)\n\n const queryKey = !inputIsCollection\n ? getExplicitQueryKey(configOrQueryOrCollection)\n : undefined\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown>\n let streamIdentity: unknown = undefined\n let identityError: UnhashableQueryIRError | undefined\n\n if (queryKey) {\n identityDeps = queryKey\n streamIdentity = [`queryKey`, queryKey]\n } else if (deps !== undefined) {\n identityDeps = resolvedDeps\n // Deps decide reuse. Only hydration and Suspense read the query hash; the\n // development warning about unhashable queries still derives it.\n if (\n dbClient ||\n forSuspense ||\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n )\n try {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n streamIdentity = [\n `deps`,\n resolvedDeps,\n getPreparedLiveQueryIdentity(preparedQueryValue),\n ]\n } catch (error) {\n if (!(error instanceof UnhashableQueryIRError)) throw error\n warnUnhashableDerivedIdentity(error)\n identityError = error\n }\n } else if (inputIsCollection) {\n identityDeps = []\n streamIdentity = [`collection`, configOrQueryOrCollection.id]\n } else {\n const preparation = prepareDerivedQuery(\n configOrQueryOrCollection,\n dbClient,\n instance.derivedIdentityProfiler,\n instance.deferredCollections,\n )\n preparedQueryValue = preparation.value\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n streamIdentity = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = instance.legacyUnhashableIdentity\n identityError = preparation.error\n }\n }\n\n let queryHash: string | undefined\n if (streamIdentity !== undefined) {\n try {\n queryHash = getStableValueHash(streamIdentity, `queryKey`)\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n if (queryKey !== undefined) throw error\n identityError = error\n } else {\n throw error\n }\n }\n }\n\n if (deps !== undefined) {\n warnDeprecatedDepsArray()\n }\n\n if (\n queryKey === undefined &&\n deps === undefined &&\n preparedQueryValue !== unpreparedQueryValue\n ) {\n claimSourceIds(\n instance.sourceIds,\n preparedQueryValue,\n dbClient,\n `useLiveQuery`,\n resumeDeferredCollections,\n )\n }\n\n const canReuseSuspenseKey =\n forSuspense &&\n !inputIsCollection &&\n queryHash !== undefined &&\n instance.collection !== null &&\n instance.client === dbClient &&\n instance.queryHash === queryHash &&\n instance.suspenseKey !== undefined\n\n if (\n forSuspense &&\n !inputIsCollection &&\n queryHash &&\n !canReuseSuspenseKey &&\n preparedQueryValue === unpreparedQueryValue\n ) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n\n const suspenseKey =\n queryHash && forSuspense && !inputIsCollection\n ? canReuseSuspenseKey\n ? instance.suspenseKey\n : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash)\n : queryHash\n\n const suspenseCollections =\n forSuspense && !inputIsCollection && suspenseKey\n ? getSuspenseCollections(dbClient)\n : undefined\n const suspenseEntry = suspenseKey\n ? suspenseCollections?.get(suspenseKey)\n : undefined\n const suspenseCollection = suspenseEntry?.collection\n\n const identityChanged =\n instance.deps === null ||\n (deps !== undefined\n ? instance.deps.length !== identityDeps.length ||\n instance.deps.some((dep, index) => dep !== identityDeps[index])\n : !deepEquals(instance.deps, identityDeps))\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !instance.collection ||\n (inputIsCollection && instance.config !== configOrQueryOrCollection) ||\n (!inputIsCollection && (instance.client !== dbClient || identityChanged))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (\n syncMode === `on-demand` &&\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n instance.collection = configOrQueryOrCollection\n instance.config = configOrQueryOrCollection\n } else {\n if (suspenseCollection) {\n instance.collection = suspenseCollection\n } else {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n instance.collection = resolveLiveQueryValue(preparedQueryValue, {\n gcTime: forSuspense\n ? DEFAULT_SUSPENSE_GC_TIME_MS\n : DEFAULT_GC_TIME_MS,\n // Hydration and Suspense key the live-query Collection by identity.\n pool: !forSuspense && !dbClient,\n // Without a DbClient, Suspense starts it through a preload below.\n startSync: !forSuspense || dbClient !== undefined,\n }) as SuspenseCollection | null\n if (suspenseCollections && suspenseKey && instance.collection) {\n const collection = instance.collection\n const removeCleanupListener = collection.on(`status:cleaned-up`, () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n )\n const removeErrorListener = collection.on(`status:error`, () => {\n // Keep the failed collection through React's immediate retry so\n // the hook can throw its actual load error to an ErrorBoundary.\n // Retire it afterward so a later boundary reset starts fresh.\n setTimeout(\n () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n 0,\n )\n })\n const entry: SuspenseCollectionEntry = {\n collection,\n removeStatusListeners: () => {\n removeCleanupListener()\n removeErrorListener()\n },\n }\n suspenseCollections.set(suspenseKey, entry)\n }\n }\n instance.config = configOrQueryOrCollection\n instance.deps = [...identityDeps]\n }\n instance.client = dbClient\n instance.queryHash = queryHash\n instance.suspenseKey = suspenseKey\n instance.identityError = identityError\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n instance.observer = createLiveQueryObserver(instance.collection, {\n mode: `wholesale`,\n client: dbClient,\n queryHash: instance.queryHash,\n onPreload: resumeDeferredCollections,\n })\n }\n const observer = instance.observer!\n // A Suspense render asks for its data. Preloading starts the sync run, so a\n // source that already holds the rows makes it ready before the snapshot is\n // read instead of suspending once. useLiveSuspenseQuery surfaces failures.\n // A DbClient defers source starts past render and owns its streamed query.\n if (\n forSuspense &&\n !dbClient &&\n (instance.collection?.status === `idle` ||\n instance.collection?.status === `cleaned-up`)\n ) {\n observer.preloadForInitialRender().catch(() => {})\n }\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n if (!instance.subscribe || needsNewCollection) {\n instance.subscribe = (onStoreChange: () => void) => {\n const unsubscribe = observer.subscribe(onStoreChange)\n resumeDeferredCollections()\n return unsubscribe\n }\n }\n\n const returned = useSyncExternalStore(\n instance.subscribe,\n () => observer.getSnapshot(),\n () => observer.getServerSnapshot(),\n )\n // Only useLiveSuspenseQuery reads this, and it costs a define per render.\n if (forSuspense) {\n setLiveQueryResultInfo(returned, {\n client: dbClient,\n queryHash: instance.queryHash,\n identityError: instance.identityError,\n observer,\n })\n }\n return returned as any\n}\n"],"names":["shouldWarnInDevelopment","claimSourceIds","useSyncExternalStore","setLiveQueryResultInfo"],"mappings":";;;;;;;;;AAwCA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AAIA;AAIE;AACA;AACA;AACF;AAEA;AAGE;AACA;AACA;AACE;AACA;AAAmD;AAErD;AACF;AAEA;AAKE;AACA;AACA;AACA;AACF;AA2CO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AACA;AAIA;AAAQ;AAC8F;AAExG;AAEA;AACE;AAIF;AAEA;AACE;AACA;AAA0B;AAC1B;AAEG;AAI2C;AAE9C;AACF;AAEA;AAIE;AAIE;AAAA;AAGF;AAEA;AAIA;AACE;AAAA;AAGF;AACA;AACE;AACA;AAAA;AAGF;AACA;AAEA;AAIA;AAAQ;AAC2I;AAGrJ;AAEA;AACE;AAKF;AAEA;AACE;AAMF;AAEO;AAKL;AACF;AAcO;AAML;AAAsBA;AACpB;AAEF;AACA;AAEA;AACE;AACA;AAAO;AACG;AACD;AAC2B;AAAA;AAGpC;AACE;AAAqD;AAGvD;AAAM;AAEN;AACE;AACA;AACA;AACA;AACA;AAA+C;AACjD;AAEJ;AAEO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AAEA;AAAQ;AAC4F;AAItG;AAkYO;AAIL;AACF;AAGO;AAIL;AACF;AAIA;AACE;AAAO;AACO;AACN;AACE;AACA;AACsC;AACrB;AACV;AACJ;AACF;AACC;AAAA;AAEe;AAGd;AACD;AACC;AACE;AACE;AACJ;AAEf;AAEA;AAKE;AAEA;AACA;AAGA;AAEA;AACA;AACA;AAGA;AAGA;AAEA;AACA;AACA;AAEA;AACE;AACA;AAAsC;AAEtC;AAGA;AAKE;AACE;AAAqB;AACnB;AACA;AACS;AAEX;AAAiB;AACf;AACA;AAC+C;AAAA;AAGjD;AACA;AACA;AAAgB;AAClB;AAEF;AACA;AAA4D;AAE5D;AAAoB;AAClB;AACA;AACS;AACA;AAEX;AACA;AACE;AACA;AAA6B;AAE7B;AACA;AACA;AAA4B;AAC9B;AAGF;AACA;AACE;AACE;AAAyD;AAEzD;AACE;AACA;AAAgB;AAEhB;AAAM;AACR;AACF;AAGF;AACE;AAAA;AAGF;AAKEC;AAAAA;AACW;AACT;AACA;AACA;AACA;AAAA;AAIJ;AASA;AAOE;AAAqB;AACnB;AACA;AACS;AAAA;AAIb;AAOA;AAIA;AAGA;AAEA;AAQA;AAKA;AACE;AAIE;AAGA;AAIE;AAAQ;;AACN;AAAA;;AAAA;AAAA;AAAA;AAQJ;AACA;AACA;AAAkB;AAElB;AACE;AAAsB;AAEtB;AACE;AAAqB;AACnB;AACA;AACS;AAAA;AAGb;AAAgE;AAG1D;AAAA;AAEmB;AAAA;AAEiB;AAE1C;AACE;AACA;AAAyC;AAAG;AAC1C;AACE;AACA;AACA;AAAA;AAGJ;AAIE;AAAA;AAEI;AACE;AACA;AACA;AAAA;AAEJ;AAAA;AAGJ;AAAuC;AACrC;AAEE;AACA;AAAA;AACF;AAEF;AAA0C;AAC5C;AAEF;AACA;AAAgC;AAElC;AACA;AACA;AACA;AAAyB;AAS3B;AAOE;AAAiE;AACzD;AACE;AACY;AACT;AACZ;AAEH;AAKA;AAME;AAA+C;AAAE;AAKnD;AACE;AACE;AACA;AACA;AAAO;AACT;AAGF;AAAiBC;AACN;AACM;AACA;AAGjB;AACEC;AAAiC;AACvB;AACY;AACI;AACxB;AACD;AAEH;AACF;;;;;;;"}
1
+ {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef, useSyncExternalStore } from 'react'\nimport {\n UnhashableQueryIRError,\n createLiveQueryObserver,\n deepEquals,\n getPreparedLiveQueryIdentity,\n getStableValueHash,\n isCollection,\n prepareLiveQueryValue,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { setLiveQueryResultInfo } from './live-query-internals'\nimport { shouldWarnInDevelopment } from './development'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n getPreparedSources,\n getSourceObjectToken,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport type {\n Collection,\n CollectionImpl,\n CollectionStatus,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n// Suspense renders can be abandoned before React subscribes. Keep their\n// collection briefly so a nearby retry can commit without starting over.\nconst DEFAULT_SUSPENSE_GC_TIME_MS = 5000\nconst DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16\nconst DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10\nconst DERIVED_IDENTITY_TOTAL_WARN_MS = 50\nconst warnedDepsCallsites = new Set<string>()\nconst warnedDerivedIdentityCallsites = new Set<string>()\nconst warnedUnhashableIdentityCallsites = new Set<string>()\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\ntype SuspenseCollection = Collection<object, string | number, {}>\ntype SuspenseCollectionEntry = {\n collection: SuspenseCollection\n removeStatusListeners: () => void\n}\n\nconst unscopedSuspenseCollections = new Map<string, SuspenseCollectionEntry>()\nconst suspenseCollectionsByClient = new WeakMap<\n DbClient,\n Map<string, SuspenseCollectionEntry>\n>()\nfunction getSourceQualifiedSuspenseKey(\n preparedValue: unknown,\n queryHash: string,\n client: DbClient | undefined,\n): string {\n const sourceIds = getPreparedSources(\n preparedValue,\n client,\n `useLiveSuspenseQuery`,\n ).map(getSourceObjectToken)\n if (sourceIds.length === 0) return queryHash\n return `${sourceIds.join(`,`)}:${queryHash}`\n}\n\nfunction getSuspenseCollections(\n client: DbClient | undefined,\n): Map<string, SuspenseCollectionEntry> {\n if (!client) return unscopedSuspenseCollections\n let collections = suspenseCollectionsByClient.get(client)\n if (!collections) {\n collections = new Map()\n suspenseCollectionsByClient.set(client, collections)\n }\n return collections\n}\n\nfunction releaseSuspenseCollection(\n collections: Map<string, SuspenseCollectionEntry>,\n queryHash: string,\n collection: SuspenseCollection,\n): void {\n const entry = collections.get(queryHash)\n if (entry?.collection !== collection) return\n collections.delete(queryHash)\n entry.removeStatusListeners()\n}\n\nexport type DerivedIdentityProfiler = {\n renderCount: number\n totalMs: number\n maxMs: number\n warned: boolean\n}\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\nexport type LiveQueryKey = ReadonlyArray<unknown>\ntype UseLiveQueryConfigOptions<TContext extends Context> = Omit<\n LiveQueryCollectionConfig<TContext>,\n `query`\n> & {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n}\n\ntype ConfiguredQueryBuilder<TContext extends Context> = Extract<\n LiveQueryCollectionConfig<TContext>[`query`],\n QueryBuilder<TContext>\n>\n\nexport type UseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> &\n Pick<LiveQueryCollectionConfig<TContext>, `query`>\n\nexport type ConditionalUseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> & {\n query:\n | ConfiguredQueryBuilder<TContext>\n | ((\n q: InitialQueryBuilder,\n ) => ConfiguredQueryBuilder<TContext> | undefined | null)\n }\n\nexport function warnDeprecatedDepsArray(\n hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedDepsCallsites.has(callsite)) {\n return\n }\n warnedDepsCallsites.add(callsite)\n const replacement =\n hookName === `useLiveQuery`\n ? `useLiveQuery({ query })`\n : `useLiveInfiniteQuery(query, { queryKey })`\n console.warn(\n `[${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.`,\n )\n}\n\nfunction getCurrentTime(): number {\n return typeof performance !== `undefined` &&\n typeof performance.now === `function`\n ? performance.now()\n : Date.now()\n}\n\nfunction getWarningCallsite(stackIndex: number): string {\n const stack = new Error().stack ?? `unknown`\n const lines = stack.split(`\\n`)\n const userFrame = lines\n .slice(1)\n .find(\n (line) =>\n !line.includes(`useLiveQuery.ts`) &&\n !line.includes(`useLiveSuspenseQuery.ts`) &&\n !line.includes(`useLiveInfiniteQuery.ts`),\n )\n return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack\n}\n\nfunction warnDerivedIdentityHotPath(\n profiler: DerivedIdentityProfiler,\n durationMs: number,\n): void {\n if (\n profiler.warned ||\n !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n return\n }\n\n const isSlowSingleRender =\n durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS\n const isHotRenderPath =\n profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&\n profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS\n\n if (!isSlowSingleRender && !isHotRenderPath) {\n return\n }\n\n const callsite = getWarningCallsite(5)\n if (warnedDerivedIdentityCallsites.has(callsite)) {\n profiler.warned = true\n return\n }\n\n warnedDerivedIdentityCallsites.add(callsite)\n profiler.warned = true\n\n const reason = isSlowSingleRender\n ? `one render took ${durationMs.toFixed(1)}ms`\n : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`\n\n console.warn(\n `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +\n `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,\n )\n}\n\nfunction getExplicitQueryKey(value: unknown): LiveQueryKey | undefined {\n return value &&\n typeof value === `object` &&\n Array.isArray((value as { queryKey?: unknown }).queryKey)\n ? (value as { queryKey: LiveQueryKey }).queryKey\n : undefined\n}\n\nfunction getExplicitDbClient(value: unknown): DbClient | undefined {\n return value &&\n typeof value === `object` &&\n `client` in value &&\n (value as { client?: unknown }).client !== undefined\n ? (value as { client: DbClient }).client\n : undefined\n}\n\nexport function prepareQueryValue(\n value: unknown,\n dbClient: DbClient | undefined,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): unknown {\n return prepareLiveQueryValue(value, dbClient, deferredCollections)\n}\n\ntype DerivedQueryPreparation =\n | {\n status: `hashable`\n value: unknown\n identityDeps: Array<unknown>\n }\n | {\n status: `unhashable`\n value: unknown\n error: UnhashableQueryIRError\n }\n\nexport function prepareDerivedQuery(\n value: unknown,\n dbClient: DbClient | undefined,\n profiler: DerivedIdentityProfiler,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): DerivedQueryPreparation {\n const shouldProfile = shouldWarnInDevelopment(\n `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,\n )\n const start = shouldProfile ? getCurrentTime() : 0\n const preparedValue = prepareQueryValue(value, dbClient, deferredCollections)\n\n try {\n const identity = getPreparedLiveQueryIdentity(preparedValue)\n return {\n status: `hashable`,\n value: preparedValue,\n identityDeps: [`derived`, identity],\n }\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n return { status: `unhashable`, value: preparedValue, error }\n }\n\n throw error\n } finally {\n if (shouldProfile) {\n const durationMs = getCurrentTime() - start\n profiler.renderCount += 1\n profiler.totalMs += durationMs\n profiler.maxMs = Math.max(profiler.maxMs, durationMs)\n warnDerivedIdentityHotPath(profiler, durationMs)\n }\n }\n}\n\nexport function warnUnhashableDerivedIdentity(\n error: UnhashableQueryIRError,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedUnhashableIdentityCallsites.has(callsite)) {\n return\n }\n warnedUnhashableIdentityCallsites.add(callsite)\n\n console.warn(\n `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` +\n `It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` +\n `Unhashable queries without queryKey will throw in 1.0.`,\n )\n}\n\n/**\n * Create a live query using a query function.\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Prefer config object syntax\n * const { data, isLoading } = useLiveQuery({\n * query: (q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * })\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * })\n *\n * @example\n * // Structured captured values are included in derived query identity\n * const { data, state } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // Return undefined or null to disable a query\n * const { data, isEnabled } = useLiveQuery({\n * query: (q) => {\n * if (!userId) return undefined\n * return q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.userId, userId))\n * },\n * })\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery({\n * query: (q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * })\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todoCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({\n * query: queryBuilder,\n * })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 7: Accept config object with a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 8: Accept config object with legacy deps\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 9: Accept config object with legacy deps and a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n deps: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 10: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 10: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps?: Array<unknown>,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps)\n}\n\n/** @internal Shared implementation for the Suspense wrapper. */\nexport function useLiveQueryForSuspense(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps, true)\n}\n\n// What one hook instance keeps across renders. It lives in a single ref\n// slot, so a render neither looks up nor allocates more.\nfunction createHookInstance(dbClient: DbClient | undefined) {\n return {\n collection: null as Collection<object, string | number, {}> | null,\n deps: null as Array<unknown> | null,\n config: null as unknown,\n client: dbClient,\n legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,\n derivedIdentityProfiler: {\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n } as DerivedIdentityProfiler,\n deferredCollections: new Set<\n CollectionImpl<any, string | number, any, any, any>\n >(),\n sourceIds: createSourceIdBindings(),\n observer: null as LiveQueryObserver<object, string | number> | null,\n queryHash: undefined as string | undefined,\n suspenseKey: undefined as string | undefined,\n identityError: undefined as UnhashableQueryIRError | undefined,\n subscribe: null as ((onStoreChange: () => void) => () => void) | null,\n }\n}\n\nfunction useLiveQueryImpl(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n forSuspense = false,\n) {\n const contextDbClient = useOptionalDbClient()\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n const dbClient = inputIsCollection\n ? contextDbClient\n : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)\n const resolvedDeps = deps ?? []\n\n const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)\n const instance = (instanceRef.current ??= createHookInstance(dbClient))\n const resumeDeferredCollections = () =>\n resumeSyncStarts(instance.deferredCollections)\n\n const queryKey = !inputIsCollection\n ? getExplicitQueryKey(configOrQueryOrCollection)\n : undefined\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown>\n let streamIdentity: unknown = undefined\n let identityError: UnhashableQueryIRError | undefined\n\n if (queryKey) {\n identityDeps = queryKey\n streamIdentity = [`queryKey`, queryKey]\n } else if (deps !== undefined) {\n identityDeps = resolvedDeps\n // Deps decide reuse. Only hydration and Suspense read the query hash; the\n // development warning about unhashable queries still derives it.\n if (\n dbClient ||\n forSuspense ||\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n )\n try {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n streamIdentity = [\n `deps`,\n resolvedDeps,\n getPreparedLiveQueryIdentity(preparedQueryValue),\n ]\n } catch (error) {\n if (!(error instanceof UnhashableQueryIRError)) throw error\n warnUnhashableDerivedIdentity(error)\n identityError = error\n }\n } else if (inputIsCollection) {\n identityDeps = []\n streamIdentity = [`collection`, configOrQueryOrCollection.id]\n } else {\n const preparation = prepareDerivedQuery(\n configOrQueryOrCollection,\n dbClient,\n instance.derivedIdentityProfiler,\n instance.deferredCollections,\n )\n preparedQueryValue = preparation.value\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n streamIdentity = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = instance.legacyUnhashableIdentity\n identityError = preparation.error\n }\n }\n\n let queryHash: string | undefined\n if (streamIdentity !== undefined) {\n try {\n queryHash = getStableValueHash(streamIdentity, `queryKey`)\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n if (queryKey !== undefined) throw error\n identityError = error\n } else {\n throw error\n }\n }\n }\n\n if (deps !== undefined) {\n warnDeprecatedDepsArray()\n }\n\n if (\n queryKey === undefined &&\n deps === undefined &&\n preparedQueryValue !== unpreparedQueryValue\n ) {\n claimSourceIds(\n instance.sourceIds,\n preparedQueryValue,\n dbClient,\n `useLiveQuery`,\n resumeDeferredCollections,\n )\n }\n\n const canReuseSuspenseKey =\n forSuspense &&\n !inputIsCollection &&\n queryHash !== undefined &&\n instance.collection !== null &&\n instance.client === dbClient &&\n instance.queryHash === queryHash &&\n instance.suspenseKey !== undefined\n\n if (\n forSuspense &&\n !inputIsCollection &&\n queryHash &&\n !canReuseSuspenseKey &&\n preparedQueryValue === unpreparedQueryValue\n ) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n\n const suspenseKey =\n queryHash && forSuspense && !inputIsCollection\n ? canReuseSuspenseKey\n ? instance.suspenseKey\n : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient)\n : queryHash\n\n const suspenseCollections =\n forSuspense && !inputIsCollection && suspenseKey\n ? getSuspenseCollections(dbClient)\n : undefined\n const suspenseEntry = suspenseKey\n ? suspenseCollections?.get(suspenseKey)\n : undefined\n const suspenseCollection = suspenseEntry?.collection\n\n const identityChanged =\n instance.deps === null ||\n (deps !== undefined\n ? instance.deps.length !== identityDeps.length ||\n instance.deps.some((dep, index) => dep !== identityDeps[index])\n : !deepEquals(instance.deps, identityDeps))\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !instance.collection ||\n (inputIsCollection && instance.config !== configOrQueryOrCollection) ||\n (!inputIsCollection && (instance.client !== dbClient || identityChanged))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (\n syncMode === `on-demand` &&\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n instance.collection = configOrQueryOrCollection\n instance.config = configOrQueryOrCollection\n } else {\n if (suspenseCollection) {\n instance.collection = suspenseCollection\n } else {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n if (!dbClient) {\n getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`)\n }\n instance.collection = resolveLiveQueryValue(preparedQueryValue, {\n gcTime: forSuspense\n ? DEFAULT_SUSPENSE_GC_TIME_MS\n : DEFAULT_GC_TIME_MS,\n // Hydration and Suspense key the live-query Collection by identity.\n pool: !forSuspense && !dbClient,\n // Without a DbClient, Suspense starts it through a preload below.\n startSync: !forSuspense || dbClient !== undefined,\n }) as SuspenseCollection | null\n if (suspenseCollections && suspenseKey && instance.collection) {\n const collection = instance.collection\n const removeCleanupListener = collection.on(`status:cleaned-up`, () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n )\n const removeErrorListener = collection.on(`status:error`, () => {\n // Keep the failed collection through React's immediate retry so\n // the hook can throw its actual load error to an ErrorBoundary.\n // Retire it afterward so a later boundary reset starts fresh.\n setTimeout(\n () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n 0,\n )\n })\n const entry: SuspenseCollectionEntry = {\n collection,\n removeStatusListeners: () => {\n removeCleanupListener()\n removeErrorListener()\n },\n }\n suspenseCollections.set(suspenseKey, entry)\n }\n }\n instance.config = configOrQueryOrCollection\n instance.deps = [...identityDeps]\n }\n instance.client = dbClient\n instance.queryHash = queryHash\n instance.suspenseKey = suspenseKey\n instance.identityError = identityError\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n instance.observer = createLiveQueryObserver(instance.collection, {\n mode: `wholesale`,\n client: dbClient,\n queryHash: instance.queryHash,\n onPreload: resumeDeferredCollections,\n })\n }\n const observer = instance.observer!\n // A Suspense render asks for its data. Preloading starts the sync run, so a\n // source that already holds the rows makes it ready before the snapshot is\n // read instead of suspending once. useLiveSuspenseQuery surfaces failures.\n // A DbClient defers source starts past render and owns its streamed query.\n if (\n forSuspense &&\n !dbClient &&\n (instance.collection?.status === `idle` ||\n instance.collection?.status === `cleaned-up`)\n ) {\n observer.preloadForInitialRender().catch(() => {})\n }\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n if (!instance.subscribe || needsNewCollection) {\n instance.subscribe = (onStoreChange: () => void) => {\n const unsubscribe = observer.subscribe(onStoreChange)\n resumeDeferredCollections()\n return unsubscribe\n }\n }\n\n const returned = useSyncExternalStore(\n instance.subscribe,\n () => observer.getSnapshot(),\n () => observer.getServerSnapshot(),\n )\n // Only useLiveSuspenseQuery reads this, and it costs a define per render.\n if (forSuspense) {\n setLiveQueryResultInfo(returned, {\n client: dbClient,\n queryHash: instance.queryHash,\n identityError: instance.identityError,\n observer,\n })\n }\n return returned as any\n}\n"],"names":["getPreparedSources","shouldWarnInDevelopment","claimSourceIds","useSyncExternalStore","setLiveQueryResultInfo"],"mappings":";;;;;;;;;AAwCA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AAIA;AAKE;AAAkBA;AAChB;AACA;AACA;AAEF;AACA;AACF;AAEA;AAGE;AACA;AACA;AACE;AACA;AAAmD;AAErD;AACF;AAEA;AAKE;AACA;AACA;AACA;AACF;AA2CO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AACA;AAIA;AAAQ;AAC8F;AAExG;AAEA;AACE;AAIF;AAEA;AACE;AACA;AAA0B;AAC1B;AAEG;AAI2C;AAE9C;AACF;AAEA;AAIE;AAIE;AAAA;AAGF;AAEA;AAIA;AACE;AAAA;AAGF;AACA;AACE;AACA;AAAA;AAGF;AACA;AAEA;AAIA;AAAQ;AAC2I;AAGrJ;AAEA;AACE;AAKF;AAEA;AACE;AAMF;AAEO;AAKL;AACF;AAcO;AAML;AAAsBC;AACpB;AAEF;AACA;AAEA;AACE;AACA;AAAO;AACG;AACD;AAC2B;AAAA;AAGpC;AACE;AAAqD;AAGvD;AAAM;AAEN;AACE;AACA;AACA;AACA;AACA;AAA+C;AACjD;AAEJ;AAEO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AAEA;AAAQ;AAC4F;AAItG;AAkYO;AAIL;AACF;AAGO;AAIL;AACF;AAIA;AACE;AAAO;AACO;AACN;AACE;AACA;AACsC;AACrB;AACV;AACJ;AACF;AACC;AAAA;AAEe;AAGd;AACD;AACC;AACE;AACE;AACJ;AAEf;AAEA;AAKE;AAEA;AACA;AAGA;AAEA;AACA;AACA;AAGA;AAGA;AAEA;AACA;AACA;AAEA;AACE;AACA;AAAsC;AAEtC;AAGA;AAKE;AACE;AAAqB;AACnB;AACA;AACS;AAEX;AAAiB;AACf;AACA;AAC+C;AAAA;AAGjD;AACA;AACA;AAAgB;AAClB;AAEF;AACA;AAA4D;AAE5D;AAAoB;AAClB;AACA;AACS;AACA;AAEX;AACA;AACE;AACA;AAA6B;AAE7B;AACA;AACA;AAA4B;AAC9B;AAGF;AACA;AACE;AACE;AAAyD;AAEzD;AACE;AACA;AAAgB;AAEhB;AAAM;AACR;AACF;AAGF;AACE;AAAA;AAGF;AAKEC;AAAAA;AACW;AACT;AACA;AACA;AACA;AAAA;AAIJ;AASA;AAOE;AAAqB;AACnB;AACA;AACS;AAAA;AAIb;AAOA;AAIA;AAGA;AAEA;AAQA;AAKA;AACE;AAIE;AAGA;AAIE;AAAQ;;AACN;AAAA;;AAAA;AAAA;AAAA;AAQJ;AACA;AACA;AAAkB;AAElB;AACE;AAAsB;AAEtB;AACE;AAAqB;AACnB;AACA;AACS;AAAA;AAGb;AACEF;AAA+D;AAEjE;AAAgE;AAG1D;AAAA;AAEmB;AAAA;AAEiB;AAE1C;AACE;AACA;AAAyC;AAAG;AAC1C;AACE;AACA;AACA;AAAA;AAGJ;AAIE;AAAA;AAEI;AACE;AACA;AACA;AAAA;AAEJ;AAAA;AAGJ;AAAuC;AACrC;AAEE;AACA;AAAA;AACF;AAEF;AAA0C;AAC5C;AAEF;AACA;AAAgC;AAElC;AACA;AACA;AACA;AAAyB;AAS3B;AAOE;AAAiE;AACzD;AACE;AACY;AACT;AACZ;AAEH;AAKA;AAME;AAA+C;AAAE;AAKnD;AACE;AACE;AACA;AACA;AAAO;AACT;AAGF;AAAiBG;AACN;AACM;AACA;AAGjB;AACEC;AAAiC;AACvB;AACY;AACI;AACxB;AACD;AAEH;AACF;;;;;;;"}
@@ -2,13 +2,18 @@
2
2
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
3
  const react = require("react");
4
4
  const db = require("@tanstack/db");
5
+ const DbProvider = require("./DbProvider.cjs");
6
+ const sourceIdBindings = require("./source-id-bindings.cjs");
5
7
  function useLiveQueryEffect(config, deps = []) {
8
+ const client = DbProvider.useOptionalDbClient();
6
9
  const configRef = react.useRef(config);
7
10
  configRef.current = config;
8
11
  react.useEffect(() => {
12
+ const query = db.prepareLiveQueryValue(config.query, client);
13
+ if (!client) sourceIdBindings.getPreparedSources(query, client, `useLiveQueryEffect`);
9
14
  const effect = db.createEffect({
10
15
  id: config.id,
11
- query: config.query,
16
+ query,
12
17
  skipInitial: config.skipInitial,
13
18
  onEnter: (event, ctx) => configRef.current.onEnter?.(event, ctx),
14
19
  onUpdate: (event, ctx) => configRef.current.onUpdate?.(event, ctx),
@@ -18,9 +23,11 @@ function useLiveQueryEffect(config, deps = []) {
18
23
  onSourceError: config.onSourceError ? (error) => configRef.current.onSourceError?.(error) : void 0
19
24
  });
20
25
  return () => {
21
- effect.dispose();
26
+ void effect.dispose().catch((error) => {
27
+ console.error(`[useLiveQueryEffect] failed to dispose effect:`, error);
28
+ });
22
29
  };
23
- }, deps);
30
+ }, [...deps, client]);
24
31
  }
25
32
  exports.useLiveQueryEffect = useLiveQueryEffect;
26
33
  //# sourceMappingURL=useLiveQueryEffect.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQueryEffect.cjs","sources":["../../src/useLiveQueryEffect.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { createEffect } from '@tanstack/db'\nimport type { Effect, EffectConfig } from '@tanstack/db'\n\n/**\n * React hook for creating a reactive effect that fires handlers when rows\n * enter, exit, or update within a query result.\n *\n * The effect is created on mount and disposed on unmount. If `deps` change,\n * the previous effect is disposed and a new one is created.\n *\n * @example\n * ```tsx\n * function ChatComponent() {\n * useLiveQueryEffect(\n * {\n * query: (q) => q.from({ msg: messages }).where(({ msg }) => eq(msg.role, 'user')),\n * skipInitial: true,\n * onEnter: async (event) => {\n * await generateResponse(event.value)\n * },\n * },\n * []\n * )\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useLiveQueryEffect<\n TRow extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(config: EffectConfig<TRow, TKey>, deps: React.DependencyList = []): void {\n const configRef = useRef<EffectConfig<TRow, TKey>>(config)\n configRef.current = config\n\n useEffect(() => {\n const effect: Effect = createEffect<TRow, TKey>({\n id: config.id,\n query: config.query,\n skipInitial: config.skipInitial,\n onEnter: (event, ctx) => configRef.current.onEnter?.(event, ctx),\n onUpdate: (event, ctx) => configRef.current.onUpdate?.(event, ctx),\n onExit: (event, ctx) => configRef.current.onExit?.(event, ctx),\n onBatch: (events, ctx) => configRef.current.onBatch?.(events, ctx),\n onError: config.onError\n ? (error, event) => configRef.current.onError?.(error, event)\n : undefined,\n onSourceError: config.onSourceError\n ? (error) => configRef.current.onSourceError?.(error)\n : undefined,\n })\n\n return () => {\n // Fire-and-forget disposal; AbortSignal cancels in-flight work\n effect.dispose()\n }\n }, deps)\n}\n"],"names":["useRef","useEffect","createEffect"],"mappings":";;;;AA6BO,SAAS,mBAGd,QAAkC,OAA6B,IAAU;AACzE,QAAM,YAAYA,MAAAA,OAAiC,MAAM;AACzD,YAAU,UAAU;AAEpBC,QAAAA,UAAU,MAAM;AACd,UAAM,SAAiBC,GAAAA,aAAyB;AAAA,MAC9C,IAAI,OAAO;AAAA,MACX,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,SAAS,CAAC,OAAO,QAAQ,UAAU,QAAQ,UAAU,OAAO,GAAG;AAAA,MAC/D,UAAU,CAAC,OAAO,QAAQ,UAAU,QAAQ,WAAW,OAAO,GAAG;AAAA,MACjE,QAAQ,CAAC,OAAO,QAAQ,UAAU,QAAQ,SAAS,OAAO,GAAG;AAAA,MAC7D,SAAS,CAAC,QAAQ,QAAQ,UAAU,QAAQ,UAAU,QAAQ,GAAG;AAAA,MACjE,SAAS,OAAO,UACZ,CAAC,OAAO,UAAU,UAAU,QAAQ,UAAU,OAAO,KAAK,IAC1D;AAAA,MACJ,eAAe,OAAO,gBAClB,CAAC,UAAU,UAAU,QAAQ,gBAAgB,KAAK,IAClD;AAAA,IAAA,CACL;AAED,WAAO,MAAM;AAEX,aAAO,QAAA;AAAA,IACT;AAAA,EACF,GAAG,IAAI;AACT;;"}
1
+ {"version":3,"file":"useLiveQueryEffect.cjs","sources":["../../src/useLiveQueryEffect.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { createEffect, prepareLiveQueryValue } from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { getPreparedSources } from './source-id-bindings'\nimport type { EffectConfig } from '@tanstack/db'\n\n/**\n * React hook for creating a reactive effect that fires handlers when rows\n * enter, exit, or update within a query result.\n *\n * The effect is created on mount and disposed on unmount. If `deps` change,\n * the previous effect is disposed and a new one is created.\n *\n * @example\n * ```tsx\n * function ChatComponent() {\n * useLiveQueryEffect(\n * {\n * query: (q) => q.from({ msg: messages }).where(({ msg }) => eq(msg.role, 'user')),\n * skipInitial: true,\n * onEnter: async (event) => {\n * await generateResponse(event.value)\n * },\n * },\n * []\n * )\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useLiveQueryEffect<\n TRow extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(config: EffectConfig<TRow, TKey>, deps: React.DependencyList = []): void {\n const client = useOptionalDbClient()\n const configRef = useRef<EffectConfig<TRow, TKey>>(config)\n configRef.current = config\n\n useEffect(() => {\n const query = prepareLiveQueryValue(config.query, client)\n if (!client) getPreparedSources(query, client, `useLiveQueryEffect`)\n const effect = createEffect<TRow, TKey>({\n id: config.id,\n query: query as EffectConfig<TRow, TKey>[`query`],\n skipInitial: config.skipInitial,\n onEnter: (event, ctx) => configRef.current.onEnter?.(event, ctx),\n onUpdate: (event, ctx) => configRef.current.onUpdate?.(event, ctx),\n onExit: (event, ctx) => configRef.current.onExit?.(event, ctx),\n onBatch: (events, ctx) => configRef.current.onBatch?.(events, ctx),\n onError: config.onError\n ? (error, event) => configRef.current.onError?.(error, event)\n : undefined,\n onSourceError: config.onSourceError\n ? (error) => configRef.current.onSourceError?.(error)\n : undefined,\n })\n\n return () => {\n // React cannot await cleanup. Report a failed source release.\n void effect.dispose().catch((error) => {\n console.error(`[useLiveQueryEffect] failed to dispose effect:`, error)\n })\n }\n }, [...deps, client])\n}\n"],"names":["useOptionalDbClient","useRef","useEffect","prepareLiveQueryValue","getPreparedSources","createEffect"],"mappings":";;;;;;AA+BO,SAAS,mBAGd,QAAkC,OAA6B,IAAU;AACzE,QAAM,SAASA,WAAAA,oBAAA;AACf,QAAM,YAAYC,MAAAA,OAAiC,MAAM;AACzD,YAAU,UAAU;AAEpBC,QAAAA,UAAU,MAAM;AACd,UAAM,QAAQC,GAAAA,sBAAsB,OAAO,OAAO,MAAM;AACxD,QAAI,CAAC,OAAQC,kBAAAA,mBAAmB,OAAO,QAAQ,oBAAoB;AACnE,UAAM,SAASC,GAAAA,aAAyB;AAAA,MACtC,IAAI,OAAO;AAAA,MACX;AAAA,MACA,aAAa,OAAO;AAAA,MACpB,SAAS,CAAC,OAAO,QAAQ,UAAU,QAAQ,UAAU,OAAO,GAAG;AAAA,MAC/D,UAAU,CAAC,OAAO,QAAQ,UAAU,QAAQ,WAAW,OAAO,GAAG;AAAA,MACjE,QAAQ,CAAC,OAAO,QAAQ,UAAU,QAAQ,SAAS,OAAO,GAAG;AAAA,MAC7D,SAAS,CAAC,QAAQ,QAAQ,UAAU,QAAQ,UAAU,QAAQ,GAAG;AAAA,MACjE,SAAS,OAAO,UACZ,CAAC,OAAO,UAAU,UAAU,QAAQ,UAAU,OAAO,KAAK,IAC1D;AAAA,MACJ,eAAe,OAAO,gBAClB,CAAC,UAAU,UAAU,QAAQ,gBAAgB,KAAK,IAClD;AAAA,IAAA,CACL;AAED,WAAO,MAAM;AAEX,WAAK,OAAO,QAAA,EAAU,MAAM,CAAC,UAAU;AACrC,gBAAQ,MAAM,kDAAkD,KAAK;AAAA,MACvE,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,GAAG,MAAM,MAAM,CAAC;AACtB;;"}
@@ -1,6 +1,7 @@
1
1
  import { DbClient } from '@tanstack/db';
2
+ export { resumeDeferredLiveQueryCollections as resumeSyncStarts } from '@tanstack/db';
2
3
  export declare function getSourceObjectToken(source: object): number;
3
- export declare function getPreparedSources(preparedValue: unknown): Array<{
4
+ export declare function getPreparedSources(preparedValue: unknown, client?: DbClient, hookName?: string): Array<{
4
5
  id: string;
5
6
  }>;
6
7
  /** The source object each Collection ID named while one hook is mounted. */
@@ -17,10 +18,3 @@ export declare function createSourceIdBindings(): SourceIdBindings;
17
18
  * rejected render keeps no sync-start deferral.
18
19
  */
19
20
  export declare function claimSourceIds(bindings: SourceIdBindings, preparedValue: unknown, client: DbClient | undefined, hookName: string, release: () => void): void;
20
- /**
21
- * Resumes every deferred sync start, then rethrows the first startup error, so
22
- * one failing source cannot keep a later shared source deferred.
23
- */
24
- export declare function resumeSyncStarts(collections: Set<{
25
- _resumeSyncStart: () => void;
26
- }>): void;
@@ -1,4 +1,5 @@
1
- import { isCollection, BaseQueryBuilder, IR } from "@tanstack/db";
1
+ import { getPreparedLiveQuerySources } from "@tanstack/db";
2
+ import { resumeDeferredLiveQueryCollections } from "@tanstack/db";
2
3
  const sourceObjectTokens = /* @__PURE__ */ new WeakMap();
3
4
  let nextSourceObjectToken = 0;
4
5
  function getSourceObjectToken(source) {
@@ -9,12 +10,15 @@ function getSourceObjectToken(source) {
9
10
  }
10
11
  return id;
11
12
  }
12
- function getPreparedSources(preparedValue) {
13
- if (isCollection(preparedValue)) return [preparedValue];
14
- const query = preparedValue instanceof BaseQueryBuilder ? preparedValue : preparedValue && typeof preparedValue === `object` && `query` in preparedValue && preparedValue.query instanceof BaseQueryBuilder ? preparedValue.query : void 0;
15
- return query ? IR.collectCollectionSources(query._getQuery()).map(
16
- ({ collection }) => collection
17
- ) : [];
13
+ function getPreparedSources(preparedValue, client, hookName) {
14
+ return getPreparedLiveQuerySources(
15
+ preparedValue,
16
+ !client && hookName ? (alias) => {
17
+ throw new Error(
18
+ `[${hookName}] Collection descriptor "${alias}" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`
19
+ );
20
+ } : void 0
21
+ );
18
22
  }
19
23
  function createSourceIdBindings() {
20
24
  return { unscoped: /* @__PURE__ */ new Map(), byClient: /* @__PURE__ */ new WeakMap() };
@@ -22,7 +26,7 @@ function createSourceIdBindings() {
22
26
  function claimSourceIds(bindings, preparedValue, client, hookName, release) {
23
27
  const prior = client ? bindings.byClient.get(client) : bindings.unscoped;
24
28
  const seen = /* @__PURE__ */ new Map();
25
- for (const source of getPreparedSources(preparedValue)) {
29
+ for (const source of getPreparedSources(preparedValue, client, hookName)) {
26
30
  const token = getSourceObjectToken(source);
27
31
  const previous = seen.get(source.id) ?? prior?.get(source.id);
28
32
  if (previous !== void 0 && previous !== token) {
@@ -37,24 +41,11 @@ function claimSourceIds(bindings, preparedValue, client, hookName, release) {
37
41
  for (const [id, token] of seen) claimed.set(id, token);
38
42
  if (client) bindings.byClient.set(client, claimed);
39
43
  }
40
- function resumeSyncStarts(collections) {
41
- const pending = Array.from(collections);
42
- collections.clear();
43
- const errors = [];
44
- for (const collection of pending) {
45
- try {
46
- collection._resumeSyncStart();
47
- } catch (error) {
48
- errors.push(error);
49
- }
50
- }
51
- if (errors.length) throw errors[0];
52
- }
53
44
  export {
54
45
  claimSourceIds,
55
46
  createSourceIdBindings,
56
47
  getPreparedSources,
57
48
  getSourceObjectToken,
58
- resumeSyncStarts
49
+ resumeDeferredLiveQueryCollections as resumeSyncStarts
59
50
  };
60
51
  //# sourceMappingURL=source-id-bindings.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"source-id-bindings.js","sources":["../../src/source-id-bindings.ts"],"sourcesContent":["import { BaseQueryBuilder, IR, isCollection } from '@tanstack/db'\nimport type { DbClient } from '@tanstack/db'\n\nconst sourceObjectTokens = new WeakMap<object, number>()\nlet nextSourceObjectToken = 0\n\nexport function getSourceObjectToken(source: object): number {\n let id = sourceObjectTokens.get(source)\n if (id === undefined) {\n id = ++nextSourceObjectToken\n sourceObjectTokens.set(source, id)\n }\n return id\n}\n\nexport function getPreparedSources(\n preparedValue: unknown,\n): Array<{ id: string }> {\n if (isCollection(preparedValue)) return [preparedValue]\n const query =\n preparedValue instanceof BaseQueryBuilder\n ? preparedValue\n : preparedValue &&\n typeof preparedValue === `object` &&\n `query` in preparedValue &&\n preparedValue.query instanceof BaseQueryBuilder\n ? preparedValue.query\n : undefined\n return query\n ? IR.collectCollectionSources(query._getQuery()).map(\n ({ collection }) => collection,\n )\n : []\n}\n\n/** The source object each Collection ID named while one hook is mounted. */\nexport type SourceIdBindings = {\n unscoped: Map<string, number>\n byClient: WeakMap<DbClient, Map<string, number>>\n}\n\nexport function createSourceIdBindings(): SourceIdBindings {\n return { unscoped: new Map(), byClient: new WeakMap() }\n}\n\n/**\n * Derived query identity names each source by its Collection ID. While a hook\n * is mounted, an ID must keep naming one source object in each client scope,\n * or the hook could reuse a live query that reads the old source. Claims the\n * prepared value's sources, or calls `release` and throws on a collision so a\n * rejected render keeps no sync-start deferral.\n */\nexport function claimSourceIds(\n bindings: SourceIdBindings,\n preparedValue: unknown,\n client: DbClient | undefined,\n hookName: string,\n release: () => void,\n): void {\n const prior = client ? bindings.byClient.get(client) : bindings.unscoped\n const seen = new Map<string, number>()\n for (const source of getPreparedSources(preparedValue)) {\n const token = getSourceObjectToken(source)\n const previous = seen.get(source.id) ?? prior?.get(source.id)\n if (previous !== undefined && previous !== token) {\n release()\n throw new Error(\n `[${hookName}] Source Collection \"${source.id}\" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,\n )\n }\n seen.set(source.id, token)\n }\n const claimed = prior ?? new Map<string, number>()\n for (const [id, token] of seen) claimed.set(id, token)\n if (client) bindings.byClient.set(client, claimed)\n}\n\n/**\n * Resumes every deferred sync start, then rethrows the first startup error, so\n * one failing source cannot keep a later shared source deferred.\n */\nexport function resumeSyncStarts(\n collections: Set<{ _resumeSyncStart: () => void }>,\n): void {\n const pending = Array.from(collections)\n collections.clear()\n const errors: Array<unknown> = []\n for (const collection of pending) {\n try {\n collection._resumeSyncStart()\n } catch (error) {\n errors.push(error)\n }\n }\n if (errors.length) throw errors[0]\n}\n"],"names":[],"mappings":";AAGA,MAAM,yCAAyB,QAAA;AAC/B,IAAI,wBAAwB;AAErB,SAAS,qBAAqB,QAAwB;AAC3D,MAAI,KAAK,mBAAmB,IAAI,MAAM;AACtC,MAAI,OAAO,QAAW;AACpB,SAAK,EAAE;AACP,uBAAmB,IAAI,QAAQ,EAAE;AAAA,EACnC;AACA,SAAO;AACT;AAEO,SAAS,mBACd,eACuB;AACvB,MAAI,aAAa,aAAa,EAAG,QAAO,CAAC,aAAa;AACtD,QAAM,QACJ,yBAAyB,mBACrB,gBACA,iBACE,OAAO,kBAAkB,YACzB,WAAW,iBACX,cAAc,iBAAiB,mBAC/B,cAAc,QACd;AACR,SAAO,QACH,GAAG,yBAAyB,MAAM,UAAA,CAAW,EAAE;AAAA,IAC7C,CAAC,EAAE,WAAA,MAAiB;AAAA,EAAA,IAEtB,CAAA;AACN;AAQO,SAAS,yBAA2C;AACzD,SAAO,EAAE,UAAU,oBAAI,IAAA,GAAO,UAAU,oBAAI,UAAQ;AACtD;AASO,SAAS,eACd,UACA,eACA,QACA,UACA,SACM;AACN,QAAM,QAAQ,SAAS,SAAS,SAAS,IAAI,MAAM,IAAI,SAAS;AAChE,QAAM,2BAAW,IAAA;AACjB,aAAW,UAAU,mBAAmB,aAAa,GAAG;AACtD,UAAM,QAAQ,qBAAqB,MAAM;AACzC,UAAM,WAAW,KAAK,IAAI,OAAO,EAAE,KAAK,OAAO,IAAI,OAAO,EAAE;AAC5D,QAAI,aAAa,UAAa,aAAa,OAAO;AAChD,cAAA;AACA,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,wBAAwB,OAAO,EAAE;AAAA,MAAA;AAAA,IAEjD;AACA,SAAK,IAAI,OAAO,IAAI,KAAK;AAAA,EAC3B;AACA,QAAM,UAAU,SAAS,oBAAI,IAAA;AAC7B,aAAW,CAAC,IAAI,KAAK,KAAK,KAAM,SAAQ,IAAI,IAAI,KAAK;AACrD,MAAI,OAAQ,UAAS,SAAS,IAAI,QAAQ,OAAO;AACnD;AAMO,SAAS,iBACd,aACM;AACN,QAAM,UAAU,MAAM,KAAK,WAAW;AACtC,cAAY,MAAA;AACZ,QAAM,SAAyB,CAAA;AAC/B,aAAW,cAAc,SAAS;AAChC,QAAI;AACF,iBAAW,iBAAA;AAAA,IACb,SAAS,OAAO;AACd,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AACA,MAAI,OAAO,OAAQ,OAAM,OAAO,CAAC;AACnC;"}
1
+ {"version":3,"file":"source-id-bindings.js","sources":["../../src/source-id-bindings.ts"],"sourcesContent":["import { getPreparedLiveQuerySources } from '@tanstack/db'\nimport type { DbClient } from '@tanstack/db'\n\nexport { resumeDeferredLiveQueryCollections as resumeSyncStarts } from '@tanstack/db'\n\nconst sourceObjectTokens = new WeakMap<object, number>()\nlet nextSourceObjectToken = 0\n\nexport function getSourceObjectToken(source: object): number {\n let id = sourceObjectTokens.get(source)\n if (id === undefined) {\n id = ++nextSourceObjectToken\n sourceObjectTokens.set(source, id)\n }\n return id\n}\n\nexport function getPreparedSources(\n preparedValue: unknown,\n client?: DbClient,\n hookName?: string,\n): Array<{ id: string }> {\n return getPreparedLiveQuerySources(\n preparedValue,\n !client && hookName\n ? (alias) => {\n throw new Error(\n `[${hookName}] Collection descriptor \"${alias}\" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`,\n )\n }\n : undefined,\n )\n}\n\n/** The source object each Collection ID named while one hook is mounted. */\nexport type SourceIdBindings = {\n unscoped: Map<string, number>\n byClient: WeakMap<DbClient, Map<string, number>>\n}\n\nexport function createSourceIdBindings(): SourceIdBindings {\n return { unscoped: new Map(), byClient: new WeakMap() }\n}\n\n/**\n * Derived query identity names each source by its Collection ID. While a hook\n * is mounted, an ID must keep naming one source object in each client scope,\n * or the hook could reuse a live query that reads the old source. Claims the\n * prepared value's sources, or calls `release` and throws on a collision so a\n * rejected render keeps no sync-start deferral.\n */\nexport function claimSourceIds(\n bindings: SourceIdBindings,\n preparedValue: unknown,\n client: DbClient | undefined,\n hookName: string,\n release: () => void,\n): void {\n const prior = client ? bindings.byClient.get(client) : bindings.unscoped\n const seen = new Map<string, number>()\n for (const source of getPreparedSources(preparedValue, client, hookName)) {\n const token = getSourceObjectToken(source)\n const previous = seen.get(source.id) ?? prior?.get(source.id)\n if (previous !== undefined && previous !== token) {\n release()\n throw new Error(\n `[${hookName}] Source Collection \"${source.id}\" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,\n )\n }\n seen.set(source.id, token)\n }\n const claimed = prior ?? new Map<string, number>()\n for (const [id, token] of seen) claimed.set(id, token)\n if (client) bindings.byClient.set(client, claimed)\n}\n"],"names":[],"mappings":";;AAKA,MAAM,yCAAyB,QAAA;AAC/B,IAAI,wBAAwB;AAErB,SAAS,qBAAqB,QAAwB;AAC3D,MAAI,KAAK,mBAAmB,IAAI,MAAM;AACtC,MAAI,OAAO,QAAW;AACpB,SAAK,EAAE;AACP,uBAAmB,IAAI,QAAQ,EAAE;AAAA,EACnC;AACA,SAAO;AACT;AAEO,SAAS,mBACd,eACA,QACA,UACuB;AACvB,SAAO;AAAA,IACL;AAAA,IACA,CAAC,UAAU,WACP,CAAC,UAAU;AACT,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,4BAA4B,KAAK;AAAA,MAAA;AAAA,IAEjD,IACA;AAAA,EAAA;AAER;AAQO,SAAS,yBAA2C;AACzD,SAAO,EAAE,UAAU,oBAAI,IAAA,GAAO,UAAU,oBAAI,UAAQ;AACtD;AASO,SAAS,eACd,UACA,eACA,QACA,UACA,SACM;AACN,QAAM,QAAQ,SAAS,SAAS,SAAS,IAAI,MAAM,IAAI,SAAS;AAChE,QAAM,2BAAW,IAAA;AACjB,aAAW,UAAU,mBAAmB,eAAe,QAAQ,QAAQ,GAAG;AACxE,UAAM,QAAQ,qBAAqB,MAAM;AACzC,UAAM,WAAW,KAAK,IAAI,OAAO,EAAE,KAAK,OAAO,IAAI,OAAO,EAAE;AAC5D,QAAI,aAAa,UAAa,aAAa,OAAO;AAChD,cAAA;AACA,YAAM,IAAI;AAAA,QACR,IAAI,QAAQ,wBAAwB,OAAO,EAAE;AAAA,MAAA;AAAA,IAEjD;AACA,SAAK,IAAI,OAAO,IAAI,KAAK;AAAA,EAC3B;AACA,QAAM,UAAU,SAAS,oBAAI,IAAA;AAC7B,aAAW,CAAC,IAAI,KAAK,KAAK,KAAM,SAAQ,IAAI,IAAI,KAAK;AACrD,MAAI,OAAQ,UAAS,SAAS,IAAI,QAAQ,OAAO;AACnD;"}
@@ -1,8 +1,8 @@
1
1
  "use client";
2
2
  import { useRef, useCallback, useSyncExternalStore } from "react";
3
- import { normalizeLiveQueryWindowPageSize, getLiveQueryWindowInputKind, compareLiveQueryWindowDependencies, shouldPreserveLiveQueryWindowPageCount, liveQueryWindowMatches, resolveLiveQueryWindowInput, createLiveQueryCollection, getLiveQueryWindowCollectionWarning, assertLiveQueryWindowManyResult, createLiveQueryWindowController, fetchNextLiveQueryWindowPage } from "@tanstack/db";
3
+ import { normalizeLiveQueryWindowPageSize, getLiveQueryWindowInputKind, compareLiveQueryWindowDependencies, shouldPreserveLiveQueryWindowPageCount, liveQueryWindowMatches, resolveLiveQueryWindowInput, createLiveQueryCollection, getLiveQueryWindowCollectionWarning, assertLiveQueryWindowManyResult, createLiveQueryWindowController, resumeDeferredLiveQueryCollections, fetchNextLiveQueryWindowPage } from "@tanstack/db";
4
4
  import { useOptionalDbClient } from "./DbProvider.js";
5
- import { claimSourceIds, createSourceIdBindings, resumeSyncStarts } from "./source-id-bindings.js";
5
+ import { claimSourceIds, createSourceIdBindings } from "./source-id-bindings.js";
6
6
  import { warnDeprecatedDepsArray, prepareDerivedQuery, warnUnhashableDerivedIdentity, prepareQueryValue } from "./useLiveQuery.js";
7
7
  const DEFAULT_GC_TIME_MS = 1;
8
8
  const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
@@ -63,7 +63,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
63
63
  preparation.value,
64
64
  dbClient,
65
65
  `useLiveInfiniteQuery`,
66
- () => resumeSyncStarts(deferredCollections)
66
+ () => resumeDeferredLiveQueryCollections(deferredCollections)
67
67
  );
68
68
  if (preparation.status === `hashable`) {
69
69
  identityDeps = preparation.identityDeps;
@@ -194,7 +194,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
194
194
  currentRenderState.warned = true;
195
195
  console.warn(currentRenderState.warning);
196
196
  }
197
- resumeSyncStarts(currentRenderState.deferredCollections);
197
+ resumeDeferredLiveQueryCollections(currentRenderState.deferredCollections);
198
198
  return unsubscribe;
199
199
  },
200
200
  [controller, currentRenderState]
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n assertLiveQueryWindowManyResult,\n compareLiveQueryWindowDependencies,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n fetchNextLiveQueryWindowPage,\n getLiveQueryWindowCollectionWarning,\n getLiveQueryWindowInputKind,\n liveQueryWindowMatches,\n normalizeLiveQueryWindowPageSize,\n resolveLiveQueryWindowInput,\n shouldPreserveLiveQueryWindowPageCount,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport {\n prepareDerivedQuery,\n prepareQueryValue,\n warnDeprecatedDepsArray,\n warnUnhashableDerivedIdentity,\n} from './useLiveQuery'\nimport type { SourceIdBindings } from './source-id-bindings'\nimport type {\n DerivedIdentityProfiler,\n LiveQueryKey,\n useLiveQuery,\n} from './useLiveQuery'\nimport type {\n Collection,\n CollectionImpl as CollectionImplType,\n CollectionStatus,\n Context,\n DbClient,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryPersistedStatus,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\n// Keep the generic parameter for existing typed config wrappers.\nexport type UseLiveInfiniteQueryConfig<_TContext extends Context> = {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n pageSize?: number\n /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = {\n data: Array<TResult>\n state: Map<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & NonSingleResult\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true\n pages: Array<Array<TResult>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\ntype InfiniteQueryRenderState = {\n inputKind: `collection` | `query`\n inputCollection: Collection<any, any, any> | null\n inputQuery: unknown\n client: DbClient | undefined\n identityMode: `collection` | `queryKey` | `legacyDeps` | `derived`\n dependencies: Array<unknown> | null\n pageSize: number\n initialPageParam: number\n collection: Collection<any, any, any>\n controller: LiveQueryWindowController<any, any>\n warning: string | null\n warned: boolean\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n}\n\ntype RenderedWindowCollection = {\n client: DbClient | undefined\n dependencies: Array<unknown>\n collection: Collection<any, any, any>\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n}\n\n/**\n * Whether a recorded query identity matches the current one. Legacy deps\n * compare by reference. Derived identities and query keys compare by structure.\n */\nfunction queryIdentityMatches(\n comparison: { changed: boolean; structurallyEqual: boolean },\n sameClient: boolean,\n usesLegacyDeps: boolean,\n): boolean {\n return (\n sameClient &&\n (usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual)\n )\n}\n\n/**\n * Create an infinite query using a query function with live updates.\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and an optional initial page label\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext> {\n if (`getNextPageParam` in config) {\n throw new Error(\n `getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.`,\n )\n }\n const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize)\n const initialPageParam = config.initialPageParam ?? 0\n const contextDbClient = useOptionalDbClient()\n const dbClient = config.client ?? contextDbClient\n\n const inputIsCollection =\n getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`\n\n const committedRef = useRef<InfiniteQueryRenderState | null>(null)\n const committed = committedRef.current\n // Like useLiveQuery's instance memo, keep the window collection built by the\n // latest render. A second render before commit, such as a StrictMode double\n // render, reuses it instead of starting another one. Only the collection is\n // kept. Each render still builds its own controller and page count from the\n // committed state, which an uncommitted render never changes.\n const renderedCollectionRef = useRef<RenderedWindowCollection | null>(null)\n const sourceIdsRef = useRef<SourceIdBindings | null>(null)\n const inputKind = inputIsCollection ? `collection` : `query`\n const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const legacyUnhashableIdentityRef = useRef<Array<unknown>>([\n `legacy-unhashable`,\n ])\n const deferredCollections = new Set<\n CollectionImplType<any, string | number, any, any, any>\n >()\n\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown> = []\n let identityMode: InfiniteQueryRenderState[`identityMode`] = `collection`\n\n if (!inputIsCollection) {\n if (config.queryKey !== undefined) {\n identityMode = `queryKey`\n identityDeps = config.queryKey\n } else if (deps !== undefined) {\n identityMode = `legacyDeps`\n identityDeps = deps\n warnDeprecatedDepsArray(`useLiveInfiniteQuery`)\n } else if (\n committed?.identityMode === `derived` &&\n committed.inputQuery === queryFnOrCollection &&\n committed.client === dbClient\n ) {\n identityMode = `derived`\n identityDeps = committed.dependencies ?? []\n } else {\n identityMode = `derived`\n const preparation = prepareDerivedQuery(\n queryFnOrCollection,\n dbClient,\n derivedIdentityProfilerRef.current,\n deferredCollections,\n )\n preparedQueryValue = preparation.value\n claimSourceIds(\n (sourceIdsRef.current ??= createSourceIdBindings()),\n preparation.value,\n dbClient,\n `useLiveInfiniteQuery`,\n () => resumeSyncStarts(deferredCollections),\n )\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = legacyUnhashableIdentityRef.current\n }\n }\n }\n\n const usesLegacyDeps =\n !inputIsCollection && config.queryKey === undefined && deps !== undefined\n const dependencyComparison = compareLiveQueryWindowDependencies(\n committed?.dependencies,\n identityDeps,\n )\n const sameClient = committed?.client === dbClient\n const dependenciesChanged =\n !inputIsCollection &&\n !queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps)\n const dependenciesStructurallyEqual =\n usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual\n const needsNewCollection =\n committed === null ||\n committed.inputKind !== inputKind ||\n (inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n committed === null ||\n committed.pageSize !== pageSize ||\n committed.initialPageParam !== initialPageParam\n const needsNewController =\n committed === null || needsNewCollection || pageShapeChanged\n\n let renderState = committed\n if (needsNewController) {\n let collection = committed?.collection\n let warning: string | null = null\n let startInRender = false\n let suppliedCollection = false\n let createdCollection = false\n\n const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({\n hasPreviousController: committed !== null,\n previousInputKind: committed?.inputKind,\n inputKind,\n sameCollection:\n inputIsCollection && committed?.inputCollection === queryFnOrCollection,\n dependenciesChanged,\n dependenciesStructurallyEqual,\n pageShapeChanged,\n })\n const previousPageCount = committed\n ? Math.max(1, committed.controller.getSnapshot().pages.length)\n : 1\n const initialPageCount = canPreservePageCount ? previousPageCount : 1\n // The peek-ahead window for every retained page.\n const requiredLimit = initialPageCount * pageSize + 1\n\n if (needsNewCollection) {\n let inputValue = queryFnOrCollection\n if (!inputIsCollection) {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n queryFnOrCollection,\n dbClient,\n deferredCollections,\n )\n }\n inputValue = () => preparedQueryValue\n }\n const rendered = renderedCollectionRef.current\n if (\n !inputIsCollection &&\n rendered !== null &&\n queryIdentityMatches(\n compareLiveQueryWindowDependencies(\n rendered.dependencies,\n identityDeps,\n ),\n rendered.client === dbClient,\n usesLegacyDeps,\n ) &&\n rendered.collection.status !== `cleaned-up` &&\n rendered.collection.status !== `error` &&\n liveQueryWindowMatches(rendered.collection, requiredLimit)\n ) {\n // An earlier render built this collection and nothing has committed\n // since. Its window is exactly the retained pages, so its first\n // rows are correct. Sources it deferred still resume at commit. A\n // collection in terminal error, such as after its source restarted,\n // cannot serve the query again, so a new one replaces it.\n collection = rendered.collection\n for (const deferred of rendered.deferredCollections) {\n deferredCollections.add(deferred)\n }\n } else {\n const input = resolveLiveQueryWindowInput<TContext>(inputValue)\n if (input.kind === `collection`) {\n collection = input.collection\n suppliedCollection = true\n // A supplied collection is never reused, so release the last one.\n renderedCollectionRef.current = null\n } else {\n // Wrap the query with the peek-ahead window for every retained page,\n // so a collection that starts syncing now never publishes fewer rows\n // than the controller's pages. The controller grows the limit via\n // setWindow.\n collection = createLiveQueryCollection({\n query: input.query.limit(requiredLimit).offset(0),\n gcTime: DEFAULT_GC_TIME_MS,\n })\n startInRender = true\n createdCollection = true\n }\n }\n }\n\n if (!collection) {\n throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`)\n }\n\n if (inputIsCollection) {\n warning =\n getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null\n } else {\n assertLiveQueryWindowManyResult(collection)\n }\n // Like useLiveQuery, start sync during render once the input is valid. A\n // render reads only rows the sources already hold, such as a preloaded\n // collection's, and sends no request until the subscription commits. GC\n // reclaims a render that never commits. A supplied window that the\n // controller must still adjust waits for the subscription.\n if (suppliedCollection) {\n startInRender = liveQueryWindowMatches(collection, requiredLimit)\n }\n if (startInRender) collection.startSyncImmediate()\n // Keep a created collection for the next render only once it validated and\n // started, so a retry after a startup error builds a new one and throws.\n if (createdCollection) {\n renderedCollectionRef.current = {\n client: dbClient,\n dependencies: [...identityDeps],\n collection,\n deferredCollections,\n }\n }\n\n renderState = {\n inputKind,\n inputCollection: inputIsCollection ? collection : null,\n inputQuery: inputIsCollection ? null : queryFnOrCollection,\n client: dbClient,\n identityMode,\n dependencies: inputIsCollection ? null : [...identityDeps],\n pageSize,\n initialPageParam,\n collection,\n controller: createLiveQueryWindowController(collection, {\n pageSize,\n initialPageParam,\n initialPageCount,\n }),\n warning,\n warned: false,\n deferredCollections,\n }\n }\n const currentRenderState = renderState!\n const controller = currentRenderState.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = currentRenderState\n if (currentRenderState.warning && !currentRenderState.warned) {\n currentRenderState.warned = true\n console.warn(currentRenderState.warning)\n }\n resumeSyncStarts(currentRenderState.deferredCollections)\n return unsubscribe\n },\n [controller, currentRenderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n get state() {\n return snapshot.state as EnabledLiveQueryReturn<TContext>[`state`]\n },\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n persistedStatus: snapshot.persistedStatus,\n isPersistedReady: snapshot.isPersistedReady,\n persistedError: snapshot.persistedError,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled: snapshot.isEnabled,\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":[],"mappings":";;;;;;AAiDA;AACA;AA2FA;AAKE;AAIF;AAgCO;AAKL;AACE;AAAU;AACR;AAAA;AAGJ;AACA;AACA;AACA;AAEA;AAGA;AACA;AAMA;AACA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAA2D;AACzD;AAEF;AAIA;AAEA;AACA;AAEA;AACE;AACE;AACA;AAAsB;AAEtB;AACA;AACA;AAA8C;AAM9C;AACA;AAAyC;AAEzC;AACA;AAAoB;AAClB;AACA;AAC2B;AAC3B;AAEF;AACA;AAAA;AAC4B;AACd;AACZ;AACA;AAC0C;AAE5C;AACE;AAA2B;AAE3B;AACA;AAA2C;AAC7C;AACF;AAGF;AAEA;AAA6B;AAChB;AACX;AAEF;AACA;AAGA;AAEA;AAKA;AAIA;AAGA;AACA;AACE;AACA;AACA;AACA;AACA;AAEA;AAAoE;AAC7B;AACP;AAC9B;AAEsD;AACtD;AACA;AACA;AAEF;AAGA;AAEA;AAEA;AACE;AACA;AACE;AACE;AAAqB;AACnB;AACA;AACA;AAAA;AAGJ;AAAmB;AAErB;AACA;AAGE;AACE;AACW;AACT;AAAA;AAEkB;AACpB;AAWF;AACA;AACE;AAAgC;AAClC;AAEA;AACA;AACE;AACA;AAEA;AAAgC;AAMhC;AAAuC;AACW;AACxC;AAEV;AACA;AAAoB;AACtB;AACF;AAGF;AACE;AAAsE;AAGxE;AACE;AACmE;AAEnE;AAA0C;AAO5C;AACE;AAAgE;AAElE;AAGA;AACE;AAAgC;AACtB;AACsB;AAC9B;AACA;AAAA;AAIJ;AAAc;AACZ;AACkD;AACX;AAC/B;AACR;AACyD;AACzD;AACA;AACA;AACwD;AACtD;AACA;AACA;AACD;AACD;AACQ;AACR;AAAA;AAGJ;AACA;AAEA;AAAkB;AAEd;AACA;AACA;AACE;AACA;AAAuC;AAEzC;AACA;AAAO;AACT;AAC+B;AAEjC;AACA;AAEA;AAAsB;AACyB;AAClC;AAGb;AAAO;AACU;AAEb;AAAgB;AAClB;AACiB;AACG;AACF;AACQ;AACC;AACF;AACR;AACC;AACI;AAEX;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACb;AAEpB;;;;"}
1
+ {"version":3,"file":"useLiveInfiniteQuery.js","sources":["../../src/useLiveInfiniteQuery.ts"],"sourcesContent":["'use client'\n\nimport { useCallback, useRef, useSyncExternalStore } from 'react'\nimport {\n assertLiveQueryWindowManyResult,\n compareLiveQueryWindowDependencies,\n createLiveQueryCollection,\n createLiveQueryWindowController,\n fetchNextLiveQueryWindowPage,\n getLiveQueryWindowCollectionWarning,\n getLiveQueryWindowInputKind,\n liveQueryWindowMatches,\n normalizeLiveQueryWindowPageSize,\n resolveLiveQueryWindowInput,\n shouldPreserveLiveQueryWindowPageCount,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport {\n prepareDerivedQuery,\n prepareQueryValue,\n warnDeprecatedDepsArray,\n warnUnhashableDerivedIdentity,\n} from './useLiveQuery'\nimport type { SourceIdBindings } from './source-id-bindings'\nimport type {\n DerivedIdentityProfiler,\n LiveQueryKey,\n useLiveQuery,\n} from './useLiveQuery'\nimport type {\n Collection,\n CollectionImpl as CollectionImplType,\n CollectionStatus,\n Context,\n DbClient,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryPersistedStatus,\n LiveQueryWindowController,\n NonSingleResult,\n QueryBuilder,\n} from '@tanstack/db'\n\n// Live queries created here are cleaned up immediately (0 disables GC).\nconst DEFAULT_GC_TIME_MS = 1\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\n// Keep the generic parameter for existing typed config wrappers.\nexport type UseLiveInfiniteQueryConfig<_TContext extends Context> = {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n pageSize?: number\n /** First result-page label, not a server cursor or remote offset. */\n initialPageParam?: number\n}\n\nexport type UseLiveInfiniteQueryReturn<TContext extends Context> = Omit<\n ReturnType<typeof useLiveQuery<TContext>>,\n `data`\n> & {\n data: InferResultType<TContext>\n pages: Array<Array<InferResultType<TContext>[number]>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\nexport type UseLiveInfiniteQueryReturnWithCollection<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n> = {\n data: Array<TResult>\n state: Map<TKey, TResult>\n collection: Collection<TResult, TKey, TUtils> & NonSingleResult\n status: CollectionStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true\n pages: Array<Array<TResult>>\n pageParams: Array<number>\n fetchNextPage: () => Promise<void>\n hasNextPage: boolean\n isFetchingNextPage: boolean\n error: unknown\n}\n\ntype EnabledLiveQueryReturn<TContext extends Context> = ReturnType<\n typeof useLiveQuery<TContext>\n>\n\ntype InfiniteQueryRenderState = {\n inputKind: `collection` | `query`\n inputCollection: Collection<any, any, any> | null\n inputQuery: unknown\n client: DbClient | undefined\n identityMode: `collection` | `queryKey` | `legacyDeps` | `derived`\n dependencies: Array<unknown> | null\n pageSize: number\n initialPageParam: number\n collection: Collection<any, any, any>\n controller: LiveQueryWindowController<any, any>\n warning: string | null\n warned: boolean\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n}\n\ntype RenderedWindowCollection = {\n client: DbClient | undefined\n dependencies: Array<unknown>\n collection: Collection<any, any, any>\n deferredCollections: Set<\n CollectionImplType<any, string | number, any, any, any>\n >\n}\n\n/**\n * Whether a recorded query identity matches the current one. Legacy deps\n * compare by reference. Derived identities and query keys compare by structure.\n */\nfunction queryIdentityMatches(\n comparison: { changed: boolean; structurallyEqual: boolean },\n sameClient: boolean,\n usesLegacyDeps: boolean,\n): boolean {\n return (\n sameClient &&\n (usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual)\n )\n}\n\n/**\n * Create an infinite query using a query function with live updates.\n *\n * Uses `utils.setWindow()` to dynamically adjust the limit/offset window\n * without recreating the live query collection on each page change.\n *\n * @param queryFn - Query function that defines what data to fetch. Must include `.orderBy()` for setWindow to work.\n * @param config - Configuration including pageSize and an optional initial page label\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with pages, data, and pagination controls\n */\n\n// Overload for pre-created collection (non-single result)\nexport function useLiveInfiniteQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n config: UseLiveInfiniteQueryConfig<any>,\n): UseLiveInfiniteQueryReturnWithCollection<TResult, TKey, TUtils>\n\n// Overload for query function\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext>\n\n// Implementation\nexport function useLiveInfiniteQuery<TContext extends Context>(\n queryFnOrCollection: any,\n config: UseLiveInfiniteQueryConfig<TContext>,\n deps?: Array<unknown>,\n): UseLiveInfiniteQueryReturn<TContext> {\n if (`getNextPageParam` in config) {\n throw new Error(\n `getNextPageParam is not supported by useLiveInfiniteQuery. Use an on-demand collection and fulfill meta.loadSubsetOptions in queryFn for server pagination.`,\n )\n }\n const pageSize = normalizeLiveQueryWindowPageSize(config.pageSize)\n const initialPageParam = config.initialPageParam ?? 0\n const contextDbClient = useOptionalDbClient()\n const dbClient = config.client ?? contextDbClient\n\n const inputIsCollection =\n getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`\n\n const committedRef = useRef<InfiniteQueryRenderState | null>(null)\n const committed = committedRef.current\n // Like useLiveQuery's instance memo, keep the window collection built by the\n // latest render. A second render before commit, such as a StrictMode double\n // render, reuses it instead of starting another one. Only the collection is\n // kept. Each render still builds its own controller and page count from the\n // committed state, which an uncommitted render never changes.\n const renderedCollectionRef = useRef<RenderedWindowCollection | null>(null)\n const sourceIdsRef = useRef<SourceIdBindings | null>(null)\n const inputKind = inputIsCollection ? `collection` : `query`\n const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const legacyUnhashableIdentityRef = useRef<Array<unknown>>([\n `legacy-unhashable`,\n ])\n const deferredCollections = new Set<\n CollectionImplType<any, string | number, any, any, any>\n >()\n\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown> = []\n let identityMode: InfiniteQueryRenderState[`identityMode`] = `collection`\n\n if (!inputIsCollection) {\n if (config.queryKey !== undefined) {\n identityMode = `queryKey`\n identityDeps = config.queryKey\n } else if (deps !== undefined) {\n identityMode = `legacyDeps`\n identityDeps = deps\n warnDeprecatedDepsArray(`useLiveInfiniteQuery`)\n } else if (\n committed?.identityMode === `derived` &&\n committed.inputQuery === queryFnOrCollection &&\n committed.client === dbClient\n ) {\n identityMode = `derived`\n identityDeps = committed.dependencies ?? []\n } else {\n identityMode = `derived`\n const preparation = prepareDerivedQuery(\n queryFnOrCollection,\n dbClient,\n derivedIdentityProfilerRef.current,\n deferredCollections,\n )\n preparedQueryValue = preparation.value\n claimSourceIds(\n (sourceIdsRef.current ??= createSourceIdBindings()),\n preparation.value,\n dbClient,\n `useLiveInfiniteQuery`,\n () => resumeSyncStarts(deferredCollections),\n )\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = legacyUnhashableIdentityRef.current\n }\n }\n }\n\n const usesLegacyDeps =\n !inputIsCollection && config.queryKey === undefined && deps !== undefined\n const dependencyComparison = compareLiveQueryWindowDependencies(\n committed?.dependencies,\n identityDeps,\n )\n const sameClient = committed?.client === dbClient\n const dependenciesChanged =\n !inputIsCollection &&\n !queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps)\n const dependenciesStructurallyEqual =\n usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual\n const needsNewCollection =\n committed === null ||\n committed.inputKind !== inputKind ||\n (inputIsCollection && committed.inputCollection !== queryFnOrCollection) ||\n dependenciesChanged\n const pageShapeChanged =\n committed === null ||\n committed.pageSize !== pageSize ||\n committed.initialPageParam !== initialPageParam\n const needsNewController =\n committed === null || needsNewCollection || pageShapeChanged\n\n let renderState = committed\n if (needsNewController) {\n let collection = committed?.collection\n let warning: string | null = null\n let startInRender = false\n let suppliedCollection = false\n let createdCollection = false\n\n const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({\n hasPreviousController: committed !== null,\n previousInputKind: committed?.inputKind,\n inputKind,\n sameCollection:\n inputIsCollection && committed?.inputCollection === queryFnOrCollection,\n dependenciesChanged,\n dependenciesStructurallyEqual,\n pageShapeChanged,\n })\n const previousPageCount = committed\n ? Math.max(1, committed.controller.getSnapshot().pages.length)\n : 1\n const initialPageCount = canPreservePageCount ? previousPageCount : 1\n // The peek-ahead window for every retained page.\n const requiredLimit = initialPageCount * pageSize + 1\n\n if (needsNewCollection) {\n let inputValue = queryFnOrCollection\n if (!inputIsCollection) {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n queryFnOrCollection,\n dbClient,\n deferredCollections,\n )\n }\n inputValue = () => preparedQueryValue\n }\n const rendered = renderedCollectionRef.current\n if (\n !inputIsCollection &&\n rendered !== null &&\n queryIdentityMatches(\n compareLiveQueryWindowDependencies(\n rendered.dependencies,\n identityDeps,\n ),\n rendered.client === dbClient,\n usesLegacyDeps,\n ) &&\n rendered.collection.status !== `cleaned-up` &&\n rendered.collection.status !== `error` &&\n liveQueryWindowMatches(rendered.collection, requiredLimit)\n ) {\n // An earlier render built this collection and nothing has committed\n // since. Its window is exactly the retained pages, so its first\n // rows are correct. Sources it deferred still resume at commit. A\n // collection in terminal error, such as after its source restarted,\n // cannot serve the query again, so a new one replaces it.\n collection = rendered.collection\n for (const deferred of rendered.deferredCollections) {\n deferredCollections.add(deferred)\n }\n } else {\n const input = resolveLiveQueryWindowInput<TContext>(inputValue)\n if (input.kind === `collection`) {\n collection = input.collection\n suppliedCollection = true\n // A supplied collection is never reused, so release the last one.\n renderedCollectionRef.current = null\n } else {\n // Wrap the query with the peek-ahead window for every retained page,\n // so a collection that starts syncing now never publishes fewer rows\n // than the controller's pages. The controller grows the limit via\n // setWindow.\n collection = createLiveQueryCollection({\n query: input.query.limit(requiredLimit).offset(0),\n gcTime: DEFAULT_GC_TIME_MS,\n })\n startInRender = true\n createdCollection = true\n }\n }\n }\n\n if (!collection) {\n throw new Error(`useLiveInfiniteQuery: Failed to create a collection.`)\n }\n\n if (inputIsCollection) {\n warning =\n getLiveQueryWindowCollectionWarning(collection, pageSize + 1) ?? null\n } else {\n assertLiveQueryWindowManyResult(collection)\n }\n // Like useLiveQuery, start sync during render once the input is valid. A\n // render reads only rows the sources already hold, such as a preloaded\n // collection's, and sends no request until the subscription commits. GC\n // reclaims a render that never commits. A supplied window that the\n // controller must still adjust waits for the subscription.\n if (suppliedCollection) {\n startInRender = liveQueryWindowMatches(collection, requiredLimit)\n }\n if (startInRender) collection.startSyncImmediate()\n // Keep a created collection for the next render only once it validated and\n // started, so a retry after a startup error builds a new one and throws.\n if (createdCollection) {\n renderedCollectionRef.current = {\n client: dbClient,\n dependencies: [...identityDeps],\n collection,\n deferredCollections,\n }\n }\n\n renderState = {\n inputKind,\n inputCollection: inputIsCollection ? collection : null,\n inputQuery: inputIsCollection ? null : queryFnOrCollection,\n client: dbClient,\n identityMode,\n dependencies: inputIsCollection ? null : [...identityDeps],\n pageSize,\n initialPageParam,\n collection,\n controller: createLiveQueryWindowController(collection, {\n pageSize,\n initialPageParam,\n initialPageCount,\n }),\n warning,\n warned: false,\n deferredCollections,\n }\n }\n const currentRenderState = renderState!\n const controller = currentRenderState.controller\n\n const subscribe = useCallback(\n (onStoreChange: () => void) => {\n const unsubscribe = controller.subscribe(onStoreChange)\n committedRef.current = currentRenderState\n if (currentRenderState.warning && !currentRenderState.warned) {\n currentRenderState.warned = true\n console.warn(currentRenderState.warning)\n }\n resumeSyncStarts(currentRenderState.deferredCollections)\n return unsubscribe\n },\n [controller, currentRenderState],\n )\n const getSnapshot = useCallback(() => controller.getSnapshot(), [controller])\n const snapshot = useSyncExternalStore(subscribe, getSnapshot, getSnapshot)\n\n const fetchNextPage = useCallback(\n () => fetchNextLiveQueryWindowPage(controller),\n [controller],\n )\n\n return {\n data: snapshot.data as InferResultType<TContext>,\n get state() {\n return snapshot.state as EnabledLiveQueryReturn<TContext>[`state`]\n },\n status: snapshot.status as EnabledLiveQueryReturn<TContext>[`status`],\n isLoading: snapshot.isLoading,\n isReady: snapshot.isReady,\n persistedStatus: snapshot.persistedStatus,\n isPersistedReady: snapshot.isPersistedReady,\n persistedError: snapshot.persistedError,\n isIdle: snapshot.isIdle,\n isError: snapshot.isError,\n isCleanedUp: snapshot.isCleanedUp,\n collection:\n snapshot.collection as EnabledLiveQueryReturn<TContext>[`collection`],\n isEnabled: snapshot.isEnabled,\n pages: snapshot.pages as Array<Array<InferResultType<TContext>[number]>>,\n pageParams: snapshot.pageParams as Array<number>,\n fetchNextPage,\n hasNextPage: snapshot.hasNextPage,\n isFetchingNextPage: snapshot.isFetchingNextPage,\n error: snapshot.error,\n }\n}\n"],"names":["resumeSyncStarts"],"mappings":";;;;;;AAiDA;AACA;AA2FA;AAKE;AAIF;AAgCO;AAKL;AACE;AAAU;AACR;AAAA;AAGJ;AACA;AACA;AACA;AAEA;AAGA;AACA;AAMA;AACA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAA2D;AACzD;AAEF;AAIA;AAEA;AACA;AAEA;AACE;AACE;AACA;AAAsB;AAEtB;AACA;AACA;AAA8C;AAM9C;AACA;AAAyC;AAEzC;AACA;AAAoB;AAClB;AACA;AAC2B;AAC3B;AAEF;AACA;AAAA;AAC4B;AACd;AACZ;AACA;AAC0C;AAE5C;AACE;AAA2B;AAE3B;AACA;AAA2C;AAC7C;AACF;AAGF;AAEA;AAA6B;AAChB;AACX;AAEF;AACA;AAGA;AAEA;AAKA;AAIA;AAGA;AACA;AACE;AACA;AACA;AACA;AACA;AAEA;AAAoE;AAC7B;AACP;AAC9B;AAEsD;AACtD;AACA;AACA;AAEF;AAGA;AAEA;AAEA;AACE;AACA;AACE;AACE;AAAqB;AACnB;AACA;AACA;AAAA;AAGJ;AAAmB;AAErB;AACA;AAGE;AACE;AACW;AACT;AAAA;AAEkB;AACpB;AAWF;AACA;AACE;AAAgC;AAClC;AAEA;AACA;AACE;AACA;AAEA;AAAgC;AAMhC;AAAuC;AACW;AACxC;AAEV;AACA;AAAoB;AACtB;AACF;AAGF;AACE;AAAsE;AAGxE;AACE;AACmE;AAEnE;AAA0C;AAO5C;AACE;AAAgE;AAElE;AAGA;AACE;AAAgC;AACtB;AACsB;AAC9B;AACA;AAAA;AAIJ;AAAc;AACZ;AACkD;AACX;AAC/B;AACR;AACyD;AACzD;AACA;AACA;AACwD;AACtD;AACA;AACA;AACD;AACD;AACQ;AACR;AAAA;AAGJ;AACA;AAEA;AAAkB;AAEd;AACA;AACA;AACE;AACA;AAAuC;AAEzCA;AACA;AAAO;AACT;AAC+B;AAEjC;AACA;AAEA;AAAsB;AACyB;AAClC;AAGb;AAAO;AACU;AAEb;AAAgB;AAClB;AACiB;AACG;AACF;AACQ;AACC;AACF;AACR;AACC;AACI;AAEX;AACS;AACJ;AACK;AACrB;AACsB;AACO;AACb;AAEpB;;;;"}
@@ -1,10 +1,10 @@
1
1
  "use client";
2
2
  import { useRef, useSyncExternalStore } from "react";
3
- import { getPreparedLiveQueryIdentity, UnhashableQueryIRError, prepareLiveQueryValue, isCollection, getStableValueHash, deepEquals, resolveLiveQueryValue, createLiveQueryObserver } from "@tanstack/db";
3
+ import { getPreparedLiveQueryIdentity, UnhashableQueryIRError, prepareLiveQueryValue, isCollection, getStableValueHash, deepEquals, resolveLiveQueryValue, createLiveQueryObserver, resumeDeferredLiveQueryCollections } from "@tanstack/db";
4
4
  import { useOptionalDbClient } from "./DbProvider.js";
5
5
  import { setLiveQueryResultInfo } from "./live-query-internals.js";
6
6
  import { shouldWarnInDevelopment } from "./development.js";
7
- import { claimSourceIds, createSourceIdBindings, resumeSyncStarts, getPreparedSources, getSourceObjectToken } from "./source-id-bindings.js";
7
+ import { claimSourceIds, getPreparedSources, createSourceIdBindings, getSourceObjectToken } from "./source-id-bindings.js";
8
8
  const DEFAULT_GC_TIME_MS = 1;
9
9
  const DEFAULT_SUSPENSE_GC_TIME_MS = 5e3;
10
10
  const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16;
@@ -16,8 +16,12 @@ const warnedUnhashableIdentityCallsites = /* @__PURE__ */ new Set();
16
16
  const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
17
17
  const unscopedSuspenseCollections = /* @__PURE__ */ new Map();
18
18
  const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap();
19
- function getSourceQualifiedSuspenseKey(preparedValue, queryHash) {
20
- const sourceIds = getPreparedSources(preparedValue).map(getSourceObjectToken);
19
+ function getSourceQualifiedSuspenseKey(preparedValue, queryHash, client) {
20
+ const sourceIds = getPreparedSources(
21
+ preparedValue,
22
+ client,
23
+ `useLiveSuspenseQuery`
24
+ ).map(getSourceObjectToken);
21
25
  if (sourceIds.length === 0) return queryHash;
22
26
  return `${sourceIds.join(`,`)}:${queryHash}`;
23
27
  }
@@ -168,7 +172,7 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
168
172
  const resolvedDeps = deps ?? [];
169
173
  const instanceRef = useRef(null);
170
174
  const instance = instanceRef.current ??= createHookInstance(dbClient);
171
- const resumeDeferredCollections = () => resumeSyncStarts(instance.deferredCollections);
175
+ const resumeDeferredCollections = () => resumeDeferredLiveQueryCollections(instance.deferredCollections);
172
176
  const queryKey = !inputIsCollection ? getExplicitQueryKey(configOrQueryOrCollection) : void 0;
173
177
  let preparedQueryValue = unpreparedQueryValue;
174
178
  let identityDeps;
@@ -249,7 +253,7 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
249
253
  instance.deferredCollections
250
254
  );
251
255
  }
252
- const suspenseKey = queryHash && forSuspense && !inputIsCollection ? canReuseSuspenseKey ? instance.suspenseKey : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash) : queryHash;
256
+ const suspenseKey = queryHash && forSuspense && !inputIsCollection ? canReuseSuspenseKey ? instance.suspenseKey : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient) : queryHash;
253
257
  const suspenseCollections = forSuspense && !inputIsCollection && suspenseKey ? getSuspenseCollections(dbClient) : void 0;
254
258
  const suspenseEntry = suspenseKey ? suspenseCollections?.get(suspenseKey) : void 0;
255
259
  const suspenseCollection = suspenseEntry?.collection;
@@ -282,6 +286,9 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
282
286
  instance.deferredCollections
283
287
  );
284
288
  }
289
+ if (!dbClient) {
290
+ getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`);
291
+ }
285
292
  instance.collection = resolveLiveQueryValue(preparedQueryValue, {
286
293
  gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS,
287
294
  // Hydration and Suspense key the live-query Collection by identity.
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef, useSyncExternalStore } from 'react'\nimport {\n UnhashableQueryIRError,\n createLiveQueryObserver,\n deepEquals,\n getPreparedLiveQueryIdentity,\n getStableValueHash,\n isCollection,\n prepareLiveQueryValue,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { setLiveQueryResultInfo } from './live-query-internals'\nimport { shouldWarnInDevelopment } from './development'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n getPreparedSources,\n getSourceObjectToken,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport type {\n Collection,\n CollectionImpl,\n CollectionStatus,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n// Suspense renders can be abandoned before React subscribes. Keep their\n// collection briefly so a nearby retry can commit without starting over.\nconst DEFAULT_SUSPENSE_GC_TIME_MS = 5000\nconst DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16\nconst DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10\nconst DERIVED_IDENTITY_TOTAL_WARN_MS = 50\nconst warnedDepsCallsites = new Set<string>()\nconst warnedDerivedIdentityCallsites = new Set<string>()\nconst warnedUnhashableIdentityCallsites = new Set<string>()\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\ntype SuspenseCollection = Collection<object, string | number, {}>\ntype SuspenseCollectionEntry = {\n collection: SuspenseCollection\n removeStatusListeners: () => void\n}\n\nconst unscopedSuspenseCollections = new Map<string, SuspenseCollectionEntry>()\nconst suspenseCollectionsByClient = new WeakMap<\n DbClient,\n Map<string, SuspenseCollectionEntry>\n>()\nfunction getSourceQualifiedSuspenseKey(\n preparedValue: unknown,\n queryHash: string,\n): string {\n const sourceIds = getPreparedSources(preparedValue).map(getSourceObjectToken)\n if (sourceIds.length === 0) return queryHash\n return `${sourceIds.join(`,`)}:${queryHash}`\n}\n\nfunction getSuspenseCollections(\n client: DbClient | undefined,\n): Map<string, SuspenseCollectionEntry> {\n if (!client) return unscopedSuspenseCollections\n let collections = suspenseCollectionsByClient.get(client)\n if (!collections) {\n collections = new Map()\n suspenseCollectionsByClient.set(client, collections)\n }\n return collections\n}\n\nfunction releaseSuspenseCollection(\n collections: Map<string, SuspenseCollectionEntry>,\n queryHash: string,\n collection: SuspenseCollection,\n): void {\n const entry = collections.get(queryHash)\n if (entry?.collection !== collection) return\n collections.delete(queryHash)\n entry.removeStatusListeners()\n}\n\nexport type DerivedIdentityProfiler = {\n renderCount: number\n totalMs: number\n maxMs: number\n warned: boolean\n}\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\nexport type LiveQueryKey = ReadonlyArray<unknown>\ntype UseLiveQueryConfigOptions<TContext extends Context> = Omit<\n LiveQueryCollectionConfig<TContext>,\n `query`\n> & {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n}\n\ntype ConfiguredQueryBuilder<TContext extends Context> = Extract<\n LiveQueryCollectionConfig<TContext>[`query`],\n QueryBuilder<TContext>\n>\n\nexport type UseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> &\n Pick<LiveQueryCollectionConfig<TContext>, `query`>\n\nexport type ConditionalUseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> & {\n query:\n | ConfiguredQueryBuilder<TContext>\n | ((\n q: InitialQueryBuilder,\n ) => ConfiguredQueryBuilder<TContext> | undefined | null)\n }\n\nexport function warnDeprecatedDepsArray(\n hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedDepsCallsites.has(callsite)) {\n return\n }\n warnedDepsCallsites.add(callsite)\n const replacement =\n hookName === `useLiveQuery`\n ? `useLiveQuery({ query })`\n : `useLiveInfiniteQuery(query, { queryKey })`\n console.warn(\n `[${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.`,\n )\n}\n\nfunction getCurrentTime(): number {\n return typeof performance !== `undefined` &&\n typeof performance.now === `function`\n ? performance.now()\n : Date.now()\n}\n\nfunction getWarningCallsite(stackIndex: number): string {\n const stack = new Error().stack ?? `unknown`\n const lines = stack.split(`\\n`)\n const userFrame = lines\n .slice(1)\n .find(\n (line) =>\n !line.includes(`useLiveQuery.ts`) &&\n !line.includes(`useLiveSuspenseQuery.ts`) &&\n !line.includes(`useLiveInfiniteQuery.ts`),\n )\n return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack\n}\n\nfunction warnDerivedIdentityHotPath(\n profiler: DerivedIdentityProfiler,\n durationMs: number,\n): void {\n if (\n profiler.warned ||\n !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n return\n }\n\n const isSlowSingleRender =\n durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS\n const isHotRenderPath =\n profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&\n profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS\n\n if (!isSlowSingleRender && !isHotRenderPath) {\n return\n }\n\n const callsite = getWarningCallsite(5)\n if (warnedDerivedIdentityCallsites.has(callsite)) {\n profiler.warned = true\n return\n }\n\n warnedDerivedIdentityCallsites.add(callsite)\n profiler.warned = true\n\n const reason = isSlowSingleRender\n ? `one render took ${durationMs.toFixed(1)}ms`\n : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`\n\n console.warn(\n `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +\n `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,\n )\n}\n\nfunction getExplicitQueryKey(value: unknown): LiveQueryKey | undefined {\n return value &&\n typeof value === `object` &&\n Array.isArray((value as { queryKey?: unknown }).queryKey)\n ? (value as { queryKey: LiveQueryKey }).queryKey\n : undefined\n}\n\nfunction getExplicitDbClient(value: unknown): DbClient | undefined {\n return value &&\n typeof value === `object` &&\n `client` in value &&\n (value as { client?: unknown }).client !== undefined\n ? (value as { client: DbClient }).client\n : undefined\n}\n\nexport function prepareQueryValue(\n value: unknown,\n dbClient: DbClient | undefined,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): unknown {\n return prepareLiveQueryValue(value, dbClient, deferredCollections)\n}\n\ntype DerivedQueryPreparation =\n | {\n status: `hashable`\n value: unknown\n identityDeps: Array<unknown>\n }\n | {\n status: `unhashable`\n value: unknown\n error: UnhashableQueryIRError\n }\n\nexport function prepareDerivedQuery(\n value: unknown,\n dbClient: DbClient | undefined,\n profiler: DerivedIdentityProfiler,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): DerivedQueryPreparation {\n const shouldProfile = shouldWarnInDevelopment(\n `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,\n )\n const start = shouldProfile ? getCurrentTime() : 0\n const preparedValue = prepareQueryValue(value, dbClient, deferredCollections)\n\n try {\n const identity = getPreparedLiveQueryIdentity(preparedValue)\n return {\n status: `hashable`,\n value: preparedValue,\n identityDeps: [`derived`, identity],\n }\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n return { status: `unhashable`, value: preparedValue, error }\n }\n\n throw error\n } finally {\n if (shouldProfile) {\n const durationMs = getCurrentTime() - start\n profiler.renderCount += 1\n profiler.totalMs += durationMs\n profiler.maxMs = Math.max(profiler.maxMs, durationMs)\n warnDerivedIdentityHotPath(profiler, durationMs)\n }\n }\n}\n\nexport function warnUnhashableDerivedIdentity(\n error: UnhashableQueryIRError,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedUnhashableIdentityCallsites.has(callsite)) {\n return\n }\n warnedUnhashableIdentityCallsites.add(callsite)\n\n console.warn(\n `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` +\n `It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` +\n `Unhashable queries without queryKey will throw in 1.0.`,\n )\n}\n\n/**\n * Create a live query using a query function.\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Prefer config object syntax\n * const { data, isLoading } = useLiveQuery({\n * query: (q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * })\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * })\n *\n * @example\n * // Structured captured values are included in derived query identity\n * const { data, state } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // Return undefined or null to disable a query\n * const { data, isEnabled } = useLiveQuery({\n * query: (q) => {\n * if (!userId) return undefined\n * return q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.userId, userId))\n * },\n * })\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery({\n * query: (q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * })\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todoCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({\n * query: queryBuilder,\n * })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 7: Accept config object with a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 8: Accept config object with legacy deps\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 9: Accept config object with legacy deps and a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n deps: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 10: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 10: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps?: Array<unknown>,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps)\n}\n\n/** @internal Shared implementation for the Suspense wrapper. */\nexport function useLiveQueryForSuspense(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps, true)\n}\n\n// What one hook instance keeps across renders. It lives in a single ref\n// slot, so a render neither looks up nor allocates more.\nfunction createHookInstance(dbClient: DbClient | undefined) {\n return {\n collection: null as Collection<object, string | number, {}> | null,\n deps: null as Array<unknown> | null,\n config: null as unknown,\n client: dbClient,\n legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,\n derivedIdentityProfiler: {\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n } as DerivedIdentityProfiler,\n deferredCollections: new Set<\n CollectionImpl<any, string | number, any, any, any>\n >(),\n sourceIds: createSourceIdBindings(),\n observer: null as LiveQueryObserver<object, string | number> | null,\n queryHash: undefined as string | undefined,\n suspenseKey: undefined as string | undefined,\n identityError: undefined as UnhashableQueryIRError | undefined,\n subscribe: null as ((onStoreChange: () => void) => () => void) | null,\n }\n}\n\nfunction useLiveQueryImpl(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n forSuspense = false,\n) {\n const contextDbClient = useOptionalDbClient()\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n const dbClient = inputIsCollection\n ? contextDbClient\n : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)\n const resolvedDeps = deps ?? []\n\n const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)\n const instance = (instanceRef.current ??= createHookInstance(dbClient))\n const resumeDeferredCollections = () =>\n resumeSyncStarts(instance.deferredCollections)\n\n const queryKey = !inputIsCollection\n ? getExplicitQueryKey(configOrQueryOrCollection)\n : undefined\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown>\n let streamIdentity: unknown = undefined\n let identityError: UnhashableQueryIRError | undefined\n\n if (queryKey) {\n identityDeps = queryKey\n streamIdentity = [`queryKey`, queryKey]\n } else if (deps !== undefined) {\n identityDeps = resolvedDeps\n // Deps decide reuse. Only hydration and Suspense read the query hash; the\n // development warning about unhashable queries still derives it.\n if (\n dbClient ||\n forSuspense ||\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n )\n try {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n streamIdentity = [\n `deps`,\n resolvedDeps,\n getPreparedLiveQueryIdentity(preparedQueryValue),\n ]\n } catch (error) {\n if (!(error instanceof UnhashableQueryIRError)) throw error\n warnUnhashableDerivedIdentity(error)\n identityError = error\n }\n } else if (inputIsCollection) {\n identityDeps = []\n streamIdentity = [`collection`, configOrQueryOrCollection.id]\n } else {\n const preparation = prepareDerivedQuery(\n configOrQueryOrCollection,\n dbClient,\n instance.derivedIdentityProfiler,\n instance.deferredCollections,\n )\n preparedQueryValue = preparation.value\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n streamIdentity = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = instance.legacyUnhashableIdentity\n identityError = preparation.error\n }\n }\n\n let queryHash: string | undefined\n if (streamIdentity !== undefined) {\n try {\n queryHash = getStableValueHash(streamIdentity, `queryKey`)\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n if (queryKey !== undefined) throw error\n identityError = error\n } else {\n throw error\n }\n }\n }\n\n if (deps !== undefined) {\n warnDeprecatedDepsArray()\n }\n\n if (\n queryKey === undefined &&\n deps === undefined &&\n preparedQueryValue !== unpreparedQueryValue\n ) {\n claimSourceIds(\n instance.sourceIds,\n preparedQueryValue,\n dbClient,\n `useLiveQuery`,\n resumeDeferredCollections,\n )\n }\n\n const canReuseSuspenseKey =\n forSuspense &&\n !inputIsCollection &&\n queryHash !== undefined &&\n instance.collection !== null &&\n instance.client === dbClient &&\n instance.queryHash === queryHash &&\n instance.suspenseKey !== undefined\n\n if (\n forSuspense &&\n !inputIsCollection &&\n queryHash &&\n !canReuseSuspenseKey &&\n preparedQueryValue === unpreparedQueryValue\n ) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n\n const suspenseKey =\n queryHash && forSuspense && !inputIsCollection\n ? canReuseSuspenseKey\n ? instance.suspenseKey\n : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash)\n : queryHash\n\n const suspenseCollections =\n forSuspense && !inputIsCollection && suspenseKey\n ? getSuspenseCollections(dbClient)\n : undefined\n const suspenseEntry = suspenseKey\n ? suspenseCollections?.get(suspenseKey)\n : undefined\n const suspenseCollection = suspenseEntry?.collection\n\n const identityChanged =\n instance.deps === null ||\n (deps !== undefined\n ? instance.deps.length !== identityDeps.length ||\n instance.deps.some((dep, index) => dep !== identityDeps[index])\n : !deepEquals(instance.deps, identityDeps))\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !instance.collection ||\n (inputIsCollection && instance.config !== configOrQueryOrCollection) ||\n (!inputIsCollection && (instance.client !== dbClient || identityChanged))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (\n syncMode === `on-demand` &&\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n instance.collection = configOrQueryOrCollection\n instance.config = configOrQueryOrCollection\n } else {\n if (suspenseCollection) {\n instance.collection = suspenseCollection\n } else {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n instance.collection = resolveLiveQueryValue(preparedQueryValue, {\n gcTime: forSuspense\n ? DEFAULT_SUSPENSE_GC_TIME_MS\n : DEFAULT_GC_TIME_MS,\n // Hydration and Suspense key the live-query Collection by identity.\n pool: !forSuspense && !dbClient,\n // Without a DbClient, Suspense starts it through a preload below.\n startSync: !forSuspense || dbClient !== undefined,\n }) as SuspenseCollection | null\n if (suspenseCollections && suspenseKey && instance.collection) {\n const collection = instance.collection\n const removeCleanupListener = collection.on(`status:cleaned-up`, () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n )\n const removeErrorListener = collection.on(`status:error`, () => {\n // Keep the failed collection through React's immediate retry so\n // the hook can throw its actual load error to an ErrorBoundary.\n // Retire it afterward so a later boundary reset starts fresh.\n setTimeout(\n () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n 0,\n )\n })\n const entry: SuspenseCollectionEntry = {\n collection,\n removeStatusListeners: () => {\n removeCleanupListener()\n removeErrorListener()\n },\n }\n suspenseCollections.set(suspenseKey, entry)\n }\n }\n instance.config = configOrQueryOrCollection\n instance.deps = [...identityDeps]\n }\n instance.client = dbClient\n instance.queryHash = queryHash\n instance.suspenseKey = suspenseKey\n instance.identityError = identityError\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n instance.observer = createLiveQueryObserver(instance.collection, {\n mode: `wholesale`,\n client: dbClient,\n queryHash: instance.queryHash,\n onPreload: resumeDeferredCollections,\n })\n }\n const observer = instance.observer!\n // A Suspense render asks for its data. Preloading starts the sync run, so a\n // source that already holds the rows makes it ready before the snapshot is\n // read instead of suspending once. useLiveSuspenseQuery surfaces failures.\n // A DbClient defers source starts past render and owns its streamed query.\n if (\n forSuspense &&\n !dbClient &&\n (instance.collection?.status === `idle` ||\n instance.collection?.status === `cleaned-up`)\n ) {\n observer.preloadForInitialRender().catch(() => {})\n }\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n if (!instance.subscribe || needsNewCollection) {\n instance.subscribe = (onStoreChange: () => void) => {\n const unsubscribe = observer.subscribe(onStoreChange)\n resumeDeferredCollections()\n return unsubscribe\n }\n }\n\n const returned = useSyncExternalStore(\n instance.subscribe,\n () => observer.getSnapshot(),\n () => observer.getServerSnapshot(),\n )\n // Only useLiveSuspenseQuery reads this, and it costs a define per render.\n if (forSuspense) {\n setLiveQueryResultInfo(returned, {\n client: dbClient,\n queryHash: instance.queryHash,\n identityError: instance.identityError,\n observer,\n })\n }\n return returned as any\n}\n"],"names":[],"mappings":";;;;;;;AAwCA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AAIA;AAIE;AACA;AACA;AACF;AAEA;AAGE;AACA;AACA;AACE;AACA;AAAmD;AAErD;AACF;AAEA;AAKE;AACA;AACA;AACA;AACF;AA2CO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AACA;AAIA;AAAQ;AAC8F;AAExG;AAEA;AACE;AAIF;AAEA;AACE;AACA;AAA0B;AAC1B;AAEG;AAI2C;AAE9C;AACF;AAEA;AAIE;AAIE;AAAA;AAGF;AAEA;AAIA;AACE;AAAA;AAGF;AACA;AACE;AACA;AAAA;AAGF;AACA;AAEA;AAIA;AAAQ;AAC2I;AAGrJ;AAEA;AACE;AAKF;AAEA;AACE;AAMF;AAEO;AAKL;AACF;AAcO;AAML;AAAsB;AACpB;AAEF;AACA;AAEA;AACE;AACA;AAAO;AACG;AACD;AAC2B;AAAA;AAGpC;AACE;AAAqD;AAGvD;AAAM;AAEN;AACE;AACA;AACA;AACA;AACA;AAA+C;AACjD;AAEJ;AAEO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AAEA;AAAQ;AAC4F;AAItG;AAkYO;AAIL;AACF;AAGO;AAIL;AACF;AAIA;AACE;AAAO;AACO;AACN;AACE;AACA;AACsC;AACrB;AACV;AACJ;AACF;AACC;AAAA;AAEe;AAGd;AACD;AACC;AACE;AACE;AACJ;AAEf;AAEA;AAKE;AAEA;AACA;AAGA;AAEA;AACA;AACA;AAGA;AAGA;AAEA;AACA;AACA;AAEA;AACE;AACA;AAAsC;AAEtC;AAGA;AAKE;AACE;AAAqB;AACnB;AACA;AACS;AAEX;AAAiB;AACf;AACA;AAC+C;AAAA;AAGjD;AACA;AACA;AAAgB;AAClB;AAEF;AACA;AAA4D;AAE5D;AAAoB;AAClB;AACA;AACS;AACA;AAEX;AACA;AACE;AACA;AAA6B;AAE7B;AACA;AACA;AAA4B;AAC9B;AAGF;AACA;AACE;AACE;AAAyD;AAEzD;AACE;AACA;AAAgB;AAEhB;AAAM;AACR;AACF;AAGF;AACE;AAAA;AAGF;AAKE;AAAA;AACW;AACT;AACA;AACA;AACA;AAAA;AAIJ;AASA;AAOE;AAAqB;AACnB;AACA;AACS;AAAA;AAIb;AAOA;AAIA;AAGA;AAEA;AAQA;AAKA;AACE;AAIE;AAGA;AAIE;AAAQ;;AACN;AAAA;;AAAA;AAAA;AAAA;AAQJ;AACA;AACA;AAAkB;AAElB;AACE;AAAsB;AAEtB;AACE;AAAqB;AACnB;AACA;AACS;AAAA;AAGb;AAAgE;AAG1D;AAAA;AAEmB;AAAA;AAEiB;AAE1C;AACE;AACA;AAAyC;AAAG;AAC1C;AACE;AACA;AACA;AAAA;AAGJ;AAIE;AAAA;AAEI;AACE;AACA;AACA;AAAA;AAEJ;AAAA;AAGJ;AAAuC;AACrC;AAEE;AACA;AAAA;AACF;AAEF;AAA0C;AAC5C;AAEF;AACA;AAAgC;AAElC;AACA;AACA;AACA;AAAyB;AAS3B;AAOE;AAAiE;AACzD;AACE;AACY;AACT;AACZ;AAEH;AAKA;AAME;AAA+C;AAAE;AAKnD;AACE;AACE;AACA;AACA;AAAO;AACT;AAGF;AAAiB;AACN;AACM;AACA;AAGjB;AACE;AAAiC;AACvB;AACY;AACI;AACxB;AACD;AAEH;AACF;;;;;;;;;"}
1
+ {"version":3,"file":"useLiveQuery.js","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef, useSyncExternalStore } from 'react'\nimport {\n UnhashableQueryIRError,\n createLiveQueryObserver,\n deepEquals,\n getPreparedLiveQueryIdentity,\n getStableValueHash,\n isCollection,\n prepareLiveQueryValue,\n resolveLiveQueryValue,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { setLiveQueryResultInfo } from './live-query-internals'\nimport { shouldWarnInDevelopment } from './development'\nimport {\n claimSourceIds,\n createSourceIdBindings,\n getPreparedSources,\n getSourceObjectToken,\n resumeSyncStarts,\n} from './source-id-bindings'\nimport type {\n Collection,\n CollectionImpl,\n CollectionStatus,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\n LiveQueryPersistedStatus,\n NonSingleResult,\n QueryBuilder,\n SingleResult,\n} from '@tanstack/db'\n\nconst DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)\n// Suspense renders can be abandoned before React subscribes. Keep their\n// collection briefly so a nearby retry can commit without starting over.\nconst DEFAULT_SUSPENSE_GC_TIME_MS = 5000\nconst DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16\nconst DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10\nconst DERIVED_IDENTITY_TOTAL_WARN_MS = 50\nconst warnedDepsCallsites = new Set<string>()\nconst warnedDerivedIdentityCallsites = new Set<string>()\nconst warnedUnhashableIdentityCallsites = new Set<string>()\nconst unpreparedQueryValue = Symbol(`unpreparedQueryValue`)\n\ntype SuspenseCollection = Collection<object, string | number, {}>\ntype SuspenseCollectionEntry = {\n collection: SuspenseCollection\n removeStatusListeners: () => void\n}\n\nconst unscopedSuspenseCollections = new Map<string, SuspenseCollectionEntry>()\nconst suspenseCollectionsByClient = new WeakMap<\n DbClient,\n Map<string, SuspenseCollectionEntry>\n>()\nfunction getSourceQualifiedSuspenseKey(\n preparedValue: unknown,\n queryHash: string,\n client: DbClient | undefined,\n): string {\n const sourceIds = getPreparedSources(\n preparedValue,\n client,\n `useLiveSuspenseQuery`,\n ).map(getSourceObjectToken)\n if (sourceIds.length === 0) return queryHash\n return `${sourceIds.join(`,`)}:${queryHash}`\n}\n\nfunction getSuspenseCollections(\n client: DbClient | undefined,\n): Map<string, SuspenseCollectionEntry> {\n if (!client) return unscopedSuspenseCollections\n let collections = suspenseCollectionsByClient.get(client)\n if (!collections) {\n collections = new Map()\n suspenseCollectionsByClient.set(client, collections)\n }\n return collections\n}\n\nfunction releaseSuspenseCollection(\n collections: Map<string, SuspenseCollectionEntry>,\n queryHash: string,\n collection: SuspenseCollection,\n): void {\n const entry = collections.get(queryHash)\n if (entry?.collection !== collection) return\n collections.delete(queryHash)\n entry.removeStatusListeners()\n}\n\nexport type DerivedIdentityProfiler = {\n renderCount: number\n totalMs: number\n maxMs: number\n warned: boolean\n}\n\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\nexport type LiveQueryKey = ReadonlyArray<unknown>\ntype UseLiveQueryConfigOptions<TContext extends Context> = Omit<\n LiveQueryCollectionConfig<TContext>,\n `query`\n> & {\n /**\n * Explicit identity for queries that contain opaque functional variants or\n * are hot enough that deriving identity from structured IR is too expensive.\n * Structured queries should omit this so DB can derive identity directly.\n */\n queryKey?: LiveQueryKey\n /** Override the nearest DbProvider for this query. */\n client?: DbClient\n}\n\ntype ConfiguredQueryBuilder<TContext extends Context> = Extract<\n LiveQueryCollectionConfig<TContext>[`query`],\n QueryBuilder<TContext>\n>\n\nexport type UseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> &\n Pick<LiveQueryCollectionConfig<TContext>, `query`>\n\nexport type ConditionalUseLiveQueryConfig<TContext extends Context> =\n UseLiveQueryConfigOptions<TContext> & {\n query:\n | ConfiguredQueryBuilder<TContext>\n | ((\n q: InitialQueryBuilder,\n ) => ConfiguredQueryBuilder<TContext> | undefined | null)\n }\n\nexport function warnDeprecatedDepsArray(\n hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedDepsCallsites.has(callsite)) {\n return\n }\n warnedDepsCallsites.add(callsite)\n const replacement =\n hookName === `useLiveQuery`\n ? `useLiveQuery({ query })`\n : `useLiveInfiniteQuery(query, { queryKey })`\n console.warn(\n `[${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.`,\n )\n}\n\nfunction getCurrentTime(): number {\n return typeof performance !== `undefined` &&\n typeof performance.now === `function`\n ? performance.now()\n : Date.now()\n}\n\nfunction getWarningCallsite(stackIndex: number): string {\n const stack = new Error().stack ?? `unknown`\n const lines = stack.split(`\\n`)\n const userFrame = lines\n .slice(1)\n .find(\n (line) =>\n !line.includes(`useLiveQuery.ts`) &&\n !line.includes(`useLiveSuspenseQuery.ts`) &&\n !line.includes(`useLiveInfiniteQuery.ts`),\n )\n return userFrame?.trim() ?? lines[stackIndex]?.trim() ?? stack\n}\n\nfunction warnDerivedIdentityHotPath(\n profiler: DerivedIdentityProfiler,\n durationMs: number,\n): void {\n if (\n profiler.warned ||\n !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n return\n }\n\n const isSlowSingleRender =\n durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS\n const isHotRenderPath =\n profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&\n profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS\n\n if (!isSlowSingleRender && !isHotRenderPath) {\n return\n }\n\n const callsite = getWarningCallsite(5)\n if (warnedDerivedIdentityCallsites.has(callsite)) {\n profiler.warned = true\n return\n }\n\n warnedDerivedIdentityCallsites.add(callsite)\n profiler.warned = true\n\n const reason = isSlowSingleRender\n ? `one render took ${durationMs.toFixed(1)}ms`\n : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`\n\n console.warn(\n `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +\n `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,\n )\n}\n\nfunction getExplicitQueryKey(value: unknown): LiveQueryKey | undefined {\n return value &&\n typeof value === `object` &&\n Array.isArray((value as { queryKey?: unknown }).queryKey)\n ? (value as { queryKey: LiveQueryKey }).queryKey\n : undefined\n}\n\nfunction getExplicitDbClient(value: unknown): DbClient | undefined {\n return value &&\n typeof value === `object` &&\n `client` in value &&\n (value as { client?: unknown }).client !== undefined\n ? (value as { client: DbClient }).client\n : undefined\n}\n\nexport function prepareQueryValue(\n value: unknown,\n dbClient: DbClient | undefined,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): unknown {\n return prepareLiveQueryValue(value, dbClient, deferredCollections)\n}\n\ntype DerivedQueryPreparation =\n | {\n status: `hashable`\n value: unknown\n identityDeps: Array<unknown>\n }\n | {\n status: `unhashable`\n value: unknown\n error: UnhashableQueryIRError\n }\n\nexport function prepareDerivedQuery(\n value: unknown,\n dbClient: DbClient | undefined,\n profiler: DerivedIdentityProfiler,\n deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,\n): DerivedQueryPreparation {\n const shouldProfile = shouldWarnInDevelopment(\n `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,\n )\n const start = shouldProfile ? getCurrentTime() : 0\n const preparedValue = prepareQueryValue(value, dbClient, deferredCollections)\n\n try {\n const identity = getPreparedLiveQueryIdentity(preparedValue)\n return {\n status: `hashable`,\n value: preparedValue,\n identityDeps: [`derived`, identity],\n }\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n return { status: `unhashable`, value: preparedValue, error }\n }\n\n throw error\n } finally {\n if (shouldProfile) {\n const durationMs = getCurrentTime() - start\n profiler.renderCount += 1\n profiler.totalMs += durationMs\n profiler.maxMs = Math.max(profiler.maxMs, durationMs)\n warnDerivedIdentityHotPath(profiler, durationMs)\n }\n }\n}\n\nexport function warnUnhashableDerivedIdentity(\n error: UnhashableQueryIRError,\n): void {\n if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {\n return\n }\n\n const callsite = getWarningCallsite(4)\n if (warnedUnhashableIdentityCallsites.has(callsite)) {\n return\n }\n warnedUnhashableIdentityCallsites.add(callsite)\n\n console.warn(\n `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` +\n `It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` +\n `Unhashable queries without queryKey will throw in 1.0.`,\n )\n}\n\n/**\n * Create a live query using a query function.\n * @param queryFn - Query function that defines what data to fetch\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Prefer config object syntax\n * const { data, isLoading } = useLiveQuery({\n * query: (q) =>\n * q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.completed, false))\n * .select(({ todos }) => ({ id: todos.id, text: todos.text }))\n * })\n *\n * @example\n * // Single result query\n * const { data } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * })\n *\n * @example\n * // Structured captured values are included in derived query identity\n * const { data, state } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * })\n *\n * @example\n * // Return undefined or null to disable a query\n * const { data, isEnabled } = useLiveQuery({\n * query: (q) => {\n * if (!userId) return undefined\n * return q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.userId, userId))\n * },\n * })\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery({\n * query: (q) =>\n * q.from({ issues: issueCollection })\n * .join({ persons: personCollection }, ({ issues, persons }) =>\n * eq(issues.userId, persons.id)\n * )\n * .select(({ issues, persons }) => ({\n * id: issues.id,\n * title: issues.title,\n * userName: persons.name\n * }))\n * })\n *\n * @example\n * // Handle loading and error states\n * const { data, isLoading, isError, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todoCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error: {status}</div>\n *\n * return (\n * <ul>\n * {data.map(todo => <li key={todo.id}>{todo.text}</li>)}\n * </ul>\n * )\n */\n// Overload 1: Accept query function that always returns QueryBuilder\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled if always returns QueryBuilder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true if always returns QueryBuilder\n}\n\n// Overload 2: Accept query function that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => QueryBuilder<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 3: Accept query function that can return LiveQueryCollectionConfig\nexport function useLiveQuery<TContext extends Context>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => LiveQueryCollectionConfig<TContext> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 4: Accept query function that can return Collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) => Collection<TResult, TKey, TUtils> | undefined | null,\n deps?: Array<unknown>,\n): {\n state: Map<TKey, TResult> | undefined\n data: Array<TResult> | undefined\n collection: Collection<TResult, TKey, TUtils> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 5: Accept query function that can return all types\nexport function useLiveQuery<\n TContext extends Context,\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n queryFn: (\n q: InitialQueryBuilder,\n ) =>\n | QueryBuilder<TContext>\n | LiveQueryCollectionConfig<TContext>\n | Collection<TResult, TKey, TUtils>\n | undefined\n | null,\n deps?: Array<unknown>,\n): {\n state:\n Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined\n data: InferResultType<TContext> | Array<TResult> | undefined\n collection:\n | Collection<GetResult<TContext>, string | number, {}>\n | Collection<TResult, TKey, TUtils>\n | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Create a live query using configuration object\n * @param config - Configuration object with query and options\n * @param deps - Deprecated array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic config object usage\n * const { data, status } = useLiveQuery({\n * query: (q) => q.from({ todos: todosCollection }),\n * gcTime: 60000\n * })\n *\n * @example\n * // With query builder and options\n * const queryBuilder = new Query()\n * .from({ persons: collection })\n * .where(({ persons }) => gt(persons.age, 30))\n * .select(({ persons }) => ({ id: persons.id, name: persons.name }))\n *\n * const { data, isReady } = useLiveQuery({\n * query: queryBuilder,\n * })\n *\n * @example\n * // Handle all states uniformly\n * const { data, isLoading, isReady, isError } = useLiveQuery({\n * query: (q) => q.from({ items: itemCollection })\n * })\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Something went wrong</div>\n * if (!isReady) return <div>Preparing...</div>\n *\n * return <div>{data.length} items loaded</div>\n */\n// Overload 6: Accept config object\nexport function useLiveQuery<TContext extends Context>(\n config: UseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 7: Accept config object with a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n// Overload 8: Accept config object with legacy deps\nexport function useLiveQuery<TContext extends Context>(\n config: LiveQueryCollectionConfig<TContext>,\n deps?: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>>\n data: InferResultType<TContext>\n collection: Collection<GetResult<TContext>, string | number, {}>\n status: CollectionStatus // Can't be disabled when query always returns a builder\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true when query always returns a builder\n}\n\n// Overload 9: Accept config object with legacy deps and a query that can return undefined/null\nexport function useLiveQuery<TContext extends Context>(\n config: ConditionalUseLiveQueryConfig<TContext>,\n deps: Array<unknown>,\n): {\n state: Map<string | number, GetResult<TContext>> | undefined\n data: InferResultType<TContext> | undefined\n collection: Collection<GetResult<TContext>, string | number, {}> | undefined\n status: UseLiveQueryStatus\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: boolean\n}\n\n/**\n * Subscribe to an existing live query collection\n * @param liveQueryCollection - Pre-created live query collection to subscribe to\n * @returns Object with reactive data, state, and status information\n * @example\n * // Using pre-created live query collection\n * const myLiveQuery = createLiveQueryCollection((q) =>\n * q.from({ todos: todosCollection }).where(({ todos }) => eq(todos.active, true))\n * )\n * const { data, collection } = useLiveQuery(myLiveQuery)\n *\n * @example\n * // Access collection methods directly\n * const { data, collection, isReady } = useLiveQuery(existingCollection)\n *\n * // Use collection for mutations\n * const handleToggle = (id) => {\n * collection.update(id, draft => { draft.completed = !draft.completed })\n * }\n *\n * @example\n * // Handle states consistently\n * const { data, isLoading, isError } = useLiveQuery(sharedCollection)\n *\n * if (isLoading) return <div>Loading...</div>\n * if (isError) return <div>Error loading data</div>\n *\n * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>\n */\n// Overload 10: Accept pre-created live query collection\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & NonSingleResult,\n): {\n state: Map<TKey, TResult>\n data: Array<TResult>\n collection: Collection<TResult, TKey, TUtils>\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 10: Accept pre-created live query collection with singleResult: true\nexport function useLiveQuery<\n TResult extends object,\n TKey extends string | number,\n TUtils extends Record<string, any>,\n>(\n liveQueryCollection: Collection<TResult, TKey, TUtils> & SingleResult,\n): {\n state: Map<TKey, TResult>\n data: TResult | undefined\n collection: Collection<TResult, TKey, TUtils> & SingleResult\n status: CollectionStatus // Can't be disabled for pre-created live query collections\n isLoading: boolean\n isReady: boolean\n persistedStatus: LiveQueryPersistedStatus\n isPersistedReady: boolean\n persistedError: unknown | undefined\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Implementation - use function overloads to infer the actual collection type\nexport function useLiveQuery(\n configOrQueryOrCollection: any,\n deps?: Array<unknown>,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps)\n}\n\n/** @internal Shared implementation for the Suspense wrapper. */\nexport function useLiveQueryForSuspense(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n) {\n return useLiveQueryImpl(configOrQueryOrCollection, deps, true)\n}\n\n// What one hook instance keeps across renders. It lives in a single ref\n// slot, so a render neither looks up nor allocates more.\nfunction createHookInstance(dbClient: DbClient | undefined) {\n return {\n collection: null as Collection<object, string | number, {}> | null,\n deps: null as Array<unknown> | null,\n config: null as unknown,\n client: dbClient,\n legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,\n derivedIdentityProfiler: {\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n } as DerivedIdentityProfiler,\n deferredCollections: new Set<\n CollectionImpl<any, string | number, any, any, any>\n >(),\n sourceIds: createSourceIdBindings(),\n observer: null as LiveQueryObserver<object, string | number> | null,\n queryHash: undefined as string | undefined,\n suspenseKey: undefined as string | undefined,\n identityError: undefined as UnhashableQueryIRError | undefined,\n subscribe: null as ((onStoreChange: () => void) => () => void) | null,\n }\n}\n\nfunction useLiveQueryImpl(\n configOrQueryOrCollection: any,\n deps: Array<unknown> | undefined,\n forSuspense = false,\n) {\n const contextDbClient = useOptionalDbClient()\n // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n const dbClient = inputIsCollection\n ? contextDbClient\n : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)\n const resolvedDeps = deps ?? []\n\n const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)\n const instance = (instanceRef.current ??= createHookInstance(dbClient))\n const resumeDeferredCollections = () =>\n resumeSyncStarts(instance.deferredCollections)\n\n const queryKey = !inputIsCollection\n ? getExplicitQueryKey(configOrQueryOrCollection)\n : undefined\n let preparedQueryValue: unknown | typeof unpreparedQueryValue =\n unpreparedQueryValue\n let identityDeps: ReadonlyArray<unknown>\n let streamIdentity: unknown = undefined\n let identityError: UnhashableQueryIRError | undefined\n\n if (queryKey) {\n identityDeps = queryKey\n streamIdentity = [`queryKey`, queryKey]\n } else if (deps !== undefined) {\n identityDeps = resolvedDeps\n // Deps decide reuse. Only hydration and Suspense read the query hash; the\n // development warning about unhashable queries still derives it.\n if (\n dbClient ||\n forSuspense ||\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n )\n try {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n streamIdentity = [\n `deps`,\n resolvedDeps,\n getPreparedLiveQueryIdentity(preparedQueryValue),\n ]\n } catch (error) {\n if (!(error instanceof UnhashableQueryIRError)) throw error\n warnUnhashableDerivedIdentity(error)\n identityError = error\n }\n } else if (inputIsCollection) {\n identityDeps = []\n streamIdentity = [`collection`, configOrQueryOrCollection.id]\n } else {\n const preparation = prepareDerivedQuery(\n configOrQueryOrCollection,\n dbClient,\n instance.derivedIdentityProfiler,\n instance.deferredCollections,\n )\n preparedQueryValue = preparation.value\n if (preparation.status === `hashable`) {\n identityDeps = preparation.identityDeps\n streamIdentity = preparation.identityDeps\n } else {\n warnUnhashableDerivedIdentity(preparation.error)\n identityDeps = instance.legacyUnhashableIdentity\n identityError = preparation.error\n }\n }\n\n let queryHash: string | undefined\n if (streamIdentity !== undefined) {\n try {\n queryHash = getStableValueHash(streamIdentity, `queryKey`)\n } catch (error) {\n if (error instanceof UnhashableQueryIRError) {\n if (queryKey !== undefined) throw error\n identityError = error\n } else {\n throw error\n }\n }\n }\n\n if (deps !== undefined) {\n warnDeprecatedDepsArray()\n }\n\n if (\n queryKey === undefined &&\n deps === undefined &&\n preparedQueryValue !== unpreparedQueryValue\n ) {\n claimSourceIds(\n instance.sourceIds,\n preparedQueryValue,\n dbClient,\n `useLiveQuery`,\n resumeDeferredCollections,\n )\n }\n\n const canReuseSuspenseKey =\n forSuspense &&\n !inputIsCollection &&\n queryHash !== undefined &&\n instance.collection !== null &&\n instance.client === dbClient &&\n instance.queryHash === queryHash &&\n instance.suspenseKey !== undefined\n\n if (\n forSuspense &&\n !inputIsCollection &&\n queryHash &&\n !canReuseSuspenseKey &&\n preparedQueryValue === unpreparedQueryValue\n ) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n\n const suspenseKey =\n queryHash && forSuspense && !inputIsCollection\n ? canReuseSuspenseKey\n ? instance.suspenseKey\n : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient)\n : queryHash\n\n const suspenseCollections =\n forSuspense && !inputIsCollection && suspenseKey\n ? getSuspenseCollections(dbClient)\n : undefined\n const suspenseEntry = suspenseKey\n ? suspenseCollections?.get(suspenseKey)\n : undefined\n const suspenseCollection = suspenseEntry?.collection\n\n const identityChanged =\n instance.deps === null ||\n (deps !== undefined\n ? instance.deps.length !== identityDeps.length ||\n instance.deps.some((dep, index) => dep !== identityDeps[index])\n : !deepEquals(instance.deps, identityDeps))\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !instance.collection ||\n (inputIsCollection && instance.config !== configOrQueryOrCollection) ||\n (!inputIsCollection && (instance.client !== dbClient || identityChanged))\n\n if (needsNewCollection) {\n if (inputIsCollection) {\n // Warn when passing a collection directly with on-demand sync mode\n // In on-demand mode, data is only loaded when queries with predicates request it\n // Passing the collection directly doesn't provide any predicates, so no data loads\n const syncMode = (\n configOrQueryOrCollection as { config?: { syncMode?: string } }\n ).config?.syncMode\n if (\n syncMode === `on-demand` &&\n shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)\n ) {\n console.warn(\n `[useLiveQuery] Warning: Passing a collection with syncMode \"on-demand\" directly to useLiveQuery ` +\n `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\\n\\n` +\n `Instead, use a query builder function:\\n` +\n ` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\\n\\n` +\n `Or switch to syncMode \"eager\" if you want all data to sync automatically.`,\n )\n }\n // It's already a collection, ensure sync is started for React hooks\n configOrQueryOrCollection.startSyncImmediate()\n instance.collection = configOrQueryOrCollection\n instance.config = configOrQueryOrCollection\n } else {\n if (suspenseCollection) {\n instance.collection = suspenseCollection\n } else {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n instance.deferredCollections,\n )\n }\n if (!dbClient) {\n getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`)\n }\n instance.collection = resolveLiveQueryValue(preparedQueryValue, {\n gcTime: forSuspense\n ? DEFAULT_SUSPENSE_GC_TIME_MS\n : DEFAULT_GC_TIME_MS,\n // Hydration and Suspense key the live-query Collection by identity.\n pool: !forSuspense && !dbClient,\n // Without a DbClient, Suspense starts it through a preload below.\n startSync: !forSuspense || dbClient !== undefined,\n }) as SuspenseCollection | null\n if (suspenseCollections && suspenseKey && instance.collection) {\n const collection = instance.collection\n const removeCleanupListener = collection.on(`status:cleaned-up`, () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n )\n const removeErrorListener = collection.on(`status:error`, () => {\n // Keep the failed collection through React's immediate retry so\n // the hook can throw its actual load error to an ErrorBoundary.\n // Retire it afterward so a later boundary reset starts fresh.\n setTimeout(\n () =>\n releaseSuspenseCollection(\n suspenseCollections,\n suspenseKey,\n collection,\n ),\n 0,\n )\n })\n const entry: SuspenseCollectionEntry = {\n collection,\n removeStatusListeners: () => {\n removeCleanupListener()\n removeErrorListener()\n },\n }\n suspenseCollections.set(suspenseKey, entry)\n }\n }\n instance.config = configOrQueryOrCollection\n instance.deps = [...identityDeps]\n }\n instance.client = dbClient\n instance.queryHash = queryHash\n instance.suspenseKey = suspenseKey\n instance.identityError = identityError\n }\n\n // Recreate the observer when the underlying collection changes. The observer\n // is not disposed explicitly here or on unmount: `useSyncExternalStore`\n // unsubscribes it when the subscribe changes or the component unmounts, which\n // detaches the collection subscription; the observer is then GC'd. (An unmount\n // effect that disposed it would misfire under StrictMode/offscreen effect\n // replay, leaving a disposed observer in the ref.)\n if (needsNewCollection) {\n // Defer the initial notify: useSyncExternalStore must not be notified\n // synchronously during subscribe.\n // Wholesale mode: React re-reads getSnapshot() on notify, keeps the\n // hook's pre-observer loading policy, and — because wholesale delivers\n // nothing synchronously during subscribe — never notifies\n // useSyncExternalStore inside its own subscribe call.\n instance.observer = createLiveQueryObserver(instance.collection, {\n mode: `wholesale`,\n client: dbClient,\n queryHash: instance.queryHash,\n onPreload: resumeDeferredCollections,\n })\n }\n const observer = instance.observer!\n // A Suspense render asks for its data. Preloading starts the sync run, so a\n // source that already holds the rows makes it ready before the snapshot is\n // read instead of suspending once. useLiveSuspenseQuery surfaces failures.\n // A DbClient defers source starts past render and owns its streamed query.\n if (\n forSuspense &&\n !dbClient &&\n (instance.collection?.status === `idle` ||\n instance.collection?.status === `cleaned-up`)\n ) {\n observer.preloadForInitialRender().catch(() => {})\n }\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n if (!instance.subscribe || needsNewCollection) {\n instance.subscribe = (onStoreChange: () => void) => {\n const unsubscribe = observer.subscribe(onStoreChange)\n resumeDeferredCollections()\n return unsubscribe\n }\n }\n\n const returned = useSyncExternalStore(\n instance.subscribe,\n () => observer.getSnapshot(),\n () => observer.getServerSnapshot(),\n )\n // Only useLiveSuspenseQuery reads this, and it costs a define per render.\n if (forSuspense) {\n setLiveQueryResultInfo(returned, {\n client: dbClient,\n queryHash: instance.queryHash,\n identityError: instance.identityError,\n observer,\n })\n }\n return returned as any\n}\n"],"names":[],"mappings":";;;;;;;AAwCA;AAGA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAQA;AACA;AAIA;AAKE;AAAkB;AAChB;AACA;AACA;AAEF;AACA;AACF;AAEA;AAGE;AACA;AACA;AACE;AACA;AAAmD;AAErD;AACF;AAEA;AAKE;AACA;AACA;AACA;AACF;AA2CO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AACA;AAIA;AAAQ;AAC8F;AAExG;AAEA;AACE;AAIF;AAEA;AACE;AACA;AAA0B;AAC1B;AAEG;AAI2C;AAE9C;AACF;AAEA;AAIE;AAIE;AAAA;AAGF;AAEA;AAIA;AACE;AAAA;AAGF;AACA;AACE;AACA;AAAA;AAGF;AACA;AAEA;AAIA;AAAQ;AAC2I;AAGrJ;AAEA;AACE;AAKF;AAEA;AACE;AAMF;AAEO;AAKL;AACF;AAcO;AAML;AAAsB;AACpB;AAEF;AACA;AAEA;AACE;AACA;AAAO;AACG;AACD;AAC2B;AAAA;AAGpC;AACE;AAAqD;AAGvD;AAAM;AAEN;AACE;AACA;AACA;AACA;AACA;AAA+C;AACjD;AAEJ;AAEO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AAEA;AAAQ;AAC4F;AAItG;AAkYO;AAIL;AACF;AAGO;AAIL;AACF;AAIA;AACE;AAAO;AACO;AACN;AACE;AACA;AACsC;AACrB;AACV;AACJ;AACF;AACC;AAAA;AAEe;AAGd;AACD;AACC;AACE;AACE;AACJ;AAEf;AAEA;AAKE;AAEA;AACA;AAGA;AAEA;AACA;AACA;AAGA;AAGA;AAEA;AACA;AACA;AAEA;AACE;AACA;AAAsC;AAEtC;AAGA;AAKE;AACE;AAAqB;AACnB;AACA;AACS;AAEX;AAAiB;AACf;AACA;AAC+C;AAAA;AAGjD;AACA;AACA;AAAgB;AAClB;AAEF;AACA;AAA4D;AAE5D;AAAoB;AAClB;AACA;AACS;AACA;AAEX;AACA;AACE;AACA;AAA6B;AAE7B;AACA;AACA;AAA4B;AAC9B;AAGF;AACA;AACE;AACE;AAAyD;AAEzD;AACE;AACA;AAAgB;AAEhB;AAAM;AACR;AACF;AAGF;AACE;AAAA;AAGF;AAKE;AAAA;AACW;AACT;AACA;AACA;AACA;AAAA;AAIJ;AASA;AAOE;AAAqB;AACnB;AACA;AACS;AAAA;AAIb;AAOA;AAIA;AAGA;AAEA;AAQA;AAKA;AACE;AAIE;AAGA;AAIE;AAAQ;;AACN;AAAA;;AAAA;AAAA;AAAA;AAQJ;AACA;AACA;AAAkB;AAElB;AACE;AAAsB;AAEtB;AACE;AAAqB;AACnB;AACA;AACS;AAAA;AAGb;AACE;AAA+D;AAEjE;AAAgE;AAG1D;AAAA;AAEmB;AAAA;AAEiB;AAE1C;AACE;AACA;AAAyC;AAAG;AAC1C;AACE;AACA;AACA;AAAA;AAGJ;AAIE;AAAA;AAEI;AACE;AACA;AACA;AAAA;AAEJ;AAAA;AAGJ;AAAuC;AACrC;AAEE;AACA;AAAA;AACF;AAEF;AAA0C;AAC5C;AAEF;AACA;AAAgC;AAElC;AACA;AACA;AACA;AAAyB;AAS3B;AAOE;AAAiE;AACzD;AACE;AACY;AACT;AACZ;AAEH;AAKA;AAME;AAA+C;AAAE;AAKnD;AACE;AACE;AACA;AACA;AAAO;AACT;AAGF;AAAiB;AACN;AACM;AACA;AAGjB;AACE;AAAiC;AACvB;AACY;AACI;AACxB;AACD;AAEH;AACF;;;;;;;;;"}
@@ -1,12 +1,17 @@
1
1
  import { useRef, useEffect } from "react";
2
- import { createEffect } from "@tanstack/db";
2
+ import { prepareLiveQueryValue, createEffect } from "@tanstack/db";
3
+ import { useOptionalDbClient } from "./DbProvider.js";
4
+ import { getPreparedSources } from "./source-id-bindings.js";
3
5
  function useLiveQueryEffect(config, deps = []) {
6
+ const client = useOptionalDbClient();
4
7
  const configRef = useRef(config);
5
8
  configRef.current = config;
6
9
  useEffect(() => {
10
+ const query = prepareLiveQueryValue(config.query, client);
11
+ if (!client) getPreparedSources(query, client, `useLiveQueryEffect`);
7
12
  const effect = createEffect({
8
13
  id: config.id,
9
- query: config.query,
14
+ query,
10
15
  skipInitial: config.skipInitial,
11
16
  onEnter: (event, ctx) => configRef.current.onEnter?.(event, ctx),
12
17
  onUpdate: (event, ctx) => configRef.current.onUpdate?.(event, ctx),
@@ -16,9 +21,11 @@ function useLiveQueryEffect(config, deps = []) {
16
21
  onSourceError: config.onSourceError ? (error) => configRef.current.onSourceError?.(error) : void 0
17
22
  });
18
23
  return () => {
19
- effect.dispose();
24
+ void effect.dispose().catch((error) => {
25
+ console.error(`[useLiveQueryEffect] failed to dispose effect:`, error);
26
+ });
20
27
  };
21
- }, deps);
28
+ }, [...deps, client]);
22
29
  }
23
30
  export {
24
31
  useLiveQueryEffect
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQueryEffect.js","sources":["../../src/useLiveQueryEffect.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { createEffect } from '@tanstack/db'\nimport type { Effect, EffectConfig } from '@tanstack/db'\n\n/**\n * React hook for creating a reactive effect that fires handlers when rows\n * enter, exit, or update within a query result.\n *\n * The effect is created on mount and disposed on unmount. If `deps` change,\n * the previous effect is disposed and a new one is created.\n *\n * @example\n * ```tsx\n * function ChatComponent() {\n * useLiveQueryEffect(\n * {\n * query: (q) => q.from({ msg: messages }).where(({ msg }) => eq(msg.role, 'user')),\n * skipInitial: true,\n * onEnter: async (event) => {\n * await generateResponse(event.value)\n * },\n * },\n * []\n * )\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useLiveQueryEffect<\n TRow extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(config: EffectConfig<TRow, TKey>, deps: React.DependencyList = []): void {\n const configRef = useRef<EffectConfig<TRow, TKey>>(config)\n configRef.current = config\n\n useEffect(() => {\n const effect: Effect = createEffect<TRow, TKey>({\n id: config.id,\n query: config.query,\n skipInitial: config.skipInitial,\n onEnter: (event, ctx) => configRef.current.onEnter?.(event, ctx),\n onUpdate: (event, ctx) => configRef.current.onUpdate?.(event, ctx),\n onExit: (event, ctx) => configRef.current.onExit?.(event, ctx),\n onBatch: (events, ctx) => configRef.current.onBatch?.(events, ctx),\n onError: config.onError\n ? (error, event) => configRef.current.onError?.(error, event)\n : undefined,\n onSourceError: config.onSourceError\n ? (error) => configRef.current.onSourceError?.(error)\n : undefined,\n })\n\n return () => {\n // Fire-and-forget disposal; AbortSignal cancels in-flight work\n effect.dispose()\n }\n }, deps)\n}\n"],"names":[],"mappings":";;AA6BO,SAAS,mBAGd,QAAkC,OAA6B,IAAU;AACzE,QAAM,YAAY,OAAiC,MAAM;AACzD,YAAU,UAAU;AAEpB,YAAU,MAAM;AACd,UAAM,SAAiB,aAAyB;AAAA,MAC9C,IAAI,OAAO;AAAA,MACX,OAAO,OAAO;AAAA,MACd,aAAa,OAAO;AAAA,MACpB,SAAS,CAAC,OAAO,QAAQ,UAAU,QAAQ,UAAU,OAAO,GAAG;AAAA,MAC/D,UAAU,CAAC,OAAO,QAAQ,UAAU,QAAQ,WAAW,OAAO,GAAG;AAAA,MACjE,QAAQ,CAAC,OAAO,QAAQ,UAAU,QAAQ,SAAS,OAAO,GAAG;AAAA,MAC7D,SAAS,CAAC,QAAQ,QAAQ,UAAU,QAAQ,UAAU,QAAQ,GAAG;AAAA,MACjE,SAAS,OAAO,UACZ,CAAC,OAAO,UAAU,UAAU,QAAQ,UAAU,OAAO,KAAK,IAC1D;AAAA,MACJ,eAAe,OAAO,gBAClB,CAAC,UAAU,UAAU,QAAQ,gBAAgB,KAAK,IAClD;AAAA,IAAA,CACL;AAED,WAAO,MAAM;AAEX,aAAO,QAAA;AAAA,IACT;AAAA,EACF,GAAG,IAAI;AACT;"}
1
+ {"version":3,"file":"useLiveQueryEffect.js","sources":["../../src/useLiveQueryEffect.ts"],"sourcesContent":["import { useEffect, useRef } from 'react'\nimport { createEffect, prepareLiveQueryValue } from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { getPreparedSources } from './source-id-bindings'\nimport type { EffectConfig } from '@tanstack/db'\n\n/**\n * React hook for creating a reactive effect that fires handlers when rows\n * enter, exit, or update within a query result.\n *\n * The effect is created on mount and disposed on unmount. If `deps` change,\n * the previous effect is disposed and a new one is created.\n *\n * @example\n * ```tsx\n * function ChatComponent() {\n * useLiveQueryEffect(\n * {\n * query: (q) => q.from({ msg: messages }).where(({ msg }) => eq(msg.role, 'user')),\n * skipInitial: true,\n * onEnter: async (event) => {\n * await generateResponse(event.value)\n * },\n * },\n * []\n * )\n *\n * return <div>...</div>\n * }\n * ```\n */\nexport function useLiveQueryEffect<\n TRow extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(config: EffectConfig<TRow, TKey>, deps: React.DependencyList = []): void {\n const client = useOptionalDbClient()\n const configRef = useRef<EffectConfig<TRow, TKey>>(config)\n configRef.current = config\n\n useEffect(() => {\n const query = prepareLiveQueryValue(config.query, client)\n if (!client) getPreparedSources(query, client, `useLiveQueryEffect`)\n const effect = createEffect<TRow, TKey>({\n id: config.id,\n query: query as EffectConfig<TRow, TKey>[`query`],\n skipInitial: config.skipInitial,\n onEnter: (event, ctx) => configRef.current.onEnter?.(event, ctx),\n onUpdate: (event, ctx) => configRef.current.onUpdate?.(event, ctx),\n onExit: (event, ctx) => configRef.current.onExit?.(event, ctx),\n onBatch: (events, ctx) => configRef.current.onBatch?.(events, ctx),\n onError: config.onError\n ? (error, event) => configRef.current.onError?.(error, event)\n : undefined,\n onSourceError: config.onSourceError\n ? (error) => configRef.current.onSourceError?.(error)\n : undefined,\n })\n\n return () => {\n // React cannot await cleanup. Report a failed source release.\n void effect.dispose().catch((error) => {\n console.error(`[useLiveQueryEffect] failed to dispose effect:`, error)\n })\n }\n }, [...deps, client])\n}\n"],"names":[],"mappings":";;;;AA+BO,SAAS,mBAGd,QAAkC,OAA6B,IAAU;AACzE,QAAM,SAAS,oBAAA;AACf,QAAM,YAAY,OAAiC,MAAM;AACzD,YAAU,UAAU;AAEpB,YAAU,MAAM;AACd,UAAM,QAAQ,sBAAsB,OAAO,OAAO,MAAM;AACxD,QAAI,CAAC,OAAQ,oBAAmB,OAAO,QAAQ,oBAAoB;AACnE,UAAM,SAAS,aAAyB;AAAA,MACtC,IAAI,OAAO;AAAA,MACX;AAAA,MACA,aAAa,OAAO;AAAA,MACpB,SAAS,CAAC,OAAO,QAAQ,UAAU,QAAQ,UAAU,OAAO,GAAG;AAAA,MAC/D,UAAU,CAAC,OAAO,QAAQ,UAAU,QAAQ,WAAW,OAAO,GAAG;AAAA,MACjE,QAAQ,CAAC,OAAO,QAAQ,UAAU,QAAQ,SAAS,OAAO,GAAG;AAAA,MAC7D,SAAS,CAAC,QAAQ,QAAQ,UAAU,QAAQ,UAAU,QAAQ,GAAG;AAAA,MACjE,SAAS,OAAO,UACZ,CAAC,OAAO,UAAU,UAAU,QAAQ,UAAU,OAAO,KAAK,IAC1D;AAAA,MACJ,eAAe,OAAO,gBAClB,CAAC,UAAU,UAAU,QAAQ,gBAAgB,KAAK,IAClD;AAAA,IAAA,CACL;AAED,WAAO,MAAM;AAEX,WAAK,OAAO,QAAA,EAAU,MAAM,CAAC,UAAU;AACrC,gBAAQ,MAAM,kDAAkD,KAAK;AAAA,MACvE,CAAC;AAAA,IACH;AAAA,EACF,GAAG,CAAC,GAAG,MAAM,MAAM,CAAC;AACtB;"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-db",
3
- "version": "0.5.8",
3
+ "version": "0.5.10",
4
4
  "description": "React integration for @tanstack/db",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  "skills"
41
41
  ],
42
42
  "dependencies": {
43
- "@tanstack/db": "0.13.0"
43
+ "@tanstack/db": "0.15.0"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "react": ">=18.0.0"
@@ -1,6 +1,8 @@
1
- import { BaseQueryBuilder, IR, isCollection } from '@tanstack/db'
1
+ import { getPreparedLiveQuerySources } from '@tanstack/db'
2
2
  import type { DbClient } from '@tanstack/db'
3
3
 
4
+ export { resumeDeferredLiveQueryCollections as resumeSyncStarts } from '@tanstack/db'
5
+
4
6
  const sourceObjectTokens = new WeakMap<object, number>()
5
7
  let nextSourceObjectToken = 0
6
8
 
@@ -15,22 +17,19 @@ export function getSourceObjectToken(source: object): number {
15
17
 
16
18
  export function getPreparedSources(
17
19
  preparedValue: unknown,
20
+ client?: DbClient,
21
+ hookName?: string,
18
22
  ): Array<{ id: string }> {
19
- if (isCollection(preparedValue)) return [preparedValue]
20
- const query =
21
- preparedValue instanceof BaseQueryBuilder
22
- ? preparedValue
23
- : preparedValue &&
24
- typeof preparedValue === `object` &&
25
- `query` in preparedValue &&
26
- preparedValue.query instanceof BaseQueryBuilder
27
- ? preparedValue.query
28
- : undefined
29
- return query
30
- ? IR.collectCollectionSources(query._getQuery()).map(
31
- ({ collection }) => collection,
32
- )
33
- : []
23
+ return getPreparedLiveQuerySources(
24
+ preparedValue,
25
+ !client && hookName
26
+ ? (alias) => {
27
+ throw new Error(
28
+ `[${hookName}] Collection descriptor "${alias}" requires a DbClient when the query is consumed. Wrap this component in <DbProvider client={client}> or use a concrete Collection.`,
29
+ )
30
+ }
31
+ : undefined,
32
+ )
34
33
  }
35
34
 
36
35
  /** The source object each Collection ID named while one hook is mounted. */
@@ -59,7 +58,7 @@ export function claimSourceIds(
59
58
  ): void {
60
59
  const prior = client ? bindings.byClient.get(client) : bindings.unscoped
61
60
  const seen = new Map<string, number>()
62
- for (const source of getPreparedSources(preparedValue)) {
61
+ for (const source of getPreparedSources(preparedValue, client, hookName)) {
63
62
  const token = getSourceObjectToken(source)
64
63
  const previous = seen.get(source.id) ?? prior?.get(source.id)
65
64
  if (previous !== undefined && previous !== token) {
@@ -74,23 +73,3 @@ export function claimSourceIds(
74
73
  for (const [id, token] of seen) claimed.set(id, token)
75
74
  if (client) bindings.byClient.set(client, claimed)
76
75
  }
77
-
78
- /**
79
- * Resumes every deferred sync start, then rethrows the first startup error, so
80
- * one failing source cannot keep a later shared source deferred.
81
- */
82
- export function resumeSyncStarts(
83
- collections: Set<{ _resumeSyncStart: () => void }>,
84
- ): void {
85
- const pending = Array.from(collections)
86
- collections.clear()
87
- const errors: Array<unknown> = []
88
- for (const collection of pending) {
89
- try {
90
- collection._resumeSyncStart()
91
- } catch (error) {
92
- errors.push(error)
93
- }
94
- }
95
- if (errors.length) throw errors[0]
96
- }
@@ -64,8 +64,13 @@ const suspenseCollectionsByClient = new WeakMap<
64
64
  function getSourceQualifiedSuspenseKey(
65
65
  preparedValue: unknown,
66
66
  queryHash: string,
67
+ client: DbClient | undefined,
67
68
  ): string {
68
- const sourceIds = getPreparedSources(preparedValue).map(getSourceObjectToken)
69
+ const sourceIds = getPreparedSources(
70
+ preparedValue,
71
+ client,
72
+ `useLiveSuspenseQuery`,
73
+ ).map(getSourceObjectToken)
69
74
  if (sourceIds.length === 0) return queryHash
70
75
  return `${sourceIds.join(`,`)}:${queryHash}`
71
76
  }
@@ -870,7 +875,7 @@ function useLiveQueryImpl(
870
875
  queryHash && forSuspense && !inputIsCollection
871
876
  ? canReuseSuspenseKey
872
877
  ? instance.suspenseKey
873
- : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash)
878
+ : getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient)
874
879
  : queryHash
875
880
 
876
881
  const suspenseCollections =
@@ -930,6 +935,9 @@ function useLiveQueryImpl(
930
935
  instance.deferredCollections,
931
936
  )
932
937
  }
938
+ if (!dbClient) {
939
+ getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`)
940
+ }
933
941
  instance.collection = resolveLiveQueryValue(preparedQueryValue, {
934
942
  gcTime: forSuspense
935
943
  ? DEFAULT_SUSPENSE_GC_TIME_MS
@@ -1,6 +1,8 @@
1
1
  import { useEffect, useRef } from 'react'
2
- import { createEffect } from '@tanstack/db'
3
- import type { Effect, EffectConfig } from '@tanstack/db'
2
+ import { createEffect, prepareLiveQueryValue } from '@tanstack/db'
3
+ import { useOptionalDbClient } from './DbProvider'
4
+ import { getPreparedSources } from './source-id-bindings'
5
+ import type { EffectConfig } from '@tanstack/db'
4
6
 
5
7
  /**
6
8
  * React hook for creating a reactive effect that fires handlers when rows
@@ -31,13 +33,16 @@ export function useLiveQueryEffect<
31
33
  TRow extends object = Record<string, unknown>,
32
34
  TKey extends string | number = string | number,
33
35
  >(config: EffectConfig<TRow, TKey>, deps: React.DependencyList = []): void {
36
+ const client = useOptionalDbClient()
34
37
  const configRef = useRef<EffectConfig<TRow, TKey>>(config)
35
38
  configRef.current = config
36
39
 
37
40
  useEffect(() => {
38
- const effect: Effect = createEffect<TRow, TKey>({
41
+ const query = prepareLiveQueryValue(config.query, client)
42
+ if (!client) getPreparedSources(query, client, `useLiveQueryEffect`)
43
+ const effect = createEffect<TRow, TKey>({
39
44
  id: config.id,
40
- query: config.query,
45
+ query: query as EffectConfig<TRow, TKey>[`query`],
41
46
  skipInitial: config.skipInitial,
42
47
  onEnter: (event, ctx) => configRef.current.onEnter?.(event, ctx),
43
48
  onUpdate: (event, ctx) => configRef.current.onUpdate?.(event, ctx),
@@ -52,8 +57,10 @@ export function useLiveQueryEffect<
52
57
  })
53
58
 
54
59
  return () => {
55
- // Fire-and-forget disposal; AbortSignal cancels in-flight work
56
- effect.dispose()
60
+ // React cannot await cleanup. Report a failed source release.
61
+ void effect.dispose().catch((error) => {
62
+ console.error(`[useLiveQueryEffect] failed to dispose effect:`, error)
63
+ })
57
64
  }
58
- }, deps)
65
+ }, [...deps, client])
59
66
  }