@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.
- package/dist/local/Database.js +2 -2
- 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/options.d.ts +7 -4
- package/dist/local/client/reactiveEngine.js +5 -1
- package/dist/local/client/wsMutationExecutor.js +1 -0
- package/dist/local/logPosition.d.ts +13 -2
- package/dist/local/logPosition.js +17 -5
- package/dist/local/sync/SyncWebSocket.js +16 -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 +2 -3
- 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/options.ts +7 -4
- package/src/local/client/reactiveEngine.ts +5 -1
- package/src/local/client/wsMutationExecutor.ts +1 -0
- package/src/local/logPosition.ts +19 -6
- package/src/local/sync/SyncWebSocket.ts +15 -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
|
@@ -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 {
|
|
17
|
+
import { AbloConnectionError, AbloIdempotencyError, } from '@abloatai/transaction/errors';
|
|
18
18
|
import { LogPosition, } from '../../logPosition.js';
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
|
|
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,
|
|
23
|
+
import { entityKey, takeUnsentCreateForModel, findCreateBarrierForDelete, deferDeleteUntilCreateSettles, releaseDeferredDeletesForCreate, } from './coalesceRules.js';
|
|
25
24
|
import { DeltaConfirmationTracker } from './deltaConfirmation.js';
|
|
26
|
-
import { deserializePersistedTransaction, isNonReplayablePersistedRow, pendingMutationRecordId,
|
|
25
|
+
import { deserializePersistedTransaction, isNonReplayablePersistedRow, pendingMutationRecordId, } from './replayValidation.js';
|
|
27
26
|
import { deserializeLegacyPendingMutation, loadPersistedTransactions, persistQueuedTransaction, removePersistedTransaction, settlePersistedFailure, } from './mutationPersistence.js';
|
|
28
|
-
import { createCommitEnvelopeMember,
|
|
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
|
|
1395
|
+
return generateTransactionId();
|
|
1395
1396
|
}
|
|
1396
1397
|
mergeData(local, remote) {
|
|
1397
|
-
return
|
|
1398
|
+
return mergeMutationData(local, remote);
|
|
1398
1399
|
}
|
|
1399
1400
|
extractCreateData(model) {
|
|
1400
|
-
return
|
|
1401
|
+
return createDataFor(model, this.runtime);
|
|
1401
1402
|
}
|
|
1402
1403
|
mapChangesToInput(modelName, changes) {
|
|
1403
|
-
return
|
|
1404
|
+
return changesToInput(modelName, changes, this.runtime);
|
|
1404
1405
|
}
|
|
1405
1406
|
extractUpdateData(model) {
|
|
1406
|
-
return
|
|
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
|
-
|
|
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) => {
|
|
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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/humans",
|
|
3
|
-
"version": "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.
|
|
87
|
+
"@abloatai/transaction": "^0.57.0",
|
|
88
88
|
"mobx": "^6.13.7",
|
|
89
89
|
"uuid": "^11.1.0",
|
|
90
90
|
"zod": "^4.4.3"
|
package/src/local/Database.ts
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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 ─────────────────────────────────────
|
package/src/local/Model.ts
CHANGED
|
@@ -1106,7 +1106,7 @@ export abstract class Model {
|
|
|
1106
1106
|
}
|
|
1107
1107
|
|
|
1108
1108
|
// Try to get model class by identifier
|
|
1109
|
-
|
|
1109
|
+
const ModelClass = getActiveRegistry().getModelByName(modelIdentifier);
|
|
1110
1110
|
|
|
1111
1111
|
if (!ModelClass) {
|
|
1112
1112
|
throw new AbloValidationError(
|
package/src/local/SyncClient.ts
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
type
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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;
|