@abloatai/humans 0.56.0 → 0.57.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.
@@ -18,14 +18,9 @@ import type { LocalModel } from '../../localModelContract.js';
18
18
  import type { MutationPersistencePort } from '../../mutationPersistence.js';
19
19
  import { globalRuntime } from '../../context.js';
20
20
  import type { RuntimeContext } from '../../RuntimeContext.js';
21
- import type { MutationOperationType } from '@abloatai/transaction/types';
22
21
  import {
23
- AbloError,
24
22
  AbloConnectionError,
25
23
  AbloIdempotencyError,
26
- AbloNotFoundError,
27
- AbloValidationError,
28
- errorCodeSpec,
29
24
  } from '@abloatai/transaction/errors';
30
25
  import {
31
26
  LogPosition,
@@ -34,28 +29,28 @@ import {
34
29
  import type { WriteOptions } from '../../interfaces/index.js';
35
30
  import type { OnStaleMode, StaleNotification, ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
36
31
  import {
37
- mutationCommitResultSchema,
32
+ type CommitOperationResult,
38
33
  type MutationCommitResult,
39
34
  } from '@abloatai/transaction/wire/commit';
40
35
  import {
41
- projectCommitPayload,
42
36
  computePriorityScore,
43
37
  normalizeModelKey,
44
38
  // Includes stale guards as well as request identity/audit barriers.
45
- hasCommitCoalescingBarrier,
46
- applyWriteOptions,
47
- asTransportError,
48
- extractStatusCode,
49
- TX_TYPE_TO_MUTATION_OP,
50
39
  type MutationInput,
51
40
  type QueuedMutation,
52
41
  type UserContext,
53
- type WriteOperationFields,
54
42
  } from './commitPayload.js';
43
+ import {
44
+ generateTransactionId,
45
+ mergeMutationData,
46
+ createDataFor,
47
+ changesToInput,
48
+ updateDataFor,
49
+ previousDataFor,
50
+ } from './mutationInput.js';
55
51
  import { MutationStore } from './MutationStore.js';
56
52
  import {
57
53
  entityKey,
58
- mergeUpdateData,
59
54
  takeUnsentCreateForModel,
60
55
  findCreateBarrierForDelete,
61
56
  deferDeleteUntilCreateSettles,
@@ -66,9 +61,6 @@ import {
66
61
  deserializePersistedTransaction,
67
62
  isNonReplayablePersistedRow,
68
63
  pendingMutationRecordId,
69
- legacyPendingMutationRecordSchema,
70
- pendingMutationRecordSchema,
71
- persistedMutationSchema,
72
64
  } from './replayValidation.js';
73
65
  import {
74
66
  deserializeLegacyPendingMutation,
@@ -80,12 +72,7 @@ import {
80
72
  } from './mutationPersistence.js';
81
73
  import {
82
74
  createCommitEnvelopeMember,
83
- createDurableCommitEnvelope,
84
- commitEnvelopeRecordId,
85
- durableCommitEnvelopeSchema,
86
75
  type DurableCommitEnvelope,
87
- type DurableCommitOperation,
88
- type DurableCommitOperationInput,
89
76
  type CommitOutboxScope,
90
77
  } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
91
78
  import type { DurableWriteStore } from './durableWriteStore.js';
@@ -101,7 +88,6 @@ import {
101
88
  removeDurableCommit,
102
89
  sealDurableCommit,
103
90
  type CommitTransportContext,
104
- type SealDurableCommitInput,
105
91
  } from './commitTransport.js';
106
92
  import {
107
93
  processCommitLane,
@@ -245,18 +231,18 @@ export class MutationQueue extends EventEmitter {
245
231
  private readonly runtime: RuntimeContext;
246
232
  /** Durable transaction journal owned by this queue, before commit sealing. */
247
233
  private persistence: MutationPersistencePort | null = null;
248
- private deferredMutations: Array<{
234
+ private deferredMutations: {
249
235
  type: 'create' | 'update' | 'delete' | 'archive';
250
236
  model: LocalModel;
251
237
  capturedChanges?: Record<string, unknown>;
252
238
  writeOptions?: WriteOptions;
253
- }> = [];
254
- private pendingPersistenceStages: Array<{
239
+ }[] = [];
240
+ private pendingPersistenceStages: {
255
241
  transaction: QueuedMutation;
256
242
  modelData: Record<string, unknown>;
257
243
  resolve: () => void;
258
244
  reject: (error: Error) => void;
259
- }> = [];
245
+ }[] = [];
260
246
  private persistenceStageScheduled = false;
261
247
  private pendingDrainPromise: Promise<void> | null = null;
262
248
 
@@ -315,7 +301,7 @@ export class MutationQueue extends EventEmitter {
315
301
  commitOutbox: this.commitOutbox,
316
302
  commitOutboxScope: this.commitOutboxScope,
317
303
  mutationExecutor: this.mutationExecutor,
318
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
304
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
319
305
  };
320
306
  }
321
307
 
@@ -334,7 +320,7 @@ export class MutationQueue extends EventEmitter {
334
320
  setCommitProcessing: (value) => { this.commitProcessing = value; },
335
321
  durableReplayBlock: this.durableReplayBlock,
336
322
  sealDurableCommit: (input) => this.sealDurableCommit(input),
337
- assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
323
+ assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
338
324
  dispatchCommit: async (envelope) => this.parseMutationCommitResult(
339
325
  await this.dispatchCommitBounded(envelope.operations, {
340
326
  idempotencyKey: envelope.idempotencyKey,
@@ -345,9 +331,9 @@ export class MutationQueue extends EventEmitter {
345
331
  persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
346
332
  removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
347
333
  queuedCommitEchoSyncId: (transaction) => this.queuedCommitEchoSyncId(transaction),
348
- completeQueuedCommit: (transaction, syncId) => this.completeQueuedCommit(transaction, syncId),
349
- scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
350
- noteAck: (syncId) => this.noteAck(syncId),
334
+ completeQueuedCommit: (transaction, syncId) => { this.completeQueuedCommit(transaction, syncId); },
335
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
336
+ noteAck: (syncId) => { this.noteAck(syncId); },
351
337
  isDefinitiveRejection: (error) => this.isDefinitiveRejection(error),
352
338
  isPermanentError: (error) => this.isPermanentError(error),
353
339
  scheduleRetry: (delayMs) => {
@@ -357,7 +343,7 @@ export class MutationQueue extends EventEmitter {
357
343
  void this.processCommitLane();
358
344
  }, delayMs);
359
345
  },
360
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
346
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
361
347
  };
362
348
  }
363
349
 
@@ -374,15 +360,15 @@ export class MutationQueue extends EventEmitter {
374
360
 
375
361
  private get commitApiContext(): CommitApiContext {
376
362
  return {
377
- assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
363
+ assertDurableReplayOpen: () => { this.assertDurableReplayOpen(); },
378
364
  commitStore: this.commitStore,
379
365
  commitLane: this.commitLane,
380
366
  replicationLagErrors: this.replicationLagErrors,
381
- clearReplicationLagState: (transactionId) => this.clearReplicationLagState(transactionId),
367
+ clearReplicationLagState: (transactionId) => { this.clearReplicationLagState(transactionId); },
382
368
  nextCommitSequence: () => this.nextCommitSequence(),
383
369
  sealDurableCommit: (input) => this.sealDurableCommit(input),
384
370
  processCommitLane: () => this.processCommitLane(),
385
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
371
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
386
372
  };
387
373
  }
388
374
 
@@ -390,7 +376,7 @@ export class MutationQueue extends EventEmitter {
390
376
  return {
391
377
  enableOptimistic: this.config.enableOptimistic,
392
378
  persistenceReady: !!this.persistence && !!this.commitOutboxScope,
393
- assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
379
+ assertDurableReplayOpen: () => { this.assertDurableReplayOpen(); },
394
380
  generateId: () => this.generateId(),
395
381
  normalizeModelKey,
396
382
  computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
@@ -399,11 +385,11 @@ export class MutationQueue extends EventEmitter {
399
385
  extractPreviousData: (model, input) => this.extractPreviousData(model, input),
400
386
  mapChangesToInput: (modelName, changes) => this.mapChangesToInput(modelName, changes),
401
387
  isReorderPayload: (input) => this.isReorderPayload(input),
402
- attachConfirmation: (transaction) => this.attachConfirmation(transaction),
403
- add: (transaction) => this.store.add(transaction),
404
- applyOptimisticCreate: (model, transaction) => this.applyOptimisticCreate(model, transaction),
405
- applyOptimisticUpdate: (model, transaction) => this.applyOptimisticUpdate(model, transaction),
406
- applyOptimisticDelete: (model, transaction) => this.applyOptimisticDelete(model, transaction),
388
+ attachConfirmation: (transaction) => { this.attachConfirmation(transaction); },
389
+ add: (transaction) => { this.store.add(transaction); },
390
+ applyOptimisticCreate: (model, transaction) => { this.applyOptimisticCreate(model, transaction); },
391
+ applyOptimisticUpdate: (model, transaction) => { this.applyOptimisticUpdate(model, transaction); },
392
+ applyOptimisticDelete: (model, transaction) => { this.applyOptimisticDelete(model, transaction); },
407
393
  takeUnsentCreateForModel: (modelName, modelId) => this.takeUnsentCreateForModel(modelName, modelId),
408
394
  cancelUnsentCreateForDelete: (transaction) => this.cancelUnsentCreateForDelete(transaction),
409
395
  completeLocalDelete: (model, context, writeOptions, sourceMutationIds) => this.completeLocalDelete(model, context, writeOptions, sourceMutationIds),
@@ -411,11 +397,11 @@ export class MutationQueue extends EventEmitter {
411
397
  pendingMergeByModel: this.pendingMergeByModel,
412
398
  inFlightByModel: this.inFlightByModel,
413
399
  findCreateBarrierForDelete: (modelName, modelId) => this.findCreateBarrierForDelete(modelName, modelId),
414
- deferDeleteUntilCreateSettles: (create, transaction) => this.deferDeleteUntilCreateSettles(create, transaction),
400
+ deferDeleteUntilCreateSettles: (create, transaction) => { this.deferDeleteUntilCreateSettles(create, transaction); },
415
401
  logger: this.runtime.logger,
416
402
  persistAndStage: (transaction, modelData) => this.persistAndStage(transaction, modelData),
417
403
  persistQueuedTransaction: (transaction, modelData) => this.persistQueuedTransaction(transaction, modelData),
418
- stageTransaction: (transaction) => this.stageTransaction(transaction),
404
+ stageTransaction: (transaction) => { this.stageTransaction(transaction); },
419
405
  emit: (event, payload) => this.emit(event, payload),
420
406
  };
421
407
  }
@@ -425,9 +411,9 @@ export class MutationQueue extends EventEmitter {
425
411
  executionQueue: this.executionQueue,
426
412
  inFlightByModel: this.inFlightByModel,
427
413
  pendingMergeByModel: this.pendingMergeByModel,
428
- ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
429
- scheduleProcessing: (immediate) => this.scheduleProcessing(immediate),
430
- storeRemove: (transactionId) => this.store.remove(transactionId),
414
+ ensureDerivedFields: (transaction) => { this.ensureDerivedFields(transaction); },
415
+ scheduleProcessing: (immediate) => { this.scheduleProcessing(immediate); },
416
+ storeRemove: (transactionId) => { this.store.remove(transactionId); },
431
417
  };
432
418
  }
433
419
 
@@ -440,7 +426,7 @@ export class MutationQueue extends EventEmitter {
440
426
  isProcessing: this.isProcessing,
441
427
  setIsProcessing: (value) => { this.isProcessing = value; },
442
428
  takeNextExecutionBatch: () => this.takeNextExecutionBatch(),
443
- ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
429
+ ensureDerivedFields: (transaction) => { this.ensureDerivedFields(transaction); },
444
430
  ensureCommitEnvelope: (batch) => this.ensureCommitEnvelope([...batch]),
445
431
  executingCount: this.executingCount,
446
432
  setExecutingCount: (value) => { this.executingCount = value; },
@@ -449,7 +435,7 @@ export class MutationQueue extends EventEmitter {
449
435
  generateId: () => this.generateId(),
450
436
  computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
451
437
  store: this.store,
452
- enqueue: (transaction) => this.enqueue(transaction),
438
+ enqueue: (transaction) => { this.enqueue(transaction); },
453
439
  optimisticUpdates: this.localMutationPort.updates,
454
440
  commitNotifications: this.commitNotifications,
455
441
  commitMissingIds: this.commitMissingIds,
@@ -458,22 +444,22 @@ export class MutationQueue extends EventEmitter {
458
444
  parseMutationCommitResult: (value) => this.parseMutationCommitResult(value),
459
445
  persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
460
446
  removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
461
- assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
447
+ assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
462
448
  sealDurableCommit: (input) => this.sealDurableCommit(input),
463
- noteAck: (syncId) => this.noteAck(syncId),
449
+ noteAck: (syncId) => { this.noteAck(syncId); },
464
450
  classifyReceiptNotifications: (operations, notifications) => this.classifyReceiptNotifications(operations, notifications),
465
451
  receiptTargetKey: (modelName, modelId) => this.receiptTargetKey(modelName, modelId),
466
- scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
467
- scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs),
468
- clearReplicationLagState: (transactionId) => this.clearReplicationLagState(transactionId),
469
- completeQueuedCommit: (transaction, syncId) => this.completeQueuedCommit(transaction, syncId),
452
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
453
+ scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => { this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs); },
454
+ clearReplicationLagState: (transactionId) => { this.clearReplicationLagState(transactionId); },
455
+ completeQueuedCommit: (transaction, syncId) => { this.completeQueuedCommit(transaction, syncId); },
470
456
  queuedCommitMatchesCorrelation: (transaction, correlationId) => this.queuedCommitMatchesCorrelation(transaction, correlationId),
471
457
  recentDeltaCorrelations: this.recentDeltaCorrelations,
472
458
  lastSeenSyncId: this.lastSeenSyncId,
473
- scheduleProcessing: (immediate) => this.scheduleProcessing(immediate),
459
+ scheduleProcessing: (immediate) => { this.scheduleProcessing(immediate); },
474
460
  handleFailure: (transaction, error) => this.handleFailure(transaction, error),
475
461
  isDefinitiveRejection: (error) => this.isDefinitiveRejection(error),
476
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
462
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
477
463
  emit: (event, payload) => this.emit(event, payload),
478
464
  rollbackOptimistic: (transaction, reason, error) => this.rollbackOptimistic(transaction, reason, error),
479
465
  };
@@ -486,7 +472,7 @@ export class MutationQueue extends EventEmitter {
486
472
  store: this.store,
487
473
  isPermanentError: (error) => this.isPermanentError(error),
488
474
  rollbackOptimistic: (transaction, reason, error) => this.rollbackOptimistic(transaction, reason, error),
489
- enqueue: (transaction) => this.enqueue(transaction),
475
+ enqueue: (transaction) => { this.enqueue(transaction); },
490
476
  getLastPermanentErrorSignature: () => this.lastPermanentErrorSig,
491
477
  setLastPermanentErrorSignature: (signature) => { this.lastPermanentErrorSig = signature; },
492
478
  emit: (event, payload) => this.emit(event, payload),
@@ -499,7 +485,7 @@ export class MutationQueue extends EventEmitter {
499
485
  store: this.store,
500
486
  rollbackOptimistic: (transaction, reason) => this.rollbackOptimistic(transaction, reason),
501
487
  mergeData: (local, remote) => this.mergeData(local, remote),
502
- enqueue: (transaction) => this.enqueue(transaction),
488
+ enqueue: (transaction) => { this.enqueue(transaction); },
503
489
  };
504
490
  }
505
491
 
@@ -524,21 +510,21 @@ export class MutationQueue extends EventEmitter {
524
510
  store: this.store,
525
511
  executionQueue: this.executionQueue,
526
512
  optimisticUpdates: this.localMutationPort.updates,
527
- assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
513
+ assertDurableReplayOpen: () => { this.assertDurableReplayOpen(); },
528
514
  processCommitLane: () => this.processCommitLane(),
529
515
  takePendingDrainBatch: (pending) => this.takePendingDrainBatch(pending),
530
516
  ensureCommitEnvelope: (batch) => this.ensureCommitEnvelope(batch),
531
- ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
517
+ ensureDerivedFields: (transaction) => { this.ensureDerivedFields(transaction); },
532
518
  sourceMutationIdsFor: (batch) => this.sourceMutationIdsFor(batch),
533
519
  sealDurableCommit: (input) => this.sealDurableCommit(input),
534
- assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
520
+ assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
535
521
  parseMutationCommitResult: (value) => this.parseMutationCommitResult(value),
536
522
  dispatchCommitBounded: (...args) => this.dispatchCommitBounded(...args),
537
523
  persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
538
524
  removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
539
- scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
540
- scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs),
541
- enqueue: (transaction) => this.enqueue(transaction),
525
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
526
+ scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => { this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs); },
527
+ enqueue: (transaction) => { this.enqueue(transaction); },
542
528
  recentDeltaCorrelations: this.recentDeltaCorrelations,
543
529
  emit: (event, payload) => this.emit(event, payload),
544
530
  };
@@ -564,7 +550,7 @@ export class MutationQueue extends EventEmitter {
564
550
  commitOutboxScope: this.commitOutboxScope,
565
551
  config: this.config,
566
552
  store: this.store,
567
- enqueue: (transaction) => this.enqueue(transaction),
553
+ enqueue: (transaction) => { this.enqueue(transaction); },
568
554
  computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
569
555
  deserializeTransaction: (data) => this.deserializeTransaction(data),
570
556
  };
@@ -1703,6 +1689,7 @@ export class MutationQueue extends EventEmitter {
1703
1689
  lastSyncId: number;
1704
1690
  notifications?: StaleNotification[];
1705
1691
  missingIds?: string[];
1692
+ operationResults?: CommitOperationResult[];
1706
1693
  }> {
1707
1694
  return waitForCommitReceipt(this.commitReceiptContext, clientTxId);
1708
1695
  }
@@ -1879,47 +1866,33 @@ export class MutationQueue extends EventEmitter {
1879
1866
  }
1880
1867
 
1881
1868
  /** Generates a unique local transaction id. */
1869
+ // The payload rules live in `mutationInput`; these keep the call sites here
1870
+ // reading as the queue's own vocabulary.
1882
1871
  private generateId(): string {
1883
- return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1872
+ return generateTransactionId();
1884
1873
  }
1885
1874
 
1886
1875
  private mergeData(
1887
1876
  local: MutationInput | undefined,
1888
1877
  remote: MutationInput | undefined
1889
1878
  ): MutationInput {
1890
- return { ...(remote || {}), ...(local || {}) };
1879
+ return mergeMutationData(local, remote);
1891
1880
  }
1892
1881
 
1893
1882
  private extractCreateData(model: LocalModel): MutationInput {
1894
- return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false }, this.runtime);
1883
+ return createDataFor(model, this.runtime);
1895
1884
  }
1896
1885
 
1897
1886
  private mapChangesToInput(modelName: string, changes: Record<string, unknown>): MutationInput {
1898
- return projectCommitPayload(modelName, changes, { dropUndefined: true }, this.runtime);
1887
+ return changesToInput(modelName, changes, this.runtime);
1899
1888
  }
1900
1889
 
1901
1890
  private extractUpdateData(model: LocalModel): MutationInput {
1902
- return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true }, this.runtime);
1891
+ return updateDataFor(model, this.runtime);
1903
1892
  }
1904
1893
 
1905
- // Derive previous values for changed fields to support accurate rollback.
1906
- // Model-specific special cases do not belong here; a model that needs to
1907
- // surface previous state beyond `modifiedProperties` should expose a typed
1908
- // `getPreviousData()` accessor for this method to call.
1909
1894
  private extractPreviousData(model: LocalModel, updateInput?: MutationInput): MutationInput {
1910
- // When the update's written keys are known, capture a before-image for
1911
- // exactly those keys, so the recorded undo inverse reverts them and nothing
1912
- // else — a full-row inverse would clobber concurrent edits to unrelated
1913
- // fields. `fallbackToLive: false` makes `Model.capturePreviousValues` omit
1914
- // any key it cannot resolve, and `buildUndoOps` then drops an un-revertible
1915
- // inverse rather than inventing one. With no `updateInput` (a full extract)
1916
- // it falls back to every tracked field. `Model.capturePreviousValues` is the
1917
- // single before-image source, shared with
1918
- // `RecordingMutation.snapshotFields`.
1919
- const keys = updateInput
1920
- ? Object.keys(updateInput)
1921
- : [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
1922
- return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
1895
+ return previousDataFor(model, updateInput);
1923
1896
  }
1924
1897
 
1925
1898
  /** Returns a snapshot of queue counts and the current configuration. */
@@ -1,6 +1,9 @@
1
1
  import type { RuntimeContext } from '../../RuntimeContext.js';
2
2
  import type { ReadDependency, TrackDependency, OnStaleMode, StaleNotification } from '@abloatai/transaction/coordination/schema';
3
- import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
3
+ import type {
4
+ CommitOperationResult,
5
+ MutationCommitResult,
6
+ } from '@abloatai/transaction/wire/commit';
4
7
  import type {
5
8
  DurableCommitEnvelope,
6
9
  DurableCommitOperation,
@@ -28,6 +31,8 @@ export interface CommitTransaction {
28
31
  transientAttempts?: number;
29
32
  firstTransientFailureAt?: number;
30
33
  lastSyncId?: number;
34
+ /** Fresh transport rows. Deliberately transient: durable server replays redact row data. */
35
+ operationResults?: CommitOperationResult[];
31
36
  correlationId?: string;
32
37
  error?: Error;
33
38
  sealedAt: number;
@@ -77,7 +82,12 @@ export interface CommitReceiptContext {
77
82
  export function waitForCommitReceipt(
78
83
  ctx: CommitReceiptContext,
79
84
  clientTxId: string,
80
- ): Promise<{ lastSyncId: number; notifications?: StaleNotification[]; missingIds?: string[] }> {
85
+ ): Promise<{
86
+ lastSyncId: number;
87
+ notifications?: StaleNotification[];
88
+ missingIds?: string[];
89
+ operationResults?: CommitOperationResult[];
90
+ }> {
81
91
  const drainNotifications = (): StaleNotification[] | undefined => {
82
92
  const notifications = ctx.commitNotifications.get(clientTxId);
83
93
  if (!notifications) return undefined;
@@ -90,21 +100,28 @@ export function waitForCommitReceipt(
90
100
  ctx.commitMissingIds.delete(clientTxId);
91
101
  return ids.length > 0 ? ids : undefined;
92
102
  };
93
- const receipt = (lastSyncId: number) => {
103
+ const receipt = (transaction: CommitTransaction, lastSyncId: number) => {
94
104
  const missingIds = drainMissingIds();
95
105
  return {
96
106
  lastSyncId,
97
107
  notifications: drainNotifications(),
108
+ ...(transaction.operationResults
109
+ ? { operationResults: transaction.operationResults }
110
+ : {}),
98
111
  ...(missingIds ? { missingIds } : {}),
99
112
  };
100
113
  };
101
114
  return new Promise((resolve, reject) => {
102
115
  const existing = ctx.commitStore.get(clientTxId);
103
- if (existing?.status === 'completed') { resolve(receipt(existing.lastSyncId ?? 0)); return; }
116
+ if (existing?.status === 'completed') { resolve(receipt(existing, existing.lastSyncId ?? 0)); return; }
104
117
  if (existing?.status === 'failed' && existing.error) { reject(existing.error); return; }
105
118
  const lagError = ctx.replicationLagErrors.get(clientTxId);
106
119
  if (lagError) { reject(lagError); return; }
107
- const onCompleted = (tx: object) => { cleanup(); resolve(receipt((tx as CommitTransaction).lastSyncId ?? 0)); };
120
+ const onCompleted = (tx: object) => {
121
+ cleanup();
122
+ const completed = tx as CommitTransaction;
123
+ resolve(receipt(completed, completed.lastSyncId ?? 0));
124
+ };
108
125
  const onFailed = (payload: object) => { cleanup(); reject((payload as { error: Error }).error); };
109
126
  const onLagged = (payload: object) => { cleanup(); reject((payload as { error: Error }).error); };
110
127
  const cleanup = () => {
@@ -152,6 +169,7 @@ export async function processCommitLane(ctx: CommitLaneContext): Promise<void> {
152
169
  const result = await ctx.dispatchCommit(durableEnvelope);
153
170
  tx.durableEnvelope = await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
154
171
  tx.lastSyncId = result.lastSyncId;
172
+ if (result.operationResults?.length) tx.operationResults = [...result.operationResults];
155
173
  if (result.notifications?.length) ctx.commitNotifications.set(tx.id, result.notifications);
156
174
  if (result.missingIds?.length) ctx.commitMissingIds.set(tx.id, result.missingIds);
157
175
  ctx.commitLane.shift();
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The input a queued mutation carries, derived from a local model.
3
+ *
4
+ * Six small rules that answer one question: given a model and what changed on
5
+ * it, what does the commit actually send? They sat as private methods on
6
+ * `MutationQueue` because that is where they were called, which is how a queue
7
+ * ends up also owning the payload vocabulary.
8
+ *
9
+ * The before-image rule is the one worth reading twice, and it is stated in
10
+ * `previousDataFor` rather than here.
11
+ */
12
+
13
+ import type { LocalModel } from '../../localModelContract.js';
14
+ import type { RuntimeContext } from '../../RuntimeContext.js';
15
+ import { projectCommitPayload, type MutationInput } from './commitPayload.js';
16
+
17
+ /** A queued transaction's local identity, unique per process and monotonic enough to sort. */
18
+ export function generateTransactionId(): string {
19
+ return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
20
+ }
21
+
22
+ /** Local values win: they are the edit the caller made, remote is the base. */
23
+ export function mergeMutationData(
24
+ local: MutationInput | undefined,
25
+ remote: MutationInput | undefined,
26
+ ): MutationInput {
27
+ return { ...(remote ?? {}), ...(local ?? {}) };
28
+ }
29
+
30
+ /** A create sends the whole row, `undefined` included, so absence is explicit. */
31
+ export function createDataFor(model: LocalModel, runtime?: RuntimeContext): MutationInput {
32
+ return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false }, runtime);
33
+ }
34
+
35
+ /** An arbitrary change set, projected the way an update is. */
36
+ export function changesToInput(
37
+ modelName: string,
38
+ changes: Record<string, unknown>,
39
+ runtime?: RuntimeContext,
40
+ ): MutationInput {
41
+ return projectCommitPayload(modelName, changes, { dropUndefined: true }, runtime);
42
+ }
43
+
44
+ /** An update sends only what changed. */
45
+ export function updateDataFor(model: LocalModel, runtime?: RuntimeContext): MutationInput {
46
+ return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true }, runtime);
47
+ }
48
+
49
+ /**
50
+ * The before-image an undo reverts to.
51
+ *
52
+ * When the update's written keys are known, capture a before-image for exactly
53
+ * those keys, so the recorded undo inverse reverts them and nothing else — a
54
+ * full-row inverse would clobber concurrent edits to unrelated fields.
55
+ * `fallbackToLive: false` makes `Model.capturePreviousValues` omit any key it
56
+ * cannot resolve, and `buildUndoOps` then drops an un-revertible inverse rather
57
+ * than inventing one. With no `updateInput` (a full extract) it falls back to
58
+ * every tracked field.
59
+ *
60
+ * Model-specific special cases do not belong here: a model that needs to
61
+ * surface previous state beyond `modifiedProperties` should expose a typed
62
+ * `getPreviousData()` accessor for this to call.
63
+ */
64
+ export function previousDataFor(model: LocalModel, updateInput?: MutationInput): MutationInput {
65
+ const keys = updateInput
66
+ ? Object.keys(updateInput)
67
+ : [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
68
+ return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
69
+ }
package/src/surface.ts CHANGED
@@ -35,6 +35,7 @@ export const PUBLIC_MODEL_VERBS = [
35
35
  'get',
36
36
  'retrieve',
37
37
  'list',
38
+ 'listAll',
38
39
  'local',
39
40
  'create',
40
41
  'update',