@abloatai/humans 0.48.0 → 0.50.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 (106) hide show
  1. package/dist/client.d.ts +6 -0
  2. package/dist/humans.d.ts +1 -1
  3. package/dist/local/RuntimeContext.d.ts +1 -1
  4. package/dist/local/SyncClient.d.ts +1 -1
  5. package/dist/local/SyncClient.js +3 -3
  6. package/dist/local/client/createModelProxy.d.ts +7 -1
  7. package/dist/local/client/createModelProxy.js +146 -39
  8. package/dist/local/client/options.d.ts +1 -1
  9. package/dist/local/client/reactiveEngine.js +69 -7
  10. package/dist/local/client/resourceTypes.d.ts +1 -1
  11. package/dist/local/client/resourceTypes.js +1 -1
  12. package/dist/local/client/storeCluster.d.ts +3 -0
  13. package/dist/local/client/storeCluster.js +4 -2
  14. package/dist/local/client/storeLifecycle.d.ts +2 -0
  15. package/dist/local/client/storeLifecycle.js +1 -0
  16. package/dist/local/client/wsMutationExecutor.d.ts +4 -1
  17. package/dist/local/client/wsMutationExecutor.js +26 -2
  18. package/dist/local/context.js +5 -1
  19. package/dist/local/interfaces/index.d.ts +3 -1
  20. package/dist/local/mutators/UndoManager.d.ts +1 -1
  21. package/dist/local/mutators/UndoManager.js +1 -1
  22. package/dist/local/query/client.js +15 -0
  23. package/dist/local/query/types.d.ts +14 -0
  24. package/dist/local/stores/syncAction.d.ts +1 -1
  25. package/dist/local/stores/syncAction.js +1 -1
  26. package/dist/local/sync/ConnectionManager.d.ts +1 -1
  27. package/dist/local/sync/ConnectionManager.js +1 -1
  28. package/dist/local/sync/OnDemandLoader.d.ts +3 -0
  29. package/dist/local/sync/OnDemandLoader.js +15 -2
  30. package/dist/local/sync/SyncWebSocket.d.ts +1 -1
  31. package/dist/local/sync/SyncWebSocket.js +1 -1
  32. package/dist/local/sync/commitFrames.d.ts +1 -1
  33. package/dist/local/sync/commitFrames.js +1 -1
  34. package/dist/local/sync/contextPorts.d.ts +1 -1
  35. package/dist/local/sync/contextPorts.js +1 -1
  36. package/dist/local/sync/credentialLifecycle.d.ts +1 -1
  37. package/dist/local/sync/credentialLifecycle.js +1 -1
  38. package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
  39. package/dist/local/sync/wsFrameHandlers.js +1 -1
  40. package/dist/local/transactions/databaseCommitOutbox.d.ts +1 -1
  41. package/dist/local/transactions/localMutation.d.ts +1 -1
  42. package/dist/local/transactions/localMutation.js +1 -1
  43. package/dist/local/transactions/mutations/MutationQueue.d.ts +2 -2
  44. package/dist/local/transactions/mutations/MutationQueue.js +4 -4
  45. package/dist/local/transactions/mutations/batchProcessing.d.ts +1 -1
  46. package/dist/local/transactions/mutations/commitApi.d.ts +1 -1
  47. package/dist/local/transactions/mutations/commitLane.d.ts +1 -1
  48. package/dist/local/transactions/mutations/commitPayload.d.ts +2 -1
  49. package/dist/local/transactions/mutations/commitPayload.js +8 -0
  50. package/dist/local/transactions/mutations/commitTransport.d.ts +1 -1
  51. package/dist/local/transactions/mutations/commitTransport.js +1 -1
  52. package/dist/local/transactions/mutations/durableCommitRestore.d.ts +1 -1
  53. package/dist/local/transactions/mutations/durableCommitRestore.js +1 -1
  54. package/dist/local/transactions/mutations/durableWriteStore.d.ts +4 -4
  55. package/dist/local/transactions/mutations/durableWriteStore.js +3 -3
  56. package/dist/local/transactions/mutations/localMutation.d.ts +1 -1
  57. package/dist/local/transactions/mutations/mutationPersistence.d.ts +1 -1
  58. package/dist/local/transactions/mutations/pendingDrain.d.ts +2 -2
  59. package/dist/local/transactions/mutations/pendingDrain.js +1 -1
  60. package/dist/local/transactions/mutations/replayValidation.d.ts +13 -1
  61. package/dist/local/transactions/mutations/replayValidation.js +3 -2
  62. package/dist/local/transactions/reconnectDrain.d.ts +1 -1
  63. package/dist/react/useSyncStatus.d.ts +1 -1
  64. package/dist/react/useSyncStatus.js +1 -1
  65. package/package.json +2 -2
  66. package/src/Ablo.ts +1 -1
  67. package/src/client.ts +7 -0
  68. package/src/humans.ts +1 -1
  69. package/src/local/RuntimeContext.ts +1 -1
  70. package/src/local/SyncClient.ts +3 -3
  71. package/src/local/client/createModelProxy.ts +236 -55
  72. package/src/local/client/options.ts +1 -1
  73. package/src/local/client/reactiveEngine.ts +81 -4
  74. package/src/local/client/resourceTypes.ts +1 -1
  75. package/src/local/client/storeCluster.ts +12 -2
  76. package/src/local/client/storeLifecycle.ts +3 -0
  77. package/src/local/client/wsMutationExecutor.ts +43 -1
  78. package/src/local/context.ts +5 -1
  79. package/src/local/interfaces/index.ts +7 -5
  80. package/src/local/mutators/UndoManager.ts +1 -1
  81. package/src/local/query/client.ts +17 -0
  82. package/src/local/query/types.ts +16 -1
  83. package/src/local/stores/syncAction.ts +1 -1
  84. package/src/local/sync/ConnectionManager.ts +1 -1
  85. package/src/local/sync/OnDemandLoader.ts +20 -3
  86. package/src/local/sync/SyncWebSocket.ts +1 -1
  87. package/src/local/sync/commitFrames.ts +1 -1
  88. package/src/local/sync/contextPorts.ts +1 -1
  89. package/src/local/sync/credentialLifecycle.ts +1 -1
  90. package/src/local/sync/wsFrameHandlers.ts +1 -1
  91. package/src/local/transactions/databaseCommitOutbox.ts +1 -1
  92. package/src/local/transactions/localMutation.ts +1 -1
  93. package/src/local/transactions/mutations/MutationQueue.ts +4 -4
  94. package/src/local/transactions/mutations/batchProcessing.ts +1 -1
  95. package/src/local/transactions/mutations/commitApi.ts +1 -1
  96. package/src/local/transactions/mutations/commitLane.ts +1 -1
  97. package/src/local/transactions/mutations/commitPayload.ts +10 -1
  98. package/src/local/transactions/mutations/commitTransport.ts +1 -1
  99. package/src/local/transactions/mutations/durableCommitRestore.ts +2 -2
  100. package/src/local/transactions/mutations/durableWriteStore.ts +4 -4
  101. package/src/local/transactions/mutations/localMutation.ts +1 -1
  102. package/src/local/transactions/mutations/mutationPersistence.ts +1 -1
  103. package/src/local/transactions/mutations/pendingDrain.ts +2 -2
  104. package/src/local/transactions/mutations/replayValidation.ts +3 -2
  105. package/src/local/transactions/reconnectDrain.ts +1 -1
  106. package/src/react/useSyncStatus.ts +1 -1
package/dist/client.d.ts CHANGED
@@ -18,6 +18,7 @@ import type { SyncWebSocket, CoreSyncEventMap } from './local/sync/SyncWebSocket
18
18
  import type { SyncStatus } from './local/BaseSyncedStore.js';
19
19
  import type { ModelOperations } from './local/client/createModelProxy.js';
20
20
  import type { ClaimResource, CommitResource, CreateAgentClientParams, SessionResource } from '@abloatai/transaction/resources/httpResources';
21
+ import type { EffectiveAuthority } from '@abloatai/transaction/auth';
21
22
  export type { LocalReadOptions } from './local/client/resourceTypes.js';
22
23
  /** The typed sync engine client — one property per model in the schema */
23
24
  export type AbloClient<S extends SchemaRecord> = {
@@ -156,6 +157,11 @@ export type AbloClient<S extends SchemaRecord> = {
156
157
  * ```
157
158
  */
158
159
  readonly organizationId: string | null;
160
+ /**
161
+ * The effective authority of this running participant, confirmed by the
162
+ * server credential exchange/identity endpoint. `null` until `ready()`.
163
+ */
164
+ readonly identity: EffectiveAuthority | null;
159
165
  /**
160
166
  * Destroy every IndexedDB database owned by this engine. Disconnects
161
167
  * the WebSocket, releases timers, and deletes all `ablo_*` / `ablo-*`
package/dist/humans.d.ts CHANGED
@@ -2,7 +2,7 @@
2
2
  * Public human-facing local-state capability.
3
3
  *
4
4
  * The plugin and its local-state runtime belong to @abloatai/humans. The
5
- * shared settlement package supplies only the handoff identity and wire
5
+ * shared confirmation package supplies only the handoff identity and wire
6
6
  * contracts.
7
7
  */
8
8
  import type { PluginContext, AppliedChange } from './plugin.js';
@@ -25,7 +25,7 @@ export interface RuntimeContext {
25
25
  config: RuntimeConfig;
26
26
  /**
27
27
  * Model metadata lookup. Injected by the client (it wires this from its
28
- * `ModelRegistry`) so the settlement core can resolve a model's metadata
28
+ * `ModelRegistry`) so the confirmation core can resolve a model's metadata
29
29
  * without importing the registry — the dependency inversion that keeps the
30
30
  * core free of the client's `Model` layer (ADR 0013).
31
31
  */
@@ -316,7 +316,7 @@ export declare class SyncClient extends EventEmitter {
316
316
  * Called when WebSocket successfully connects (can happen independently of browser online/offline)
317
317
  */
318
318
  markConnected(): void;
319
- private drainPendingSettlements;
319
+ private drainPendingConfirmations;
320
320
  /**
321
321
  * Dispose and cleanup
322
322
  */
@@ -1029,7 +1029,7 @@ export class SyncClient extends EventEmitter {
1029
1029
  if (this.pendingStages.size > 0) {
1030
1030
  await Promise.all([...this.pendingStages]);
1031
1031
  }
1032
- await this.drainPendingSettlements();
1032
+ await this.drainPendingConfirmations();
1033
1033
  }
1034
1034
  /**
1035
1035
  * Resolve a conflict between the local model and incoming server data,
@@ -1234,7 +1234,7 @@ export class SyncClient extends EventEmitter {
1234
1234
  // Browser online state may have marked the client connected before the
1235
1235
  // WebSocket itself was ready. Always kick both durable lanes on the real
1236
1236
  // socket event, even when the high-level state did not change.
1237
- void this.drainPendingSettlements().catch((error) => {
1237
+ void this.drainPendingConfirmations().catch((error) => {
1238
1238
  this.runtime.observability.captureMutationFailure({
1239
1239
  context: 'restore-commit-outbox',
1240
1240
  error: error instanceof Error ? error : new Error(String(error)),
@@ -1242,7 +1242,7 @@ export class SyncClient extends EventEmitter {
1242
1242
  });
1243
1243
  void this.processPendingMutations();
1244
1244
  }
1245
- drainPendingSettlements() {
1245
+ drainPendingConfirmations() {
1246
1246
  return this.reconnectDrain.drain(() => this.mutationQueue.drainPending());
1247
1247
  }
1248
1248
  /**
@@ -22,6 +22,7 @@ export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease
22
22
  import type { ClaimApi, ClaimAttemptEvent, JoinOptions, LocalCountOptions, LocalReadOptions } from '@abloatai/transaction/resources/modelOperations';
23
23
  import type { HttpModelClient } from '@abloatai/transaction/transport/httpClient';
24
24
  import type { ParticipantKind } from '@abloatai/transaction/types/participant';
25
+ import { type ReadSetContext } from '@abloatai/transaction/internal/read-set';
25
26
  export interface ModelClientMeta {
26
27
  readonly key: string;
27
28
  readonly typename: string;
@@ -33,6 +34,11 @@ export declare function getModelClientMeta(modelClient: unknown): ModelClientMet
33
34
  */
34
35
  type EntityHalf = Pick<ModelTarget, 'model' | 'id'>;
35
36
  export interface ModelCollaboration {
37
+ /** Exact point evidence from the HTTP read boundary (stamp captured before data). */
38
+ readPoint(model: string, id: string): Promise<{
39
+ data: unknown;
40
+ stamp: number;
41
+ }>;
36
42
  createClaim(options: {
37
43
  /**
38
44
  * The locator, in the spelling the SDK surface and the HTTP routes use.
@@ -245,4 +251,4 @@ export declare function createModelProxy<T, C>(schemaKey: string, registeredMode
245
251
  * collaborator, a test most of all, is pushed into a cast through `unknown`
246
252
  * to supply the one method that is actually read.
247
253
  */
248
- hydration: Pick<OnDemandLoader, 'fetch'>, collaboration?: ModelCollaboration): ModelOperations<T, C>;
254
+ hydration: Pick<OnDemandLoader, 'fetch'> & Partial<Pick<OnDemandLoader, 'getReadEvidence'>>, collaboration?: ModelCollaboration, readSetContext?: ReadSetContext): ModelOperations<T, C>;
@@ -27,6 +27,7 @@ import { subTarget } from '@abloatai/transaction/coordination';
27
27
  import { declaredMeta } from '@abloatai/transaction/coordination/claimMeta';
28
28
  import { ModelScope } from '@abloatai/transaction/types';
29
29
  import { claimQueueView, resolveClaimContentionOptions, } from '@abloatai/transaction/resources/modelOperations';
30
+ import { abortReadSetCommit, capturePointRead, consumeReadSet, prepareReadSet, } from '@abloatai/transaction/internal/read-set';
30
31
  const modelClientMeta = new WeakMap();
31
32
  export function getModelClientMeta(modelClient) {
32
33
  if (typeof modelClient !== 'object' || modelClient === null)
@@ -43,7 +44,8 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
43
44
  * collaborator, a test most of all, is pushed into a cast through `unknown`
44
45
  * to supply the one method that is actually read.
45
46
  */
46
- hydration, collaboration) {
47
+ hydration, collaboration, readSetContext) {
48
+ const readSetClientIdentity = syncClient;
47
49
  /**
48
50
  * Resolve a row **this** resource owns.
49
51
  *
@@ -152,24 +154,45 @@ hydration, collaboration) {
152
154
  value.object === 'claim' &&
153
155
  typeof value.id === 'string' &&
154
156
  typeof value.release === 'function';
155
- const mutationOptions = (params) => {
157
+ const preparedMutation = (params) => {
158
+ const prepared = prepareReadSet(readSetContext, readSetClientIdentity, params.readAt, params.onStale, params.idempotencyKey, params.reads);
156
159
  const rest = {
157
- ...(params.idempotencyKey !== undefined
158
- ? { idempotencyKey: params.idempotencyKey }
159
- : {}),
160
+ ...(prepared.idempotencyKey !== undefined
161
+ ? { idempotencyKey: prepared.idempotencyKey }
162
+ : params.idempotencyKey !== undefined
163
+ ? { idempotencyKey: params.idempotencyKey }
164
+ : {}),
160
165
  ...(params.label !== undefined ? { label: params.label } : {}),
161
- ...(params.readAt !== undefined ? { readAt: params.readAt } : {}),
162
- ...(params.onStale !== undefined ? { onStale: params.onStale } : {}),
166
+ ...(prepared.readAt !== undefined
167
+ ? { readAt: prepared.readAt }
168
+ : params.readAt !== undefined
169
+ ? { readAt: params.readAt }
170
+ : {}),
171
+ ...(prepared.onStale !== undefined
172
+ ? { onStale: prepared.onStale }
173
+ : params.onStale !== undefined
174
+ ? { onStale: params.onStale }
175
+ : {}),
163
176
  ...(params.fenceToken !== undefined ? { fenceToken: params.fenceToken } : {}),
164
177
  ...(params.claimRef !== undefined ? { claimRef: params.claimRef } : {}),
165
- ...(params.reads !== undefined ? { reads: params.reads } : {}),
178
+ ...(prepared.reads !== undefined
179
+ ? { reads: prepared.reads === null ? null : [...prepared.reads] }
180
+ : params.reads !== undefined
181
+ ? { reads: params.reads }
182
+ : {}),
166
183
  ...(params.track !== undefined ? { track: params.track } : {}),
167
184
  };
168
185
  // The write-options schema — the runtime twin of the compile-time params.
169
186
  // Catches plain-JavaScript callers (for example `onStale: 'rejct'`) at the
170
187
  // call site with a typed error instead of a silent no-op or a server 400.
171
- assertWriteOptions(rest, `${schemaKey} write`);
172
- return rest;
188
+ try {
189
+ assertWriteOptions(rest, `${schemaKey} write`);
190
+ }
191
+ catch (error) {
192
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
193
+ throw error;
194
+ }
195
+ return { options: rest, prepared };
173
196
  };
174
197
  const releaseClaim = async (id) => {
175
198
  const held = activeClaims.get(id);
@@ -615,18 +638,44 @@ hydration, collaboration) {
615
638
  where: [['id', params.id]],
616
639
  limit: 1,
617
640
  });
641
+ if (readSetContext?.getStore() && collaboration) {
642
+ const read = await collaboration.readPoint(schemaKey, params.id);
643
+ const data = (read.data ?? undefined);
644
+ capturePointRead(readSetContext, readSetClientIdentity, wireModel, params.id, data, read.stamp);
645
+ return data;
646
+ }
618
647
  return rows[0];
619
648
  });
649
+ const list = guard(async (options) => {
650
+ const registry = readSetContext?.getStore();
651
+ const rows = await load(options);
652
+ if (!registry)
653
+ return rows;
654
+ for (const row of rows) {
655
+ const stamp = hydration.getReadEvidence?.(row);
656
+ if (stamp === undefined) {
657
+ // Local-first/lazy rows remain valid reads, but cannot later be used as
658
+ // guarded dependencies. `prepareReadSet` rejects them if supplied in
659
+ // `reads`; authoritative complete reads carry evidence here.
660
+ continue;
661
+ }
662
+ const id = row.id;
663
+ if (typeof id !== 'string' || id.length === 0) {
664
+ throw new AbloValidationError(`${registeredModelName}.list returned a row without an id.`, { code: 'commit_no_result' });
665
+ }
666
+ capturePointRead(readSetContext, readSetClientIdentity, wireModel, id, row, stamp);
667
+ }
668
+ return rows;
669
+ });
620
670
  const operations = {
621
671
  local,
622
672
  get,
623
673
  retrieve: get,
624
674
  // No automatic scope enrolment on bulk `list`: that would subscribe to an
625
675
  // unbounded set of rows' entity groups.
626
- list: guard(load),
676
+ list,
627
677
  create: guardWrite(async (params) => {
628
678
  const id = params.id ?? Model.generateId();
629
- const opts = mutationOptions(params);
630
679
  const claim = params.claim;
631
680
  let autoLease;
632
681
  if (claim && !isClaimHandle(claim)) {
@@ -668,16 +717,38 @@ hydration, collaboration) {
668
717
  createdAt: new Date(),
669
718
  updatedAt: new Date(),
670
719
  });
671
- const effective = {
672
- ...opts,
673
- ...(autoLease ? { claim: autoLease } : {}),
674
- ...(isClaimHandle(claim) ? { claim: { id: claim.id } } : {}),
675
- };
720
+ let prepared;
676
721
  try {
722
+ const resolved = preparedMutation(params);
723
+ prepared = resolved.prepared;
724
+ const effective = {
725
+ ...resolved.options,
726
+ ...(autoLease
727
+ ? {
728
+ claimRef: { id: autoLease.id },
729
+ ...(autoLease.fenceToken !== undefined
730
+ ? { fenceToken: autoLease.fenceToken }
731
+ : {}),
732
+ }
733
+ : {}),
734
+ ...(isClaimHandle(claim)
735
+ ? {
736
+ claimRef: { id: claim.id },
737
+ ...(claim.fenceToken !== undefined
738
+ ? { fenceToken: claim.fenceToken }
739
+ : {}),
740
+ }
741
+ : {}),
742
+ };
677
743
  syncClient.add(model, effective);
678
744
  await waitForMutation(model);
745
+ consumeReadSet(readSetContext, readSetClientIdentity, prepared.consumed, prepared.automaticCommit);
679
746
  return modelAsRow(model);
680
747
  }
748
+ catch (error) {
749
+ abortReadSetCommit(readSetContext, prepared?.automaticCommit ?? false);
750
+ throw error;
751
+ }
681
752
  finally {
682
753
  await autoLease?.release?.().catch(() => { });
683
754
  }
@@ -712,11 +783,11 @@ hydration, collaboration) {
712
783
  // `type: 'complete'` forces the round-trip — the hydration ledger
713
784
  // would otherwise serve a possibly-stale local row for a hydrated id.
714
785
  await load({ where: [['id', id]], type: 'complete' });
715
- const fresh = ownRowOrThrow(id);
716
- const snapshot = collaboration.createSnapshot(schemaKey, id);
786
+ const exact = await collaboration.readPoint(wireModel, id);
787
+ capturePointRead(readSetContext, readSetClientIdentity, wireModel, id, exact.data, exact.stamp);
717
788
  return {
718
- data: fresh ? modelAsRow(fresh) : undefined,
719
- stamp: snapshot.stamp,
789
+ data: exact.data,
790
+ stamp: exact.stamp,
720
791
  };
721
792
  },
722
793
  writeNext: async (patch, readAt) => {
@@ -724,14 +795,28 @@ hydration, collaboration) {
724
795
  if (!model) {
725
796
  throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
726
797
  }
727
- const effective = {
728
- readAt,
729
- onStale: 'reject',
730
- };
731
- model.applyChanges(patch);
732
- syncClient.update(model, effective);
733
- await waitForMutation(model);
734
- return modelAsRow(model);
798
+ const prepared = prepareReadSet(readSetContext, readSetClientIdentity, undefined, 'reject', undefined, contention?.reads);
799
+ try {
800
+ const effective = {
801
+ readAt: prepared.readAt ?? readAt,
802
+ onStale: prepared.onStale ?? 'reject',
803
+ ...(prepared.idempotencyKey
804
+ ? { idempotencyKey: prepared.idempotencyKey }
805
+ : {}),
806
+ ...(prepared.reads !== undefined
807
+ ? { reads: prepared.reads === null ? null : [...prepared.reads] }
808
+ : {}),
809
+ };
810
+ model.applyChanges(patch);
811
+ syncClient.update(model, effective);
812
+ await waitForMutation(model);
813
+ consumeReadSet(readSetContext, readSetClientIdentity, prepared.consumed, prepared.automaticCommit);
814
+ return modelAsRow(model);
815
+ }
816
+ catch (error) {
817
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
818
+ throw error;
819
+ }
735
820
  },
736
821
  });
737
822
  }
@@ -758,13 +843,17 @@ hydration, collaboration) {
758
843
  // If we hold a claim on this row, guard the write with its snapshot
759
844
  // watermark + lease so it's stale-rejected and attributed to the claim.
760
845
  const claimed = activeClaims.get(id);
761
- const opts = mutationOptions(params);
846
+ const resolved = preparedMutation(params);
847
+ const opts = resolved.options;
762
848
  const handle = isClaimHandle(params.claim) ? params.claim : undefined;
763
849
  const effective = claimed
764
850
  ? {
765
851
  readAt: claimed.lease.readAt ?? claimed.snapshot.stamp,
766
852
  onStale: 'reject',
767
853
  claimRef: { id: claimed.lease.id },
854
+ ...(claimed.lease.fenceToken !== undefined
855
+ ? { fenceToken: claimed.lease.fenceToken }
856
+ : {}),
768
857
  ...opts,
769
858
  }
770
859
  : {
@@ -781,16 +870,23 @@ hydration, collaboration) {
781
870
  }
782
871
  : {}),
783
872
  ...opts,
784
- ...(handle ? { claim: { id: handle.id } } : {}),
873
+ ...(handle ? { claimRef: { id: handle.id } } : {}),
785
874
  };
786
875
  // Local user update: `applyChanges` keeps change tracking on so the
787
876
  // edited fields land in `modifiedProperties` and are actually sent to
788
877
  // the server. (`updateFromData` is the hydration path and would discard
789
878
  // the tracking, producing an empty `input: {}` no-op mutation.)
790
- model.applyChanges(params.data);
791
- syncClient.update(model, effective);
792
- await waitForMutation(model);
793
- return modelAsRow(model);
879
+ try {
880
+ model.applyChanges(params.data);
881
+ syncClient.update(model, effective);
882
+ await waitForMutation(model);
883
+ consumeReadSet(readSetContext, readSetClientIdentity, resolved.prepared.consumed, resolved.prepared.automaticCommit);
884
+ return modelAsRow(model);
885
+ }
886
+ catch (error) {
887
+ abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
888
+ throw error;
889
+ }
794
890
  });
795
891
  function update(arg, updater, contention) {
796
892
  return updateImpl(arg, updater, contention);
@@ -826,7 +922,8 @@ hydration, collaboration) {
826
922
  if (!model)
827
923
  return;
828
924
  const claimed = activeClaims.get(id);
829
- const opts = mutationOptions(params);
925
+ const resolved = preparedMutation(params);
926
+ const opts = resolved.options;
830
927
  const handle = isClaimHandle(params.claim) ? params.claim : undefined;
831
928
  const effective = claimed
832
929
  ? {
@@ -843,13 +940,23 @@ hydration, collaboration) {
843
940
  ? {
844
941
  readAt: handle.readAt,
845
942
  onStale: 'reject',
943
+ ...(handle.fenceToken !== undefined
944
+ ? { fenceToken: handle.fenceToken }
945
+ : {}),
846
946
  }
847
947
  : {}),
848
948
  ...opts,
849
- ...(handle ? { claim: { id: handle.id } } : {}),
949
+ ...(handle ? { claimRef: { id: handle.id } } : {}),
850
950
  };
851
- syncClient.delete(model, effective);
852
- await waitForMutation(model);
951
+ try {
952
+ syncClient.delete(model, effective);
953
+ await waitForMutation(model);
954
+ consumeReadSet(readSetContext, readSetClientIdentity, resolved.prepared.consumed, resolved.prepared.automaticCommit);
955
+ }
956
+ catch (error) {
957
+ abortReadSetCommit(readSetContext, resolved.prepared.automaticCommit);
958
+ throw error;
959
+ }
853
960
  }),
854
961
  // `claim` is a callable namespace (take a claim) carrying the coordination
855
962
  // readers (`claim.state` / `claim.queue` / `claim.release` / `claim.reorder`).
@@ -8,7 +8,7 @@ import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
8
8
  import type { RuntimeConfig, Logger, MutationExecutor, ObservabilityProvider, Analytics, SessionErrorDetector, OnlineStatusProvider } from '../interfaces/index.js';
9
9
  import type { AbloPersistence } from '../persistence.js';
10
10
  import type { DurableWriteStore, DurableWritesConfig } from '@abloatai/transaction/durableWrites';
11
- import type { CommitOutboxScope } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
11
+ import type { CommitOutboxScope } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
12
12
  /**
13
13
  * An async function that resolves an apiKey at request time. Use it for credential
14
14
  * rotation — read from a vault, refresh from session storage, or pull from an
@@ -12,9 +12,9 @@
12
12
  * design step (docs/plans/package-split.md).
13
13
  */
14
14
  import { omittedModelError } from '@abloatai/transaction/schema/select';
15
- import { durableCommitOperationSchema, } from '@abloatai/transaction/transactions/settlement/commitEnvelope';
15
+ import { durableCommitOperationSchema, } from '@abloatai/transaction/transactions/confirmation/commitEnvelope';
16
16
  import { AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError } from '@abloatai/transaction/errors';
17
- import { batchFence, fenceTokenFor, modelTarget, streamTarget, subTarget, } from '@abloatai/transaction/coordination';
17
+ import { batchFence, claimIdFor, fenceTokenFor, modelTarget, streamTarget, subTarget, } from '@abloatai/transaction/coordination';
18
18
  import { validateAbloOptions } from './validateAbloOptions.js';
19
19
  import { mintSession } from '@abloatai/transaction/auth/sessionMint';
20
20
  import { revokeCapability, rotateCapability, } from '@abloatai/transaction/auth/capabilityLifecycle';
@@ -28,14 +28,42 @@ import { resolveApiKeyValue, resolveBootstrapBaseUrl } from '@abloatai/transacti
28
28
  import { claimAttemptFailure, emitClaimStatus, } from '@abloatai/transaction/resources/modelOperations';
29
29
  import { createModelProxy } from './createModelProxy.js';
30
30
  import { assertWriteOptions } from '@abloatai/transaction/resources/writeOptionsSchema';
31
+ import { modelReadResponseSchema, commitRecordSchema, commitRecordListSchema, commitRecordWhereSchema, } from '@abloatai/transaction/wire';
32
+ import { translateHttpError } from '@abloatai/transaction/errors';
33
+ import { kReadEvidence } from '@abloatai/transaction/internal/read-set';
31
34
  export function buildReactiveEngine(inputs) {
32
35
  const { options, internalOptions, url, logger, configuredApiKey, configuredAuthToken, credentialResolver, authCredentials, transport, participantId, kind, presence, cluster, createSibling, } = inputs;
33
36
  const schema = options.schema;
37
+ const pointReadBaseUrl = resolveBootstrapBaseUrl({
38
+ url,
39
+ bootstrapBaseUrl: internalOptions.bootstrapBaseUrl,
40
+ });
41
+ async function readPoint(model, id) {
42
+ const fetchImpl = internalOptions.fetch ?? globalThis.fetch;
43
+ const response = await fetchImpl(`${pointReadBaseUrl}/v1/models/${encodeURIComponent(model)}/${encodeURIComponent(id)}`, { headers: authCredentials.withAuthHeaders() });
44
+ let body;
45
+ try {
46
+ body = await response.json();
47
+ }
48
+ catch {
49
+ body = null;
50
+ }
51
+ if (!response.ok)
52
+ throw translateHttpError(response.status, body);
53
+ const parsed = modelReadResponseSchema.safeParse(body);
54
+ if (!parsed.success) {
55
+ throw new AbloConnectionError('Model point-read response failed validation.', {
56
+ code: 'commit_no_result',
57
+ cause: parsed.error,
58
+ });
59
+ }
60
+ return { data: parsed.data.data, stamp: parsed.data.stamp };
61
+ }
34
62
  // The store cluster — this client's runtime, the component graph, the
35
63
  // registered models, and the store — was constructed by `humans().init`
36
64
  // from the widened plugin context (see `./storeCluster.ts`). The engine
37
65
  // assembles around it.
38
- const { components, store } = cluster;
66
+ const { components, store, readSetContext } = cluster;
39
67
  const { modelRegistry, objectPool, syncClient, hydration, } = components;
40
68
  // Self identity, late-bound the same way the connection's values are: the
41
69
  // construction-time guess seeds it (correct on the self-hosted path, empty
@@ -93,6 +121,7 @@ export function buildReactiveEngine(inputs) {
93
121
  /** Resolved account scope — seeded once identity resolution completes;
94
122
  * exposed as the readonly `ablo.organizationId` accessor. */
95
123
  let _resolvedOrganizationId = null;
124
+ let _resolvedIdentity = null;
96
125
  const lifecycle = startStoreLifecycle({
97
126
  cluster,
98
127
  schema,
@@ -105,10 +134,11 @@ export function buildReactiveEngine(inputs) {
105
134
  kind,
106
135
  logger,
107
136
  validationError: _validationError,
108
- onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups }) => {
137
+ onIdentityResolved: ({ userId, participantKind, accountScope, syncGroups, authority }) => {
109
138
  selfParticipantId = userId;
110
139
  selfParticipantKind = participantKind;
111
140
  _resolvedOrganizationId = accountScope;
141
+ _resolvedIdentity = authority;
112
142
  presenceStream.setParticipant({
113
143
  id: userId,
114
144
  kind: participantKind,
@@ -145,7 +175,7 @@ export function buildReactiveEngine(inputs) {
145
175
  ? crypto.randomUUID()
146
176
  : `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
147
177
  }
148
- function normalizeCommitOperation(op, defaults, fence) {
178
+ function normalizeCommitOperation(op, defaults, fence, claim) {
149
179
  const type = op.action.toUpperCase();
150
180
  const id = op.id ?? '';
151
181
  return durableCommitOperationSchema.parse({
@@ -154,6 +184,7 @@ export function buildReactiveEngine(inputs) {
154
184
  id,
155
185
  input: op.data ?? undefined,
156
186
  transactionId: op.transactionId ?? undefined,
187
+ claimId: op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null) ?? undefined,
157
188
  readAt: op.readAt ?? defaults.readAt ?? undefined,
158
189
  onStale: op.onStale ?? defaults.onStale ?? undefined,
159
190
  fenceToken: op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null) ?? undefined,
@@ -163,7 +194,7 @@ export function buildReactiveEngine(inputs) {
163
194
  if (commitOptions.operations.length === 0) {
164
195
  throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
165
196
  }
166
- return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fence));
197
+ return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions, fence, commitOptions.claim ?? null));
167
198
  }
168
199
  function modelClaimFromActive(claim) {
169
200
  const target = {
@@ -377,6 +408,7 @@ export function buildReactiveEngine(inputs) {
377
408
  const registeredModelName = modelDef.typename ?? schemaKey;
378
409
  modelProxies[schemaKey] = createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, modelRegistry, hydration, {
379
410
  createClaim: (claimOptions) => publicClaims.create(claimOptions),
411
+ readPoint,
380
412
  createSnapshot: (modelKey, id) => createSnapshot({
381
413
  pool: objectPool,
382
414
  transport,
@@ -429,7 +461,7 @@ export function buildReactiveEngine(inputs) {
429
461
  scope: { [modelKey]: ids },
430
462
  ...(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
431
463
  }),
432
- });
464
+ }, readSetContext);
433
465
  }
434
466
  const commits = {
435
467
  async create(commitOptions) {
@@ -479,6 +511,29 @@ export function buildReactiveEngine(inputs) {
479
511
  ...(missingIds && missingIds.length > 0 ? { missingIds } : {}),
480
512
  };
481
513
  },
514
+ async get({ id }) {
515
+ await ready();
516
+ const response = await (internalOptions.fetch ?? globalThis.fetch)(`${pointReadBaseUrl}/v1/commits/${encodeURIComponent(id)}`, { headers: authCredentials.withAuthHeaders() });
517
+ const body = await response.json().catch(() => null);
518
+ if (!response.ok)
519
+ throw translateHttpError(response.status, body);
520
+ return commitRecordSchema.nullable().parse(body);
521
+ },
522
+ async list(listOptions = {}) {
523
+ await ready();
524
+ const where = commitRecordWhereSchema.parse(listOptions.where ?? {});
525
+ const params = new URLSearchParams();
526
+ if (where.actorId)
527
+ params.set('actorId', where.actorId);
528
+ if (where.status)
529
+ params.set('status', where.status);
530
+ const query = params.size > 0 ? `?${params.toString()}` : '';
531
+ const response = await (internalOptions.fetch ?? globalThis.fetch)(`${pointReadBaseUrl}/v1/commits${query}`, { headers: authCredentials.withAuthHeaders() });
532
+ const body = await response.json().catch(() => null);
533
+ if (!response.ok)
534
+ throw translateHttpError(response.status, body);
535
+ return commitRecordListSchema.parse(body);
536
+ },
482
537
  };
483
538
  /**
484
539
  * The control-plane credential: always the original configured secret key.
@@ -560,6 +615,9 @@ export function buildReactiveEngine(inputs) {
560
615
  get organizationId() {
561
616
  return _resolvedOrganizationId;
562
617
  },
618
+ get identity() {
619
+ return _resolvedIdentity;
620
+ },
563
621
  nudgeReconnect() {
564
622
  store.nudgeReconnect();
565
623
  },
@@ -716,6 +774,10 @@ export function buildReactiveEngine(inputs) {
716
774
  });
717
775
  },
718
776
  };
777
+ Object.defineProperty(engine, kReadEvidence, {
778
+ value: { context: cluster.readSetContext, client: syncClient },
779
+ enumerable: false,
780
+ });
719
781
  // A model the schema projection left out answers with an error naming the
720
782
  // model and the fix, not `undefined`. An app can compile against the full
721
783
  // source schema while running a projection, so the type system never sees
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The interactive resource-type surface.
3
3
  *
4
- * The transport-facing half of these types moved down into the settlement core
4
+ * The transport-facing half of these types moved down into the confirmation core
5
5
  * (ADR 0016). This module keeps the
6
6
  * consumer-facing import path intact and rejoins it with the model-proxy types
7
7
  * that stay here — `ModelOperations` chief among them, because it returns the
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * The interactive resource-type surface.
3
3
  *
4
- * The transport-facing half of these types moved down into the settlement core
4
+ * The transport-facing half of these types moved down into the confirmation core
5
5
  * (ADR 0016). This module keeps the
6
6
  * consumer-facing import path intact and rejoins it with the model-proxy types
7
7
  * that stay here — `ModelOperations` chief among them, because it returns the
@@ -19,6 +19,7 @@ import { type RuntimeContext } from '../RuntimeContext.js';
19
19
  import { BaseSyncedStore } from '../BaseSyncedStore.js';
20
20
  import { type InternalComponents } from './createInternalComponents.js';
21
21
  import type { InternalAbloOptions } from './options.js';
22
+ import { type ReadSetContext } from '@abloatai/transaction/internal/read-set';
22
23
  export type { InternalAbloOptions } from './options.js';
23
24
  /**
24
25
  * The private handoff slot on the humans surface. Symbol-keyed on purpose:
@@ -34,6 +35,8 @@ export interface StoreCluster {
34
35
  readonly components: InternalComponents;
35
36
  /** The store orchestrating the graph, holding the host-built connection. */
36
37
  readonly store: BaseSyncedStore;
38
+ /** Client-local opaque evidence registry; never an ambient execution scope. */
39
+ readonly readSetContext: ReadSetContext;
37
40
  }
38
41
  /**
39
42
  * Builds the cluster from what the context carries, or returns `null` when
@@ -24,6 +24,7 @@ import { createInternalComponents, } from './createInternalComponents.js';
24
24
  import { registerModelsFromSchema } from './modelRegistration.js';
25
25
  import { deriveConfigFromSchema } from './schemaConfig.js';
26
26
  import { createDefaultMutationExecutor } from './wsMutationExecutor.js';
27
+ import { createReadSetContext, } from '@abloatai/transaction/internal/read-set';
27
28
  /**
28
29
  * The private handoff slot on the humans surface. Symbol-keyed on purpose:
29
30
  * surface members merge onto the client by string key, and the cluster is
@@ -70,6 +71,7 @@ export function buildStoreCluster(context) {
70
71
  return null;
71
72
  }
72
73
  const schema = options.schema;
74
+ const readSetContext = createReadSetContext();
73
75
  // Config derives from the schema; caller-supplied overrides layer on top,
74
76
  // caller winning per key.
75
77
  const config = {
@@ -80,7 +82,7 @@ export function buildStoreCluster(context) {
80
82
  // connection; before it opens, sends reject with the diagnosed not-ready
81
83
  // error and the MutationQueue owns the retry. A caller-supplied executor
82
84
  // still wins (test mocks, alternative transports).
83
- const executor = options.mutationExecutor ?? createDefaultMutationExecutor(() => transport);
85
+ const executor = options.mutationExecutor ?? createDefaultMutationExecutor(() => transport, readSetContext);
84
86
  // This client's runtime — the instance the whole graph is constructed
85
87
  // with, so two clients in one process never read each other's logger,
86
88
  // config, or executor. `getModelMetadata` closes over this client's own
@@ -129,5 +131,5 @@ export function buildStoreCluster(context) {
129
131
  // Collaboration vocabulary is the application's: the SDK subscribes to
130
132
  // the event types the caller declares and to nothing by default.
131
133
  { collaborationEvents: options.collaborationEvents ?? [] });
132
- return { runtime, components, store };
134
+ return { runtime, components, store, readSetContext };
133
135
  }