@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.
- package/README.md +12 -0
- package/dist/cjs/DbProvider.cjs +23 -0
- package/dist/cjs/DbProvider.cjs.map +1 -0
- package/dist/cjs/DbProvider.d.cts +9 -0
- package/dist/cjs/HydrationBoundary.cjs +16 -0
- package/dist/cjs/HydrationBoundary.cjs.map +1 -0
- package/dist/cjs/HydrationBoundary.d.cts +7 -0
- package/dist/cjs/index.cjs +6 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +4 -1
- package/dist/cjs/live-query-internals.cjs +19 -0
- package/dist/cjs/live-query-internals.cjs.map +1 -0
- package/dist/cjs/live-query-internals.d.cts +9 -0
- package/dist/cjs/useLiveInfiniteQuery.cjs +76 -8
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +12 -58
- package/dist/cjs/useLiveQuery.cjs +263 -41
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +79 -33
- package/dist/cjs/useLiveSuspenseQuery.cjs +19 -5
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
- package/dist/cjs/useLiveSuspenseQuery.d.cts +22 -19
- package/dist/esm/DbProvider.d.ts +9 -0
- package/dist/esm/DbProvider.js +23 -0
- package/dist/esm/DbProvider.js.map +1 -0
- package/dist/esm/HydrationBoundary.d.ts +7 -0
- package/dist/esm/HydrationBoundary.js +16 -0
- package/dist/esm/HydrationBoundary.js.map +1 -0
- package/dist/esm/index.d.ts +4 -1
- package/dist/esm/index.js +6 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/live-query-internals.d.ts +9 -0
- package/dist/esm/live-query-internals.js +19 -0
- package/dist/esm/live-query-internals.js.map +1 -0
- package/dist/esm/useLiveInfiniteQuery.d.ts +12 -58
- package/dist/esm/useLiveInfiniteQuery.js +76 -8
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.d.ts +79 -33
- package/dist/esm/useLiveQuery.js +265 -43
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.d.ts +22 -19
- package/dist/esm/useLiveSuspenseQuery.js +19 -5
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
- package/package.json +2 -2
- package/skills/react-db/SKILL.md +119 -90
- package/src/DbProvider.tsx +32 -0
- package/src/HydrationBoundary.tsx +25 -0
- package/src/index.ts +8 -1
- package/src/live-query-internals.ts +36 -0
- package/src/useLiveInfiniteQuery.ts +119 -65
- package/src/useLiveQuery.ts +441 -92
- package/src/useLiveSuspenseQuery.ts +56 -29
package/src/useLiveQuery.ts
CHANGED
|
@@ -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 -
|
|
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
|
-
* //
|
|
33
|
-
* const { data, isLoading } = useLiveQuery(
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
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
|
-
* //
|
|
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
|
-
*
|
|
53
|
-
* )
|
|
297
|
+
* })
|
|
54
298
|
*
|
|
55
299
|
* @example
|
|
56
300
|
* // Join pattern
|
|
57
|
-
* const { data } = useLiveQuery(
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
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(
|
|
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 -
|
|
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({
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
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 (
|
|
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
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
observer.
|
|
447
|
-
)
|
|
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
|
}
|