@tanstack/db 0.4.6 → 0.4.8

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 (60) hide show
  1. package/dist/cjs/collection/index.cjs.map +1 -1
  2. package/dist/cjs/collection/index.d.cts +2 -1
  3. package/dist/cjs/collection/lifecycle.cjs +2 -3
  4. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  5. package/dist/cjs/collection/mutations.cjs +4 -4
  6. package/dist/cjs/collection/mutations.cjs.map +1 -1
  7. package/dist/cjs/collection/state.cjs +22 -33
  8. package/dist/cjs/collection/state.cjs.map +1 -1
  9. package/dist/cjs/collection/state.d.cts +6 -2
  10. package/dist/cjs/collection/sync.cjs +4 -3
  11. package/dist/cjs/collection/sync.cjs.map +1 -1
  12. package/dist/cjs/indexes/auto-index.cjs +0 -3
  13. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  14. package/dist/cjs/local-only.cjs +21 -2
  15. package/dist/cjs/local-only.cjs.map +1 -1
  16. package/dist/cjs/local-only.d.cts +64 -7
  17. package/dist/cjs/local-storage.cjs +71 -3
  18. package/dist/cjs/local-storage.cjs.map +1 -1
  19. package/dist/cjs/local-storage.d.cts +55 -2
  20. package/dist/cjs/query/live/collection-config-builder.cjs +54 -12
  21. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  22. package/dist/cjs/query/live/collection-config-builder.d.cts +17 -2
  23. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  24. package/dist/cjs/types.d.cts +3 -5
  25. package/dist/esm/collection/index.d.ts +2 -1
  26. package/dist/esm/collection/index.js.map +1 -1
  27. package/dist/esm/collection/lifecycle.js +2 -3
  28. package/dist/esm/collection/lifecycle.js.map +1 -1
  29. package/dist/esm/collection/mutations.js +4 -4
  30. package/dist/esm/collection/mutations.js.map +1 -1
  31. package/dist/esm/collection/state.d.ts +6 -2
  32. package/dist/esm/collection/state.js +22 -33
  33. package/dist/esm/collection/state.js.map +1 -1
  34. package/dist/esm/collection/sync.js +4 -3
  35. package/dist/esm/collection/sync.js.map +1 -1
  36. package/dist/esm/indexes/auto-index.js +0 -3
  37. package/dist/esm/indexes/auto-index.js.map +1 -1
  38. package/dist/esm/local-only.d.ts +64 -7
  39. package/dist/esm/local-only.js +21 -2
  40. package/dist/esm/local-only.js.map +1 -1
  41. package/dist/esm/local-storage.d.ts +55 -2
  42. package/dist/esm/local-storage.js +72 -4
  43. package/dist/esm/local-storage.js.map +1 -1
  44. package/dist/esm/query/live/collection-config-builder.d.ts +17 -2
  45. package/dist/esm/query/live/collection-config-builder.js +54 -12
  46. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  47. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  48. package/dist/esm/types.d.ts +3 -5
  49. package/package.json +1 -1
  50. package/src/collection/index.ts +1 -1
  51. package/src/collection/lifecycle.ts +3 -4
  52. package/src/collection/mutations.ts +8 -4
  53. package/src/collection/state.ts +52 -48
  54. package/src/collection/sync.ts +7 -6
  55. package/src/indexes/auto-index.ts +0 -8
  56. package/src/local-only.ts +119 -30
  57. package/src/local-storage.ts +170 -5
  58. package/src/query/live/collection-config-builder.ts +103 -24
  59. package/src/query/live/collection-subscriber.ts +3 -3
  60. package/src/types.ts +3 -5
@@ -21,10 +21,15 @@ import type {
21
21
  LiveQueryCollectionConfig,
22
22
  SyncState,
23
23
  } from "./types.js"
24
+ import type { AllCollectionEvents } from "../../collection/events.js"
24
25
 
25
26
  // Global counter for auto-generated collection IDs
26
27
  let liveQueryCollectionCounter = 0
27
28
 
29
+ type SyncMethods<TResult extends object> = Parameters<
30
+ SyncConfig<TResult>[`sync`]
31
+ >[0]
32
+
28
33
  export class CollectionConfigBuilder<
29
34
  TContext extends Context,
30
35
  TResult extends object = GetResult<TContext>,
@@ -44,6 +49,12 @@ export class CollectionConfigBuilder<
44
49
 
45
50
  private isGraphRunning = false
46
51
 
52
+ // Error state tracking
53
+ private isInErrorState = false
54
+
55
+ // Reference to the live query collection for error state transitions
56
+ private liveQueryCollection?: Collection<TResult, any, any>
57
+
47
58
  private graphCache: D2 | undefined
48
59
  private inputsCache: Record<string, RootStreamBuilder<unknown>> | undefined
49
60
  private pipelineCache: ResultStream | undefined
@@ -101,12 +112,12 @@ export class CollectionConfigBuilder<
101
112
  // This gives the callback a chance to load more data if needed,
102
113
  // that's used to optimize orderBy operators that set a limit,
103
114
  // in order to load some more data if we still don't have enough rows after the pipeline has run.
104
- // That can happend because even though we load N rows, the pipeline might filter some of these rows out
115
+ // That can happen because even though we load N rows, the pipeline might filter some of these rows out
105
116
  // causing the orderBy operator to receive less than N rows or even no rows at all.
106
117
  // So this callback would notice that it doesn't have enough rows and load some more.
107
- // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.
118
+ // The callback returns a boolean, when it's true it's done loading data.
108
119
  maybeRunGraph(
109
- config: Parameters<SyncConfig<TResult>[`sync`]>[0],
120
+ config: SyncMethods<TResult>,
110
121
  syncState: FullSyncState,
111
122
  callback?: () => boolean
112
123
  ) {
@@ -120,13 +131,15 @@ export class CollectionConfigBuilder<
120
131
  this.isGraphRunning = true
121
132
 
122
133
  try {
123
- const { begin, commit, markReady } = config
134
+ const { begin, commit } = config
135
+
136
+ // Don't run if the live query is in an error state
137
+ if (this.isInErrorState) {
138
+ return
139
+ }
124
140
 
125
- // We only run the graph if all the collections are ready
126
- if (
127
- this.allCollectionsReadyOrInitialCommit() &&
128
- syncState.subscribedToAllCollections
129
- ) {
141
+ // Always run the graph if subscribed (eager execution)
142
+ if (syncState.subscribedToAllCollections) {
130
143
  while (syncState.graph.pendingWork()) {
131
144
  syncState.graph.run()
132
145
  callback?.()
@@ -137,10 +150,9 @@ export class CollectionConfigBuilder<
137
150
  if (syncState.messagesCount === 0) {
138
151
  begin()
139
152
  commit()
140
- }
141
- // Mark the collection as ready after the first successful run
142
- if (this.allCollectionsReady()) {
143
- markReady()
153
+ // After initial commit, check if we should mark ready
154
+ // (in case all sources were already ready before we subscribed)
155
+ this.updateLiveQueryStatus(config)
144
156
  }
145
157
  }
146
158
  } finally {
@@ -155,7 +167,10 @@ export class CollectionConfigBuilder<
155
167
  }
156
168
  }
157
169
 
158
- private syncFn(config: Parameters<SyncConfig<TResult>[`sync`]>[0]) {
170
+ private syncFn(config: SyncMethods<TResult>) {
171
+ // Store reference to the live query collection for error state transitions
172
+ this.liveQueryCollection = config.collection
173
+
159
174
  const syncState: SyncState = {
160
175
  messagesCount: 0,
161
176
  subscribedToAllCollections: false,
@@ -233,7 +248,7 @@ export class CollectionConfigBuilder<
233
248
  }
234
249
 
235
250
  private extendPipelineWithChangeProcessing(
236
- config: Parameters<SyncConfig<TResult>[`sync`]>[0],
251
+ config: SyncMethods<TResult>,
237
252
  syncState: SyncState
238
253
  ): FullSyncState {
239
254
  const { begin, commit } = config
@@ -266,7 +281,7 @@ export class CollectionConfigBuilder<
266
281
  }
267
282
 
268
283
  private applyChanges(
269
- config: Parameters<SyncConfig<TResult>[`sync`]>[0],
284
+ config: SyncMethods<TResult>,
270
285
  changes: {
271
286
  deletes: number
272
287
  inserts: number
@@ -317,21 +332,76 @@ export class CollectionConfigBuilder<
317
332
  }
318
333
  }
319
334
 
335
+ /**
336
+ * Handle status changes from source collections
337
+ */
338
+ private handleSourceStatusChange(
339
+ config: SyncMethods<TResult>,
340
+ collectionId: string,
341
+ event: AllCollectionEvents[`status:change`]
342
+ ) {
343
+ const { status } = event
344
+
345
+ // Handle error state - any source collection in error puts live query in error
346
+ if (status === `error`) {
347
+ this.transitionToError(
348
+ `Source collection '${collectionId}' entered error state`
349
+ )
350
+ return
351
+ }
352
+
353
+ // Handle manual cleanup - this should not happen due to GC prevention,
354
+ // but could happen if user manually calls cleanup()
355
+ if (status === `cleaned-up`) {
356
+ this.transitionToError(
357
+ `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. ` +
358
+ `Live queries prevent automatic GC, so this was likely a manual cleanup() call.`
359
+ )
360
+ return
361
+ }
362
+
363
+ // Update ready status based on all source collections
364
+ this.updateLiveQueryStatus(config)
365
+ }
366
+
367
+ /**
368
+ * Update the live query status based on source collection statuses
369
+ */
370
+ private updateLiveQueryStatus(config: SyncMethods<TResult>) {
371
+ const { markReady } = config
372
+
373
+ // Don't update status if already in error
374
+ if (this.isInErrorState) {
375
+ return
376
+ }
377
+
378
+ // Mark ready when all source collections are ready
379
+ if (this.allCollectionsReady()) {
380
+ markReady()
381
+ }
382
+ }
383
+
384
+ /**
385
+ * Transition the live query to error state
386
+ */
387
+ private transitionToError(message: string) {
388
+ this.isInErrorState = true
389
+
390
+ // Log error to console for debugging
391
+ console.error(`[Live Query Error] ${message}`)
392
+
393
+ // Transition live query collection to error state
394
+ this.liveQueryCollection?._lifecycle.setStatus(`error`)
395
+ }
396
+
320
397
  private allCollectionsReady() {
321
398
  return Object.values(this.collections).every((collection) =>
322
399
  collection.isReady()
323
400
  )
324
401
  }
325
402
 
326
- private allCollectionsReadyOrInitialCommit() {
327
- return Object.values(this.collections).every(
328
- (collection) =>
329
- collection.status === `ready` || collection.status === `initialCommit`
330
- )
331
- }
332
-
333
403
  private subscribeToAllCollections(
334
- config: Parameters<SyncConfig<TResult>[`sync`]>[0],
404
+ config: SyncMethods<TResult>,
335
405
  syncState: FullSyncState
336
406
  ) {
337
407
  const loaders = Object.entries(this.collections).map(
@@ -347,6 +417,12 @@ export class CollectionConfigBuilder<
347
417
  const subscription = collectionSubscriber.subscribe()
348
418
  this.subscriptions[collectionId] = subscription
349
419
 
420
+ // Subscribe to status changes for status flow
421
+ const statusUnsubscribe = collection.on(`status:change`, (event) => {
422
+ this.handleSourceStatusChange(config, collectionId, event)
423
+ })
424
+ syncState.unsubscribeCallbacks.add(statusUnsubscribe)
425
+
350
426
  const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(
351
427
  collectionSubscriber,
352
428
  subscription
@@ -364,6 +440,9 @@ export class CollectionConfigBuilder<
364
440
  // Mark the collections as subscribed in the sync state
365
441
  syncState.subscribedToAllCollections = true
366
442
 
443
+ // Initial status check after all subscriptions are set up
444
+ this.updateLiveQueryStatus(config)
445
+
367
446
  return loadMoreDataCallback
368
447
  }
369
448
  }
@@ -103,9 +103,9 @@ export class CollectionSubscriber<
103
103
  // otherwise we end up in an infinite loop trying to load more data
104
104
  const dataLoader = sentChanges > 0 ? callback : undefined
105
105
 
106
- // We need to call `maybeRunGraph` even if there's no data to load
107
- // because we need to mark the collection as ready if it's not already
108
- // and that's only done in `maybeRunGraph`
106
+ // Always call maybeRunGraph to process changes eagerly.
107
+ // The graph will run unless the live query is in an error state.
108
+ // Status management is handled separately via status:change event listeners.
109
109
  this.collectionConfigBuilder.maybeRunGraph(
110
110
  this.config,
111
111
  this.syncState,
package/src/types.ts CHANGED
@@ -298,17 +298,15 @@ export type DeleteMutationFn<
298
298
  *
299
299
  * @example
300
300
  * // Status transitions
301
- * // idle → loading → initialCommit ready
301
+ * // idle → loading → ready (when markReady() is called)
302
302
  * // Any status can transition to → error or cleaned-up
303
303
  */
304
304
  export type CollectionStatus =
305
305
  /** Collection is created but sync hasn't started yet (when startSync config is false) */
306
306
  | `idle`
307
- /** Sync has started but hasn't received the first commit yet */
307
+ /** Sync has started and is loading data */
308
308
  | `loading`
309
- /** Collection is in the process of committing its first transaction */
310
- | `initialCommit`
311
- /** Collection has received at least one commit and is ready for use */
309
+ /** Collection has been explicitly marked ready via markReady() */
312
310
  | `ready`
313
311
  /** An error occurred during sync initialization */
314
312
  | `error`