@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.
Files changed (40) hide show
  1. package/dist/cjs/live-query-internals.cjs.map +1 -1
  2. package/dist/cjs/live-query-internals.d.cts +3 -1
  3. package/dist/cjs/source-id-bindings.cjs +9 -42
  4. package/dist/cjs/source-id-bindings.cjs.map +1 -1
  5. package/dist/cjs/source-id-bindings.d.cts +6 -17
  6. package/dist/cjs/useLiveInfiniteQuery.cjs +93 -173
  7. package/dist/cjs/useLiveInfiniteQuery.cjs.map +1 -1
  8. package/dist/cjs/useLiveInfiniteQuery.d.cts +3 -14
  9. package/dist/cjs/useLiveQuery.cjs +187 -224
  10. package/dist/cjs/useLiveQuery.cjs.map +1 -1
  11. package/dist/cjs/useLiveQuery.d.cts +37 -136
  12. package/dist/cjs/useLiveSuspenseQuery.cjs +71 -64
  13. package/dist/cjs/useLiveSuspenseQuery.cjs.map +1 -1
  14. package/dist/cjs/useLiveSuspenseQuery.d.cts +10 -9
  15. package/dist/cjs/usePacedMutations.cjs.map +1 -1
  16. package/dist/cjs/usePacedMutations.d.cts +1 -1
  17. package/dist/esm/live-query-internals.d.ts +3 -1
  18. package/dist/esm/live-query-internals.js.map +1 -1
  19. package/dist/esm/source-id-bindings.d.ts +6 -17
  20. package/dist/esm/source-id-bindings.js +9 -40
  21. package/dist/esm/source-id-bindings.js.map +1 -1
  22. package/dist/esm/useLiveInfiniteQuery.d.ts +3 -14
  23. package/dist/esm/useLiveInfiniteQuery.js +95 -175
  24. package/dist/esm/useLiveInfiniteQuery.js.map +1 -1
  25. package/dist/esm/useLiveQuery.d.ts +37 -136
  26. package/dist/esm/useLiveQuery.js +189 -226
  27. package/dist/esm/useLiveQuery.js.map +1 -1
  28. package/dist/esm/useLiveSuspenseQuery.d.ts +10 -9
  29. package/dist/esm/useLiveSuspenseQuery.js +71 -64
  30. package/dist/esm/useLiveSuspenseQuery.js.map +1 -1
  31. package/dist/esm/usePacedMutations.d.ts +1 -1
  32. package/dist/esm/usePacedMutations.js.map +1 -1
  33. package/package.json +3 -3
  34. package/skills/react-db/SKILL.md +24 -15
  35. package/src/live-query-internals.ts +3 -0
  36. package/src/source-id-bindings.ts +16 -62
  37. package/src/useLiveInfiniteQuery.ts +138 -296
  38. package/src/useLiveQuery.ts +329 -495
  39. package/src/useLiveSuspenseQuery.ts +122 -132
  40. 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 collection fails (caught by Error boundary)
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
- // Reset promise and ready state when query identity changes
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
- const observerError = queryInfo.observer.getError()
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
- const errorEntry = initialRenderErrors.get(result.collection)
271
- const initialRenderError = queryInfo.client
272
- ? errorEntry?.byClient.get(queryInfo.client)
273
- : errorEntry?.unscoped
274
- // A replacement client query must not inherit the prior request's error.
275
- if (
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
- // Only throw errors during initial load (before first ready)
286
- // After success, errors surface as stale data (matches TanStack Query behavior)
287
- if (
288
- collectionStatus === `error` &&
289
- !hasBeenReadyRef.current &&
290
- (result.persistedStatus === `unavailable` ||
291
- result.persistedStatus === `error`)
292
- ) {
293
- promiseRef.current = null
294
- // TODO: Once collections hold a reference to their last error object (#671),
295
- // we should rethrow that actual error instead of creating a generic message
296
- throw new Error(`Collection "${result.collection.id}" failed to load`)
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
- // Create or reuse promise for current collection
316
- if (!promiseRef.current) {
317
- const collection = result.collection
318
- const client = queryInfo.client
319
- const queryHash = queryInfo.queryHash
320
- let active = true
321
- const stopWatchingCleanup = collection.once(`status:cleaned-up`, () => {
322
- active = false
323
- })
324
- let preload: Promise<void>
325
- try {
326
- preload = queryInfo.observer.preloadForInitialRender()
327
- } catch (error) {
328
- stopWatchingCleanup()
329
- throw error
330
- }
331
- const preloadClientQuery =
332
- client && queryHash ? client._getLiveQuery(queryHash) : undefined
333
- promiseRef.current = preload
334
- .catch((error: unknown) => {
335
- const latestClientQuery =
336
- client && queryHash ? client._getLiveQuery(queryHash) : undefined
337
- if (active && latestClientQuery === preloadClientQuery)
338
- rememberInitialRenderError(
339
- collection,
340
- client,
341
- error,
342
- preloadClientQuery,
343
- )
344
- throw error
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
- .finally(stopWatchingCleanup)
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
- // Return data without status/loading flags (handled by Suspense/ErrorBoundary)
353
- // If error after success, return last known good state (stale data)
354
- return {
355
- state: result.state,
356
- data: result.data,
357
- collection: result.collection,
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
  }
@@ -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.isPersisted.promise
39
+ * await tx.when('settled')
40
40
  * console.log('Saved!')
41
41
  * } catch (error) {
42
42
  * console.error('Save failed:', error)