@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.
- package/dist/local/Model.js +1 -1
- package/dist/local/SyncClient.d.ts +3 -34
- package/dist/local/SyncClient.js +1 -16
- package/dist/local/client/createModelProxy.d.ts +13 -1
- package/dist/local/client/createModelProxy.js +159 -80
- package/dist/local/client/reactiveEngine.js +5 -1
- package/dist/local/client/wsMutationExecutor.js +1 -0
- package/dist/local/syncClientTypes.d.ts +41 -0
- package/dist/local/syncClientTypes.js +11 -0
- package/dist/local/transactions/mutations/MutationQueue.d.ts +2 -0
- package/dist/local/transactions/mutations/MutationQueue.js +54 -69
- package/dist/local/transactions/mutations/commitLane.d.ts +4 -1
- package/dist/local/transactions/mutations/commitLane.js +12 -3
- package/dist/local/transactions/mutations/mutationInput.d.ts +40 -0
- package/dist/local/transactions/mutations/mutationInput.js +53 -0
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/package.json +2 -2
- package/src/local/Database.ts +0 -1
- package/src/local/Model.ts +1 -1
- package/src/local/SyncClient.ts +12 -72
- package/src/local/client/createModelProxy.ts +164 -13
- package/src/local/client/reactiveEngine.ts +5 -1
- package/src/local/client/wsMutationExecutor.ts +1 -0
- package/src/local/syncClientTypes.ts +59 -0
- package/src/local/transactions/mutations/MutationQueue.ts +61 -88
- package/src/local/transactions/mutations/commitLane.ts +23 -5
- package/src/local/transactions/mutations/mutationInput.ts +69 -0
- package/src/surface.ts +1 -0
|
@@ -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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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
|
|
1879
|
+
return mergeMutationData(local, remote);
|
|
1891
1880
|
}
|
|
1892
1881
|
|
|
1893
1882
|
private extractCreateData(model: LocalModel): MutationInput {
|
|
1894
|
-
return
|
|
1883
|
+
return createDataFor(model, this.runtime);
|
|
1895
1884
|
}
|
|
1896
1885
|
|
|
1897
1886
|
private mapChangesToInput(modelName: string, changes: Record<string, unknown>): MutationInput {
|
|
1898
|
-
return
|
|
1887
|
+
return changesToInput(modelName, changes, this.runtime);
|
|
1899
1888
|
}
|
|
1900
1889
|
|
|
1901
1890
|
private extractUpdateData(model: LocalModel): MutationInput {
|
|
1902
|
-
return
|
|
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
|
-
|
|
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 {
|
|
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<{
|
|
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) => {
|
|
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
|
+
}
|