@abloatai/ablo 0.35.0 → 0.36.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/CHANGELOG.md +713 -629
- package/README.md +56 -519
- package/bin/ablo.cjs +39 -0
- package/dist/BaseSyncedStore.d.ts +24 -4
- package/dist/BaseSyncedStore.js +53 -37
- package/dist/Database.d.ts +8 -20
- package/dist/Database.js +61 -59
- package/dist/InstanceCache.d.ts +6 -2
- package/dist/InstanceCache.js +18 -16
- package/dist/Model.d.ts +17 -7
- package/dist/Model.js +17 -7
- package/dist/ModelRegistry.d.ts +4 -0
- package/dist/ModelRegistry.js +17 -15
- package/dist/NetworkMonitor.d.ts +3 -1
- package/dist/NetworkMonitor.js +7 -5
- package/dist/SyncClient.d.ts +5 -15
- package/dist/SyncClient.js +60 -57
- package/dist/client/Ablo.js +30 -19
- package/dist/client/createInternalComponents.d.ts +4 -0
- package/dist/client/createInternalComponents.js +8 -2
- package/dist/client/createModelProxy.d.ts +21 -1
- package/dist/client/createModelProxy.js +123 -57
- package/dist/client/humans.d.ts +30 -9
- package/dist/client/humans.js +45 -19
- package/dist/client/reactiveEngine.d.ts +16 -11
- package/dist/client/reactiveEngine.js +113 -335
- package/dist/client/storeCluster.d.ts +47 -0
- package/dist/client/storeCluster.js +118 -0
- package/dist/client/storeLifecycle.d.ts +61 -0
- package/dist/client/storeLifecycle.js +231 -0
- package/dist/context.d.ts +13 -0
- package/dist/context.js +23 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/docs/catalog.js +6 -3
- package/dist/index.d.ts +4 -2
- package/dist/index.js +4 -2
- package/dist/query/client.d.ts +3 -0
- package/dist/query/client.js +6 -5
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +5 -2
- package/dist/react/context.d.ts +2 -2
- package/dist/react/createAbloReact.d.ts +56 -0
- package/dist/react/createAbloReact.js +51 -0
- package/dist/react/index.d.ts +1 -0
- package/dist/react/index.js +3 -0
- package/dist/react/useAblo.d.ts +9 -2
- package/dist/react/useAblo.js +25 -7
- package/dist/schema/coordination.js +5 -1
- package/dist/schema/index.d.ts +1 -0
- package/dist/schema/index.js +4 -0
- package/dist/schema/select.js +3 -0
- package/dist/schema/serialize.js +3 -0
- package/dist/source/adapter.d.ts +7 -5
- package/dist/source/adapter.js +7 -5
- package/dist/source/index.d.ts +1 -1
- package/dist/source/index.js +1 -1
- package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
- package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
- package/dist/{core → stores}/DatabaseManager.js +13 -12
- package/dist/{core → stores}/StoreManager.d.ts +5 -3
- package/dist/{core → stores}/StoreManager.js +24 -22
- package/dist/stores/SyncActionStore.d.ts +3 -1
- package/dist/stores/SyncActionStore.js +9 -7
- package/dist/sync/BootstrapFetcher.d.ts +4 -0
- package/dist/sync/BootstrapFetcher.js +28 -26
- package/dist/sync/OnDemandLoader.d.ts +3 -0
- package/dist/sync/OnDemandLoader.js +1 -0
- package/dist/sync/bootstrapApply.d.ts +3 -0
- package/dist/sync/bootstrapApply.js +2 -2
- package/dist/sync/deltaPipeline.d.ts +13 -12
- package/dist/sync/deltaPipeline.js +21 -4
- package/dist/sync/groupChange.d.ts +3 -0
- package/dist/sync/groupChange.js +16 -14
- package/dist/sync/participants.d.ts +19 -2
- package/dist/sync/participants.js +3 -1
- package/dist/sync/schemas.d.ts +2 -1
- package/dist/sync/schemas.js +3 -3
- package/dist/syncLog/contract.d.ts +20 -0
- package/dist/syncLog/contract.js +19 -0
- package/dist/syncLog/index.d.ts +1 -0
- package/dist/syncLog/index.js +1 -0
- package/dist/transaction/auth/capability.d.ts +35 -0
- package/dist/transaction/auth/capability.js +25 -0
- package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
- package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
- package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
- package/dist/transaction/coordination/index.d.ts +4 -4
- package/dist/transaction/coordination/index.js +4 -3
- package/dist/transaction/coordination/locator.d.ts +23 -2
- package/dist/transaction/coordination/locator.js +22 -2
- package/dist/transaction/coordination/schema.d.ts +125 -62
- package/dist/transaction/coordination/schema.js +228 -64
- package/dist/transaction/coordination/targetConflict.js +32 -28
- package/dist/transaction/errorCodes.d.ts +2 -2
- package/dist/transaction/errorCodes.js +13 -9
- package/dist/transaction/plugin.d.ts +95 -2
- package/dist/transaction/plugin.js +21 -2
- package/dist/transaction/resources/httpResources.d.ts +57 -2
- package/dist/transaction/resources/modelOperations.d.ts +148 -40
- package/dist/transaction/resources/where.d.ts +16 -0
- package/dist/transaction/resources/where.js +45 -0
- package/dist/transaction/schema/field.d.ts +12 -18
- package/dist/transaction/schema/fieldRef.d.ts +38 -0
- package/dist/transaction/schema/fieldRef.js +11 -0
- package/dist/transaction/schema/openapi.d.ts +16 -15
- package/dist/transaction/schema/openapi.js +186 -25
- package/dist/transaction/schema/relation.d.ts +7 -2
- package/dist/transaction/schema/schema.d.ts +27 -0
- package/dist/transaction/schema/schema.js +20 -0
- package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
- package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
- package/dist/transaction/transport/httpClient.d.ts +9 -1
- package/dist/transaction/transport/httpClient.js +1 -0
- package/dist/transaction/transport/httpTransport.js +156 -44
- package/dist/transaction/transport/wsTransport.d.ts +2 -4
- package/dist/transaction/transport/wsTransport.js +16 -10
- package/dist/transaction/types/streams.d.ts +10 -0
- package/dist/transaction/utils/duration.d.ts +25 -0
- package/dist/transaction/utils/duration.js +32 -0
- package/dist/transaction/wire/accountResponses.d.ts +69 -0
- package/dist/transaction/wire/accountResponses.js +36 -1
- package/dist/transaction/wire/auth.d.ts +9 -2
- package/dist/transaction/wire/auth.js +7 -1
- package/dist/transaction/wire/claims.d.ts +164 -97
- package/dist/transaction/wire/claims.js +126 -28
- package/dist/transaction/wire/commit.d.ts +1 -1
- package/dist/transaction/wire/feedEvent.d.ts +27 -0
- package/dist/transaction/wire/feedEvent.js +27 -1
- package/dist/transaction/wire/frames.d.ts +2 -2
- package/dist/transaction/wire/inboundFrames.d.ts +12 -2
- package/dist/transaction/wire/index.d.ts +10 -6
- package/dist/transaction/wire/index.js +12 -3
- package/dist/transaction/wire/modelMutations.d.ts +31 -0
- package/dist/transaction/wire/modelMutations.js +52 -0
- package/dist/transaction/wire/modelShape.d.ts +78 -0
- package/dist/transaction/wire/modelShape.js +74 -0
- package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
- package/dist/transactions/mutations/MutationQueue.js +55 -45
- package/dist/transactions/mutations/commitPayload.d.ts +3 -2
- package/dist/transactions/mutations/commitPayload.js +5 -5
- package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
- package/dist/transactions/mutations/deltaConfirmation.js +10 -8
- package/dist/transactions/mutations/replayValidation.d.ts +2 -1
- package/dist/transactions/mutations/replayValidation.js +3 -2
- package/dist/{core → views}/QueryView.d.ts +1 -1
- package/dist/{core → views}/QueryView.js +1 -1
- package/dist/{core → views}/ViewRegistry.d.ts +1 -1
- package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
- package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
- package/docs/agents.md +1 -1
- package/docs/api-keys.md +6 -6
- package/docs/api.md +5 -43
- package/docs/audit.md +4 -3
- package/docs/cli.md +11 -11
- package/docs/client-behavior.md +4 -4
- package/docs/concurrency-convention.md +28 -42
- package/docs/coordination.md +235 -83
- package/docs/data-sources.md +4 -4
- package/docs/debugging.md +34 -12
- package/docs/deployment.md +8 -8
- package/docs/examples/scoped-agent.md +3 -3
- package/docs/groups.md +57 -3
- package/docs/guarantees.md +37 -10
- package/docs/how-it-works.md +29 -5
- package/docs/idempotency.md +6 -6
- package/docs/identity.md +22 -23
- package/docs/index.md +8 -8
- package/docs/integration-guide.md +14 -3
- package/docs/mcp.md +7 -7
- package/docs/migration.md +34 -15
- package/docs/projects.md +1 -1
- package/docs/react.md +19 -8
- package/docs/sessions.md +1 -1
- package/docs/webhooks.md +9 -9
- package/llms.txt +4 -4
- package/package.json +13 -20
- package/dist/cli.cjs +0 -288600
- package/dist/testing/fixtures/bootstrap.d.ts +0 -49
- package/dist/testing/fixtures/bootstrap.js +0 -59
- package/dist/testing/fixtures/deltas.d.ts +0 -83
- package/dist/testing/fixtures/deltas.js +0 -136
- package/dist/testing/fixtures/httpResponses.d.ts +0 -70
- package/dist/testing/fixtures/httpResponses.js +0 -90
- package/dist/testing/fixtures/models.d.ts +0 -83
- package/dist/testing/fixtures/models.js +0 -272
- package/dist/testing/helpers/reactWrapper.d.ts +0 -69
- package/dist/testing/helpers/reactWrapper.js +0 -67
- package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
- package/dist/testing/helpers/syncEngineHarness.js +0 -73
- package/dist/testing/helpers/wait.d.ts +0 -30
- package/dist/testing/helpers/wait.js +0 -49
- package/dist/testing/index.d.ts +0 -23
- package/dist/testing/index.js +0 -33
- package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
- package/dist/testing/mocks/FakeDatabase.js +0 -10
- package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
- package/dist/testing/mocks/MockMutationExecutor.js +0 -186
- package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
- package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
- package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
- package/dist/testing/mocks/MockSyncContext.js +0 -72
- package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
- package/dist/testing/mocks/MockSyncStore.js +0 -171
- package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
- package/dist/testing/mocks/MockWebSocket.js +0 -118
- package/docs/interaction-model.md +0 -99
- /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
- /package/dist/{core → query}/QueryProcessor.js +0 -0
- /package/dist/{core/storeContract.js → storeContract.js} +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
- /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
- /package/dist/{source → transaction}/footprint.d.ts +0 -0
- /package/dist/{source → transaction}/footprint.js +0 -0
- /package/dist/{core → views}/ViewRegistry.js +0 -0
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
import { EventEmitter } from 'events';
|
|
15
15
|
import { v4 as uuid } from 'uuid';
|
|
16
16
|
import { Model } from '../../Model.js';
|
|
17
|
-
import {
|
|
17
|
+
import { globalRuntime } from '../../context.js';
|
|
18
18
|
import { AbloError, AbloConnectionError, AbloIdempotencyError, AbloNotFoundError, AbloValidationError, errorCodeSpec, } from '../../transaction/errors.js';
|
|
19
19
|
import { LogPosition } from '../../transaction/logPosition.js';
|
|
20
20
|
import { mutationCommitResultSchema, } from '../../transaction/wire/commit.js';
|
|
@@ -51,8 +51,9 @@ export class MutationQueue extends EventEmitter {
|
|
|
51
51
|
// queue treats as transient and retries endlessly.
|
|
52
52
|
_mutationExecutor = null;
|
|
53
53
|
get mutationExecutor() {
|
|
54
|
-
return this._mutationExecutor ??
|
|
54
|
+
return this._mutationExecutor ?? this.runtime.mutationExecutor;
|
|
55
55
|
}
|
|
56
|
+
runtime;
|
|
56
57
|
executionQueue = [];
|
|
57
58
|
isProcessing = false;
|
|
58
59
|
processTimer;
|
|
@@ -100,7 +101,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
100
101
|
this.emit(event, payload);
|
|
101
102
|
}
|
|
102
103
|
catch (error) {
|
|
103
|
-
|
|
104
|
+
this.runtime.observability.captureMutationFailure({
|
|
104
105
|
context: `commit-lifecycle-listener:${event}`,
|
|
105
106
|
error: error instanceof Error ? error : String(error),
|
|
106
107
|
});
|
|
@@ -121,12 +122,12 @@ export class MutationQueue extends EventEmitter {
|
|
|
121
122
|
// the caller — without this line the session degrades into "nothing
|
|
122
123
|
// saves and nothing errors". One loud line at the moment the block
|
|
123
124
|
// engages is the only visible trace.
|
|
124
|
-
|
|
125
|
+
this.runtime.logger.warn('sync paused: a saved write from an earlier session is older than the server replay window, so newer writes are held until it is reviewed', { sealedAt: envelope.sealedAt });
|
|
125
126
|
throw this.durableReplayBlock;
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
computePriorityScore(type, modelName) {
|
|
129
|
-
return computePriorityScore(type, modelName);
|
|
130
|
+
return computePriorityScore(type, modelName, this.runtime);
|
|
130
131
|
}
|
|
131
132
|
ensureDerivedFields(transaction) {
|
|
132
133
|
if (!transaction.modelKey) {
|
|
@@ -444,7 +445,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
444
445
|
await this.commitOutbox.remove(commitEnvelopeRecordId(idempotencyKey));
|
|
445
446
|
}
|
|
446
447
|
catch (error) {
|
|
447
|
-
|
|
448
|
+
this.runtime.logger.debug('[MutationQueue] Durable-write cleanup deferred', {
|
|
448
449
|
idempotencyKey,
|
|
449
450
|
error: error instanceof Error ? error.message : String(error),
|
|
450
451
|
});
|
|
@@ -659,6 +660,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
659
660
|
confirmationResolvers = new Map();
|
|
660
661
|
constructor(config) {
|
|
661
662
|
super();
|
|
663
|
+
this.runtime = config?.runtime ?? globalRuntime;
|
|
662
664
|
this.position = config?.position ?? new LogPosition();
|
|
663
665
|
// Bind the confirmation tracker to this queue's store/ledger/events.
|
|
664
666
|
// `isConnected` closes over `isConnectedFn` so `setConnectionChecker`
|
|
@@ -671,6 +673,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
671
673
|
},
|
|
672
674
|
isConnected: () => this.isConnectedFn(),
|
|
673
675
|
position: this.position,
|
|
676
|
+
runtime: this.runtime,
|
|
674
677
|
});
|
|
675
678
|
if (config) {
|
|
676
679
|
this.config = { ...this.config, ...config };
|
|
@@ -804,7 +807,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
804
807
|
return;
|
|
805
808
|
// Each failed commit reaches the consumer through its own rejection path,
|
|
806
809
|
// so this aggregate line is forensic and logged at debug rather than warn.
|
|
807
|
-
|
|
810
|
+
this.runtime.logger.debug(`[MutationQueue] WS disconnected > ${graceMs}ms; failing ${inFlight.length} in-flight commit(s) with AbloConnectionError`, { inFlightIds: inFlight.map((id) => id.slice(0, 8)) });
|
|
808
811
|
for (const id of inFlight) {
|
|
809
812
|
const tx = this.commitStore.get(id);
|
|
810
813
|
if (!tx)
|
|
@@ -877,7 +880,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
877
880
|
this.batchIndex++;
|
|
878
881
|
const currentBatchIndex = this.batchIndex;
|
|
879
882
|
// Log batch commit for performance monitoring
|
|
880
|
-
|
|
883
|
+
this.runtime.logger.debug('[MutationQueue] commitCreatedTransactions', {
|
|
881
884
|
count: this.createdTransactions.length,
|
|
882
885
|
batchIndex: currentBatchIndex,
|
|
883
886
|
types: this.createdTransactions.map((t) => `${t.type}:${t.modelName}`),
|
|
@@ -981,7 +984,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
981
984
|
this.optimisticUpdates.delete(tx.id);
|
|
982
985
|
}
|
|
983
986
|
}
|
|
984
|
-
|
|
987
|
+
this.runtime.logger.debug('txn:commit', 0, {
|
|
985
988
|
count: batch.length,
|
|
986
989
|
lastSyncId: result.lastSyncId,
|
|
987
990
|
});
|
|
@@ -990,13 +993,13 @@ export class MutationQueue extends EventEmitter {
|
|
|
990
993
|
catch (err) {
|
|
991
994
|
// If one request fails, hand it and every later request back to the
|
|
992
995
|
// normal lane. Their envelopes stay attached for safe retry.
|
|
993
|
-
const isOffline = !
|
|
996
|
+
const isOffline = !this.runtime.onlineStatus.isOnline();
|
|
994
997
|
const isNetworkError = err instanceof Error &&
|
|
995
998
|
(err.message.includes('Failed to fetch') ||
|
|
996
999
|
err.message.includes('Network request failed') ||
|
|
997
1000
|
err.message.includes('NetworkError'));
|
|
998
1001
|
if (!isOffline || !isNetworkError) {
|
|
999
|
-
|
|
1002
|
+
this.runtime.observability.breadcrumb('Batch flush fallback failed', 'sync.transaction', 'warning', {
|
|
1000
1003
|
error: err instanceof Error ? err.message : String(err),
|
|
1001
1004
|
});
|
|
1002
1005
|
}
|
|
@@ -1113,7 +1116,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1113
1116
|
const actualModelName = model.getModelName();
|
|
1114
1117
|
// Skip Activity delete transactions - activities are permanent audit records
|
|
1115
1118
|
if (actualModelName === 'Activity') {
|
|
1116
|
-
|
|
1119
|
+
this.runtime.logger.debug('MutationQueue.delete() skipping Activity deletion - permanent audit records', { modelId: model.id });
|
|
1117
1120
|
const modelKey = normalizeModelKey(actualModelName);
|
|
1118
1121
|
const priorityScore = this.computePriorityScore('delete', actualModelName);
|
|
1119
1122
|
const mockTransaction = {
|
|
@@ -1180,7 +1183,14 @@ export class MutationQueue extends EventEmitter {
|
|
|
1180
1183
|
transaction.sourceMutationIds = [
|
|
1181
1184
|
...new Set([
|
|
1182
1185
|
...(transaction.sourceMutationIds ?? []),
|
|
1183
|
-
|
|
1186
|
+
// Absorb journal sources only from updates that never sealed an
|
|
1187
|
+
// envelope. A sealed update's seal already CONSUMED its journal rows
|
|
1188
|
+
// — they belong to that envelope forever — so listing them here
|
|
1189
|
+
// poisons this delete's own seal: the new-envelope guard finds its
|
|
1190
|
+
// sources missing, rejects with `idempotency_conflict`, and the
|
|
1191
|
+
// optimistic delete reverts. That was a user-visible lost delete
|
|
1192
|
+
// (edit a layer, then delete it while the edit was on the wire).
|
|
1193
|
+
...canceledUpdates.flatMap((candidate) => candidate.commitEnvelope ? [] : (candidate.sourceMutationIds ?? [])),
|
|
1184
1194
|
...(pendingMerge?.sourceMutationIds ?? []),
|
|
1185
1195
|
]),
|
|
1186
1196
|
];
|
|
@@ -1352,7 +1362,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1352
1362
|
// are already executing, so the server is not flooded with concurrent
|
|
1353
1363
|
// requests.
|
|
1354
1364
|
if (this.executingCount >= this.config.maxExecutingTransactions) {
|
|
1355
|
-
|
|
1365
|
+
this.runtime.logger.debug('[MutationQueue] Backpressure: delaying batch, too many executing', {
|
|
1356
1366
|
executingCount: this.executingCount,
|
|
1357
1367
|
max: this.config.maxExecutingTransactions,
|
|
1358
1368
|
});
|
|
@@ -1393,7 +1403,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1393
1403
|
this.isProcessing = true;
|
|
1394
1404
|
// Declare batch outside try so it's accessible in finally for backpressure tracking
|
|
1395
1405
|
let batch = [];
|
|
1396
|
-
await
|
|
1406
|
+
await this.runtime.observability.startSpanAsync('sync.batch', 'sync.transaction.batch', async () => {
|
|
1397
1407
|
try {
|
|
1398
1408
|
// Sort the execution queue by foreign-key priority before selecting a
|
|
1399
1409
|
// batch, so a parent row is always committed before its children, even
|
|
@@ -1529,7 +1539,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1529
1539
|
}
|
|
1530
1540
|
this.store.updateStatus(tx.id, 'awaiting_delta');
|
|
1531
1541
|
this.scheduleReplicationLagTimeout(tx.id, commitIdempotencyKey, result.correlationId);
|
|
1532
|
-
|
|
1542
|
+
this.runtime.logger.debug('tx:awaiting_delta', {
|
|
1533
1543
|
txId: tx.id.slice(0, 8),
|
|
1534
1544
|
model: tx.modelName,
|
|
1535
1545
|
reason: 'queued_forward_waiting_for_correlated_echo',
|
|
@@ -1547,7 +1557,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1547
1557
|
// A lastSyncId of 0 means the mutation succeeded but the server
|
|
1548
1558
|
// emitted no sync delta; record that anomaly for observability.
|
|
1549
1559
|
if (lastSyncId === 0) {
|
|
1550
|
-
|
|
1560
|
+
this.runtime.observability.captureCommitZeroSyncId({
|
|
1551
1561
|
operationCount: operations.length,
|
|
1552
1562
|
operations: operations.map((op) => `${op.type}:${op.model}:${op.id?.slice(0, 8) ?? '?'}`),
|
|
1553
1563
|
});
|
|
@@ -1565,7 +1575,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1565
1575
|
this.emit('transaction:completed', tx);
|
|
1566
1576
|
this.emit(`transaction:completed:${tx.id}`, tx);
|
|
1567
1577
|
this.optimisticUpdates.delete(tx.id);
|
|
1568
|
-
|
|
1578
|
+
this.runtime.logger.debug('tx:confirm_delete_zero_syncid', {
|
|
1569
1579
|
txId: tx.id.slice(0, 8),
|
|
1570
1580
|
model: tx.modelName,
|
|
1571
1581
|
reason: 'delete_idempotent_no_delta',
|
|
@@ -1580,7 +1590,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1580
1590
|
this.emit('transaction:completed', tx);
|
|
1581
1591
|
this.emit(`transaction:completed:${tx.id}`, tx);
|
|
1582
1592
|
this.optimisticUpdates.delete(tx.id);
|
|
1583
|
-
|
|
1593
|
+
this.runtime.logger.debug('tx:confirm_ack', {
|
|
1584
1594
|
txId: tx.id.slice(0, 8),
|
|
1585
1595
|
model: tx.modelName,
|
|
1586
1596
|
serverSyncId: lastSyncId,
|
|
@@ -1589,7 +1599,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1589
1599
|
}
|
|
1590
1600
|
else {
|
|
1591
1601
|
this.store.updateStatus(tx.id, 'awaiting_delta');
|
|
1592
|
-
|
|
1602
|
+
this.runtime.logger.debug('tx:awaiting_delta', {
|
|
1593
1603
|
txId: tx.id.slice(0, 8),
|
|
1594
1604
|
model: tx.modelName,
|
|
1595
1605
|
neededSyncId: lastSyncId,
|
|
@@ -1636,7 +1646,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1636
1646
|
// authoritative `warn` with the same typed cause) — passes
|
|
1637
1647
|
// through here. Logging it at `warn` made one rejected write
|
|
1638
1648
|
// surface three identical dumps; keep it at `debug`.
|
|
1639
|
-
|
|
1649
|
+
this.runtime.logger.debug('[MutationQueue] Batch commit rejected', {
|
|
1640
1650
|
batchSize: batchOps.length,
|
|
1641
1651
|
models: batchOps.map(({ op }) => `${op.type}:${op.model}`),
|
|
1642
1652
|
errorType: abloErr?.type ?? error?.name,
|
|
@@ -1654,7 +1664,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1654
1664
|
if (dispatchStarted) {
|
|
1655
1665
|
await this.removeDurableCommit(commitIdempotencyKey);
|
|
1656
1666
|
}
|
|
1657
|
-
|
|
1667
|
+
this.runtime.logger.info('[MutationQueue] Graceful handling: entity already deleted', {
|
|
1658
1668
|
batchSize: batchOps.length,
|
|
1659
1669
|
});
|
|
1660
1670
|
for (const { tx, op } of batchOps) {
|
|
@@ -1662,7 +1672,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1662
1672
|
// Row already gone: the intended state holds, mark completed.
|
|
1663
1673
|
this.store.updateStatus(tx.id, 'completed');
|
|
1664
1674
|
this.emit('transaction:completed', tx);
|
|
1665
|
-
|
|
1675
|
+
this.runtime.logger.debug('[MutationQueue] Orphaned transaction treated as success', {
|
|
1666
1676
|
txId: tx.id.slice(0, 12),
|
|
1667
1677
|
model: tx.modelName,
|
|
1668
1678
|
type: op.type,
|
|
@@ -1722,7 +1732,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
1722
1732
|
this.scheduleProcessing(true);
|
|
1723
1733
|
}
|
|
1724
1734
|
const batchEnd = typeof performance !== 'undefined' ? performance.now() : Date.now();
|
|
1725
|
-
|
|
1735
|
+
this.runtime.logger.debug('txn:batch', batchEnd - batchStart, {
|
|
1726
1736
|
maxBatchSize: this.config.maxBatchSize,
|
|
1727
1737
|
remaining: this.executionQueue.length,
|
|
1728
1738
|
executingCount: this.executingCount,
|
|
@@ -2018,7 +2028,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
2018
2028
|
}
|
|
2019
2029
|
else {
|
|
2020
2030
|
this.scheduleReplicationLagTimeout(tx.id, tx.id, result.correlationId);
|
|
2021
|
-
|
|
2031
|
+
this.runtime.logger.debug('[MutationQueue] commit lane awaiting source echo', {
|
|
2022
2032
|
txId: tx.id.slice(0, 12),
|
|
2023
2033
|
});
|
|
2024
2034
|
}
|
|
@@ -2058,7 +2068,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
2058
2068
|
// (reconnect or the next enqueueCommit) rather than tight-looping
|
|
2059
2069
|
// while the connection is down.
|
|
2060
2070
|
tx.status = 'pending';
|
|
2061
|
-
|
|
2071
|
+
this.runtime.logger.debug('[MutationQueue] commit lane transient', {
|
|
2062
2072
|
txId: tx.id.slice(0, 12),
|
|
2063
2073
|
attempts: tx.attempts,
|
|
2064
2074
|
transientAttempts: tx.transientAttempts ?? 0,
|
|
@@ -2072,7 +2082,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
2072
2082
|
// Internal bookkeeping; the consumer-facing rejection is emitted on
|
|
2073
2083
|
// 'transaction:failed' and surfaced by the permanent-error headline,
|
|
2074
2084
|
// so this line stays at debug.
|
|
2075
|
-
|
|
2085
|
+
this.runtime.logger.debug('[MutationQueue] commit lane permanent error', {
|
|
2076
2086
|
txId: tx.id.slice(0, 12),
|
|
2077
2087
|
attempts: tx.attempts,
|
|
2078
2088
|
message: error.message,
|
|
@@ -2282,7 +2292,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
2282
2292
|
const sig = `${details.type}:${details.model}:${details.modelId}:${details.errorCode ?? details.errorType}`;
|
|
2283
2293
|
const isRepeat = sig === this.lastPermanentErrorSig;
|
|
2284
2294
|
this.lastPermanentErrorSig = sig;
|
|
2285
|
-
const logger =
|
|
2295
|
+
const logger = this.runtime.logger;
|
|
2286
2296
|
// Two registers from one call site, split by log level (the default
|
|
2287
2297
|
// logger is gated at `warn`, so `debug` stays hidden unless
|
|
2288
2298
|
// ABLO_LOG_LEVEL=debug is set to inspect the engine):
|
|
@@ -2440,7 +2450,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
2440
2450
|
}
|
|
2441
2451
|
}
|
|
2442
2452
|
catch (error) {
|
|
2443
|
-
|
|
2453
|
+
this.runtime.observability.captureMutationFailure({
|
|
2444
2454
|
context: 'load-persisted-transactions',
|
|
2445
2455
|
error: error instanceof Error ? error : String(error),
|
|
2446
2456
|
});
|
|
@@ -2470,8 +2480,8 @@ export class MutationQueue extends EventEmitter {
|
|
|
2470
2480
|
envelopes.push(parsed.data);
|
|
2471
2481
|
}
|
|
2472
2482
|
else {
|
|
2473
|
-
|
|
2474
|
-
|
|
2483
|
+
this.runtime.logger.warn('A saved local write is unreadable and was held for review.');
|
|
2484
|
+
this.runtime.observability.captureMutationFailure({
|
|
2475
2485
|
context: 'restore-commit-envelope',
|
|
2476
2486
|
error: parsed.error,
|
|
2477
2487
|
});
|
|
@@ -2488,8 +2498,8 @@ export class MutationQueue extends EventEmitter {
|
|
|
2488
2498
|
if (envelope.acceptedAt === undefined &&
|
|
2489
2499
|
Date.now() - envelope.sealedAt >=
|
|
2490
2500
|
MutationQueue.DURABLE_REPLAY_WINDOW_MS) {
|
|
2491
|
-
|
|
2492
|
-
|
|
2501
|
+
this.runtime.logger.warn('A saved local write is too old to retry safely and was held for review.');
|
|
2502
|
+
this.runtime.observability.captureMutationFailure({
|
|
2493
2503
|
context: 'quarantine-expired-commit-envelope',
|
|
2494
2504
|
error: `Envelope ${envelope.idempotencyKey} is too old to replay safely`,
|
|
2495
2505
|
});
|
|
@@ -2500,7 +2510,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
2500
2510
|
envelope.scope.organizationId !== this.commitOutboxScope.organizationId ||
|
|
2501
2511
|
envelope.scope.participantId !== this.commitOutboxScope.participantId ||
|
|
2502
2512
|
envelope.scope.namespace !== this.commitOutboxScope.namespace)) {
|
|
2503
|
-
|
|
2513
|
+
this.runtime.logger.warn('A saved local write belongs to a different account or server and was held for review.');
|
|
2504
2514
|
continue;
|
|
2505
2515
|
}
|
|
2506
2516
|
if (this.commitStore.has(envelope.idempotencyKey))
|
|
@@ -2533,10 +2543,10 @@ export class MutationQueue extends EventEmitter {
|
|
|
2533
2543
|
void this.processCommitLane();
|
|
2534
2544
|
}
|
|
2535
2545
|
catch (error) {
|
|
2536
|
-
|
|
2546
|
+
this.runtime.logger.debug('[MutationQueue] Failed to restore durable writes', {
|
|
2537
2547
|
error: error instanceof Error ? error.message : String(error),
|
|
2538
2548
|
});
|
|
2539
|
-
|
|
2549
|
+
this.runtime.observability.captureMutationFailure({
|
|
2540
2550
|
context: 'restore-commit-envelopes',
|
|
2541
2551
|
error: error instanceof Error ? error : String(error),
|
|
2542
2552
|
});
|
|
@@ -2553,15 +2563,15 @@ export class MutationQueue extends EventEmitter {
|
|
|
2553
2563
|
deserializeTransaction(data) {
|
|
2554
2564
|
if (isNonReplayablePersistedRow(data))
|
|
2555
2565
|
return null;
|
|
2556
|
-
const transaction = deserializePersistedTransaction(data);
|
|
2566
|
+
const transaction = deserializePersistedTransaction(data, this.runtime);
|
|
2557
2567
|
if (!transaction) {
|
|
2558
2568
|
const rowId = typeof data === 'object' && data !== null && typeof data.id === 'string'
|
|
2559
2569
|
? data.id
|
|
2560
2570
|
: undefined;
|
|
2561
|
-
|
|
2571
|
+
this.runtime.logger.debug('[MutationQueue] Dropping malformed persisted transaction', {
|
|
2562
2572
|
rowId,
|
|
2563
2573
|
});
|
|
2564
|
-
|
|
2574
|
+
this.runtime.observability.captureMutationFailure({
|
|
2565
2575
|
context: 'deserialize-persisted-transaction',
|
|
2566
2576
|
error: `Persisted transaction failed schema validation${rowId ? ` (id: ${rowId})` : ''}`,
|
|
2567
2577
|
});
|
|
@@ -2589,7 +2599,7 @@ export class MutationQueue extends EventEmitter {
|
|
|
2589
2599
|
// listener) must surface, not vanish — the status flip above is
|
|
2590
2600
|
// already committed either way.
|
|
2591
2601
|
void this.rollbackOptimistic(transaction, 'model_cancelled').catch((error) => {
|
|
2592
|
-
|
|
2602
|
+
this.runtime.observability.captureMutationFailure({
|
|
2593
2603
|
context: 'rollback-model-cancelled',
|
|
2594
2604
|
error: error instanceof Error ? error : String(error),
|
|
2595
2605
|
});
|
|
@@ -2623,13 +2633,13 @@ export class MutationQueue extends EventEmitter {
|
|
|
2623
2633
|
if (fkValue === parentId) {
|
|
2624
2634
|
this.store.updateStatus(transaction.id, 'rolled_back');
|
|
2625
2635
|
void this.rollbackOptimistic(transaction, 'cascade_parent_deleted').catch((error) => {
|
|
2626
|
-
|
|
2636
|
+
this.runtime.observability.captureMutationFailure({
|
|
2627
2637
|
context: 'rollback-cascade-parent-deleted',
|
|
2628
2638
|
error: error instanceof Error ? error : String(error),
|
|
2629
2639
|
});
|
|
2630
2640
|
});
|
|
2631
2641
|
cancelled++;
|
|
2632
|
-
|
|
2642
|
+
this.runtime.logger.debug('[MutationQueue] Cascade cancelled orphaned transaction', {
|
|
2633
2643
|
txId: transaction.id.slice(0, 12),
|
|
2634
2644
|
model: childModelName,
|
|
2635
2645
|
foreignKey,
|
|
@@ -2654,13 +2664,13 @@ export class MutationQueue extends EventEmitter {
|
|
|
2654
2664
|
return { ...(remote || {}), ...(local || {}) };
|
|
2655
2665
|
}
|
|
2656
2666
|
extractCreateData(model) {
|
|
2657
|
-
return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false });
|
|
2667
|
+
return projectCommitPayload(model.getModelName(), model.toJSON(), { dropUndefined: false }, this.runtime);
|
|
2658
2668
|
}
|
|
2659
2669
|
mapChangesToInput(modelName, changes) {
|
|
2660
|
-
return projectCommitPayload(modelName, changes, { dropUndefined: true });
|
|
2670
|
+
return projectCommitPayload(modelName, changes, { dropUndefined: true }, this.runtime);
|
|
2661
2671
|
}
|
|
2662
2672
|
extractUpdateData(model) {
|
|
2663
|
-
return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true });
|
|
2673
|
+
return projectCommitPayload(model.getModelName(), model.getChanges(), { dropUndefined: true }, this.runtime);
|
|
2664
2674
|
}
|
|
2665
2675
|
// Derive previous values for changed fields to support accurate rollback.
|
|
2666
2676
|
// Model-specific special cases do not belong here; a model that needs to
|
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* transport errors. Nothing here holds queue state or timers, so the batched
|
|
9
9
|
* queue path and the HTTP commit path share the same projection.
|
|
10
10
|
*/
|
|
11
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
11
12
|
import { MutationOperationType } from '../../transaction/types/index.js';
|
|
12
13
|
import type { OnStaleMode } from '../../transaction/coordination/schema.js';
|
|
13
14
|
import type { MutationOptions, WriteOptions } from '../../interfaces/index.js';
|
|
@@ -40,7 +41,7 @@ export type MutationInput = Record<string, unknown>;
|
|
|
40
41
|
*/
|
|
41
42
|
export declare function projectCommitPayload(modelName: string, source: Record<string, unknown>, opts: {
|
|
42
43
|
dropUndefined: boolean;
|
|
43
|
-
}): MutationInput;
|
|
44
|
+
}, runtime?: RuntimeContext): MutationInput;
|
|
44
45
|
export interface QueuedMutation {
|
|
45
46
|
id: string;
|
|
46
47
|
type: 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
@@ -118,7 +119,7 @@ export declare const stripModelSuffix: (modelName: string) => string;
|
|
|
118
119
|
* foreign-key ordering because the row already exists, so they all share the
|
|
119
120
|
* configured default non-create priority.
|
|
120
121
|
*/
|
|
121
|
-
export declare const computePriorityScore: (type: QueuedMutation["type"], modelName: string) => number;
|
|
122
|
+
export declare const computePriorityScore: (type: QueuedMutation["type"], modelName: string, runtime?: RuntimeContext) => number;
|
|
122
123
|
export declare const TX_TYPE_TO_MUTATION_OP: Record<QueuedMutation['type'], MutationOperationType>;
|
|
123
124
|
export declare function hasStaleWriteOptions(options?: WriteOptions): boolean;
|
|
124
125
|
/** Options whose identity/audit semantics forbid merging two caller writes. */
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* transport errors. Nothing here holds queue state or timers, so the batched
|
|
9
9
|
* queue path and the HTTP commit path share the same projection.
|
|
10
10
|
*/
|
|
11
|
-
import {
|
|
11
|
+
import { globalRuntime } from '../../context.js';
|
|
12
12
|
import { MutationOperationType } from '../../transaction/types/index.js';
|
|
13
13
|
import { snapshotJsonValue } from '../../transaction/utils/json.js';
|
|
14
14
|
/**
|
|
@@ -35,8 +35,8 @@ const FRAMEWORK_KEYS = new Set(['__class', '__typename', 'clientId', 'syncStatus
|
|
|
35
35
|
* for example a manually registered model — projection snapshots all serialized
|
|
36
36
|
* data apart from the framework keys.
|
|
37
37
|
*/
|
|
38
|
-
export function projectCommitPayload(modelName, source, opts) {
|
|
39
|
-
const metadata =
|
|
38
|
+
export function projectCommitPayload(modelName, source, opts, runtime = globalRuntime) {
|
|
39
|
+
const metadata = runtime.getModelMetadata(modelName);
|
|
40
40
|
const fields = metadata?.fields;
|
|
41
41
|
const out = {};
|
|
42
42
|
if (!fields) {
|
|
@@ -90,8 +90,8 @@ export const stripModelSuffix = (modelName) => modelName.replace('Model', '');
|
|
|
90
90
|
* foreign-key ordering because the row already exists, so they all share the
|
|
91
91
|
* configured default non-create priority.
|
|
92
92
|
*/
|
|
93
|
-
export const computePriorityScore = (type, modelName) => {
|
|
94
|
-
const { modelCreatePriority, defaultCreatePriority, defaultNonCreatePriority } =
|
|
93
|
+
export const computePriorityScore = (type, modelName, runtime = globalRuntime) => {
|
|
94
|
+
const { modelCreatePriority, defaultCreatePriority, defaultNonCreatePriority } = runtime.config;
|
|
95
95
|
if (type !== 'create')
|
|
96
96
|
return defaultNonCreatePriority;
|
|
97
97
|
return modelCreatePriority.get(modelName) ?? defaultCreatePriority;
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
* {@link DeltaConfirmationContext} interface, not the queue class itself, so it
|
|
8
8
|
* has no cyclic dependency and can be tested on its own.
|
|
9
9
|
*/
|
|
10
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
10
11
|
import type { LogPosition } from '../../transaction/logPosition.js';
|
|
11
12
|
import type { QueuedMutation } from './commitPayload.js';
|
|
12
13
|
/**
|
|
@@ -30,6 +31,8 @@ export interface DeltaConfirmationContext {
|
|
|
30
31
|
emit(event: string, payload?: unknown): void;
|
|
31
32
|
isConnected(): boolean;
|
|
32
33
|
position: LogPosition;
|
|
34
|
+
/** The owning client's runtime. Defaults to the module-global bridge. */
|
|
35
|
+
runtime?: RuntimeContext;
|
|
33
36
|
}
|
|
34
37
|
export declare class DeltaConfirmationTracker {
|
|
35
38
|
private readonly ctx;
|
|
@@ -37,6 +40,7 @@ export declare class DeltaConfirmationTracker {
|
|
|
37
40
|
private static readonly DELTA_MAX_TIMEOUT_MS;
|
|
38
41
|
private deltaConfirmationTimeouts;
|
|
39
42
|
private deltaConfirmationRetries;
|
|
43
|
+
private readonly runtime;
|
|
40
44
|
constructor(ctx: DeltaConfirmationContext);
|
|
41
45
|
/** Applied-cursor alias, kept so the read sites below stay legible. */
|
|
42
46
|
private get lastSeenSyncId();
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* {@link DeltaConfirmationContext} interface, not the queue class itself, so it
|
|
8
8
|
* has no cyclic dependency and can be tested on its own.
|
|
9
9
|
*/
|
|
10
|
-
import {
|
|
10
|
+
import { globalRuntime } from '../../context.js';
|
|
11
11
|
export class DeltaConfirmationTracker {
|
|
12
12
|
ctx;
|
|
13
13
|
// Retry configuration for delta confirmation, using exponential backoff.
|
|
@@ -20,8 +20,10 @@ export class DeltaConfirmationTracker {
|
|
|
20
20
|
deltaConfirmationTimeouts = new Map();
|
|
21
21
|
// Track retry attempts per transaction for exponential backoff
|
|
22
22
|
deltaConfirmationRetries = new Map();
|
|
23
|
+
runtime;
|
|
23
24
|
constructor(ctx) {
|
|
24
25
|
this.ctx = ctx;
|
|
26
|
+
this.runtime = ctx.runtime ?? globalRuntime;
|
|
25
27
|
}
|
|
26
28
|
/** Applied-cursor alias, kept so the read sites below stay legible. */
|
|
27
29
|
get lastSeenSyncId() {
|
|
@@ -48,7 +50,7 @@ export class DeltaConfirmationTracker {
|
|
|
48
50
|
const executingTxs = this.ctx.store.getByStatus('executing');
|
|
49
51
|
// Debug: Show state when delta arrives
|
|
50
52
|
if (awaitingTxs.length > 0 || executingTxs.length > 0) {
|
|
51
|
-
|
|
53
|
+
this.runtime.logger.debug('tx:delta_received', {
|
|
52
54
|
syncId,
|
|
53
55
|
lastSeenSyncId: this.lastSeenSyncId,
|
|
54
56
|
awaitingCount: awaitingTxs.length,
|
|
@@ -86,7 +88,7 @@ export class DeltaConfirmationTracker {
|
|
|
86
88
|
this.ctx.emit(`transaction:completed:${tx.id}`, tx);
|
|
87
89
|
this.ctx.optimisticUpdates.delete(tx.id);
|
|
88
90
|
confirmedCount++;
|
|
89
|
-
|
|
91
|
+
this.runtime.logger.debug('tx:confirm_via_delta', {
|
|
90
92
|
txId: tx.id.slice(0, 8),
|
|
91
93
|
model: tx.modelName,
|
|
92
94
|
neededSyncId: tx.syncIdNeededForCompletion,
|
|
@@ -98,7 +100,7 @@ export class DeltaConfirmationTracker {
|
|
|
98
100
|
// Log batch summary only if we confirmed something
|
|
99
101
|
if (confirmedCount > 0) {
|
|
100
102
|
// Leave a breadcrumb when transactions confirm.
|
|
101
|
-
|
|
103
|
+
this.runtime.observability.breadcrumb('Transactions confirmed via delta', 'sync.transaction', 'info', {
|
|
102
104
|
count: confirmedCount,
|
|
103
105
|
syncId,
|
|
104
106
|
remainingAwaiting: awaitingTxs.length - confirmedCount,
|
|
@@ -126,7 +128,7 @@ export class DeltaConfirmationTracker {
|
|
|
126
128
|
if (!this.ctx.isConnected()) {
|
|
127
129
|
// Self-healing: re-schedule the confirmation wait while offline, no
|
|
128
130
|
// consumer action needed → debug.
|
|
129
|
-
|
|
131
|
+
this.runtime.logger.debug('[MutationQueue] Timeout fired while disconnected - re-scheduling', {
|
|
130
132
|
txId: tx.id.slice(0, 8),
|
|
131
133
|
model: tx.modelName,
|
|
132
134
|
});
|
|
@@ -135,7 +137,7 @@ export class DeltaConfirmationTracker {
|
|
|
135
137
|
return;
|
|
136
138
|
}
|
|
137
139
|
const retryCount = this.deltaConfirmationRetries.get(tx.id) ?? 0;
|
|
138
|
-
|
|
140
|
+
this.runtime.observability.captureReconciliation({
|
|
139
141
|
reason: 'delta_timeout',
|
|
140
142
|
model: tx.modelName,
|
|
141
143
|
modelId: tx.modelId,
|
|
@@ -165,7 +167,7 @@ export class DeltaConfirmationTracker {
|
|
|
165
167
|
});
|
|
166
168
|
// Self-healing retry with backoff — the server already committed; we're
|
|
167
169
|
// just waiting on the delta. No consumer action → debug.
|
|
168
|
-
|
|
170
|
+
this.runtime.logger.debug('[MutationQueue] Re-scheduling with backoff', {
|
|
169
171
|
txId: tx.id.slice(0, 8),
|
|
170
172
|
model: tx.modelName,
|
|
171
173
|
nextTimeoutMs: nextTimeout,
|
|
@@ -180,7 +182,7 @@ export class DeltaConfirmationTracker {
|
|
|
180
182
|
// session, reconnecting and catching up on deltas will confirm it.
|
|
181
183
|
this.deltaConfirmationRetries.delete(tx.id);
|
|
182
184
|
this.deltaConfirmationTimeouts.delete(tx.id);
|
|
183
|
-
|
|
185
|
+
this.runtime.observability.captureDeltaRetryExhausted({
|
|
184
186
|
txId: tx.id,
|
|
185
187
|
model: tx.modelName,
|
|
186
188
|
modelId: tx.modelId,
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
import type { QueuedMutation } from './commitPayload.js';
|
|
19
|
+
import type { RuntimeContext } from '../../RuntimeContext.js';
|
|
19
20
|
/**
|
|
20
21
|
* The shape of a persisted transaction that can be replayed: the fields the
|
|
21
22
|
* transaction queue reads when it re-enqueues the row — its id, operation type,
|
|
@@ -78,7 +79,7 @@ export declare function isNonReplayablePersistedRow(row: unknown): boolean;
|
|
|
78
79
|
* fields the stored row lacks — status, attempts, priority, timestamp — are
|
|
79
80
|
* re-derived the same way a freshly staged transaction derives them.
|
|
80
81
|
*/
|
|
81
|
-
export declare function deserializePersistedTransaction(row: unknown): QueuedMutation | null;
|
|
82
|
+
export declare function deserializePersistedTransaction(row: unknown, runtime?: RuntimeContext): QueuedMutation | null;
|
|
82
83
|
/**
|
|
83
84
|
* The shape of one entry in the persisted offline mutation queue — an item of
|
|
84
85
|
* the `'queue'` row's `mutations` array, carrying the fields read when the
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { z } from 'zod';
|
|
18
18
|
import { computePriorityScore, normalizeModelKey } from './commitPayload.js';
|
|
19
|
+
import { globalRuntime } from '../../context.js';
|
|
19
20
|
import { commitEnvelopeMemberSchema, commitOutboxScopeSchema, } from '../../transaction/transactions/settlement/commitEnvelope.js';
|
|
20
21
|
import { onStaleModeSchema } from '../../transaction/coordination/schema.js';
|
|
21
22
|
/** The subset of a write's options that is stored with each transaction or queued mutation. */
|
|
@@ -87,7 +88,7 @@ export function isNonReplayablePersistedRow(row) {
|
|
|
87
88
|
* fields the stored row lacks — status, attempts, priority, timestamp — are
|
|
88
89
|
* re-derived the same way a freshly staged transaction derives them.
|
|
89
90
|
*/
|
|
90
|
-
export function deserializePersistedTransaction(row) {
|
|
91
|
+
export function deserializePersistedTransaction(row, runtime = globalRuntime) {
|
|
91
92
|
const parsed = persistedTransactionSchema.safeParse(row);
|
|
92
93
|
if (!parsed.success)
|
|
93
94
|
return null;
|
|
@@ -105,7 +106,7 @@ export function deserializePersistedTransaction(row) {
|
|
|
105
106
|
createdAt: tx.createdAt ?? Date.now(),
|
|
106
107
|
attempts: 0,
|
|
107
108
|
priority: 'normal',
|
|
108
|
-
priorityScore: computePriorityScore(tx.type, tx.modelName),
|
|
109
|
+
priorityScore: computePriorityScore(tx.type, tx.modelName, runtime),
|
|
109
110
|
...(tx.batchId !== undefined ? { batchId: tx.batchId } : {}),
|
|
110
111
|
...(tx.commitEnvelope !== undefined
|
|
111
112
|
? { commitEnvelope: tx.commitEnvelope }
|
|
@@ -12,7 +12,7 @@ import { type IObservableArray } from 'mobx';
|
|
|
12
12
|
import { type Model } from '../Model.js';
|
|
13
13
|
import { ModelScope } from '../transaction/types/index.js';
|
|
14
14
|
import type { ViewRegistry } from './ViewRegistry.js';
|
|
15
|
-
import type { IncrementalView } from './
|
|
15
|
+
import type { IncrementalView } from './incrementalView.js';
|
|
16
16
|
/**
|
|
17
17
|
* The slice of the object pool that a {@link QueryView} reads. It is a minimal
|
|
18
18
|
* structural interface rather than the concrete pool class, which lets a query
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
import { observable, runInAction } from 'mobx';
|
|
12
12
|
import { modelAsRow } from '../Model.js';
|
|
13
13
|
import { ModelScope } from '../transaction/types/index.js';
|
|
14
|
-
import { compareValues, binaryInsertionIndex, findIndexById, } from './
|
|
14
|
+
import { compareValues, binaryInsertionIndex, findIndexById, } from './incrementalView.js';
|
|
15
15
|
// ---------------------------------------------------------------------------
|
|
16
16
|
// QueryView
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* QueryView subscribed to that typename.
|
|
7
7
|
*/
|
|
8
8
|
import { type Model } from '../Model.js';
|
|
9
|
-
import type { IncrementalView } from './
|
|
9
|
+
import type { IncrementalView } from './incrementalView.js';
|
|
10
10
|
export declare class ViewRegistry {
|
|
11
11
|
private views;
|
|
12
12
|
register(typename: string, view: IncrementalView): void;
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* ensures every view sorts and filters identically. The functions
|
|
6
|
-
* plain arrays and values — they hold no reference to models,
|
|
7
|
-
* reactivity system.
|
|
2
|
+
* The {@link IncrementalView} contract — the interface a live view implements
|
|
3
|
+
* to receive add/update/remove notifications — together with the sorting,
|
|
4
|
+
* matching, and binary-insertion rules every view shares. Keeping the rules in
|
|
5
|
+
* one place ensures every view sorts and filters identically. The functions
|
|
6
|
+
* here work on plain arrays and values — they hold no reference to models,
|
|
7
|
+
* pools, or the reactivity system.
|
|
8
8
|
*/
|
|
9
9
|
/**
|
|
10
10
|
* The interface a live view implements to receive incremental updates: one call
|