@tanstack/react-db 0.5.5 → 0.5.8

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.
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ const db = require("@tanstack/db");
4
+ const sourceObjectTokens = /* @__PURE__ */ new WeakMap();
5
+ let nextSourceObjectToken = 0;
6
+ function getSourceObjectToken(source) {
7
+ let id = sourceObjectTokens.get(source);
8
+ if (id === void 0) {
9
+ id = ++nextSourceObjectToken;
10
+ sourceObjectTokens.set(source, id);
11
+ }
12
+ return id;
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
+ ) : [];
20
+ }
21
+ function createSourceIdBindings() {
22
+ return { unscoped: /* @__PURE__ */ new Map(), byClient: /* @__PURE__ */ new WeakMap() };
23
+ }
24
+ function claimSourceIds(bindings, preparedValue, client, hookName, release) {
25
+ const prior = client ? bindings.byClient.get(client) : bindings.unscoped;
26
+ const seen = /* @__PURE__ */ new Map();
27
+ for (const source of getPreparedSources(preparedValue)) {
28
+ const token = getSourceObjectToken(source);
29
+ const previous = seen.get(source.id) ?? prior?.get(source.id);
30
+ if (previous !== void 0 && previous !== token) {
31
+ release();
32
+ throw new Error(
33
+ `[${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.`
34
+ );
35
+ }
36
+ seen.set(source.id, token);
37
+ }
38
+ const claimed = prior ?? /* @__PURE__ */ new Map();
39
+ for (const [id, token] of seen) claimed.set(id, token);
40
+ if (client) bindings.byClient.set(client, claimed);
41
+ }
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
+ }
55
+ exports.claimSourceIds = claimSourceIds;
56
+ exports.createSourceIdBindings = createSourceIdBindings;
57
+ exports.getPreparedSources = getPreparedSources;
58
+ exports.getSourceObjectToken = getSourceObjectToken;
59
+ exports.resumeSyncStarts = resumeSyncStarts;
60
+ //# sourceMappingURL=source-id-bindings.cjs.map
@@ -0,0 +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;;;;;;"}
@@ -0,0 +1,26 @@
1
+ import { DbClient } from '@tanstack/db';
2
+ export declare function getSourceObjectToken(source: object): number;
3
+ export declare function getPreparedSources(preparedValue: unknown): Array<{
4
+ id: string;
5
+ }>;
6
+ /** The source object each Collection ID named while one hook is mounted. */
7
+ export type SourceIdBindings = {
8
+ unscoped: Map<string, number>;
9
+ byClient: WeakMap<DbClient, Map<string, number>>;
10
+ };
11
+ export declare function createSourceIdBindings(): SourceIdBindings;
12
+ /**
13
+ * Derived query identity names each source by its Collection ID. While a hook
14
+ * is mounted, an ID must keep naming one source object in each client scope,
15
+ * or the hook could reuse a live query that reads the old source. Claims the
16
+ * prepared value's sources, or calls `release` and throws on a collision so a
17
+ * rejected render keeps no sync-start deferral.
18
+ */
19
+ 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;
@@ -4,9 +4,13 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
4
4
  const react = require("react");
5
5
  const db = require("@tanstack/db");
6
6
  const DbProvider = require("./DbProvider.cjs");
7
+ const sourceIdBindings = require("./source-id-bindings.cjs");
7
8
  const useLiveQuery = require("./useLiveQuery.cjs");
8
9
  const DEFAULT_GC_TIME_MS = 1;
9
10
  const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
11
+ function queryIdentityMatches(comparison, sameClient, usesLegacyDeps) {
12
+ return sameClient && (usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual);
13
+ }
10
14
  function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
11
15
  if (`getNextPageParam` in config) {
12
16
  throw new Error(
@@ -20,6 +24,8 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
20
24
  const inputIsCollection = db.getLiveQueryWindowInputKind(queryFnOrCollection) === `collection`;
21
25
  const committedRef = react.useRef(null);
22
26
  const committed = committedRef.current;
27
+ const renderedCollectionRef = react.useRef(null);
28
+ const sourceIdsRef = react.useRef(null);
23
29
  const inputKind = inputIsCollection ? `collection` : `query`;
24
30
  const derivedIdentityProfilerRef = react.useRef({
25
31
  renderCount: 0,
@@ -54,6 +60,13 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
54
60
  deferredCollections
55
61
  );
56
62
  preparedQueryValue = preparation.value;
63
+ sourceIdBindings.claimSourceIds(
64
+ sourceIdsRef.current ??= sourceIdBindings.createSourceIdBindings(),
65
+ preparation.value,
66
+ dbClient,
67
+ `useLiveInfiniteQuery`,
68
+ () => sourceIdBindings.resumeSyncStarts(deferredCollections)
69
+ );
57
70
  if (preparation.status === `hashable`) {
58
71
  identityDeps = preparation.identityDeps;
59
72
  } else {
@@ -68,7 +81,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
68
81
  identityDeps
69
82
  );
70
83
  const sameClient = committed?.client === dbClient;
71
- const dependenciesChanged = !inputIsCollection && (!sameClient || (usesLegacyDeps ? dependencyComparison.changed : !dependencyComparison.structurallyEqual));
84
+ const dependenciesChanged = !inputIsCollection && !queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps);
72
85
  const dependenciesStructurallyEqual = usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual;
73
86
  const needsNewCollection = committed === null || committed.inputKind !== inputKind || inputIsCollection && committed.inputCollection !== queryFnOrCollection || dependenciesChanged;
74
87
  const pageShapeChanged = committed === null || committed.pageSize !== pageSize || committed.initialPageParam !== initialPageParam;
@@ -77,6 +90,21 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
77
90
  if (needsNewController) {
78
91
  let collection = committed?.collection;
79
92
  let warning = null;
93
+ let startInRender = false;
94
+ let suppliedCollection = false;
95
+ let createdCollection = false;
96
+ const canPreservePageCount = db.shouldPreserveLiveQueryWindowPageCount({
97
+ hasPreviousController: committed !== null,
98
+ previousInputKind: committed?.inputKind,
99
+ inputKind,
100
+ sameCollection: inputIsCollection && committed?.inputCollection === queryFnOrCollection,
101
+ dependenciesChanged,
102
+ dependenciesStructurallyEqual,
103
+ pageShapeChanged
104
+ });
105
+ const previousPageCount = committed ? Math.max(1, committed.controller.getSnapshot().pages.length) : 1;
106
+ const initialPageCount = canPreservePageCount ? previousPageCount : 1;
107
+ const requiredLimit = initialPageCount * pageSize + 1;
80
108
  if (needsNewCollection) {
81
109
  let inputValue = queryFnOrCollection;
82
110
  if (!inputIsCollection) {
@@ -89,17 +117,33 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
89
117
  }
90
118
  inputValue = () => preparedQueryValue;
91
119
  }
92
- const input = db.resolveLiveQueryWindowInput(inputValue);
93
- if (input.kind === `collection`) {
94
- collection = input.collection;
120
+ const rendered = renderedCollectionRef.current;
121
+ if (!inputIsCollection && rendered !== null && queryIdentityMatches(
122
+ db.compareLiveQueryWindowDependencies(
123
+ rendered.dependencies,
124
+ identityDeps
125
+ ),
126
+ rendered.client === dbClient,
127
+ usesLegacyDeps
128
+ ) && rendered.collection.status !== `cleaned-up` && rendered.collection.status !== `error` && db.liveQueryWindowMatches(rendered.collection, requiredLimit)) {
129
+ collection = rendered.collection;
130
+ for (const deferred of rendered.deferredCollections) {
131
+ deferredCollections.add(deferred);
132
+ }
95
133
  } else {
96
- collection = db.createLiveQueryCollection({
97
- query: input.query.limit(pageSize + 1).offset(0),
98
- // Construction happens during render. Synchronization starts only when
99
- // useSyncExternalStore commits the controller subscription.
100
- startSync: false,
101
- gcTime: DEFAULT_GC_TIME_MS
102
- });
134
+ const input = db.resolveLiveQueryWindowInput(inputValue);
135
+ if (input.kind === `collection`) {
136
+ collection = input.collection;
137
+ suppliedCollection = true;
138
+ renderedCollectionRef.current = null;
139
+ } else {
140
+ collection = db.createLiveQueryCollection({
141
+ query: input.query.limit(requiredLimit).offset(0),
142
+ gcTime: DEFAULT_GC_TIME_MS
143
+ });
144
+ startInRender = true;
145
+ createdCollection = true;
146
+ }
103
147
  }
104
148
  }
105
149
  if (!collection) {
@@ -110,17 +154,18 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
110
154
  } else {
111
155
  db.assertLiveQueryWindowManyResult(collection);
112
156
  }
113
- const canPreservePageCount = db.shouldPreserveLiveQueryWindowPageCount({
114
- hasPreviousController: committed !== null,
115
- previousInputKind: committed?.inputKind,
116
- inputKind,
117
- sameCollection: inputIsCollection && committed?.inputCollection === collection,
118
- dependenciesChanged,
119
- dependenciesStructurallyEqual,
120
- pageShapeChanged
121
- });
122
- const previousPageCount = committed ? Math.max(1, committed.controller.getSnapshot().pages.length) : 1;
123
- const initialPageCount = canPreservePageCount ? previousPageCount : 1;
157
+ if (suppliedCollection) {
158
+ startInRender = db.liveQueryWindowMatches(collection, requiredLimit);
159
+ }
160
+ if (startInRender) collection.startSyncImmediate();
161
+ if (createdCollection) {
162
+ renderedCollectionRef.current = {
163
+ client: dbClient,
164
+ dependencies: [...identityDeps],
165
+ collection,
166
+ deferredCollections
167
+ };
168
+ }
124
169
  renderState = {
125
170
  inputKind,
126
171
  inputCollection: inputIsCollection ? collection : null,
@@ -151,10 +196,7 @@ function useLiveInfiniteQuery(queryFnOrCollection, config, deps) {
151
196
  currentRenderState.warned = true;
152
197
  console.warn(currentRenderState.warning);
153
198
  }
154
- for (const collection of currentRenderState.deferredCollections) {
155
- collection._resumeSyncStart();
156
- }
157
- currentRenderState.deferredCollections.clear();
199
+ sourceIdBindings.resumeSyncStarts(currentRenderState.deferredCollections);
158
200
  return unsubscribe;
159
201
  },
160
202
  [controller, currentRenderState]
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveInfiniteQuery.cjs","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 normalizeLiveQueryWindowPageSize,\n resolveLiveQueryWindowInput,\n shouldPreserveLiveQueryWindowPageCount,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport {\n prepareDerivedQuery,\n prepareQueryValue,\n warnDeprecatedDepsArray,\n warnUnhashableDerivedIdentity,\n} from './useLiveQuery'\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\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 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 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 (!sameClient ||\n (usesLegacyDeps\n ? dependencyComparison.changed\n : !dependencyComparison.structurallyEqual))\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\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 input = resolveLiveQueryWindowInput<TContext>(inputValue)\n if (input.kind === `collection`) {\n collection = input.collection\n } else {\n // Wrap the query with the first page's peek-ahead window; the controller\n // grows the limit from here via setWindow.\n collection = createLiveQueryCollection({\n query: input.query.limit(pageSize + 1).offset(0),\n // Construction happens during render. Synchronization starts only when\n // useSyncExternalStore commits the controller subscription.\n startSync: false,\n gcTime: DEFAULT_GC_TIME_MS,\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\n const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({\n hasPreviousController: committed !== null,\n previousInputKind: committed?.inputKind,\n inputKind,\n sameCollection:\n inputIsCollection && committed?.inputCollection === collection,\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 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 for (const collection of currentRenderState.deferredCollections) {\n collection._resumeSyncStart()\n }\n currentRenderState.deferredCollections.clear()\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":["warnDeprecatedDepsArray","prepareDerivedQuery","warnUnhashableDerivedIdentity","compareLiveQueryWindowDependencies","prepareQueryValue","assertLiveQueryWindowManyResult","useCallback"],"mappings":";;;;;;;AA0CA;AACA;AA4GO;AAKL;AACE;AAAU;AACR;AAAA;AAGJ;AACA;AACA;AACA;AAEA;AAGA;AACA;AACA;AACA;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAA2D;AACzD;AAEF;AAIA;AAEA;AACA;AAEA;AACE;AACE;AACA;AAAsB;AAEtB;AACA;AACAA;AAA8C;AAM9C;AACA;AAAyC;AAEzC;AACA;AAAoBC;AAClB;AACA;AAC2B;AAC3B;AAEF;AACA;AACE;AAA2B;AAE3BC;AACA;AAA2C;AAC7C;AACF;AAGF;AAEA;AAA6BC;AAChB;AACX;AAEF;AACA;AAMA;AAEA;AAKA;AAIA;AAGA;AACA;AACE;AACA;AAEA;AACE;AACA;AACE;AACE;AAAqBC;AACnB;AACA;AACA;AAAA;AAGJ;AAAmB;AAErB;AACA;AACE;AAAmB;AAInB;AAAuC;AACU;AAAA;AAAA;AAGpC;AACH;AACT;AACH;AAGF;AACE;AAAsE;AAGxE;AACE;AACmE;AAEnEC;AAA0C;AAG5C;AAAoE;AAC7B;AACP;AAC9B;AAEsD;AACtD;AACA;AACA;AAEF;AAGA;AACA;AAAc;AACZ;AACkD;AACX;AAC/B;AACR;AACyD;AACzD;AACA;AACA;AACwD;AACtD;AACA;AACA;AACD;AACD;AACQ;AACR;AAAA;AAGJ;AACA;AAEA;AAAkBC;AAEd;AACA;AACA;AACE;AACA;AAAuC;AAEzC;AACE;AAAW;AAEb;AACA;AAAO;AACT;AAC+B;AAEjC;AACA;AAEA;AAAsBA;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.cjs","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":["warnDeprecatedDepsArray","prepareDerivedQuery","claimSourceIds","warnUnhashableDerivedIdentity","compareLiveQueryWindowDependencies","prepareQueryValue","assertLiveQueryWindowManyResult","useCallback","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;AACAA;AAA8C;AAM9C;AACA;AAAyC;AAEzC;AACA;AAAoBC;AAClB;AACA;AAC2B;AAC3B;AAEF;AACAC;AAAAA;AAC4B;AACd;AACZ;AACA;AAC0C;AAE5C;AACE;AAA2B;AAE3BC;AACA;AAA2C;AAC7C;AACF;AAGF;AAEA;AAA6BC;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;AAAqBC;AACnB;AACA;AACA;AAAA;AAGJ;AAAmB;AAErB;AACA;AAGE;AACED;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;AAEnEE;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;AAAkBC;AAEd;AACA;AACA;AACE;AACA;AAAuC;AAEzCC;AACA;AAAO;AACT;AAC+B;AAEjC;AACA;AAEA;AAAsBD;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;;"}
@@ -6,6 +6,7 @@ const db = require("@tanstack/db");
6
6
  const DbProvider = require("./DbProvider.cjs");
7
7
  const liveQueryInternals = require("./live-query-internals.cjs");
8
8
  const development = require("./development.cjs");
9
+ const sourceIdBindings = require("./source-id-bindings.cjs");
9
10
  const DEFAULT_GC_TIME_MS = 1;
10
11
  const DEFAULT_SUSPENSE_GC_TIME_MS = 5e3;
11
12
  const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16;
@@ -17,25 +18,8 @@ const warnedUnhashableIdentityCallsites = /* @__PURE__ */ new Set();
17
18
  const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
18
19
  const unscopedSuspenseCollections = /* @__PURE__ */ new Map();
19
20
  const suspenseCollectionsByClient = /* @__PURE__ */ new WeakMap();
20
- const sourceObjectTokens = /* @__PURE__ */ new WeakMap();
21
- let nextSourceObjectToken = 0;
22
- function getSourceObjectToken(source) {
23
- let id = sourceObjectTokens.get(source);
24
- if (id === void 0) {
25
- id = ++nextSourceObjectToken;
26
- sourceObjectTokens.set(source, id);
27
- }
28
- return id;
29
- }
30
- function getPreparedSources(preparedValue) {
31
- if (db.isCollection(preparedValue)) return [preparedValue];
32
- const query = preparedValue instanceof db.BaseQueryBuilder ? preparedValue : preparedValue && typeof preparedValue === `object` && `query` in preparedValue && preparedValue.query instanceof db.BaseQueryBuilder ? preparedValue.query : void 0;
33
- return query ? db.IR.collectCollectionSources(query._getQuery()).map(
34
- ({ collection }) => collection
35
- ) : [];
36
- }
37
21
  function getSourceQualifiedSuspenseKey(preparedValue, queryHash) {
38
- const sourceIds = getPreparedSources(preparedValue).map(getSourceObjectToken);
22
+ const sourceIds = sourceIdBindings.getPreparedSources(preparedValue).map(sourceIdBindings.getSourceObjectToken);
39
23
  if (sourceIds.length === 0) return queryHash;
40
24
  return `${sourceIds.join(`,`)}:${queryHash}`;
41
25
  }
@@ -171,10 +155,7 @@ function createHookInstance(dbClient) {
171
155
  warned: false
172
156
  },
173
157
  deferredCollections: /* @__PURE__ */ new Set(),
174
- sourceIds: {
175
- unscoped: /* @__PURE__ */ new Map(),
176
- byClient: /* @__PURE__ */ new WeakMap()
177
- },
158
+ sourceIds: sourceIdBindings.createSourceIdBindings(),
178
159
  observer: null,
179
160
  queryHash: void 0,
180
161
  suspenseKey: void 0,
@@ -189,19 +170,7 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
189
170
  const resolvedDeps = deps ?? [];
190
171
  const instanceRef = react.useRef(null);
191
172
  const instance = instanceRef.current ??= createHookInstance(dbClient);
192
- const resumeDeferredCollections = () => {
193
- const collections = Array.from(instance.deferredCollections);
194
- instance.deferredCollections.clear();
195
- const errors = [];
196
- for (const collection of collections) {
197
- try {
198
- collection._resumeSyncStart();
199
- } catch (error) {
200
- errors.push(error);
201
- }
202
- }
203
- if (errors.length) throw errors[0];
204
- };
173
+ const resumeDeferredCollections = () => sourceIdBindings.resumeSyncStarts(instance.deferredCollections);
205
174
  const queryKey = !inputIsCollection ? getExplicitQueryKey(configOrQueryOrCollection) : void 0;
206
175
  let preparedQueryValue = unpreparedQueryValue;
207
176
  let identityDeps;
@@ -266,22 +235,13 @@ function useLiveQueryImpl(configOrQueryOrCollection, deps, forSuspense = false)
266
235
  warnDeprecatedDepsArray();
267
236
  }
268
237
  if (queryKey === void 0 && deps === void 0 && preparedQueryValue !== unpreparedQueryValue) {
269
- const prior = dbClient ? instance.sourceIds.byClient.get(dbClient) : instance.sourceIds.unscoped;
270
- const seen = /* @__PURE__ */ new Map();
271
- for (const source of getPreparedSources(preparedQueryValue)) {
272
- const token = getSourceObjectToken(source);
273
- const previous = seen.get(source.id) ?? prior?.get(source.id);
274
- if (previous !== void 0 && previous !== token) {
275
- resumeDeferredCollections();
276
- throw new Error(
277
- `[useLiveQuery] 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.`
278
- );
279
- }
280
- seen.set(source.id, token);
281
- }
282
- const bindings = prior ?? /* @__PURE__ */ new Map();
283
- for (const [id, token] of seen) bindings.set(id, token);
284
- if (dbClient) instance.sourceIds.byClient.set(dbClient, bindings);
238
+ sourceIdBindings.claimSourceIds(
239
+ instance.sourceIds,
240
+ preparedQueryValue,
241
+ dbClient,
242
+ `useLiveQuery`,
243
+ resumeDeferredCollections
244
+ );
285
245
  }
286
246
  const canReuseSuspenseKey = forSuspense && !inputIsCollection && queryHash !== void 0 && instance.collection !== null && instance.client === dbClient && instance.queryHash === queryHash && instance.suspenseKey !== void 0;
287
247
  if (forSuspense && !inputIsCollection && queryHash && !canReuseSuspenseKey && preparedQueryValue === unpreparedQueryValue) {
@@ -327,7 +287,9 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
327
287
  instance.collection = db.resolveLiveQueryValue(preparedQueryValue, {
328
288
  gcTime: forSuspense ? DEFAULT_SUSPENSE_GC_TIME_MS : DEFAULT_GC_TIME_MS,
329
289
  // Hydration and Suspense key the live-query Collection by identity.
330
- pool: !forSuspense && !dbClient
290
+ pool: !forSuspense && !dbClient,
291
+ // Without a DbClient, Suspense starts it through a preload below.
292
+ startSync: !forSuspense || dbClient !== void 0
331
293
  });
332
294
  if (suspenseCollections && suspenseKey && instance.collection) {
333
295
  const collection = instance.collection;
@@ -376,6 +338,10 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
376
338
  });
377
339
  }
378
340
  const observer = instance.observer;
341
+ if (forSuspense && !dbClient && (instance.collection?.status === `idle` || instance.collection?.status === `cleaned-up`)) {
342
+ observer.preloadForInitialRender().catch(() => {
343
+ });
344
+ }
379
345
  if (!instance.subscribe || needsNewCollection) {
380
346
  instance.subscribe = (onStoreChange) => {
381
347
  const unsubscribe = observer.subscribe(onStoreChange);