@abloatai/humans 0.57.0 → 0.58.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 (121) hide show
  1. package/dist/Ablo.d.ts +8 -18
  2. package/dist/client.d.ts +6 -19
  3. package/dist/core.d.ts +1 -1
  4. package/dist/core.js +1 -1
  5. package/dist/local/Database.d.ts +1 -1
  6. package/dist/local/SyncClient.js +5 -0
  7. package/dist/local/client/createInternalComponents.d.ts +1 -1
  8. package/dist/local/client/createInternalComponents.js +1 -1
  9. package/dist/local/client/{createModelProxy.d.ts → createModelOperations.d.ts} +11 -10
  10. package/dist/local/client/{createModelProxy.js → createModelOperations.js} +240 -313
  11. package/dist/local/client/options.d.ts +2 -2
  12. package/dist/local/client/reactiveEngine.js +34 -45
  13. package/dist/local/client/resourceTypes.d.ts +3 -3
  14. package/dist/local/client/resourceTypes.js +1 -1
  15. package/dist/local/client/wsMutationExecutor.d.ts +4 -4
  16. package/dist/local/client/wsMutationExecutor.js +3 -5
  17. package/dist/local/coordination/ClaimLog.d.ts +3 -3
  18. package/dist/local/coordination/ClaimLog.js +2 -2
  19. package/dist/local/interfaces/index.d.ts +5 -7
  20. package/dist/local/mutators/mutateActions.d.ts +3 -2
  21. package/dist/local/query/types.d.ts +2 -2
  22. package/dist/local/stores/syncAction.d.ts +1 -1
  23. package/dist/local/stores/syncAction.js +1 -1
  24. package/dist/local/sync/ConnectionManager.d.ts +1 -1
  25. package/dist/local/sync/ConnectionManager.js +1 -1
  26. package/dist/local/sync/OnDemandLoader.d.ts +1 -1
  27. package/dist/local/sync/OnDemandLoader.js +1 -1
  28. package/dist/local/sync/SyncWebSocket.d.ts +3 -3
  29. package/dist/local/sync/SyncWebSocket.js +2 -2
  30. package/dist/local/sync/commitFrames.d.ts +1 -1
  31. package/dist/local/sync/commitFrames.js +1 -1
  32. package/dist/local/sync/contextOnChange.d.ts +8 -0
  33. package/dist/local/sync/contextOnChange.js +65 -0
  34. package/dist/local/sync/createClaimStream.d.ts +1 -1
  35. package/dist/local/sync/createClaimStream.js +47 -28
  36. package/dist/local/sync/credentialLifecycle.d.ts +1 -1
  37. package/dist/local/sync/credentialLifecycle.js +1 -1
  38. package/dist/local/sync/participants.d.ts +1 -1
  39. package/dist/local/sync/schemas.d.ts +2 -2
  40. package/dist/local/sync/schemas.js +1 -1
  41. package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
  42. package/dist/local/sync/wsFrameHandlers.js +1 -1
  43. package/dist/local/transactions/databaseCommitOutbox.d.ts +2 -2
  44. package/dist/local/transactions/mutations/MutationQueue.d.ts +4 -19
  45. package/dist/local/transactions/mutations/MutationQueue.js +5 -70
  46. package/dist/local/transactions/mutations/batchProcessing.d.ts +2 -13
  47. package/dist/local/transactions/mutations/batchProcessing.js +0 -20
  48. package/dist/local/transactions/mutations/commitApi.d.ts +2 -3
  49. package/dist/local/transactions/mutations/commitApi.js +0 -4
  50. package/dist/local/transactions/mutations/commitLane.d.ts +3 -8
  51. package/dist/local/transactions/mutations/commitLane.js +0 -11
  52. package/dist/local/transactions/mutations/commitPayload.d.ts +2 -4
  53. package/dist/local/transactions/mutations/commitPayload.js +2 -6
  54. package/dist/local/transactions/mutations/commitTransport.d.ts +3 -4
  55. package/dist/local/transactions/mutations/commitTransport.js +2 -5
  56. package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
  57. package/dist/local/transactions/mutations/durableCommitRestore.js +1 -4
  58. package/dist/local/transactions/mutations/durableWriteStore.d.ts +5 -5
  59. package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
  60. package/dist/local/transactions/mutations/failurePolicy.d.ts +2 -2
  61. package/dist/local/transactions/mutations/mutationPersistence.d.ts +1 -1
  62. package/dist/local/transactions/mutations/pendingDrain.d.ts +2 -2
  63. package/dist/local/transactions/mutations/replayValidation.d.ts +0 -20
  64. package/dist/local/transactions/mutations/replayValidation.js +1 -3
  65. package/dist/local/transactions/persistedTransaction.d.ts +0 -2
  66. package/dist/plugin.d.ts +2 -2
  67. package/dist/presenceStream.d.ts +1 -1
  68. package/dist/react/createAbloReact.d.ts +1 -1
  69. package/dist/react/useAblo.d.ts +2 -2
  70. package/dist/react/useAblo.js +1 -1
  71. package/dist/surface.d.ts +1 -1
  72. package/dist/surface.js +1 -2
  73. package/package.json +2 -2
  74. package/src/Ablo.ts +9 -27
  75. package/src/client.ts +6 -22
  76. package/src/core.ts +1 -1
  77. package/src/local/Database.ts +1 -1
  78. package/src/local/SyncClient.ts +5 -0
  79. package/src/local/client/createInternalComponents.ts +2 -2
  80. package/src/local/client/{createModelProxy.ts → createModelOperations.ts} +292 -367
  81. package/src/local/client/options.ts +2 -2
  82. package/src/local/client/reactiveEngine.ts +63 -55
  83. package/src/local/client/resourceTypes.ts +4 -4
  84. package/src/local/client/wsMutationExecutor.ts +3 -9
  85. package/src/local/coordination/ClaimLog.ts +3 -3
  86. package/src/local/interfaces/index.ts +5 -7
  87. package/src/local/mutators/mutateActions.ts +3 -2
  88. package/src/local/query/types.ts +2 -2
  89. package/src/local/stores/syncAction.ts +1 -1
  90. package/src/local/sync/ConnectionManager.ts +1 -1
  91. package/src/local/sync/OnDemandLoader.ts +1 -1
  92. package/src/local/sync/SyncWebSocket.ts +3 -3
  93. package/src/local/sync/commitFrames.ts +1 -2
  94. package/src/local/sync/contextOnChange.ts +92 -0
  95. package/src/local/sync/createClaimStream.ts +77 -39
  96. package/src/local/sync/credentialLifecycle.ts +1 -1
  97. package/src/local/sync/participants.ts +1 -1
  98. package/src/local/sync/schemas.ts +1 -1
  99. package/src/local/sync/wsFrameHandlers.ts +1 -1
  100. package/src/local/transactions/databaseCommitOutbox.ts +2 -2
  101. package/src/local/transactions/mutations/MutationQueue.ts +8 -95
  102. package/src/local/transactions/mutations/batchProcessing.ts +2 -38
  103. package/src/local/transactions/mutations/commitApi.ts +3 -7
  104. package/src/local/transactions/mutations/commitLane.ts +3 -17
  105. package/src/local/transactions/mutations/commitPayload.ts +3 -11
  106. package/src/local/transactions/mutations/commitTransport.ts +3 -7
  107. package/src/local/transactions/mutations/durableCommitRestore.ts +2 -5
  108. package/src/local/transactions/mutations/durableWriteStore.ts +5 -5
  109. package/src/local/transactions/mutations/failurePolicy.ts +2 -2
  110. package/src/local/transactions/mutations/mutationPersistence.ts +1 -1
  111. package/src/local/transactions/mutations/pendingDrain.ts +2 -2
  112. package/src/local/transactions/mutations/replayValidation.ts +1 -3
  113. package/src/local/transactions/persistedTransaction.ts +0 -3
  114. package/src/plugin.ts +2 -2
  115. package/src/presenceStream.ts +2 -2
  116. package/src/react/createAbloReact.ts +1 -1
  117. package/src/react/useAblo.ts +2 -2
  118. package/src/surface.ts +2 -3
  119. package/dist/local/sync/createSnapshot.d.ts +0 -29
  120. package/dist/local/sync/createSnapshot.js +0 -116
  121. package/src/local/sync/createSnapshot.ts +0 -160
@@ -24,10 +24,11 @@
24
24
  import { asyncIteratorFrom } from '@abloatai/transaction/utils/asyncIterator';
25
25
  import { toMs } from '@abloatai/transaction/utils/duration';
26
26
  import { claimDescription, descriptionFromMeta, participantKindFromWire, } from '@abloatai/transaction/coordination/schema';
27
- import { isTargetTuple, subTarget, streamTarget, wireTarget, } from '@abloatai/transaction/coordination/locator';
28
- import { declaredMeta } from '@abloatai/transaction/coordination/claimMeta';
27
+ import { isTargetTuple, subTarget, streamTarget, wireTarget, } from '@abloatai/transaction/claims';
28
+ import { bindClaimLifetime, createClaimLifetime, } from '@abloatai/transaction/claims/lifetime';
29
+ import { declaredMeta } from '@abloatai/transaction/claims';
29
30
  import { AbloClaimedError, AbloConnectionError } from '@abloatai/transaction/errors';
30
- import { resolveHeartbeatOptions } from '@abloatai/transaction/coordination/claimHeartbeatLoop';
31
+ import { resolveHeartbeatOptions } from '@abloatai/transaction/claims';
31
32
  import { noopLogger } from '@abloatai/transaction/logger';
32
33
  /** Readable target for the coordination trace: `documents:abc` / `documents:abc.title`. */
33
34
  function claimLabel(type, id, field) {
@@ -47,7 +48,7 @@ export function createClaimStream(config, transport = null) {
47
48
  // ── State: others' open claims, keyed by claimId ───────────────
48
49
  const activeByClaimId = new Map();
49
50
  let claimsSnapshot = Object.freeze([]);
50
- // ── State: our own open claims (for re-announce on reconnect) ───
51
+ // ── State: our own open grants, keyed by the exact claim id ─────
51
52
  const ownClaims = new Map();
52
53
  // ── State: per-entity wait queues, from `claim_queue` frames ────
53
54
  // Keyed `type:id`; the value is the FIFO line of queued claims. Powers
@@ -157,15 +158,18 @@ export function createClaimStream(config, transport = null) {
157
158
  unsubs.push(t.subscribe('claim_rejected', (rejection) => {
158
159
  if (!rejection.claimId)
159
160
  return;
160
- if (ownClaims.has(rejection.claimId)) {
161
+ const own = ownClaims.get(rejection.claimId);
162
+ if (own) {
161
163
  const tgt = rejection.target
162
164
  ? claimLabel(rejection.target.entityType, rejection.target.entityId, rejection.target.field)
163
165
  : rejection.claimId;
164
166
  logger.info(`claim: rejected ${tgt}${rejection.heldBy ? ` — held by ${rejection.heldBy}` : ''}`, { claimId: rejection.claimId, reason: rejection.reason });
165
167
  }
166
- // Drop the rejected own-claim so reconnect doesn't re-announce
167
- // a claim the server already rejected (would just spam both
168
- // sides with conflicts).
168
+ own?.lifetime.end(new AbloClaimedError(rejection.message ?? `Claim ${rejection.claimId} was rejected.`, {
169
+ code: rejection.reason === 'conflict'
170
+ ? 'claim_conflict'
171
+ : 'claim_lease_unavailable',
172
+ }));
169
173
  ownClaims.delete(rejection.claimId);
170
174
  // A holder on another server may have claimed before this client joined
171
175
  // the row group, so its one-shot presence frame was missed. A conflict
@@ -193,12 +197,12 @@ export function createClaimStream(config, transport = null) {
193
197
  // The frame is validated by the transport's dispatcher against the same
194
198
  // schema this parameter's type comes from, so it arrives proven.
195
199
  t.subscribe('claim_lost', (lost) => {
196
- if (ownClaims.has(lost.claimId)) {
197
- const c = ownClaims.get(lost.claimId);
198
- logger.info(`claim: lost ${c ? claimLabel(c.entityType, c.entityId, c.field) : lost.claimId} (preempted or expired)`, { claimId: lost.claimId });
200
+ const own = ownClaims.get(lost.claimId);
201
+ if (own) {
202
+ const c = own.claim;
203
+ logger.info(`claim: lost ${claimLabel(c.entityType, c.entityId, c.field)} (preempted or expired)`, { claimId: lost.claimId });
199
204
  }
200
- // Drop the lost own-claim so reconnect doesn't re-announce a lease we
201
- // no longer hold.
205
+ own?.lifetime.end(new AbloClaimedError(`Claim ${lost.claimId} was lost because it ${lost.reason === 'expired' ? 'expired' : 'was preempted'}. Re-acquire it before continuing.`, { code: 'claim_lost' }));
202
206
  ownClaims.delete(lost.claimId);
203
207
  for (const l of lostListeners) {
204
208
  try {
@@ -260,17 +264,27 @@ export function createClaimStream(config, transport = null) {
260
264
  });
261
265
  return;
262
266
  }
263
- const c = ownClaims.get(ack.claimId);
264
- reject(new AbloClaimedError(`The lease behind ${c ? claimLabel(c.entityType, c.entityId, c.field) : `claim ${ack.claimId}`} is no longer held — it expired or was granted onward while this participant was working. Re-acquire the claim and retry; a write attempted under the old lease is rejected by its \`readAt\` guard.`, { code: 'claim_lost' }));
267
+ const own = ownClaims.get(ack.claimId);
268
+ const c = own?.claim;
269
+ const error = new AbloClaimedError(`The lease behind ${c ? claimLabel(c.entityType, c.entityId, c.field) : `claim ${ack.claimId}`} is no longer held — it expired or was granted onward while this participant was working. Re-acquire the claim and retry; a write attempted under the old lease is rejected by its \`readAt\` guard.`, { code: 'claim_lost' });
270
+ own?.lifetime.end(error);
271
+ ownClaims.delete(ack.claimId);
272
+ reject(error);
265
273
  });
266
274
  }));
267
- // (3) On reconnect, re-announce every open self-claim the
268
- // server's claim state is in-memory and is lost across
269
- // restarts. Without this, peers would see our claims vanish
270
- // whenever the connection blipped.
271
- unsubs.push(t.subscribe('connected', () => {
272
- for (const [claimId, claim] of ownClaims) {
273
- sendBegin(claimId, claim);
275
+ // (3) A granted handle belongs to this connection's exact grant. The server
276
+ // releases the participant's claims when its last socket leaves; trying
277
+ // to recreate them on `connected` silently swapped the lease beneath an
278
+ // old `data` / `readAt` / fenceToken tuple. End them instead. A caller
279
+ // reconnects, re-reads, and takes a fresh handle.
280
+ unsubs.push(t.subscribe('disconnected', () => {
281
+ const error = new AbloConnectionError('The connection holding this claim ended. Reconnect and acquire a fresh claim before continuing.');
282
+ for (const { lifetime } of ownClaims.values()) {
283
+ lifetime.end(error);
284
+ }
285
+ ownClaims.clear();
286
+ for (const claimId of [...pendingHeartbeats.keys()]) {
287
+ settleHeartbeat(claimId, ({ reject }) => reject(error));
274
288
  }
275
289
  }));
276
290
  }
@@ -308,14 +322,14 @@ export function createClaimStream(config, transport = null) {
308
322
  }
309
323
  /**
310
324
  * Send one heartbeat and await its ack. Rejects with
311
- * {@link AbloConnectionError} (transient the auto-heartbeat loop retries
312
- * on its next tick) when the socket is down or the ack times out, and with
325
+ * {@link AbloConnectionError} when the socket is down or the ack times out,
326
+ * and with
313
327
  * {@link AbloClaimedError} (definitive) when the server answers that the
314
328
  * lease is no longer ours.
315
329
  */
316
330
  function sendHeartbeat(claimId, claim, options) {
317
331
  if (!attached?.isConnected()) {
318
- return Promise.reject(new AbloConnectionError(`The heartbeat for ${claimLabel(claim.entityType, claim.entityId, claim.field)} was skipped because the connection is down. The keepalive renews held leases automatically on reconnect; the next beat retries.`));
332
+ return Promise.reject(new AbloConnectionError(`The heartbeat for ${claimLabel(claim.entityType, claim.entityId, claim.field)} was skipped because the connection is down. This connection's grant has ended; reconnect and acquire a fresh claim.`));
319
333
  }
320
334
  return new Promise((resolve, reject) => {
321
335
  settleHeartbeat(claimId, ({ reject: rejectPrior }) => {
@@ -371,7 +385,8 @@ export function createClaimStream(config, transport = null) {
371
385
  estimatedMs,
372
386
  queue: args.queue,
373
387
  };
374
- ownClaims.set(claimId, claim);
388
+ const lifetime = createClaimLifetime();
389
+ ownClaims.set(claimId, { claim, lifetime });
375
390
  sendBegin(claimId, claim);
376
391
  // Coordination trace (info): the creator can see their human/agent claims.
377
392
  logger.info(`claim: requesting ${claimLabel(claim.entityType, claim.entityId, claim.field)} for "${claim.description}"` +
@@ -381,11 +396,12 @@ export function createClaimStream(config, transport = null) {
381
396
  if (revoked)
382
397
  return;
383
398
  revoked = true;
399
+ lifetime.end();
384
400
  ownClaims.delete(claimId);
385
401
  sendAbandon(claimId, claim);
386
402
  logger.info(`claim: released ${claimLabel(claim.entityType, claim.entityId, claim.field)}`, { claimId });
387
403
  };
388
- return {
404
+ return bindClaimLifetime({
389
405
  object: 'claim',
390
406
  id: claimId,
391
407
  status: 'active',
@@ -403,7 +419,7 @@ export function createClaimStream(config, transport = null) {
403
419
  [Symbol.asyncDispose]: async () => {
404
420
  revoke();
405
421
  },
406
- };
422
+ }, lifetime);
407
423
  }
408
424
  function resolveTarget(target) {
409
425
  if (isTargetTuple(target))
@@ -471,6 +487,9 @@ export function createClaimStream(config, transport = null) {
471
487
  reject(new AbloConnectionError('The claim stream was disposed while this heartbeat was awaiting its reply.'));
472
488
  });
473
489
  }
490
+ const error = new AbloConnectionError('The claim stream was disposed. Acquire a fresh claim before continuing.');
491
+ for (const { lifetime } of ownClaims.values())
492
+ lifetime.end(error);
474
493
  listeners.clear();
475
494
  rejectionListeners.clear();
476
495
  lostListeners.clear();
@@ -4,4 +4,4 @@
4
4
  * as much as a browser does. This path re-exports it so existing importers
5
5
  * stay unchanged.
6
6
  */
7
- export { DEFAULT_PREROLL_INTERVAL_MS, MIN_PREROLL_DELAY_MS, computePrerollDelayMs, CredentialLifecycle, type CredentialRefreshOutcome, type CredentialRecoveryOutcome, type CredentialRefreshResult, type CredentialRefresher, type CredentialLifecycleContext, } from '@abloatai/transaction/transport/credentialLifecycle';
7
+ export { DEFAULT_PREROLL_INTERVAL_MS, MIN_PREROLL_DELAY_MS, computePrerollDelayMs, CredentialLifecycle, type CredentialRefreshOutcome, type CredentialRecoveryOutcome, type CredentialRefreshResult, type CredentialRefresher, type CredentialLifecycleContext, } from '@abloatai/transaction/transport/connection';
@@ -4,4 +4,4 @@
4
4
  * as much as a browser does. This path re-exports it so existing importers
5
5
  * stay unchanged.
6
6
  */
7
- export { DEFAULT_PREROLL_INTERVAL_MS, MIN_PREROLL_DELAY_MS, computePrerollDelayMs, CredentialLifecycle, } from '@abloatai/transaction/transport/credentialLifecycle';
7
+ export { DEFAULT_PREROLL_INTERVAL_MS, MIN_PREROLL_DELAY_MS, computePrerollDelayMs, CredentialLifecycle, } from '@abloatai/transaction/transport/connection';
@@ -1,4 +1,4 @@
1
- import type { JoinOptions } from '@abloatai/transaction/resources/modelOperations';
1
+ import type { JoinOptions } from '@abloatai/transaction/client/resources/modelOperations';
2
2
  import type { SyncWebSocket } from './SyncWebSocket.js';
3
3
  import type { Schema } from '@abloatai/transaction/schema/schema';
4
4
  import type { Claim, Activity, ClaimTarget, ClaimStream, Peer, PresenceStream, PresenceTarget } from '@abloatai/transaction/types/streams';
@@ -20,10 +20,10 @@ export declare const ServerDeltaSchema: z.ZodObject<{
20
20
  id: z.ZodNumber;
21
21
  data: z.ZodNullable<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString]>>;
22
22
  actionType: z.ZodEnum<{
23
+ A: "A";
23
24
  I: "I";
24
25
  U: "U";
25
26
  D: "D";
26
- A: "A";
27
27
  V: "V";
28
28
  C: "C";
29
29
  G: "G";
@@ -44,10 +44,10 @@ export declare const BootstrapResponseSchema: z.ZodObject<{
44
44
  id: z.ZodNumber;
45
45
  data: z.ZodNullable<z.ZodUnion<readonly [z.ZodRecord<z.ZodString, z.ZodUnknown>, z.ZodString]>>;
46
46
  actionType: z.ZodEnum<{
47
+ A: "A";
47
48
  I: "I";
48
49
  U: "U";
49
50
  D: "D";
50
- A: "A";
51
51
  V: "V";
52
52
  C: "C";
53
53
  G: "G";
@@ -8,7 +8,7 @@
8
8
  import { z } from 'zod';
9
9
  import { globalRuntime } from "../context.js";
10
10
  import { AbloValidationError } from "@abloatai/transaction/errors";
11
- import { syncDeltaWireCoreSchema } from '@abloatai/transaction/wire/delta';
11
+ import { syncDeltaWireCoreSchema } from '@abloatai/transaction/observation';
12
12
  // ─── Server Delta Schema ─────────────────────────────────────────────────────
13
13
  /**
14
14
  * A delta as it arrives in a bootstrap payload.
@@ -5,4 +5,4 @@
5
5
  * the transport object. This path re-exports it so existing importers stay
6
6
  * unchanged.
7
7
  */
8
- export { isRecord, readWsInboundFrame, wsFrameHandlers, dispatchWsFrame, type PendingCommit, type PendingClaim, type PendingSubscription, type WsInboundFrame, type WsSession, type WsFrameHandler, } from '@abloatai/transaction/transport/wsFrameHandlers';
8
+ export { isRecord, readWsInboundFrame, wsFrameHandlers, dispatchWsFrame, type PendingCommit, type PendingClaim, type PendingSubscription, type WsInboundFrame, type WsSession, type WsFrameHandler, } from '@abloatai/transaction/transport/websocket';
@@ -5,4 +5,4 @@
5
5
  * the transport object. This path re-exports it so existing importers stay
6
6
  * unchanged.
7
7
  */
8
- export { isRecord, readWsInboundFrame, wsFrameHandlers, dispatchWsFrame, } from '@abloatai/transaction/transport/wsFrameHandlers';
8
+ export { isRecord, readWsInboundFrame, wsFrameHandlers, dispatchWsFrame, } from '@abloatai/transaction/transport/websocket';
@@ -1,5 +1,5 @@
1
- import type { DurableWriteStore } from '@abloatai/transaction/durableWrites';
2
- import type { PendingWrite } from '@abloatai/transaction/transactions/confirmation/pendingWrite';
1
+ import type { DurableWriteStore } from '@abloatai/transaction/commit';
2
+ import type { PendingWrite } from '@abloatai/transaction/commit';
3
3
  export interface CommitOutboxDatabase {
4
4
  sealTransactionRecord(record: PendingWrite, consumedRecordIds: readonly string[]): Promise<unknown>;
5
5
  getPersistedTransactions(): Promise<readonly unknown[]>;
@@ -17,10 +17,10 @@ import type { MutationPersistencePort } from '../../mutationPersistence.js';
17
17
  import type { RuntimeContext } from '../../RuntimeContext.js';
18
18
  import { type LogPositionPort } from '../../logPosition.js';
19
19
  import type { WriteOptions } from '../../interfaces/index.js';
20
- import type { StaleNotification, ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
21
- import { type CommitOperationResult } from '@abloatai/transaction/wire/commit';
20
+ import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
21
+ import { type CommitOperationResult } from '@abloatai/transaction/commit';
22
22
  import { type MutationInput, type QueuedMutation, type UserContext } from './commitPayload.js';
23
- import { type CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
23
+ import { type CommitOutboxScope } from '@abloatai/transaction/commit';
24
24
  import type { DurableWriteStore } from './durableWriteStore.js';
25
25
  import { type LocalMutationPort } from './localMutation.js';
26
26
  import { type CommitTransaction } from './commitLane.js';
@@ -151,7 +151,7 @@ export declare class MutationQueue extends EventEmitter {
151
151
  private get queueCoalescingContext();
152
152
  private get batchProcessingContext();
153
153
  private get failureHandlingContext();
154
- private get conflictPolicyContext();
154
+ private get conflictResolutionContext();
155
155
  private get processingSchedulerContext();
156
156
  private get pendingDrainContext();
157
157
  private get durableCommitRestoreContext();
@@ -163,18 +163,6 @@ export declare class MutationQueue extends EventEmitter {
163
163
  private computePriorityScore;
164
164
  private ensureDerivedFields;
165
165
  private entityKey;
166
- /** Collision-safe receipt target identity across models sharing a row id. */
167
- private receiptTargetKey;
168
- /**
169
- * Relates stale notifications back to write targets without assuming the
170
- * server's canonical model name uses the same spelling as the public schema
171
- * key (`Item` versus `items`). Exact `(model,id)` wins; a globally unique id
172
- * is the compatibility fallback. An ambiguous same-id cross-model mismatch
173
- * is deliberately left unclassified, so it cannot falsely settle a queued
174
- * write. A notification with no write-target id (or an explicit group) is a
175
- * declared-read conflict and holds the whole batch.
176
- */
177
- private classifyReceiptNotifications;
178
166
  private resolveConfirmation;
179
167
  private takeUnsentCreateForModel;
180
168
  private cancelUnsentCreateForDelete;
@@ -185,7 +173,6 @@ export declare class MutationQueue extends EventEmitter {
185
173
  private config;
186
174
  private executingCount;
187
175
  private readonly localMutationPort;
188
- private commitNotifications;
189
176
  /** Zero-row targets returned on a successful atomic commit receipt. */
190
177
  private commitMissingIds;
191
178
  private readonly deltaConfirmation;
@@ -367,12 +354,10 @@ export declare class MutationQueue extends EventEmitter {
367
354
  /** Enqueues a pre-built atomic commit through the commit API coordinator. */
368
355
  enqueueCommit(clientTxId: string, operations: CommitTransaction['operations'], options?: {
369
356
  reads?: ReadDependency[] | null;
370
- track?: TrackDependency[] | null;
371
357
  }): Promise<void>;
372
358
  private processCommitLane;
373
359
  waitForCommitReceipt(clientTxId: string): Promise<{
374
360
  lastSyncId: number;
375
- notifications?: StaleNotification[];
376
361
  missingIds?: string[];
377
362
  operationResults?: CommitOperationResult[];
378
363
  }>;
@@ -16,7 +16,7 @@ import { v4 as uuid } from 'uuid';
16
16
  import { globalRuntime } from '../../context.js';
17
17
  import { AbloConnectionError, AbloIdempotencyError, } from '@abloatai/transaction/errors';
18
18
  import { LogPosition, } from '../../logPosition.js';
19
- import {} from '@abloatai/transaction/wire/commit';
19
+ import {} from '@abloatai/transaction/commit';
20
20
  import { computePriorityScore, normalizeModelKey, } from './commitPayload.js';
21
21
  import { generateTransactionId, mergeMutationData, createDataFor, changesToInput, updateDataFor, previousDataFor, } from './mutationInput.js';
22
22
  import { MutationStore } from './MutationStore.js';
@@ -24,8 +24,7 @@ import { entityKey, takeUnsentCreateForModel, findCreateBarrierForDelete, deferD
24
24
  import { DeltaConfirmationTracker } from './deltaConfirmation.js';
25
25
  import { deserializePersistedTransaction, isNonReplayablePersistedRow, pendingMutationRecordId, } from './replayValidation.js';
26
26
  import { deserializeLegacyPendingMutation, loadPersistedTransactions, persistQueuedTransaction, removePersistedTransaction, settlePersistedFailure, } from './mutationPersistence.js';
27
- import { createCommitEnvelopeMember, } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
28
- import { stableStringify } from '@abloatai/transaction/utils/json';
27
+ import { createCommitEnvelopeMember, } from '@abloatai/transaction/commit';
29
28
  import { createLocalMutationPort, } from './localMutation.js';
30
29
  import { dispatchCommitBounded, parseMutationCommitResult, persistDurableCommitAcceptance, removeDurableCommit, sealDurableCommit, } from './commitTransport.js';
31
30
  import { processCommitLane, waitForCommitReceipt, } from './commitLane.js';
@@ -131,7 +130,6 @@ export class MutationQueue extends EventEmitter {
131
130
  retryBackoff: this.config.retryBackoff,
132
131
  },
133
132
  commitLane: this.commitLane,
134
- commitNotifications: this.commitNotifications,
135
133
  commitMissingIds: this.commitMissingIds,
136
134
  commitProcessing: this.commitProcessing,
137
135
  setCommitProcessing: (value) => { this.commitProcessing = value; },
@@ -141,7 +139,6 @@ export class MutationQueue extends EventEmitter {
141
139
  dispatchCommit: async (envelope) => this.parseMutationCommitResult(await this.dispatchCommitBounded(envelope.operations, {
142
140
  idempotencyKey: envelope.idempotencyKey,
143
141
  ...(envelope.commitOptions.reads ? { reads: envelope.commitOptions.reads } : {}),
144
- ...(envelope.commitOptions.track ? { track: envelope.commitOptions.track } : {}),
145
142
  })),
146
143
  persistDurableCommitAcceptance: (envelope, result) => this.persistDurableCommitAcceptance(envelope, result),
147
144
  removeDurableCommit: (idempotencyKey) => this.removeDurableCommit(idempotencyKey),
@@ -165,7 +162,6 @@ export class MutationQueue extends EventEmitter {
165
162
  get commitReceiptContext() {
166
163
  return {
167
164
  commitStore: this.commitStore,
168
- commitNotifications: this.commitNotifications,
169
165
  commitMissingIds: this.commitMissingIds,
170
166
  replicationLagErrors: this.replicationLagErrors,
171
167
  on: (event, listener) => this.on(event, listener),
@@ -248,7 +244,6 @@ export class MutationQueue extends EventEmitter {
248
244
  store: this.store,
249
245
  enqueue: (transaction) => { this.enqueue(transaction); },
250
246
  optimisticUpdates: this.localMutationPort.updates,
251
- commitNotifications: this.commitNotifications,
252
247
  commitMissingIds: this.commitMissingIds,
253
248
  sourceMutationIdsFor: (batch) => this.sourceMutationIdsFor(batch),
254
249
  dispatchCommitBounded: (...args) => this.dispatchCommitBounded(...args),
@@ -258,8 +253,6 @@ export class MutationQueue extends EventEmitter {
258
253
  assertEnvelopeInsideReplayWindow: (envelope) => { this.assertEnvelopeInsideReplayWindow(envelope); },
259
254
  sealDurableCommit: (input) => this.sealDurableCommit(input),
260
255
  noteAck: (syncId) => { this.noteAck(syncId); },
261
- classifyReceiptNotifications: (operations, notifications) => this.classifyReceiptNotifications(operations, notifications),
262
- receiptTargetKey: (modelName, modelId) => this.receiptTargetKey(modelName, modelId),
263
256
  scheduleReplicationLagTimeout: (transactionId, clientTxId, correlationId) => { this.scheduleReplicationLagTimeout(transactionId, clientTxId, correlationId); },
264
257
  scheduleDeltaConfirmationTimeout: (transaction, timeoutMs) => { this.scheduleDeltaConfirmationTimeout(transaction, timeoutMs); },
265
258
  clearReplicationLagState: (transactionId) => { this.clearReplicationLagState(transactionId); },
@@ -288,7 +281,7 @@ export class MutationQueue extends EventEmitter {
288
281
  emit: (event, payload) => this.emit(event, payload),
289
282
  };
290
283
  }
291
- get conflictPolicyContext() {
284
+ get conflictResolutionContext() {
292
285
  return {
293
286
  config: this.config,
294
287
  store: this.store,
@@ -409,59 +402,7 @@ export class MutationQueue extends EventEmitter {
409
402
  entityKey(modelName, modelId) {
410
403
  return entityKey(modelName, modelId);
411
404
  }
412
- /** Collision-safe receipt target identity across models sharing a row id. */
413
- receiptTargetKey(modelName, modelId) {
414
- return stableStringify([normalizeModelKey(modelName), modelId]);
415
- }
416
- /**
417
- * Relates stale notifications back to write targets without assuming the
418
- * server's canonical model name uses the same spelling as the public schema
419
- * key (`Item` versus `items`). Exact `(model,id)` wins; a globally unique id
420
- * is the compatibility fallback. An ambiguous same-id cross-model mismatch
421
- * is deliberately left unclassified, so it cannot falsely settle a queued
422
- * write. A notification with no write-target id (or an explicit group) is a
423
- * declared-read conflict and holds the whole batch.
424
- */
425
- classifyReceiptNotifications(operations, notifications) {
426
- const targets = operations.map((operation) => ({
427
- id: operation.id,
428
- key: this.receiptTargetKey(operation.model, operation.id),
429
- }));
430
- const heldTargets = new Set();
431
- const notificationsByTarget = new Map();
432
- let holdsEntireBatch = false;
433
- for (const notification of notifications) {
434
- // Scope is decided before any target matching. A group premise fires over
435
- // the WHOLE batch by convention, and its `target` now names the row that
436
- // actually moved — which may well be a row this batch is writing, so
437
- // matching first would misread a batch-wide hold as a per-row one.
438
- if (notification.scope === 'group') {
439
- holdsEntireBatch = true;
440
- continue;
441
- }
442
- const candidates = targets.filter((target) => target.id === notification.target.id);
443
- const notificationKey = this.receiptTargetKey(notification.target.model, notification.target.id);
444
- const exactTargets = candidates.filter((target) => target.key === notificationKey);
445
- const candidateKeys = new Set((exactTargets.length > 0 ? exactTargets : candidates).map((target) => target.key));
446
- if (candidates.length === 0) {
447
- holdsEntireBatch = true;
448
- continue;
449
- }
450
- if (candidateKeys.size !== 1) {
451
- // Same id across multiple differently named models with no exact match:
452
- // the id-only compatibility fallback is ambiguous. Await the echo.
453
- continue;
454
- }
455
- const [targetKey] = candidateKeys;
456
- if (!targetKey)
457
- continue;
458
- heldTargets.add(targetKey);
459
- const targetNotifications = notificationsByTarget.get(targetKey) ?? [];
460
- targetNotifications.push(notification);
461
- notificationsByTarget.set(targetKey, targetNotifications);
462
- }
463
- return { holdsEntireBatch, heldTargets, notificationsByTarget };
464
- }
405
+ /* Stale receipt classification was removed; stale premises now reject. */
465
406
  resolveConfirmation(transaction) {
466
407
  const resolver = this.confirmationResolvers.get(transaction.id);
467
408
  if (!resolver)
@@ -543,11 +484,6 @@ export class MutationQueue extends EventEmitter {
543
484
  // Track executing transactions for backpressure
544
485
  executingCount = 0;
545
486
  localMutationPort;
546
- // Stale-context notifications, keyed by transaction id. When the server
547
- // accepts a commit but reports that an operation's premise had moved,
548
- // the notification lands here from the commit acknowledgement and is drained
549
- // by `waitForCommitReceipt`, so the receipt can carry it back to the caller.
550
- commitNotifications = new Map();
551
487
  /** Zero-row targets returned on a successful atomic commit receipt. */
552
488
  commitMissingIds = new Map();
553
489
  // Delta-confirmation tracking (ack watermark advance + the awaiting_delta
@@ -1266,7 +1202,7 @@ export class MutationQueue extends EventEmitter {
1266
1202
  await handleFailure(this.failureHandlingContext, transaction, error);
1267
1203
  }
1268
1204
  async handleConflict(transaction, serverData) {
1269
- await resolveConflict(this.conflictPolicyContext, transaction, serverData);
1205
+ await resolveConflict(this.conflictResolutionContext, transaction, serverData);
1270
1206
  }
1271
1207
  applyOptimisticCreate(model, transaction) {
1272
1208
  this.localMutationPort.applyCreate(model, transaction);
@@ -1518,7 +1454,6 @@ export class MutationQueue extends EventEmitter {
1518
1454
  this.recentDeltaCorrelations.clear();
1519
1455
  this.commitLane = [];
1520
1456
  this.commitStore.clear();
1521
- this.commitNotifications.clear();
1522
1457
  this.commitMissingIds.clear();
1523
1458
  // Clear event listeners
1524
1459
  this.removeAllListeners();
@@ -3,9 +3,8 @@ import type { MutationQueueConfig } from './MutationQueue.js';
3
3
  import type { MutationInput, QueuedMutation } from './commitPayload.js';
4
4
  import type { MutationStore } from './MutationStore.js';
5
5
  import type { OptimisticUpdateEntry } from './localMutation.js';
6
- import type { StaleNotification } from '@abloatai/transaction/coordination/schema';
7
- import type { MutationCommitResult } from '@abloatai/transaction/wire/commit';
8
- import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
6
+ import type { MutationCommitResult } from '@abloatai/transaction/commit';
7
+ import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
9
8
  export interface BatchProcessingContext {
10
9
  readonly runtime: RuntimeContext;
11
10
  readonly config: MutationQueueConfig;
@@ -28,7 +27,6 @@ export interface BatchProcessingContext {
28
27
  readonly store: MutationStore;
29
28
  readonly enqueue: (transaction: QueuedMutation) => void;
30
29
  readonly optimisticUpdates: Map<string, OptimisticUpdateEntry>;
31
- readonly commitNotifications: Map<string, StaleNotification[]>;
32
30
  readonly commitMissingIds: Map<string, string[]>;
33
31
  readonly sourceMutationIdsFor: (batch: readonly QueuedMutation[]) => string[];
34
32
  readonly dispatchCommitBounded: (...args: Parameters<import('../../interfaces/index.js').MutationExecutor['commit']>) => ReturnType<import('../../interfaces/index.js').MutationExecutor['commit']>;
@@ -38,15 +36,6 @@ export interface BatchProcessingContext {
38
36
  readonly assertEnvelopeInsideReplayWindow: (envelope: DurableCommitEnvelope) => void;
39
37
  readonly sealDurableCommit: (input: Parameters<typeof import('./commitTransport.js').sealDurableCommit>[1]) => Promise<DurableCommitEnvelope>;
40
38
  readonly noteAck: (syncId: number | undefined) => void;
41
- readonly classifyReceiptNotifications: (operations: readonly {
42
- model: string;
43
- id: string;
44
- }[], notifications: readonly StaleNotification[]) => {
45
- holdsEntireBatch: boolean;
46
- heldTargets: Set<string>;
47
- notificationsByTarget: Map<string, StaleNotification[]>;
48
- };
49
- readonly receiptTargetKey: (modelName: string, modelId: string) => string;
50
39
  readonly scheduleReplicationLagTimeout: (transactionId: string, clientTxId?: string, correlationId?: string) => void;
51
40
  readonly scheduleDeltaConfirmationTimeout: (transaction: QueuedMutation, timeoutMs: number) => void;
52
41
  readonly clearReplicationLagState: (transactionId: string) => void;
@@ -87,17 +87,6 @@ export async function processBatch(ctx) {
87
87
  }));
88
88
  await ctx.persistDurableCommitAcceptance(durableEnvelope, result);
89
89
  const lastSyncId = result.lastSyncId;
90
- const notifications = result.notifications;
91
- const { holdsEntireBatch, heldTargets, notificationsByTarget, } = ctx.classifyReceiptNotifications(batchOps.map(({ op }) => ({ model: op.model, id: op.id })), notifications ?? []);
92
- for (const { tx } of batchOps) {
93
- const txTarget = ctx.receiptTargetKey(tx.modelKey, tx.modelId);
94
- const txNotifs = holdsEntireBatch
95
- ? notifications
96
- : notificationsByTarget.get(txTarget);
97
- if (txNotifs && txNotifs.length > 0) {
98
- ctx.commitNotifications.set(tx.id, txNotifs);
99
- }
100
- }
101
90
  const missingIds = new Set(result.missingIds ?? []);
102
91
  const settlingBatchOps = [];
103
92
  for (const entry of batchOps) {
@@ -106,15 +95,6 @@ export async function processBatch(ctx) {
106
95
  await ctx.handleFailure(tx, new AbloNotFoundError(`${tx.modelName}/${tx.modelId} was not found or is outside this credential's scope.`, [tx.modelId]));
107
96
  continue;
108
97
  }
109
- if (holdsEntireBatch ||
110
- heldTargets.has(ctx.receiptTargetKey(tx.modelKey, tx.modelId))) {
111
- await ctx.rollbackOptimistic(tx, 'conflict_server_wins');
112
- ctx.store.updateStatus(tx.id, 'completed');
113
- ctx.emit('transaction:completed', tx);
114
- ctx.emit(`transaction:completed:${tx.id}`, tx);
115
- ctx.optimisticUpdates.delete(tx.id);
116
- continue;
117
- }
118
98
  settlingBatchOps.push(entry);
119
99
  }
120
100
  // A forwarded write has been accepted, but the source database
@@ -1,5 +1,5 @@
1
- import type { ReadDependency, TrackDependency } from '@abloatai/transaction/coordination/schema';
2
- import type { DurableCommitEnvelope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
1
+ import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
2
+ import type { DurableCommitEnvelope } from '@abloatai/transaction/commit';
3
3
  import type { CommitTransaction } from './commitLane.js';
4
4
  import type { SealDurableCommitInput } from './commitTransport.js';
5
5
  export interface CommitApiContext {
@@ -15,5 +15,4 @@ export interface CommitApiContext {
15
15
  }
16
16
  export declare function enqueueCommit(ctx: CommitApiContext, clientTxId: string, operations: CommitTransaction['operations'], options?: {
17
17
  reads?: ReadDependency[] | null;
18
- track?: TrackDependency[] | null;
19
18
  }): Promise<void>;
@@ -8,12 +8,10 @@ export async function enqueueCommit(ctx, clientTxId, operations, options = {}) {
8
8
  const existingIntent = stableStringify({
9
9
  operations: existing.operations,
10
10
  reads: existing.reads ?? null,
11
- track: existing.track ?? null,
12
11
  });
13
12
  const incomingIntent = stableStringify({
14
13
  operations,
15
14
  reads: options.reads ?? null,
16
- track: options.track ?? null,
17
15
  });
18
16
  if (existingIntent !== incomingIntent) {
19
17
  throw new AbloIdempotencyError('Idempotency key reused with a different atomic commit request', { code: 'idempotency_conflict' });
@@ -34,7 +32,6 @@ export async function enqueueCommit(ctx, clientTxId, operations, options = {}) {
34
32
  kind: 'commit',
35
33
  operations: [...operations],
36
34
  ...(options.reads ? { reads: options.reads } : {}),
37
- ...(options.track ? { track: options.track } : {}),
38
35
  status: 'pending',
39
36
  createdAt: now,
40
37
  attempts: 0,
@@ -48,7 +45,6 @@ export async function enqueueCommit(ctx, clientTxId, operations, options = {}) {
48
45
  operations: tx.operations,
49
46
  commitOptions: {
50
47
  ...(tx.reads ? { reads: tx.reads } : {}),
51
- ...(tx.track ? { track: tx.track } : {}),
52
48
  },
53
49
  createdAt: tx.createdAt,
54
50
  sealedAt: tx.sealedAt,
@@ -1,7 +1,7 @@
1
1
  import type { RuntimeContext } from '../../RuntimeContext.js';
2
- import type { ReadDependency, TrackDependency, OnStaleMode, StaleNotification } from '@abloatai/transaction/coordination/schema';
3
- import type { CommitOperationResult, MutationCommitResult } from '@abloatai/transaction/wire/commit';
4
- import type { DurableCommitEnvelope, DurableCommitOperation } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
2
+ import type { ReadDependency } from '@abloatai/transaction/coordination/schema';
3
+ import type { CommitOperationResult, MutationCommitResult } from '@abloatai/transaction/commit';
4
+ import type { DurableCommitEnvelope, DurableCommitOperation } from '@abloatai/transaction/commit';
5
5
  import type { SealDurableCommitInput } from './commitTransport.js';
6
6
  export interface CommitTransaction {
7
7
  id: string;
@@ -13,10 +13,8 @@ export interface CommitTransaction {
13
13
  input?: Record<string, unknown>;
14
14
  transactionId?: string;
15
15
  readAt?: number | null;
16
- onStale?: OnStaleMode | null;
17
16
  }[];
18
17
  reads?: ReadDependency[] | null;
19
- track?: TrackDependency[] | null;
20
18
  status: 'pending' | 'executing' | 'awaiting_delta' | 'completed' | 'failed';
21
19
  createdAt: number;
22
20
  attempts: number;
@@ -44,7 +42,6 @@ export interface CommitLaneContext {
44
42
  };
45
43
  };
46
44
  readonly commitLane: CommitTransaction[];
47
- readonly commitNotifications: Map<string, StaleNotification[]>;
48
45
  readonly commitMissingIds: Map<string, string[]>;
49
46
  readonly commitProcessing: boolean;
50
47
  readonly setCommitProcessing: (value: boolean) => void;
@@ -65,7 +62,6 @@ export interface CommitLaneContext {
65
62
  }
66
63
  export interface CommitReceiptContext {
67
64
  readonly commitStore: Map<string, CommitTransaction>;
68
- readonly commitNotifications: Map<string, StaleNotification[]>;
69
65
  readonly commitMissingIds: Map<string, string[]>;
70
66
  readonly replicationLagErrors: Map<string, Error>;
71
67
  readonly on: (event: string, listener: (payload: object) => void) => void;
@@ -73,7 +69,6 @@ export interface CommitReceiptContext {
73
69
  }
74
70
  export declare function waitForCommitReceipt(ctx: CommitReceiptContext, clientTxId: string): Promise<{
75
71
  lastSyncId: number;
76
- notifications?: StaleNotification[];
77
72
  missingIds?: string[];
78
73
  operationResults?: CommitOperationResult[];
79
74
  }>;