@abloatai/transaction 0.47.0 → 0.49.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 (270) hide show
  1. package/CONVENTIONS.md +7 -7
  2. package/README.md +1 -1
  3. package/dist/ai-sdk/modelTools.d.ts +1 -1
  4. package/dist/ai-sdk/modelTools.js +1 -1
  5. package/dist/ai-sdk/updateTool.d.ts +2 -2
  6. package/dist/ai-sdk/updateTool.d.ts.map +1 -1
  7. package/dist/auth/bootstrapScope.d.ts +1 -1
  8. package/dist/auth/capability.d.ts +32 -10
  9. package/dist/auth/capability.d.ts.map +1 -1
  10. package/dist/auth/capability.js +9 -3
  11. package/dist/auth/capability.js.map +1 -1
  12. package/dist/auth/capabilityLifecycle.d.ts +2 -2
  13. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  14. package/dist/auth/capabilityLifecycle.js +3 -2
  15. package/dist/auth/capabilityLifecycle.js.map +1 -1
  16. package/dist/auth/identity.d.ts +3 -0
  17. package/dist/auth/identity.d.ts.map +1 -1
  18. package/dist/auth/identity.js +13 -1
  19. package/dist/auth/identity.js.map +1 -1
  20. package/dist/auth/index.d.ts +2 -2
  21. package/dist/auth/index.d.ts.map +1 -1
  22. package/dist/auth/index.js +1 -1
  23. package/dist/auth/index.js.map +1 -1
  24. package/dist/auth/schemas.d.ts +18 -1
  25. package/dist/auth/schemas.d.ts.map +1 -1
  26. package/dist/auth/schemas.js +3 -1
  27. package/dist/auth/schemas.js.map +1 -1
  28. package/dist/auth/sessionMint.d.ts.map +1 -1
  29. package/dist/auth/sessionMint.js +0 -1
  30. package/dist/auth/sessionMint.js.map +1 -1
  31. package/dist/branches.d.ts +61 -12
  32. package/dist/branches.d.ts.map +1 -1
  33. package/dist/branches.js +25 -3
  34. package/dist/branches.js.map +1 -1
  35. package/dist/commitRecordRuntime.d.ts +30 -0
  36. package/dist/commitRecordRuntime.d.ts.map +1 -0
  37. package/dist/commitRecordRuntime.js +188 -0
  38. package/dist/commitRecordRuntime.js.map +1 -0
  39. package/dist/coordination/index.d.ts +3 -3
  40. package/dist/coordination/index.d.ts.map +1 -1
  41. package/dist/coordination/index.js +2 -2
  42. package/dist/coordination/index.js.map +1 -1
  43. package/dist/coordination/locator.d.ts +5 -0
  44. package/dist/coordination/locator.d.ts.map +1 -1
  45. package/dist/coordination/locator.js +12 -0
  46. package/dist/coordination/locator.js.map +1 -1
  47. package/dist/coordination/schema.d.ts +205 -15
  48. package/dist/coordination/schema.d.ts.map +1 -1
  49. package/dist/coordination/schema.js +88 -47
  50. package/dist/coordination/schema.js.map +1 -1
  51. package/dist/coordination/targetConflict.js +1 -1
  52. package/dist/durableWrites.d.ts +3 -3
  53. package/dist/durableWrites.d.ts.map +1 -1
  54. package/dist/durableWrites.js +1 -1
  55. package/dist/errorCodes.d.ts +35 -2
  56. package/dist/errorCodes.d.ts.map +1 -1
  57. package/dist/errorCodes.js +67 -19
  58. package/dist/errorCodes.js.map +1 -1
  59. package/dist/errorObservation.d.ts +82 -0
  60. package/dist/errorObservation.d.ts.map +1 -0
  61. package/dist/errorObservation.js +101 -0
  62. package/dist/errorObservation.js.map +1 -0
  63. package/dist/errors.d.ts +2 -0
  64. package/dist/errors.d.ts.map +1 -1
  65. package/dist/errors.js +6 -1
  66. package/dist/errors.js.map +1 -1
  67. package/dist/footprint.d.ts +10 -22
  68. package/dist/footprint.d.ts.map +1 -1
  69. package/dist/footprint.js +12 -24
  70. package/dist/footprint.js.map +1 -1
  71. package/dist/index.d.ts +5 -2
  72. package/dist/index.d.ts.map +1 -1
  73. package/dist/index.js +1 -1
  74. package/dist/index.js.map +1 -1
  75. package/dist/internal/read-set.d.ts +8 -0
  76. package/dist/internal/read-set.d.ts.map +1 -0
  77. package/dist/internal/read-set.js +7 -0
  78. package/dist/internal/read-set.js.map +1 -0
  79. package/dist/logger.d.ts +1 -1
  80. package/dist/observability.d.ts +1 -1
  81. package/dist/observability.js +1 -1
  82. package/dist/readSetContext.d.ts +54 -0
  83. package/dist/readSetContext.d.ts.map +1 -0
  84. package/dist/readSetContext.js +98 -0
  85. package/dist/readSetContext.js.map +1 -0
  86. package/dist/resources/functionalUpdate.d.ts +13 -3
  87. package/dist/resources/functionalUpdate.d.ts.map +1 -1
  88. package/dist/resources/functionalUpdate.js.map +1 -1
  89. package/dist/resources/httpResources.d.ts +23 -7
  90. package/dist/resources/httpResources.d.ts.map +1 -1
  91. package/dist/resources/modelOperations.d.ts +2 -2
  92. package/dist/resources/modelOperations.js +1 -1
  93. package/dist/resources/mutationOptions.d.ts +1 -1
  94. package/dist/resources/mutationOptions.js +1 -1
  95. package/dist/resources/where.d.ts +1 -1
  96. package/dist/resources/where.js +1 -1
  97. package/dist/resources/writeOptionsSchema.d.ts +39 -4
  98. package/dist/resources/writeOptionsSchema.d.ts.map +1 -1
  99. package/dist/resources/writeOptionsSchema.js +12 -6
  100. package/dist/resources/writeOptionsSchema.js.map +1 -1
  101. package/dist/schema/index.js +1 -1
  102. package/dist/schema/index.js.map +1 -1
  103. package/dist/schema/openapi.d.ts.map +1 -1
  104. package/dist/schema/openapi.js +45 -11
  105. package/dist/schema/openapi.js.map +1 -1
  106. package/dist/server/commit.d.ts +7 -1
  107. package/dist/server/commit.d.ts.map +1 -1
  108. package/dist/server/index.d.ts +1 -1
  109. package/dist/server/index.d.ts.map +1 -1
  110. package/dist/server/index.js +1 -1
  111. package/dist/server/index.js.map +1 -1
  112. package/dist/source/adapters/kysely.d.ts.map +1 -1
  113. package/dist/source/adapters/kysely.js +11 -4
  114. package/dist/source/adapters/kysely.js.map +1 -1
  115. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  116. package/dist/source/adapters/kyselyMutationCore.js +14 -2
  117. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  118. package/dist/source/contract.d.ts +2 -0
  119. package/dist/source/contract.d.ts.map +1 -1
  120. package/dist/source/index.d.ts +1 -1
  121. package/dist/source/index.d.ts.map +1 -1
  122. package/dist/source/types.d.ts +1 -1
  123. package/dist/testing/fixtures/httpResponses.d.ts +30 -1
  124. package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
  125. package/dist/testing/fixtures/httpResponses.js +48 -1
  126. package/dist/testing/fixtures/httpResponses.js.map +1 -1
  127. package/dist/transactionLayer.d.ts +1 -1
  128. package/dist/transactions/{settlement → confirmation}/commitEnvelope.d.ts +4 -2
  129. package/dist/transactions/confirmation/commitEnvelope.d.ts.map +1 -0
  130. package/dist/transactions/{settlement → confirmation}/commitEnvelope.js +8 -3
  131. package/dist/transactions/confirmation/commitEnvelope.js.map +1 -0
  132. package/dist/transactions/confirmation/httpCommitEnvelope.d.ts.map +1 -0
  133. package/dist/transactions/confirmation/httpCommitEnvelope.js.map +1 -0
  134. package/dist/transactions/confirmation/idempotencyKey.d.ts.map +1 -0
  135. package/dist/transactions/confirmation/idempotencyKey.js.map +1 -0
  136. package/dist/transactions/{settlement → confirmation}/pendingWrite.d.ts +3 -2
  137. package/dist/transactions/confirmation/pendingWrite.d.ts.map +1 -0
  138. package/dist/transactions/confirmation/pendingWrite.js.map +1 -0
  139. package/dist/transport/commitFrames.d.ts +1 -0
  140. package/dist/transport/commitFrames.d.ts.map +1 -1
  141. package/dist/transport/commitFrames.js +8 -7
  142. package/dist/transport/commitFrames.js.map +1 -1
  143. package/dist/transport/connectionManager.d.ts +1 -1
  144. package/dist/transport/connectionManager.js +1 -1
  145. package/dist/transport/httpClient.d.ts +23 -9
  146. package/dist/transport/httpClient.d.ts.map +1 -1
  147. package/dist/transport/httpClient.js +134 -15
  148. package/dist/transport/httpClient.js.map +1 -1
  149. package/dist/transport/httpOptions.d.ts +9 -1
  150. package/dist/transport/httpOptions.d.ts.map +1 -1
  151. package/dist/transport/httpTransport.d.ts +3 -0
  152. package/dist/transport/httpTransport.d.ts.map +1 -1
  153. package/dist/transport/httpTransport.js +109 -22
  154. package/dist/transport/httpTransport.js.map +1 -1
  155. package/dist/transport/wsFrameHandlers.d.ts +3 -1
  156. package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
  157. package/dist/transport/wsFrameHandlers.js +38 -17
  158. package/dist/transport/wsFrameHandlers.js.map +1 -1
  159. package/dist/transport/wsTransport.d.ts +3 -0
  160. package/dist/transport/wsTransport.d.ts.map +1 -1
  161. package/dist/transport/wsTransport.js +32 -1
  162. package/dist/transport/wsTransport.js.map +1 -1
  163. package/dist/utils/json.d.ts +1 -1
  164. package/dist/utils/json.js +1 -1
  165. package/dist/wire/accountResponses.d.ts +1 -1
  166. package/dist/wire/auth.d.ts +2 -2
  167. package/dist/wire/commit.d.ts +1652 -213
  168. package/dist/wire/commit.d.ts.map +1 -1
  169. package/dist/wire/commit.js +196 -132
  170. package/dist/wire/commit.js.map +1 -1
  171. package/dist/wire/delta.d.ts +1 -1
  172. package/dist/wire/errorEnvelope.d.ts +1 -0
  173. package/dist/wire/errorEnvelope.d.ts.map +1 -1
  174. package/dist/wire/errorEnvelope.js +1 -0
  175. package/dist/wire/errorEnvelope.js.map +1 -1
  176. package/dist/wire/feedCursor.d.ts +1 -1
  177. package/dist/wire/feedCursor.js +1 -1
  178. package/dist/wire/feedEvent.d.ts +3 -3
  179. package/dist/wire/frames.d.ts +8 -4
  180. package/dist/wire/frames.d.ts.map +1 -1
  181. package/dist/wire/frames.js +6 -3
  182. package/dist/wire/frames.js.map +1 -1
  183. package/dist/wire/inboundFrames.d.ts +44 -4
  184. package/dist/wire/inboundFrames.d.ts.map +1 -1
  185. package/dist/wire/index.d.ts +6 -4
  186. package/dist/wire/index.d.ts.map +1 -1
  187. package/dist/wire/index.js +6 -5
  188. package/dist/wire/index.js.map +1 -1
  189. package/dist/wire/modelMutations.d.ts +35 -0
  190. package/dist/wire/modelMutations.d.ts.map +1 -1
  191. package/dist/wire/modelMutations.js +9 -2
  192. package/dist/wire/modelMutations.js.map +1 -1
  193. package/dist/wire/modelResponses.d.ts +12 -1
  194. package/dist/wire/modelResponses.d.ts.map +1 -1
  195. package/dist/wire/modelResponses.js +12 -1
  196. package/dist/wire/modelResponses.js.map +1 -1
  197. package/package.json +12 -2
  198. package/src/ai-sdk/modelTools.ts +1 -1
  199. package/src/ai-sdk/updateTool.ts +2 -2
  200. package/src/auth/bootstrapScope.ts +1 -1
  201. package/src/auth/capability.ts +13 -4
  202. package/src/auth/capabilityLifecycle.ts +3 -2
  203. package/src/auth/identity.ts +16 -1
  204. package/src/auth/index.ts +2 -0
  205. package/src/auth/schemas.ts +3 -0
  206. package/src/auth/sessionMint.ts +0 -1
  207. package/src/branches.ts +27 -3
  208. package/src/commitRecordRuntime.ts +245 -0
  209. package/src/coordination/index.ts +24 -1
  210. package/src/coordination/locator.ts +17 -0
  211. package/src/coordination/schema.ts +114 -49
  212. package/src/coordination/targetConflict.ts +1 -1
  213. package/src/durableWrites.ts +3 -3
  214. package/src/errorCodes.ts +205 -25
  215. package/src/errorObservation.ts +121 -0
  216. package/src/errors.ts +5 -1
  217. package/src/footprint.ts +16 -27
  218. package/src/index.ts +8 -1
  219. package/src/internal/read-set.ts +18 -0
  220. package/src/logger.ts +1 -1
  221. package/src/observability.ts +1 -1
  222. package/src/readSetContext.ts +164 -0
  223. package/src/resources/functionalUpdate.ts +16 -4
  224. package/src/resources/httpResources.ts +27 -8
  225. package/src/resources/modelOperations.ts +2 -2
  226. package/src/resources/mutationOptions.ts +1 -1
  227. package/src/resources/where.ts +1 -1
  228. package/src/resources/writeOptionsSchema.ts +30 -12
  229. package/src/schema/index.ts +1 -1
  230. package/src/schema/openapi.ts +52 -11
  231. package/src/server/commit.ts +7 -1
  232. package/src/server/index.ts +5 -1
  233. package/src/source/adapters/kysely.ts +12 -4
  234. package/src/source/adapters/kyselyMutationCore.ts +14 -2
  235. package/src/source/index.ts +1 -1
  236. package/src/source/types.ts +1 -1
  237. package/src/testing/fixtures/httpResponses.ts +60 -1
  238. package/src/transactionLayer.ts +1 -1
  239. package/src/transactions/{settlement → confirmation}/commitEnvelope.ts +13 -3
  240. package/src/transport/commitFrames.ts +7 -5
  241. package/src/transport/connectionManager.ts +1 -1
  242. package/src/transport/httpClient.ts +256 -29
  243. package/src/transport/httpOptions.ts +9 -1
  244. package/src/transport/httpTransport.ts +139 -27
  245. package/src/transport/wsFrameHandlers.ts +46 -8
  246. package/src/transport/wsTransport.ts +42 -1
  247. package/src/utils/json.ts +1 -1
  248. package/src/wire/commit.ts +248 -147
  249. package/src/wire/errorEnvelope.ts +1 -0
  250. package/src/wire/feedCursor.ts +1 -1
  251. package/src/wire/frames.ts +9 -4
  252. package/src/wire/index.ts +38 -5
  253. package/src/wire/modelMutations.ts +16 -2
  254. package/src/wire/modelResponses.ts +14 -1
  255. package/dist/transactions/settlement/commitEnvelope.d.ts.map +0 -1
  256. package/dist/transactions/settlement/commitEnvelope.js.map +0 -1
  257. package/dist/transactions/settlement/httpCommitEnvelope.d.ts.map +0 -1
  258. package/dist/transactions/settlement/httpCommitEnvelope.js.map +0 -1
  259. package/dist/transactions/settlement/idempotencyKey.d.ts.map +0 -1
  260. package/dist/transactions/settlement/idempotencyKey.js.map +0 -1
  261. package/dist/transactions/settlement/pendingWrite.d.ts.map +0 -1
  262. package/dist/transactions/settlement/pendingWrite.js.map +0 -1
  263. /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.d.ts +0 -0
  264. /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.js +0 -0
  265. /package/dist/transactions/{settlement → confirmation}/idempotencyKey.d.ts +0 -0
  266. /package/dist/transactions/{settlement → confirmation}/idempotencyKey.js +0 -0
  267. /package/dist/transactions/{settlement → confirmation}/pendingWrite.js +0 -0
  268. /package/src/transactions/{settlement → confirmation}/httpCommitEnvelope.ts +0 -0
  269. /package/src/transactions/{settlement → confirmation}/idempotencyKey.ts +0 -0
  270. /package/src/transactions/{settlement → confirmation}/pendingWrite.ts +0 -0
@@ -22,7 +22,7 @@ import { z } from 'zod';
22
22
  import {
23
23
  reconcileFunctionalUpdate,
24
24
  type ModelUpdater,
25
- type ContentionOptions,
25
+ type FunctionalUpdateOptions,
26
26
  } from '../resources/functionalUpdate.js';
27
27
  import {
28
28
  assertBrowserSafety,
@@ -36,7 +36,14 @@ import {
36
36
  warnIfCliKeyMismatch,
37
37
  } from '../auth/apiKey.js';
38
38
  import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
39
- import { commitReceiptSchema, type CommitReceiptWire } from '../wire/commit.js';
39
+ import {
40
+ commitReceiptSchema,
41
+ commitRecordSchema,
42
+ commitRecordListSchema,
43
+ commitRecordListOptionsSchema,
44
+ commitRecordWhereSchema,
45
+ type CommitReceiptWire,
46
+ } from '../wire/commit.js';
40
47
  import { logListResponseSchema } from '../wire/feedEvent.js';
41
48
  import {
42
49
  claimAcquireResponseSchema,
@@ -89,6 +96,7 @@ import {
89
96
  rotateCapability,
90
97
  } from '../auth/capabilityLifecycle.js';
91
98
  import { parseIdentityResolveResponse } from '../auth/schemas.js';
99
+ import type { EffectiveAuthority } from '../auth/capability.js';
92
100
 
93
101
  /**
94
102
  * Interpret a heartbeat reply for a lease this handle HOLDS: anything other
@@ -136,6 +144,7 @@ import {
136
144
  subTarget,
137
145
  streamTarget,
138
146
  batchFence,
147
+ claimIdFor,
139
148
  fenceTokenFor,
140
149
  } from '../coordination/locator.js';
141
150
  import { declaredMeta, wireMeta } from '../coordination/claimMeta.js';
@@ -150,8 +159,8 @@ import {
150
159
  isHttpCommitReplayExpired,
151
160
  type DurableHttpCommitEnvelope,
152
161
  type DurableHttpCommitMethod,
153
- } from '../transactions/settlement/httpCommitEnvelope.js';
154
- import type { CommitOutboxScope } from '../transactions/settlement/commitEnvelope.js';
162
+ } from '../transactions/confirmation/httpCommitEnvelope.js';
163
+ import type { CommitOutboxScope } from '../transactions/confirmation/commitEnvelope.js';
155
164
  import { resolveDurableWrites } from '../durableWrites.js';
156
165
 
157
166
  /** @internal Private options for the schema-agnostic HTTP protocol transport. */
@@ -204,6 +213,8 @@ export interface HttpTransport {
204
213
  */
205
214
  readonly claims: HttpClaimsResource;
206
215
  readonly logs: HttpLogsResource;
216
+ /** Server-confirmed authority of the active bearer, populated by `ready()`. */
217
+ readonly identity: EffectiveAuthority | null;
207
218
  model<T = Record<string, unknown>, Fields = T>(
208
219
  name: string,
209
220
  ): HttpTransportModel<T, Fields>;
@@ -348,6 +359,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
348
359
  let httpOutboxScopeNamespace: string | null = null;
349
360
 
350
361
  let readyPromise: Promise<void> | null = null;
362
+ let effectiveAuthority: EffectiveAuthority | null = null;
351
363
  let httpCommitLane: Promise<void> = Promise.resolve();
352
364
 
353
365
  function runInHttpCommitLane<T>(work: () => Promise<T>): Promise<T> {
@@ -385,7 +397,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
385
397
  return httpOutboxScopeNamespace;
386
398
  }
387
399
 
388
- async function ready(): Promise<void> {
400
+ async function prepare(): Promise<void> {
389
401
  if (readyPromise) return readyPromise;
390
402
 
391
403
  readyPromise = (async () => {
@@ -401,6 +413,14 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
401
413
  }
402
414
  }
403
415
 
416
+ async function ready(): Promise<void> {
417
+ await prepare();
418
+ if (!effectiveAuthority) {
419
+ const rawIdentity = await requestRaw('/auth/identity', { method: 'GET' }, true);
420
+ effectiveAuthority = parseIdentityResolveResponse(rawIdentity).authority;
421
+ }
422
+ }
423
+
404
424
  async function authHeaders(sealedProtocolVersion?: number): Promise<Record<string, string>> {
405
425
  const apiKey = await resolveApiKeyValue(configuredApiKey);
406
426
  const token = apiKey ?? configuredAuthToken;
@@ -463,7 +483,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
463
483
  },
464
484
  skipReady = false
465
485
  ): Promise<unknown> {
466
- if (!skipReady) await ready();
486
+ if (!skipReady) await prepare();
467
487
  const { idempotencyKey, sealedProtocolVersion, ...requestInit } = init;
468
488
  const headers = await authHeaders(sealedProtocolVersion);
469
489
  if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
@@ -589,7 +609,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
589
609
  try {
590
610
  await commitOutbox.remove(recordId);
591
611
  } catch (cause) {
592
- // Do not report the remote outcome until local settlement is durable.
612
+ // Do not report the remote outcome until local confirmation is durable.
593
613
  // The retained record can still be replayed inside the safe window.
594
614
  throw new AbloConnectionError(
595
615
  'The server settled the commit, but its local outbox record could not be cleared.',
@@ -652,6 +672,33 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
652
672
  readonly sealedProtocolVersion?: number;
653
673
  }
654
674
 
675
+ function observeCommitReceipt(
676
+ request: ExactHttpCommitRequest,
677
+ receipt: CommitResponse,
678
+ ): void {
679
+ // A successful receipt carries the same server-authored authority as the
680
+ // identity exchange. Reuse it so startup replay does not add a redundant
681
+ // auth request before callers can inspect `identity`.
682
+ effectiveAuthority = receipt.authority;
683
+ if (!options.onCommitReceipt) return;
684
+ let body: unknown = request.body;
685
+ try {
686
+ body = JSON.parse(request.body) as unknown;
687
+ } catch {
688
+ // Preserve the exact opaque body for observability if it is not JSON.
689
+ }
690
+ try {
691
+ options.onCommitReceipt({
692
+ receipt,
693
+ method: request.method,
694
+ path: request.path,
695
+ body,
696
+ });
697
+ } catch {
698
+ // Receipt observation must never change the commit outcome.
699
+ }
700
+ }
701
+
655
702
  function replicationLagTimeout(
656
703
  request: ExactHttpCommitRequest,
657
704
  response: CommitResponse
@@ -713,6 +760,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
713
760
  true
714
761
  );
715
762
  const next = parseSuccessfulCommitResponse(raw, request.idempotencyKey);
763
+ observeCommitReceipt(request, next);
716
764
  if (next.correlationId !== correlationId) {
717
765
  throw new AbloIdempotencyError(
718
766
  'The same HTTP commit replay returned different source correlation evidence.',
@@ -794,6 +842,16 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
794
842
  true
795
843
  );
796
844
  const response = parseSuccessfulCommitResponse(raw, envelope.idempotencyKey);
845
+ observeCommitReceipt(
846
+ {
847
+ idempotencyKey: envelope.idempotencyKey,
848
+ method: envelope.request.method,
849
+ path: envelope.request.path,
850
+ body: envelope.request.body,
851
+ sealedProtocolVersion: envelope.protocolVersion,
852
+ },
853
+ response,
854
+ );
797
855
  if (
798
856
  envelope.correlationId !== undefined &&
799
857
  response.correlationId !== envelope.correlationId
@@ -929,11 +987,11 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
929
987
  body: unknown;
930
988
  wait: CommitWait;
931
989
  },
932
- beforeSettlement?: (response: CommitResponse) => Promise<void>
990
+ beforeConfirmation?: (response: CommitResponse) => Promise<void>
933
991
  ): Promise<CommitResponse> {
934
992
  return runInHttpCommitLane(async () => {
935
- await ready();
936
- // `ready()` covers startup. Re-draining here makes every later write wait
993
+ await prepare();
994
+ // Startup preparation covers replay. Re-draining here makes every later write wait
937
995
  // behind an ambiguous predecessor from this same process.
938
996
  const replayed = await replayHttpCommitOutbox();
939
997
  const prior = replayed.get(input.idempotencyKey);
@@ -970,7 +1028,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
970
1028
  }
971
1029
  }
972
1030
  if (priorResponse.status === 'confirmed') {
973
- await beforeSettlement?.(priorResponse);
1031
+ await beforeConfirmation?.(priorResponse);
974
1032
  await settleHttpEnvelope(prior.envelope.id);
975
1033
  }
976
1034
  return priorResponse;
@@ -1000,6 +1058,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1000
1058
  true
1001
1059
  );
1002
1060
  response = parseSuccessfulCommitResponse(raw, input.idempotencyKey);
1061
+ observeCommitReceipt(exactRequest, response);
1003
1062
  if (durableEnvelope && response.status === 'queued') {
1004
1063
  await persistHttpAcceptance(durableEnvelope, response);
1005
1064
  }
@@ -1013,12 +1072,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1013
1072
  throw error;
1014
1073
  }
1015
1074
 
1016
- // A model-create readback can participate in settlement: if it fails,
1075
+ // A model-create readback can participate in confirmation: if it fails,
1017
1076
  // retain the exact write so a same-key retry recovers the generated id.
1018
1077
  // A queued source receipt cannot be read back from the log yet and stays
1019
1078
  // durable until a later confirmed replay.
1020
1079
  if (response.status === 'confirmed') {
1021
- await beforeSettlement?.(response);
1080
+ await beforeConfirmation?.(response);
1022
1081
  if (durableEnvelope) await settleHttpEnvelope(durableEnvelope.id);
1023
1082
  }
1024
1083
  return response;
@@ -1044,7 +1103,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1044
1103
  function normalizeCommitOperation(
1045
1104
  op: CommitOperationInput,
1046
1105
  defaults: Pick<CommitCreateOptions, 'readAt' | 'onStale'>,
1047
- fence: BatchFence | null
1106
+ fence: BatchFence | null,
1107
+ claim: Claim | null,
1048
1108
  ): CommitOperationInput {
1049
1109
  return {
1050
1110
  action: op.action,
@@ -1052,6 +1112,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1052
1112
  id: op.id ?? null,
1053
1113
  data: op.data ?? null,
1054
1114
  transactionId: op.transactionId ?? null,
1115
+ claimId:
1116
+ op.claimId ?? claimIdFor(claim?.target, claim?.id, op.model, op.id ?? null),
1055
1117
  readAt: op.readAt ?? defaults.readAt ?? null,
1056
1118
  onStale: op.onStale ?? defaults.onStale ?? null,
1057
1119
  fenceToken: op.fenceToken ?? fenceTokenFor(fence, op.model, op.id ?? null),
@@ -1068,7 +1130,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1068
1130
  });
1069
1131
  }
1070
1132
  return commitOptions.operations.map((op) =>
1071
- normalizeCommitOperation(op, commitOptions, fence)
1133
+ normalizeCommitOperation(op, commitOptions, fence, commitOptions.claim ?? null)
1072
1134
  );
1073
1135
  }
1074
1136
 
@@ -1270,6 +1332,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1270
1332
  onStale: commitOptions.onStale,
1271
1333
  wait: commitOptions.wait,
1272
1334
  claim: commitOptions.claim,
1335
+ reads: commitOptions.reads,
1336
+ track: commitOptions.track,
1273
1337
  },
1274
1338
  'commits.create'
1275
1339
  );
@@ -1318,7 +1382,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1318
1382
  // `requestJson` throws via `translateHttpError` on any non-2xx, so
1319
1383
  // reaching here implies success and `body` is already the success-only
1320
1384
  // receipt union — a rejection is a separate type that never arrives here.
1321
- // The settlement status therefore passes through verbatim: no branch may
1385
+ // The confirmation status therefore passes through verbatim: no branch may
1322
1386
  // collapse a state the server reported into a different one.
1323
1387
  return {
1324
1388
  id: body.id ?? body.clientTxId,
@@ -1330,6 +1394,34 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1330
1394
  ...(body.missingIds && body.missingIds.length > 0 ? { missingIds: body.missingIds } : {}),
1331
1395
  };
1332
1396
  },
1397
+ get({ id }) {
1398
+ if (!id) {
1399
+ throw new AbloValidationError('commits.get requires a non-empty id.', {
1400
+ code: 'write_options_invalid',
1401
+ param: 'id',
1402
+ });
1403
+ }
1404
+ return requestJson(
1405
+ `/v1/commits/${encodeURIComponent(id)}`,
1406
+ { method: 'GET' },
1407
+ commitRecordSchema.nullable(),
1408
+ );
1409
+ },
1410
+ list(options = {}) {
1411
+ const parsed = commitRecordListOptionsSchema.parse(options);
1412
+ const where = commitRecordWhereSchema.parse(parsed.where ?? {});
1413
+ const params = new URLSearchParams();
1414
+ if (where.actorId) params.set('actorId', where.actorId);
1415
+ if (where.status) params.set('status', where.status);
1416
+ if (parsed.cursor) params.set('cursor', parsed.cursor);
1417
+ if (parsed.limit !== undefined) params.set('limit', String(parsed.limit));
1418
+ const query = params.size > 0 ? `?${params.toString()}` : '';
1419
+ return requestJson(
1420
+ `/v1/commits${query}`,
1421
+ { method: 'GET' },
1422
+ commitRecordListSchema,
1423
+ );
1424
+ },
1333
1425
  };
1334
1426
 
1335
1427
  const logs: HttpLogsResource = {
@@ -1352,7 +1444,10 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1352
1444
  },
1353
1445
  };
1354
1446
 
1355
- async function listModel<T>(modelName: string, options?: ServerReadOptions<T>): Promise<T[]> {
1447
+ async function listModel<T>(
1448
+ modelName: string,
1449
+ options?: ServerReadOptions<T>,
1450
+ ): Promise<{ readonly data: readonly T[]; readonly evidence?: readonly { id: string; stamp: number }[] }> {
1356
1451
  const params = new URLSearchParams();
1357
1452
  if (options?.limit !== undefined) params.set('limit', String(options.limit));
1358
1453
  if (options?.orderBy) {
@@ -1380,7 +1475,10 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1380
1475
  // transport is schema-agnostic — it moves rows for whatever schema the
1381
1476
  // caller declared, and `T` is that declaration. Row validation belongs to
1382
1477
  // the typed facade above, which holds the model's schema.
1383
- return res.data as T[];
1478
+ return {
1479
+ data: res.data as T[],
1480
+ ...(res.evidence ? { evidence: res.evidence } : {}),
1481
+ };
1384
1482
  }
1385
1483
 
1386
1484
  async function retrieveModel<T>(
@@ -1426,7 +1524,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1426
1524
  id: string,
1427
1525
  data: Record<string, unknown> | undefined,
1428
1526
  options: ModelMutationOptions | undefined,
1429
- beforeSettlement?: (response: CommitResponse) => Promise<void>
1527
+ beforeConfirmation?: (response: CommitResponse) => Promise<void>
1430
1528
  ): Promise<CommitReceipt> {
1431
1529
  assertWriteOptions(
1432
1530
  options && {
@@ -1434,6 +1532,10 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1434
1532
  readAt: options.readAt,
1435
1533
  onStale: options.onStale,
1436
1534
  claim: options.claim,
1535
+ claimRef: options.claimRef,
1536
+ fenceToken: options.fenceToken,
1537
+ reads: options.reads,
1538
+ track: options.track,
1437
1539
  },
1438
1540
  `${modelName} ${action}`
1439
1541
  );
@@ -1460,6 +1562,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1460
1562
  claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.id,
1461
1563
  onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
1462
1564
  readAt,
1565
+ reads: options?.reads,
1566
+ track: options?.track,
1463
1567
  // The claim's fencing token (Option B), so the per-model HTTP write door
1464
1568
  // fences the same as the WS proxy and `commits.create`.
1465
1569
  fenceToken: options?.fenceToken ?? claimHandle?.fenceToken,
@@ -1480,7 +1584,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1480
1584
  // lower-level `commits.create` lifecycle surface.
1481
1585
  wait: 'confirmed',
1482
1586
  },
1483
- beforeSettlement
1587
+ beforeConfirmation
1484
1588
  );
1485
1589
  } catch (error) {
1486
1590
  // The per-model write door (`ablo.<model>.update/create/delete`). Capture
@@ -1490,7 +1594,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1490
1594
  }
1491
1595
 
1492
1596
  // Same contract as `commits.create` above: a non-2xx already threw, so
1493
- // `body` is the success-only receipt union and its settlement status passes
1597
+ // `body` is the success-only receipt union and its confirmation status passes
1494
1598
  // through verbatim rather than through a catch-all branch.
1495
1599
  return {
1496
1600
  id: body.serverTxId,
@@ -1773,7 +1877,14 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1773
1877
  if (!claimInput) return run(input);
1774
1878
 
1775
1879
  if (isClaimHandle(claimInput)) {
1776
- return run({ ...input, claimRef: { id: claimInput.id }, claim: undefined });
1880
+ return run({
1881
+ ...input,
1882
+ claimRef: { id: claimInput.id },
1883
+ ...(claimInput.fenceToken !== undefined
1884
+ ? { fenceToken: claimInput.fenceToken }
1885
+ : {}),
1886
+ claim: undefined,
1887
+ });
1777
1888
  }
1778
1889
 
1779
1890
  // `isClaimHandle` ruled out the handle form above; the generic mismatch
@@ -1806,14 +1917,14 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1806
1917
  function updateModel(
1807
1918
  id: string,
1808
1919
  updater: ModelUpdater<T>,
1809
- options?: ContentionOptions
1920
+ options?: FunctionalUpdateOptions
1810
1921
  ): Promise<CommitReceipt | undefined>;
1811
1922
  function updateModel(
1812
1923
  arg:
1813
1924
  | (ModelMutationOptions & { readonly id: string; readonly data: Record<string, unknown> })
1814
1925
  | string,
1815
1926
  updater?: ModelUpdater<T>,
1816
- contention?: ContentionOptions
1927
+ contention?: FunctionalUpdateOptions
1817
1928
  ): Promise<CommitReceipt | undefined> {
1818
1929
  // Functional form: update(id, current => next). The SDK owns the
1819
1930
  // read-fresh → compute → compare-and-swap → reconcile loop; correctness
@@ -1857,7 +1968,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1857
1968
  retrieve(params: ModelReadOptions & { readonly id: string }): Promise<HttpTransportRead<T>> {
1858
1969
  return retrieveModel<T>(name, params);
1859
1970
  },
1860
- list(options?: ServerReadOptions<T>): Promise<T[]> {
1971
+ list(options?: ServerReadOptions<T>): ReturnType<typeof listModel<T>> {
1861
1972
  return listModel<T>(name, options);
1862
1973
  },
1863
1974
  async create(
@@ -1893,7 +2004,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1893
2004
  }
1894
2005
  );
1895
2006
  if (created === undefined) {
1896
- throw new AbloConnectionError('Create settlement did not return its row.', {
2007
+ throw new AbloConnectionError('Create confirmation did not return its row.', {
1897
2008
  code: 'commit_no_result',
1898
2009
  });
1899
2010
  }
@@ -1935,9 +2046,10 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
1935
2046
 
1936
2047
  return {
1937
2048
  ready,
2049
+ get identity() { return effectiveAuthority; },
1938
2050
  waitForFlush: () =>
1939
2051
  runInHttpCommitLane(async () => {
1940
- await ready();
2052
+ await prepare();
1941
2053
  const replayed = await replayHttpCommitOutbox();
1942
2054
  await confirmReplayedHttpCommits(replayed);
1943
2055
  }),
@@ -14,7 +14,11 @@ import {
14
14
  errorFromWire,
15
15
  type RequiredCapability,
16
16
  } from '../errors.js';
17
- import { commitAckSchema, commitReceiptSchema } from '../wire/commit.js';
17
+ import {
18
+ commitAckSchema,
19
+ commitReceiptSchema,
20
+ type CommitReceiptWire,
21
+ } from '../wire/commit.js';
18
22
  import { subscriptionAckPayloadSchema } from '../coordination/schema.js';
19
23
  import {
20
24
  WS_INBOUND_FRAMES,
@@ -35,9 +39,10 @@ import type { SocketObservability } from '../observability.js';
35
39
  * server, or rejected on timeout / disconnect.
36
40
  */
37
41
  export interface PendingCommit {
38
- resolve: (value: CommitAck) => void;
42
+ resolve: (value: CommitAck | CommitReceiptWire) => void;
39
43
  reject: (err: Error) => void;
40
44
  timeout: ReturnType<typeof setTimeout>;
45
+ readonly returnReceipt?: boolean;
41
46
  }
42
47
 
43
48
  /**
@@ -202,11 +207,14 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
202
207
  });
203
208
  }
204
209
  pending.resolve(
205
- commitAckSchema.parse({
210
+ pending.returnReceipt
211
+ ? receipt
212
+ : commitAckSchema.parse({
206
213
  status: receipt.status,
207
214
  ...(receipt.correlationId
208
215
  ? { correlationId: receipt.correlationId }
209
216
  : {}),
217
+ statusAt: receipt.statusAt,
210
218
  lastSyncId: receipt.lastSyncId,
211
219
  ...(receipt.notifications && receipt.notifications.length > 0
212
220
  ? { notifications: receipt.notifications }
@@ -214,7 +222,7 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
214
222
  ...(receipt.missingIds && receipt.missingIds.length > 0
215
223
  ? { missingIds: receipt.missingIds }
216
224
  : {}),
217
- }),
225
+ }),
218
226
  );
219
227
  } else {
220
228
  // Capture the full server error so the caller can see what actually
@@ -233,6 +241,7 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
233
241
  code?: unknown;
234
242
  message?: unknown;
235
243
  request_id?: unknown;
244
+ event_id?: unknown;
236
245
  requiredCapability?: unknown;
237
246
  details?: unknown;
238
247
  };
@@ -261,6 +270,9 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
261
270
  !Array.isArray(obj.details)
262
271
  ? (obj.details as Readonly<Record<string, unknown>>)
263
272
  : undefined;
273
+ if (typeof obj.event_id === 'string') {
274
+ details = { ...(details ?? {}), event_id: obj.event_id };
275
+ }
264
276
  } else {
265
277
  errorMessage = 'mutation failed on server';
266
278
  }
@@ -341,7 +353,12 @@ const handleClaimAck: WsFrameHandler = (session, message) => {
341
353
  });
342
354
  } else {
343
355
  const err = error as
344
- | { code?: unknown; message?: unknown }
356
+ | {
357
+ code?: unknown;
358
+ message?: unknown;
359
+ request_id?: unknown;
360
+ event_id?: unknown;
361
+ }
345
362
  | undefined;
346
363
  const code =
347
364
  err?.code && typeof err.code === 'string'
@@ -351,6 +368,11 @@ const handleClaimAck: WsFrameHandler = (session, message) => {
351
368
  err?.message && typeof err.message === 'string'
352
369
  ? err.message
353
370
  : 'claim rejected by server';
371
+ const requestId =
372
+ typeof err?.request_id === 'string' ? err.request_id : undefined;
373
+ const eventId =
374
+ typeof err?.event_id === 'string' ? err.event_id : undefined;
375
+ const details = eventId === undefined ? undefined : { event_id: eventId };
354
376
  // Capability denials get the typed CapabilityError so
355
377
  // callers can read `.requiredCapability` and attenuate-
356
378
  // and-retry the claim with a narrower token.
@@ -366,12 +388,23 @@ const handleClaimAck: WsFrameHandler = (session, message) => {
366
388
  typeof (rc as { scope?: unknown }).scope === 'string'
367
389
  ? (rc as RequiredCapability)
368
390
  : undefined;
369
- pending.reject(new CapabilityError(code, msg, requiredCapability));
391
+ pending.reject(
392
+ new CapabilityError(code, msg, requiredCapability, {
393
+ ...(requestId !== undefined ? { requestId } : {}),
394
+ ...(details !== undefined ? { details } : {}),
395
+ }),
396
+ );
370
397
  } else {
371
398
  // Route through the shared factory so a failed claim_ack is a
372
399
  // typed AbloError (registry code → right subclass), symmetric
373
400
  // with the commit `mutation_result` path — never a bare Error.
374
- pending.reject(errorFromWire(msg, { code }));
401
+ pending.reject(
402
+ errorFromWire(msg, {
403
+ code,
404
+ requestId,
405
+ details,
406
+ }),
407
+ );
375
408
  }
376
409
  }
377
410
  };
@@ -405,10 +438,15 @@ const handleSubscriptionAck: WsFrameHandler = (session, message) => {
405
438
  session.options.syncGroups = ack.syncGroups;
406
439
  pending.resolve({ syncGroups: ack.syncGroups });
407
440
  } else {
441
+ const eventId = ack.error?.event_id;
408
442
  pending.reject(
409
443
  errorFromWire(
410
444
  ack.error?.message ?? 'update_subscription rejected by server',
411
- { code: ack.error?.code ?? 'malformed_subscription' },
445
+ {
446
+ code: ack.error?.code ?? 'malformed_subscription',
447
+ requestId: ack.error?.request_id,
448
+ ...(eventId !== undefined ? { details: { event_id: eventId } } : {}),
449
+ },
412
450
  ),
413
451
  );
414
452
  }
@@ -56,6 +56,7 @@ import {
56
56
  type AuthTokenGetter,
57
57
  } from '../auth/credentialSource.js';
58
58
  import { buildCommitFrame, type CommitAck, type CommitFrameOperation } from './commitFrames.js';
59
+ import type { CommitReceiptWire } from '../wire/commit.js';
59
60
  import {
60
61
  dispatchWsFrame,
61
62
  readWsInboundFrame,
@@ -1040,7 +1041,47 @@ export class WsTransport<
1040
1041
  ),
1041
1042
  );
1042
1043
  }, timeoutMs);
1043
- this.pendingMutations.set(clientTxId, { resolve, reject, timeout });
1044
+ this.pendingMutations.set(clientTxId, {
1045
+ resolve: (value) => resolve(value as CommitAck),
1046
+ reject,
1047
+ timeout,
1048
+ });
1049
+ try {
1050
+ const frame = buildCommitFrame(operations, clientTxId, reads, track);
1051
+ this.ws!.send(JSON.stringify(frame));
1052
+ } catch (error) {
1053
+ clearTimeout(timeout);
1054
+ this.pendingMutations.delete(clientTxId);
1055
+ reject(toAbloError(error));
1056
+ }
1057
+ });
1058
+ }
1059
+
1060
+ /** @internal Exact wire receipt used to correlate a logical commit record. */
1061
+ sendCommitReceipt(
1062
+ operations: readonly CommitFrameOperation[],
1063
+ clientTxId: string,
1064
+ timeoutMs = 15_000,
1065
+ reads?: readonly ReadDependency[] | null,
1066
+ track?: readonly TrackDependency[] | null,
1067
+ ): Promise<CommitReceiptWire> {
1068
+ if (this.ws?.readyState !== WebSocket.OPEN) {
1069
+ return Promise.reject(this.notConnectedError('commit'));
1070
+ }
1071
+ return new Promise<CommitReceiptWire>((resolve, reject) => {
1072
+ const timeout = setTimeout(() => {
1073
+ this.pendingMutations.delete(clientTxId);
1074
+ reject(new AbloConnectionError(
1075
+ `commit timed out after ${timeoutMs}ms (clientTxId=${clientTxId})`,
1076
+ { code: 'commit_no_result' },
1077
+ ));
1078
+ }, timeoutMs);
1079
+ this.pendingMutations.set(clientTxId, {
1080
+ resolve: (value) => resolve(value as CommitReceiptWire),
1081
+ reject,
1082
+ timeout,
1083
+ returnReceipt: true,
1084
+ });
1044
1085
  try {
1045
1086
  const frame = buildCommitFrame(operations, clientTxId, reads, track);
1046
1087
  this.ws!.send(JSON.stringify(frame));
package/src/utils/json.ts CHANGED
@@ -38,7 +38,7 @@ import type { JsonValue } from '../types/streams.js';
38
38
  * Traversal intentionally creates fresh objects and arrays instead of relying
39
39
  * on a framework-specific unwrapping API. That makes Proxy-wrapped plain data
40
40
  * (including MobX observables) safe for IndexedDB's structured-clone algorithm
41
- * while keeping the settlement core independent of any reactive framework.
41
+ * while keeping the confirmation core independent of any reactive framework.
42
42
  *
43
43
  * The accepted contract is deliberately narrower than `JSON.stringify`:
44
44
  * values that JSON would silently corrupt (`NaN` to `null`, `Map` to `{}`) or