@tanstack/react-db 0.5.11 → 1.0.0-rc.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
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
3
|
import { useRef } from 'react'
|
|
4
|
+
import { rejectLiveQueryInput, toBoundaryError } from '@tanstack/db'
|
|
4
5
|
import { useLiveQueryForSuspense } from './useLiveQuery'
|
|
5
6
|
import { getLiveQueryResultInfo } from './live-query-internals'
|
|
6
7
|
import type { UseLiveQueryConfig } from './useLiveQuery'
|
|
@@ -9,9 +10,9 @@ import type {
|
|
|
9
10
|
Context,
|
|
10
11
|
DbClient,
|
|
11
12
|
GetResult,
|
|
13
|
+
HealthNode,
|
|
12
14
|
InferResultType,
|
|
13
15
|
InitialQueryBuilder,
|
|
14
|
-
LiveQueryCollectionConfig,
|
|
15
16
|
NonSingleResult,
|
|
16
17
|
QueryBuilder,
|
|
17
18
|
SingleResult,
|
|
@@ -60,10 +61,12 @@ function rememberInitialRenderError(
|
|
|
60
61
|
/**
|
|
61
62
|
* Create a live query with React Suspense support
|
|
62
63
|
* @param queryFn - Query function that defines what data to fetch
|
|
63
|
-
* @param deps - Deprecated array of dependencies that trigger query re-execution when changed
|
|
64
64
|
* @returns Object with reactive data and state - data is guaranteed to be defined
|
|
65
65
|
* @throws Promise when data is loading (caught by Suspense boundary)
|
|
66
|
-
* @throws Error when
|
|
66
|
+
* @throws Error when the first load fails or the Collection crashes (caught by
|
|
67
|
+
* an Error boundary): the cause itself when it is an Error, otherwise an Error
|
|
68
|
+
* with the problem's message (`health.error.message`) whose `cause` is the
|
|
69
|
+
* value the sync adapter reported
|
|
67
70
|
* @example
|
|
68
71
|
* // Basic usage with Suspense
|
|
69
72
|
* function TodoList() {
|
|
@@ -150,11 +153,11 @@ function rememberInitialRenderError(
|
|
|
150
153
|
// Overload 1: Accept query function that always returns QueryBuilder
|
|
151
154
|
export function useLiveSuspenseQuery<TContext extends Context>(
|
|
152
155
|
queryFn: (q: InitialQueryBuilder) => QueryBuilder<TContext>,
|
|
153
|
-
deps?: Array<unknown>,
|
|
154
156
|
): {
|
|
155
157
|
state: Map<string | number, GetResult<TContext>>
|
|
156
158
|
data: InferResultType<TContext>
|
|
157
159
|
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
160
|
+
health: HealthNode | undefined
|
|
158
161
|
}
|
|
159
162
|
|
|
160
163
|
// Overload 2: Accept config object
|
|
@@ -164,16 +167,7 @@ export function useLiveSuspenseQuery<TContext extends Context>(
|
|
|
164
167
|
state: Map<string | number, GetResult<TContext>>
|
|
165
168
|
data: InferResultType<TContext>
|
|
166
169
|
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
// Overload 3: Accept legacy config object
|
|
170
|
-
export function useLiveSuspenseQuery<TContext extends Context>(
|
|
171
|
-
config: LiveQueryCollectionConfig<TContext>,
|
|
172
|
-
deps?: Array<unknown>,
|
|
173
|
-
): {
|
|
174
|
-
state: Map<string | number, GetResult<TContext>>
|
|
175
|
-
data: InferResultType<TContext>
|
|
176
|
-
collection: Collection<GetResult<TContext>, string | number, {}>
|
|
170
|
+
health: HealthNode | undefined
|
|
177
171
|
}
|
|
178
172
|
|
|
179
173
|
// Overload 4: Accept pre-created live query collection
|
|
@@ -187,6 +181,7 @@ export function useLiveSuspenseQuery<
|
|
|
187
181
|
state: Map<TKey, TResult>
|
|
188
182
|
data: Array<TResult>
|
|
189
183
|
collection: Collection<TResult, TKey, TUtils>
|
|
184
|
+
health: HealthNode | undefined
|
|
190
185
|
}
|
|
191
186
|
|
|
192
187
|
// Overload 5: Accept pre-created live query collection with singleResult: true
|
|
@@ -200,22 +195,17 @@ export function useLiveSuspenseQuery<
|
|
|
200
195
|
state: Map<TKey, TResult>
|
|
201
196
|
data: TResult | undefined
|
|
202
197
|
collection: Collection<TResult, TKey, TUtils> & SingleResult
|
|
198
|
+
health: HealthNode | undefined
|
|
203
199
|
}
|
|
204
200
|
|
|
205
201
|
// Implementation - uses useLiveQuery internally and adds Suspense logic
|
|
206
|
-
export function useLiveSuspenseQuery(
|
|
207
|
-
configOrQueryOrCollection: any,
|
|
208
|
-
deps?: Array<unknown>,
|
|
209
|
-
) {
|
|
202
|
+
export function useLiveSuspenseQuery(configOrQueryOrCollection: any) {
|
|
210
203
|
const promiseRef = useRef<Promise<void> | null>(null)
|
|
211
204
|
const collectionRef = useRef<Collection<any, any, any> | null>(null)
|
|
212
205
|
const hasBeenReadyRef = useRef(false)
|
|
213
206
|
|
|
214
207
|
// Use useLiveQuery to handle collection management and reactivity
|
|
215
|
-
const result =
|
|
216
|
-
deps === undefined
|
|
217
|
-
? useLiveQueryForSuspense(configOrQueryOrCollection, undefined)
|
|
218
|
-
: useLiveQueryForSuspense(configOrQueryOrCollection, deps)
|
|
208
|
+
const result = useLiveQueryForSuspense(configOrQueryOrCollection)
|
|
219
209
|
|
|
220
210
|
if (!result.isEnabled) {
|
|
221
211
|
// Suspense queries cannot be disabled - this matches TanStack Query's useSuspenseQuery behavior
|
|
@@ -229,81 +219,43 @@ export function useLiveSuspenseQuery(
|
|
|
229
219
|
}
|
|
230
220
|
|
|
231
221
|
const queryInfo = getLiveQueryResultInfo(result)
|
|
222
|
+
// The render boundary: an error thrown before this render commits releases
|
|
223
|
+
// the sync starts it deferred. A thrown promise suspends; its preload owns
|
|
224
|
+
// those starts.
|
|
225
|
+
try {
|
|
226
|
+
// Reset promise and ready state when query identity changes
|
|
227
|
+
if (collectionRef.current !== result.collection) {
|
|
228
|
+
promiseRef.current = null
|
|
229
|
+
collectionRef.current = result.collection
|
|
230
|
+
hasBeenReadyRef.current = false
|
|
231
|
+
}
|
|
232
232
|
|
|
233
|
-
|
|
234
|
-
if (collectionRef.current !== result.collection) {
|
|
235
|
-
promiseRef.current = null
|
|
236
|
-
collectionRef.current = result.collection
|
|
237
|
-
hasBeenReadyRef.current = false
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
// SUSPENSE LOGIC: Throw promise or error based on collection status
|
|
241
|
-
|
|
242
|
-
const collectionStatus = result.collection.status
|
|
243
|
-
|
|
244
|
-
// Track when we reach ready state
|
|
245
|
-
if (result.isReady || queryInfo.observer.isInitialRenderReady()) {
|
|
246
|
-
hasBeenReadyRef.current = true
|
|
247
|
-
promiseRef.current = null
|
|
248
|
-
clearInitialRenderError(result.collection, queryInfo.client)
|
|
249
|
-
}
|
|
233
|
+
// SUSPENSE LOGIC: Throw promise or error based on collection status
|
|
250
234
|
|
|
251
|
-
|
|
252
|
-
// A client request can reject with undefined, which getError() cannot
|
|
253
|
-
// distinguish from no error. The request status preserves that distinction.
|
|
254
|
-
const clientQuery =
|
|
255
|
-
queryInfo.client && queryInfo.queryHash
|
|
256
|
-
? queryInfo.client._getLiveQuery(queryInfo.queryHash)
|
|
257
|
-
: undefined
|
|
258
|
-
// A configured persisted restore may still satisfy this render after the
|
|
259
|
-
// client query fails. Let the observer classify that failure below.
|
|
260
|
-
if (
|
|
261
|
-
!hasBeenReadyRef.current &&
|
|
262
|
-
(result.persistedStatus === `unavailable` ||
|
|
263
|
-
result.persistedStatus === `error`) &&
|
|
264
|
-
(observerError !== undefined || clientQuery?.status === `error`)
|
|
265
|
-
) {
|
|
266
|
-
promiseRef.current = null
|
|
267
|
-
throw observerError === undefined ? clientQuery?.error : observerError
|
|
268
|
-
}
|
|
235
|
+
const collectionStatus = result.collection.status
|
|
269
236
|
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
initialRenderError?.clientQuery !== undefined &&
|
|
277
|
-
initialRenderError.clientQuery !== clientQuery
|
|
278
|
-
) {
|
|
279
|
-
clearInitialRenderError(result.collection, queryInfo.client)
|
|
280
|
-
} else if (initialRenderError && !hasBeenReadyRef.current) {
|
|
281
|
-
promiseRef.current = null
|
|
282
|
-
throw initialRenderError.error
|
|
283
|
-
}
|
|
237
|
+
// Track when we reach ready state
|
|
238
|
+
if (result.isReady) {
|
|
239
|
+
hasBeenReadyRef.current = true
|
|
240
|
+
promiseRef.current = null
|
|
241
|
+
clearInitialRenderError(result.collection, queryInfo.client)
|
|
242
|
+
}
|
|
284
243
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
244
|
+
const observerError = queryInfo.observer.getError()
|
|
245
|
+
// A client request can reject with undefined, which getError() cannot
|
|
246
|
+
// distinguish from no error. The request status preserves that distinction.
|
|
247
|
+
const clientQuery =
|
|
248
|
+
queryInfo.client && queryInfo.queryHash
|
|
249
|
+
? queryInfo.client._getLiveQuery(queryInfo.queryHash)
|
|
250
|
+
: undefined
|
|
251
|
+
if (
|
|
252
|
+
!hasBeenReadyRef.current &&
|
|
253
|
+
(observerError !== undefined || clientQuery?.status === `error`)
|
|
254
|
+
) {
|
|
255
|
+
promiseRef.current = null
|
|
256
|
+
throw observerError === undefined ? clientQuery?.error : observerError
|
|
257
|
+
}
|
|
298
258
|
|
|
299
|
-
if (
|
|
300
|
-
!hasBeenReadyRef.current &&
|
|
301
|
-
(result.isLoading ||
|
|
302
|
-
result.isIdle ||
|
|
303
|
-
result.isError ||
|
|
304
|
-
(collectionStatus === `error` &&
|
|
305
|
-
result.persistedStatus !== `unavailable`))
|
|
306
|
-
) {
|
|
307
259
|
if (queryInfo.client?._isSsrStreamingEnabled() && !queryInfo.queryHash) {
|
|
308
260
|
const reason = queryInfo.identityError
|
|
309
261
|
? `${queryInfo.identityError.reason} at ${queryInfo.identityError.path}`
|
|
@@ -312,48 +264,86 @@ export function useLiveSuspenseQuery(
|
|
|
312
264
|
`Cannot stream this live query during SSR because ${reason}. Provide an explicit serializable queryKey.`,
|
|
313
265
|
)
|
|
314
266
|
}
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
267
|
+
const errorEntry = initialRenderErrors.get(result.collection)
|
|
268
|
+
const initialRenderError = queryInfo.client
|
|
269
|
+
? errorEntry?.byClient.get(queryInfo.client)
|
|
270
|
+
: errorEntry?.unscoped
|
|
271
|
+
// A replacement client query must not inherit the prior request's error.
|
|
272
|
+
if (
|
|
273
|
+
initialRenderError?.clientQuery !== undefined &&
|
|
274
|
+
initialRenderError.clientQuery !== clientQuery
|
|
275
|
+
) {
|
|
276
|
+
clearInitialRenderError(result.collection, queryInfo.client)
|
|
277
|
+
} else if (initialRenderError && !hasBeenReadyRef.current) {
|
|
278
|
+
promiseRef.current = null
|
|
279
|
+
throw toBoundaryError(initialRenderError.error)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
// A failed first load throws only before the first ready; later sync
|
|
283
|
+
// problems surface in `health` with the data kept (as in TanStack Query).
|
|
284
|
+
// A crash throws at any time: the Collection can no longer serve reads.
|
|
285
|
+
if (
|
|
286
|
+
collectionStatus === `crashed` ||
|
|
287
|
+
(collectionStatus === `error` && !hasBeenReadyRef.current)
|
|
288
|
+
) {
|
|
289
|
+
promiseRef.current = null
|
|
290
|
+
throw toBoundaryError(result.collection.health.error?.cause)
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (
|
|
294
|
+
!hasBeenReadyRef.current &&
|
|
295
|
+
(result.isLoading ||
|
|
296
|
+
result.isIdle ||
|
|
297
|
+
result.isLoadingError ||
|
|
298
|
+
result.isCrashed)
|
|
299
|
+
) {
|
|
300
|
+
// Create or reuse promise for current collection
|
|
301
|
+
if (!promiseRef.current) {
|
|
302
|
+
const collection = result.collection
|
|
303
|
+
const client = queryInfo.client
|
|
304
|
+
const queryHash = queryInfo.queryHash
|
|
305
|
+
let active = true
|
|
306
|
+
const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {
|
|
307
|
+
active = false
|
|
345
308
|
})
|
|
346
|
-
|
|
309
|
+
let preload: Promise<void>
|
|
310
|
+
try {
|
|
311
|
+
preload = queryInfo.observer.preload()
|
|
312
|
+
} catch (error) {
|
|
313
|
+
stopWatchingCleanup()
|
|
314
|
+
throw error
|
|
315
|
+
}
|
|
316
|
+
const preloadClientQuery =
|
|
317
|
+
client && queryHash ? client._getLiveQuery(queryHash) : undefined
|
|
318
|
+
promiseRef.current = preload
|
|
319
|
+
.catch((error: unknown) => {
|
|
320
|
+
const latestClientQuery =
|
|
321
|
+
client && queryHash ? client._getLiveQuery(queryHash) : undefined
|
|
322
|
+
if (active && latestClientQuery === preloadClientQuery)
|
|
323
|
+
rememberInitialRenderError(
|
|
324
|
+
collection,
|
|
325
|
+
client,
|
|
326
|
+
error,
|
|
327
|
+
preloadClientQuery,
|
|
328
|
+
)
|
|
329
|
+
throw error
|
|
330
|
+
})
|
|
331
|
+
.finally(stopWatchingCleanup)
|
|
332
|
+
}
|
|
333
|
+
// React Suspense catches this promise and retries after preload settles.
|
|
334
|
+
throw promiseRef.current
|
|
347
335
|
}
|
|
348
|
-
// React Suspense catches this promise and retries after preload settles.
|
|
349
|
-
throw promiseRef.current
|
|
350
|
-
}
|
|
351
336
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
337
|
+
// Return data without status/loading flags (handled by Suspense/ErrorBoundary)
|
|
338
|
+
// If error after success, return last known good state (stale data)
|
|
339
|
+
return {
|
|
340
|
+
state: result.state,
|
|
341
|
+
data: result.data,
|
|
342
|
+
collection: result.collection,
|
|
343
|
+
health: result.health,
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
if (error instanceof Promise) throw error
|
|
347
|
+
return rejectLiveQueryInput(queryInfo.deferredCollections, error)
|
|
358
348
|
}
|
|
359
349
|
}
|
package/src/usePacedMutations.ts
CHANGED
|
@@ -36,7 +36,7 @@ import type { PacedMutationsConfig, Transaction } from '@tanstack/db'
|
|
|
36
36
|
*
|
|
37
37
|
* // Optional: await persistence or handle errors
|
|
38
38
|
* try {
|
|
39
|
-
* await tx.
|
|
39
|
+
* await tx.when('settled')
|
|
40
40
|
* console.log('Saved!')
|
|
41
41
|
* } catch (error) {
|
|
42
42
|
* console.error('Save failed:', error)
|