@abloatai/humans 0.55.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.
Files changed (37) hide show
  1. package/dist/local/Database.js +2 -2
  2. package/dist/local/Model.js +1 -1
  3. package/dist/local/SyncClient.d.ts +3 -34
  4. package/dist/local/SyncClient.js +1 -16
  5. package/dist/local/client/createModelProxy.d.ts +13 -1
  6. package/dist/local/client/createModelProxy.js +159 -80
  7. package/dist/local/client/options.d.ts +7 -4
  8. package/dist/local/client/reactiveEngine.js +5 -1
  9. package/dist/local/client/wsMutationExecutor.js +1 -0
  10. package/dist/local/logPosition.d.ts +13 -2
  11. package/dist/local/logPosition.js +17 -5
  12. package/dist/local/sync/SyncWebSocket.js +16 -0
  13. package/dist/local/syncClientTypes.d.ts +41 -0
  14. package/dist/local/syncClientTypes.js +11 -0
  15. package/dist/local/transactions/mutations/MutationQueue.d.ts +2 -0
  16. package/dist/local/transactions/mutations/MutationQueue.js +54 -69
  17. package/dist/local/transactions/mutations/commitLane.d.ts +4 -1
  18. package/dist/local/transactions/mutations/commitLane.js +12 -3
  19. package/dist/local/transactions/mutations/mutationInput.d.ts +40 -0
  20. package/dist/local/transactions/mutations/mutationInput.js +53 -0
  21. package/dist/surface.d.ts +1 -1
  22. package/dist/surface.js +1 -0
  23. package/package.json +2 -2
  24. package/src/local/Database.ts +2 -3
  25. package/src/local/Model.ts +1 -1
  26. package/src/local/SyncClient.ts +12 -72
  27. package/src/local/client/createModelProxy.ts +164 -13
  28. package/src/local/client/options.ts +7 -4
  29. package/src/local/client/reactiveEngine.ts +5 -1
  30. package/src/local/client/wsMutationExecutor.ts +1 -0
  31. package/src/local/logPosition.ts +19 -6
  32. package/src/local/sync/SyncWebSocket.ts +15 -0
  33. package/src/local/syncClientTypes.ts +59 -0
  34. package/src/local/transactions/mutations/MutationQueue.ts +61 -88
  35. package/src/local/transactions/mutations/commitLane.ts +23 -5
  36. package/src/local/transactions/mutations/mutationInput.ts +69 -0
  37. package/src/surface.ts +1 -0
@@ -14,18 +14,17 @@
14
14
  import { EventEmitter } from 'events';
15
15
  import { v4 as uuid } from 'uuid';
16
16
  import { globalRuntime } from '../../context.js';
17
- import { AbloError, AbloConnectionError, AbloIdempotencyError, AbloNotFoundError, AbloValidationError, errorCodeSpec, } from '@abloatai/transaction/errors';
17
+ import { AbloConnectionError, AbloIdempotencyError, } from '@abloatai/transaction/errors';
18
18
  import { LogPosition, } from '../../logPosition.js';
19
- import { mutationCommitResultSchema, } from '@abloatai/transaction/wire/commit';
20
- import { projectCommitPayload, computePriorityScore, normalizeModelKey,
21
- // Includes stale guards as well as request identity/audit barriers.
22
- hasCommitCoalescingBarrier, applyWriteOptions, asTransportError, extractStatusCode, TX_TYPE_TO_MUTATION_OP, } from './commitPayload.js';
19
+ import {} from '@abloatai/transaction/wire/commit';
20
+ import { computePriorityScore, normalizeModelKey, } from './commitPayload.js';
21
+ import { generateTransactionId, mergeMutationData, createDataFor, changesToInput, updateDataFor, previousDataFor, } from './mutationInput.js';
23
22
  import { MutationStore } from './MutationStore.js';
24
- import { entityKey, mergeUpdateData, takeUnsentCreateForModel, findCreateBarrierForDelete, deferDeleteUntilCreateSettles, releaseDeferredDeletesForCreate, } from './coalesceRules.js';
23
+ import { entityKey, takeUnsentCreateForModel, findCreateBarrierForDelete, deferDeleteUntilCreateSettles, releaseDeferredDeletesForCreate, } from './coalesceRules.js';
25
24
  import { DeltaConfirmationTracker } from './deltaConfirmation.js';
26
- import { deserializePersistedTransaction, isNonReplayablePersistedRow, pendingMutationRecordId, legacyPendingMutationRecordSchema, pendingMutationRecordSchema, persistedMutationSchema, } from './replayValidation.js';
25
+ import { deserializePersistedTransaction, isNonReplayablePersistedRow, pendingMutationRecordId, } from './replayValidation.js';
27
26
  import { deserializeLegacyPendingMutation, loadPersistedTransactions, persistQueuedTransaction, removePersistedTransaction, settlePersistedFailure, } from './mutationPersistence.js';
28
- import { createCommitEnvelopeMember, createDurableCommitEnvelope, commitEnvelopeRecordId, durableCommitEnvelopeSchema, } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
27
+ import { createCommitEnvelopeMember, } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
29
28
  import { stableStringify } from '@abloatai/transaction/utils/json';
30
29
  import { createLocalMutationPort, } from './localMutation.js';
31
30
  import { dispatchCommitBounded, parseMutationCommitResult, persistDurableCommitAcceptance, removeDurableCommit, sealDurableCommit, } from './commitTransport.js';
@@ -120,7 +119,7 @@ export class MutationQueue extends EventEmitter {
120
119
  commitOutbox: this.commitOutbox,
121
120
  commitOutboxScope: this.commitOutboxScope,
122
121
  mutationExecutor: this.mutationExecutor,
123
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
122
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
124
123
  };
125
124
  }
126
125
  get commitLaneContext() {
@@ -138,7 +137,7 @@ export class MutationQueue extends EventEmitter {
138
137
  setCommitProcessing: (value) => { this.commitProcessing = value; },
139
138
  durableReplayBlock: this.durableReplayBlock,
140
139
  sealDurableCommit: (input) => this.sealDurableCommit(input),
141
- assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
140
+ assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
142
141
  dispatchCommit: async (envelope) => this.parseMutationCommitResult(await this.dispatchCommitBounded(envelope.operations, {
143
142
  idempotencyKey: envelope.idempotencyKey,
144
143
  ...(envelope.commitOptions.reads ? { reads: envelope.commitOptions.reads } : {}),
@@ -147,9 +146,9 @@ export class MutationQueue extends EventEmitter {
147
146
  persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
148
147
  removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
149
148
  queuedCommitEchoSyncId: (transaction) => this.queuedCommitEchoSyncId(transaction),
150
- completeQueuedCommit: (transaction, syncId) => this.completeQueuedCommit(transaction, syncId),
151
- scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
152
- noteAck: (syncId) => this.noteAck(syncId),
149
+ completeQueuedCommit: (transaction, syncId) => { this.completeQueuedCommit(transaction, syncId); },
150
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
151
+ noteAck: (syncId) => { this.noteAck(syncId); },
153
152
  isDefinitiveRejection: (error) => this.isDefinitiveRejection(error),
154
153
  isPermanentError: (error) => this.isPermanentError(error),
155
154
  scheduleRetry: (delayMs) => {
@@ -160,7 +159,7 @@ export class MutationQueue extends EventEmitter {
160
159
  void this.processCommitLane();
161
160
  }, delayMs);
162
161
  },
163
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
162
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
164
163
  };
165
164
  }
166
165
  get commitReceiptContext() {
@@ -175,22 +174,22 @@ export class MutationQueue extends EventEmitter {
175
174
  }
176
175
  get commitApiContext() {
177
176
  return {
178
- assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
177
+ assertDurableReplayOpen: () => { this.assertDurableReplayOpen(); },
179
178
  commitStore: this.commitStore,
180
179
  commitLane: this.commitLane,
181
180
  replicationLagErrors: this.replicationLagErrors,
182
- clearReplicationLagState: (transactionId) => this.clearReplicationLagState(transactionId),
181
+ clearReplicationLagState: (transactionId) => { this.clearReplicationLagState(transactionId); },
183
182
  nextCommitSequence: () => this.nextCommitSequence(),
184
183
  sealDurableCommit: (input) => this.sealDurableCommit(input),
185
184
  processCommitLane: () => this.processCommitLane(),
186
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
185
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
187
186
  };
188
187
  }
189
188
  get modelMutationContext() {
190
189
  return {
191
190
  enableOptimistic: this.config.enableOptimistic,
192
191
  persistenceReady: !!this.persistence && !!this.commitOutboxScope,
193
- assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
192
+ assertDurableReplayOpen: () => { this.assertDurableReplayOpen(); },
194
193
  generateId: () => this.generateId(),
195
194
  normalizeModelKey,
196
195
  computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
@@ -199,11 +198,11 @@ export class MutationQueue extends EventEmitter {
199
198
  extractPreviousData: (model, input) => this.extractPreviousData(model, input),
200
199
  mapChangesToInput: (modelName, changes) => this.mapChangesToInput(modelName, changes),
201
200
  isReorderPayload: (input) => this.isReorderPayload(input),
202
- attachConfirmation: (transaction) => this.attachConfirmation(transaction),
203
- add: (transaction) => this.store.add(transaction),
204
- applyOptimisticCreate: (model, transaction) => this.applyOptimisticCreate(model, transaction),
205
- applyOptimisticUpdate: (model, transaction) => this.applyOptimisticUpdate(model, transaction),
206
- applyOptimisticDelete: (model, transaction) => this.applyOptimisticDelete(model, transaction),
201
+ attachConfirmation: (transaction) => { this.attachConfirmation(transaction); },
202
+ add: (transaction) => { this.store.add(transaction); },
203
+ applyOptimisticCreate: (model, transaction) => { this.applyOptimisticCreate(model, transaction); },
204
+ applyOptimisticUpdate: (model, transaction) => { this.applyOptimisticUpdate(model, transaction); },
205
+ applyOptimisticDelete: (model, transaction) => { this.applyOptimisticDelete(model, transaction); },
207
206
  takeUnsentCreateForModel: (modelName, modelId) => this.takeUnsentCreateForModel(modelName, modelId),
208
207
  cancelUnsentCreateForDelete: (transaction) => this.cancelUnsentCreateForDelete(transaction),
209
208
  completeLocalDelete: (model, context, writeOptions, sourceMutationIds) => this.completeLocalDelete(model, context, writeOptions, sourceMutationIds),
@@ -211,11 +210,11 @@ export class MutationQueue extends EventEmitter {
211
210
  pendingMergeByModel: this.pendingMergeByModel,
212
211
  inFlightByModel: this.inFlightByModel,
213
212
  findCreateBarrierForDelete: (modelName, modelId) => this.findCreateBarrierForDelete(modelName, modelId),
214
- deferDeleteUntilCreateSettles: (create, transaction) => this.deferDeleteUntilCreateSettles(create, transaction),
213
+ deferDeleteUntilCreateSettles: (create, transaction) => { this.deferDeleteUntilCreateSettles(create, transaction); },
215
214
  logger: this.runtime.logger,
216
215
  persistAndStage: (transaction, modelData) => this.persistAndStage(transaction, modelData),
217
216
  persistQueuedTransaction: (transaction, modelData) => this.persistQueuedTransaction(transaction, modelData),
218
- stageTransaction: (transaction) => this.stageTransaction(transaction),
217
+ stageTransaction: (transaction) => { this.stageTransaction(transaction); },
219
218
  emit: (event, payload) => this.emit(event, payload),
220
219
  };
221
220
  }
@@ -224,9 +223,9 @@ export class MutationQueue extends EventEmitter {
224
223
  executionQueue: this.executionQueue,
225
224
  inFlightByModel: this.inFlightByModel,
226
225
  pendingMergeByModel: this.pendingMergeByModel,
227
- ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
228
- scheduleProcessing: (immediate) => this.scheduleProcessing(immediate),
229
- storeRemove: (transactionId) => this.store.remove(transactionId),
226
+ ensureDerivedFields: (transaction) => { this.ensureDerivedFields(transaction); },
227
+ scheduleProcessing: (immediate) => { this.scheduleProcessing(immediate); },
228
+ storeRemove: (transactionId) => { this.store.remove(transactionId); },
230
229
  };
231
230
  }
232
231
  get batchProcessingContext() {
@@ -238,7 +237,7 @@ export class MutationQueue extends EventEmitter {
238
237
  isProcessing: this.isProcessing,
239
238
  setIsProcessing: (value) => { this.isProcessing = value; },
240
239
  takeNextExecutionBatch: () => this.takeNextExecutionBatch(),
241
- ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
240
+ ensureDerivedFields: (transaction) => { this.ensureDerivedFields(transaction); },
242
241
  ensureCommitEnvelope: (batch) => this.ensureCommitEnvelope([...batch]),
243
242
  executingCount: this.executingCount,
244
243
  setExecutingCount: (value) => { this.executingCount = value; },
@@ -247,7 +246,7 @@ export class MutationQueue extends EventEmitter {
247
246
  generateId: () => this.generateId(),
248
247
  computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
249
248
  store: this.store,
250
- enqueue: (transaction) => this.enqueue(transaction),
249
+ enqueue: (transaction) => { this.enqueue(transaction); },
251
250
  optimisticUpdates: this.localMutationPort.updates,
252
251
  commitNotifications: this.commitNotifications,
253
252
  commitMissingIds: this.commitMissingIds,
@@ -256,22 +255,22 @@ export class MutationQueue extends EventEmitter {
256
255
  parseMutationCommitResult: (value) => this.parseMutationCommitResult(value),
257
256
  persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
258
257
  removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
259
- assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
258
+ assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
260
259
  sealDurableCommit: (input) => this.sealDurableCommit(input),
261
- noteAck: (syncId) => this.noteAck(syncId),
260
+ noteAck: (syncId) => { this.noteAck(syncId); },
262
261
  classifyReceiptNotifications: (operations, notifications) => this.classifyReceiptNotifications(operations, notifications),
263
262
  receiptTargetKey: (modelName, modelId) => this.receiptTargetKey(modelName, modelId),
264
- scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
265
- scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs),
266
- clearReplicationLagState: (transactionId) => this.clearReplicationLagState(transactionId),
267
- completeQueuedCommit: (transaction, syncId) => this.completeQueuedCommit(transaction, syncId),
263
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
264
+ scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => { this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs); },
265
+ clearReplicationLagState: (transactionId) => { this.clearReplicationLagState(transactionId); },
266
+ completeQueuedCommit: (transaction, syncId) => { this.completeQueuedCommit(transaction, syncId); },
268
267
  queuedCommitMatchesCorrelation: (transaction, correlationId) => this.queuedCommitMatchesCorrelation(transaction, correlationId),
269
268
  recentDeltaCorrelations: this.recentDeltaCorrelations,
270
269
  lastSeenSyncId: this.lastSeenSyncId,
271
- scheduleProcessing: (immediate) => this.scheduleProcessing(immediate),
270
+ scheduleProcessing: (immediate) => { this.scheduleProcessing(immediate); },
272
271
  handleFailure: (transaction, error) => this.handleFailure(transaction, error),
273
272
  isDefinitiveRejection: (error) => this.isDefinitiveRejection(error),
274
- emitCommitLifecycle: (event, payload) => this.emitCommitLifecycle(event, payload),
273
+ emitCommitLifecycle: (event, payload) => { this.emitCommitLifecycle(event, payload); },
275
274
  emit: (event, payload) => this.emit(event, payload),
276
275
  rollbackOptimistic: (transaction, reason, error) => this.rollbackOptimistic(transaction, reason, error),
277
276
  };
@@ -283,7 +282,7 @@ export class MutationQueue extends EventEmitter {
283
282
  store: this.store,
284
283
  isPermanentError: (error) => this.isPermanentError(error),
285
284
  rollbackOptimistic: (transaction, reason, error) => this.rollbackOptimistic(transaction, reason, error),
286
- enqueue: (transaction) => this.enqueue(transaction),
285
+ enqueue: (transaction) => { this.enqueue(transaction); },
287
286
  getLastPermanentErrorSignature: () => this.lastPermanentErrorSig,
288
287
  setLastPermanentErrorSignature: (signature) => { this.lastPermanentErrorSig = signature; },
289
288
  emit: (event, payload) => this.emit(event, payload),
@@ -295,7 +294,7 @@ export class MutationQueue extends EventEmitter {
295
294
  store: this.store,
296
295
  rollbackOptimistic: (transaction, reason) => this.rollbackOptimistic(transaction, reason),
297
296
  mergeData: (local, remote) => this.mergeData(local, remote),
298
- enqueue: (transaction) => this.enqueue(transaction),
297
+ enqueue: (transaction) => { this.enqueue(transaction); },
299
298
  };
300
299
  }
301
300
  get processingSchedulerContext() {
@@ -318,21 +317,21 @@ export class MutationQueue extends EventEmitter {
318
317
  store: this.store,
319
318
  executionQueue: this.executionQueue,
320
319
  optimisticUpdates: this.localMutationPort.updates,
321
- assertDurableReplayOpen: () => this.assertDurableReplayOpen(),
320
+ assertDurableReplayOpen: () => { this.assertDurableReplayOpen(); },
322
321
  processCommitLane: () => this.processCommitLane(),
323
322
  takePendingDrainBatch: (pending) => this.takePendingDrainBatch(pending),
324
323
  ensureCommitEnvelope: (batch) => this.ensureCommitEnvelope(batch),
325
- ensureDerivedFields: (transaction) => this.ensureDerivedFields(transaction),
324
+ ensureDerivedFields: (transaction) => { this.ensureDerivedFields(transaction); },
326
325
  sourceMutationIdsFor: (batch) => this.sourceMutationIdsFor(batch),
327
326
  sealDurableCommit: (input) => this.sealDurableCommit(input),
328
- assertEnvelopeInsideReplayWindow: (envelope) => this.assertEnvelopeInsideReplayWindow(envelope),
327
+ assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
329
328
  parseMutationCommitResult: (value) => this.parseMutationCommitResult(value),
330
329
  dispatchCommitBounded: (...args) => this.dispatchCommitBounded(...args),
331
330
  persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
332
331
  removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
333
- scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId),
334
- scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs),
335
- enqueue: (transaction) => this.enqueue(transaction),
332
+ scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
333
+ scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => { this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs); },
334
+ enqueue: (transaction) => { this.enqueue(transaction); },
336
335
  recentDeltaCorrelations: this.recentDeltaCorrelations,
337
336
  emit: (event, payload) => this.emit(event, payload),
338
337
  };
@@ -356,7 +355,7 @@ export class MutationQueue extends EventEmitter {
356
355
  commitOutboxScope: this.commitOutboxScope,
357
356
  config: this.config,
358
357
  store: this.store,
359
- enqueue: (transaction) => this.enqueue(transaction),
358
+ enqueue: (transaction) => { this.enqueue(transaction); },
360
359
  computePriorityScore: (type, modelName) => this.computePriorityScore(type, modelName),
361
360
  deserializeTransaction: (data) => this.deserializeTransaction(data),
362
361
  };
@@ -1390,39 +1389,25 @@ export class MutationQueue extends EventEmitter {
1390
1389
  ];
1391
1390
  }
1392
1391
  /** Generates a unique local transaction id. */
1392
+ // The payload rules live in `mutationInput`; these keep the call sites here
1393
+ // reading as the queue's own vocabulary.
1393
1394
  generateId() {
1394
- return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
1395
+ return generateTransactionId();
1395
1396
  }
1396
1397
  mergeData(local, remote) {
1397
- return { ...(remote || {}), ...(local || {}) };
1398
+ return mergeMutationData(local, remote);
1398
1399
  }
1399
1400
  extractCreateData(model) {
1400
- return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false }, this.runtime);
1401
+ return createDataFor(model, this.runtime);
1401
1402
  }
1402
1403
  mapChangesToInput(modelName, changes) {
1403
- return projectCommitPayload(modelName, changes, { dropUndefined: true }, this.runtime);
1404
+ return changesToInput(modelName, changes, this.runtime);
1404
1405
  }
1405
1406
  extractUpdateData(model) {
1406
- return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true }, this.runtime);
1407
+ return updateDataFor(model, this.runtime);
1407
1408
  }
1408
- // Derive previous values for changed fields to support accurate rollback.
1409
- // Model-specific special cases do not belong here; a model that needs to
1410
- // surface previous state beyond `modifiedProperties` should expose a typed
1411
- // `getPreviousData()` accessor for this method to call.
1412
1409
  extractPreviousData(model, updateInput) {
1413
- // When the update's written keys are known, capture a before-image for
1414
- // exactly those keys, so the recorded undo inverse reverts them and nothing
1415
- // else — a full-row inverse would clobber concurrent edits to unrelated
1416
- // fields. `fallbackToLive: false` makes `Model.capturePreviousValues` omit
1417
- // any key it cannot resolve, and `buildUndoOps` then drops an un-revertible
1418
- // inverse rather than inventing one. With no `updateInput` (a full extract)
1419
- // it falls back to every tracked field. `Model.capturePreviousValues` is the
1420
- // single before-image source, shared with
1421
- // `RecordingMutation.snapshotFields`.
1422
- const keys = updateInput
1423
- ? Object.keys(updateInput)
1424
- : [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
1425
- return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
1410
+ return previousDataFor(model, updateInput);
1426
1411
  }
1427
1412
  /** Returns a snapshot of queue counts and the current configuration. */
1428
1413
  getStats() {
@@ -1,6 +1,6 @@
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 { CommitOperationResult, MutationCommitResult } from '@abloatai/transaction/wire/commit';
4
4
  import type { DurableCommitEnvelope, DurableCommitOperation } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
5
5
  import type { SealDurableCommitInput } from './commitTransport.js';
6
6
  export interface CommitTransaction {
@@ -23,6 +23,8 @@ export interface CommitTransaction {
23
23
  transientAttempts?: number;
24
24
  firstTransientFailureAt?: number;
25
25
  lastSyncId?: number;
26
+ /** Fresh transport rows. Deliberately transient: durable server replays redact row data. */
27
+ operationResults?: CommitOperationResult[];
26
28
  correlationId?: string;
27
29
  error?: Error;
28
30
  sealedAt: number;
@@ -73,5 +75,6 @@ export declare function waitForCommitReceipt(ctx: CommitReceiptContext, clientTx
73
75
  lastSyncId: number;
74
76
  notifications?: StaleNotification[];
75
77
  missingIds?: string[];
78
+ operationResults?: CommitOperationResult[];
76
79
  }>;
77
80
  export declare function processCommitLane(ctx: CommitLaneContext): Promise<void>;
@@ -14,18 +14,21 @@ export function waitForCommitReceipt(ctx, clientTxId) {
14
14
  ctx.commitMissingIds.delete(clientTxId);
15
15
  return ids.length > 0 ? ids : undefined;
16
16
  };
17
- const receipt = (lastSyncId) => {
17
+ const receipt = (transaction, lastSyncId) => {
18
18
  const missingIds = drainMissingIds();
19
19
  return {
20
20
  lastSyncId,
21
21
  notifications: drainNotifications(),
22
+ ...(transaction.operationResults
23
+ ? { operationResults: transaction.operationResults }
24
+ : {}),
22
25
  ...(missingIds ? { missingIds } : {}),
23
26
  };
24
27
  };
25
28
  return new Promise((resolve, reject) => {
26
29
  const existing = ctx.commitStore.get(clientTxId);
27
30
  if (existing?.status === 'completed') {
28
- resolve(receipt(existing.lastSyncId ?? 0));
31
+ resolve(receipt(existing, existing.lastSyncId ?? 0));
29
32
  return;
30
33
  }
31
34
  if (existing?.status === 'failed' && existing.error) {
@@ -37,7 +40,11 @@ export function waitForCommitReceipt(ctx, clientTxId) {
37
40
  reject(lagError);
38
41
  return;
39
42
  }
40
- const onCompleted = (tx) => { cleanup(); resolve(receipt(tx.lastSyncId ?? 0)); };
43
+ const onCompleted = (tx) => {
44
+ cleanup();
45
+ const completed = tx;
46
+ resolve(receipt(completed, completed.lastSyncId ?? 0));
47
+ };
41
48
  const onFailed = (payload) => { cleanup(); reject(payload.error); };
42
49
  const onLagged = (payload) => { cleanup(); reject(payload.error); };
43
50
  const cleanup = () => {
@@ -86,6 +93,8 @@ export async function processCommitLane(ctx) {
86
93
  const result = await ctx.dispatchCommit(durableEnvelope);
87
94
  tx.durableEnvelope = await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
88
95
  tx.lastSyncId = result.lastSyncId;
96
+ if (result.operationResults?.length)
97
+ tx.operationResults = [...result.operationResults];
89
98
  if (result.notifications?.length)
90
99
  ctx.commitNotifications.set(tx.id, result.notifications);
91
100
  if (result.missingIds?.length)
@@ -0,0 +1,40 @@
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
+ import type { LocalModel } from '../../localModelContract.js';
13
+ import type { RuntimeContext } from '../../RuntimeContext.js';
14
+ import { type MutationInput } from './commitPayload.js';
15
+ /** A queued transaction's local identity, unique per process and monotonic enough to sort. */
16
+ export declare function generateTransactionId(): string;
17
+ /** Local values win: they are the edit the caller made, remote is the base. */
18
+ export declare function mergeMutationData(local: MutationInput | undefined, remote: MutationInput | undefined): MutationInput;
19
+ /** A create sends the whole row, `undefined` included, so absence is explicit. */
20
+ export declare function createDataFor(model: LocalModel, runtime?: RuntimeContext): MutationInput;
21
+ /** An arbitrary change set, projected the way an update is. */
22
+ export declare function changesToInput(modelName: string, changes: Record<string, unknown>, runtime?: RuntimeContext): MutationInput;
23
+ /** An update sends only what changed. */
24
+ export declare function updateDataFor(model: LocalModel, runtime?: RuntimeContext): MutationInput;
25
+ /**
26
+ * The before-image an undo reverts to.
27
+ *
28
+ * When the update's written keys are known, capture a before-image for exactly
29
+ * those keys, so the recorded undo inverse reverts them and nothing else — a
30
+ * full-row inverse would clobber concurrent edits to unrelated fields.
31
+ * `fallbackToLive: false` makes `Model.capturePreviousValues` omit any key it
32
+ * cannot resolve, and `buildUndoOps` then drops an un-revertible inverse rather
33
+ * than inventing one. With no `updateInput` (a full extract) it falls back to
34
+ * every tracked field.
35
+ *
36
+ * Model-specific special cases do not belong here: a model that needs to
37
+ * surface previous state beyond `modifiedProperties` should expose a typed
38
+ * `getPreviousData()` accessor for this to call.
39
+ */
40
+ export declare function previousDataFor(model: LocalModel, updateInput?: MutationInput): MutationInput;
@@ -0,0 +1,53 @@
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
+ import { projectCommitPayload } from './commitPayload.js';
13
+ /** A queued transaction's local identity, unique per process and monotonic enough to sort. */
14
+ export function generateTransactionId() {
15
+ return `tx_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
16
+ }
17
+ /** Local values win: they are the edit the caller made, remote is the base. */
18
+ export function mergeMutationData(local, remote) {
19
+ return { ...(remote ?? {}), ...(local ?? {}) };
20
+ }
21
+ /** A create sends the whole row, `undefined` included, so absence is explicit. */
22
+ export function createDataFor(model, runtime) {
23
+ return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false }, runtime);
24
+ }
25
+ /** An arbitrary change set, projected the way an update is. */
26
+ export function changesToInput(modelName, changes, runtime) {
27
+ return projectCommitPayload(modelName, changes, { dropUndefined: true }, runtime);
28
+ }
29
+ /** An update sends only what changed. */
30
+ export function updateDataFor(model, runtime) {
31
+ return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true }, runtime);
32
+ }
33
+ /**
34
+ * The before-image an undo reverts to.
35
+ *
36
+ * When the update's written keys are known, capture a before-image for exactly
37
+ * those keys, so the recorded undo inverse reverts them and nothing else — a
38
+ * full-row inverse would clobber concurrent edits to unrelated fields.
39
+ * `fallbackToLive: false` makes `Model.capturePreviousValues` omit any key it
40
+ * cannot resolve, and `buildUndoOps` then drops an un-revertible inverse rather
41
+ * than inventing one. With no `updateInput` (a full extract) it falls back to
42
+ * every tracked field.
43
+ *
44
+ * Model-specific special cases do not belong here: a model that needs to
45
+ * surface previous state beyond `modifiedProperties` should expose a typed
46
+ * `getPreviousData()` accessor for this to call.
47
+ */
48
+ export function previousDataFor(model, updateInput) {
49
+ const keys = updateInput
50
+ ? Object.keys(updateInput)
51
+ : [...(model.modifiedProperties instanceof Map ? model.modifiedProperties.keys() : [])];
52
+ return { id: model.id, ...model.capturePreviousValues(keys, { fallbackToLive: false }) };
53
+ }
package/dist/surface.d.ts CHANGED
@@ -19,7 +19,7 @@
19
19
  * tuple, so it is the one list of model-verb names a generated summary can
20
20
  * describe.
21
21
  */
22
- export declare const PUBLIC_MODEL_VERBS: readonly ["get", "retrieve", "list", "local", "create", "update", "delete", "claim", "track", "join", "onChange"];
22
+ export declare const PUBLIC_MODEL_VERBS: readonly ["get", "retrieve", "list", "listAll", "local", "create", "update", "delete", "claim", "track", "join", "onChange"];
23
23
  /**
24
24
  * The option keys accepted by `local.list` and `onChange`, matching the
25
25
  * keys of {@link LocalReadOptions}. Note that the lifecycle filter is named
package/dist/surface.js CHANGED
@@ -24,6 +24,7 @@ export const PUBLIC_MODEL_VERBS = [
24
24
  'get',
25
25
  'retrieve',
26
26
  'list',
27
+ 'listAll',
27
28
  'local',
28
29
  'create',
29
30
  'update',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/humans",
3
- "version": "0.55.0",
3
+ "version": "0.57.0",
4
4
  "description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -84,7 +84,7 @@
84
84
  "directory": "packages/humans"
85
85
  },
86
86
  "dependencies": {
87
- "@abloatai/transaction": "^0.55.0",
87
+ "@abloatai/transaction": "^0.57.0",
88
88
  "mobx": "^6.13.7",
89
89
  "uuid": "^11.1.0",
90
90
  "zod": "^4.4.3"
@@ -5,7 +5,6 @@
5
5
  * server. {@link BaseSyncedStore} drives it, and {@link InstanceCache} holds the
6
6
  * in-memory mirror of what this class persists.
7
7
  */
8
-
9
8
  import { DatabaseManager, type DatabaseInfo, type WorkspaceMetadata } from './stores/DatabaseManager.js';
10
9
  import type { PersistenceIdentity } from './stores/persistenceIdentity.js';
11
10
  import { StoreManager } from './stores/StoreManager.js';
@@ -21,7 +20,7 @@ import {
21
20
  } from './stores/persistenceCleanup.js';
22
21
  import type { BootstrapFetcher, BootstrapData } from './sync/BootstrapFetcher.js';
23
22
  import { InMemoryObjectStore } from './adapters/inMemoryStorage.js';
24
- import { logPositionSchema } from './logPosition.js';
23
+ import { logPositionSnapshotSchema } from './logPosition.js';
25
24
  import type { SyncDeltaAction } from '@abloatai/transaction/wire/delta';
26
25
  import type { BootstrapType } from '@abloatai/transaction/types';
27
26
  import { highestPersistedPrefixSyncId } from './sync/persistedPrefix.js';
@@ -448,7 +447,7 @@ export class Database {
448
447
  // which only catches falsy, and get sent to the server as the resume
449
448
  // point). Invalid → 0 → full bootstrap, the safe degradation.
450
449
  const metadataLastSyncId =
451
- logPositionSchema.shape.persisted.safeParse(metadata?.lastSyncId).data ?? 0;
450
+ logPositionSnapshotSchema.shape.persisted.safeParse(metadata?.lastSyncId).data ?? 0;
452
451
  const dataAge = metadata?.updatedAt ? Date.now() - metadata.updatedAt.getTime() : Infinity;
453
452
 
454
453
  // ── Cache-validity check ─────────────────────────────────────
@@ -1106,7 +1106,7 @@ export abstract class Model {
1106
1106
  }
1107
1107
 
1108
1108
  // Try to get model class by identifier
1109
- let ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
1109
+ const ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
1110
1110
 
1111
1111
  if (!ModelClass) {
1112
1112
  throw new AbloValidationError(
@@ -34,85 +34,25 @@ import {
34
34
  type UnconfirmedWritesMetrics,
35
35
  } from './transactions/mutations/UnconfirmedWrites.js';
36
36
  import type { DurableWriteStore } from './transactions/mutations/durableWriteStore.js';
37
- import type { CommitTransaction } from './transactions/mutations/commitLane.js';
38
37
  import type { Database } from './Database.js';
39
- import type { BootstrapData } from './sync/BootstrapFetcher.js';
40
38
  import type { MutationPersistencePort } from './mutationPersistence.js';
41
39
  import type { WriteOptions } from './interfaces/index.js';
42
40
  import { LogPosition } from './logPosition.js';
43
41
  import { createLocalMutationPort } from './transactions/localMutation.js';
44
42
  import { createReconnectDrain } from './transactions/reconnectDrain.js';
45
43
  import { DatabaseCommitOutboxStore } from './transactions/databaseCommitOutbox.js';
46
-
47
- interface SyncObserver {
48
- onSync?: (event: SyncEvent) => void;
49
- }
50
-
51
- interface SyncEvent {
52
- type: 'create' | 'update' | 'delete' | 'archive' | 'rollback';
53
- modelType: string;
54
- model?: Model;
55
- modelId?: string;
56
- transactionType?: string; // Original transaction type that was rolled back
57
- }
58
-
59
- interface SyncState {
60
- connectionState: 'connected' | 'disconnected' | 'connecting';
61
- pendingMutations: number;
62
- lastSyncAt?: Date;
63
- error?: Error;
64
- }
65
-
66
- export interface RehydrationStats {
67
- added: number;
68
- updated: number;
69
- removed: number;
70
- skipped: number;
71
- healed: number;
72
- elapsedMs: number;
73
- }
74
-
75
- type EventHandler = () => void;
76
-
77
- /**
78
- * The slice of a bootstrap answer the pool applies: its rows, the models whose
79
- * server query failed, and the log position the snapshot was taken at — the
80
- * position every row in it reflects. `lastSyncId` is optional only for callers
81
- * applying rows with no snapshot position to speak of; the fetcher always
82
- * names one.
83
- */
84
- export type BootstrapSnapshot = Pick<BootstrapData, 'models' | 'failedModels'> &
85
- Partial<Pick<BootstrapData, 'lastSyncId'>>;
86
-
87
- /**
88
- * What `transaction:completed` carries: a model mutation (one row, confirmed
89
- * at `syncIdNeededForCompletion`) or an explicit commit (one row per operation,
90
- * confirmed at `lastSyncId`). Each arm projects its own queue record.
91
- */
92
- type CompletedTransaction =
93
- | (Pick<QueuedMutation, 'id' | 'modelId' | 'syncIdNeededForCompletion'> & {
94
- lastSyncId?: undefined;
95
- operations?: undefined;
96
- })
97
- | (Pick<CommitTransaction, 'id' | 'lastSyncId' | 'operations'> & {
98
- modelId?: undefined;
99
- syncIdNeededForCompletion?: undefined;
100
- });
101
-
102
- /**
103
- * Converts an untyped server `updatedAt` value — an ISO string, epoch number,
104
- * or Date read off an untyped row — into epoch milliseconds for
105
- * last-write-wins comparison. Falsy or non-date values become 0, matching the
106
- * conflict resolver's rule that a missing timestamp sorts as the epoch.
107
- */
108
- function toEpochMs(value: unknown): number {
109
- if (!value) return 0;
110
- if (value instanceof Date) return value.getTime();
111
- if (typeof value === 'string' || typeof value === 'number') {
112
- return new Date(value).getTime();
113
- }
114
- return 0;
115
- }
44
+ import {
45
+ toEpochMs,
46
+ type CompletedTransaction,
47
+ type EventHandler,
48
+ type RehydrationStats,
49
+ type SyncEvent,
50
+ type SyncObserver,
51
+ type SyncState,
52
+ } from './syncClientTypes.js';
53
+ import type { BootstrapSnapshot } from './syncClientTypes.js';
54
+
55
+ export type { BootstrapSnapshot, RehydrationStats } from './syncClientTypes.js';
116
56
 
117
57
  export class SyncClient extends EventEmitter {
118
58
  private objectPool: InstanceCache;