@tanstack/db 0.0.24 → 0.0.25

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.
@@ -120,6 +120,7 @@ export interface SyncConfig<T extends object = Record<string, unknown>, TKey ext
120
120
  begin: () => void;
121
121
  write: (message: Omit<ChangeMessage<T>, `key`>) => void;
122
122
  commit: () => void;
123
+ markReady: () => void;
123
124
  }) => void;
124
125
  /**
125
126
  * Get the sync metadata for insert operations
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.0.24",
4
+ "version": "0.0.25",
5
5
  "dependencies": {
6
6
  "@electric-sql/d2mini": "^0.1.7",
7
7
  "@standard-schema/spec": "^1.0.0"
package/src/collection.ts CHANGED
@@ -229,8 +229,9 @@ export class CollectionImpl<
229
229
  private hasReceivedFirstCommit = false
230
230
  private isCommittingSyncTransactions = false
231
231
 
232
- // Array to store one-time commit listeners
233
- private onFirstCommitCallbacks: Array<() => void> = []
232
+ // Array to store one-time ready listeners
233
+ private onFirstReadyCallbacks: Array<() => void> = []
234
+ private hasBeenReady = false
234
235
 
235
236
  // Event batching for preventing duplicate emissions during transaction flows
236
237
  private batchedEvents: Array<ChangeMessage<T, TKey>> = []
@@ -244,17 +245,66 @@ export class CollectionImpl<
244
245
  private syncCleanupFn: (() => void) | null = null
245
246
 
246
247
  /**
247
- * Register a callback to be executed on the next commit
248
+ * Register a callback to be executed when the collection first becomes ready
248
249
  * Useful for preloading collections
249
- * @param callback Function to call after the next commit
250
+ * @param callback Function to call when the collection first becomes ready
250
251
  * @example
251
- * collection.onFirstCommit(() => {
252
- * console.log('Collection has received first data')
252
+ * collection.onFirstReady(() => {
253
+ * console.log('Collection is ready for the first time')
253
254
  * // Safe to access collection.state now
254
255
  * })
255
256
  */
256
- public onFirstCommit(callback: () => void): void {
257
- this.onFirstCommitCallbacks.push(callback)
257
+ public onFirstReady(callback: () => void): void {
258
+ // If already ready, call immediately
259
+ if (this.hasBeenReady) {
260
+ callback()
261
+ return
262
+ }
263
+
264
+ this.onFirstReadyCallbacks.push(callback)
265
+ }
266
+
267
+ /**
268
+ * Check if the collection is ready for use
269
+ * Returns true if the collection has been marked as ready by its sync implementation
270
+ * @returns true if the collection is ready, false otherwise
271
+ * @example
272
+ * if (collection.isReady()) {
273
+ * console.log('Collection is ready, data is available')
274
+ * // Safe to access collection.state
275
+ * } else {
276
+ * console.log('Collection is still loading')
277
+ * }
278
+ */
279
+ public isReady(): boolean {
280
+ return this._status === `ready`
281
+ }
282
+
283
+ /**
284
+ * Mark the collection as ready for use
285
+ * This is called by sync implementations to explicitly signal that the collection is ready,
286
+ * providing a more intuitive alternative to using commits for readiness signaling
287
+ * @private - Should only be called by sync implementations
288
+ */
289
+ private markReady(): void {
290
+ // Can transition to ready from loading or initialCommit states
291
+ if (this._status === `loading` || this._status === `initialCommit`) {
292
+ this.setStatus(`ready`)
293
+
294
+ // Call any registered first ready callbacks (only on first time becoming ready)
295
+ if (!this.hasBeenReady) {
296
+ this.hasBeenReady = true
297
+
298
+ // Also mark as having received first commit for backwards compatibility
299
+ if (!this.hasReceivedFirstCommit) {
300
+ this.hasReceivedFirstCommit = true
301
+ }
302
+
303
+ const callbacks = [...this.onFirstReadyCallbacks]
304
+ this.onFirstReadyCallbacks = []
305
+ callbacks.forEach((callback) => callback())
306
+ }
307
+ }
258
308
  }
259
309
 
260
310
  public id = ``
@@ -302,7 +352,7 @@ export class CollectionImpl<
302
352
  Array<CollectionStatus>
303
353
  > = {
304
354
  idle: [`loading`, `error`, `cleaned-up`],
305
- loading: [`initialCommit`, `error`, `cleaned-up`],
355
+ loading: [`initialCommit`, `ready`, `error`, `cleaned-up`],
306
356
  initialCommit: [`ready`, `error`, `cleaned-up`],
307
357
  ready: [`cleaned-up`, `error`],
308
358
  error: [`cleaned-up`, `idle`],
@@ -455,11 +505,9 @@ export class CollectionImpl<
455
505
  }
456
506
 
457
507
  this.commitPendingTransactions()
458
-
459
- // Transition from initialCommit to ready after the first commit is complete
460
- if (this._status === `initialCommit`) {
461
- this.setStatus(`ready`)
462
- }
508
+ },
509
+ markReady: () => {
510
+ this.markReady()
463
511
  },
464
512
  })
465
513
 
@@ -492,7 +540,7 @@ export class CollectionImpl<
492
540
  }
493
541
 
494
542
  // Register callback BEFORE starting sync to avoid race condition
495
- this.onFirstCommit(() => {
543
+ this.onFirstReady(() => {
496
544
  resolve()
497
545
  })
498
546
 
@@ -555,7 +603,8 @@ export class CollectionImpl<
555
603
  this.pendingSyncedTransactions = []
556
604
  this.syncedKeys.clear()
557
605
  this.hasReceivedFirstCommit = false
558
- this.onFirstCommitCallbacks = []
606
+ this.hasBeenReady = false
607
+ this.onFirstReadyCallbacks = []
559
608
  this.preloadPromise = null
560
609
  this.batchedEvents = []
561
610
  this.shouldBatchEvents = false
@@ -1184,8 +1233,8 @@ export class CollectionImpl<
1184
1233
  // Call any registered one-time commit listeners
1185
1234
  if (!this.hasReceivedFirstCommit) {
1186
1235
  this.hasReceivedFirstCommit = true
1187
- const callbacks = [...this.onFirstCommitCallbacks]
1188
- this.onFirstCommitCallbacks = []
1236
+ const callbacks = [...this.onFirstReadyCallbacks]
1237
+ this.onFirstReadyCallbacks = []
1189
1238
  callbacks.forEach((callback) => callback())
1190
1239
  }
1191
1240
  }
@@ -1812,14 +1861,14 @@ export class CollectionImpl<
1812
1861
  * @returns Promise that resolves to a Map containing all items in the collection
1813
1862
  */
1814
1863
  stateWhenReady(): Promise<Map<TKey, T>> {
1815
- // If we already have data or there are no loading collections, resolve immediately
1816
- if (this.size > 0 || this.hasReceivedFirstCommit === true) {
1864
+ // If we already have data or collection is ready, resolve immediately
1865
+ if (this.size > 0 || this.isReady()) {
1817
1866
  return Promise.resolve(this.state)
1818
1867
  }
1819
1868
 
1820
- // Otherwise, wait for the first commit
1869
+ // Otherwise, wait for the collection to be ready
1821
1870
  return new Promise<Map<TKey, T>>((resolve) => {
1822
- this.onFirstCommit(() => {
1871
+ this.onFirstReady(() => {
1823
1872
  resolve(this.state)
1824
1873
  })
1825
1874
  })
@@ -1841,14 +1890,14 @@ export class CollectionImpl<
1841
1890
  * @returns Promise that resolves to an Array containing all items in the collection
1842
1891
  */
1843
1892
  toArrayWhenReady(): Promise<Array<T>> {
1844
- // If we already have data or there are no loading collections, resolve immediately
1845
- if (this.size > 0 || this.hasReceivedFirstCommit === true) {
1893
+ // If we already have data or collection is ready, resolve immediately
1894
+ if (this.size > 0 || this.isReady()) {
1846
1895
  return Promise.resolve(this.toArray)
1847
1896
  }
1848
1897
 
1849
- // Otherwise, wait for the first commit
1898
+ // Otherwise, wait for the collection to be ready
1850
1899
  return new Promise<Array<T>>((resolve) => {
1851
- this.onFirstCommit(() => {
1900
+ this.onFirstReady(() => {
1852
1901
  resolve(this.toArray)
1853
1902
  })
1854
1903
  })
package/src/local-only.ts CHANGED
@@ -240,7 +240,7 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
240
240
  * @returns Unsubscribe function (empty since no ongoing sync is needed)
241
241
  */
242
242
  sync: (params) => {
243
- const { begin, write, commit } = params
243
+ const { begin, write, commit, markReady } = params
244
244
 
245
245
  // Capture sync functions for later use by confirmOperationsSync
246
246
  syncBegin = begin
@@ -259,6 +259,9 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
259
259
  commit()
260
260
  }
261
261
 
262
+ // Mark collection as ready since local-only collections are immediately ready
263
+ markReady()
264
+
262
265
  // Return empty unsubscribe function - no ongoing sync needed
263
266
  return () => {}
264
267
  },
@@ -586,7 +586,7 @@ function createLocalStorageSync<T extends object>(
586
586
 
587
587
  const syncConfig: SyncConfig<T> & { manualTrigger?: () => void } = {
588
588
  sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {
589
- const { begin, write, commit } = params
589
+ const { begin, write, commit, markReady } = params
590
590
 
591
591
  // Store sync params for later use
592
592
  syncParams = params
@@ -608,6 +608,9 @@ function createLocalStorageSync<T extends object>(
608
608
  lastKnownData.set(key, storedItem)
609
609
  })
610
610
 
611
+ // Mark collection as ready after initial load
612
+ markReady()
613
+
611
614
  // Listen for storage events from other tabs
612
615
  const handleStorageEvent = (event: StorageEvent) => {
613
616
  // Only respond to changes to our specific key and from our storage
package/src/proxy.ts CHANGED
@@ -40,10 +40,21 @@ interface ChangeTracker<T extends object> {
40
40
  copy_: T
41
41
  proxyCount: number
42
42
  assigned_: Record<string | symbol, boolean>
43
- parent?: {
44
- tracker: ChangeTracker<Record<string | symbol, unknown>>
45
- prop: string | symbol
46
- }
43
+ parent?:
44
+ | {
45
+ tracker: ChangeTracker<Record<string | symbol, unknown>>
46
+ prop: string | symbol
47
+ }
48
+ | {
49
+ tracker: ChangeTracker<Record<string | symbol, unknown>>
50
+ prop: string | symbol
51
+ updateMap: (newValue: unknown) => void
52
+ }
53
+ | {
54
+ tracker: ChangeTracker<Record<string | symbol, unknown>>
55
+ prop: unknown
56
+ updateSet: (newValue: unknown) => void
57
+ }
47
58
  target: T
48
59
  }
49
60
 
@@ -330,9 +341,18 @@ export function createChangeProxy<
330
341
  if (state.parent) {
331
342
  debugLog(`propagating change to parent`)
332
343
 
333
- // Update parent's copy with this object's current state
334
- state.parent.tracker.copy_[state.parent.prop] = state.copy_
335
- state.parent.tracker.assigned_[state.parent.prop] = true
344
+ // Check if this is a special Map parent with updateMap function
345
+ if (`updateMap` in state.parent) {
346
+ // Use the special updateMap function for Maps
347
+ state.parent.updateMap(state.copy_)
348
+ } else if (`updateSet` in state.parent) {
349
+ // Use the special updateSet function for Sets
350
+ state.parent.updateSet(state.copy_)
351
+ } else {
352
+ // Update parent's copy with this object's current state
353
+ state.parent.tracker.copy_[state.parent.prop] = state.copy_
354
+ state.parent.tracker.assigned_[state.parent.prop] = true
355
+ }
336
356
 
337
357
  // Mark parent as changed
338
358
  markChanged(state.parent.tracker)
@@ -410,7 +430,7 @@ export function createChangeProxy<
410
430
  // Update parent status based on child changes
411
431
  function checkParentStatus(
412
432
  parentState: ChangeTracker<Record<string | symbol, unknown>>,
413
- childProp: string | symbol
433
+ childProp: string | symbol | unknown
414
434
  ) {
415
435
  debugLog(`checkParentStatus called for child prop:`, childProp)
416
436
 
@@ -565,6 +585,28 @@ export function createChangeProxy<
565
585
  // to track changes when the values are accessed and potentially modified
566
586
  const originalIterator = result
567
587
 
588
+ // For values() iterator on Maps, we need to create a value-to-key mapping
589
+ const valueToKeyMap = new Map()
590
+ if (methodName === `values` && ptarget instanceof Map) {
591
+ // Build a mapping from value to key for reverse lookup
592
+ // Use the copy_ (which is the current state) to build the mapping
593
+ for (const [
594
+ key,
595
+ mapValue,
596
+ ] of changeTracker.copy_.entries()) {
597
+ valueToKeyMap.set(mapValue, key)
598
+ }
599
+ }
600
+
601
+ // For Set iterators, we need to create an original-to-modified mapping
602
+ const originalToModifiedMap = new Map()
603
+ if (ptarget instanceof Set) {
604
+ // Initialize with original values
605
+ for (const setValue of changeTracker.copy_.values()) {
606
+ originalToModifiedMap.set(setValue, setValue)
607
+ }
608
+ }
609
+
568
610
  // Create a proxy for the iterator that will mark changes when next() is called
569
611
  return {
570
612
  next() {
@@ -587,15 +629,25 @@ export function createChangeProxy<
587
629
  nextResult.value[1] &&
588
630
  typeof nextResult.value[1] === `object`
589
631
  ) {
632
+ const mapKey = nextResult.value[0]
633
+ // Create a special parent tracker that knows how to update the Map
634
+ const mapParent = {
635
+ tracker: changeTracker,
636
+ prop: mapKey,
637
+ updateMap: (newValue: unknown) => {
638
+ // Update the Map in the copy
639
+ if (changeTracker.copy_ instanceof Map) {
640
+ changeTracker.copy_.set(mapKey, newValue)
641
+ }
642
+ },
643
+ }
644
+
590
645
  // Create a proxy for the value and replace it in the result
591
646
  const { proxy: valueProxy } =
592
- memoizedCreateChangeProxy(nextResult.value[1], {
593
- tracker: changeTracker,
594
- prop:
595
- typeof nextResult.value[0] === `symbol`
596
- ? nextResult.value[0]
597
- : String(nextResult.value[0]),
598
- })
647
+ memoizedCreateChangeProxy(
648
+ nextResult.value[1],
649
+ mapParent
650
+ )
599
651
  nextResult.value[1] = valueProxy
600
652
  }
601
653
  } else if (
@@ -608,16 +660,68 @@ export function createChangeProxy<
608
660
  typeof nextResult.value === `object` &&
609
661
  nextResult.value !== null
610
662
  ) {
611
- // For Set, we need to track the whole object
612
- // For Map, we would need the key, but we don't have it here
613
- // So we'll use a symbol as a placeholder
614
- const tempKey = Symbol(`iterator-value`)
615
- const { proxy: valueProxy } =
616
- memoizedCreateChangeProxy(nextResult.value, {
663
+ // For Map values(), try to find the key using our mapping
664
+ if (
665
+ methodName === `values` &&
666
+ ptarget instanceof Map
667
+ ) {
668
+ const mapKey = valueToKeyMap.get(nextResult.value)
669
+ if (mapKey !== undefined) {
670
+ // Create a special parent tracker for this Map value
671
+ const mapParent = {
672
+ tracker: changeTracker,
673
+ prop: mapKey,
674
+ updateMap: (newValue: unknown) => {
675
+ // Update the Map in the copy
676
+ if (changeTracker.copy_ instanceof Map) {
677
+ changeTracker.copy_.set(mapKey, newValue)
678
+ }
679
+ },
680
+ }
681
+
682
+ const { proxy: valueProxy } =
683
+ memoizedCreateChangeProxy(
684
+ nextResult.value,
685
+ mapParent
686
+ )
687
+ nextResult.value = valueProxy
688
+ }
689
+ } else if (ptarget instanceof Set) {
690
+ // For Set, we need to track modifications and update the Set accordingly
691
+ const setOriginalValue = nextResult.value
692
+ const setParent = {
617
693
  tracker: changeTracker,
618
- prop: tempKey,
619
- })
620
- nextResult.value = valueProxy
694
+ prop: setOriginalValue, // Use the original value as the prop
695
+ updateSet: (newValue: unknown) => {
696
+ // Update the Set in the copy by removing old value and adding new one
697
+ if (changeTracker.copy_ instanceof Set) {
698
+ changeTracker.copy_.delete(setOriginalValue)
699
+ changeTracker.copy_.add(newValue)
700
+ // Update our mapping for future iterations
701
+ originalToModifiedMap.set(
702
+ setOriginalValue,
703
+ newValue
704
+ )
705
+ }
706
+ },
707
+ }
708
+
709
+ const { proxy: valueProxy } =
710
+ memoizedCreateChangeProxy(
711
+ nextResult.value,
712
+ setParent
713
+ )
714
+ nextResult.value = valueProxy
715
+ } else {
716
+ // For other cases, use a symbol as a placeholder
717
+ const tempKey = Symbol(`iterator-value`)
718
+ const { proxy: valueProxy } =
719
+ memoizedCreateChangeProxy(nextResult.value, {
720
+ tracker: changeTracker,
721
+ prop: tempKey,
722
+ })
723
+ nextResult.value = valueProxy
724
+ }
621
725
  }
622
726
  }
623
727
  }
@@ -203,7 +203,7 @@ export function liveQueryCollectionOptions<
203
203
  // Create the sync configuration
204
204
  const sync: SyncConfig<TResult> = {
205
205
  rowUpdateMode: `full`,
206
- sync: ({ begin, write, commit, collection: theCollection }) => {
206
+ sync: ({ begin, write, commit, markReady, collection: theCollection }) => {
207
207
  const { graph, inputs, pipeline } = maybeCompileBasePipeline()
208
208
  let messagesCount = 0
209
209
  pipeline.pipe(
@@ -295,6 +295,8 @@ export function liveQueryCollectionOptions<
295
295
  begin()
296
296
  commit()
297
297
  }
298
+ // Mark the collection as ready after the first successful run
299
+ markReady()
298
300
  }
299
301
  }
300
302
 
package/src/types.ts CHANGED
@@ -203,6 +203,7 @@ export interface SyncConfig<
203
203
  begin: () => void
204
204
  write: (message: Omit<ChangeMessage<T>, `key`>) => void
205
205
  commit: () => void
206
+ markReady: () => void
206
207
  }) => void
207
208
 
208
209
  /**