@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
@@ -9,7 +9,13 @@ import {
9
9
  } from "../errors"
10
10
  import { deepEquals } from "../utils"
11
11
  import type { StandardSchemaV1 } from "@standard-schema/spec"
12
- import type { ChangeMessage, CollectionConfig } from "../types"
12
+ import type {
13
+ ChangeMessage,
14
+ CleanupFn,
15
+ CollectionConfig,
16
+ OnLoadMoreOptions,
17
+ SyncConfigRes,
18
+ } from "../types"
13
19
  import type { CollectionImpl } from "./index.js"
14
20
  import type { CollectionStateManager } from "./state"
15
21
  import type { CollectionLifecycleManager } from "./lifecycle"
@@ -28,6 +34,9 @@ export class CollectionSyncManager<
28
34
 
29
35
  public preloadPromise: Promise<void> | null = null
30
36
  public syncCleanupFn: (() => void) | null = null
37
+ public syncOnLoadMoreFn:
38
+ | ((options: OnLoadMoreOptions) => void | Promise<void>)
39
+ | null = null
31
40
 
32
41
  /**
33
42
  * Creates a new CollectionSyncManager instance
@@ -52,7 +61,6 @@ export class CollectionSyncManager<
52
61
  * This is called when the collection is first accessed or preloaded
53
62
  */
54
63
  public startSync(): void {
55
- const state = this.state
56
64
  if (
57
65
  this.lifecycle.status !== `idle` &&
58
66
  this.lifecycle.status !== `cleaned-up`
@@ -63,120 +71,125 @@ export class CollectionSyncManager<
63
71
  this.lifecycle.setStatus(`loading`)
64
72
 
65
73
  try {
66
- const cleanupFn = this.config.sync.sync({
67
- collection: this.collection,
68
- begin: () => {
69
- state.pendingSyncedTransactions.push({
70
- committed: false,
71
- operations: [],
72
- deletedKeys: new Set(),
73
- })
74
- },
75
- write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {
76
- const pendingTransaction =
77
- state.pendingSyncedTransactions[
78
- state.pendingSyncedTransactions.length - 1
79
- ]
80
- if (!pendingTransaction) {
81
- throw new NoPendingSyncTransactionWriteError()
82
- }
83
- if (pendingTransaction.committed) {
84
- throw new SyncTransactionAlreadyCommittedWriteError()
85
- }
86
- const key = this.config.getKey(messageWithoutKey.value)
87
-
88
- let messageType = messageWithoutKey.type
89
-
90
- // Check if an item with this key already exists when inserting
91
- if (messageWithoutKey.type === `insert`) {
92
- const insertingIntoExistingSynced = state.syncedData.has(key)
93
- const hasPendingDeleteForKey =
94
- pendingTransaction.deletedKeys.has(key)
95
- const isTruncateTransaction = pendingTransaction.truncate === true
96
- // Allow insert after truncate in the same transaction even if it existed in syncedData
97
- if (
98
- insertingIntoExistingSynced &&
99
- !hasPendingDeleteForKey &&
100
- !isTruncateTransaction
101
- ) {
102
- const existingValue = state.syncedData.get(key)
74
+ const syncRes = normalizeSyncFnResult(
75
+ this.config.sync.sync({
76
+ collection: this.collection,
77
+ begin: () => {
78
+ this.state.pendingSyncedTransactions.push({
79
+ committed: false,
80
+ operations: [],
81
+ deletedKeys: new Set(),
82
+ })
83
+ },
84
+ write: (messageWithoutKey: Omit<ChangeMessage<TOutput>, `key`>) => {
85
+ const pendingTransaction =
86
+ this.state.pendingSyncedTransactions[
87
+ this.state.pendingSyncedTransactions.length - 1
88
+ ]
89
+ if (!pendingTransaction) {
90
+ throw new NoPendingSyncTransactionWriteError()
91
+ }
92
+ if (pendingTransaction.committed) {
93
+ throw new SyncTransactionAlreadyCommittedWriteError()
94
+ }
95
+ const key = this.config.getKey(messageWithoutKey.value)
96
+
97
+ let messageType = messageWithoutKey.type
98
+
99
+ // Check if an item with this key already exists when inserting
100
+ if (messageWithoutKey.type === `insert`) {
101
+ const insertingIntoExistingSynced = this.state.syncedData.has(key)
102
+ const hasPendingDeleteForKey =
103
+ pendingTransaction.deletedKeys.has(key)
104
+ const isTruncateTransaction = pendingTransaction.truncate === true
105
+ // Allow insert after truncate in the same transaction even if it existed in syncedData
103
106
  if (
104
- existingValue !== undefined &&
105
- deepEquals(existingValue, messageWithoutKey.value)
107
+ insertingIntoExistingSynced &&
108
+ !hasPendingDeleteForKey &&
109
+ !isTruncateTransaction
106
110
  ) {
107
- // The "insert" is an echo of a value we already have locally.
108
- // Treat it as an update so we preserve optimistic intent without
109
- // throwing a duplicate-key error during reconciliation.
110
- messageType = `update`
111
- } else {
112
- throw new DuplicateKeySyncError(key, this.id)
111
+ const existingValue = this.state.syncedData.get(key)
112
+ if (
113
+ existingValue !== undefined &&
114
+ deepEquals(existingValue, messageWithoutKey.value)
115
+ ) {
116
+ // The "insert" is an echo of a value we already have locally.
117
+ // Treat it as an update so we preserve optimistic intent without
118
+ // throwing a duplicate-key error during reconciliation.
119
+ messageType = `update`
120
+ } else {
121
+ throw new DuplicateKeySyncError(key, this.id)
122
+ }
113
123
  }
114
124
  }
115
- }
116
-
117
- const message: ChangeMessage<TOutput> = {
118
- ...messageWithoutKey,
119
- type: messageType,
120
- key,
121
- }
122
- pendingTransaction.operations.push(message)
123
-
124
- if (messageType === `delete`) {
125
- pendingTransaction.deletedKeys.add(key)
126
- }
127
- },
128
- commit: () => {
129
- const pendingTransaction =
130
- state.pendingSyncedTransactions[
131
- state.pendingSyncedTransactions.length - 1
132
- ]
133
- if (!pendingTransaction) {
134
- throw new NoPendingSyncTransactionCommitError()
135
- }
136
- if (pendingTransaction.committed) {
137
- throw new SyncTransactionAlreadyCommittedError()
138
- }
139
-
140
- pendingTransaction.committed = true
141
-
142
- // Update status to initialCommit when transitioning from loading
143
- // This indicates we're in the process of committing the first transaction
144
- if (this.lifecycle.status === `loading`) {
145
- this.lifecycle.setStatus(`initialCommit`)
146
- }
147
-
148
- state.commitPendingTransactions()
149
- },
150
- markReady: () => {
151
- this.lifecycle.markReady()
152
- },
153
- truncate: () => {
154
- const pendingTransaction =
155
- state.pendingSyncedTransactions[
156
- state.pendingSyncedTransactions.length - 1
157
- ]
158
- if (!pendingTransaction) {
159
- throw new NoPendingSyncTransactionWriteError()
160
- }
161
- if (pendingTransaction.committed) {
162
- throw new SyncTransactionAlreadyCommittedWriteError()
163
- }
164
-
165
- // Clear all operations from the current transaction
166
- pendingTransaction.operations = []
167
- pendingTransaction.deletedKeys.clear()
168
-
169
- // Mark the transaction as a truncate operation. During commit, this triggers:
170
- // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
171
- // - Clearing of syncedData/syncedMetadata
172
- // - Subsequent synced ops applied on the fresh base
173
- // - Finally, optimistic mutations re-applied on top (single batch)
174
- pendingTransaction.truncate = true
175
- },
176
- })
125
+
126
+ const message: ChangeMessage<TOutput> = {
127
+ ...messageWithoutKey,
128
+ type: messageType,
129
+ key,
130
+ }
131
+ pendingTransaction.operations.push(message)
132
+
133
+ if (messageType === `delete`) {
134
+ pendingTransaction.deletedKeys.add(key)
135
+ }
136
+ },
137
+ commit: () => {
138
+ const pendingTransaction =
139
+ this.state.pendingSyncedTransactions[
140
+ this.state.pendingSyncedTransactions.length - 1
141
+ ]
142
+ if (!pendingTransaction) {
143
+ throw new NoPendingSyncTransactionCommitError()
144
+ }
145
+ if (pendingTransaction.committed) {
146
+ throw new SyncTransactionAlreadyCommittedError()
147
+ }
148
+
149
+ pendingTransaction.committed = true
150
+
151
+ // Update status to initialCommit when transitioning from loading
152
+ // This indicates we're in the process of committing the first transaction
153
+ if (this.lifecycle.status === `loading`) {
154
+ this.lifecycle.setStatus(`initialCommit`)
155
+ }
156
+
157
+ this.state.commitPendingTransactions()
158
+ },
159
+ markReady: () => {
160
+ this.lifecycle.markReady()
161
+ },
162
+ truncate: () => {
163
+ const pendingTransaction =
164
+ this.state.pendingSyncedTransactions[
165
+ this.state.pendingSyncedTransactions.length - 1
166
+ ]
167
+ if (!pendingTransaction) {
168
+ throw new NoPendingSyncTransactionWriteError()
169
+ }
170
+ if (pendingTransaction.committed) {
171
+ throw new SyncTransactionAlreadyCommittedWriteError()
172
+ }
173
+
174
+ // Clear all operations from the current transaction
175
+ pendingTransaction.operations = []
176
+ pendingTransaction.deletedKeys.clear()
177
+
178
+ // Mark the transaction as a truncate operation. During commit, this triggers:
179
+ // - Delete events for all previously synced keys (excluding optimistic-deleted keys)
180
+ // - Clearing of syncedData/syncedMetadata
181
+ // - Subsequent synced ops applied on the fresh base
182
+ // - Finally, optimistic mutations re-applied on top (single batch)
183
+ pendingTransaction.truncate = true
184
+ },
185
+ })
186
+ )
177
187
 
178
188
  // Store cleanup function if provided
179
- this.syncCleanupFn = typeof cleanupFn === `function` ? cleanupFn : null
189
+ this.syncCleanupFn = syncRes?.cleanup ?? null
190
+
191
+ // Store onLoadMore function if provided
192
+ this.syncOnLoadMoreFn = syncRes?.onLoadMore ?? null
180
193
  } catch (error) {
181
194
  this.lifecycle.setStatus(`error`)
182
195
  throw error
@@ -225,6 +238,18 @@ export class CollectionSyncManager<
225
238
  return this.preloadPromise
226
239
  }
227
240
 
241
+ /**
242
+ * Requests the sync layer to load more data.
243
+ * @param options Options to control what data is being loaded
244
+ * @returns If data loading is asynchronous, this method returns a promise that resolves when the data is loaded.
245
+ * If data loading is synchronous, the data is loaded when the method returns.
246
+ */
247
+ public syncMore(options: OnLoadMoreOptions): void | Promise<void> {
248
+ if (this.syncOnLoadMoreFn) {
249
+ return this.syncOnLoadMoreFn(options)
250
+ }
251
+ }
252
+
228
253
  public cleanup(): void {
229
254
  try {
230
255
  if (this.syncCleanupFn) {
@@ -248,3 +273,15 @@ export class CollectionSyncManager<
248
273
  this.preloadPromise = null
249
274
  }
250
275
  }
276
+
277
+ function normalizeSyncFnResult(result: void | CleanupFn | SyncConfigRes) {
278
+ if (typeof result === `function`) {
279
+ return { cleanup: result }
280
+ }
281
+
282
+ if (typeof result === `object`) {
283
+ return result
284
+ }
285
+
286
+ return undefined
287
+ }
package/src/index.ts CHANGED
@@ -1,4 +1,8 @@
1
1
  // Re-export all public APIs
2
+ // Re-export IR types under their own namespace
3
+ // because custom collections need access to the IR types
4
+ import * as IR from "./query/ir.js"
5
+
2
6
  export * from "./collection/index.js"
3
7
  export * from "./SortedMap"
4
8
  export * from "./transactions"
@@ -18,3 +22,4 @@ export { type IndexOptions } from "./indexes/index-options.js"
18
22
 
19
23
  // Re-export some stuff explicitly to ensure the type & value is exported
20
24
  export type { Collection } from "./collection/index.js"
25
+ export { IR }
@@ -58,7 +58,10 @@ export function ensureIndexForField<
58
58
  options: compareFn ? { compareFn, compareOptions } : {},
59
59
  })
60
60
  } catch (error) {
61
- console.warn(`Failed to create auto-index for field "${fieldName}":`, error)
61
+ console.warn(
62
+ `${collection.id ? `[${collection.id}] ` : ``}Failed to create auto-index for field "${fieldName}":`,
63
+ error
64
+ )
62
65
  }
63
66
  }
64
67
 
package/src/local-only.ts CHANGED
@@ -1,14 +1,19 @@
1
1
  import type {
2
2
  BaseCollectionConfig,
3
3
  CollectionConfig,
4
+ DeleteMutationFn,
4
5
  DeleteMutationFnParams,
5
6
  InferSchemaOutput,
7
+ InsertMutationFn,
6
8
  InsertMutationFnParams,
7
9
  OperationType,
10
+ PendingMutation,
8
11
  SyncConfig,
12
+ UpdateMutationFn,
9
13
  UpdateMutationFnParams,
10
14
  UtilsRecord,
11
15
  } from "./types"
16
+ import type { Collection } from "./collection/index"
12
17
  import type { StandardSchemaV1 } from "@standard-schema/spec"
13
18
 
14
19
  /**
@@ -33,9 +38,44 @@ export interface LocalOnlyCollectionConfig<
33
38
  }
34
39
 
35
40
  /**
36
- * Local-only collection utilities type (currently empty but matches the pattern)
41
+ * Local-only collection utilities type
37
42
  */
38
- export interface LocalOnlyCollectionUtils extends UtilsRecord {}
43
+ export interface LocalOnlyCollectionUtils extends UtilsRecord {
44
+ /**
45
+ * Accepts mutations from a transaction that belong to this collection and persists them.
46
+ * This should be called in your transaction's mutationFn to persist local-only data.
47
+ *
48
+ * @param transaction - The transaction containing mutations to accept
49
+ * @example
50
+ * const localData = createCollection(localOnlyCollectionOptions({...}))
51
+ *
52
+ * const tx = createTransaction({
53
+ * mutationFn: async ({ transaction }) => {
54
+ * // Make API call first
55
+ * await api.save(...)
56
+ * // Then persist local-only mutations after success
57
+ * localData.utils.acceptMutations(transaction)
58
+ * }
59
+ * })
60
+ */
61
+ acceptMutations: (transaction: {
62
+ mutations: Array<PendingMutation<Record<string, unknown>>>
63
+ }) => void
64
+ }
65
+
66
+ type LocalOnlyCollectionOptionsResult<
67
+ T extends object,
68
+ TKey extends string | number,
69
+ TSchema extends StandardSchemaV1 | never = never,
70
+ > = Omit<
71
+ CollectionConfig<T, TKey, TSchema>,
72
+ `onInsert` | `onUpdate` | `onDelete`
73
+ > & {
74
+ onInsert?: InsertMutationFn<T, TKey, LocalOnlyCollectionUtils>
75
+ onUpdate?: UpdateMutationFn<T, TKey, LocalOnlyCollectionUtils>
76
+ onDelete?: DeleteMutationFn<T, TKey, LocalOnlyCollectionUtils>
77
+ utils: LocalOnlyCollectionUtils
78
+ }
39
79
 
40
80
  /**
41
81
  * Creates Local-only collection options for use with a standard Collection
@@ -44,10 +84,16 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
44
84
  * that immediately "syncs" all optimistic changes to the collection, making them permanent.
45
85
  * Perfect for local-only data that doesn't need persistence or external synchronization.
46
86
  *
87
+ * **Using with Manual Transactions:**
88
+ *
89
+ * For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`
90
+ * to persist changes made during `tx.mutate()`. This is necessary because local-only collections
91
+ * don't participate in the standard mutation handler flow for manual transactions.
92
+ *
47
93
  * @template T - The schema type if a schema is provided, otherwise the type of items in the collection
48
94
  * @template TKey - The type of the key returned by getKey
49
95
  * @param config - Configuration options for the Local-only collection
50
- * @returns Collection options with utilities (currently empty but follows the pattern)
96
+ * @returns Collection options with utilities including acceptMutations
51
97
  *
52
98
  * @example
53
99
  * // Basic local-only collection
@@ -80,6 +126,32 @@ export interface LocalOnlyCollectionUtils extends UtilsRecord {}
80
126
  * },
81
127
  * })
82
128
  * )
129
+ *
130
+ * @example
131
+ * // Using with manual transactions
132
+ * const localData = createCollection(
133
+ * localOnlyCollectionOptions({
134
+ * getKey: (item) => item.id,
135
+ * })
136
+ * )
137
+ *
138
+ * const tx = createTransaction({
139
+ * mutationFn: async ({ transaction }) => {
140
+ * // Use local data in API call
141
+ * const localMutations = transaction.mutations.filter(m => m.collection === localData)
142
+ * await api.save({ metadata: localMutations[0]?.modified })
143
+ *
144
+ * // Persist local-only mutations after API success
145
+ * localData.utils.acceptMutations(transaction)
146
+ * }
147
+ * })
148
+ *
149
+ * tx.mutate(() => {
150
+ * localData.insert({ id: 1, data: 'metadata' })
151
+ * apiCollection.insert({ id: 2, data: 'main data' })
152
+ * })
153
+ *
154
+ * await tx.commit()
83
155
  */
84
156
 
85
157
  // Overload for when schema is provided
@@ -90,8 +162,7 @@ export function localOnlyCollectionOptions<
90
162
  config: LocalOnlyCollectionConfig<InferSchemaOutput<T>, T, TKey> & {
91
163
  schema: T
92
164
  }
93
- ): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
94
- utils: LocalOnlyCollectionUtils
165
+ ): LocalOnlyCollectionOptionsResult<InferSchemaOutput<T>, TKey, T> & {
95
166
  schema: T
96
167
  }
97
168
 
@@ -104,15 +175,17 @@ export function localOnlyCollectionOptions<
104
175
  config: LocalOnlyCollectionConfig<T, never, TKey> & {
105
176
  schema?: never // prohibit schema
106
177
  }
107
- ): CollectionConfig<T, TKey> & {
108
- utils: LocalOnlyCollectionUtils
178
+ ): LocalOnlyCollectionOptionsResult<T, TKey> & {
109
179
  schema?: never // no schema in the result
110
180
  }
111
181
 
112
- export function localOnlyCollectionOptions(
113
- config: LocalOnlyCollectionConfig<any, any, string | number>
114
- ): CollectionConfig<any, string | number, any> & {
115
- utils: LocalOnlyCollectionUtils
182
+ export function localOnlyCollectionOptions<
183
+ T extends object = object,
184
+ TSchema extends StandardSchemaV1 = never,
185
+ TKey extends string | number = string | number,
186
+ >(
187
+ config: LocalOnlyCollectionConfig<T, TSchema, TKey>
188
+ ): LocalOnlyCollectionOptionsResult<T, TKey, TSchema> & {
116
189
  schema?: StandardSchemaV1
117
190
  } {
118
191
  const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config
@@ -125,11 +198,7 @@ export function localOnlyCollectionOptions(
125
198
  * Wraps the user's onInsert handler to also confirm the transaction immediately
126
199
  */
127
200
  const wrappedOnInsert = async (
128
- params: InsertMutationFnParams<
129
- any,
130
- string | number,
131
- LocalOnlyCollectionUtils
132
- >
201
+ params: InsertMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
133
202
  ) => {
134
203
  // Call user handler first if provided
135
204
  let handlerResult
@@ -147,11 +216,7 @@ export function localOnlyCollectionOptions(
147
216
  * Wrapper for onUpdate handler that also confirms the transaction immediately
148
217
  */
149
218
  const wrappedOnUpdate = async (
150
- params: UpdateMutationFnParams<
151
- any,
152
- string | number,
153
- LocalOnlyCollectionUtils
154
- >
219
+ params: UpdateMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
155
220
  ) => {
156
221
  // Call user handler first if provided
157
222
  let handlerResult
@@ -169,11 +234,7 @@ export function localOnlyCollectionOptions(
169
234
  * Wrapper for onDelete handler that also confirms the transaction immediately
170
235
  */
171
236
  const wrappedOnDelete = async (
172
- params: DeleteMutationFnParams<
173
- any,
174
- string | number,
175
- LocalOnlyCollectionUtils
176
- >
237
+ params: DeleteMutationFnParams<T, TKey, LocalOnlyCollectionUtils>
177
238
  ) => {
178
239
  // Call user handler first if provided
179
240
  let handlerResult
@@ -187,13 +248,38 @@ export function localOnlyCollectionOptions(
187
248
  return handlerResult
188
249
  }
189
250
 
251
+ /**
252
+ * Accepts mutations from a transaction that belong to this collection and persists them
253
+ */
254
+ const acceptMutations = (transaction: {
255
+ mutations: Array<PendingMutation<Record<string, unknown>>>
256
+ }) => {
257
+ // Filter mutations that belong to this collection
258
+ const collectionMutations = transaction.mutations.filter(
259
+ (m) =>
260
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
261
+ m.collection === syncResult.collection
262
+ )
263
+
264
+ if (collectionMutations.length === 0) {
265
+ return
266
+ }
267
+
268
+ // Persist the mutations through sync
269
+ syncResult.confirmOperationsSync(
270
+ collectionMutations as Array<PendingMutation<T>>
271
+ )
272
+ }
273
+
190
274
  return {
191
275
  ...restConfig,
192
276
  sync: syncResult.sync,
193
277
  onInsert: wrappedOnInsert,
194
278
  onUpdate: wrappedOnUpdate,
195
279
  onDelete: wrappedOnDelete,
196
- utils: {} as LocalOnlyCollectionUtils,
280
+ utils: {
281
+ acceptMutations,
282
+ } as LocalOnlyCollectionUtils,
197
283
  startSync: true,
198
284
  gcTime: 0,
199
285
  }
@@ -212,11 +298,12 @@ export function localOnlyCollectionOptions(
212
298
  function createLocalOnlySync<T extends object, TKey extends string | number>(
213
299
  initialData?: Array<T>
214
300
  ) {
215
- // Capture sync functions for transaction confirmation
301
+ // Capture sync functions and collection for transaction confirmation
216
302
  let syncBegin: (() => void) | null = null
217
303
  let syncWrite: ((message: { type: OperationType; value: T }) => void) | null =
218
304
  null
219
305
  let syncCommit: (() => void) | null = null
306
+ let collection: Collection<T, TKey, LocalOnlyCollectionUtils> | null = null
220
307
 
221
308
  const sync: SyncConfig<T, TKey> = {
222
309
  /**
@@ -227,10 +314,11 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
227
314
  sync: (params) => {
228
315
  const { begin, write, commit, markReady } = params
229
316
 
230
- // Capture sync functions for later use by confirmOperationsSync
317
+ // Capture sync functions and collection for later use
231
318
  syncBegin = begin
232
319
  syncWrite = write
233
320
  syncCommit = commit
321
+ collection = params.collection
234
322
 
235
323
  // Apply initial data if provided
236
324
  if (initialData && initialData.length > 0) {
@@ -265,7 +353,7 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
265
353
  *
266
354
  * @param mutations - Array of mutation objects from the transaction
267
355
  */
268
- const confirmOperationsSync = (mutations: Array<any>) => {
356
+ const confirmOperationsSync = (mutations: Array<PendingMutation<T>>) => {
269
357
  if (!syncBegin || !syncWrite || !syncCommit) {
270
358
  return // Sync not initialized yet, which is fine
271
359
  }
@@ -286,5 +374,6 @@ function createLocalOnlySync<T extends object, TKey extends string | number>(
286
374
  return {
287
375
  sync,
288
376
  confirmOperationsSync,
377
+ collection,
289
378
  }
290
379
  }