@tanstack/react-db 0.5.12 → 1.0.0-rc.1.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.
- package/dist/cjs/live-query-internals.cjs.map +1 -1
- package/dist/cjs/live-query-internals.d.cts +3 -1
- package/dist/cjs/source-id-bindings.cjs +9 -42
- package/dist/cjs/source-id-bindings.cjs.map +1 -1
- package/dist/cjs/source-id-bindings.d.cts +6 -17
- package/dist/cjs/useLiveInfiniteQuery.cjs +93 -173
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveInfiniteQuery.d.cts +3 -14
- package/dist/cjs/useLiveQuery.cjs +187 -224
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.d.cts +37 -136
- package/dist/cjs/useLiveSuspenseQuery.cjs +71 -64
- package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
- package/dist/cjs/useLiveSuspenseQuery.d.cts +10 -9
- package/dist/cjs/usePacedMutations.cjs.map +1 -1
- package/dist/cjs/usePacedMutations.d.cts +1 -1
- package/dist/esm/live-query-internals.d.ts +3 -1
- package/dist/esm/live-query-internals.js.map +1 -1
- package/dist/esm/source-id-bindings.d.ts +6 -17
- package/dist/esm/source-id-bindings.js +9 -40
- package/dist/esm/source-id-bindings.js.map +1 -1
- package/dist/esm/useLiveInfiniteQuery.d.ts +3 -14
- package/dist/esm/useLiveInfiniteQuery.js +95 -175
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.d.ts +37 -136
- package/dist/esm/useLiveQuery.js +189 -226
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/dist/esm/useLiveSuspenseQuery.d.ts +10 -9
- package/dist/esm/useLiveSuspenseQuery.js +71 -64
- package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
- package/dist/esm/usePacedMutations.d.ts +1 -1
- package/dist/esm/usePacedMutations.js.map +1 -1
- package/package.json +3 -3
- package/skills/react-db/SKILL.md +24 -15
- package/src/live-query-internals.ts +3 -0
- package/src/source-id-bindings.ts +16 -62
- package/src/useLiveInfiniteQuery.ts +138 -296
- package/src/useLiveQuery.ts +329 -495
- package/src/useLiveSuspenseQuery.ts +122 -132
- package/src/usePacedMutations.ts +1 -1
package/src/useLiveQuery.ts
CHANGED
|
@@ -4,22 +4,25 @@ import { useRef, useSyncExternalStore } from 'react'
|
|
|
4
4
|
import {
|
|
5
5
|
UnhashableQueryIRError,
|
|
6
6
|
createLiveQueryObserver,
|
|
7
|
+
createLiveQuerySourceIdClaims,
|
|
7
8
|
deepEquals,
|
|
8
|
-
|
|
9
|
+
getLiveQueryKey,
|
|
10
|
+
getSourceObjectToken,
|
|
9
11
|
getStableValueHash,
|
|
12
|
+
identifyLiveQuery,
|
|
10
13
|
isCollection,
|
|
11
14
|
prepareLiveQueryValue,
|
|
15
|
+
rejectLiveQueryInput,
|
|
16
|
+
releaseDeferredOnReclaim,
|
|
12
17
|
resolveLiveQueryValue,
|
|
18
|
+
resumeDeferredLiveQueryCollections,
|
|
13
19
|
} from '@tanstack/db'
|
|
14
20
|
import { useOptionalDbClient } from './DbProvider'
|
|
15
21
|
import { setLiveQueryResultInfo } from './live-query-internals'
|
|
16
22
|
import { shouldWarnInDevelopment } from './development'
|
|
17
23
|
import {
|
|
18
|
-
claimSourceIds,
|
|
19
|
-
createSourceIdBindings,
|
|
20
24
|
getPreparedSources,
|
|
21
|
-
|
|
22
|
-
resumeSyncStarts,
|
|
25
|
+
getUnboundDescriptorReporter,
|
|
23
26
|
} from './source-id-bindings'
|
|
24
27
|
import type {
|
|
25
28
|
Collection,
|
|
@@ -31,9 +34,13 @@ import type {
|
|
|
31
34
|
InferResultType,
|
|
32
35
|
InitialQueryBuilder,
|
|
33
36
|
LiveQueryCollectionConfig,
|
|
37
|
+
LiveQueryIdentityScope,
|
|
38
|
+
LiveQueryKey,
|
|
34
39
|
LiveQueryObserver,
|
|
35
|
-
|
|
40
|
+
LiveQuerySourceIdClaims,
|
|
41
|
+
LiveQueryStatusFlags,
|
|
36
42
|
NonSingleResult,
|
|
43
|
+
PooledLiveQueries,
|
|
37
44
|
QueryBuilder,
|
|
38
45
|
SingleResult,
|
|
39
46
|
} from '@tanstack/db'
|
|
@@ -45,9 +52,7 @@ const DEFAULT_SUSPENSE_GC_TIME_MS = 5000
|
|
|
45
52
|
const DERIVED_IDENTITY_SINGLE_RENDER_WARN_MS = 16
|
|
46
53
|
const DERIVED_IDENTITY_RENDER_COUNT_WARN_THRESHOLD = 10
|
|
47
54
|
const DERIVED_IDENTITY_TOTAL_WARN_MS = 50
|
|
48
|
-
const warnedDepsCallsites = new Set<string>()
|
|
49
55
|
const warnedDerivedIdentityCallsites = new Set<string>()
|
|
50
|
-
const warnedUnhashableIdentityCallsites = new Set<string>()
|
|
51
56
|
const unpreparedQueryValue = Symbol(`unpreparedQueryValue`)
|
|
52
57
|
|
|
53
58
|
type SuspenseCollection = Collection<object, string | number, {}>
|
|
@@ -106,7 +111,7 @@ export type DerivedIdentityProfiler = {
|
|
|
106
111
|
}
|
|
107
112
|
|
|
108
113
|
export type UseLiveQueryStatus = CollectionStatus | `disabled`
|
|
109
|
-
export type LiveQueryKey
|
|
114
|
+
export type { LiveQueryKey }
|
|
110
115
|
type UseLiveQueryConfigOptions<TContext extends Context> = Omit<
|
|
111
116
|
LiveQueryCollectionConfig<TContext>,
|
|
112
117
|
`query`
|
|
@@ -119,6 +124,11 @@ type UseLiveQueryConfigOptions<TContext extends Context> = Omit<
|
|
|
119
124
|
queryKey?: LiveQueryKey
|
|
120
125
|
/** Override the nearest DbProvider for this query. */
|
|
121
126
|
client?: DbClient
|
|
127
|
+
/**
|
|
128
|
+
* Pass `pooledLiveQueries` to share one partition among equality-filtered
|
|
129
|
+
* queries of one shape. Suspense and DbClient queries never pool.
|
|
130
|
+
*/
|
|
131
|
+
pool?: PooledLiveQueries
|
|
122
132
|
}
|
|
123
133
|
|
|
124
134
|
type ConfiguredQueryBuilder<TContext extends Context> = Extract<
|
|
@@ -139,27 +149,6 @@ export type ConditionalUseLiveQueryConfig<TContext extends Context> =
|
|
|
139
149
|
) => ConfiguredQueryBuilder<TContext> | undefined | null)
|
|
140
150
|
}
|
|
141
151
|
|
|
142
|
-
export function warnDeprecatedDepsArray(
|
|
143
|
-
hookName: `useLiveQuery` | `useLiveInfiniteQuery` = `useLiveQuery`,
|
|
144
|
-
): void {
|
|
145
|
-
if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_DEPRECATION_WARNINGS`)) {
|
|
146
|
-
return
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
const callsite = getWarningCallsite(4)
|
|
150
|
-
if (warnedDepsCallsites.has(callsite)) {
|
|
151
|
-
return
|
|
152
|
-
}
|
|
153
|
-
warnedDepsCallsites.add(callsite)
|
|
154
|
-
const replacement =
|
|
155
|
-
hookName === `useLiveQuery`
|
|
156
|
-
? `useLiveQuery({ query })`
|
|
157
|
-
: `useLiveInfiniteQuery(query, { queryKey })`
|
|
158
|
-
console.warn(
|
|
159
|
-
`[${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.`,
|
|
160
|
-
)
|
|
161
|
-
}
|
|
162
|
-
|
|
163
152
|
function getCurrentTime(): number {
|
|
164
153
|
return typeof performance !== `undefined` &&
|
|
165
154
|
typeof performance.now === `function`
|
|
@@ -187,7 +176,10 @@ function warnDerivedIdentityHotPath(
|
|
|
187
176
|
): void {
|
|
188
177
|
if (
|
|
189
178
|
profiler.warned ||
|
|
190
|
-
!
|
|
179
|
+
!(
|
|
180
|
+
shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) &&
|
|
181
|
+
process.env.NODE_ENV !== `production`
|
|
182
|
+
)
|
|
191
183
|
) {
|
|
192
184
|
return
|
|
193
185
|
}
|
|
@@ -221,14 +213,6 @@ function warnDerivedIdentityHotPath(
|
|
|
221
213
|
)
|
|
222
214
|
}
|
|
223
215
|
|
|
224
|
-
function getExplicitQueryKey(value: unknown): LiveQueryKey | undefined {
|
|
225
|
-
return value &&
|
|
226
|
-
typeof value === `object` &&
|
|
227
|
-
Array.isArray((value as { queryKey?: unknown }).queryKey)
|
|
228
|
-
? (value as { queryKey: LiveQueryKey }).queryKey
|
|
229
|
-
: undefined
|
|
230
|
-
}
|
|
231
|
-
|
|
232
216
|
function getExplicitDbClient(value: unknown): DbClient | undefined {
|
|
233
217
|
return value &&
|
|
234
218
|
typeof value === `object` &&
|
|
@@ -246,43 +230,52 @@ export function prepareQueryValue(
|
|
|
246
230
|
return prepareLiveQueryValue(value, dbClient, deferredCollections)
|
|
247
231
|
}
|
|
248
232
|
|
|
249
|
-
type DerivedQueryPreparation =
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
233
|
+
type DerivedQueryPreparation = {
|
|
234
|
+
value: unknown
|
|
235
|
+
identityDeps: Array<unknown>
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** How a hook checks a prepared query whose identity it compares. */
|
|
239
|
+
export function getIdentityScope(
|
|
240
|
+
claims: LiveQuerySourceIdClaims,
|
|
241
|
+
dbClient: DbClient | undefined,
|
|
242
|
+
hookName: string,
|
|
243
|
+
): LiveQueryIdentityScope {
|
|
244
|
+
return {
|
|
245
|
+
claims,
|
|
246
|
+
client: dbClient,
|
|
247
|
+
bindingName: hookName,
|
|
248
|
+
onUnboundDescriptor: getUnboundDescriptorReporter(dbClient, hookName),
|
|
249
|
+
}
|
|
250
|
+
}
|
|
260
251
|
|
|
252
|
+
/**
|
|
253
|
+
* Prepare a query without `queryKey`, derive its identity, and claim its
|
|
254
|
+
* source Collection IDs. A query whose IR has no stable identity throws
|
|
255
|
+
* `UnhashableQueryIRError`. The hook's render boundary releases the sync
|
|
256
|
+
* starts a rejected render deferred.
|
|
257
|
+
*/
|
|
261
258
|
export function prepareDerivedQuery(
|
|
262
259
|
value: unknown,
|
|
263
|
-
dbClient: DbClient | undefined,
|
|
264
260
|
profiler: DerivedIdentityProfiler,
|
|
261
|
+
scope: LiveQueryIdentityScope,
|
|
265
262
|
deferredCollections: Set<CollectionImpl<any, string | number, any, any, any>>,
|
|
266
263
|
): DerivedQueryPreparation {
|
|
267
|
-
const shouldProfile =
|
|
268
|
-
`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS
|
|
269
|
-
|
|
264
|
+
const shouldProfile =
|
|
265
|
+
shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`) &&
|
|
266
|
+
process.env.NODE_ENV !== `production`
|
|
270
267
|
const start = shouldProfile ? getCurrentTime() : 0
|
|
271
|
-
const preparedValue = prepareQueryValue(
|
|
268
|
+
const preparedValue = prepareQueryValue(
|
|
269
|
+
value,
|
|
270
|
+
scope.client,
|
|
271
|
+
deferredCollections,
|
|
272
|
+
)
|
|
272
273
|
|
|
273
274
|
try {
|
|
274
|
-
const identity = getPreparedLiveQueryIdentity(preparedValue)
|
|
275
275
|
return {
|
|
276
|
-
status: `hashable`,
|
|
277
276
|
value: preparedValue,
|
|
278
|
-
identityDeps: [`derived`,
|
|
279
|
-
}
|
|
280
|
-
} catch (error) {
|
|
281
|
-
if (error instanceof UnhashableQueryIRError) {
|
|
282
|
-
return { status: `unhashable`, value: preparedValue, error }
|
|
277
|
+
identityDeps: [`derived`, identifyLiveQuery(preparedValue, scope)],
|
|
283
278
|
}
|
|
284
|
-
|
|
285
|
-
throw error
|
|
286
279
|
} finally {
|
|
287
280
|
if (shouldProfile) {
|
|
288
281
|
const durationMs = getCurrentTime() - start
|
|
@@ -294,30 +287,9 @@ export function prepareDerivedQuery(
|
|
|
294
287
|
}
|
|
295
288
|
}
|
|
296
289
|
|
|
297
|
-
export function warnUnhashableDerivedIdentity(
|
|
298
|
-
error: UnhashableQueryIRError,
|
|
299
|
-
): void {
|
|
300
|
-
if (!shouldWarnInDevelopment(`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`)) {
|
|
301
|
-
return
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
const callsite = getWarningCallsite(4)
|
|
305
|
-
if (warnedUnhashableIdentityCallsites.has(callsite)) {
|
|
306
|
-
return
|
|
307
|
-
}
|
|
308
|
-
warnedUnhashableIdentityCallsites.add(callsite)
|
|
309
|
-
|
|
310
|
-
console.warn(
|
|
311
|
-
`[useLiveQuery] This query cannot derive a stable identity because ${error.reason} at ${error.path}. ` +
|
|
312
|
-
`It will keep the legacy mount-stable behavior for now. Add queryKey: [...] to make captured values reactive. ` +
|
|
313
|
-
`Unhashable queries without queryKey will throw in 1.0.`,
|
|
314
|
-
)
|
|
315
|
-
}
|
|
316
|
-
|
|
317
290
|
/**
|
|
318
291
|
* Create a live query using a query function.
|
|
319
292
|
* @param queryFn - Query function that defines what data to fetch
|
|
320
|
-
* @param deps - Deprecated array of dependencies that trigger query re-execution when changed
|
|
321
293
|
* @returns Object with reactive data, state, and status information
|
|
322
294
|
* @example
|
|
323
295
|
* // Prefer config object syntax
|
|
@@ -370,12 +342,12 @@ export function warnUnhashableDerivedIdentity(
|
|
|
370
342
|
*
|
|
371
343
|
* @example
|
|
372
344
|
* // Handle loading and error states
|
|
373
|
-
* const { data, isLoading,
|
|
345
|
+
* const { data, isLoading, isLoadingError, status } = useLiveQuery({
|
|
374
346
|
* query: (q) => q.from({ todos: todoCollection })
|
|
375
347
|
* })
|
|
376
348
|
*
|
|
377
349
|
* if (isLoading) return <div>Loading...</div>
|
|
378
|
-
* if (
|
|
350
|
+
* if (isLoadingError) return <div>Error: {status}</div>
|
|
379
351
|
*
|
|
380
352
|
* return (
|
|
381
353
|
* <ul>
|
|
@@ -386,66 +358,39 @@ export function warnUnhashableDerivedIdentity(
|
|
|
386
358
|
// Overload 1: Accept query function that always returns QueryBuilder
|
|
387
359
|
export function useLiveQuery<TContext extends Context>(
|
|
388
360
|
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
|
|
389
|
-
deps?: Array<unknown>,
|
|
390
361
|
): {
|
|
391
362
|
state: Map<string | number, GetResult<TContext>>
|
|
392
363
|
data: InferResultType<TContext>
|
|
393
364
|
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
394
365
|
status: CollectionStatus // Can't be disabled if always returns QueryBuilder
|
|
395
|
-
isLoading: boolean
|
|
396
|
-
isReady: boolean
|
|
397
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
398
|
-
isPersistedReady: boolean
|
|
399
|
-
persistedError: unknown | undefined
|
|
400
|
-
isIdle: boolean
|
|
401
|
-
isError: boolean
|
|
402
|
-
isCleanedUp: boolean
|
|
403
366
|
isEnabled: true // Always true if always returns QueryBuilder
|
|
404
|
-
}
|
|
367
|
+
} & LiveQueryStatusFlags
|
|
405
368
|
|
|
406
369
|
// Overload 2: Accept query function that can return undefined/null
|
|
407
370
|
export function useLiveQuery<TContext extends Context>(
|
|
408
371
|
queryFn: (
|
|
409
372
|
q: InitialQueryBuilder,
|
|
410
373
|
) => QueryBuilder<TContext> | undefined | null,
|
|
411
|
-
deps?: Array<unknown>,
|
|
412
374
|
): {
|
|
413
375
|
state: Map<string | number, GetResult<TContext>> | undefined
|
|
414
376
|
data: InferResultType<TContext> | undefined
|
|
415
377
|
collection: Collection<GetResult<TContext>, string | number, {}> | undefined
|
|
416
378
|
status: UseLiveQueryStatus
|
|
417
|
-
isLoading: boolean
|
|
418
|
-
isReady: boolean
|
|
419
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
420
|
-
isPersistedReady: boolean
|
|
421
|
-
persistedError: unknown | undefined
|
|
422
|
-
isIdle: boolean
|
|
423
|
-
isError: boolean
|
|
424
|
-
isCleanedUp: boolean
|
|
425
379
|
isEnabled: boolean
|
|
426
|
-
}
|
|
380
|
+
} & LiveQueryStatusFlags
|
|
427
381
|
|
|
428
382
|
// Overload 3: Accept query function that can return LiveQueryCollectionConfig
|
|
429
383
|
export function useLiveQuery<TContext extends Context>(
|
|
430
384
|
queryFn: (
|
|
431
385
|
q: InitialQueryBuilder,
|
|
432
386
|
) => LiveQueryCollectionConfig<TContext> | undefined | null,
|
|
433
|
-
deps?: Array<unknown>,
|
|
434
387
|
): {
|
|
435
388
|
state: Map<string | number, GetResult<TContext>> | undefined
|
|
436
389
|
data: InferResultType<TContext> | undefined
|
|
437
390
|
collection: Collection<GetResult<TContext>, string | number, {}> | undefined
|
|
438
391
|
status: UseLiveQueryStatus
|
|
439
|
-
isLoading: boolean
|
|
440
|
-
isReady: boolean
|
|
441
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
442
|
-
isPersistedReady: boolean
|
|
443
|
-
persistedError: unknown | undefined
|
|
444
|
-
isIdle: boolean
|
|
445
|
-
isError: boolean
|
|
446
|
-
isCleanedUp: boolean
|
|
447
392
|
isEnabled: boolean
|
|
448
|
-
}
|
|
393
|
+
} & LiveQueryStatusFlags
|
|
449
394
|
|
|
450
395
|
// Overload 4: Accept query function that can return Collection
|
|
451
396
|
export function useLiveQuery<
|
|
@@ -456,22 +401,13 @@ export function useLiveQuery<
|
|
|
456
401
|
queryFn: (
|
|
457
402
|
q: InitialQueryBuilder,
|
|
458
403
|
) => Collection<TResult, TKey, TUtils> | undefined | null,
|
|
459
|
-
deps?: Array<unknown>,
|
|
460
404
|
): {
|
|
461
405
|
state: Map<TKey, TResult> | undefined
|
|
462
406
|
data: Array<TResult> | undefined
|
|
463
407
|
collection: Collection<TResult, TKey, TUtils> | undefined
|
|
464
408
|
status: UseLiveQueryStatus
|
|
465
|
-
isLoading: boolean
|
|
466
|
-
isReady: boolean
|
|
467
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
468
|
-
isPersistedReady: boolean
|
|
469
|
-
persistedError: unknown | undefined
|
|
470
|
-
isIdle: boolean
|
|
471
|
-
isError: boolean
|
|
472
|
-
isCleanedUp: boolean
|
|
473
409
|
isEnabled: boolean
|
|
474
|
-
}
|
|
410
|
+
} & LiveQueryStatusFlags
|
|
475
411
|
|
|
476
412
|
// Overload 5: Accept query function that can return all types
|
|
477
413
|
export function useLiveQuery<
|
|
@@ -488,7 +424,6 @@ export function useLiveQuery<
|
|
|
488
424
|
| Collection<TResult, TKey, TUtils>
|
|
489
425
|
| undefined
|
|
490
426
|
| null,
|
|
491
|
-
deps?: Array<unknown>,
|
|
492
427
|
): {
|
|
493
428
|
state:
|
|
494
429
|
Map<string | number, GetResult<TContext>> | Map<TKey, TResult> | undefined
|
|
@@ -498,21 +433,12 @@ export function useLiveQuery<
|
|
|
498
433
|
| Collection<TResult, TKey, TUtils>
|
|
499
434
|
| undefined
|
|
500
435
|
status: UseLiveQueryStatus
|
|
501
|
-
isLoading: boolean
|
|
502
|
-
isReady: boolean
|
|
503
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
504
|
-
isPersistedReady: boolean
|
|
505
|
-
persistedError: unknown | undefined
|
|
506
|
-
isIdle: boolean
|
|
507
|
-
isError: boolean
|
|
508
|
-
isCleanedUp: boolean
|
|
509
436
|
isEnabled: boolean
|
|
510
|
-
}
|
|
437
|
+
} & LiveQueryStatusFlags
|
|
511
438
|
|
|
512
439
|
/**
|
|
513
440
|
* Create a live query using configuration object
|
|
514
441
|
* @param config - Configuration object with query and options
|
|
515
|
-
* @param deps - Deprecated array of dependencies that trigger query re-execution when changed
|
|
516
442
|
* @returns Object with reactive data, state, and status information
|
|
517
443
|
* @example
|
|
518
444
|
* // Basic config object usage
|
|
@@ -534,12 +460,12 @@ export function useLiveQuery<
|
|
|
534
460
|
*
|
|
535
461
|
* @example
|
|
536
462
|
* // Handle all states uniformly
|
|
537
|
-
* const { data, isLoading, isReady,
|
|
463
|
+
* const { data, isLoading, isReady, isLoadingError } = useLiveQuery({
|
|
538
464
|
* query: (q) => q.from({ items: itemCollection })
|
|
539
465
|
* })
|
|
540
466
|
*
|
|
541
467
|
* if (isLoading) return <div>Loading...</div>
|
|
542
|
-
* if (
|
|
468
|
+
* if (isLoadingError) return <div>Something went wrong</div>
|
|
543
469
|
* if (!isReady) return <div>Preparing...</div>
|
|
544
470
|
*
|
|
545
471
|
* return <div>{data.length} items loaded</div>
|
|
@@ -552,16 +478,8 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
552
478
|
data: InferResultType<TContext>
|
|
553
479
|
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
554
480
|
status: CollectionStatus // Can't be disabled when query always returns a builder
|
|
555
|
-
isLoading: boolean
|
|
556
|
-
isReady: boolean
|
|
557
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
558
|
-
isPersistedReady: boolean
|
|
559
|
-
persistedError: unknown | undefined
|
|
560
|
-
isIdle: boolean
|
|
561
|
-
isError: boolean
|
|
562
|
-
isCleanedUp: boolean
|
|
563
481
|
isEnabled: true // Always true when query always returns a builder
|
|
564
|
-
}
|
|
482
|
+
} & LiveQueryStatusFlags
|
|
565
483
|
|
|
566
484
|
// Overload 7: Accept config object with a query that can return undefined/null
|
|
567
485
|
export function useLiveQuery<TContext extends Context>(
|
|
@@ -571,56 +489,8 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
571
489
|
data: InferResultType<TContext> | undefined
|
|
572
490
|
collection: Collection<GetResult<TContext>, string | number, {}> | undefined
|
|
573
491
|
status: UseLiveQueryStatus
|
|
574
|
-
isLoading: boolean
|
|
575
|
-
isReady: boolean
|
|
576
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
577
|
-
isPersistedReady: boolean
|
|
578
|
-
persistedError: unknown | undefined
|
|
579
|
-
isIdle: boolean
|
|
580
|
-
isError: boolean
|
|
581
|
-
isCleanedUp: boolean
|
|
582
|
-
isEnabled: boolean
|
|
583
|
-
}
|
|
584
|
-
|
|
585
|
-
// Overload 8: Accept config object with legacy deps
|
|
586
|
-
export function useLiveQuery<TContext extends Context>(
|
|
587
|
-
config: LiveQueryCollectionConfig<TContext>,
|
|
588
|
-
deps?: Array<unknown>,
|
|
589
|
-
): {
|
|
590
|
-
state: Map<string | number, GetResult<TContext>>
|
|
591
|
-
data: InferResultType<TContext>
|
|
592
|
-
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
593
|
-
status: CollectionStatus // Can't be disabled when query always returns a builder
|
|
594
|
-
isLoading: boolean
|
|
595
|
-
isReady: boolean
|
|
596
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
597
|
-
isPersistedReady: boolean
|
|
598
|
-
persistedError: unknown | undefined
|
|
599
|
-
isIdle: boolean
|
|
600
|
-
isError: boolean
|
|
601
|
-
isCleanedUp: boolean
|
|
602
|
-
isEnabled: true // Always true when query always returns a builder
|
|
603
|
-
}
|
|
604
|
-
|
|
605
|
-
// Overload 9: Accept config object with legacy deps and a query that can return undefined/null
|
|
606
|
-
export function useLiveQuery<TContext extends Context>(
|
|
607
|
-
config: ConditionalUseLiveQueryConfig<TContext>,
|
|
608
|
-
deps: Array<unknown>,
|
|
609
|
-
): {
|
|
610
|
-
state: Map<string | number, GetResult<TContext>> | undefined
|
|
611
|
-
data: InferResultType<TContext> | undefined
|
|
612
|
-
collection: Collection<GetResult<TContext>, string | number, {}> | undefined
|
|
613
|
-
status: UseLiveQueryStatus
|
|
614
|
-
isLoading: boolean
|
|
615
|
-
isReady: boolean
|
|
616
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
617
|
-
isPersistedReady: boolean
|
|
618
|
-
persistedError: unknown | undefined
|
|
619
|
-
isIdle: boolean
|
|
620
|
-
isError: boolean
|
|
621
|
-
isCleanedUp: boolean
|
|
622
492
|
isEnabled: boolean
|
|
623
|
-
}
|
|
493
|
+
} & LiveQueryStatusFlags
|
|
624
494
|
|
|
625
495
|
/**
|
|
626
496
|
* Subscribe to an existing live query collection
|
|
@@ -644,10 +514,10 @@ export function useLiveQuery<TContext extends Context>(
|
|
|
644
514
|
*
|
|
645
515
|
* @example
|
|
646
516
|
* // Handle states consistently
|
|
647
|
-
* const { data, isLoading,
|
|
517
|
+
* const { data, isLoading, isLoadingError } = useLiveQuery(sharedCollection)
|
|
648
518
|
*
|
|
649
519
|
* if (isLoading) return <div>Loading...</div>
|
|
650
|
-
* if (
|
|
520
|
+
* if (isLoadingError) return <div>Error loading data</div>
|
|
651
521
|
*
|
|
652
522
|
* return <div>{data.map(item => <Item key={item.id} {...item} />)}</div>
|
|
653
523
|
*/
|
|
@@ -663,16 +533,8 @@ export function useLiveQuery<
|
|
|
663
533
|
data: Array<TResult>
|
|
664
534
|
collection: Collection<TResult, TKey, TUtils>
|
|
665
535
|
status: CollectionStatus // Can't be disabled for pre-created live query collections
|
|
666
|
-
isLoading: boolean
|
|
667
|
-
isReady: boolean
|
|
668
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
669
|
-
isPersistedReady: boolean
|
|
670
|
-
persistedError: unknown | undefined
|
|
671
|
-
isIdle: boolean
|
|
672
|
-
isError: boolean
|
|
673
|
-
isCleanedUp: boolean
|
|
674
536
|
isEnabled: true // Always true for pre-created live query collections
|
|
675
|
-
}
|
|
537
|
+
} & LiveQueryStatusFlags
|
|
676
538
|
|
|
677
539
|
// Overload 10: Accept pre-created live query collection with singleResult: true
|
|
678
540
|
export function useLiveQuery<
|
|
@@ -686,31 +548,17 @@ export function useLiveQuery<
|
|
|
686
548
|
data: TResult | undefined
|
|
687
549
|
collection: Collection<TResult, TKey, TUtils> & SingleResult
|
|
688
550
|
status: CollectionStatus // Can't be disabled for pre-created live query collections
|
|
689
|
-
isLoading: boolean
|
|
690
|
-
isReady: boolean
|
|
691
|
-
persistedStatus: LiveQueryPersistedStatus
|
|
692
|
-
isPersistedReady: boolean
|
|
693
|
-
persistedError: unknown | undefined
|
|
694
|
-
isIdle: boolean
|
|
695
|
-
isError: boolean
|
|
696
|
-
isCleanedUp: boolean
|
|
697
551
|
isEnabled: true // Always true for pre-created live query collections
|
|
698
|
-
}
|
|
552
|
+
} & LiveQueryStatusFlags
|
|
699
553
|
|
|
700
554
|
// Implementation - use function overloads to infer the actual collection type
|
|
701
|
-
export function useLiveQuery(
|
|
702
|
-
configOrQueryOrCollection
|
|
703
|
-
deps?: Array<unknown>,
|
|
704
|
-
) {
|
|
705
|
-
return useLiveQueryImpl(configOrQueryOrCollection, deps)
|
|
555
|
+
export function useLiveQuery(configOrQueryOrCollection: any) {
|
|
556
|
+
return useLiveQueryImpl(configOrQueryOrCollection)
|
|
706
557
|
}
|
|
707
558
|
|
|
708
559
|
/** @internal Shared implementation for the Suspense wrapper. */
|
|
709
|
-
export function useLiveQueryForSuspense(
|
|
710
|
-
configOrQueryOrCollection
|
|
711
|
-
deps: Array<unknown> | undefined,
|
|
712
|
-
) {
|
|
713
|
-
return useLiveQueryImpl(configOrQueryOrCollection, deps, true)
|
|
560
|
+
export function useLiveQueryForSuspense(configOrQueryOrCollection: any) {
|
|
561
|
+
return useLiveQueryImpl(configOrQueryOrCollection, true)
|
|
714
562
|
}
|
|
715
563
|
|
|
716
564
|
// What one hook instance keeps across renders. It lives in a single ref
|
|
@@ -718,10 +566,9 @@ export function useLiveQueryForSuspense(
|
|
|
718
566
|
function createHookInstance(dbClient: DbClient | undefined) {
|
|
719
567
|
return {
|
|
720
568
|
collection: null as Collection<object, string | number, {}> | null,
|
|
721
|
-
|
|
569
|
+
identityDeps: null as Array<unknown> | null,
|
|
722
570
|
config: null as unknown,
|
|
723
571
|
client: dbClient,
|
|
724
|
-
legacyUnhashableIdentity: [`legacy-unhashable`] as Array<unknown>,
|
|
725
572
|
derivedIdentityProfiler: {
|
|
726
573
|
renderCount: 0,
|
|
727
574
|
totalMs: 0,
|
|
@@ -731,7 +578,8 @@ function createHookInstance(dbClient: DbClient | undefined) {
|
|
|
731
578
|
deferredCollections: new Set<
|
|
732
579
|
CollectionImpl<any, string | number, any, any, any>
|
|
733
580
|
>(),
|
|
734
|
-
sourceIds:
|
|
581
|
+
sourceIds: createLiveQuerySourceIdClaims(),
|
|
582
|
+
cancelReclaimRelease: undefined as (() => void) | undefined,
|
|
735
583
|
observer: null as LiveQueryObserver<object, string | number> | null,
|
|
736
584
|
queryHash: undefined as string | undefined,
|
|
737
585
|
suspenseKey: undefined as string | undefined,
|
|
@@ -740,193 +588,147 @@ function createHookInstance(dbClient: DbClient | undefined) {
|
|
|
740
588
|
}
|
|
741
589
|
}
|
|
742
590
|
|
|
743
|
-
function useLiveQueryImpl(
|
|
744
|
-
configOrQueryOrCollection: any,
|
|
745
|
-
deps: Array<unknown> | undefined,
|
|
746
|
-
forSuspense = false,
|
|
747
|
-
) {
|
|
591
|
+
function useLiveQueryImpl(configOrQueryOrCollection: any, forSuspense = false) {
|
|
748
592
|
const contextDbClient = useOptionalDbClient()
|
|
749
593
|
// Check if it's already a collection
|
|
750
594
|
const inputIsCollection = isCollection(configOrQueryOrCollection)
|
|
751
595
|
const dbClient = inputIsCollection
|
|
752
596
|
? contextDbClient
|
|
753
597
|
: (getExplicitDbClient(configOrQueryOrCollection) ?? contextDbClient)
|
|
754
|
-
const resolvedDeps = deps ?? []
|
|
755
598
|
|
|
756
599
|
const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)
|
|
757
600
|
const instance = (instanceRef.current ??= createHookInstance(dbClient))
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
let preparedQueryValue: unknown | typeof unpreparedQueryValue =
|
|
765
|
-
unpreparedQueryValue
|
|
766
|
-
let identityDeps: ReadonlyArray<unknown>
|
|
767
|
-
let streamIdentity: unknown = undefined
|
|
768
|
-
let identityError: UnhashableQueryIRError | undefined
|
|
601
|
+
// A commit releases the starts itself, so it drops the reclaim listener.
|
|
602
|
+
const resumeDeferredCollections = () => {
|
|
603
|
+
instance.cancelReclaimRelease?.()
|
|
604
|
+
instance.cancelReclaimRelease = undefined
|
|
605
|
+
resumeDeferredLiveQueryCollections(instance.deferredCollections)
|
|
606
|
+
}
|
|
769
607
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
identityError = error
|
|
797
|
-
}
|
|
798
|
-
} else if (inputIsCollection) {
|
|
799
|
-
identityDeps = []
|
|
800
|
-
streamIdentity = [`collection`, configOrQueryOrCollection.id]
|
|
801
|
-
} else {
|
|
802
|
-
const preparation = prepareDerivedQuery(
|
|
803
|
-
configOrQueryOrCollection,
|
|
804
|
-
dbClient,
|
|
805
|
-
instance.derivedIdentityProfiler,
|
|
806
|
-
instance.deferredCollections,
|
|
807
|
-
)
|
|
808
|
-
preparedQueryValue = preparation.value
|
|
809
|
-
if (preparation.status === `hashable`) {
|
|
608
|
+
// The render boundary: a render that throws anywhere after preparation
|
|
609
|
+
// never commits, so it releases the sync starts it deferred here.
|
|
610
|
+
try {
|
|
611
|
+
const queryKey = !inputIsCollection
|
|
612
|
+
? getLiveQueryKey(configOrQueryOrCollection)
|
|
613
|
+
: undefined
|
|
614
|
+
let preparedQueryValue: unknown | typeof unpreparedQueryValue =
|
|
615
|
+
unpreparedQueryValue
|
|
616
|
+
let identityDeps: ReadonlyArray<unknown>
|
|
617
|
+
let streamIdentity: unknown = undefined
|
|
618
|
+
let identityError: UnhashableQueryIRError | undefined
|
|
619
|
+
|
|
620
|
+
if (queryKey) {
|
|
621
|
+
identityDeps = queryKey
|
|
622
|
+
streamIdentity = [`queryKey`, queryKey]
|
|
623
|
+
} else if (inputIsCollection) {
|
|
624
|
+
identityDeps = []
|
|
625
|
+
streamIdentity = [`collection`, configOrQueryOrCollection.id]
|
|
626
|
+
} else {
|
|
627
|
+
const preparation = prepareDerivedQuery(
|
|
628
|
+
configOrQueryOrCollection,
|
|
629
|
+
instance.derivedIdentityProfiler,
|
|
630
|
+
getIdentityScope(instance.sourceIds, dbClient, `useLiveQuery`),
|
|
631
|
+
instance.deferredCollections,
|
|
632
|
+
)
|
|
633
|
+
preparedQueryValue = preparation.value
|
|
810
634
|
identityDeps = preparation.identityDeps
|
|
811
635
|
streamIdentity = preparation.identityDeps
|
|
812
|
-
} else {
|
|
813
|
-
warnUnhashableDerivedIdentity(preparation.error)
|
|
814
|
-
identityDeps = instance.legacyUnhashableIdentity
|
|
815
|
-
identityError = preparation.error
|
|
816
636
|
}
|
|
817
|
-
}
|
|
818
637
|
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
638
|
+
let queryHash: string | undefined
|
|
639
|
+
if (streamIdentity !== undefined) {
|
|
640
|
+
try {
|
|
641
|
+
queryHash = getStableValueHash(streamIdentity, `queryKey`)
|
|
642
|
+
} catch (error) {
|
|
643
|
+
if (error instanceof UnhashableQueryIRError) {
|
|
644
|
+
if (queryKey !== undefined) throw error
|
|
645
|
+
identityError = error
|
|
646
|
+
} else {
|
|
647
|
+
throw error
|
|
648
|
+
}
|
|
829
649
|
}
|
|
830
650
|
}
|
|
831
|
-
}
|
|
832
|
-
|
|
833
|
-
if (deps !== undefined) {
|
|
834
|
-
warnDeprecatedDepsArray()
|
|
835
|
-
}
|
|
836
|
-
|
|
837
|
-
if (
|
|
838
|
-
queryKey === undefined &&
|
|
839
|
-
deps === undefined &&
|
|
840
|
-
preparedQueryValue !== unpreparedQueryValue
|
|
841
|
-
) {
|
|
842
|
-
claimSourceIds(
|
|
843
|
-
instance.sourceIds,
|
|
844
|
-
preparedQueryValue,
|
|
845
|
-
dbClient,
|
|
846
|
-
`useLiveQuery`,
|
|
847
|
-
resumeDeferredCollections,
|
|
848
|
-
)
|
|
849
|
-
}
|
|
850
651
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
652
|
+
const canReuseSuspenseKey =
|
|
653
|
+
forSuspense &&
|
|
654
|
+
!inputIsCollection &&
|
|
655
|
+
queryHash !== undefined &&
|
|
656
|
+
instance.collection !== null &&
|
|
657
|
+
instance.client === dbClient &&
|
|
658
|
+
instance.queryHash === queryHash &&
|
|
659
|
+
instance.suspenseKey !== undefined
|
|
859
660
|
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
const suspenseKey =
|
|
875
|
-
queryHash && forSuspense && !inputIsCollection
|
|
876
|
-
? canReuseSuspenseKey
|
|
877
|
-
? instance.suspenseKey
|
|
878
|
-
: getSourceQualifiedSuspenseKey(preparedQueryValue, queryHash, dbClient)
|
|
879
|
-
: queryHash
|
|
661
|
+
if (
|
|
662
|
+
forSuspense &&
|
|
663
|
+
!inputIsCollection &&
|
|
664
|
+
queryHash &&
|
|
665
|
+
!canReuseSuspenseKey &&
|
|
666
|
+
preparedQueryValue === unpreparedQueryValue
|
|
667
|
+
) {
|
|
668
|
+
preparedQueryValue = prepareQueryValue(
|
|
669
|
+
configOrQueryOrCollection,
|
|
670
|
+
dbClient,
|
|
671
|
+
instance.deferredCollections,
|
|
672
|
+
)
|
|
673
|
+
}
|
|
880
674
|
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
675
|
+
const suspenseKey =
|
|
676
|
+
queryHash && forSuspense && !inputIsCollection
|
|
677
|
+
? canReuseSuspenseKey
|
|
678
|
+
? instance.suspenseKey
|
|
679
|
+
: getSourceQualifiedSuspenseKey(
|
|
680
|
+
preparedQueryValue,
|
|
681
|
+
queryHash,
|
|
682
|
+
dbClient,
|
|
683
|
+
)
|
|
684
|
+
: queryHash
|
|
685
|
+
|
|
686
|
+
const suspenseCollections =
|
|
687
|
+
forSuspense && !inputIsCollection && suspenseKey
|
|
688
|
+
? getSuspenseCollections(dbClient)
|
|
689
|
+
: undefined
|
|
690
|
+
const suspenseEntry = suspenseKey
|
|
691
|
+
? suspenseCollections?.get(suspenseKey)
|
|
884
692
|
: undefined
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
)
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
configOrQueryOrCollection.startSyncImmediate()
|
|
925
|
-
instance.collection = configOrQueryOrCollection
|
|
926
|
-
instance.config = configOrQueryOrCollection
|
|
927
|
-
} else {
|
|
928
|
-
if (suspenseCollection) {
|
|
929
|
-
instance.collection = suspenseCollection
|
|
693
|
+
const suspenseCollection = suspenseEntry?.collection
|
|
694
|
+
|
|
695
|
+
const identityChanged =
|
|
696
|
+
instance.identityDeps === null ||
|
|
697
|
+
!deepEquals(instance.identityDeps, identityDeps)
|
|
698
|
+
|
|
699
|
+
// Check if we need to create/recreate the collection
|
|
700
|
+
const needsNewCollection =
|
|
701
|
+
!instance.collection ||
|
|
702
|
+
(inputIsCollection && instance.config !== configOrQueryOrCollection) ||
|
|
703
|
+
(!inputIsCollection && (instance.client !== dbClient || identityChanged))
|
|
704
|
+
|
|
705
|
+
if (needsNewCollection) {
|
|
706
|
+
if (inputIsCollection) {
|
|
707
|
+
// Warn when passing a collection directly with on-demand sync mode
|
|
708
|
+
// In on-demand mode, data is only loaded when queries with predicates request it
|
|
709
|
+
// Passing the collection directly doesn't provide any predicates, so no data loads
|
|
710
|
+
const syncMode = (
|
|
711
|
+
configOrQueryOrCollection as { config?: { syncMode?: string } }
|
|
712
|
+
).config?.syncMode
|
|
713
|
+
if (
|
|
714
|
+
syncMode === `on-demand` &&
|
|
715
|
+
shouldWarnInDevelopment(
|
|
716
|
+
`TANSTACK_DB_DISABLE_QUERY_IDENTITY_WARNINGS`,
|
|
717
|
+
) &&
|
|
718
|
+
process.env.NODE_ENV !== `production`
|
|
719
|
+
) {
|
|
720
|
+
console.warn(
|
|
721
|
+
`[useLiveQuery] Warning: Passing a collection with syncMode "on-demand" directly to useLiveQuery ` +
|
|
722
|
+
`will not load any data. In on-demand mode, data is only loaded when queries with predicates request it.\n\n` +
|
|
723
|
+
`Instead, use a query builder function:\n` +
|
|
724
|
+
` const { data } = useLiveQuery({ query: (q) => q.from({ c: myCollection }).select(({ c }) => c) })\n\n` +
|
|
725
|
+
`Or switch to syncMode "eager" if you want all data to sync automatically.`,
|
|
726
|
+
)
|
|
727
|
+
}
|
|
728
|
+
// It's already a collection, ensure sync is started for React hooks
|
|
729
|
+
configOrQueryOrCollection.startSyncImmediate()
|
|
730
|
+
instance.collection = configOrQueryOrCollection
|
|
731
|
+
instance.config = configOrQueryOrCollection
|
|
930
732
|
} else {
|
|
931
733
|
if (preparedQueryValue === unpreparedQueryValue) {
|
|
932
734
|
preparedQueryValue = prepareQueryValue(
|
|
@@ -935,117 +737,149 @@ function useLiveQueryImpl(
|
|
|
935
737
|
instance.deferredCollections,
|
|
936
738
|
)
|
|
937
739
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
740
|
+
|
|
741
|
+
if (suspenseCollection) {
|
|
742
|
+
instance.collection = suspenseCollection
|
|
743
|
+
} else {
|
|
744
|
+
// A keyed query claims nothing, so it reports an unbound descriptor
|
|
745
|
+
// here; the other forms reported it when they claimed.
|
|
746
|
+
if (!dbClient && queryKey !== undefined) {
|
|
747
|
+
getPreparedSources(preparedQueryValue, dbClient, `useLiveQuery`)
|
|
748
|
+
}
|
|
749
|
+
instance.collection = resolveLiveQueryValue(preparedQueryValue, {
|
|
750
|
+
gcTime: forSuspense
|
|
751
|
+
? DEFAULT_SUSPENSE_GC_TIME_MS
|
|
752
|
+
: DEFAULT_GC_TIME_MS,
|
|
753
|
+
// Hydration and Suspense key the live-query Collection by identity,
|
|
754
|
+
// so they never pool, even when the config passes a pool.
|
|
755
|
+
pool: forSuspense || dbClient ? false : undefined,
|
|
756
|
+
// Without a DbClient, Suspense starts it through a preload below.
|
|
757
|
+
startSync: !forSuspense || dbClient !== undefined,
|
|
758
|
+
}) as SuspenseCollection | null
|
|
759
|
+
// A render that never commits releases its deferred sync starts
|
|
760
|
+
// when garbage collection reclaims the live query it built. A
|
|
761
|
+
// Collection the callback returned is not this render's to reclaim.
|
|
762
|
+
if (instance.collection && !isCollection(preparedQueryValue)) {
|
|
763
|
+
instance.cancelReclaimRelease?.()
|
|
764
|
+
instance.cancelReclaimRelease = releaseDeferredOnReclaim(
|
|
765
|
+
instance.collection,
|
|
766
|
+
instance.deferredCollections,
|
|
767
|
+
)
|
|
768
|
+
}
|
|
769
|
+
if (suspenseCollections && suspenseKey && instance.collection) {
|
|
770
|
+
const collection = instance.collection
|
|
771
|
+
const removeCleanupListener = collection.on(
|
|
772
|
+
`status:cleaned-up`,
|
|
964
773
|
() =>
|
|
965
774
|
releaseSuspenseCollection(
|
|
966
775
|
suspenseCollections,
|
|
967
776
|
suspenseKey,
|
|
968
777
|
collection,
|
|
969
778
|
),
|
|
970
|
-
0,
|
|
971
779
|
)
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
780
|
+
// Keep a failed or crashed collection through React's immediate
|
|
781
|
+
// retry so the hook can throw its actual error to an ErrorBoundary.
|
|
782
|
+
// Retire it afterward so a later boundary reset starts fresh.
|
|
783
|
+
const retireAfterRetry = () =>
|
|
784
|
+
setTimeout(
|
|
785
|
+
() =>
|
|
786
|
+
releaseSuspenseCollection(
|
|
787
|
+
suspenseCollections,
|
|
788
|
+
suspenseKey,
|
|
789
|
+
collection,
|
|
790
|
+
),
|
|
791
|
+
0,
|
|
792
|
+
)
|
|
793
|
+
const removeErrorListener = collection.on(
|
|
794
|
+
`status:error`,
|
|
795
|
+
retireAfterRetry,
|
|
796
|
+
)
|
|
797
|
+
const removeCrashListener = collection.on(
|
|
798
|
+
`status:crashed`,
|
|
799
|
+
retireAfterRetry,
|
|
800
|
+
)
|
|
801
|
+
const entry: SuspenseCollectionEntry = {
|
|
802
|
+
collection,
|
|
803
|
+
removeStatusListeners: () => {
|
|
804
|
+
removeCleanupListener()
|
|
805
|
+
removeErrorListener()
|
|
806
|
+
removeCrashListener()
|
|
807
|
+
},
|
|
808
|
+
}
|
|
809
|
+
suspenseCollections.set(suspenseKey, entry)
|
|
979
810
|
}
|
|
980
|
-
suspenseCollections.set(suspenseKey, entry)
|
|
981
811
|
}
|
|
812
|
+
instance.config = configOrQueryOrCollection
|
|
813
|
+
instance.identityDeps = [...identityDeps]
|
|
982
814
|
}
|
|
983
|
-
instance.
|
|
984
|
-
instance.
|
|
815
|
+
instance.client = dbClient
|
|
816
|
+
instance.queryHash = queryHash
|
|
817
|
+
instance.suspenseKey = suspenseKey
|
|
818
|
+
instance.identityError = identityError
|
|
985
819
|
}
|
|
986
|
-
instance.client = dbClient
|
|
987
|
-
instance.queryHash = queryHash
|
|
988
|
-
instance.suspenseKey = suspenseKey
|
|
989
|
-
instance.identityError = identityError
|
|
990
|
-
}
|
|
991
820
|
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
instance.collection?.status === `
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
821
|
+
// Recreate the observer when the underlying collection changes. The observer
|
|
822
|
+
// is not disposed explicitly here or on unmount: `useSyncExternalStore`
|
|
823
|
+
// unsubscribes it when the subscribe changes or the component unmounts, which
|
|
824
|
+
// detaches the collection subscription; the observer is then GC'd. (An unmount
|
|
825
|
+
// effect that disposed it would misfire under StrictMode/offscreen effect
|
|
826
|
+
// replay, leaving a disposed observer in the ref.)
|
|
827
|
+
if (needsNewCollection) {
|
|
828
|
+
// Defer the initial notify: useSyncExternalStore must not be notified
|
|
829
|
+
// synchronously during subscribe.
|
|
830
|
+
// Wholesale mode: React re-reads getSnapshot() on notify, keeps the
|
|
831
|
+
// hook's pre-observer loading policy, and — because wholesale delivers
|
|
832
|
+
// nothing synchronously during subscribe — never notifies
|
|
833
|
+
// useSyncExternalStore inside its own subscribe call.
|
|
834
|
+
instance.observer = createLiveQueryObserver(instance.collection, {
|
|
835
|
+
mode: `wholesale`,
|
|
836
|
+
client: dbClient,
|
|
837
|
+
queryHash: instance.queryHash,
|
|
838
|
+
onPreload: resumeDeferredCollections,
|
|
839
|
+
suspendUntilReady: forSuspense,
|
|
840
|
+
})
|
|
841
|
+
}
|
|
842
|
+
const observer = instance.observer!
|
|
843
|
+
// A Suspense render asks for its data. Preloading starts the sync run, so a
|
|
844
|
+
// source that already holds the rows makes it ready before the snapshot is
|
|
845
|
+
// read instead of suspending once. useLiveSuspenseQuery surfaces failures.
|
|
846
|
+
// A DbClient defers source starts past render and owns its streamed query.
|
|
847
|
+
if (
|
|
848
|
+
forSuspense &&
|
|
849
|
+
!dbClient &&
|
|
850
|
+
(instance.collection?.status === `idle` ||
|
|
851
|
+
instance.collection?.status === `cleaned-up`)
|
|
852
|
+
) {
|
|
853
|
+
observer.preload().catch(() => {})
|
|
854
|
+
}
|
|
1025
855
|
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
856
|
+
// Stable subscribe bound to the current observer; the observer owns the
|
|
857
|
+
// subscription, ready-race, and disposal.
|
|
858
|
+
if (!instance.subscribe || needsNewCollection) {
|
|
859
|
+
instance.subscribe = (onStoreChange: () => void) => {
|
|
860
|
+
const unsubscribe = observer.subscribe(onStoreChange)
|
|
861
|
+
resumeDeferredCollections()
|
|
862
|
+
return unsubscribe
|
|
863
|
+
}
|
|
1033
864
|
}
|
|
1034
|
-
}
|
|
1035
865
|
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
866
|
+
const returned = useSyncExternalStore(
|
|
867
|
+
instance.subscribe,
|
|
868
|
+
() => observer.getSnapshot(),
|
|
869
|
+
() => observer.getServerSnapshot(forSuspense),
|
|
870
|
+
)
|
|
871
|
+
// Only useLiveSuspenseQuery reads this, and it costs a define per render.
|
|
872
|
+
if (forSuspense) {
|
|
873
|
+
setLiveQueryResultInfo(returned, {
|
|
874
|
+
client: dbClient,
|
|
875
|
+
queryHash: instance.queryHash,
|
|
876
|
+
identityError: instance.identityError,
|
|
877
|
+
observer,
|
|
878
|
+
deferredCollections: instance.deferredCollections,
|
|
879
|
+
})
|
|
880
|
+
}
|
|
881
|
+
return returned as any
|
|
882
|
+
} catch (error) {
|
|
883
|
+
return rejectLiveQueryInput(instance.deferredCollections, error)
|
|
1049
884
|
}
|
|
1050
|
-
return returned as any
|
|
1051
885
|
}
|