11import { createDeferred } from './deferred'
2+ import { deepEquals } from './utils'
23import { safeRandomUUID } from './utils/uuid'
34import { normalizeError } from './utils/error.js'
45import './duplicate-instance-check'
@@ -159,9 +160,11 @@ function getTransactionAmbientScope(transaction: object): TransactionScope {
159160 * - (update, update) → update (replace with latest, union changes)
160161 * - (delete, delete) → delete (replace with latest)
161162 * - (insert, insert) → insert (replace with latest)
163+ * - (delete, insert) → insert without an authoritative row, null if restoring
164+ * the authoritative row, otherwise update
162165 *
163- * Note: (delete, update) and (delete, insert) should never occur as the collection
164- * layer prevents operations on deleted items within the same transaction.
166+ * Note: (delete, update) should never occur as the collection layer prevents
167+ * update operations on deleted items within the same transaction.
165168 *
166169 * @param existing - The existing mutation in the transaction
167170 * @param incoming - The new mutation being applied
@@ -199,7 +202,8 @@ function mergePendingMutations<T extends object>(
199202 return null
200203
201204 case `update-delete` :
202- // Delete after update: delete dominates
205+ case `delete-delete` :
206+ // Delete dominates an update or earlier delete.
203207 return incoming
204208
205209 case `update-update` : {
@@ -216,11 +220,39 @@ function mergePendingMutations<T extends object>(
216220 }
217221 }
218222
219- case `delete-delete` :
220223 case `insert-insert` :
221224 // Same type: replace with latest
222225 return incoming
223226
227+ case `delete-insert` : {
228+ const original = existing . collection . _state . syncedData . get ( existing . key )
229+ if ( original === undefined ) return incoming
230+ if ( deepEquals ( original , incoming . modified ) ) {
231+ return null
232+ }
233+
234+ const modified = incoming . modified
235+ const keys = new Set ( [ ...Object . keys ( original ) , ...Object . keys ( modified ) ] )
236+ const changes : Partial < T > = { }
237+ for ( const key of keys ) {
238+ if (
239+ Object . hasOwn ( original , key ) !== Object . hasOwn ( modified , key ) ||
240+ ! deepEquals ( original [ key as keyof T ] , modified [ key as keyof T ] )
241+ ) {
242+ changes [ key as keyof T ] = modified [ key as keyof T ]
243+ }
244+ }
245+
246+ return {
247+ ...incoming ,
248+ type : `update` ,
249+ original,
250+ changes,
251+ metadata : incoming . metadata ?? existing . metadata ,
252+ syncMetadata : { ...existing . syncMetadata , ...incoming . syncMetadata } ,
253+ }
254+ }
255+
224256 default : {
225257 // Exhaustiveness check
226258 const _exhaustive : never = `${ existing . type } -${ incoming . type } ` as never
@@ -450,6 +482,7 @@ class Transaction<T extends object = Record<string, unknown>> {
450482 * - **insert + delete** → removed (mutations cancel each other out)
451483 * - **update + delete** → delete (delete dominates)
452484 * - **update + update** → update (union changes, keep first original)
485+ * - **delete + insert** → removed if restored, otherwise update
453486 * - **same type** → replace with latest
454487 *
455488 * This merging reduces over-the-wire churn and keeps the optimistic local view
0 commit comments