@tanstack/db 0.3.2 → 0.4.1

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 (171) hide show
  1. package/dist/cjs/{change-events.cjs → collection/change-events.cjs} +13 -42
  2. package/dist/cjs/collection/change-events.cjs.map +1 -0
  3. package/dist/{esm/change-events.d.ts → cjs/collection/change-events.d.cts} +6 -6
  4. package/dist/cjs/collection/changes.cjs +108 -0
  5. package/dist/cjs/collection/changes.cjs.map +1 -0
  6. package/dist/cjs/collection/changes.d.cts +53 -0
  7. package/dist/cjs/{collection-events.cjs → collection/events.cjs} +7 -5
  8. package/dist/cjs/collection/events.cjs.map +1 -0
  9. package/dist/cjs/{collection-events.d.cts → collection/events.d.cts} +7 -4
  10. package/dist/cjs/collection/index.cjs +417 -0
  11. package/dist/cjs/collection/index.cjs.map +1 -0
  12. package/dist/{esm/collection.d.ts → cjs/collection/index.d.cts} +46 -184
  13. package/dist/cjs/collection/indexes.cjs +124 -0
  14. package/dist/cjs/collection/indexes.cjs.map +1 -0
  15. package/dist/cjs/collection/indexes.d.cts +47 -0
  16. package/dist/cjs/collection/lifecycle.cjs +196 -0
  17. package/dist/cjs/collection/lifecycle.cjs.map +1 -0
  18. package/dist/cjs/collection/lifecycle.d.cts +81 -0
  19. package/dist/cjs/collection/mutations.cjs +315 -0
  20. package/dist/cjs/collection/mutations.cjs.map +1 -0
  21. package/dist/cjs/collection/mutations.d.cts +33 -0
  22. package/dist/cjs/collection/state.cjs +597 -0
  23. package/dist/cjs/collection/state.cjs.map +1 -0
  24. package/dist/cjs/collection/state.d.cts +122 -0
  25. package/dist/cjs/collection/subscription.cjs +160 -0
  26. package/dist/cjs/collection/subscription.cjs.map +1 -0
  27. package/dist/cjs/collection/subscription.d.cts +57 -0
  28. package/dist/cjs/collection/sync.cjs +154 -0
  29. package/dist/cjs/collection/sync.cjs.map +1 -0
  30. package/dist/cjs/collection/sync.d.cts +34 -0
  31. package/dist/cjs/index.cjs +8 -8
  32. package/dist/cjs/index.d.cts +2 -2
  33. package/dist/cjs/indexes/auto-index.cjs.map +1 -1
  34. package/dist/cjs/indexes/auto-index.d.cts +1 -1
  35. package/dist/cjs/indexes/base-index.cjs.map +1 -1
  36. package/dist/cjs/indexes/base-index.d.cts +2 -2
  37. package/dist/cjs/indexes/btree-index.cjs +2 -2
  38. package/dist/cjs/indexes/btree-index.cjs.map +1 -1
  39. package/dist/cjs/indexes/btree-index.d.cts +1 -1
  40. package/dist/cjs/query/builder/index.cjs +2 -2
  41. package/dist/cjs/query/builder/index.cjs.map +1 -1
  42. package/dist/cjs/query/builder/types.d.cts +1 -1
  43. package/dist/cjs/query/compiler/index.cjs +5 -2
  44. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  45. package/dist/cjs/query/compiler/index.d.cts +3 -2
  46. package/dist/cjs/query/compiler/joins.cjs +22 -24
  47. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  48. package/dist/cjs/query/compiler/joins.d.cts +3 -2
  49. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  50. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  51. package/dist/cjs/query/ir.cjs.map +1 -1
  52. package/dist/cjs/query/ir.d.cts +1 -1
  53. package/dist/cjs/query/live/collection-config-builder.cjs +29 -12
  54. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  55. package/dist/cjs/query/live/collection-config-builder.d.cts +3 -0
  56. package/dist/cjs/query/live/collection-subscriber.cjs +43 -104
  57. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  58. package/dist/cjs/query/live/collection-subscriber.d.cts +4 -7
  59. package/dist/cjs/query/live-query-collection.cjs +2 -2
  60. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  61. package/dist/cjs/query/live-query-collection.d.cts +1 -1
  62. package/dist/cjs/transactions.cjs +3 -3
  63. package/dist/cjs/transactions.cjs.map +1 -1
  64. package/dist/cjs/types.d.cts +12 -10
  65. package/dist/cjs/utils/browser-polyfills.cjs +22 -0
  66. package/dist/cjs/utils/browser-polyfills.cjs.map +1 -0
  67. package/dist/cjs/utils/browser-polyfills.d.cts +9 -0
  68. package/dist/{cjs/change-events.d.cts → esm/collection/change-events.d.ts} +6 -6
  69. package/dist/esm/{change-events.js → collection/change-events.js} +13 -42
  70. package/dist/esm/collection/change-events.js.map +1 -0
  71. package/dist/esm/collection/changes.d.ts +53 -0
  72. package/dist/esm/collection/changes.js +108 -0
  73. package/dist/esm/collection/changes.js.map +1 -0
  74. package/dist/esm/{collection-events.d.ts → collection/events.d.ts} +7 -4
  75. package/dist/esm/{collection-events.js → collection/events.js} +7 -5
  76. package/dist/esm/collection/events.js.map +1 -0
  77. package/dist/{cjs/collection.d.cts → esm/collection/index.d.ts} +46 -184
  78. package/dist/esm/collection/index.js +417 -0
  79. package/dist/esm/collection/index.js.map +1 -0
  80. package/dist/esm/collection/indexes.d.ts +47 -0
  81. package/dist/esm/collection/indexes.js +124 -0
  82. package/dist/esm/collection/indexes.js.map +1 -0
  83. package/dist/esm/collection/lifecycle.d.ts +81 -0
  84. package/dist/esm/collection/lifecycle.js +196 -0
  85. package/dist/esm/collection/lifecycle.js.map +1 -0
  86. package/dist/esm/collection/mutations.d.ts +33 -0
  87. package/dist/esm/collection/mutations.js +315 -0
  88. package/dist/esm/collection/mutations.js.map +1 -0
  89. package/dist/esm/collection/state.d.ts +122 -0
  90. package/dist/esm/collection/state.js +597 -0
  91. package/dist/esm/collection/state.js.map +1 -0
  92. package/dist/esm/collection/subscription.d.ts +57 -0
  93. package/dist/esm/collection/subscription.js +160 -0
  94. package/dist/esm/collection/subscription.js.map +1 -0
  95. package/dist/esm/collection/sync.d.ts +34 -0
  96. package/dist/esm/collection/sync.js +154 -0
  97. package/dist/esm/collection/sync.js.map +1 -0
  98. package/dist/esm/index.d.ts +2 -2
  99. package/dist/esm/index.js +1 -1
  100. package/dist/esm/indexes/auto-index.d.ts +1 -1
  101. package/dist/esm/indexes/auto-index.js.map +1 -1
  102. package/dist/esm/indexes/base-index.d.ts +2 -2
  103. package/dist/esm/indexes/base-index.js.map +1 -1
  104. package/dist/esm/indexes/btree-index.d.ts +1 -1
  105. package/dist/esm/indexes/btree-index.js +2 -2
  106. package/dist/esm/indexes/btree-index.js.map +1 -1
  107. package/dist/esm/proxy.js +1 -1
  108. package/dist/esm/query/builder/index.js +1 -1
  109. package/dist/esm/query/builder/index.js.map +1 -1
  110. package/dist/esm/query/builder/types.d.ts +1 -1
  111. package/dist/esm/query/compiler/index.d.ts +3 -2
  112. package/dist/esm/query/compiler/index.js +5 -2
  113. package/dist/esm/query/compiler/index.js.map +1 -1
  114. package/dist/esm/query/compiler/joins.d.ts +3 -2
  115. package/dist/esm/query/compiler/joins.js +22 -24
  116. package/dist/esm/query/compiler/joins.js.map +1 -1
  117. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  118. package/dist/esm/query/compiler/order-by.js.map +1 -1
  119. package/dist/esm/query/ir.d.ts +1 -1
  120. package/dist/esm/query/ir.js.map +1 -1
  121. package/dist/esm/query/live/collection-config-builder.d.ts +3 -0
  122. package/dist/esm/query/live/collection-config-builder.js +29 -12
  123. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  124. package/dist/esm/query/live/collection-subscriber.d.ts +4 -7
  125. package/dist/esm/query/live/collection-subscriber.js +43 -104
  126. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  127. package/dist/esm/query/live-query-collection.d.ts +1 -1
  128. package/dist/esm/query/live-query-collection.js +1 -1
  129. package/dist/esm/query/live-query-collection.js.map +1 -1
  130. package/dist/esm/transactions.js +3 -3
  131. package/dist/esm/transactions.js.map +1 -1
  132. package/dist/esm/types.d.ts +12 -10
  133. package/dist/esm/utils/browser-polyfills.d.ts +9 -0
  134. package/dist/esm/utils/browser-polyfills.js +22 -0
  135. package/dist/esm/utils/browser-polyfills.js.map +1 -0
  136. package/package.json +2 -2
  137. package/src/{change-events.ts → collection/change-events.ts} +25 -39
  138. package/src/collection/changes.ts +163 -0
  139. package/src/{collection-events.ts → collection/events.ts} +8 -6
  140. package/src/collection/index.ts +808 -0
  141. package/src/collection/indexes.ts +172 -0
  142. package/src/collection/lifecycle.ts +289 -0
  143. package/src/collection/mutations.ts +535 -0
  144. package/src/collection/state.ts +866 -0
  145. package/src/collection/subscription.ts +239 -0
  146. package/src/collection/sync.ts +235 -0
  147. package/src/index.ts +2 -2
  148. package/src/indexes/auto-index.ts +1 -1
  149. package/src/indexes/base-index.ts +3 -3
  150. package/src/indexes/btree-index.ts +2 -2
  151. package/src/query/builder/index.ts +1 -1
  152. package/src/query/builder/types.ts +1 -1
  153. package/src/query/compiler/index.ts +7 -1
  154. package/src/query/compiler/joins.ts +28 -41
  155. package/src/query/compiler/order-by.ts +1 -1
  156. package/src/query/ir.ts +1 -1
  157. package/src/query/live/collection-config-builder.ts +48 -22
  158. package/src/query/live/collection-subscriber.ts +63 -168
  159. package/src/query/live-query-collection.ts +2 -2
  160. package/src/transactions.ts +3 -3
  161. package/src/types.ts +14 -15
  162. package/src/utils/browser-polyfills.ts +39 -0
  163. package/dist/cjs/change-events.cjs.map +0 -1
  164. package/dist/cjs/collection-events.cjs.map +0 -1
  165. package/dist/cjs/collection.cjs +0 -1625
  166. package/dist/cjs/collection.cjs.map +0 -1
  167. package/dist/esm/change-events.js.map +0 -1
  168. package/dist/esm/collection-events.js.map +0 -1
  169. package/dist/esm/collection.js +0 -1625
  170. package/dist/esm/collection.js.map +0 -1
  171. package/src/collection.ts +0 -2564
@@ -0,0 +1,535 @@
1
+ import { withArrayChangeTracking, withChangeTracking } from "../proxy"
2
+ import { createTransaction, getActiveTransaction } from "../transactions"
3
+ import {
4
+ DeleteKeyNotFoundError,
5
+ DuplicateKeyError,
6
+ InvalidSchemaError,
7
+ KeyUpdateNotAllowedError,
8
+ MissingDeleteHandlerError,
9
+ MissingInsertHandlerError,
10
+ MissingUpdateArgumentError,
11
+ MissingUpdateHandlerError,
12
+ NoKeysPassedToDeleteError,
13
+ NoKeysPassedToUpdateError,
14
+ SchemaMustBeSynchronousError,
15
+ SchemaValidationError,
16
+ UndefinedKeyError,
17
+ UpdateKeyNotFoundError,
18
+ } from "../errors"
19
+ import type { Collection, CollectionImpl } from "./index.js"
20
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
21
+ import type {
22
+ CollectionConfig,
23
+ InsertConfig,
24
+ OperationConfig,
25
+ PendingMutation,
26
+ StandardSchema,
27
+ Transaction as TransactionType,
28
+ TransactionWithMutations,
29
+ UtilsRecord,
30
+ WritableDeep,
31
+ } from "../types"
32
+ import type { CollectionLifecycleManager } from "./lifecycle"
33
+ import type { CollectionStateManager } from "./state"
34
+
35
+ export class CollectionMutationsManager<
36
+ TOutput extends object = Record<string, unknown>,
37
+ TKey extends string | number = string | number,
38
+ TUtils extends UtilsRecord = {},
39
+ TSchema extends StandardSchemaV1 = StandardSchemaV1,
40
+ TInput extends object = TOutput,
41
+ > {
42
+ private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
43
+ private state!: CollectionStateManager<TOutput, TKey, TSchema, TInput>
44
+ private collection!: CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>
45
+ private config!: CollectionConfig<TOutput, TKey, TSchema>
46
+ private id: string
47
+
48
+ constructor(config: CollectionConfig<TOutput, TKey, TSchema>, id: string) {
49
+ this.id = id
50
+ this.config = config
51
+ }
52
+
53
+ setDeps(deps: {
54
+ lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>
55
+ state: CollectionStateManager<TOutput, TKey, TSchema, TInput>
56
+ collection: CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>
57
+ }) {
58
+ this.lifecycle = deps.lifecycle
59
+ this.state = deps.state
60
+ this.collection = deps.collection
61
+ }
62
+
63
+ private ensureStandardSchema(schema: unknown): StandardSchema<TOutput> {
64
+ // If the schema already implements the standard-schema interface, return it
65
+ if (schema && `~standard` in (schema as {})) {
66
+ return schema as StandardSchema<TOutput>
67
+ }
68
+
69
+ throw new InvalidSchemaError()
70
+ }
71
+
72
+ public validateData(
73
+ data: unknown,
74
+ type: `insert` | `update`,
75
+ key?: TKey
76
+ ): TOutput | never {
77
+ if (!this.config.schema) return data as TOutput
78
+
79
+ const standardSchema = this.ensureStandardSchema(this.config.schema)
80
+
81
+ // For updates, we need to merge with the existing data before validation
82
+ if (type === `update` && key) {
83
+ // Get the existing data for this key
84
+ const existingData = this.state.get(key)
85
+
86
+ if (
87
+ existingData &&
88
+ data &&
89
+ typeof data === `object` &&
90
+ typeof existingData === `object`
91
+ ) {
92
+ // Merge the update with the existing data
93
+ const mergedData = Object.assign({}, existingData, data)
94
+
95
+ // Validate the merged data
96
+ const result = standardSchema[`~standard`].validate(mergedData)
97
+
98
+ // Ensure validation is synchronous
99
+ if (result instanceof Promise) {
100
+ throw new SchemaMustBeSynchronousError()
101
+ }
102
+
103
+ // If validation fails, throw a SchemaValidationError with the issues
104
+ if (`issues` in result && result.issues) {
105
+ const typedIssues = result.issues.map((issue) => ({
106
+ message: issue.message,
107
+ path: issue.path?.map((p) => String(p)),
108
+ }))
109
+ throw new SchemaValidationError(type, typedIssues)
110
+ }
111
+
112
+ // Extract only the modified keys from the validated result
113
+ const validatedMergedData = result.value as TOutput
114
+ const modifiedKeys = Object.keys(data)
115
+ const extractedChanges = Object.fromEntries(
116
+ modifiedKeys.map((k) => [k, validatedMergedData[k as keyof TOutput]])
117
+ ) as TOutput
118
+
119
+ return extractedChanges
120
+ }
121
+ }
122
+
123
+ // For inserts or updates without existing data, validate the data directly
124
+ const result = standardSchema[`~standard`].validate(data)
125
+
126
+ // Ensure validation is synchronous
127
+ if (result instanceof Promise) {
128
+ throw new SchemaMustBeSynchronousError()
129
+ }
130
+
131
+ // If validation fails, throw a SchemaValidationError with the issues
132
+ if (`issues` in result && result.issues) {
133
+ const typedIssues = result.issues.map((issue) => ({
134
+ message: issue.message,
135
+ path: issue.path?.map((p) => String(p)),
136
+ }))
137
+ throw new SchemaValidationError(type, typedIssues)
138
+ }
139
+
140
+ return result.value as TOutput
141
+ }
142
+
143
+ public generateGlobalKey(key: any, item: any): string {
144
+ if (typeof key === `undefined`) {
145
+ throw new UndefinedKeyError(item)
146
+ }
147
+
148
+ return `KEY::${this.id}/${key}`
149
+ }
150
+
151
+ /**
152
+ * Inserts one or more items into the collection
153
+ */
154
+ insert = (data: TInput | Array<TInput>, config?: InsertConfig) => {
155
+ this.lifecycle.validateCollectionUsable(`insert`)
156
+ const state = this.state
157
+ const ambientTransaction = getActiveTransaction()
158
+
159
+ // If no ambient transaction exists, check for an onInsert handler early
160
+ if (!ambientTransaction && !this.config.onInsert) {
161
+ throw new MissingInsertHandlerError()
162
+ }
163
+
164
+ const items = Array.isArray(data) ? data : [data]
165
+ const mutations: Array<PendingMutation<TOutput>> = []
166
+
167
+ // Create mutations for each item
168
+ items.forEach((item) => {
169
+ // Validate the data against the schema if one exists
170
+ const validatedData = this.validateData(item, `insert`)
171
+
172
+ // Check if an item with this ID already exists in the collection
173
+ const key = this.config.getKey(validatedData)
174
+ if (this.state.has(key)) {
175
+ throw new DuplicateKeyError(key)
176
+ }
177
+ const globalKey = this.generateGlobalKey(key, item)
178
+
179
+ const mutation: PendingMutation<TOutput, `insert`> = {
180
+ mutationId: crypto.randomUUID(),
181
+ original: {},
182
+ modified: validatedData,
183
+ // Pick the values from validatedData based on what's passed in - this is for cases
184
+ // where a schema has default values. The validated data has the extra default
185
+ // values but for changes, we just want to show the data that was actually passed in.
186
+ changes: Object.fromEntries(
187
+ Object.keys(item).map((k) => [
188
+ k,
189
+ validatedData[k as keyof typeof validatedData],
190
+ ])
191
+ ) as TInput,
192
+ globalKey,
193
+ key,
194
+ metadata: config?.metadata as unknown,
195
+ syncMetadata: this.config.sync.getSyncMetadata?.() || {},
196
+ optimistic: config?.optimistic ?? true,
197
+ type: `insert`,
198
+ createdAt: new Date(),
199
+ updatedAt: new Date(),
200
+ collection: this.collection,
201
+ }
202
+
203
+ mutations.push(mutation)
204
+ })
205
+
206
+ // If an ambient transaction exists, use it
207
+ if (ambientTransaction) {
208
+ ambientTransaction.applyMutations(mutations)
209
+
210
+ state.transactions.set(ambientTransaction.id, ambientTransaction)
211
+ state.scheduleTransactionCleanup(ambientTransaction)
212
+ state.recomputeOptimisticState(true)
213
+
214
+ return ambientTransaction
215
+ } else {
216
+ // Create a new transaction with a mutation function that calls the onInsert handler
217
+ const directOpTransaction = createTransaction<TOutput>({
218
+ mutationFn: async (params) => {
219
+ // Call the onInsert handler with the transaction and collection
220
+ return await this.config.onInsert!({
221
+ transaction:
222
+ params.transaction as unknown as TransactionWithMutations<
223
+ TOutput,
224
+ `insert`
225
+ >,
226
+ collection: this.collection as unknown as Collection<TOutput, TKey>,
227
+ })
228
+ },
229
+ })
230
+
231
+ // Apply mutations to the new transaction
232
+ directOpTransaction.applyMutations(mutations)
233
+ directOpTransaction.commit()
234
+
235
+ // Add the transaction to the collection's transactions store
236
+ state.transactions.set(directOpTransaction.id, directOpTransaction)
237
+ state.scheduleTransactionCleanup(directOpTransaction)
238
+ state.recomputeOptimisticState(true)
239
+
240
+ return directOpTransaction
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Updates one or more items in the collection using a callback function
246
+ */
247
+ update(
248
+ keys: (TKey | unknown) | Array<TKey | unknown>,
249
+ configOrCallback:
250
+ | ((draft: WritableDeep<TInput>) => void)
251
+ | ((drafts: Array<WritableDeep<TInput>>) => void)
252
+ | OperationConfig,
253
+ maybeCallback?:
254
+ | ((draft: WritableDeep<TInput>) => void)
255
+ | ((drafts: Array<WritableDeep<TInput>>) => void)
256
+ ) {
257
+ if (typeof keys === `undefined`) {
258
+ throw new MissingUpdateArgumentError()
259
+ }
260
+
261
+ const state = this.state
262
+ this.lifecycle.validateCollectionUsable(`update`)
263
+
264
+ const ambientTransaction = getActiveTransaction()
265
+
266
+ // If no ambient transaction exists, check for an onUpdate handler early
267
+ if (!ambientTransaction && !this.config.onUpdate) {
268
+ throw new MissingUpdateHandlerError()
269
+ }
270
+
271
+ const isArray = Array.isArray(keys)
272
+ const keysArray = isArray ? keys : [keys]
273
+
274
+ if (isArray && keysArray.length === 0) {
275
+ throw new NoKeysPassedToUpdateError()
276
+ }
277
+
278
+ const callback =
279
+ typeof configOrCallback === `function` ? configOrCallback : maybeCallback!
280
+ const config =
281
+ typeof configOrCallback === `function` ? {} : configOrCallback
282
+
283
+ // Get the current objects or empty objects if they don't exist
284
+ const currentObjects = keysArray.map((key) => {
285
+ const item = this.state.get(key)
286
+ if (!item) {
287
+ throw new UpdateKeyNotFoundError(key)
288
+ }
289
+
290
+ return item
291
+ }) as unknown as Array<TInput>
292
+
293
+ let changesArray
294
+ if (isArray) {
295
+ // Use the proxy to track changes for all objects
296
+ changesArray = withArrayChangeTracking(
297
+ currentObjects,
298
+ callback as (draft: Array<TInput>) => void
299
+ )
300
+ } else {
301
+ const result = withChangeTracking(
302
+ currentObjects[0]!,
303
+ callback as (draft: TInput) => void
304
+ )
305
+ changesArray = [result]
306
+ }
307
+
308
+ // Create mutations for each object that has changes
309
+ const mutations: Array<
310
+ PendingMutation<
311
+ TOutput,
312
+ `update`,
313
+ CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>
314
+ >
315
+ > = keysArray
316
+ .map((key, index) => {
317
+ const itemChanges = changesArray[index] // User-provided changes for this specific item
318
+
319
+ // Skip items with no changes
320
+ if (!itemChanges || Object.keys(itemChanges).length === 0) {
321
+ return null
322
+ }
323
+
324
+ const originalItem = currentObjects[index] as unknown as TOutput
325
+ // Validate the user-provided changes for this item
326
+ const validatedUpdatePayload = this.validateData(
327
+ itemChanges,
328
+ `update`,
329
+ key
330
+ )
331
+
332
+ // Construct the full modified item by applying the validated update payload to the original item
333
+ const modifiedItem = Object.assign(
334
+ {},
335
+ originalItem,
336
+ validatedUpdatePayload
337
+ )
338
+
339
+ // Check if the ID of the item is being changed
340
+ const originalItemId = this.config.getKey(originalItem)
341
+ const modifiedItemId = this.config.getKey(modifiedItem)
342
+
343
+ if (originalItemId !== modifiedItemId) {
344
+ throw new KeyUpdateNotAllowedError(originalItemId, modifiedItemId)
345
+ }
346
+
347
+ const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem)
348
+
349
+ return {
350
+ mutationId: crypto.randomUUID(),
351
+ original: originalItem,
352
+ modified: modifiedItem,
353
+ // Pick the values from modifiedItem based on what's passed in - this is for cases
354
+ // where a schema has default values or transforms. The modified data has the extra
355
+ // default or transformed values but for changes, we just want to show the data that
356
+ // was actually passed in.
357
+ changes: Object.fromEntries(
358
+ Object.keys(itemChanges).map((k) => [
359
+ k,
360
+ modifiedItem[k as keyof typeof modifiedItem],
361
+ ])
362
+ ) as TInput,
363
+ globalKey,
364
+ key,
365
+ metadata: config.metadata as unknown,
366
+ syncMetadata: (state.syncedMetadata.get(key) || {}) as Record<
367
+ string,
368
+ unknown
369
+ >,
370
+ optimistic: config.optimistic ?? true,
371
+ type: `update`,
372
+ createdAt: new Date(),
373
+ updatedAt: new Date(),
374
+ collection: this.collection,
375
+ }
376
+ })
377
+ .filter(Boolean) as Array<
378
+ PendingMutation<
379
+ TOutput,
380
+ `update`,
381
+ CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>
382
+ >
383
+ >
384
+
385
+ // If no changes were made, return an empty transaction early
386
+ if (mutations.length === 0) {
387
+ const emptyTransaction = createTransaction({
388
+ mutationFn: async () => {},
389
+ })
390
+ emptyTransaction.commit()
391
+ // Schedule cleanup for empty transaction
392
+ state.scheduleTransactionCleanup(emptyTransaction)
393
+ return emptyTransaction
394
+ }
395
+
396
+ // If an ambient transaction exists, use it
397
+ if (ambientTransaction) {
398
+ ambientTransaction.applyMutations(mutations)
399
+
400
+ state.transactions.set(ambientTransaction.id, ambientTransaction)
401
+ state.scheduleTransactionCleanup(ambientTransaction)
402
+ state.recomputeOptimisticState(true)
403
+
404
+ return ambientTransaction
405
+ }
406
+
407
+ // No need to check for onUpdate handler here as we've already checked at the beginning
408
+
409
+ // Create a new transaction with a mutation function that calls the onUpdate handler
410
+ const directOpTransaction = createTransaction<TOutput>({
411
+ mutationFn: async (params) => {
412
+ // Call the onUpdate handler with the transaction and collection
413
+ return this.config.onUpdate!({
414
+ transaction:
415
+ params.transaction as unknown as TransactionWithMutations<
416
+ TOutput,
417
+ `update`
418
+ >,
419
+ collection: this.collection as unknown as Collection<TOutput, TKey>,
420
+ })
421
+ },
422
+ })
423
+
424
+ // Apply mutations to the new transaction
425
+ directOpTransaction.applyMutations(mutations)
426
+ directOpTransaction.commit()
427
+
428
+ // Add the transaction to the collection's transactions store
429
+
430
+ state.transactions.set(directOpTransaction.id, directOpTransaction)
431
+ state.scheduleTransactionCleanup(directOpTransaction)
432
+ state.recomputeOptimisticState(true)
433
+
434
+ return directOpTransaction
435
+ }
436
+
437
+ /**
438
+ * Deletes one or more items from the collection
439
+ */
440
+ delete = (
441
+ keys: Array<TKey> | TKey,
442
+ config?: OperationConfig
443
+ ): TransactionType<any> => {
444
+ const state = this.state
445
+ this.lifecycle.validateCollectionUsable(`delete`)
446
+
447
+ const ambientTransaction = getActiveTransaction()
448
+
449
+ // If no ambient transaction exists, check for an onDelete handler early
450
+ if (!ambientTransaction && !this.config.onDelete) {
451
+ throw new MissingDeleteHandlerError()
452
+ }
453
+
454
+ if (Array.isArray(keys) && keys.length === 0) {
455
+ throw new NoKeysPassedToDeleteError()
456
+ }
457
+
458
+ const keysArray = Array.isArray(keys) ? keys : [keys]
459
+ const mutations: Array<
460
+ PendingMutation<
461
+ TOutput,
462
+ `delete`,
463
+ CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>
464
+ >
465
+ > = []
466
+
467
+ for (const key of keysArray) {
468
+ if (!this.state.has(key)) {
469
+ throw new DeleteKeyNotFoundError(key)
470
+ }
471
+ const globalKey = this.generateGlobalKey(key, this.state.get(key)!)
472
+ const mutation: PendingMutation<
473
+ TOutput,
474
+ `delete`,
475
+ CollectionImpl<TOutput, TKey, TUtils, TSchema, TInput>
476
+ > = {
477
+ mutationId: crypto.randomUUID(),
478
+ original: this.state.get(key)!,
479
+ modified: this.state.get(key)!,
480
+ changes: this.state.get(key)!,
481
+ globalKey,
482
+ key,
483
+ metadata: config?.metadata as unknown,
484
+ syncMetadata: (state.syncedMetadata.get(key) || {}) as Record<
485
+ string,
486
+ unknown
487
+ >,
488
+ optimistic: config?.optimistic ?? true,
489
+ type: `delete`,
490
+ createdAt: new Date(),
491
+ updatedAt: new Date(),
492
+ collection: this.collection,
493
+ }
494
+
495
+ mutations.push(mutation)
496
+ }
497
+
498
+ // If an ambient transaction exists, use it
499
+ if (ambientTransaction) {
500
+ ambientTransaction.applyMutations(mutations)
501
+
502
+ state.transactions.set(ambientTransaction.id, ambientTransaction)
503
+ state.scheduleTransactionCleanup(ambientTransaction)
504
+ state.recomputeOptimisticState(true)
505
+
506
+ return ambientTransaction
507
+ }
508
+
509
+ // Create a new transaction with a mutation function that calls the onDelete handler
510
+ const directOpTransaction = createTransaction<TOutput>({
511
+ autoCommit: true,
512
+ mutationFn: async (params) => {
513
+ // Call the onDelete handler with the transaction and collection
514
+ return this.config.onDelete!({
515
+ transaction:
516
+ params.transaction as unknown as TransactionWithMutations<
517
+ TOutput,
518
+ `delete`
519
+ >,
520
+ collection: this.collection as unknown as Collection<TOutput, TKey>,
521
+ })
522
+ },
523
+ })
524
+
525
+ // Apply mutations to the new transaction
526
+ directOpTransaction.applyMutations(mutations)
527
+ directOpTransaction.commit()
528
+
529
+ state.transactions.set(directOpTransaction.id, directOpTransaction)
530
+ state.scheduleTransactionCleanup(directOpTransaction)
531
+ state.recomputeOptimisticState(true)
532
+
533
+ return directOpTransaction
534
+ }
535
+ }