@tanstack/db 0.4.5 → 0.4.7

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 (89) hide show
  1. package/dist/cjs/collection/change-events.cjs +1 -1
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/change-events.d.cts +1 -1
  4. package/dist/cjs/collection/index.cjs +11 -0
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +8 -1
  7. package/dist/cjs/collection/lifecycle.cjs +4 -1
  8. package/dist/cjs/collection/lifecycle.cjs.map +1 -1
  9. package/dist/cjs/collection/mutations.cjs +4 -4
  10. package/dist/cjs/collection/mutations.cjs.map +1 -1
  11. package/dist/cjs/collection/subscription.cjs +21 -1
  12. package/dist/cjs/collection/subscription.cjs.map +1 -1
  13. package/dist/cjs/collection/subscription.d.cts +4 -3
  14. package/dist/cjs/collection/sync.cjs +94 -71
  15. package/dist/cjs/collection/sync.cjs.map +1 -1
  16. package/dist/cjs/collection/sync.d.cts +9 -1
  17. package/dist/cjs/index.cjs +2 -0
  18. package/dist/cjs/index.cjs.map +1 -1
  19. package/dist/cjs/index.d.cts +2 -0
  20. package/dist/cjs/indexes/auto-index.cjs +4 -1
  21. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  22. package/dist/cjs/local-only.cjs +21 -2
  23. package/dist/cjs/local-only.cjs.map +1 -1
  24. package/dist/cjs/local-only.d.cts +64 -7
  25. package/dist/cjs/local-storage.cjs +71 -3
  26. package/dist/cjs/local-storage.cjs.map +1 -1
  27. package/dist/cjs/local-storage.d.cts +55 -2
  28. package/dist/cjs/query/compiler/expressions.cjs +19 -0
  29. package/dist/cjs/query/compiler/expressions.cjs.map +1 -1
  30. package/dist/cjs/query/compiler/expressions.d.cts +2 -1
  31. package/dist/cjs/query/compiler/order-by.cjs +2 -1
  32. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  33. package/dist/cjs/query/compiler/order-by.d.cts +2 -1
  34. package/dist/cjs/query/live/collection-subscriber.cjs +18 -8
  35. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  36. package/dist/cjs/query/live/collection-subscriber.d.cts +1 -0
  37. package/dist/cjs/types.d.cts +11 -1
  38. package/dist/esm/collection/change-events.d.ts +1 -1
  39. package/dist/esm/collection/change-events.js +1 -1
  40. package/dist/esm/collection/change-events.js.map +1 -1
  41. package/dist/esm/collection/index.d.ts +8 -1
  42. package/dist/esm/collection/index.js +11 -0
  43. package/dist/esm/collection/index.js.map +1 -1
  44. package/dist/esm/collection/lifecycle.js +4 -1
  45. package/dist/esm/collection/lifecycle.js.map +1 -1
  46. package/dist/esm/collection/mutations.js +4 -4
  47. package/dist/esm/collection/mutations.js.map +1 -1
  48. package/dist/esm/collection/subscription.d.ts +4 -3
  49. package/dist/esm/collection/subscription.js +22 -2
  50. package/dist/esm/collection/subscription.js.map +1 -1
  51. package/dist/esm/collection/sync.d.ts +9 -1
  52. package/dist/esm/collection/sync.js +94 -71
  53. package/dist/esm/collection/sync.js.map +1 -1
  54. package/dist/esm/index.d.ts +2 -0
  55. package/dist/esm/index.js +2 -0
  56. package/dist/esm/index.js.map +1 -1
  57. package/dist/esm/indexes/auto-index.js +4 -1
  58. package/dist/esm/indexes/auto-index.js.map +1 -1
  59. package/dist/esm/local-only.d.ts +64 -7
  60. package/dist/esm/local-only.js +21 -2
  61. package/dist/esm/local-only.js.map +1 -1
  62. package/dist/esm/local-storage.d.ts +55 -2
  63. package/dist/esm/local-storage.js +72 -4
  64. package/dist/esm/local-storage.js.map +1 -1
  65. package/dist/esm/query/compiler/expressions.d.ts +2 -1
  66. package/dist/esm/query/compiler/expressions.js +19 -0
  67. package/dist/esm/query/compiler/expressions.js.map +1 -1
  68. package/dist/esm/query/compiler/order-by.d.ts +2 -1
  69. package/dist/esm/query/compiler/order-by.js +2 -1
  70. package/dist/esm/query/compiler/order-by.js.map +1 -1
  71. package/dist/esm/query/live/collection-subscriber.d.ts +1 -0
  72. package/dist/esm/query/live/collection-subscriber.js +19 -9
  73. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  74. package/dist/esm/types.d.ts +11 -1
  75. package/package.json +1 -1
  76. package/src/collection/change-events.ts +5 -2
  77. package/src/collection/index.ts +13 -0
  78. package/src/collection/lifecycle.ts +4 -1
  79. package/src/collection/mutations.ts +8 -4
  80. package/src/collection/subscription.ts +34 -4
  81. package/src/collection/sync.ts +147 -110
  82. package/src/index.ts +5 -0
  83. package/src/indexes/auto-index.ts +4 -1
  84. package/src/local-only.ts +119 -30
  85. package/src/local-storage.ts +170 -5
  86. package/src/query/compiler/expressions.ts +26 -1
  87. package/src/query/compiler/order-by.ts +3 -1
  88. package/src/query/live/collection-subscriber.ts +31 -10
  89. package/src/types.ts +13 -1
@@ -12,6 +12,7 @@ import type {
12
12
  DeleteMutationFnParams,
13
13
  InferSchemaOutput,
14
14
  InsertMutationFnParams,
15
+ PendingMutation,
15
16
  SyncConfig,
16
17
  UpdateMutationFnParams,
17
18
  UtilsRecord,
@@ -90,6 +91,26 @@ export type GetStorageSizeFn = () => number
90
91
  export interface LocalStorageCollectionUtils extends UtilsRecord {
91
92
  clearStorage: ClearStorageFn
92
93
  getStorageSize: GetStorageSizeFn
94
+ /**
95
+ * Accepts mutations from a transaction that belong to this collection and persists them to localStorage.
96
+ * This should be called in your transaction's mutationFn to persist local-storage data.
97
+ *
98
+ * @param transaction - The transaction containing mutations to accept
99
+ * @example
100
+ * const localSettings = createCollection(localStorageCollectionOptions({...}))
101
+ *
102
+ * const tx = createTransaction({
103
+ * mutationFn: async ({ transaction }) => {
104
+ * // Make API call first
105
+ * await api.save(...)
106
+ * // Then persist local-storage mutations after success
107
+ * localSettings.utils.acceptMutations(transaction)
108
+ * }
109
+ * })
110
+ */
111
+ acceptMutations: (transaction: {
112
+ mutations: Array<PendingMutation<Record<string, unknown>>>
113
+ }) => void
93
114
  }
94
115
 
95
116
  /**
@@ -123,11 +144,17 @@ function generateUuid(): string {
123
144
  * This function creates a collection that persists data to localStorage/sessionStorage
124
145
  * and synchronizes changes across browser tabs using storage events.
125
146
  *
147
+ * **Using with Manual Transactions:**
148
+ *
149
+ * For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`
150
+ * to persist changes made during `tx.mutate()`. This is necessary because local-storage collections
151
+ * don't participate in the standard mutation handler flow for manual transactions.
152
+ *
126
153
  * @template TExplicit - The explicit type of items in the collection (highest priority)
127
154
  * @template TSchema - The schema type for validation and type inference (second priority)
128
155
  * @template TFallback - The fallback type if no explicit or schema type is provided
129
156
  * @param config - Configuration options for the localStorage collection
130
- * @returns Collection options with utilities including clearStorage and getStorageSize
157
+ * @returns Collection options with utilities including clearStorage, getStorageSize, and acceptMutations
131
158
  *
132
159
  * @example
133
160
  * // Basic localStorage collection
@@ -159,6 +186,33 @@ function generateUuid(): string {
159
186
  * },
160
187
  * })
161
188
  * )
189
+ *
190
+ * @example
191
+ * // Using with manual transactions
192
+ * const localSettings = createCollection(
193
+ * localStorageCollectionOptions({
194
+ * storageKey: 'user-settings',
195
+ * getKey: (item) => item.id,
196
+ * })
197
+ * )
198
+ *
199
+ * const tx = createTransaction({
200
+ * mutationFn: async ({ transaction }) => {
201
+ * // Use settings data in API call
202
+ * const settingsMutations = transaction.mutations.filter(m => m.collection === localSettings)
203
+ * await api.updateUserProfile({ settings: settingsMutations[0]?.modified })
204
+ *
205
+ * // Persist local-storage mutations after API success
206
+ * localSettings.utils.acceptMutations(transaction)
207
+ * }
208
+ * })
209
+ *
210
+ * tx.mutate(() => {
211
+ * localSettings.insert({ id: 'theme', value: 'dark' })
212
+ * apiCollection.insert({ id: 2, data: 'profile data' })
213
+ * })
214
+ *
215
+ * await tx.commit()
162
216
  */
163
217
 
164
218
  // Overload for when schema is provided
@@ -397,6 +451,76 @@ export function localStorageCollectionOptions(
397
451
  // Default id to a pattern based on storage key if not provided
398
452
  const collectionId = id ?? `local-collection:${config.storageKey}`
399
453
 
454
+ /**
455
+ * Accepts mutations from a transaction that belong to this collection and persists them to storage
456
+ */
457
+ const acceptMutations = (transaction: {
458
+ mutations: Array<PendingMutation<Record<string, unknown>>>
459
+ }) => {
460
+ // Filter mutations that belong to this collection
461
+ // Use collection ID for filtering if collection reference isn't available yet
462
+ const collectionMutations = transaction.mutations.filter((m) => {
463
+ // Try to match by collection reference first
464
+ if (sync.collection && m.collection === sync.collection) {
465
+ return true
466
+ }
467
+ // Fall back to matching by collection ID
468
+ return m.collection.id === collectionId
469
+ })
470
+
471
+ if (collectionMutations.length === 0) {
472
+ return
473
+ }
474
+
475
+ // Validate all mutations can be serialized before modifying storage
476
+ for (const mutation of collectionMutations) {
477
+ switch (mutation.type) {
478
+ case `insert`:
479
+ case `update`:
480
+ validateJsonSerializable(mutation.modified, mutation.type)
481
+ break
482
+ case `delete`:
483
+ validateJsonSerializable(mutation.original, mutation.type)
484
+ break
485
+ }
486
+ }
487
+
488
+ // Load current data from storage
489
+ const currentData = loadFromStorage<Record<string, unknown>>(
490
+ config.storageKey,
491
+ storage
492
+ )
493
+
494
+ // Apply each mutation
495
+ for (const mutation of collectionMutations) {
496
+ // Use the engine's pre-computed key to avoid key derivation issues
497
+ const key = mutation.key
498
+
499
+ switch (mutation.type) {
500
+ case `insert`:
501
+ case `update`: {
502
+ const storedItem: StoredItem<Record<string, unknown>> = {
503
+ versionKey: generateUuid(),
504
+ data: mutation.modified,
505
+ }
506
+ currentData.set(key, storedItem)
507
+ break
508
+ }
509
+ case `delete`: {
510
+ currentData.delete(key)
511
+ break
512
+ }
513
+ }
514
+ }
515
+
516
+ // Save to storage
517
+ saveToStorage(currentData)
518
+
519
+ // Confirm the mutations in the collection to move them from optimistic to synced state
520
+ // This writes them through the sync interface to make them "synced" instead of "optimistic"
521
+ sync.confirmOperationsSync(collectionMutations)
522
+ }
523
+
400
524
  return {
401
525
  ...restConfig,
402
526
  id: collectionId,
@@ -407,6 +531,7 @@ export function localStorageCollectionOptions(
407
531
  utils: {
408
532
  clearStorage,
409
533
  getStorageSize,
534
+ acceptMutations,
410
535
  },
411
536
  }
412
537
  }
@@ -480,8 +605,13 @@ function createLocalStorageSync<T extends object>(
480
605
  storageEventApi: StorageEventApi,
481
606
  _getKey: (item: T) => string | number,
482
607
  lastKnownData: Map<string | number, StoredItem<T>>
483
- ): SyncConfig<T> & { manualTrigger?: () => void } {
608
+ ): SyncConfig<T> & {
609
+ manualTrigger?: () => void
610
+ collection: any
611
+ confirmOperationsSync: (mutations: Array<any>) => void
612
+ } {
484
613
  let syncParams: Parameters<SyncConfig<T>[`sync`]>[0] | null = null
614
+ let collection: any = null
485
615
 
486
616
  /**
487
617
  * Compare two Maps to find differences using version keys
@@ -556,12 +686,16 @@ function createLocalStorageSync<T extends object>(
556
686
  }
557
687
  }
558
688
 
559
- const syncConfig: SyncConfig<T> & { manualTrigger?: () => void } = {
689
+ const syncConfig: SyncConfig<T> & {
690
+ manualTrigger?: () => void
691
+ collection: any
692
+ } = {
560
693
  sync: (params: Parameters<SyncConfig<T>[`sync`]>[0]) => {
561
694
  const { begin, write, commit, markReady } = params
562
695
 
563
- // Store sync params for later use
696
+ // Store sync params and collection for later use
564
697
  syncParams = params
698
+ collection = params.collection
565
699
 
566
700
  // Initial load
567
701
  const initialData = loadFromStorage<T>(storageKey, storage)
@@ -613,7 +747,38 @@ function createLocalStorageSync<T extends object>(
613
747
 
614
748
  // Manual trigger function for local updates
615
749
  manualTrigger: processStorageChanges,
750
+
751
+ // Collection instance reference
752
+ collection,
616
753
  }
617
754
 
618
- return syncConfig
755
+ /**
756
+ * Confirms mutations by writing them through the sync interface
757
+ * This moves mutations from optimistic to synced state
758
+ * @param mutations - Array of mutation objects to confirm
759
+ */
760
+ const confirmOperationsSync = (mutations: Array<any>) => {
761
+ if (!syncParams) {
762
+ // Sync not initialized yet, mutations will be handled on next sync
763
+ return
764
+ }
765
+
766
+ const { begin, write, commit } = syncParams
767
+
768
+ // Write the mutations through sync to confirm them
769
+ begin()
770
+ mutations.forEach((mutation: any) => {
771
+ write({
772
+ type: mutation.type,
773
+ value:
774
+ mutation.type === `delete` ? mutation.original : mutation.modified,
775
+ })
776
+ })
777
+ commit()
778
+ }
779
+
780
+ return {
781
+ ...syncConfig,
782
+ confirmOperationsSync,
783
+ }
619
784
  }
@@ -1,5 +1,5 @@
1
1
  import { Func, PropRef, Value } from "../ir.js"
2
- import type { BasicExpression } from "../ir.js"
2
+ import type { BasicExpression, OrderBy } from "../ir.js"
3
3
 
4
4
  /**
5
5
  * Functions supported by the collection index system.
@@ -90,3 +90,28 @@ export function convertToBasicExpression(
90
90
  return new Func(whereClause.name, args)
91
91
  }
92
92
  }
93
+
94
+ export function convertOrderByToBasicExpression(
95
+ orderBy: OrderBy,
96
+ collectionAlias: string
97
+ ): OrderBy {
98
+ const normalizedOrderBy = orderBy.map((clause) => {
99
+ const basicExp = convertToBasicExpression(
100
+ clause.expression,
101
+ collectionAlias
102
+ )
103
+
104
+ if (!basicExp) {
105
+ throw new Error(
106
+ `Failed to convert orderBy expression to a basic expression: ${clause.expression}`
107
+ )
108
+ }
109
+
110
+ return {
111
+ ...clause,
112
+ expression: basicExp,
113
+ }
114
+ })
115
+
116
+ return normalizedOrderBy
117
+ }
@@ -6,13 +6,14 @@ import { findIndexForField } from "../../utils/index-optimization.js"
6
6
  import { compileExpression } from "./evaluators.js"
7
7
  import { replaceAggregatesByRefs } from "./group-by.js"
8
8
  import type { CompiledSingleRowExpression } from "./evaluators.js"
9
- import type { OrderByClause, QueryIR, Select } from "../ir.js"
9
+ import type { OrderBy, OrderByClause, QueryIR, Select } from "../ir.js"
10
10
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
11
11
  import type { IStreamBuilder, KeyValue } from "@tanstack/db-ivm"
12
12
  import type { IndexInterface } from "../../indexes/base-index.js"
13
13
  import type { Collection } from "../../collection/index.js"
14
14
 
15
15
  export type OrderByOptimizationInfo = {
16
+ orderBy: OrderBy
16
17
  offset: number
17
18
  limit: number
18
19
  comparator: (
@@ -160,6 +161,7 @@ export function processOrderBy(
160
161
  comparator,
161
162
  valueExtractorForRawRow,
162
163
  index,
164
+ orderBy: orderByClause,
163
165
  }
164
166
 
165
167
  optimizableOrderByCollections[followRefCollection.id] =
@@ -1,5 +1,8 @@
1
1
  import { MultiSet } from "@tanstack/db-ivm"
2
- import { convertToBasicExpression } from "../compiler/expressions.js"
2
+ import {
3
+ convertOrderByToBasicExpression,
4
+ convertToBasicExpression,
5
+ } from "../compiler/expressions.js"
3
6
  import type { FullSyncState } from "./types.js"
4
7
  import type { MultiSetArray, RootStreamBuilder } from "@tanstack/db-ivm"
5
8
  import type { Collection } from "../../collection/index.js"
@@ -16,26 +19,29 @@ export class CollectionSubscriber<
16
19
  // Keep track of the biggest value we've sent so far (needed for orderBy optimization)
17
20
  private biggest: any = undefined
18
21
 
22
+ private collectionAlias: string
23
+
19
24
  constructor(
20
25
  private collectionId: string,
21
26
  private collection: Collection,
22
27
  private config: Parameters<SyncConfig<TResult>[`sync`]>[0],
23
28
  private syncState: FullSyncState,
24
29
  private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>
25
- ) {}
26
-
27
- subscribe(): CollectionSubscription {
28
- const collectionAlias = findCollectionAlias(
30
+ ) {
31
+ this.collectionAlias = findCollectionAlias(
29
32
  this.collectionId,
30
33
  this.collectionConfigBuilder.query
31
- )
32
- const whereClause = this.getWhereClauseFromAlias(collectionAlias)
34
+ )!
35
+ }
36
+
37
+ subscribe(): CollectionSubscription {
38
+ const whereClause = this.getWhereClauseFromAlias(this.collectionAlias)
33
39
 
34
40
  if (whereClause) {
35
41
  // Convert WHERE clause to BasicExpression format for collection subscription
36
42
  const whereExpression = convertToBasicExpression(
37
43
  whereClause,
38
- collectionAlias!
44
+ this.collectionAlias
39
45
  )
40
46
 
41
47
  if (whereExpression) {
@@ -128,7 +134,7 @@ export class CollectionSubscriber<
128
134
  private subscribeToOrderedChanges(
129
135
  whereExpression: BasicExpression<boolean> | undefined
130
136
  ) {
131
- const { offset, limit, comparator, dataNeeded, index } =
137
+ const { orderBy, offset, limit, comparator, dataNeeded, index } =
132
138
  this.collectionConfigBuilder.optimizableOrderByCollections[
133
139
  this.collectionId
134
140
  ]!
@@ -164,10 +170,17 @@ export class CollectionSubscriber<
164
170
 
165
171
  subscription.setOrderByIndex(index)
166
172
 
173
+ // Normalize the orderBy clauses such that the references are relative to the collection
174
+ const normalizedOrderBy = convertOrderByToBasicExpression(
175
+ orderBy,
176
+ this.collectionAlias
177
+ )
178
+
167
179
  // Load the first `offset + limit` values from the index
168
180
  // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[
169
181
  subscription.requestLimitedSnapshot({
170
182
  limit: offset + limit,
183
+ orderBy: normalizedOrderBy,
171
184
  })
172
185
 
173
186
  return subscription
@@ -225,7 +238,7 @@ export class CollectionSubscriber<
225
238
  // Loads the next `n` items from the collection
226
239
  // starting from the biggest item it has sent
227
240
  private loadNextItems(n: number, subscription: CollectionSubscription) {
228
- const { valueExtractorForRawRow } =
241
+ const { orderBy, valueExtractorForRawRow } =
229
242
  this.collectionConfigBuilder.optimizableOrderByCollections[
230
243
  this.collectionId
231
244
  ]!
@@ -233,8 +246,16 @@ export class CollectionSubscriber<
233
246
  const biggestSentValue = biggestSentRow
234
247
  ? valueExtractorForRawRow(biggestSentRow)
235
248
  : biggestSentRow
249
+
250
+ // Normalize the orderBy clauses such that the references are relative to the collection
251
+ const normalizedOrderBy = convertOrderByToBasicExpression(
252
+ orderBy,
253
+ this.collectionAlias
254
+ )
255
+
236
256
  // Take the `n` items after the biggest sent value
237
257
  subscription.requestLimitedSnapshot({
258
+ orderBy: normalizedOrderBy,
238
259
  limit: n,
239
260
  minValue: biggestSentValue,
240
261
  })
package/src/types.ts CHANGED
@@ -150,6 +150,18 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>
150
150
 
151
151
  export type OperationType = `insert` | `update` | `delete`
152
152
 
153
+ export type OnLoadMoreOptions = {
154
+ where?: BasicExpression<boolean>
155
+ orderBy?: OrderBy
156
+ limit?: number
157
+ }
158
+
159
+ export type CleanupFn = () => void
160
+
161
+ export type SyncConfigRes = {
162
+ cleanup?: CleanupFn
163
+ onLoadMore?: (options: OnLoadMoreOptions) => void | Promise<void>
164
+ }
153
165
  export interface SyncConfig<
154
166
  T extends object = Record<string, unknown>,
155
167
  TKey extends string | number = string | number,
@@ -161,7 +173,7 @@ export interface SyncConfig<
161
173
  commit: () => void
162
174
  markReady: () => void
163
175
  truncate: () => void
164
- }) => void
176
+ }) => void | CleanupFn | SyncConfigRes
165
177
 
166
178
  /**
167
179
  * Get the sync metadata for insert operations