@tanstack/react-db 0.2.1 → 0.3.1

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.
Files changed (52) hide show
  1. package/README.md +12 -0
  2. package/dist/cjs/DbProvider.cjs +23 -0
  3. package/dist/cjs/DbProvider.cjs.map +1 -0
  4. package/dist/cjs/DbProvider.d.cts +9 -0
  5. package/dist/cjs/HydrationBoundary.cjs +16 -0
  6. package/dist/cjs/HydrationBoundary.cjs.map +1 -0
  7. package/dist/cjs/HydrationBoundary.d.cts +7 -0
  8. package/dist/cjs/index.cjs +6 -0
  9. package/dist/cjs/index.cjs.map +1 -1
  10. package/dist/cjs/index.d.cts +4 -1
  11. package/dist/cjs/live-query-internals.cjs +19 -0
  12. package/dist/cjs/live-query-internals.cjs.map +1 -0
  13. package/dist/cjs/live-query-internals.d.cts +9 -0
  14. package/dist/cjs/useLiveInfiniteQuery.cjs +76 -8
  15. package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
  16. package/dist/cjs/useLiveInfiniteQuery.d.cts +12 -58
  17. package/dist/cjs/useLiveQuery.cjs +263 -41
  18. package/dist/cjs/useLiveQuery.cjs.map +1 -1
  19. package/dist/cjs/useLiveQuery.d.cts +79 -33
  20. package/dist/cjs/useLiveSuspenseQuery.cjs +19 -5
  21. package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
  22. package/dist/cjs/useLiveSuspenseQuery.d.cts +22 -19
  23. package/dist/esm/DbProvider.d.ts +9 -0
  24. package/dist/esm/DbProvider.js +23 -0
  25. package/dist/esm/DbProvider.js.map +1 -0
  26. package/dist/esm/HydrationBoundary.d.ts +7 -0
  27. package/dist/esm/HydrationBoundary.js +16 -0
  28. package/dist/esm/HydrationBoundary.js.map +1 -0
  29. package/dist/esm/index.d.ts +4 -1
  30. package/dist/esm/index.js +6 -0
  31. package/dist/esm/index.js.map +1 -1
  32. package/dist/esm/live-query-internals.d.ts +9 -0
  33. package/dist/esm/live-query-internals.js +19 -0
  34. package/dist/esm/live-query-internals.js.map +1 -0
  35. package/dist/esm/useLiveInfiniteQuery.d.ts +12 -58
  36. package/dist/esm/useLiveInfiniteQuery.js +76 -8
  37. package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
  38. package/dist/esm/useLiveQuery.d.ts +79 -33
  39. package/dist/esm/useLiveQuery.js +265 -43
  40. package/dist/esm/useLiveQuery.js.map +1 -1
  41. package/dist/esm/useLiveSuspenseQuery.d.ts +22 -19
  42. package/dist/esm/useLiveSuspenseQuery.js +19 -5
  43. package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
  44. package/package.json +2 -2
  45. package/skills/react-db/SKILL.md +107 -84
  46. package/src/DbProvider.tsx +32 -0
  47. package/src/HydrationBoundary.tsx +25 -0
  48. package/src/index.ts +8 -1
  49. package/src/live-query-internals.ts +36 -0
  50. package/src/useLiveInfiniteQuery.ts +119 -65
  51. package/src/useLiveQuery.ts +441 -92
  52. package/src/useLiveSuspenseQuery.ts +56 -29
@@ -1,28 +1,250 @@
1
+ "use client";
1
2
  "use strict";
2
3
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
4
  const react = require("react");
4
5
  const db = require("@tanstack/db");
6
+ const DbProvider = require("./DbProvider.cjs");
7
+ const liveQueryInternals = require("./live-query-internals.cjs");
5
8
  const DEFAULT_GC_TIME_MS = 1;
6
- function useLiveQuery(configOrQueryOrCollection, deps = []) {
9
+ const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16;
10
+ const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10;
11
+ const DERIVED_IDENTITY_TOTAL_WARN_MS = 50;
12
+ const warnedDepsCallsites = /* @__PURE__ */ new Set();
13
+ const warnedDerivedIdentityCallsites = /* @__PURE__ */ new Set();
14
+ const warnedUnhashableIdentityCallsites = /* @__PURE__ */ new Set();
15
+ const unpreparedQueryValue = /* @__PURE__ */ Symbol(`unpreparedQueryValue`);
16
+ function warnDeprecatedDepsArray(hookName = `useLiveQuery`) {
17
+ if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {
18
+ return;
19
+ }
20
+ const callsite = getWarningCallsite(4);
21
+ if (warnedDepsCallsites.has(callsite)) {
22
+ return;
23
+ }
24
+ warnedDepsCallsites.add(callsite);
25
+ const replacement = hookName === `useLiveQuery` ? `useLiveQuery({ query })` : `useLiveInfiniteQuery(query, { queryKey })`;
26
+ console.warn(
27
+ `[${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.`
28
+ );
29
+ }
30
+ function shouldWarnInDevelopment(disableEnvVar) {
31
+ if (typeof process === `undefined`) {
32
+ return false;
33
+ }
34
+ return process.env.NODE_ENV !== `production` && process.env[disableEnvVar] !== `1`;
35
+ }
36
+ function getCurrentTime() {
37
+ return typeof performance !== `undefined` && typeof performance.now === `function` ? performance.now() : Date.now();
38
+ }
39
+ function getWarningCallsite(stackIndex) {
40
+ const stack = new Error().stack ?? `unknown`;
41
+ return stack.split(`
42
+ `)[stackIndex]?.trim() ?? stack;
43
+ }
44
+ function warnDerivedIdentityHotPath(profiler, durationMs) {
45
+ if (profiler.warned || !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {
46
+ return;
47
+ }
48
+ const isSlowSingleRender = durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS;
49
+ const isHotRenderPath = profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD && profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS;
50
+ if (!isSlowSingleRender && !isHotRenderPath) {
51
+ return;
52
+ }
53
+ const callsite = getWarningCallsite(5);
54
+ if (warnedDerivedIdentityCallsites.has(callsite)) {
55
+ profiler.warned = true;
56
+ return;
57
+ }
58
+ warnedDerivedIdentityCallsites.add(callsite);
59
+ profiler.warned = true;
60
+ const reason = isSlowSingleRender ? `one render took ${durationMs.toFixed(1)}ms` : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`;
61
+ console.warn(
62
+ `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`
63
+ );
64
+ }
65
+ function getExplicitQueryKey(value) {
66
+ return value && typeof value === `object` && Array.isArray(value.queryKey) ? value.queryKey : void 0;
67
+ }
68
+ function getExplicitDbClient(value) {
69
+ return value && typeof value === `object` && `client` in value && value.client !== void 0 ? value.client : void 0;
70
+ }
71
+ function prepareQueryValue(value, dbClient, deferredCollections) {
72
+ return db.prepareLiveQueryValue(value, dbClient, deferredCollections);
73
+ }
74
+ function prepareDerivedQuery(value, dbClient, profiler, deferredCollections) {
75
+ const shouldProfile = shouldWarnInDevelopment(
76
+ `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`
77
+ );
78
+ const start = shouldProfile ? getCurrentTime() : 0;
79
+ const preparedValue = prepareQueryValue(value, dbClient, deferredCollections);
80
+ try {
81
+ const identity = db.getPreparedLiveQueryIdentity(preparedValue);
82
+ return {
83
+ status: `hashable`,
84
+ value: preparedValue,
85
+ identityDeps: [`derived`, identity]
86
+ };
87
+ } catch (error) {
88
+ if (error instanceof db.UnhashableQueryIRError) {
89
+ return { status: `unhashable`, value: preparedValue, error };
90
+ }
91
+ throw error;
92
+ } finally {
93
+ if (shouldProfile) {
94
+ const durationMs = getCurrentTime() - start;
95
+ profiler.renderCount += 1;
96
+ profiler.totalMs += durationMs;
97
+ profiler.maxMs = Math.max(profiler.maxMs, durationMs);
98
+ warnDerivedIdentityHotPath(profiler, durationMs);
99
+ }
100
+ }
101
+ }
102
+ function warnUnhashableDerivedIdentity(error) {
103
+ if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {
104
+ return;
105
+ }
106
+ const callsite = getWarningCallsite(4);
107
+ if (warnedUnhashableIdentityCallsites.has(callsite)) {
108
+ return;
109
+ }
110
+ warnedUnhashableIdentityCallsites.add(callsite);
111
+ console.warn(
112
+ `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. Unhashable queries without queryKey will throw in 1.0.`
113
+ );
114
+ }
115
+ function createCollectionFromPreparedQuery(value) {
116
+ if (value === void 0 || value === null) {
117
+ return null;
118
+ }
119
+ if (db.isCollection(value)) {
120
+ value.startSyncImmediate();
121
+ return value;
122
+ }
123
+ if (value instanceof db.BaseQueryBuilder) {
124
+ return db.createLiveQueryCollection({
125
+ query: value,
126
+ startSync: true,
127
+ gcTime: DEFAULT_GC_TIME_MS
128
+ });
129
+ }
130
+ if (typeof value === `object`) {
131
+ return db.createLiveQueryCollection({
132
+ startSync: true,
133
+ gcTime: DEFAULT_GC_TIME_MS,
134
+ ...value
135
+ });
136
+ }
137
+ throw new Error(
138
+ `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof value}`
139
+ );
140
+ }
141
+ function useLiveQuery(configOrQueryOrCollection, deps) {
142
+ const contextDbClient = DbProvider.useOptionalDbClient();
7
143
  const inputIsCollection = db.isCollection(configOrQueryOrCollection);
144
+ const dbClient = inputIsCollection ? contextDbClient : getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient;
145
+ const resolvedDeps = deps ?? [];
8
146
  const collectionRef = react.useRef(
9
147
  null
10
148
  );
11
149
  const depsRef = react.useRef(null);
12
150
  const configRef = react.useRef(null);
151
+ const clientRef = react.useRef(dbClient);
152
+ const legacyUnhashableIdentityRef = react.useRef([
153
+ `legacy-unhashable`
154
+ ]);
155
+ const derivedIdentityProfilerRef = react.useRef({
156
+ renderCount: 0,
157
+ totalMs: 0,
158
+ maxMs: 0,
159
+ warned: false
160
+ });
161
+ const deferredCollectionsRef = react.useRef(
162
+ /* @__PURE__ */ new Set()
163
+ );
13
164
  const observerRef = react.useRef(
14
165
  null
15
166
  );
16
- const needsNewCollection = !collectionRef.current || inputIsCollection && configRef.current !== configOrQueryOrCollection || !inputIsCollection && (depsRef.current === null || depsRef.current.length !== deps.length || depsRef.current.some((dep, i) => dep !== deps[i]));
167
+ const queryHashRef = react.useRef(void 0);
168
+ const identityErrorRef = react.useRef(void 0);
169
+ const queryKey = !inputIsCollection ? getExplicitQueryKey(configOrQueryOrCollection) : void 0;
170
+ let preparedQueryValue = unpreparedQueryValue;
171
+ let identityDeps;
172
+ let streamIdentity = void 0;
173
+ let identityError;
174
+ if (queryKey) {
175
+ identityDeps = queryKey;
176
+ streamIdentity = [`queryKey`, queryKey];
177
+ } else if (deps !== void 0) {
178
+ identityDeps = resolvedDeps;
179
+ try {
180
+ preparedQueryValue = prepareQueryValue(
181
+ configOrQueryOrCollection,
182
+ dbClient,
183
+ deferredCollectionsRef.current
184
+ );
185
+ streamIdentity = [
186
+ `deps`,
187
+ resolvedDeps,
188
+ db.getPreparedLiveQueryIdentity(preparedQueryValue)
189
+ ];
190
+ } catch (error) {
191
+ if (!(error instanceof db.UnhashableQueryIRError)) throw error;
192
+ warnUnhashableDerivedIdentity(error);
193
+ identityError = error;
194
+ }
195
+ } else if (inputIsCollection) {
196
+ identityDeps = [];
197
+ streamIdentity = [`collection`, configOrQueryOrCollection.id];
198
+ } else {
199
+ const preparation = prepareDerivedQuery(
200
+ configOrQueryOrCollection,
201
+ dbClient,
202
+ derivedIdentityProfilerRef.current,
203
+ deferredCollectionsRef.current
204
+ );
205
+ preparedQueryValue = preparation.value;
206
+ if (preparation.status === `hashable`) {
207
+ identityDeps = preparation.identityDeps;
208
+ streamIdentity = preparation.identityDeps;
209
+ } else {
210
+ warnUnhashableDerivedIdentity(preparation.error);
211
+ identityDeps = legacyUnhashableIdentityRef.current;
212
+ identityError = preparation.error;
213
+ }
214
+ }
215
+ let queryHash;
216
+ if (streamIdentity !== void 0) {
217
+ try {
218
+ queryHash = db.getStableValueHash(streamIdentity, `queryKey`);
219
+ } catch (error) {
220
+ if (error instanceof db.UnhashableQueryIRError) {
221
+ if (queryKey !== void 0) throw error;
222
+ identityError = error;
223
+ } else {
224
+ throw error;
225
+ }
226
+ }
227
+ }
228
+ if (deps !== void 0) {
229
+ warnDeprecatedDepsArray();
230
+ }
231
+ const identityChanged = depsRef.current === null || (deps !== void 0 ? depsRef.current.length !== identityDeps.length || depsRef.current.some((dep, index) => dep !== identityDeps[index]) : !db.deepEquals(depsRef.current, identityDeps));
232
+ const needsNewCollection = !collectionRef.current || inputIsCollection && configRef.current !== configOrQueryOrCollection || !inputIsCollection && (clientRef.current !== dbClient || identityChanged);
233
+ const resumeDeferredCollections = () => {
234
+ for (const collection of deferredCollectionsRef.current) {
235
+ collection._resumeSyncStart();
236
+ }
237
+ deferredCollectionsRef.current.clear();
238
+ };
17
239
  if (needsNewCollection) {
18
240
  if (inputIsCollection) {
19
241
  const syncMode = configOrQueryOrCollection.config?.syncMode;
20
- if (syncMode === `on-demand`) {
242
+ if (syncMode === `on-demand` && shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {
21
243
  console.warn(
22
244
  `[useLiveQuery] Warning: Passing a collection with syncMode "on-demand" directly to useLiveQuery will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.
23
245
 
24
246
  Instead, use a query builder function:
25
- const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))
247
+ const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })
26
248
 
27
249
  Or switch to syncMode "eager" if you want all data to sync automatically.`
28
250
  );
@@ -31,56 +253,56 @@ Or switch to syncMode "eager" if you want all data to sync automatically.`
31
253
  collectionRef.current = configOrQueryOrCollection;
32
254
  configRef.current = configOrQueryOrCollection;
33
255
  } else {
34
- if (typeof configOrQueryOrCollection === `function`) {
35
- const queryBuilder = new db.BaseQueryBuilder();
36
- const result = configOrQueryOrCollection(queryBuilder);
37
- if (result === void 0 || result === null) {
38
- collectionRef.current = null;
39
- } else if (db.isCollection(result)) {
40
- result.startSyncImmediate();
41
- collectionRef.current = result;
42
- } else if (result instanceof db.BaseQueryBuilder) {
43
- collectionRef.current = db.createLiveQueryCollection({
44
- query: configOrQueryOrCollection,
45
- startSync: true,
46
- gcTime: DEFAULT_GC_TIME_MS
47
- });
48
- } else if (result && typeof result === `object`) {
49
- collectionRef.current = db.createLiveQueryCollection({
50
- startSync: true,
51
- gcTime: DEFAULT_GC_TIME_MS,
52
- ...result
53
- });
54
- } else {
55
- throw new Error(
56
- `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`
57
- );
58
- }
59
- depsRef.current = [...deps];
60
- } else {
61
- collectionRef.current = db.createLiveQueryCollection({
62
- startSync: true,
63
- gcTime: DEFAULT_GC_TIME_MS,
64
- ...configOrQueryOrCollection
65
- });
66
- depsRef.current = [...deps];
256
+ if (preparedQueryValue === unpreparedQueryValue) {
257
+ preparedQueryValue = prepareQueryValue(
258
+ configOrQueryOrCollection,
259
+ dbClient,
260
+ deferredCollectionsRef.current
261
+ );
67
262
  }
263
+ collectionRef.current = createCollectionFromPreparedQuery(
264
+ preparedQueryValue
265
+ );
266
+ configRef.current = configOrQueryOrCollection;
267
+ depsRef.current = [...identityDeps];
68
268
  }
269
+ clientRef.current = dbClient;
270
+ queryHashRef.current = queryHash;
271
+ identityErrorRef.current = identityError;
69
272
  }
70
273
  if (needsNewCollection) {
71
274
  observerRef.current = db.createLiveQueryObserver(collectionRef.current, {
72
- mode: `wholesale`
275
+ mode: `wholesale`,
276
+ client: dbClient,
277
+ queryHash: queryHashRef.current,
278
+ onPreload: resumeDeferredCollections
73
279
  });
74
280
  }
75
281
  const observer = observerRef.current;
76
282
  const subscribeRef = react.useRef(null);
77
283
  if (!subscribeRef.current || needsNewCollection) {
78
- subscribeRef.current = (onStoreChange) => observer.subscribe(() => onStoreChange());
284
+ subscribeRef.current = (onStoreChange) => {
285
+ const unsubscribe = observer.subscribe(() => onStoreChange());
286
+ resumeDeferredCollections();
287
+ return unsubscribe;
288
+ };
79
289
  }
80
- return react.useSyncExternalStore(
290
+ const returned = react.useSyncExternalStore(
81
291
  subscribeRef.current,
82
- () => observer.getSnapshot()
292
+ () => observer.getSnapshot(),
293
+ () => observer.getServerSnapshot()
83
294
  );
295
+ liveQueryInternals.setLiveQueryResultInfo(returned, {
296
+ client: dbClient,
297
+ queryHash: queryHashRef.current,
298
+ identityError: identityErrorRef.current,
299
+ observer
300
+ });
301
+ return returned;
84
302
  }
303
+ exports.prepareDerivedQuery = prepareDerivedQuery;
304
+ exports.prepareQueryValue = prepareQueryValue;
85
305
  exports.useLiveQuery = useLiveQuery;
306
+ exports.warnDeprecatedDepsArray = warnDeprecatedDepsArray;
307
+ exports.warnUnhashableDerivedIdentity = warnUnhashableDerivedIdentity;
86
308
  //# sourceMappingURL=useLiveQuery.cjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["import { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n createLiveQueryCollection,\n createLiveQueryObserver,\n isCollection,\n} from '@tanstack/db'\nimport type {\n Collection,\n CollectionStatus,\n Context,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\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\nexport type UseLiveQueryStatus = CollectionStatus | `disabled`\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 - Array of dependencies that trigger query re-execution when changed\n * @returns Object with reactive data, state, and status information\n * @example\n * // Basic query with object syntax\n * const { data, isLoading } = useLiveQuery((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 * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => eq(todos.id, 1))\n * .findOne()\n * )\n *\n * @example\n * // With dependencies that trigger re-execution\n * const { data, state } = useLiveQuery(\n * (q) => q.from({ todos: todosCollection })\n * .where(({ todos }) => gt(todos.priority, minPriority)),\n * [minPriority] // Re-run when minPriority changes\n * )\n *\n * @example\n * // Join pattern\n * const { data } = useLiveQuery((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((q) =>\n * 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 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 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 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 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>>\n | Map<TKey, TResult>\n | 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 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 - 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({ query: queryBuilder })\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: 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 for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\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 7: 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 isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 8: 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 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 // Check if it's already a collection\n const inputIsCollection = isCollection(configOrQueryOrCollection)\n\n // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null,\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n\n // The shared observer owns subscription, the ready-race, and the snapshot.\n const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(\n null,\n )\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||\n (!inputIsCollection &&\n (depsRef.current === null ||\n depsRef.current.length !== deps.length ||\n depsRef.current.some((dep, i) => dep !== deps[i])))\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 (syncMode === `on-demand`) {\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((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 collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n // Handle different callback return types\n if (typeof configOrQueryOrCollection === `function`) {\n // Call the function with a query builder to see what it returns\n const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder\n const result = configOrQueryOrCollection(queryBuilder)\n\n if (result === undefined || result === null) {\n // Callback returned undefined/null - disabled query\n collectionRef.current = null\n } else if (isCollection(result)) {\n // Callback returned a Collection instance - use it directly\n result.startSyncImmediate()\n collectionRef.current = result\n } else if (result instanceof BaseQueryBuilder) {\n // Callback returned QueryBuilder - create live query collection using the original callback\n // (not the result, since the result might be from a different query builder instance)\n collectionRef.current = createLiveQueryCollection({\n query: configOrQueryOrCollection,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n } else if (result && typeof result === `object`) {\n // Assume it's a LiveQueryCollectionConfig\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...result,\n })\n } else {\n // Unexpected return type\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,\n )\n }\n depsRef.current = [...deps]\n } else {\n // Original logic for config objects\n collectionRef.current = createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...configOrQueryOrCollection,\n })\n depsRef.current = [...deps]\n }\n }\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 observerRef.current = createLiveQueryObserver(collectionRef.current, {\n mode: `wholesale`,\n })\n }\n const observer = observerRef.current!\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange) =>\n observer.subscribe(() => onStoreChange())\n }\n\n // The observer returns a stable snapshot per revision, which is the return\n // shape this hook exposes. Keep the return loose to satisfy the overloads.\n return useSyncExternalStore(subscribeRef.current, () =>\n observer.getSnapshot(),\n ) as any\n}\n"],"names":["isCollection","useRef","BaseQueryBuilder","createLiveQueryCollection","createLiveQueryObserver","useSyncExternalStore"],"mappings":";;;;AAqBA,MAAM,qBAAqB;AAuSpB,SAAS,aACd,2BACA,OAAuB,IACvB;AAEA,QAAM,oBAAoBA,GAAAA,aAAa,yBAAyB;AAGhE,QAAM,gBAAgBC,MAAAA;AAAAA,IACpB;AAAA,EAAA;AAEF,QAAM,UAAUA,MAAAA,OAA8B,IAAI;AAClD,QAAM,YAAYA,MAAAA,OAAgB,IAAI;AAGtC,QAAM,cAAcA,MAAAA;AAAAA,IAClB;AAAA,EAAA;AAIF,QAAM,qBACJ,CAAC,cAAc,WACd,qBAAqB,UAAU,YAAY,6BAC3C,CAAC,sBACC,QAAQ,YAAY,QACnB,QAAQ,QAAQ,WAAW,KAAK,UAChC,QAAQ,QAAQ,KAAK,CAAC,KAAK,MAAM,QAAQ,KAAK,CAAC,CAAC;AAEtD,MAAI,oBAAoB;AACtB,QAAI,mBAAmB;AAIrB,YAAM,WACJ,0BACA,QAAQ;AACV,UAAI,aAAa,aAAa;AAC5B,gBAAQ;AAAA,UACN;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,QAAA;AAAA,MAMJ;AAEA,gCAA0B,mBAAA;AAC1B,oBAAc,UAAU;AACxB,gBAAU,UAAU;AAAA,IACtB,OAAO;AAEL,UAAI,OAAO,8BAA8B,YAAY;AAEnD,cAAM,eAAe,IAAIC,oBAAA;AACzB,cAAM,SAAS,0BAA0B,YAAY;AAErD,YAAI,WAAW,UAAa,WAAW,MAAM;AAE3C,wBAAc,UAAU;AAAA,QAC1B,WAAWF,gBAAa,MAAM,GAAG;AAE/B,iBAAO,mBAAA;AACP,wBAAc,UAAU;AAAA,QAC1B,WAAW,kBAAkBE,qBAAkB;AAG7C,wBAAc,UAAUC,6BAA0B;AAAA,YAChD,OAAO;AAAA,YACP,WAAW;AAAA,YACX,QAAQ;AAAA,UAAA,CACT;AAAA,QACH,WAAW,UAAU,OAAO,WAAW,UAAU;AAE/C,wBAAc,UAAUA,6BAA0B;AAAA,YAChD,WAAW;AAAA,YACX,QAAQ;AAAA,YACR,GAAG;AAAA,UAAA,CACJ;AAAA,QACH,OAAO;AAEL,gBAAM,IAAI;AAAA,YACR,qHAAqH,OAAO,MAAM;AAAA,UAAA;AAAA,QAEtI;AACA,gBAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,MAC5B,OAAO;AAEL,sBAAc,UAAUA,6BAA0B;AAAA,UAChD,WAAW;AAAA,UACX,QAAQ;AAAA,UACR,GAAG;AAAA,QAAA,CACJ;AACD,gBAAQ,UAAU,CAAC,GAAG,IAAI;AAAA,MAC5B;AAAA,IACF;AAAA,EACF;AAQA,MAAI,oBAAoB;AAOtB,gBAAY,UAAUC,2BAAwB,cAAc,SAAS;AAAA,MACnE,MAAM;AAAA,IAAA,CACP;AAAA,EACH;AACA,QAAM,WAAW,YAAY;AAI7B,QAAM,eAAeH,MAAAA,OAEnB,IAAI;AACN,MAAI,CAAC,aAAa,WAAW,oBAAoB;AAC/C,iBAAa,UAAU,CAAC,kBACtB,SAAS,UAAU,MAAM,eAAe;AAAA,EAC5C;AAIA,SAAOI,MAAAA;AAAAA,IAAqB,aAAa;AAAA,IAAS,MAChD,SAAS,YAAA;AAAA,EAAY;AAEzB;;"}
1
+ {"version":3,"file":"useLiveQuery.cjs","sources":["../../src/useLiveQuery.ts"],"sourcesContent":["'use client'\n\nimport { useRef, useSyncExternalStore } from 'react'\nimport {\n BaseQueryBuilder,\n UnhashableQueryIRError,\n createLiveQueryCollection,\n createLiveQueryObserver,\n deepEquals,\n getPreparedLiveQueryIdentity,\n getStableValueHash,\n isCollection,\n prepareLiveQueryValue,\n} from '@tanstack/db'\nimport { useOptionalDbClient } from './DbProvider'\nimport { setLiveQueryResultInfo } from './live-query-internals'\nimport type {\n Collection,\n CollectionImpl,\n CollectionStatus,\n Context,\n DbClient,\n GetResult,\n InferResultType,\n InitialQueryBuilder,\n LiveQueryCollectionConfig,\n LiveQueryObserver,\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)\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\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>\nexport type UseLiveQueryConfig<TContext extends Context> =\n LiveQueryCollectionConfig<TContext> & {\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\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 shouldWarnInDevelopment(disableEnvVar: string): boolean {\n if (typeof process === `undefined`) {\n return false\n }\n\n return (\n process.env.NODE_ENV !== `production` && process.env[disableEnvVar] !== `1`\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 return stack.split(`\\n`)[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\nfunction createCollectionFromPreparedQuery(value: unknown) {\n if (value === undefined || value === null) {\n return null\n }\n\n if (isCollection(value)) {\n value.startSyncImmediate()\n return value\n }\n\n if (value instanceof BaseQueryBuilder) {\n return createLiveQueryCollection({\n query: value,\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n })\n }\n\n if (typeof value === `object`) {\n return createLiveQueryCollection({\n startSync: true,\n gcTime: DEFAULT_GC_TIME_MS,\n ...(value as LiveQueryCollectionConfig<any>),\n })\n }\n\n throw new Error(\n `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof value}`,\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 * // 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 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 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 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 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>>\n | Map<TKey, TResult>\n | 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 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 for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\n}\n\n// Overload 7: 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 for config objects\n isLoading: boolean\n isReady: boolean\n isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for config objects\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 8: 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 isIdle: boolean\n isError: boolean\n isCleanedUp: boolean\n isEnabled: true // Always true for pre-created live query collections\n}\n\n// Overload 9: 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 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 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 // Use refs to cache collection and track dependencies\n const collectionRef = useRef<Collection<object, string | number, {}> | null>(\n null,\n )\n const depsRef = useRef<Array<unknown> | null>(null)\n const configRef = useRef<unknown>(null)\n const clientRef = useRef(dbClient)\n const legacyUnhashableIdentityRef = useRef<Array<unknown>>([\n `legacy-unhashable`,\n ])\n\n const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({\n renderCount: 0,\n totalMs: 0,\n maxMs: 0,\n warned: false,\n })\n const deferredCollectionsRef = useRef(\n new Set<CollectionImpl<any, string | number, any, any, any>>(),\n )\n const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(\n null,\n )\n const queryHashRef = useRef<string | undefined>(undefined)\n const identityErrorRef = useRef<UnhashableQueryIRError | undefined>(undefined)\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 try {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n deferredCollectionsRef.current,\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 derivedIdentityProfilerRef.current,\n deferredCollectionsRef.current,\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 = legacyUnhashableIdentityRef.current\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 const identityChanged =\n depsRef.current === null ||\n (deps !== undefined\n ? depsRef.current.length !== identityDeps.length ||\n depsRef.current.some((dep, index) => dep !== identityDeps[index])\n : !deepEquals(depsRef.current, identityDeps))\n\n // Check if we need to create/recreate the collection\n const needsNewCollection =\n !collectionRef.current ||\n (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||\n (!inputIsCollection && (clientRef.current !== dbClient || identityChanged))\n\n const resumeDeferredCollections = () => {\n for (const collection of deferredCollectionsRef.current) {\n collection._resumeSyncStart()\n }\n deferredCollectionsRef.current.clear()\n }\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 collectionRef.current = configOrQueryOrCollection\n configRef.current = configOrQueryOrCollection\n } else {\n if (preparedQueryValue === unpreparedQueryValue) {\n preparedQueryValue = prepareQueryValue(\n configOrQueryOrCollection,\n dbClient,\n deferredCollectionsRef.current,\n )\n }\n collectionRef.current = createCollectionFromPreparedQuery(\n preparedQueryValue,\n ) as Collection<object, string | number, {}>\n configRef.current = configOrQueryOrCollection\n depsRef.current = [...identityDeps]\n }\n clientRef.current = dbClient\n queryHashRef.current = queryHash\n identityErrorRef.current = 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 observerRef.current = createLiveQueryObserver(collectionRef.current, {\n mode: `wholesale`,\n client: dbClient,\n queryHash: queryHashRef.current,\n onPreload: resumeDeferredCollections,\n })\n }\n const observer = observerRef.current!\n\n // Stable subscribe bound to the current observer; the observer owns the\n // subscription, ready-race, and disposal.\n const subscribeRef = useRef<\n ((onStoreChange: () => void) => () => void) | null\n >(null)\n if (!subscribeRef.current || needsNewCollection) {\n subscribeRef.current = (onStoreChange: () => void) => {\n const unsubscribe = observer.subscribe(() => onStoreChange())\n resumeDeferredCollections()\n return unsubscribe\n }\n }\n\n const returned = useSyncExternalStore(\n subscribeRef.current,\n () => observer.getSnapshot(),\n () => observer.getServerSnapshot(),\n )\n setLiveQueryResultInfo(returned, {\n client: dbClient,\n queryHash: queryHashRef.current,\n identityError: identityErrorRef.current,\n observer,\n })\n return returned as any\n}\n"],"names":["useRef","useSyncExternalStore","setLiveQueryResultInfo"],"mappings":";;;;;;;AAgCA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AAuBO;AAGL;AACE;AAAA;AAGF;AACA;AACE;AAAA;AAEF;AACA;AAIA;AAAQ;AAC8F;AAExG;AAEA;AACE;AACE;AAAO;AAGT;AAGF;AAEA;AACE;AAIF;AAEA;AACE;AACA;AAAmB;AACrB;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;AAEA;AACE;AACE;AAAO;AAGT;AACE;AACA;AAAO;AAGT;AACE;AAAiC;AACxB;AACI;AACH;AACT;AAGH;AACE;AAAiC;AACpB;AACH;AACJ;AACL;AAGH;AAAU;AACyH;AAErI;AAwTO;AAIL;AAEA;AACA;AAGA;AAGA;AAAsBA;AACpB;AAEF;AACA;AACA;AACA;AAA2D;AACzD;AAGF;AAAmE;AACpD;AACJ;AACF;AACC;AAEV;AAA+BA;AACzB;AAEN;AAAoBA;AAClB;AAEF;AACA;AAEA;AAGA;AAEA;AACA;AACA;AAEA;AACE;AACA;AAAsC;AAEtC;AACA;AACE;AAAqB;AACnB;AACA;AACuB;AAEzB;AAAiB;AACf;AACA;AAC+C;AAAA;AAGjD;AACA;AACA;AAAgB;AAClB;AAEA;AACA;AAA4D;AAE5D;AAAoB;AAClB;AACA;AAC2B;AACJ;AAEzB;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;AAQA;AAKA;AACE;AACE;AAAW;AAEb;AAA+B;AAGjC;AACE;AAIE;AAGA;AAIE;AAAQ;;AACN;AAAA;;AAAA;AAAA;AAAA;AAQJ;AACA;AACA;AAAoB;AAEpB;AACE;AAAqB;AACnB;AACA;AACuB;AAAA;AAG3B;AAAwB;AACtB;AAEF;AACA;AAAkC;AAEpC;AACA;AACA;AAA2B;AAS7B;AAOE;AAAqE;AAC7D;AACE;AACgB;AACb;AACZ;AAEH;AAIA;AAGA;AACE;AACE;AACA;AACA;AAAO;AACT;AAGF;AAAiBC;AACF;AACE;AACA;AAEjBC;AAAiC;AACvB;AACgB;AACQ;AAChC;AAEF;AACF;;;;;;"}
@@ -1,53 +1,84 @@
1
- import { Collection, CollectionStatus, Context, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
1
+ import { UnhashableQueryIRError, Collection, CollectionImpl, CollectionStatus, Context, DbClient, GetResult, InferResultType, InitialQueryBuilder, LiveQueryCollectionConfig, NonSingleResult, QueryBuilder, SingleResult } from '@tanstack/db';
2
+ export type DerivedIdentityProfiler = {
3
+ renderCount: number;
4
+ totalMs: number;
5
+ maxMs: number;
6
+ warned: boolean;
7
+ };
2
8
  export type UseLiveQueryStatus = CollectionStatus | `disabled`;
9
+ export type LiveQueryKey = ReadonlyArray<unknown>;
10
+ export type UseLiveQueryConfig<TContext extends Context> = LiveQueryCollectionConfig<TContext> & {
11
+ /**
12
+ * Explicit identity for queries that contain opaque functional variants or
13
+ * are hot enough that deriving identity from structured IR is too expensive.
14
+ * Structured queries should omit this so DB can derive identity directly.
15
+ */
16
+ queryKey?: LiveQueryKey;
17
+ /** Override the nearest DbProvider for this query. */
18
+ client?: DbClient;
19
+ };
20
+ export declare function warnDeprecatedDepsArray(hookName?: `useLiveQuery` | `useLiveInfiniteQuery`): void;
21
+ export declare function prepareQueryValue(value: unknown, dbClient: DbClient | undefined, deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>): unknown;
22
+ type DerivedQueryPreparation = {
23
+ status: `hashable`;
24
+ value: unknown;
25
+ identityDeps: Array<unknown>;
26
+ } | {
27
+ status: `unhashable`;
28
+ value: unknown;
29
+ error: UnhashableQueryIRError;
30
+ };
31
+ export declare function prepareDerivedQuery(value: unknown, dbClient: DbClient | undefined, profiler: DerivedIdentityProfiler, deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>): DerivedQueryPreparation;
32
+ export declare function warnUnhashableDerivedIdentity(error: UnhashableQueryIRError): void;
3
33
  /**
4
- * Create a live query using a query function
34
+ * Create a live query using a query function.
5
35
  * @param queryFn - Query function that defines what data to fetch
6
- * @param deps - Array of dependencies that trigger query re-execution when changed
36
+ * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
7
37
  * @returns Object with reactive data, state, and status information
8
38
  * @example
9
- * // Basic query with object syntax
10
- * const { data, isLoading } = useLiveQuery((q) =>
11
- * q.from({ todos: todosCollection })
12
- * .where(({ todos }) => eq(todos.completed, false))
13
- * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
14
- * )
39
+ * // Prefer config object syntax
40
+ * const { data, isLoading } = useLiveQuery({
41
+ * query: (q) =>
42
+ * q.from({ todos: todosCollection })
43
+ * .where(({ todos }) => eq(todos.completed, false))
44
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
45
+ * })
15
46
  *
16
47
  * @example
17
48
  * // Single result query
18
- * const { data } = useLiveQuery(
19
- * (q) => q.from({ todos: todosCollection })
49
+ * const { data } = useLiveQuery({
50
+ * query: (q) => q.from({ todos: todosCollection })
20
51
  * .where(({ todos }) => eq(todos.id, 1))
21
52
  * .findOne()
22
- * )
53
+ * })
23
54
  *
24
55
  * @example
25
- * // With dependencies that trigger re-execution
26
- * const { data, state } = useLiveQuery(
27
- * (q) => q.from({ todos: todosCollection })
56
+ * // Structured captured values are included in derived query identity
57
+ * const { data, state } = useLiveQuery({
58
+ * query: (q) => q.from({ todos: todosCollection })
28
59
  * .where(({ todos }) => gt(todos.priority, minPriority)),
29
- * [minPriority] // Re-run when minPriority changes
30
- * )
60
+ * })
31
61
  *
32
62
  * @example
33
63
  * // Join pattern
34
- * const { data } = useLiveQuery((q) =>
35
- * q.from({ issues: issueCollection })
36
- * .join({ persons: personCollection }, ({ issues, persons }) =>
37
- * eq(issues.userId, persons.id)
38
- * )
39
- * .select(({ issues, persons }) => ({
40
- * id: issues.id,
41
- * title: issues.title,
42
- * userName: persons.name
43
- * }))
44
- * )
64
+ * const { data } = useLiveQuery({
65
+ * query: (q) =>
66
+ * q.from({ issues: issueCollection })
67
+ * .join({ persons: personCollection }, ({ issues, persons }) =>
68
+ * eq(issues.userId, persons.id)
69
+ * )
70
+ * .select(({ issues, persons }) => ({
71
+ * id: issues.id,
72
+ * title: issues.title,
73
+ * userName: persons.name
74
+ * }))
75
+ * })
45
76
  *
46
77
  * @example
47
78
  * // Handle loading and error states
48
- * const { data, isLoading, isError, status } = useLiveQuery((q) =>
49
- * q.from({ todos: todoCollection })
50
- * )
79
+ * const { data, isLoading, isError, status } = useLiveQuery({
80
+ * query: (q) => q.from({ todos: todoCollection })
81
+ * })
51
82
  *
52
83
  * if (isLoading) return <div>Loading...</div>
53
84
  * if (isError) return <div>Error: {status}</div>
@@ -121,7 +152,7 @@ export declare function useLiveQuery<TContext extends Context, TResult extends o
121
152
  /**
122
153
  * Create a live query using configuration object
123
154
  * @param config - Configuration object with query and options
124
- * @param deps - Array of dependencies that trigger query re-execution when changed
155
+ * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
125
156
  * @returns Object with reactive data, state, and status information
126
157
  * @example
127
158
  * // Basic config object usage
@@ -137,7 +168,9 @@ export declare function useLiveQuery<TContext extends Context, TResult extends o
137
168
  * .where(({ persons }) => gt(persons.age, 30))
138
169
  * .select(({ persons }) => ({ id: persons.id, name: persons.name }))
139
170
  *
140
- * const { data, isReady } = useLiveQuery({ query: queryBuilder })
171
+ * const { data, isReady } = useLiveQuery({
172
+ * query: queryBuilder,
173
+ * })
141
174
  *
142
175
  * @example
143
176
  * // Handle all states uniformly
@@ -151,6 +184,18 @@ export declare function useLiveQuery<TContext extends Context, TResult extends o
151
184
  *
152
185
  * return <div>{data.length} items loaded</div>
153
186
  */
187
+ export declare function useLiveQuery<TContext extends Context>(config: UseLiveQueryConfig<TContext>): {
188
+ state: Map<string | number, GetResult<TContext>>;
189
+ data: InferResultType<TContext>;
190
+ collection: Collection<GetResult<TContext>, string | number, {}>;
191
+ status: CollectionStatus;
192
+ isLoading: boolean;
193
+ isReady: boolean;
194
+ isIdle: boolean;
195
+ isError: boolean;
196
+ isCleanedUp: boolean;
197
+ isEnabled: true;
198
+ };
154
199
  export declare function useLiveQuery<TContext extends Context>(config: LiveQueryCollectionConfig<TContext>, deps?: Array<unknown>): {
155
200
  state: Map<string | number, GetResult<TContext>>;
156
201
  data: InferResultType<TContext>;
@@ -216,3 +261,4 @@ export declare function useLiveQuery<TResult extends object, TKey extends string
216
261
  isCleanedUp: boolean;
217
262
  isEnabled: true;
218
263
  };
264
+ export {};