@tanstack/react-db 0.5.5 → 0.5.7
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/source-id-bindings.cjs +60 -0
- package/dist/cjs/source-id-bindings.cjs.map +1 -0
- package/dist/cjs/source-id-bindings.d.cts +26 -0
- package/dist/cjs/useLiveInfiniteQuery.cjs +68 -26
- package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
- package/dist/cjs/useLiveQuery.cjs +18 -52
- package/dist/cjs/useLiveQuery.cjs.map +1 -1
- package/dist/esm/source-id-bindings.d.ts +26 -0
- package/dist/esm/source-id-bindings.js +60 -0
- package/dist/esm/source-id-bindings.js.map +1 -0
- package/dist/esm/useLiveInfiniteQuery.js +69 -27
- package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
- package/dist/esm/useLiveQuery.js +18 -52
- package/dist/esm/useLiveQuery.js.map +1 -1
- package/package.json +2 -2
- package/src/source-id-bindings.ts +96 -0
- package/src/useLiveInfiniteQuery.ts +129 -34
- package/src/useLiveQuery.ts +31 -68
|
@@ -9,17 +9,24 @@ import {
|
|
|
9
9
|
fetchNextLiveQueryWindowPage,
|
|
10
10
|
getLiveQueryWindowCollectionWarning,
|
|
11
11
|
getLiveQueryWindowInputKind,
|
|
12
|
+
liveQueryWindowMatches,
|
|
12
13
|
normalizeLiveQueryWindowPageSize,
|
|
13
14
|
resolveLiveQueryWindowInput,
|
|
14
15
|
shouldPreserveLiveQueryWindowPageCount,
|
|
15
16
|
} from '@tanstack/db'
|
|
16
17
|
import { useOptionalDbClient } from './DbProvider'
|
|
18
|
+
import {
|
|
19
|
+
claimSourceIds,
|
|
20
|
+
createSourceIdBindings,
|
|
21
|
+
resumeSyncStarts,
|
|
22
|
+
} from './source-id-bindings'
|
|
17
23
|
import {
|
|
18
24
|
prepareDerivedQuery,
|
|
19
25
|
prepareQueryValue,
|
|
20
26
|
warnDeprecatedDepsArray,
|
|
21
27
|
warnUnhashableDerivedIdentity,
|
|
22
28
|
} from './useLiveQuery'
|
|
29
|
+
import type { SourceIdBindings } from './source-id-bindings'
|
|
23
30
|
import type {
|
|
24
31
|
DerivedIdentityProfiler,
|
|
25
32
|
LiveQueryKey,
|
|
@@ -119,6 +126,30 @@ type InfiniteQueryRenderState = {
|
|
|
119
126
|
>
|
|
120
127
|
}
|
|
121
128
|
|
|
129
|
+
type RenderedWindowCollection = {
|
|
130
|
+
client: DbClient | undefined
|
|
131
|
+
dependencies: Array<unknown>
|
|
132
|
+
collection: Collection<any, any, any>
|
|
133
|
+
deferredCollections: Set<
|
|
134
|
+
CollectionImplType<any, string | number, any, any, any>
|
|
135
|
+
>
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether a recorded query identity matches the current one. Legacy deps
|
|
140
|
+
* compare by reference. Derived identities and query keys compare by structure.
|
|
141
|
+
*/
|
|
142
|
+
function queryIdentityMatches(
|
|
143
|
+
comparison: { changed: boolean; structurallyEqual: boolean },
|
|
144
|
+
sameClient: boolean,
|
|
145
|
+
usesLegacyDeps: boolean,
|
|
146
|
+
): boolean {
|
|
147
|
+
return (
|
|
148
|
+
sameClient &&
|
|
149
|
+
(usesLegacyDeps ? !comparison.changed : comparison.structurallyEqual)
|
|
150
|
+
)
|
|
151
|
+
}
|
|
152
|
+
|
|
122
153
|
/**
|
|
123
154
|
* Create an infinite query using a query function with live updates.
|
|
124
155
|
*
|
|
@@ -169,6 +200,13 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
169
200
|
|
|
170
201
|
const committedRef = useRef<InfiniteQueryRenderState | null>(null)
|
|
171
202
|
const committed = committedRef.current
|
|
203
|
+
// Like useLiveQuery's instance memo, keep the window collection built by the
|
|
204
|
+
// latest render. A second render before commit, such as a StrictMode double
|
|
205
|
+
// render, reuses it instead of starting another one. Only the collection is
|
|
206
|
+
// kept. Each render still builds its own controller and page count from the
|
|
207
|
+
// committed state, which an uncommitted render never changes.
|
|
208
|
+
const renderedCollectionRef = useRef<RenderedWindowCollection | null>(null)
|
|
209
|
+
const sourceIdsRef = useRef<SourceIdBindings | null>(null)
|
|
172
210
|
const inputKind = inputIsCollection ? `collection` : `query`
|
|
173
211
|
const derivedIdentityProfilerRef = useRef<DerivedIdentityProfiler>({
|
|
174
212
|
renderCount: 0,
|
|
@@ -212,6 +250,13 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
212
250
|
deferredCollections,
|
|
213
251
|
)
|
|
214
252
|
preparedQueryValue = preparation.value
|
|
253
|
+
claimSourceIds(
|
|
254
|
+
(sourceIdsRef.current ??= createSourceIdBindings()),
|
|
255
|
+
preparation.value,
|
|
256
|
+
dbClient,
|
|
257
|
+
`useLiveInfiniteQuery`,
|
|
258
|
+
() => resumeSyncStarts(deferredCollections),
|
|
259
|
+
)
|
|
215
260
|
if (preparation.status === `hashable`) {
|
|
216
261
|
identityDeps = preparation.identityDeps
|
|
217
262
|
} else {
|
|
@@ -230,10 +275,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
230
275
|
const sameClient = committed?.client === dbClient
|
|
231
276
|
const dependenciesChanged =
|
|
232
277
|
!inputIsCollection &&
|
|
233
|
-
(
|
|
234
|
-
(usesLegacyDeps
|
|
235
|
-
? dependencyComparison.changed
|
|
236
|
-
: !dependencyComparison.structurallyEqual))
|
|
278
|
+
!queryIdentityMatches(dependencyComparison, sameClient, usesLegacyDeps)
|
|
237
279
|
const dependenciesStructurallyEqual =
|
|
238
280
|
usesLegacyDeps && sameClient && dependencyComparison.structurallyEqual
|
|
239
281
|
const needsNewCollection =
|
|
@@ -252,6 +294,26 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
252
294
|
if (needsNewController) {
|
|
253
295
|
let collection = committed?.collection
|
|
254
296
|
let warning: string | null = null
|
|
297
|
+
let startInRender = false
|
|
298
|
+
let suppliedCollection = false
|
|
299
|
+
let createdCollection = false
|
|
300
|
+
|
|
301
|
+
const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({
|
|
302
|
+
hasPreviousController: committed !== null,
|
|
303
|
+
previousInputKind: committed?.inputKind,
|
|
304
|
+
inputKind,
|
|
305
|
+
sameCollection:
|
|
306
|
+
inputIsCollection && committed?.inputCollection === queryFnOrCollection,
|
|
307
|
+
dependenciesChanged,
|
|
308
|
+
dependenciesStructurallyEqual,
|
|
309
|
+
pageShapeChanged,
|
|
310
|
+
})
|
|
311
|
+
const previousPageCount = committed
|
|
312
|
+
? Math.max(1, committed.controller.getSnapshot().pages.length)
|
|
313
|
+
: 1
|
|
314
|
+
const initialPageCount = canPreservePageCount ? previousPageCount : 1
|
|
315
|
+
// The peek-ahead window for every retained page.
|
|
316
|
+
const requiredLimit = initialPageCount * pageSize + 1
|
|
255
317
|
|
|
256
318
|
if (needsNewCollection) {
|
|
257
319
|
let inputValue = queryFnOrCollection
|
|
@@ -265,19 +327,50 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
265
327
|
}
|
|
266
328
|
inputValue = () => preparedQueryValue
|
|
267
329
|
}
|
|
268
|
-
const
|
|
269
|
-
if (
|
|
270
|
-
|
|
330
|
+
const rendered = renderedCollectionRef.current
|
|
331
|
+
if (
|
|
332
|
+
!inputIsCollection &&
|
|
333
|
+
rendered !== null &&
|
|
334
|
+
queryIdentityMatches(
|
|
335
|
+
compareLiveQueryWindowDependencies(
|
|
336
|
+
rendered.dependencies,
|
|
337
|
+
identityDeps,
|
|
338
|
+
),
|
|
339
|
+
rendered.client === dbClient,
|
|
340
|
+
usesLegacyDeps,
|
|
341
|
+
) &&
|
|
342
|
+
rendered.collection.status !== `cleaned-up` &&
|
|
343
|
+
rendered.collection.status !== `error` &&
|
|
344
|
+
liveQueryWindowMatches(rendered.collection, requiredLimit)
|
|
345
|
+
) {
|
|
346
|
+
// An earlier render built this collection and nothing has committed
|
|
347
|
+
// since. Its window is exactly the retained pages, so its first
|
|
348
|
+
// rows are correct. Sources it deferred still resume at commit. A
|
|
349
|
+
// collection in terminal error, such as after its source restarted,
|
|
350
|
+
// cannot serve the query again, so a new one replaces it.
|
|
351
|
+
collection = rendered.collection
|
|
352
|
+
for (const deferred of rendered.deferredCollections) {
|
|
353
|
+
deferredCollections.add(deferred)
|
|
354
|
+
}
|
|
271
355
|
} else {
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
356
|
+
const input = resolveLiveQueryWindowInput<TContext>(inputValue)
|
|
357
|
+
if (input.kind === `collection`) {
|
|
358
|
+
collection = input.collection
|
|
359
|
+
suppliedCollection = true
|
|
360
|
+
// A supplied collection is never reused, so release the last one.
|
|
361
|
+
renderedCollectionRef.current = null
|
|
362
|
+
} else {
|
|
363
|
+
// Wrap the query with the peek-ahead window for every retained page,
|
|
364
|
+
// so a collection that starts syncing now never publishes fewer rows
|
|
365
|
+
// than the controller's pages. The controller grows the limit via
|
|
366
|
+
// setWindow.
|
|
367
|
+
collection = createLiveQueryCollection({
|
|
368
|
+
query: input.query.limit(requiredLimit).offset(0),
|
|
369
|
+
gcTime: DEFAULT_GC_TIME_MS,
|
|
370
|
+
})
|
|
371
|
+
startInRender = true
|
|
372
|
+
createdCollection = true
|
|
373
|
+
}
|
|
281
374
|
}
|
|
282
375
|
}
|
|
283
376
|
|
|
@@ -291,21 +384,26 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
291
384
|
} else {
|
|
292
385
|
assertLiveQueryWindowManyResult(collection)
|
|
293
386
|
}
|
|
387
|
+
// Like useLiveQuery, start sync during render once the input is valid. A
|
|
388
|
+
// render reads only rows the sources already hold, such as a preloaded
|
|
389
|
+
// collection's, and sends no request until the subscription commits. GC
|
|
390
|
+
// reclaims a render that never commits. A supplied window that the
|
|
391
|
+
// controller must still adjust waits for the subscription.
|
|
392
|
+
if (suppliedCollection) {
|
|
393
|
+
startInRender = liveQueryWindowMatches(collection, requiredLimit)
|
|
394
|
+
}
|
|
395
|
+
if (startInRender) collection.startSyncImmediate()
|
|
396
|
+
// Keep a created collection for the next render only once it validated and
|
|
397
|
+
// started, so a retry after a startup error builds a new one and throws.
|
|
398
|
+
if (createdCollection) {
|
|
399
|
+
renderedCollectionRef.current = {
|
|
400
|
+
client: dbClient,
|
|
401
|
+
dependencies: [...identityDeps],
|
|
402
|
+
collection,
|
|
403
|
+
deferredCollections,
|
|
404
|
+
}
|
|
405
|
+
}
|
|
294
406
|
|
|
295
|
-
const canPreservePageCount = shouldPreserveLiveQueryWindowPageCount({
|
|
296
|
-
hasPreviousController: committed !== null,
|
|
297
|
-
previousInputKind: committed?.inputKind,
|
|
298
|
-
inputKind,
|
|
299
|
-
sameCollection:
|
|
300
|
-
inputIsCollection && committed?.inputCollection === collection,
|
|
301
|
-
dependenciesChanged,
|
|
302
|
-
dependenciesStructurallyEqual,
|
|
303
|
-
pageShapeChanged,
|
|
304
|
-
})
|
|
305
|
-
const previousPageCount = committed
|
|
306
|
-
? Math.max(1, committed.controller.getSnapshot().pages.length)
|
|
307
|
-
: 1
|
|
308
|
-
const initialPageCount = canPreservePageCount ? previousPageCount : 1
|
|
309
407
|
renderState = {
|
|
310
408
|
inputKind,
|
|
311
409
|
inputCollection: inputIsCollection ? collection : null,
|
|
@@ -337,10 +435,7 @@ export function useLiveInfiniteQuery<TContext extends Context>(
|
|
|
337
435
|
currentRenderState.warned = true
|
|
338
436
|
console.warn(currentRenderState.warning)
|
|
339
437
|
}
|
|
340
|
-
|
|
341
|
-
collection._resumeSyncStart()
|
|
342
|
-
}
|
|
343
|
-
currentRenderState.deferredCollections.clear()
|
|
438
|
+
resumeSyncStarts(currentRenderState.deferredCollections)
|
|
344
439
|
return unsubscribe
|
|
345
440
|
},
|
|
346
441
|
[controller, currentRenderState],
|
package/src/useLiveQuery.ts
CHANGED
|
@@ -2,8 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
import { useRef, useSyncExternalStore } from 'react'
|
|
4
4
|
import {
|
|
5
|
-
BaseQueryBuilder,
|
|
6
|
-
IR,
|
|
7
5
|
UnhashableQueryIRError,
|
|
8
6
|
createLiveQueryObserver,
|
|
9
7
|
deepEquals,
|
|
@@ -16,6 +14,13 @@ import {
|
|
|
16
14
|
import { useOptionalDbClient } from './DbProvider'
|
|
17
15
|
import { setLiveQueryResultInfo } from './live-query-internals'
|
|
18
16
|
import { shouldWarnInDevelopment } from './development'
|
|
17
|
+
import {
|
|
18
|
+
claimSourceIds,
|
|
19
|
+
createSourceIdBindings,
|
|
20
|
+
getPreparedSources,
|
|
21
|
+
getSourceObjectToken,
|
|
22
|
+
resumeSyncStarts,
|
|
23
|
+
} from './source-id-bindings'
|
|
19
24
|
import type {
|
|
20
25
|
Collection,
|
|
21
26
|
CollectionImpl,
|
|
@@ -56,36 +61,6 @@ const suspenseCollectionsByClient = new WeakMap<
|
|
|
56
61
|
DbClient,
|
|
57
62
|
Map<string, SuspenseCollectionEntry>
|
|
58
63
|
>()
|
|
59
|
-
const sourceObjectTokens = new WeakMap<object, number>()
|
|
60
|
-
let nextSourceObjectToken = 0
|
|
61
|
-
|
|
62
|
-
function getSourceObjectToken(source: object): number {
|
|
63
|
-
let id = sourceObjectTokens.get(source)
|
|
64
|
-
if (id === undefined) {
|
|
65
|
-
id = ++nextSourceObjectToken
|
|
66
|
-
sourceObjectTokens.set(source, id)
|
|
67
|
-
}
|
|
68
|
-
return id
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
function getPreparedSources(preparedValue: unknown): Array<{ id: string }> {
|
|
72
|
-
if (isCollection(preparedValue)) return [preparedValue]
|
|
73
|
-
const query =
|
|
74
|
-
preparedValue instanceof BaseQueryBuilder
|
|
75
|
-
? preparedValue
|
|
76
|
-
: preparedValue &&
|
|
77
|
-
typeof preparedValue === `object` &&
|
|
78
|
-
`query` in preparedValue &&
|
|
79
|
-
preparedValue.query instanceof BaseQueryBuilder
|
|
80
|
-
? preparedValue.query
|
|
81
|
-
: undefined
|
|
82
|
-
return query
|
|
83
|
-
? IR.collectCollectionSources(query._getQuery()).map(
|
|
84
|
-
({ collection }) => collection,
|
|
85
|
-
)
|
|
86
|
-
: []
|
|
87
|
-
}
|
|
88
|
-
|
|
89
64
|
function getSourceQualifiedSuspenseKey(
|
|
90
65
|
preparedValue: unknown,
|
|
91
66
|
queryHash: string,
|
|
@@ -751,10 +726,7 @@ function createHookInstance(dbClient: DbClient | undefined) {
|
|
|
751
726
|
deferredCollections: new Set<
|
|
752
727
|
CollectionImpl<any, string | number, any, any, any>
|
|
753
728
|
>(),
|
|
754
|
-
sourceIds:
|
|
755
|
-
unscoped: new Map<string, number>(),
|
|
756
|
-
byClient: new WeakMap<DbClient, Map<string, number>>(),
|
|
757
|
-
},
|
|
729
|
+
sourceIds: createSourceIdBindings(),
|
|
758
730
|
observer: null as LiveQueryObserver<object, string | number> | null,
|
|
759
731
|
queryHash: undefined as string | undefined,
|
|
760
732
|
suspenseKey: undefined as string | undefined,
|
|
@@ -778,19 +750,8 @@ function useLiveQueryImpl(
|
|
|
778
750
|
|
|
779
751
|
const instanceRef = useRef<ReturnType<typeof createHookInstance> | null>(null)
|
|
780
752
|
const instance = (instanceRef.current ??= createHookInstance(dbClient))
|
|
781
|
-
const resumeDeferredCollections = () =>
|
|
782
|
-
|
|
783
|
-
instance.deferredCollections.clear()
|
|
784
|
-
const errors: Array<unknown> = []
|
|
785
|
-
for (const collection of collections) {
|
|
786
|
-
try {
|
|
787
|
-
collection._resumeSyncStart()
|
|
788
|
-
} catch (error) {
|
|
789
|
-
errors.push(error)
|
|
790
|
-
}
|
|
791
|
-
}
|
|
792
|
-
if (errors.length) throw errors[0]
|
|
793
|
-
}
|
|
753
|
+
const resumeDeferredCollections = () =>
|
|
754
|
+
resumeSyncStarts(instance.deferredCollections)
|
|
794
755
|
|
|
795
756
|
const queryKey = !inputIsCollection
|
|
796
757
|
? getExplicitQueryKey(configOrQueryOrCollection)
|
|
@@ -873,25 +834,13 @@ function useLiveQueryImpl(
|
|
|
873
834
|
deps === undefined &&
|
|
874
835
|
preparedQueryValue !== unpreparedQueryValue
|
|
875
836
|
) {
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
if (previous !== undefined && previous !== token) {
|
|
884
|
-
// The rejected render must not retain a shared Collection's sync deferral.
|
|
885
|
-
resumeDeferredCollections()
|
|
886
|
-
throw new Error(
|
|
887
|
-
`[useLiveQuery] Source Collection "${source.id}" was replaced by a different Collection with the same ID while this hook is mounted. Unmount the hook and clean up its previous source and client scope before reusing the ID.`,
|
|
888
|
-
)
|
|
889
|
-
}
|
|
890
|
-
seen.set(source.id, token)
|
|
891
|
-
}
|
|
892
|
-
const bindings = prior ?? new Map<string, number>()
|
|
893
|
-
for (const [id, token] of seen) bindings.set(id, token)
|
|
894
|
-
if (dbClient) instance.sourceIds.byClient.set(dbClient, bindings)
|
|
837
|
+
claimSourceIds(
|
|
838
|
+
instance.sourceIds,
|
|
839
|
+
preparedQueryValue,
|
|
840
|
+
dbClient,
|
|
841
|
+
`useLiveQuery`,
|
|
842
|
+
resumeDeferredCollections,
|
|
843
|
+
)
|
|
895
844
|
}
|
|
896
845
|
|
|
897
846
|
const canReuseSuspenseKey =
|
|
@@ -987,6 +936,8 @@ function useLiveQueryImpl(
|
|
|
987
936
|
: DEFAULT_GC_TIME_MS,
|
|
988
937
|
// Hydration and Suspense key the live-query Collection by identity.
|
|
989
938
|
pool: !forSuspense && !dbClient,
|
|
939
|
+
// Without a DbClient, Suspense starts it through a preload below.
|
|
940
|
+
startSync: !forSuspense || dbClient !== undefined,
|
|
990
941
|
}) as SuspenseCollection | null
|
|
991
942
|
if (suspenseCollections && suspenseKey && instance.collection) {
|
|
992
943
|
const collection = instance.collection
|
|
@@ -1051,6 +1002,18 @@ function useLiveQueryImpl(
|
|
|
1051
1002
|
})
|
|
1052
1003
|
}
|
|
1053
1004
|
const observer = instance.observer!
|
|
1005
|
+
// A Suspense render asks for its data. Preloading starts the sync run, so a
|
|
1006
|
+
// source that already holds the rows makes it ready before the snapshot is
|
|
1007
|
+
// read instead of suspending once. useLiveSuspenseQuery surfaces failures.
|
|
1008
|
+
// A DbClient defers source starts past render and owns its streamed query.
|
|
1009
|
+
if (
|
|
1010
|
+
forSuspense &&
|
|
1011
|
+
!dbClient &&
|
|
1012
|
+
(instance.collection?.status === `idle` ||
|
|
1013
|
+
instance.collection?.status === `cleaned-up`)
|
|
1014
|
+
) {
|
|
1015
|
+
observer.preloadForInitialRender().catch(() => {})
|
|
1016
|
+
}
|
|
1054
1017
|
|
|
1055
1018
|
// Stable subscribe bound to the current observer; the observer owns the
|
|
1056
1019
|
// subscription, ready-race, and disposal.
|