@tanstack/react-db 0.2.0 → 0.3.0

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 +119 -90
  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,14 +1,25 @@
1
+ 'use client'
2
+
1
3
  import { useRef, useSyncExternalStore } from 'react'
2
4
  import {
3
5
  BaseQueryBuilder,
6
+ UnhashableQueryIRError,
4
7
  createLiveQueryCollection,
5
8
  createLiveQueryObserver,
9
+ deepEquals,
10
+ getPreparedLiveQueryIdentity,
11
+ getStableValueHash,
6
12
  isCollection,
13
+ prepareLiveQueryValue,
7
14
  } from '@tanstack/db'
15
+ import { useOptionalDbClient } from './DbProvider'
16
+ import { setLiveQueryResultInfo } from './live-query-internals'
8
17
  import type {
9
18
  Collection,
19
+ CollectionImpl,
10
20
  CollectionStatus,
11
21
  Context,
22
+ DbClient,
12
23
  GetResult,
13
24
  InferResultType,
14
25
  InitialQueryBuilder,
@@ -20,57 +31,291 @@ import type {
20
31
  } from '@tanstack/db'
21
32
 
22
33
  const DEFAULT_GC_TIME_MS = 1 // Live queries created by useLiveQuery are cleaned up immediately (0 disables GC)
34
+ const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16
35
+ const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10
36
+ const DERIVED_IDENTITY_TOTAL_WARN_MS = 50
37
+ const warnedDepsCallsites = new Set<string>()
38
+ const warnedDerivedIdentityCallsites = new Set<string>()
39
+ const warnedUnhashableIdentityCallsites = new Set<string>()
40
+ const unpreparedQueryValue = Symbol(`unpreparedQueryValue`)
41
+
42
+ export type DerivedIdentityProfiler = {
43
+ renderCount: number
44
+ totalMs: number
45
+ maxMs: number
46
+ warned: boolean
47
+ }
23
48
 
24
49
  export type UseLiveQueryStatus = CollectionStatus | `disabled`
50
+ export type LiveQueryKey = ReadonlyArray<unknown>
51
+ export type UseLiveQueryConfig<TContext extends Context> =
52
+ LiveQueryCollectionConfig<TContext> & {
53
+ /**
54
+ * Explicit identity for queries that contain opaque functional variants or
55
+ * are hot enough that deriving identity from structured IR is too expensive.
56
+ * Structured queries should omit this so DB can derive identity directly.
57
+ */
58
+ queryKey?: LiveQueryKey
59
+ /** Override the nearest DbProvider for this query. */
60
+ client?: DbClient
61
+ }
62
+
63
+ export function warnDeprecatedDepsArray(
64
+ hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`,
65
+ ): void {
66
+ if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {
67
+ return
68
+ }
69
+
70
+ const callsite = getWarningCallsite(4)
71
+ if (warnedDepsCallsites.has(callsite)) {
72
+ return
73
+ }
74
+ warnedDepsCallsites.add(callsite)
75
+ const replacement =
76
+ hookName === `useLiveQuery`
77
+ ? `useLiveQuery({ query })`
78
+ : `useLiveInfiniteQuery(query, { queryKey })`
79
+ console.warn(
80
+ `[${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.`,
81
+ )
82
+ }
83
+
84
+ function shouldWarnInDevelopment(disableEnvVar: string): boolean {
85
+ if (typeof process === `undefined`) {
86
+ return false
87
+ }
88
+
89
+ return (
90
+ process.env.NODE_ENV !== `production` && process.env[disableEnvVar] !== `1`
91
+ )
92
+ }
93
+
94
+ function getCurrentTime(): number {
95
+ return typeof performance !== `undefined` &&
96
+ typeof performance.now === `function`
97
+ ? performance.now()
98
+ : Date.now()
99
+ }
100
+
101
+ function getWarningCallsite(stackIndex: number): string {
102
+ const stack = new Error().stack ?? `unknown`
103
+ return stack.split(`\n`)[stackIndex]?.trim() ?? stack
104
+ }
105
+
106
+ function warnDerivedIdentityHotPath(
107
+ profiler: DerivedIdentityProfiler,
108
+ durationMs: number,
109
+ ): void {
110
+ if (
111
+ profiler.warned ||
112
+ !shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)
113
+ ) {
114
+ return
115
+ }
116
+
117
+ const isSlowSingleRender =
118
+ durationMs >= DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS
119
+ const isHotRenderPath =
120
+ profiler.renderCount >= DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD &&
121
+ profiler.totalMs >= DERIVED_IDENTITY_TOTAL_WARN_MS
122
+
123
+ if (!isSlowSingleRender && !isHotRenderPath) {
124
+ return
125
+ }
126
+
127
+ const callsite = getWarningCallsite(5)
128
+ if (warnedDerivedIdentityCallsites.has(callsite)) {
129
+ profiler.warned = true
130
+ return
131
+ }
132
+
133
+ warnedDerivedIdentityCallsites.add(callsite)
134
+ profiler.warned = true
135
+
136
+ const reason = isSlowSingleRender
137
+ ? `one render took ${durationMs.toFixed(1)}ms`
138
+ : `${profiler.renderCount} renders took ${profiler.totalMs.toFixed(1)}ms`
139
+
140
+ console.warn(
141
+ `[useLiveQuery] Deriving live query identity from structured query IR is running on a hot render path (${reason}, max ${profiler.maxMs.toFixed(1)}ms). ` +
142
+ `Provide an explicit queryKey to skip rebuilding and hashing the IR on every render: useLiveQuery({ queryKey: [...], query }).`,
143
+ )
144
+ }
145
+
146
+ function getExplicitQueryKey(value: unknown): LiveQueryKey | undefined {
147
+ return value &&
148
+ typeof value === `object` &&
149
+ Array.isArray((value as { queryKey?: unknown }).queryKey)
150
+ ? (value as { queryKey: LiveQueryKey }).queryKey
151
+ : undefined
152
+ }
153
+
154
+ function getExplicitDbClient(value: unknown): DbClient | undefined {
155
+ return value &&
156
+ typeof value === `object` &&
157
+ `client` in value &&
158
+ (value as { client?: unknown }).client !== undefined
159
+ ? (value as { client: DbClient }).client
160
+ : undefined
161
+ }
162
+
163
+ export function prepareQueryValue(
164
+ value: unknown,
165
+ dbClient: DbClient | undefined,
166
+ deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,
167
+ ): unknown {
168
+ return prepareLiveQueryValue(value, dbClient, deferredCollections)
169
+ }
170
+
171
+ type DerivedQueryPreparation =
172
+ | {
173
+ status: `hashable`
174
+ value: unknown
175
+ identityDeps: Array<unknown>
176
+ }
177
+ | {
178
+ status: `unhashable`
179
+ value: unknown
180
+ error: UnhashableQueryIRError
181
+ }
182
+
183
+ export function prepareDerivedQuery(
184
+ value: unknown,
185
+ dbClient: DbClient | undefined,
186
+ profiler: DerivedIdentityProfiler,
187
+ deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,
188
+ ): DerivedQueryPreparation {
189
+ const shouldProfile = shouldWarnInDevelopment(
190
+ `TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,
191
+ )
192
+ const start = shouldProfile ? getCurrentTime() : 0
193
+ const preparedValue = prepareQueryValue(value, dbClient, deferredCollections)
194
+
195
+ try {
196
+ const identity = getPreparedLiveQueryIdentity(preparedValue)
197
+ return {
198
+ status: `hashable`,
199
+ value: preparedValue,
200
+ identityDeps: [`derived`, identity],
201
+ }
202
+ } catch (error) {
203
+ if (error instanceof UnhashableQueryIRError) {
204
+ return { status: `unhashable`, value: preparedValue, error }
205
+ }
206
+
207
+ throw error
208
+ } finally {
209
+ if (shouldProfile) {
210
+ const durationMs = getCurrentTime() - start
211
+ profiler.renderCount += 1
212
+ profiler.totalMs += durationMs
213
+ profiler.maxMs = Math.max(profiler.maxMs, durationMs)
214
+ warnDerivedIdentityHotPath(profiler, durationMs)
215
+ }
216
+ }
217
+ }
218
+
219
+ export function warnUnhashableDerivedIdentity(
220
+ error: UnhashableQueryIRError,
221
+ ): void {
222
+ if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {
223
+ return
224
+ }
225
+
226
+ const callsite = getWarningCallsite(4)
227
+ if (warnedUnhashableIdentityCallsites.has(callsite)) {
228
+ return
229
+ }
230
+ warnedUnhashableIdentityCallsites.add(callsite)
231
+
232
+ console.warn(
233
+ `[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` +
234
+ `It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` +
235
+ `Unhashable queries without queryKey will throw in 1.0.`,
236
+ )
237
+ }
238
+
239
+ function createCollectionFromPreparedQuery(value: unknown) {
240
+ if (value === undefined || value === null) {
241
+ return null
242
+ }
243
+
244
+ if (isCollection(value)) {
245
+ value.startSyncImmediate()
246
+ return value
247
+ }
248
+
249
+ if (value instanceof BaseQueryBuilder) {
250
+ return createLiveQueryCollection({
251
+ query: value,
252
+ startSync: true,
253
+ gcTime: DEFAULT_GC_TIME_MS,
254
+ })
255
+ }
256
+
257
+ if (typeof value === `object`) {
258
+ return createLiveQueryCollection({
259
+ startSync: true,
260
+ gcTime: DEFAULT_GC_TIME_MS,
261
+ ...(value as LiveQueryCollectionConfig<any>),
262
+ })
263
+ }
264
+
265
+ throw new Error(
266
+ `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof value}`,
267
+ )
268
+ }
25
269
 
26
270
  /**
27
- * Create a live query using a query function
271
+ * Create a live query using a query function.
28
272
  * @param queryFn - Query function that defines what data to fetch
29
- * @param deps - Array of dependencies that trigger query re-execution when changed
273
+ * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
30
274
  * @returns Object with reactive data, state, and status information
31
275
  * @example
32
- * // Basic query with object syntax
33
- * const { data, isLoading } = useLiveQuery((q) =>
34
- * q.from({ todos: todosCollection })
35
- * .where(({ todos }) => eq(todos.completed, false))
36
- * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
37
- * )
276
+ * // Prefer config object syntax
277
+ * const { data, isLoading } = useLiveQuery({
278
+ * query: (q) =>
279
+ * q.from({ todos: todosCollection })
280
+ * .where(({ todos }) => eq(todos.completed, false))
281
+ * .select(({ todos }) => ({ id: todos.id, text: todos.text }))
282
+ * })
38
283
  *
39
284
  * @example
40
285
  * // Single result query
41
- * const { data } = useLiveQuery(
42
- * (q) => q.from({ todos: todosCollection })
286
+ * const { data } = useLiveQuery({
287
+ * query: (q) => q.from({ todos: todosCollection })
43
288
  * .where(({ todos }) => eq(todos.id, 1))
44
289
  * .findOne()
45
- * )
290
+ * })
46
291
  *
47
292
  * @example
48
- * // With dependencies that trigger re-execution
49
- * const { data, state } = useLiveQuery(
50
- * (q) => q.from({ todos: todosCollection })
293
+ * // Structured captured values are included in derived query identity
294
+ * const { data, state } = useLiveQuery({
295
+ * query: (q) => q.from({ todos: todosCollection })
51
296
  * .where(({ todos }) => gt(todos.priority, minPriority)),
52
- * [minPriority] // Re-run when minPriority changes
53
- * )
297
+ * })
54
298
  *
55
299
  * @example
56
300
  * // Join pattern
57
- * const { data } = useLiveQuery((q) =>
58
- * q.from({ issues: issueCollection })
59
- * .join({ persons: personCollection }, ({ issues, persons }) =>
60
- * eq(issues.userId, persons.id)
61
- * )
62
- * .select(({ issues, persons }) => ({
63
- * id: issues.id,
64
- * title: issues.title,
65
- * userName: persons.name
66
- * }))
67
- * )
301
+ * const { data } = useLiveQuery({
302
+ * query: (q) =>
303
+ * q.from({ issues: issueCollection })
304
+ * .join({ persons: personCollection }, ({ issues, persons }) =>
305
+ * eq(issues.userId, persons.id)
306
+ * )
307
+ * .select(({ issues, persons }) => ({
308
+ * id: issues.id,
309
+ * title: issues.title,
310
+ * userName: persons.name
311
+ * }))
312
+ * })
68
313
  *
69
314
  * @example
70
315
  * // Handle loading and error states
71
- * const { data, isLoading, isError, status } = useLiveQuery((q) =>
72
- * q.from({ todos: todoCollection })
73
- * )
316
+ * const { data, isLoading, isError, status } = useLiveQuery({
317
+ * query: (q) => q.from({ todos: todoCollection })
318
+ * })
74
319
  *
75
320
  * if (isLoading) return <div>Loading...</div>
76
321
  * if (isError) return <div>Error: {status}</div>
@@ -197,7 +442,7 @@ export function useLiveQuery<
197
442
  /**
198
443
  * Create a live query using configuration object
199
444
  * @param config - Configuration object with query and options
200
- * @param deps - Array of dependencies that trigger query re-execution when changed
445
+ * @param deps - Deprecated array of dependencies that trigger query re-execution when changed
201
446
  * @returns Object with reactive data, state, and status information
202
447
  * @example
203
448
  * // Basic config object usage
@@ -213,7 +458,9 @@ export function useLiveQuery<
213
458
  * .where(({ persons }) => gt(persons.age, 30))
214
459
  * .select(({ persons }) => ({ id: persons.id, name: persons.name }))
215
460
  *
216
- * const { data, isReady } = useLiveQuery({ query: queryBuilder })
461
+ * const { data, isReady } = useLiveQuery({
462
+ * query: queryBuilder,
463
+ * })
217
464
  *
218
465
  * @example
219
466
  * // Handle all states uniformly
@@ -228,6 +475,22 @@ export function useLiveQuery<
228
475
  * return <div>{data.length} items loaded</div>
229
476
  */
230
477
  // Overload 6: Accept config object
478
+ export function useLiveQuery<TContext extends Context>(
479
+ config: UseLiveQueryConfig<TContext>,
480
+ ): {
481
+ state: Map<string | number, GetResult<TContext>>
482
+ data: InferResultType<TContext>
483
+ collection: Collection<GetResult<TContext>, string | number, {}>
484
+ status: CollectionStatus // Can't be disabled for config objects
485
+ isLoading: boolean
486
+ isReady: boolean
487
+ isIdle: boolean
488
+ isError: boolean
489
+ isCleanedUp: boolean
490
+ isEnabled: true // Always true for config objects
491
+ }
492
+
493
+ // Overload 7: Accept config object with legacy deps
231
494
  export function useLiveQuery<TContext extends Context>(
232
495
  config: LiveQueryCollectionConfig<TContext>,
233
496
  deps?: Array<unknown>,
@@ -273,7 +536,7 @@ export function useLiveQuery<TContext extends Context>(
273
536
  *
274
537
  * return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>
275
538
  */
276
- // Overload 7: Accept pre-created live query collection
539
+ // Overload 8: Accept pre-created live query collection
277
540
  export function useLiveQuery<
278
541
  TResult extends object,
279
542
  TKey extends string | number,
@@ -293,7 +556,7 @@ export function useLiveQuery<
293
556
  isEnabled: true // Always true for pre-created live query collections
294
557
  }
295
558
 
296
- // Overload 8: Accept pre-created live query collection with singleResult: true
559
+ // Overload 9: Accept pre-created live query collection with singleResult: true
297
560
  export function useLiveQuery<
298
561
  TResult extends object,
299
562
  TKey extends string | number,
@@ -316,10 +579,15 @@ export function useLiveQuery<
316
579
  // Implementation - use function overloads to infer the actual collection type
317
580
  export function useLiveQuery(
318
581
  configOrQueryOrCollection: any,
319
- deps: Array<unknown> = [],
582
+ deps?: Array<unknown>,
320
583
  ) {
584
+ const contextDbClient = useOptionalDbClient()
321
585
  // Check if it's already a collection
322
586
  const inputIsCollection = isCollection(configOrQueryOrCollection)
587
+ const dbClient = inputIsCollection
588
+ ? contextDbClient
589
+ : (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)
590
+ const resolvedDeps = deps ?? []
323
591
 
324
592
  // Use refs to cache collection and track dependencies
325
593
  const collectionRef = useRef<Collection<object, string | number, {}> | null>(
@@ -327,20 +595,114 @@ export function useLiveQuery(
327
595
  )
328
596
  const depsRef = useRef<Array<unknown> | null>(null)
329
597
  const configRef = useRef<unknown>(null)
598
+ const clientRef = useRef(dbClient)
599
+ const legacyUnhashableIdentityRef = useRef<Array<unknown>>([
600
+ `legacy-unhashable`,
601
+ ])
330
602
 
331
- // The shared observer owns subscription, the ready-race, and the snapshot.
603
+ const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({
604
+ renderCount: 0,
605
+ totalMs: 0,
606
+ maxMs: 0,
607
+ warned: false,
608
+ })
609
+ const deferredCollectionsRef = useRef(
610
+ new Set<CollectionImpl<any, string | number, any, any, any>>(),
611
+ )
332
612
  const observerRef = useRef<LiveQueryObserver<object, string | number> | null>(
333
613
  null,
334
614
  )
615
+ const queryHashRef = useRef<string | undefined>(undefined)
616
+ const identityErrorRef = useRef<UnhashableQueryIRError | undefined>(undefined)
617
+
618
+ const queryKey = !inputIsCollection
619
+ ? getExplicitQueryKey(configOrQueryOrCollection)
620
+ : undefined
621
+ let preparedQueryValue: unknown | typeof unpreparedQueryValue =
622
+ unpreparedQueryValue
623
+ let identityDeps: ReadonlyArray<unknown>
624
+ let streamIdentity: unknown = undefined
625
+ let identityError: UnhashableQueryIRError | undefined
626
+
627
+ if (queryKey) {
628
+ identityDeps = queryKey
629
+ streamIdentity = [`queryKey`, queryKey]
630
+ } else if (deps !== undefined) {
631
+ identityDeps = resolvedDeps
632
+ try {
633
+ preparedQueryValue = prepareQueryValue(
634
+ configOrQueryOrCollection,
635
+ dbClient,
636
+ deferredCollectionsRef.current,
637
+ )
638
+ streamIdentity = [
639
+ `deps`,
640
+ resolvedDeps,
641
+ getPreparedLiveQueryIdentity(preparedQueryValue),
642
+ ]
643
+ } catch (error) {
644
+ if (!(error instanceof UnhashableQueryIRError)) throw error
645
+ warnUnhashableDerivedIdentity(error)
646
+ identityError = error
647
+ }
648
+ } else if (inputIsCollection) {
649
+ identityDeps = []
650
+ streamIdentity = [`collection`, configOrQueryOrCollection.id]
651
+ } else {
652
+ const preparation = prepareDerivedQuery(
653
+ configOrQueryOrCollection,
654
+ dbClient,
655
+ derivedIdentityProfilerRef.current,
656
+ deferredCollectionsRef.current,
657
+ )
658
+ preparedQueryValue = preparation.value
659
+ if (preparation.status === `hashable`) {
660
+ identityDeps = preparation.identityDeps
661
+ streamIdentity = preparation.identityDeps
662
+ } else {
663
+ warnUnhashableDerivedIdentity(preparation.error)
664
+ identityDeps = legacyUnhashableIdentityRef.current
665
+ identityError = preparation.error
666
+ }
667
+ }
668
+
669
+ let queryHash: string | undefined
670
+ if (streamIdentity !== undefined) {
671
+ try {
672
+ queryHash = getStableValueHash(streamIdentity, `queryKey`)
673
+ } catch (error) {
674
+ if (error instanceof UnhashableQueryIRError) {
675
+ if (queryKey !== undefined) throw error
676
+ identityError = error
677
+ } else {
678
+ throw error
679
+ }
680
+ }
681
+ }
682
+
683
+ if (deps !== undefined) {
684
+ warnDeprecatedDepsArray()
685
+ }
686
+
687
+ const identityChanged =
688
+ depsRef.current === null ||
689
+ (deps !== undefined
690
+ ? depsRef.current.length !== identityDeps.length ||
691
+ depsRef.current.some((dep, index) => dep !== identityDeps[index])
692
+ : !deepEquals(depsRef.current, identityDeps))
335
693
 
336
694
  // Check if we need to create/recreate the collection
337
695
  const needsNewCollection =
338
696
  !collectionRef.current ||
339
697
  (inputIsCollection && configRef.current !== configOrQueryOrCollection) ||
340
- (!inputIsCollection &&
341
- (depsRef.current === null ||
342
- depsRef.current.length !== deps.length ||
343
- depsRef.current.some((dep, i) => dep !== deps[i])))
698
+ (!inputIsCollection && (clientRef.current !== dbClient || identityChanged))
699
+
700
+ const resumeDeferredCollections = () => {
701
+ for (const collection of deferredCollectionsRef.current) {
702
+ collection._resumeSyncStart()
703
+ }
704
+ deferredCollectionsRef.current.clear()
705
+ }
344
706
 
345
707
  if (needsNewCollection) {
346
708
  if (inputIsCollection) {
@@ -350,12 +712,15 @@ export function useLiveQuery(
350
712
  const syncMode = (
351
713
  configOrQueryOrCollection as { config?: { syncMode?: string } }
352
714
  ).config?.syncMode
353
- if (syncMode === `on-demand`) {
715
+ if (
716
+ syncMode === `on-demand` &&
717
+ shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)
718
+ ) {
354
719
  console.warn(
355
720
  `[useLiveQuery] Warning: Passing a collection with syncMode "on-demand" directly to useLiveQuery ` +
356
721
  `will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\n\n` +
357
722
  `Instead, use a query builder function:\n` +
358
- ` const { data } = useLiveQuery((q) => q.from({ c: myCollection }).select(({ c }) => c))\n\n` +
723
+ ` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\n\n` +
359
724
  `Or switch to syncMode "eager" if you want all data to sync automatically.`,
360
725
  )
361
726
  }
@@ -364,51 +729,22 @@ export function useLiveQuery(
364
729
  collectionRef.current = configOrQueryOrCollection
365
730
  configRef.current = configOrQueryOrCollection
366
731
  } else {
367
- // Handle different callback return types
368
- if (typeof configOrQueryOrCollection === `function`) {
369
- // Call the function with a query builder to see what it returns
370
- const queryBuilder = new BaseQueryBuilder() as InitialQueryBuilder
371
- const result = configOrQueryOrCollection(queryBuilder)
372
-
373
- if (result === undefined || result === null) {
374
- // Callback returned undefined/null - disabled query
375
- collectionRef.current = null
376
- } else if (isCollection(result)) {
377
- // Callback returned a Collection instance - use it directly
378
- result.startSyncImmediate()
379
- collectionRef.current = result
380
- } else if (result instanceof BaseQueryBuilder) {
381
- // Callback returned QueryBuilder - create live query collection using the original callback
382
- // (not the result, since the result might be from a different query builder instance)
383
- collectionRef.current = createLiveQueryCollection({
384
- query: configOrQueryOrCollection,
385
- startSync: true,
386
- gcTime: DEFAULT_GC_TIME_MS,
387
- })
388
- } else if (result && typeof result === `object`) {
389
- // Assume it's a LiveQueryCollectionConfig
390
- collectionRef.current = createLiveQueryCollection({
391
- startSync: true,
392
- gcTime: DEFAULT_GC_TIME_MS,
393
- ...result,
394
- })
395
- } else {
396
- // Unexpected return type
397
- throw new Error(
398
- `useLiveQuery callback must return a QueryBuilder, LiveQueryCollectionConfig, Collection, undefined, or null. Got: ${typeof result}`,
399
- )
400
- }
401
- depsRef.current = [...deps]
402
- } else {
403
- // Original logic for config objects
404
- collectionRef.current = createLiveQueryCollection({
405
- startSync: true,
406
- gcTime: DEFAULT_GC_TIME_MS,
407
- ...configOrQueryOrCollection,
408
- })
409
- depsRef.current = [...deps]
732
+ if (preparedQueryValue === unpreparedQueryValue) {
733
+ preparedQueryValue = prepareQueryValue(
734
+ configOrQueryOrCollection,
735
+ dbClient,
736
+ deferredCollectionsRef.current,
737
+ )
410
738
  }
739
+ collectionRef.current = createCollectionFromPreparedQuery(
740
+ preparedQueryValue,
741
+ ) as Collection<object, string | number, {}>
742
+ configRef.current = configOrQueryOrCollection
743
+ depsRef.current = [...identityDeps]
411
744
  }
745
+ clientRef.current = dbClient
746
+ queryHashRef.current = queryHash
747
+ identityErrorRef.current = identityError
412
748
  }
413
749
 
414
750
  // Recreate the observer when the underlying collection changes. The observer
@@ -426,6 +762,9 @@ export function useLiveQuery(
426
762
  // useSyncExternalStore inside its own subscribe call.
427
763
  observerRef.current = createLiveQueryObserver(collectionRef.current, {
428
764
  mode: `wholesale`,
765
+ client: dbClient,
766
+ queryHash: queryHashRef.current,
767
+ onPreload: resumeDeferredCollections,
429
768
  })
430
769
  }
431
770
  const observer = observerRef.current!
@@ -436,13 +775,23 @@ export function useLiveQuery(
436
775
  ((onStoreChange: () => void) => () => void) | null
437
776
  >(null)
438
777
  if (!subscribeRef.current || needsNewCollection) {
439
- subscribeRef.current = (onStoreChange) =>
440
- observer.subscribe(() => onStoreChange())
778
+ subscribeRef.current = (onStoreChange: () => void) => {
779
+ const unsubscribe = observer.subscribe(() => onStoreChange())
780
+ resumeDeferredCollections()
781
+ return unsubscribe
782
+ }
441
783
  }
442
784
 
443
- // The observer returns a stable snapshot per revision, which is the return
444
- // shape this hook exposes. Keep the return loose to satisfy the overloads.
445
- return useSyncExternalStore(subscribeRef.current, () =>
446
- observer.getSnapshot(),
447
- ) as any
785
+ const returned = useSyncExternalStore(
786
+ subscribeRef.current,
787
+ () => observer.getSnapshot(),
788
+ () => observer.getServerSnapshot(),
789
+ )
790
+ setLiveQueryResultInfo(returned, {
791
+ client: dbClient,
792
+ queryHash: queryHashRef.current,
793
+ identityError: identityErrorRef.current,
794
+ observer,
795
+ })
796
+ return returned as any
448
797
  }