@abloatai/transaction 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 (257) 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/commitRecordRuntime.d.ts +30 -0
  32. package/dist/commitRecordRuntime.d.ts.map +1 -0
  33. package/dist/commitRecordRuntime.js +188 -0
  34. package/dist/commitRecordRuntime.js.map +1 -0
  35. package/dist/coordination/index.d.ts +3 -3
  36. package/dist/coordination/index.d.ts.map +1 -1
  37. package/dist/coordination/index.js +2 -2
  38. package/dist/coordination/index.js.map +1 -1
  39. package/dist/coordination/locator.d.ts +5 -0
  40. package/dist/coordination/locator.d.ts.map +1 -1
  41. package/dist/coordination/locator.js +12 -0
  42. package/dist/coordination/locator.js.map +1 -1
  43. package/dist/coordination/schema.d.ts +205 -15
  44. package/dist/coordination/schema.d.ts.map +1 -1
  45. package/dist/coordination/schema.js +88 -47
  46. package/dist/coordination/schema.js.map +1 -1
  47. package/dist/coordination/targetConflict.js +1 -1
  48. package/dist/durableWrites.d.ts +3 -3
  49. package/dist/durableWrites.d.ts.map +1 -1
  50. package/dist/durableWrites.js +1 -1
  51. package/dist/errorCodes.d.ts +20 -1
  52. package/dist/errorCodes.d.ts.map +1 -1
  53. package/dist/errorCodes.js +44 -10
  54. package/dist/errorCodes.js.map +1 -1
  55. package/dist/errorObservation.d.ts +82 -0
  56. package/dist/errorObservation.d.ts.map +1 -0
  57. package/dist/errorObservation.js +101 -0
  58. package/dist/errorObservation.js.map +1 -0
  59. package/dist/errors.d.ts +2 -0
  60. package/dist/errors.d.ts.map +1 -1
  61. package/dist/errors.js +6 -1
  62. package/dist/errors.js.map +1 -1
  63. package/dist/index.d.ts +5 -2
  64. package/dist/index.d.ts.map +1 -1
  65. package/dist/index.js +1 -1
  66. package/dist/index.js.map +1 -1
  67. package/dist/internal/read-set.d.ts +8 -0
  68. package/dist/internal/read-set.d.ts.map +1 -0
  69. package/dist/internal/read-set.js +7 -0
  70. package/dist/internal/read-set.js.map +1 -0
  71. package/dist/logger.d.ts +1 -1
  72. package/dist/observability.d.ts +1 -1
  73. package/dist/observability.js +1 -1
  74. package/dist/readSetContext.d.ts +65 -0
  75. package/dist/readSetContext.d.ts.map +1 -0
  76. package/dist/readSetContext.js +121 -0
  77. package/dist/readSetContext.js.map +1 -0
  78. package/dist/resources/functionalUpdate.d.ts +13 -3
  79. package/dist/resources/functionalUpdate.d.ts.map +1 -1
  80. package/dist/resources/functionalUpdate.js.map +1 -1
  81. package/dist/resources/httpResources.d.ts +23 -7
  82. package/dist/resources/httpResources.d.ts.map +1 -1
  83. package/dist/resources/modelOperations.d.ts +2 -2
  84. package/dist/resources/modelOperations.js +1 -1
  85. package/dist/resources/mutationOptions.d.ts +1 -1
  86. package/dist/resources/mutationOptions.js +1 -1
  87. package/dist/resources/where.d.ts +1 -1
  88. package/dist/resources/where.js +1 -1
  89. package/dist/resources/writeOptionsSchema.d.ts +39 -4
  90. package/dist/resources/writeOptionsSchema.d.ts.map +1 -1
  91. package/dist/resources/writeOptionsSchema.js +12 -6
  92. package/dist/resources/writeOptionsSchema.js.map +1 -1
  93. package/dist/schema/index.js +1 -1
  94. package/dist/schema/index.js.map +1 -1
  95. package/dist/schema/openapi.d.ts.map +1 -1
  96. package/dist/schema/openapi.js +41 -7
  97. package/dist/schema/openapi.js.map +1 -1
  98. package/dist/server/commit.d.ts +7 -1
  99. package/dist/server/commit.d.ts.map +1 -1
  100. package/dist/server/index.d.ts +1 -1
  101. package/dist/server/index.d.ts.map +1 -1
  102. package/dist/server/index.js +1 -1
  103. package/dist/server/index.js.map +1 -1
  104. package/dist/source/adapters/kysely.d.ts.map +1 -1
  105. package/dist/source/adapters/kysely.js +11 -4
  106. package/dist/source/adapters/kysely.js.map +1 -1
  107. package/dist/source/adapters/kyselyMutationCore.d.ts.map +1 -1
  108. package/dist/source/adapters/kyselyMutationCore.js +22 -4
  109. package/dist/source/adapters/kyselyMutationCore.js.map +1 -1
  110. package/dist/source/contract.d.ts +2 -0
  111. package/dist/source/contract.d.ts.map +1 -1
  112. package/dist/source/types.d.ts +1 -1
  113. package/dist/testing/fixtures/httpResponses.d.ts +30 -1
  114. package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
  115. package/dist/testing/fixtures/httpResponses.js +48 -1
  116. package/dist/testing/fixtures/httpResponses.js.map +1 -1
  117. package/dist/transactionLayer.d.ts +1 -1
  118. package/dist/transactions/{settlement → confirmation}/commitEnvelope.d.ts +4 -2
  119. package/dist/transactions/confirmation/commitEnvelope.d.ts.map +1 -0
  120. package/dist/transactions/{settlement → confirmation}/commitEnvelope.js +8 -3
  121. package/dist/transactions/confirmation/commitEnvelope.js.map +1 -0
  122. package/dist/transactions/confirmation/httpCommitEnvelope.d.ts.map +1 -0
  123. package/dist/transactions/confirmation/httpCommitEnvelope.js.map +1 -0
  124. package/dist/transactions/confirmation/idempotencyKey.d.ts.map +1 -0
  125. package/dist/transactions/confirmation/idempotencyKey.js.map +1 -0
  126. package/dist/transactions/{settlement → confirmation}/pendingWrite.d.ts +3 -2
  127. package/dist/transactions/confirmation/pendingWrite.d.ts.map +1 -0
  128. package/dist/transactions/confirmation/pendingWrite.js.map +1 -0
  129. package/dist/transport/commitFrames.d.ts +1 -0
  130. package/dist/transport/commitFrames.d.ts.map +1 -1
  131. package/dist/transport/commitFrames.js +8 -7
  132. package/dist/transport/commitFrames.js.map +1 -1
  133. package/dist/transport/connectionManager.d.ts +1 -1
  134. package/dist/transport/connectionManager.js +1 -1
  135. package/dist/transport/httpClient.d.ts +23 -9
  136. package/dist/transport/httpClient.d.ts.map +1 -1
  137. package/dist/transport/httpClient.js +137 -15
  138. package/dist/transport/httpClient.js.map +1 -1
  139. package/dist/transport/httpOptions.d.ts +9 -1
  140. package/dist/transport/httpOptions.d.ts.map +1 -1
  141. package/dist/transport/httpTransport.d.ts +3 -0
  142. package/dist/transport/httpTransport.d.ts.map +1 -1
  143. package/dist/transport/httpTransport.js +109 -22
  144. package/dist/transport/httpTransport.js.map +1 -1
  145. package/dist/transport/wsFrameHandlers.d.ts +3 -1
  146. package/dist/transport/wsFrameHandlers.d.ts.map +1 -1
  147. package/dist/transport/wsFrameHandlers.js +38 -17
  148. package/dist/transport/wsFrameHandlers.js.map +1 -1
  149. package/dist/transport/wsTransport.d.ts +3 -0
  150. package/dist/transport/wsTransport.d.ts.map +1 -1
  151. package/dist/transport/wsTransport.js +32 -1
  152. package/dist/transport/wsTransport.js.map +1 -1
  153. package/dist/utils/json.d.ts +1 -1
  154. package/dist/utils/json.js +1 -1
  155. package/dist/wire/accountResponses.d.ts +1 -1
  156. package/dist/wire/auth.d.ts +2 -2
  157. package/dist/wire/commit.d.ts +1652 -213
  158. package/dist/wire/commit.d.ts.map +1 -1
  159. package/dist/wire/commit.js +196 -132
  160. package/dist/wire/commit.js.map +1 -1
  161. package/dist/wire/delta.d.ts +1 -1
  162. package/dist/wire/errorEnvelope.d.ts +1 -0
  163. package/dist/wire/errorEnvelope.d.ts.map +1 -1
  164. package/dist/wire/errorEnvelope.js +1 -0
  165. package/dist/wire/errorEnvelope.js.map +1 -1
  166. package/dist/wire/feedCursor.d.ts +1 -1
  167. package/dist/wire/feedCursor.js +1 -1
  168. package/dist/wire/feedEvent.d.ts +3 -3
  169. package/dist/wire/frames.d.ts +8 -4
  170. package/dist/wire/frames.d.ts.map +1 -1
  171. package/dist/wire/frames.js +6 -3
  172. package/dist/wire/frames.js.map +1 -1
  173. package/dist/wire/inboundFrames.d.ts +44 -4
  174. package/dist/wire/inboundFrames.d.ts.map +1 -1
  175. package/dist/wire/index.d.ts +6 -4
  176. package/dist/wire/index.d.ts.map +1 -1
  177. package/dist/wire/index.js +6 -5
  178. package/dist/wire/index.js.map +1 -1
  179. package/dist/wire/modelMutations.d.ts +35 -0
  180. package/dist/wire/modelMutations.d.ts.map +1 -1
  181. package/dist/wire/modelMutations.js +9 -2
  182. package/dist/wire/modelMutations.js.map +1 -1
  183. package/dist/wire/modelResponses.d.ts +12 -1
  184. package/dist/wire/modelResponses.d.ts.map +1 -1
  185. package/dist/wire/modelResponses.js +12 -1
  186. package/dist/wire/modelResponses.js.map +1 -1
  187. package/package.json +12 -2
  188. package/src/ai-sdk/modelTools.ts +1 -1
  189. package/src/ai-sdk/updateTool.ts +2 -2
  190. package/src/auth/bootstrapScope.ts +1 -1
  191. package/src/auth/capability.ts +13 -4
  192. package/src/auth/capabilityLifecycle.ts +3 -2
  193. package/src/auth/identity.ts +16 -1
  194. package/src/auth/index.ts +2 -0
  195. package/src/auth/schemas.ts +3 -0
  196. package/src/auth/sessionMint.ts +0 -1
  197. package/src/commitRecordRuntime.ts +245 -0
  198. package/src/coordination/index.ts +24 -1
  199. package/src/coordination/locator.ts +17 -0
  200. package/src/coordination/schema.ts +114 -49
  201. package/src/coordination/targetConflict.ts +1 -1
  202. package/src/durableWrites.ts +3 -3
  203. package/src/errorCodes.ts +107 -11
  204. package/src/errorObservation.ts +121 -0
  205. package/src/errors.ts +5 -1
  206. package/src/index.ts +8 -1
  207. package/src/internal/read-set.ts +23 -0
  208. package/src/logger.ts +1 -1
  209. package/src/observability.ts +1 -1
  210. package/src/readSetContext.ts +195 -0
  211. package/src/resources/functionalUpdate.ts +16 -4
  212. package/src/resources/httpResources.ts +27 -8
  213. package/src/resources/modelOperations.ts +2 -2
  214. package/src/resources/mutationOptions.ts +1 -1
  215. package/src/resources/where.ts +1 -1
  216. package/src/resources/writeOptionsSchema.ts +30 -12
  217. package/src/schema/index.ts +1 -1
  218. package/src/schema/openapi.ts +48 -7
  219. package/src/server/commit.ts +7 -1
  220. package/src/server/index.ts +5 -1
  221. package/src/source/adapters/kysely.ts +12 -4
  222. package/src/source/adapters/kyselyMutationCore.ts +28 -4
  223. package/src/source/types.ts +1 -1
  224. package/src/testing/fixtures/httpResponses.ts +60 -1
  225. package/src/transactionLayer.ts +1 -1
  226. package/src/transactions/{settlement → confirmation}/commitEnvelope.ts +13 -3
  227. package/src/transport/commitFrames.ts +7 -5
  228. package/src/transport/connectionManager.ts +1 -1
  229. package/src/transport/httpClient.ts +260 -29
  230. package/src/transport/httpOptions.ts +9 -1
  231. package/src/transport/httpTransport.ts +139 -27
  232. package/src/transport/wsFrameHandlers.ts +46 -8
  233. package/src/transport/wsTransport.ts +42 -1
  234. package/src/utils/json.ts +1 -1
  235. package/src/wire/commit.ts +248 -147
  236. package/src/wire/errorEnvelope.ts +1 -0
  237. package/src/wire/feedCursor.ts +1 -1
  238. package/src/wire/frames.ts +9 -4
  239. package/src/wire/index.ts +38 -5
  240. package/src/wire/modelMutations.ts +16 -2
  241. package/src/wire/modelResponses.ts +14 -1
  242. package/dist/transactions/settlement/commitEnvelope.d.ts.map +0 -1
  243. package/dist/transactions/settlement/commitEnvelope.js.map +0 -1
  244. package/dist/transactions/settlement/httpCommitEnvelope.d.ts.map +0 -1
  245. package/dist/transactions/settlement/httpCommitEnvelope.js.map +0 -1
  246. package/dist/transactions/settlement/idempotencyKey.d.ts.map +0 -1
  247. package/dist/transactions/settlement/idempotencyKey.js.map +0 -1
  248. package/dist/transactions/settlement/pendingWrite.d.ts.map +0 -1
  249. package/dist/transactions/settlement/pendingWrite.js.map +0 -1
  250. /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.d.ts +0 -0
  251. /package/dist/transactions/{settlement → confirmation}/httpCommitEnvelope.js +0 -0
  252. /package/dist/transactions/{settlement → confirmation}/idempotencyKey.d.ts +0 -0
  253. /package/dist/transactions/{settlement → confirmation}/idempotencyKey.js +0 -0
  254. /package/dist/transactions/{settlement → confirmation}/pendingWrite.js +0 -0
  255. /package/src/transactions/{settlement → confirmation}/httpCommitEnvelope.ts +0 -0
  256. /package/src/transactions/{settlement → confirmation}/idempotencyKey.ts +0 -0
  257. /package/src/transactions/{settlement → confirmation}/pendingWrite.ts +0 -0
@@ -8,7 +8,12 @@
8
8
  */
9
9
 
10
10
  import { z } from 'zod';
11
- import { readDependencySchema, trackDependencySchema } from '../../coordination/schema.js';
11
+ import {
12
+ MAX_READ_SET_ENTRIES,
13
+ readDependencyListSchema,
14
+ readSetProjectionEntryCount,
15
+ trackDependencyListSchema,
16
+ } from '../../coordination/schema.js';
12
17
  import { wireCommitOperationSchema } from '../../wire/frames.js';
13
18
  import { correlationIdSchema } from '../../wire/commit.js';
14
19
  import { idempotencyKeySchema } from './idempotencyKey.js';
@@ -41,6 +46,7 @@ export const durableCommitOperationSchema = wireCommitOperationSchema
41
46
  id: true,
42
47
  input: true,
43
48
  transactionId: true,
49
+ claimId: true,
44
50
  readAt: true,
45
51
  onStale: true,
46
52
  fenceToken: true,
@@ -50,6 +56,7 @@ export const durableCommitOperationSchema = wireCommitOperationSchema
50
56
  id: z.string().min(1),
51
57
  input: z.record(z.string(), z.unknown()).optional(),
52
58
  transactionId: z.string().min(1).optional(),
59
+ claimId: z.string().min(1).optional(),
53
60
  options: z
54
61
  .strictObject({
55
62
  idempotencyKey: z.string().min(1).max(255).nullable().optional(),
@@ -62,8 +69,11 @@ export type DurableCommitOperation = z.infer<typeof durableCommitOperationSchema
62
69
  export type DurableCommitOperationInput = z.input<typeof durableCommitOperationSchema>;
63
70
 
64
71
  const durableCommitOptionsSchema = z.strictObject({
65
- reads: z.array(readDependencySchema).nullable().optional(),
66
- track: z.array(trackDependencySchema).nullable().optional(),
72
+ reads: readDependencyListSchema.nullable().optional(),
73
+ track: trackDependencyListSchema.nullable().optional(),
74
+ }).refine((value) => readSetProjectionEntryCount(value) <= MAX_READ_SET_ENTRIES, {
75
+ path: ['reads'],
76
+ message: `reads and track may contain at most ${MAX_READ_SET_ENTRIES} entries combined`,
67
77
  });
68
78
 
69
79
  export const commitOutboxScopeSchema = z.strictObject({
@@ -53,6 +53,7 @@ export interface CommitFrameOperation {
53
53
  readonly id: string;
54
54
  readonly input?: Record<string, unknown>;
55
55
  readonly transactionId?: string;
56
+ readonly claimId?: string | null;
56
57
  readonly readAt?: number | null;
57
58
  readonly onStale?: OnStaleMode | null;
58
59
  readonly fenceToken?: number | null;
@@ -80,17 +81,18 @@ export function buildCommitFrame(
80
81
  id: op.id,
81
82
  input: op.input,
82
83
  transactionId: op.transactionId,
84
+ claimId: op.claimId,
83
85
  readAt: op.readAt,
84
86
  onStale: op.onStale,
85
87
  fenceToken: op.fenceToken,
86
88
  })),
87
89
  clientTxId,
88
90
  };
89
- // The batch premise: the rows or groups the writer read before committing.
90
- if (reads && reads.length > 0) payload.reads = [...reads];
91
- // Durable premises the writer is registering: the rows or groups it
92
- // wants to keep hearing about after this commit, delivered on a future receipt.
93
- if (track && track.length > 0) payload.track = [...track];
91
+ // Preserve declaration at the wire boundary. `null`, an explicit empty list,
92
+ // and omission are distinct inputs even when today's server treats each as
93
+ // "none"; the WebSocket path must not silently erase a field the caller set.
94
+ if (reads !== undefined) payload.reads = reads === null ? null : [...reads];
95
+ if (track !== undefined) payload.track = track === null ? null : [...track];
94
96
  return { type: 'commit', payload };
95
97
  }
96
98
 
@@ -15,7 +15,7 @@
15
15
  * first successful connect and disposed on teardown.
16
16
  *
17
17
  * This is a plain state machine, not a reactive object: it lives in the
18
- * settlement core, which carries no reactivity runtime (ADR 0016). A consumer
18
+ * confirmation core, which carries no reactivity runtime (ADR 0016). A consumer
19
19
  * that wants to render recovery progress mirrors the transitions through
20
20
  * {@link ConnectionCallbacks.onStateChange} into its own observable state —
21
21
  * which is what the reactive engine's store does.
@@ -25,6 +25,7 @@ import { modelWireNames } from '../auth/capability.js';
25
25
  import type { HttpClientConfig } from './httpOptions.js';
26
26
  import type {
27
27
  CommitResource,
28
+ CommitReceipt,
28
29
  HttpClaimApi,
29
30
  HttpClaimsResource,
30
31
  HttpLogsResource,
@@ -46,8 +47,25 @@ import type {
46
47
  } from '../resources/modelOperations.js';
47
48
  import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
48
49
  import { omittedModelError } from '../schema/select.js';
49
- import type { ModelUpdater, ContentionOptions } from '../resources/functionalUpdate.js';
50
+ import {
51
+ reconcileFunctionalUpdate,
52
+ type ModelUpdater,
53
+ type FunctionalUpdateOptions,
54
+ } from '../resources/functionalUpdate.js';
55
+ import type { HeldClaim } from '../types/streams.js';
50
56
  import { AbloConnectionError, AbloValidationError } from '../errors.js';
57
+ import type { OnStaleMode, ReadDependency } from '../coordination/schema.js';
58
+ import {
59
+ abortReadSetCommit,
60
+ capturePointRead,
61
+ createReadSetContext,
62
+ consumeReadSet,
63
+ kReadEvidence,
64
+ prepareReadSet,
65
+ type ReadSetContext,
66
+ } from '../readSetContext.js';
67
+ import { recordHttpCommitReceipt } from '../commitRecordRuntime.js';
68
+ import type { EffectiveAuthority } from '../auth/capability.js';
51
69
 
52
70
  export interface AbloHttpClientOptions<S extends SchemaRecord>
53
71
  extends HttpClientConfig<S> {
@@ -59,6 +77,18 @@ export interface AbloHttpClientOptions<S extends SchemaRecord>
59
77
  readonly timeoutMs?: number;
60
78
  }
61
79
 
80
+ declare const capturedRowBrand: unique symbol;
81
+
82
+ /** A point-read row whose exact watermark is privately retained by its client. */
83
+ export type CapturedRow<T = unknown> = T & {
84
+ readonly [capturedRowBrand]: true;
85
+ };
86
+
87
+ /** Exact returned rows or low-level canonical dependencies accepted by `reads`. */
88
+ export type HttpModelMutationParams<P> = Omit<P, 'reads'> & {
89
+ readonly reads?: readonly (ReadDependency | CapturedRow)[] | null;
90
+ };
91
+
62
92
  /**
63
93
  * The per-model surface of the stateless HTTP client — everything reachable over
64
94
  * request/response: reads (`get` and `list`), writes (`create`, `update`,
@@ -91,14 +121,14 @@ export interface HttpModelClient<T, C = T> {
91
121
  * to what is already here.
92
122
  */
93
123
  /** Canonical authoritative point lookup. A miss resolves to `undefined`. */
94
- get(params: ModelRetrieveParams & ModelReadOptions): Promise<T | undefined>;
124
+ get(params: ModelRetrieveParams & ModelReadOptions): Promise<CapturedRow<T> | undefined>;
95
125
  /** @deprecated Use `get({ id })` for an authoritative point lookup. */
96
- retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<T | undefined>;
126
+ retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<CapturedRow<T> | undefined>;
97
127
  /**
98
128
  * Reads the rows matching a filter. Same resolution as `get`, in bulk,
99
129
  * and deduplicated so concurrent identical calls share one request.
100
130
  */
101
- list(options?: ServerReadOptions<T>): Promise<T[]>;
131
+ list(options?: ServerReadOptions<T>): Promise<CapturedRow<T>[]>;
102
132
  /**
103
133
  * Creates a row and returns it, including any framework-applied defaults.
104
134
  * Passing an id that already exists is idempotent: the existing row is
@@ -110,8 +140,8 @@ export interface HttpModelClient<T, C = T> {
110
140
  * back, so it resolves on the server's confirmation instead. The same is true
111
141
  * of `update` and `delete`.
112
142
  */
113
- create(params: ModelCreateParams<T, C>): Promise<T>;
114
- update(params: ModelUpdateParams<T, C>): Promise<T>;
143
+ create(params: HttpModelMutationParams<ModelCreateParams<T, C>>): Promise<T>;
144
+ update(params: HttpModelMutationParams<ModelUpdateParams<T, C>>): Promise<T>;
115
145
  /**
116
146
  * Updates a row with a function of its latest value — `update(id, current =>
117
147
  * next)`, the data-layer equivalent of a `setState(prev => next)` reducer. The
@@ -124,9 +154,9 @@ export interface HttpModelClient<T, C = T> {
124
154
  update(
125
155
  id: string,
126
156
  updater: ModelUpdater<T>,
127
- options?: ContentionOptions,
157
+ options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
128
158
  ): Promise<T | undefined>;
129
- delete(params: ModelDeleteParams<T, C>): Promise<void>;
159
+ delete(params: HttpModelMutationParams<ModelDeleteParams<T, C>>): Promise<void>;
130
160
  claim: HttpClaimApi<T, C>;
131
161
  /**
132
162
  * Registers a durable premise on a row — "keep telling me about this one".
@@ -164,7 +194,7 @@ export type AbloHttpClient<S extends SchemaRecord> = {
164
194
  } & {
165
195
  /** Runs one-time setup (durable-outbox scope resolution and replay) before the client is used. It also runs lazily ahead of the first request, so calling it yourself is optional. */
166
196
  ready(): Promise<void>;
167
- /** Replays every pending durable HTTP write in seal order and waits for settlement. */
197
+ /** Replays every pending durable HTTP write in seal order and waits for confirmation. */
168
198
  waitForFlush(): Promise<void>;
169
199
  readonly commits: CommitResource;
170
200
  /**
@@ -177,6 +207,8 @@ export type AbloHttpClient<S extends SchemaRecord> = {
177
207
  readonly claims: HttpClaimsResource;
178
208
  /** Durable, credential-scoped authoritative event pages. */
179
209
  readonly logs: HttpLogsResource;
210
+ /** Server-confirmed authority of the credential, populated by `ready()`. */
211
+ readonly identity: EffectiveAuthority | null;
180
212
  dispose(): Promise<void>;
181
213
  /** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
182
214
  getAuthToken(): Promise<string | null>;
@@ -200,6 +232,7 @@ const PROTOCOL_MEMBERS = new Set<string>([
200
232
  'commits',
201
233
  'claims',
202
234
  'logs',
235
+ 'identity',
203
236
  'getAuthToken',
204
237
  'sessions',
205
238
  ]);
@@ -217,9 +250,13 @@ function isProtocolMember(prop: string): prop is keyof HttpTransport {
217
250
  */
218
251
  function createHttpModelClient<T, C = T>(
219
252
  protocol: HttpTransportModel<T, C>,
253
+ modelName: string,
254
+ clientIdentity: object,
255
+ readSetContext: ReadSetContext | undefined,
220
256
  ): HttpModelClient<T, C> {
221
257
  async function readRow(id: string): Promise<T | undefined> {
222
258
  const read = await protocol.get({ id });
259
+ capturePointRead(readSetContext, clientIdentity, modelName, id, read.data, read.stamp);
223
260
  return read.data;
224
261
  }
225
262
 
@@ -248,16 +285,48 @@ function createHttpModelClient<T, C = T>(
248
285
  };
249
286
  }
250
287
 
251
- function update(params: ModelUpdateParams<T, C>): Promise<T>;
288
+ function preparedMutation<P extends {
289
+ readonly claim?: unknown;
290
+ readonly idempotencyKey?: string | null;
291
+ readonly readAt?: number | null;
292
+ readonly onStale?: OnStaleMode | null;
293
+ readonly reads?: readonly unknown[] | null;
294
+ }>(
295
+ params: P,
296
+ ) {
297
+ const prepared = prepareReadSet(
298
+ readSetContext,
299
+ clientIdentity,
300
+ params.readAt,
301
+ params.onStale,
302
+ params.idempotencyKey,
303
+ params.reads,
304
+ );
305
+ return {
306
+ options: {
307
+ ...mutationOptions(params),
308
+ ...(prepared.readAt !== undefined ? { readAt: prepared.readAt } : {}),
309
+ ...(prepared.onStale !== undefined ? { onStale: prepared.onStale } : {}),
310
+ ...(prepared.reads !== undefined ? { reads: prepared.reads } : {}),
311
+ ...(prepared.idempotencyKey !== undefined
312
+ ? { idempotencyKey: prepared.idempotencyKey }
313
+ : {}),
314
+ },
315
+ consumed: prepared.consumed,
316
+ automaticCommit: prepared.automaticCommit,
317
+ };
318
+ }
319
+
320
+ function update(params: HttpModelMutationParams<ModelUpdateParams<T, C>>): Promise<T>;
252
321
  function update(
253
322
  id: string,
254
323
  updater: ModelUpdater<T>,
255
- options?: ContentionOptions,
324
+ options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
256
325
  ): Promise<T | undefined>;
257
326
  async function update(
258
- arg: ModelUpdateParams<T, C> | string,
327
+ arg: HttpModelMutationParams<ModelUpdateParams<T, C>> | string,
259
328
  updater?: ModelUpdater<T>,
260
- options?: ContentionOptions,
329
+ options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
261
330
  ): Promise<T | undefined> {
262
331
  if (typeof arg === 'string') {
263
332
  if (!updater) {
@@ -266,38 +335,186 @@ function createHttpModelClient<T, C = T>(
266
335
  { code: 'write_options_invalid' },
267
336
  );
268
337
  }
269
- const receipt = await protocol.update(arg, updater, options);
338
+ const receipt = await reconcileFunctionalUpdate<
339
+ T,
340
+ CommitReceipt,
341
+ ReadDependency | CapturedRow
342
+ >(
343
+ updater,
344
+ options,
345
+ {
346
+ model: modelName,
347
+ id: arg,
348
+ readFresh: async () => {
349
+ const read = await protocol.get({ id: arg });
350
+ capturePointRead(
351
+ readSetContext,
352
+ clientIdentity,
353
+ modelName,
354
+ arg,
355
+ read.data,
356
+ read.stamp,
357
+ );
358
+ return { data: read.data, stamp: read.stamp };
359
+ },
360
+ writeNext: async (patch, readAt) => {
361
+ const prepared = preparedMutation(
362
+ {
363
+ readAt,
364
+ onStale: 'reject' as const,
365
+ reads: options?.reads,
366
+ },
367
+ );
368
+ try {
369
+ const result = await protocol.update({
370
+ ...prepared.options,
371
+ id: arg,
372
+ data: patch,
373
+ });
374
+ consumeReadSet(
375
+ readSetContext,
376
+ clientIdentity,
377
+ prepared.consumed,
378
+ prepared.automaticCommit,
379
+ );
380
+ return result;
381
+ } catch (error) {
382
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
383
+ throw error;
384
+ }
385
+ },
386
+ },
387
+ );
270
388
  return receipt === undefined ? undefined : requireUpdatedRow(arg);
271
389
  }
272
390
 
273
- await protocol.update({
274
- ...mutationOptions(arg),
275
- data: arg.data,
276
- });
391
+ const prepared = preparedMutation(arg);
392
+ try {
393
+ await protocol.update({
394
+ ...prepared.options,
395
+ id: arg.id,
396
+ data: arg.data,
397
+ });
398
+ } catch (error) {
399
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
400
+ throw error;
401
+ }
402
+ consumeReadSet(
403
+ readSetContext,
404
+ clientIdentity,
405
+ prepared.consumed,
406
+ prepared.automaticCommit,
407
+ );
277
408
  return requireUpdatedRow(arg.id);
278
409
  }
279
410
 
280
- const get = async (params: ModelRetrieveParams & ModelReadOptions): Promise<T | undefined> => {
411
+ const get = async (
412
+ params: ModelRetrieveParams & ModelReadOptions,
413
+ ): Promise<CapturedRow<T> | undefined> => {
281
414
  const read = await protocol.get(params);
282
- return read.data;
415
+ capturePointRead(
416
+ readSetContext,
417
+ clientIdentity,
418
+ modelName,
419
+ params.id,
420
+ read.data,
421
+ read.stamp,
422
+ );
423
+ return read.data as CapturedRow<T> | undefined;
424
+ };
425
+
426
+ const list = async (options?: ServerReadOptions<T>): Promise<CapturedRow<T>[]> => {
427
+ const snapshot = await protocol.list(options);
428
+ const registry = readSetContext?.getStore();
429
+ if (!registry) return [...snapshot.data] as CapturedRow<T>[];
430
+ if (!snapshot.evidence) {
431
+ throw new AbloConnectionError(
432
+ `${modelName}.list did not return row evidence. Upgrade the Ablo server or use get({ id }).`,
433
+ { code: 'commit_no_result' },
434
+ );
435
+ }
436
+ const byId = new Map(snapshot.evidence.map((entry) => [entry.id, entry.stamp]));
437
+ for (const row of snapshot.data) {
438
+ const id = typeof row === 'object' && row !== null
439
+ ? Reflect.get(row as object, 'id')
440
+ : undefined;
441
+ const stamp = typeof id === 'string' ? byId.get(id) : undefined;
442
+ if (typeof id !== 'string' || stamp === undefined) {
443
+ throw new AbloConnectionError(
444
+ `${modelName}.list returned a row without matching evidence.`,
445
+ { code: 'commit_no_result' },
446
+ );
447
+ }
448
+ capturePointRead(readSetContext, clientIdentity, modelName, id, row, stamp);
449
+ }
450
+ return [...snapshot.data] as CapturedRow<T>[];
283
451
  };
284
452
 
453
+ // Claim acquisition performs its authoritative read only after the grant.
454
+ // Preserve that exact post-grant watermark while retaining the callable
455
+ // namespace and all of its state/queue/release members through the proxy.
456
+ const claim = new Proxy(protocol.claim, {
457
+ apply(target, thisArg, argArray) {
458
+ const result = Reflect.apply(target, thisArg, argArray) as Promise<HeldClaim<T> | null>;
459
+ return result.then((held) => {
460
+ if (held && held.readAt !== undefined) {
461
+ capturePointRead(
462
+ readSetContext,
463
+ clientIdentity,
464
+ modelName,
465
+ held.target.id,
466
+ held.data,
467
+ held.readAt,
468
+ );
469
+ }
470
+ return held;
471
+ });
472
+ },
473
+ });
474
+
285
475
  return {
286
476
  get,
287
477
  retrieve: get,
288
- list: (options) => protocol.list(options),
478
+ list,
289
479
  async create(params): Promise<T> {
290
- const row = await protocol.create({
291
- ...mutationOptions(params),
292
- data: params.data as Record<string, unknown>,
293
- });
480
+ const id = params.id ?? '';
481
+ const prepared = preparedMutation(params);
482
+ let row: T;
483
+ try {
484
+ row = await protocol.create({
485
+ ...prepared.options,
486
+ ...(params.id !== undefined ? { id: params.id } : {}),
487
+ data: params.data as Record<string, unknown>,
488
+ });
489
+ } catch (error) {
490
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
491
+ throw error;
492
+ }
493
+ consumeReadSet(
494
+ readSetContext,
495
+ clientIdentity,
496
+ prepared.consumed,
497
+ prepared.automaticCommit,
498
+ );
294
499
  return row;
295
500
  },
296
501
  update,
297
502
  async delete(params): Promise<void> {
298
- await protocol.delete(mutationOptions(params));
503
+ const prepared = preparedMutation(params);
504
+ try {
505
+ await protocol.delete({ ...prepared.options, id: params.id });
506
+ } catch (error) {
507
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
508
+ throw error;
509
+ }
510
+ consumeReadSet(
511
+ readSetContext,
512
+ clientIdentity,
513
+ prepared.consumed,
514
+ prepared.automaticCommit,
515
+ );
299
516
  },
300
- claim: protocol.claim,
517
+ claim,
301
518
  track: (params) => protocol.track(params),
302
519
  };
303
520
  }
@@ -312,14 +529,20 @@ function createHttpModelClient<T, C = T>(
312
529
  export function createAbloHttpClient<S extends SchemaRecord>(
313
530
  options: AbloHttpClientOptions<S>,
314
531
  ): AbloHttpClient<S> {
315
- const { schema, ...rest } = options;
532
+ const { schema, onCommitReceipt, ...rest } = options;
533
+ const readSetContext = createReadSetContext();
316
534
  const transport: HttpTransport = createHttpTransport({
317
535
  ...rest,
318
536
  // Derived from the schema this client is bound to, never assembled by hand
319
537
  // — see `auth/capability.ts`.
320
538
  modelTypenames: modelWireNames(schema.models),
539
+ onCommitReceipt: (observation) => {
540
+ recordHttpCommitReceipt(readSetContext, observation);
541
+ onCommitReceipt?.(observation);
542
+ },
321
543
  });
322
544
  const schemaModels = new Set(Object.keys(schema.models));
545
+ const clientIdentity = {};
323
546
  const omittedModels = new Set(schema.omittedModels ?? []);
324
547
  const models = new Map<
325
548
  string,
@@ -331,13 +554,21 @@ export function createAbloHttpClient<S extends SchemaRecord>(
331
554
  ): HttpModelClient<Record<string, unknown>, Record<string, unknown>> => {
332
555
  const cached = models.get(name);
333
556
  if (cached) return cached;
334
- const created = createHttpModelClient(transport.model(name));
557
+ const created = createHttpModelClient(
558
+ transport.model(name),
559
+ name,
560
+ clientIdentity,
561
+ readSetContext,
562
+ );
335
563
  models.set(name, created);
336
564
  return created;
337
565
  };
338
566
 
339
567
  const facade = new Proxy<Partial<AbloHttpClient<S>>>({}, {
340
568
  get(_target, prop) {
569
+ if (prop === kReadEvidence) {
570
+ return { context: readSetContext, client: clientIdentity };
571
+ }
341
572
  if (typeof prop !== 'string') return undefined;
342
573
  // Real protocol members pass through unchanged.
343
574
  if (isProtocolMember(prop)) {
@@ -13,7 +13,8 @@
13
13
  import type { Schema, SchemaRecord } from '../schema/schema.js';
14
14
  import type { AuthClientOptions } from '../auth/apiKey.js';
15
15
  import type { CoordinationObservability } from '../observability.js';
16
- import type { CommitOutboxScope } from '../transactions/settlement/commitEnvelope.js';
16
+ import type { CommitOutboxScope } from '../transactions/confirmation/commitEnvelope.js';
17
+ import type { CommitReceiptWire } from '../wire/commit.js';
17
18
  import type {
18
19
  DurableWriteStore,
19
20
  DurableWritesConfig,
@@ -38,4 +39,11 @@ export interface HttpClientConfig<S extends SchemaRecord = SchemaRecord>
38
39
  commitOutboxScope?: CommitOutboxScope;
39
40
  /** Which transport carries this client's traffic. */
40
41
  transport?: 'websocket' | 'http' | undefined;
42
+ /** @internal Exact receipt phases observed while dispatching one HTTP commit. */
43
+ onCommitReceipt?: (observation: {
44
+ readonly receipt: CommitReceiptWire;
45
+ readonly method: string;
46
+ readonly path: string;
47
+ readonly body: unknown;
48
+ }) => void;
41
49
  }