@tanstack/vue-db 0.0.128 → 0.1.0

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.
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  computed,
3
3
  getCurrentInstance,
4
- nextTick,
5
4
  onUnmounted,
6
5
  reactive,
7
6
  ref,
@@ -10,6 +9,7 @@ import {
10
9
  } from 'vue'
11
10
  import {
12
11
  createLiveQueryCollection,
12
+ createLiveQueryObserver,
13
13
  isCollection,
14
14
  isSingleResultCollection,
15
15
  } from '@tanstack/db'
@@ -22,6 +22,7 @@ import type {
22
22
  InferResultType,
23
23
  InitialQueryBuilder,
24
24
  LiveQueryCollectionConfig,
25
+ LiveQueryObserver,
25
26
  NonSingleResult,
26
27
  QueryBuilder,
27
28
  SingleResult,
@@ -363,101 +364,80 @@ export function useLiveQuery(
363
364
  internalData.push(...Array.from(currentCollection.values()))
364
365
  }
365
366
 
366
- // Track current unsubscribe function
367
- let currentUnsubscribe: (() => void) | null = null
367
+ // The shared observer owns subscription, the ready-race, and status; Vue
368
+ // materializes into its own reactive map (granular) + ordered array.
369
+ let currentObserver: LiveQueryObserver<any, any> | null = null
370
+
371
+ const syncFromObserver = (
372
+ observer: LiveQueryObserver<any, any>,
373
+ currentCollection: Collection<any, any, any>,
374
+ ) => {
375
+ status.value = observer.getSnapshot().status as CollectionStatus
376
+ syncDataFromCollection(currentCollection)
377
+ }
368
378
 
369
379
  // Watch for collection changes and subscribe to updates
370
380
  watchEffect((onInvalidate) => {
371
381
  const currentCollection = collection.value
372
382
 
383
+ // Tear down any previous observer.
384
+ currentObserver?.dispose()
385
+ currentObserver = null
386
+
373
387
  // Handle null collection (disabled query)
374
388
  if (!currentCollection) {
375
389
  status.value = `disabled` as const
376
390
  state.clear()
377
391
  internalData.length = 0
378
- if (currentUnsubscribe) {
379
- currentUnsubscribe()
380
- currentUnsubscribe = null
381
- }
382
392
  return
383
393
  }
384
394
 
385
- // Update status ref whenever the effect runs
386
- status.value = currentCollection.status
387
-
388
- // Clean up previous subscription
389
- if (currentUnsubscribe) {
390
- currentUnsubscribe()
391
- }
395
+ const observer = createLiveQueryObserver(currentCollection)
396
+ currentObserver = observer
392
397
 
393
- // Initialize state with current collection data
398
+ // Initial rows arrive as the observer's first delta (includeInitialState);
399
+ // apply them and every subsequent delta granularly to the reactive map.
394
400
  state.clear()
395
- for (const [key, value] of currentCollection.entries()) {
396
- state.set(key, value)
397
- }
398
-
399
- // Initialize data array in correct order
400
- syncDataFromCollection(currentCollection)
401
401
 
402
- // Listen for the first ready event to catch status transitions
403
- // that might not trigger change events (fixes async status transition bug)
404
- currentCollection.onFirstReady(() => {
405
- // Use nextTick to ensure Vue reactivity updates properly
406
- nextTick(() => {
407
- status.value = currentCollection.status
408
- })
409
- })
410
-
411
- // Subscribe to collection changes with granular updates
412
- const subscription = currentCollection.subscribeChanges(
413
- (changes: Array<ChangeMessage<any>>) => {
414
- // Apply each change individually to the reactive state
415
- for (const change of changes) {
416
- switch (change.type) {
417
- case `insert`:
418
- case `update`:
419
- state.set(change.key, change.value)
420
- break
421
- case `delete`:
422
- state.delete(change.key)
423
- break
402
+ const unsubscribe = observer.subscribe(
403
+ (changes: Array<ChangeMessage<any>> | undefined) => {
404
+ if (changes) {
405
+ for (const change of changes) {
406
+ switch (change.type) {
407
+ case `insert`:
408
+ case `update`:
409
+ state.set(change.key, change.value)
410
+ break
411
+ case `delete`:
412
+ state.delete(change.key)
413
+ break
414
+ }
415
+ }
416
+ } else {
417
+ // Cleanup and other status-only publications carry no row deltas.
418
+ // Rebuild the keyed view so it cannot diverge from ordered data.
419
+ state.clear()
420
+ for (const [key, value] of observer.getSnapshot().state ?? []) {
421
+ state.set(key, value)
424
422
  }
425
423
  }
426
-
427
- // Update the data array to maintain sorted order
428
- syncDataFromCollection(currentCollection)
429
- // Update status ref on every change
430
- status.value = currentCollection.status
431
- },
432
- {
433
- includeInitialState: true,
424
+ syncFromObserver(observer, currentCollection)
434
425
  },
435
426
  )
436
-
437
- currentUnsubscribe = subscription.unsubscribe.bind(subscription)
438
-
439
- // Preload collection data if not already started
440
- if (currentCollection.status === `idle`) {
441
- currentCollection.preload().catch(console.error)
442
- }
427
+ syncFromObserver(observer, currentCollection)
443
428
 
444
429
  // Cleanup when effect is invalidated
445
430
  onInvalidate(() => {
446
- if (currentUnsubscribe) {
447
- currentUnsubscribe()
448
- currentUnsubscribe = null
449
- }
431
+ unsubscribe()
432
+ observer.dispose()
433
+ currentObserver = null
450
434
  })
451
435
  })
452
436
 
453
437
  // Cleanup on unmount (only if we're in a component context)
454
438
  const instance = getCurrentInstance()
455
439
  if (instance) {
456
- onUnmounted(() => {
457
- if (currentUnsubscribe) {
458
- currentUnsubscribe()
459
- }
460
- })
440
+ onUnmounted(() => currentObserver?.dispose())
461
441
  }
462
442
 
463
443
  return {