@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.
Files changed (215) hide show
  1. package/CHANGELOG.md +713 -629
  2. package/README.md +56 -519
  3. package/bin/ablo.cjs +39 -0
  4. package/dist/BaseSyncedStore.d.ts +24 -4
  5. package/dist/BaseSyncedStore.js +53 -37
  6. package/dist/Database.d.ts +8 -20
  7. package/dist/Database.js +61 -59
  8. package/dist/InstanceCache.d.ts +6 -2
  9. package/dist/InstanceCache.js +18 -16
  10. package/dist/Model.d.ts +17 -7
  11. package/dist/Model.js +17 -7
  12. package/dist/ModelRegistry.d.ts +4 -0
  13. package/dist/ModelRegistry.js +17 -15
  14. package/dist/NetworkMonitor.d.ts +3 -1
  15. package/dist/NetworkMonitor.js +7 -5
  16. package/dist/SyncClient.d.ts +5 -15
  17. package/dist/SyncClient.js +60 -57
  18. package/dist/client/Ablo.js +30 -19
  19. package/dist/client/createInternalComponents.d.ts +4 -0
  20. package/dist/client/createInternalComponents.js +8 -2
  21. package/dist/client/createModelProxy.d.ts +21 -1
  22. package/dist/client/createModelProxy.js +123 -57
  23. package/dist/client/humans.d.ts +30 -9
  24. package/dist/client/humans.js +45 -19
  25. package/dist/client/reactiveEngine.d.ts +16 -11
  26. package/dist/client/reactiveEngine.js +113 -335
  27. package/dist/client/storeCluster.d.ts +47 -0
  28. package/dist/client/storeCluster.js +118 -0
  29. package/dist/client/storeLifecycle.d.ts +61 -0
  30. package/dist/client/storeLifecycle.js +231 -0
  31. package/dist/context.d.ts +13 -0
  32. package/dist/context.js +23 -0
  33. package/dist/core/index.d.ts +1 -1
  34. package/dist/docs/catalog.js +6 -3
  35. package/dist/index.d.ts +4 -2
  36. package/dist/index.js +4 -2
  37. package/dist/query/client.d.ts +3 -0
  38. package/dist/query/client.js +6 -5
  39. package/dist/react/AbloProvider.d.ts +13 -1
  40. package/dist/react/AbloProvider.js +5 -2
  41. package/dist/react/context.d.ts +2 -2
  42. package/dist/react/createAbloReact.d.ts +56 -0
  43. package/dist/react/createAbloReact.js +51 -0
  44. package/dist/react/index.d.ts +1 -0
  45. package/dist/react/index.js +3 -0
  46. package/dist/react/useAblo.d.ts +9 -2
  47. package/dist/react/useAblo.js +25 -7
  48. package/dist/schema/coordination.js +5 -1
  49. package/dist/schema/index.d.ts +1 -0
  50. package/dist/schema/index.js +4 -0
  51. package/dist/schema/select.js +3 -0
  52. package/dist/schema/serialize.js +3 -0
  53. package/dist/source/adapter.d.ts +7 -5
  54. package/dist/source/adapter.js +7 -5
  55. package/dist/source/index.d.ts +1 -1
  56. package/dist/source/index.js +1 -1
  57. package/dist/{core/storeContract.d.ts → storeContract.d.ts} +5 -5
  58. package/dist/{core → stores}/DatabaseManager.d.ts +3 -1
  59. package/dist/{core → stores}/DatabaseManager.js +13 -12
  60. package/dist/{core → stores}/StoreManager.d.ts +5 -3
  61. package/dist/{core → stores}/StoreManager.js +24 -22
  62. package/dist/stores/SyncActionStore.d.ts +3 -1
  63. package/dist/stores/SyncActionStore.js +9 -7
  64. package/dist/sync/BootstrapFetcher.d.ts +4 -0
  65. package/dist/sync/BootstrapFetcher.js +28 -26
  66. package/dist/sync/OnDemandLoader.d.ts +3 -0
  67. package/dist/sync/OnDemandLoader.js +1 -0
  68. package/dist/sync/bootstrapApply.d.ts +3 -0
  69. package/dist/sync/bootstrapApply.js +2 -2
  70. package/dist/sync/deltaPipeline.d.ts +13 -12
  71. package/dist/sync/deltaPipeline.js +21 -4
  72. package/dist/sync/groupChange.d.ts +3 -0
  73. package/dist/sync/groupChange.js +16 -14
  74. package/dist/sync/participants.d.ts +19 -2
  75. package/dist/sync/participants.js +3 -1
  76. package/dist/sync/schemas.d.ts +2 -1
  77. package/dist/sync/schemas.js +3 -3
  78. package/dist/syncLog/contract.d.ts +20 -0
  79. package/dist/syncLog/contract.js +19 -0
  80. package/dist/syncLog/index.d.ts +1 -0
  81. package/dist/syncLog/index.js +1 -0
  82. package/dist/transaction/auth/capability.d.ts +35 -0
  83. package/dist/transaction/auth/capability.js +25 -0
  84. package/dist/transaction/coordination/awaitClaimGrant.d.ts +7 -0
  85. package/dist/transaction/coordination/awaitClaimGrant.js +12 -0
  86. package/dist/transaction/coordination/claimHeartbeatLoop.d.ts +34 -0
  87. package/dist/transaction/coordination/claimHeartbeatLoop.js +20 -0
  88. package/dist/transaction/coordination/index.d.ts +4 -4
  89. package/dist/transaction/coordination/index.js +4 -3
  90. package/dist/transaction/coordination/locator.d.ts +23 -2
  91. package/dist/transaction/coordination/locator.js +22 -2
  92. package/dist/transaction/coordination/schema.d.ts +125 -62
  93. package/dist/transaction/coordination/schema.js +228 -64
  94. package/dist/transaction/coordination/targetConflict.js +32 -28
  95. package/dist/transaction/errorCodes.d.ts +2 -2
  96. package/dist/transaction/errorCodes.js +13 -9
  97. package/dist/transaction/plugin.d.ts +95 -2
  98. package/dist/transaction/plugin.js +21 -2
  99. package/dist/transaction/resources/httpResources.d.ts +57 -2
  100. package/dist/transaction/resources/modelOperations.d.ts +148 -40
  101. package/dist/transaction/resources/where.d.ts +16 -0
  102. package/dist/transaction/resources/where.js +45 -0
  103. package/dist/transaction/schema/field.d.ts +12 -18
  104. package/dist/transaction/schema/fieldRef.d.ts +38 -0
  105. package/dist/transaction/schema/fieldRef.js +11 -0
  106. package/dist/transaction/schema/openapi.d.ts +16 -15
  107. package/dist/transaction/schema/openapi.js +186 -25
  108. package/dist/transaction/schema/relation.d.ts +7 -2
  109. package/dist/transaction/schema/schema.d.ts +27 -0
  110. package/dist/transaction/schema/schema.js +20 -0
  111. package/dist/transaction/transactions/settlement/commitEnvelope.d.ts +1 -1
  112. package/dist/transaction/transactions/settlement/pendingWrite.d.ts +1 -1
  113. package/dist/transaction/transport/httpClient.d.ts +9 -1
  114. package/dist/transaction/transport/httpClient.js +1 -0
  115. package/dist/transaction/transport/httpTransport.js +156 -44
  116. package/dist/transaction/transport/wsTransport.d.ts +2 -4
  117. package/dist/transaction/transport/wsTransport.js +16 -10
  118. package/dist/transaction/types/streams.d.ts +10 -0
  119. package/dist/transaction/utils/duration.d.ts +25 -0
  120. package/dist/transaction/utils/duration.js +32 -0
  121. package/dist/transaction/wire/accountResponses.d.ts +69 -0
  122. package/dist/transaction/wire/accountResponses.js +36 -1
  123. package/dist/transaction/wire/auth.d.ts +9 -2
  124. package/dist/transaction/wire/auth.js +7 -1
  125. package/dist/transaction/wire/claims.d.ts +164 -97
  126. package/dist/transaction/wire/claims.js +126 -28
  127. package/dist/transaction/wire/commit.d.ts +1 -1
  128. package/dist/transaction/wire/feedEvent.d.ts +27 -0
  129. package/dist/transaction/wire/feedEvent.js +27 -1
  130. package/dist/transaction/wire/frames.d.ts +2 -2
  131. package/dist/transaction/wire/inboundFrames.d.ts +12 -2
  132. package/dist/transaction/wire/index.d.ts +10 -6
  133. package/dist/transaction/wire/index.js +12 -3
  134. package/dist/transaction/wire/modelMutations.d.ts +31 -0
  135. package/dist/transaction/wire/modelMutations.js +52 -0
  136. package/dist/transaction/wire/modelShape.d.ts +78 -0
  137. package/dist/transaction/wire/modelShape.js +74 -0
  138. package/dist/transactions/mutations/MutationQueue.d.ts +6 -0
  139. package/dist/transactions/mutations/MutationQueue.js +55 -45
  140. package/dist/transactions/mutations/commitPayload.d.ts +3 -2
  141. package/dist/transactions/mutations/commitPayload.js +5 -5
  142. package/dist/transactions/mutations/deltaConfirmation.d.ts +4 -0
  143. package/dist/transactions/mutations/deltaConfirmation.js +10 -8
  144. package/dist/transactions/mutations/replayValidation.d.ts +2 -1
  145. package/dist/transactions/mutations/replayValidation.js +3 -2
  146. package/dist/{core → views}/QueryView.d.ts +1 -1
  147. package/dist/{core → views}/QueryView.js +1 -1
  148. package/dist/{core → views}/ViewRegistry.d.ts +1 -1
  149. package/dist/{core/queryUtils.d.ts → views/incrementalView.d.ts} +6 -6
  150. package/dist/{core/queryUtils.js → views/incrementalView.js} +6 -6
  151. package/docs/agents.md +1 -1
  152. package/docs/api-keys.md +6 -6
  153. package/docs/api.md +5 -43
  154. package/docs/audit.md +4 -3
  155. package/docs/cli.md +11 -11
  156. package/docs/client-behavior.md +4 -4
  157. package/docs/concurrency-convention.md +28 -42
  158. package/docs/coordination.md +235 -83
  159. package/docs/data-sources.md +4 -4
  160. package/docs/debugging.md +34 -12
  161. package/docs/deployment.md +8 -8
  162. package/docs/examples/scoped-agent.md +3 -3
  163. package/docs/groups.md +57 -3
  164. package/docs/guarantees.md +37 -10
  165. package/docs/how-it-works.md +29 -5
  166. package/docs/idempotency.md +6 -6
  167. package/docs/identity.md +22 -23
  168. package/docs/index.md +8 -8
  169. package/docs/integration-guide.md +14 -3
  170. package/docs/mcp.md +7 -7
  171. package/docs/migration.md +34 -15
  172. package/docs/projects.md +1 -1
  173. package/docs/react.md +19 -8
  174. package/docs/sessions.md +1 -1
  175. package/docs/webhooks.md +9 -9
  176. package/llms.txt +4 -4
  177. package/package.json +13 -20
  178. package/dist/cli.cjs +0 -288600
  179. package/dist/testing/fixtures/bootstrap.d.ts +0 -49
  180. package/dist/testing/fixtures/bootstrap.js +0 -59
  181. package/dist/testing/fixtures/deltas.d.ts +0 -83
  182. package/dist/testing/fixtures/deltas.js +0 -136
  183. package/dist/testing/fixtures/httpResponses.d.ts +0 -70
  184. package/dist/testing/fixtures/httpResponses.js +0 -90
  185. package/dist/testing/fixtures/models.d.ts +0 -83
  186. package/dist/testing/fixtures/models.js +0 -272
  187. package/dist/testing/helpers/reactWrapper.d.ts +0 -69
  188. package/dist/testing/helpers/reactWrapper.js +0 -67
  189. package/dist/testing/helpers/syncEngineHarness.d.ts +0 -54
  190. package/dist/testing/helpers/syncEngineHarness.js +0 -73
  191. package/dist/testing/helpers/wait.d.ts +0 -30
  192. package/dist/testing/helpers/wait.js +0 -49
  193. package/dist/testing/index.d.ts +0 -23
  194. package/dist/testing/index.js +0 -33
  195. package/dist/testing/mocks/FakeDatabase.d.ts +0 -18
  196. package/dist/testing/mocks/FakeDatabase.js +0 -10
  197. package/dist/testing/mocks/MockMutationExecutor.d.ts +0 -87
  198. package/dist/testing/mocks/MockMutationExecutor.js +0 -186
  199. package/dist/testing/mocks/MockNetworkMonitor.d.ts +0 -20
  200. package/dist/testing/mocks/MockNetworkMonitor.js +0 -46
  201. package/dist/testing/mocks/MockSyncContext.d.ts +0 -51
  202. package/dist/testing/mocks/MockSyncContext.js +0 -72
  203. package/dist/testing/mocks/MockSyncStore.d.ts +0 -88
  204. package/dist/testing/mocks/MockSyncStore.js +0 -171
  205. package/dist/testing/mocks/MockWebSocket.d.ts +0 -71
  206. package/dist/testing/mocks/MockWebSocket.js +0 -118
  207. package/docs/interaction-model.md +0 -99
  208. /package/dist/{core → query}/QueryProcessor.d.ts +0 -0
  209. /package/dist/{core → query}/QueryProcessor.js +0 -0
  210. /package/dist/{core/storeContract.js → storeContract.js} +0 -0
  211. /package/dist/{core → stores}/openIDBWithTimeout.d.ts +0 -0
  212. /package/dist/{core → stores}/openIDBWithTimeout.js +0 -0
  213. /package/dist/{source → transaction}/footprint.d.ts +0 -0
  214. /package/dist/{source → transaction}/footprint.js +0 -0
  215. /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 { getContext } from '../../context.js';
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 ?? getContext().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
- getContext().observability.captureMutationFailure({
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
- getContext().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
+ 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
- getContext().logger.debug('[MutationQueue] Durable-write cleanup deferred', {
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
- getContext().logger.debug(`[MutationQueue] WS disconnected > ${graceMs}ms; failing ${inFlight.length} in-flight commit(s) with AbloConnectionError`, { inFlightIds: inFlight.map((id) => id.slice(0, 8)) });
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
- getContext().logger.debug('[MutationQueue] commitCreatedTransactions', {
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
- getContext().logger.debug('txn:commit', 0, {
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 = !getContext().onlineStatus.isOnline();
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
- getContext().observability.breadcrumb('Batch flush fallback failed', 'sync.transaction', 'warning', {
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
- getContext().logger.debug('MutationQueue.delete() skipping Activity deletion - permanent audit records', { modelId: model.id });
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
- ...canceledUpdates.flatMap((candidate) => candidate.sourceMutationIds ?? []),
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
- getContext().logger.debug('[MutationQueue] Backpressure: delaying batch, too many executing', {
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 getContext().observability.startSpanAsync('sync.batch', 'sync.transaction.batch', async () => {
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
- getContext().logger.debug('tx:awaiting_delta', {
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
- getContext().observability.captureCommitZeroSyncId({
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
- getContext().logger.debug('tx:confirm_delete_zero_syncid', {
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
- getContext().logger.debug('tx:confirm_ack', {
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
- getContext().logger.debug('tx:awaiting_delta', {
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
- getContext().logger.debug('[MutationQueue] Batch commit rejected', {
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
- getContext().logger.info('[MutationQueue] Graceful handling: entity already deleted', {
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
- getContext().logger.debug('[MutationQueue] Orphaned transaction treated as success', {
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
- getContext().logger.debug('txn:batch', batchEnd - batchStart, {
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
- getContext().logger.debug('[MutationQueue] commit lane awaiting source echo', {
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
- getContext().logger.debug('[MutationQueue] commit lane transient', {
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
- getContext().logger.debug('[MutationQueue] commit lane permanent error', {
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 = getContext().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
- getContext().observability.captureMutationFailure({
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
- getContext().logger.warn('A saved local write is unreadable and was held for review.');
2474
- getContext().observability.captureMutationFailure({
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
- getContext().logger.warn('A saved local write is too old to retry safely and was held for review.');
2492
- getContext().observability.captureMutationFailure({
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
- getContext().logger.warn('A saved local write belongs to a different account or server and was held for review.');
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
- getContext().logger.debug('[MutationQueue] Failed to restore durable writes', {
2546
+ this.runtime.logger.debug('[MutationQueue] Failed to restore durable writes', {
2537
2547
  error: error instanceof Error ? error.message : String(error),
2538
2548
  });
2539
- getContext().observability.captureMutationFailure({
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
- getContext().logger.debug('[MutationQueue] Dropping malformed persisted transaction', {
2571
+ this.runtime.logger.debug('[MutationQueue] Dropping malformed persisted transaction', {
2562
2572
  rowId,
2563
2573
  });
2564
- getContext().observability.captureMutationFailure({
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
- getContext().observability.captureMutationFailure({
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
- getContext().observability.captureMutationFailure({
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
- getContext().logger.debug('[MutationQueue] Cascade cancelled orphaned transaction', {
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 { getContext } from '../../context.js';
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 = getContext().getModelMetadata(modelName);
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 } = getContext().config;
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 { getContext } from '../../context.js';
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
- getContext().logger.debug('tx:delta_received', {
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
- getContext().logger.debug('tx:confirm_via_delta', {
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
- getContext().observability.breadcrumb('Transactions confirmed via delta', 'sync.transaction', 'info', {
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
- getContext().logger.debug('[MutationQueue] Timeout fired while disconnected - re-scheduling', {
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
- getContext().observability.captureReconciliation({
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
- getContext().logger.debug('[MutationQueue] Re-scheduling with backoff', {
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
- getContext().observability.captureDeltaRetryExhausted({
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 './queryUtils.js';
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 './queryUtils.js';
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 './queryUtils.js';
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
- * Small, self-contained helpers for sorting, filtering, and binary insertion.
3
- * The incrementally-updated views that implement {@link IncrementalView} rely on
4
- * these for their ordering and matching, so keeping the rules in one place
5
- * ensures every view sorts and filters identically. The functions here work on
6
- * plain arrays and values — they hold no reference to models, pools, or the
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