@abloatai/transaction 0.48.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 (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 +54 -0
  75. package/dist/readSetContext.d.ts.map +1 -0
  76. package/dist/readSetContext.js +98 -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 +14 -2
  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 +134 -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 +18 -0
  208. package/src/logger.ts +1 -1
  209. package/src/observability.ts +1 -1
  210. package/src/readSetContext.ts +164 -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 +14 -2
  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 +256 -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
@@ -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,24 @@ 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
+ prepareReadSet,
64
+ type ReadSetContext,
65
+ } from '../readSetContext.js';
66
+ import { recordHttpCommitReceipt } from '../commitRecordRuntime.js';
67
+ import type { EffectiveAuthority } from '../auth/capability.js';
51
68
 
52
69
  export interface AbloHttpClientOptions<S extends SchemaRecord>
53
70
  extends HttpClientConfig<S> {
@@ -59,6 +76,18 @@ export interface AbloHttpClientOptions<S extends SchemaRecord>
59
76
  readonly timeoutMs?: number;
60
77
  }
61
78
 
79
+ declare const capturedRowBrand: unique symbol;
80
+
81
+ /** A point-read row whose exact watermark is privately retained by its client. */
82
+ export type CapturedRow<T = unknown> = T & {
83
+ readonly [capturedRowBrand]: true;
84
+ };
85
+
86
+ /** Exact returned rows or low-level canonical dependencies accepted by `reads`. */
87
+ export type HttpModelMutationParams<P> = Omit<P, 'reads'> & {
88
+ readonly reads?: readonly (ReadDependency | CapturedRow)[] | null;
89
+ };
90
+
62
91
  /**
63
92
  * The per-model surface of the stateless HTTP client — everything reachable over
64
93
  * request/response: reads (`get` and `list`), writes (`create`, `update`,
@@ -91,14 +120,14 @@ export interface HttpModelClient<T, C = T> {
91
120
  * to what is already here.
92
121
  */
93
122
  /** Canonical authoritative point lookup. A miss resolves to `undefined`. */
94
- get(params: ModelRetrieveParams & ModelReadOptions): Promise<T | undefined>;
123
+ get(params: ModelRetrieveParams & ModelReadOptions): Promise<CapturedRow<T> | undefined>;
95
124
  /** @deprecated Use `get({ id })` for an authoritative point lookup. */
96
- retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<T | undefined>;
125
+ retrieve(params: ModelRetrieveParams & ModelReadOptions): Promise<CapturedRow<T> | undefined>;
97
126
  /**
98
127
  * Reads the rows matching a filter. Same resolution as `get`, in bulk,
99
128
  * and deduplicated so concurrent identical calls share one request.
100
129
  */
101
- list(options?: ServerReadOptions<T>): Promise<T[]>;
130
+ list(options?: ServerReadOptions<T>): Promise<CapturedRow<T>[]>;
102
131
  /**
103
132
  * Creates a row and returns it, including any framework-applied defaults.
104
133
  * Passing an id that already exists is idempotent: the existing row is
@@ -110,8 +139,8 @@ export interface HttpModelClient<T, C = T> {
110
139
  * back, so it resolves on the server's confirmation instead. The same is true
111
140
  * of `update` and `delete`.
112
141
  */
113
- create(params: ModelCreateParams<T, C>): Promise<T>;
114
- update(params: ModelUpdateParams<T, C>): Promise<T>;
142
+ create(params: HttpModelMutationParams<ModelCreateParams<T, C>>): Promise<T>;
143
+ update(params: HttpModelMutationParams<ModelUpdateParams<T, C>>): Promise<T>;
115
144
  /**
116
145
  * Updates a row with a function of its latest value — `update(id, current =>
117
146
  * next)`, the data-layer equivalent of a `setState(prev => next)` reducer. The
@@ -124,9 +153,9 @@ export interface HttpModelClient<T, C = T> {
124
153
  update(
125
154
  id: string,
126
155
  updater: ModelUpdater<T>,
127
- options?: ContentionOptions,
156
+ options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
128
157
  ): Promise<T | undefined>;
129
- delete(params: ModelDeleteParams<T, C>): Promise<void>;
158
+ delete(params: HttpModelMutationParams<ModelDeleteParams<T, C>>): Promise<void>;
130
159
  claim: HttpClaimApi<T, C>;
131
160
  /**
132
161
  * Registers a durable premise on a row — "keep telling me about this one".
@@ -164,7 +193,7 @@ export type AbloHttpClient<S extends SchemaRecord> = {
164
193
  } & {
165
194
  /** 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
195
  ready(): Promise<void>;
167
- /** Replays every pending durable HTTP write in seal order and waits for settlement. */
196
+ /** Replays every pending durable HTTP write in seal order and waits for confirmation. */
168
197
  waitForFlush(): Promise<void>;
169
198
  readonly commits: CommitResource;
170
199
  /**
@@ -177,6 +206,8 @@ export type AbloHttpClient<S extends SchemaRecord> = {
177
206
  readonly claims: HttpClaimsResource;
178
207
  /** Durable, credential-scoped authoritative event pages. */
179
208
  readonly logs: HttpLogsResource;
209
+ /** Server-confirmed authority of the credential, populated by `ready()`. */
210
+ readonly identity: EffectiveAuthority | null;
180
211
  dispose(): Promise<void>;
181
212
  /** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
182
213
  getAuthToken(): Promise<string | null>;
@@ -200,6 +231,7 @@ const PROTOCOL_MEMBERS = new Set<string>([
200
231
  'commits',
201
232
  'claims',
202
233
  'logs',
234
+ 'identity',
203
235
  'getAuthToken',
204
236
  'sessions',
205
237
  ]);
@@ -217,9 +249,13 @@ function isProtocolMember(prop: string): prop is keyof HttpTransport {
217
249
  */
218
250
  function createHttpModelClient<T, C = T>(
219
251
  protocol: HttpTransportModel<T, C>,
252
+ modelName: string,
253
+ clientIdentity: object,
254
+ readSetContext: ReadSetContext | undefined,
220
255
  ): HttpModelClient<T, C> {
221
256
  async function readRow(id: string): Promise<T | undefined> {
222
257
  const read = await protocol.get({ id });
258
+ capturePointRead(readSetContext, clientIdentity, modelName, id, read.data, read.stamp);
223
259
  return read.data;
224
260
  }
225
261
 
@@ -248,16 +284,48 @@ function createHttpModelClient<T, C = T>(
248
284
  };
249
285
  }
250
286
 
251
- function update(params: ModelUpdateParams<T, C>): Promise<T>;
287
+ function preparedMutation<P extends {
288
+ readonly claim?: unknown;
289
+ readonly idempotencyKey?: string | null;
290
+ readonly readAt?: number | null;
291
+ readonly onStale?: OnStaleMode | null;
292
+ readonly reads?: readonly unknown[] | null;
293
+ }>(
294
+ params: P,
295
+ ) {
296
+ const prepared = prepareReadSet(
297
+ readSetContext,
298
+ clientIdentity,
299
+ params.readAt,
300
+ params.onStale,
301
+ params.idempotencyKey,
302
+ params.reads,
303
+ );
304
+ return {
305
+ options: {
306
+ ...mutationOptions(params),
307
+ ...(prepared.readAt !== undefined ? { readAt: prepared.readAt } : {}),
308
+ ...(prepared.onStale !== undefined ? { onStale: prepared.onStale } : {}),
309
+ ...(prepared.reads !== undefined ? { reads: prepared.reads } : {}),
310
+ ...(prepared.idempotencyKey !== undefined
311
+ ? { idempotencyKey: prepared.idempotencyKey }
312
+ : {}),
313
+ },
314
+ consumed: prepared.consumed,
315
+ automaticCommit: prepared.automaticCommit,
316
+ };
317
+ }
318
+
319
+ function update(params: HttpModelMutationParams<ModelUpdateParams<T, C>>): Promise<T>;
252
320
  function update(
253
321
  id: string,
254
322
  updater: ModelUpdater<T>,
255
- options?: ContentionOptions,
323
+ options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
256
324
  ): Promise<T | undefined>;
257
325
  async function update(
258
- arg: ModelUpdateParams<T, C> | string,
326
+ arg: HttpModelMutationParams<ModelUpdateParams<T, C>> | string,
259
327
  updater?: ModelUpdater<T>,
260
- options?: ContentionOptions,
328
+ options?: FunctionalUpdateOptions<ReadDependency | CapturedRow>,
261
329
  ): Promise<T | undefined> {
262
330
  if (typeof arg === 'string') {
263
331
  if (!updater) {
@@ -266,38 +334,186 @@ function createHttpModelClient<T, C = T>(
266
334
  { code: 'write_options_invalid' },
267
335
  );
268
336
  }
269
- const receipt = await protocol.update(arg, updater, options);
337
+ const receipt = await reconcileFunctionalUpdate<
338
+ T,
339
+ CommitReceipt,
340
+ ReadDependency | CapturedRow
341
+ >(
342
+ updater,
343
+ options,
344
+ {
345
+ model: modelName,
346
+ id: arg,
347
+ readFresh: async () => {
348
+ const read = await protocol.get({ id: arg });
349
+ capturePointRead(
350
+ readSetContext,
351
+ clientIdentity,
352
+ modelName,
353
+ arg,
354
+ read.data,
355
+ read.stamp,
356
+ );
357
+ return { data: read.data, stamp: read.stamp };
358
+ },
359
+ writeNext: async (patch, readAt) => {
360
+ const prepared = preparedMutation(
361
+ {
362
+ readAt,
363
+ onStale: 'reject' as const,
364
+ reads: options?.reads,
365
+ },
366
+ );
367
+ try {
368
+ const result = await protocol.update({
369
+ ...prepared.options,
370
+ id: arg,
371
+ data: patch,
372
+ });
373
+ consumeReadSet(
374
+ readSetContext,
375
+ clientIdentity,
376
+ prepared.consumed,
377
+ prepared.automaticCommit,
378
+ );
379
+ return result;
380
+ } catch (error) {
381
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
382
+ throw error;
383
+ }
384
+ },
385
+ },
386
+ );
270
387
  return receipt === undefined ? undefined : requireUpdatedRow(arg);
271
388
  }
272
389
 
273
- await protocol.update({
274
- ...mutationOptions(arg),
275
- data: arg.data,
276
- });
390
+ const prepared = preparedMutation(arg);
391
+ try {
392
+ await protocol.update({
393
+ ...prepared.options,
394
+ id: arg.id,
395
+ data: arg.data,
396
+ });
397
+ } catch (error) {
398
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
399
+ throw error;
400
+ }
401
+ consumeReadSet(
402
+ readSetContext,
403
+ clientIdentity,
404
+ prepared.consumed,
405
+ prepared.automaticCommit,
406
+ );
277
407
  return requireUpdatedRow(arg.id);
278
408
  }
279
409
 
280
- const get = async (params: ModelRetrieveParams & ModelReadOptions): Promise<T | undefined> => {
410
+ const get = async (
411
+ params: ModelRetrieveParams & ModelReadOptions,
412
+ ): Promise<CapturedRow<T> | undefined> => {
281
413
  const read = await protocol.get(params);
282
- return read.data;
414
+ capturePointRead(
415
+ readSetContext,
416
+ clientIdentity,
417
+ modelName,
418
+ params.id,
419
+ read.data,
420
+ read.stamp,
421
+ );
422
+ return read.data as CapturedRow<T> | undefined;
283
423
  };
284
424
 
425
+ const list = async (options?: ServerReadOptions<T>): Promise<CapturedRow<T>[]> => {
426
+ const snapshot = await protocol.list(options);
427
+ const registry = readSetContext?.getStore();
428
+ if (!registry) return [...snapshot.data] as CapturedRow<T>[];
429
+ if (!snapshot.evidence) {
430
+ throw new AbloConnectionError(
431
+ `${modelName}.list did not return row evidence. Upgrade the Ablo server or use get({ id }).`,
432
+ { code: 'commit_no_result' },
433
+ );
434
+ }
435
+ const byId = new Map(snapshot.evidence.map((entry) => [entry.id, entry.stamp]));
436
+ for (const row of snapshot.data) {
437
+ const id = typeof row === 'object' && row !== null
438
+ ? Reflect.get(row as object, 'id')
439
+ : undefined;
440
+ const stamp = typeof id === 'string' ? byId.get(id) : undefined;
441
+ if (typeof id !== 'string' || stamp === undefined) {
442
+ throw new AbloConnectionError(
443
+ `${modelName}.list returned a row without matching evidence.`,
444
+ { code: 'commit_no_result' },
445
+ );
446
+ }
447
+ capturePointRead(readSetContext, clientIdentity, modelName, id, row, stamp);
448
+ }
449
+ return [...snapshot.data] as CapturedRow<T>[];
450
+ };
451
+
452
+ // Claim acquisition performs its authoritative read only after the grant.
453
+ // Preserve that exact post-grant watermark while retaining the callable
454
+ // namespace and all of its state/queue/release members through the proxy.
455
+ const claim = new Proxy(protocol.claim, {
456
+ apply(target, thisArg, argArray) {
457
+ const result = Reflect.apply(target, thisArg, argArray) as Promise<HeldClaim<T> | null>;
458
+ return result.then((held) => {
459
+ if (held && held.readAt !== undefined) {
460
+ capturePointRead(
461
+ readSetContext,
462
+ clientIdentity,
463
+ modelName,
464
+ held.target.id,
465
+ held.data,
466
+ held.readAt,
467
+ );
468
+ }
469
+ return held;
470
+ });
471
+ },
472
+ });
473
+
285
474
  return {
286
475
  get,
287
476
  retrieve: get,
288
- list: (options) => protocol.list(options),
477
+ list,
289
478
  async create(params): Promise<T> {
290
- const row = await protocol.create({
291
- ...mutationOptions(params),
292
- data: params.data as Record<string, unknown>,
293
- });
479
+ const id = params.id ?? '';
480
+ const prepared = preparedMutation(params);
481
+ let row: T;
482
+ try {
483
+ row = await protocol.create({
484
+ ...prepared.options,
485
+ ...(params.id !== undefined ? { id: params.id } : {}),
486
+ data: params.data as Record<string, unknown>,
487
+ });
488
+ } catch (error) {
489
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
490
+ throw error;
491
+ }
492
+ consumeReadSet(
493
+ readSetContext,
494
+ clientIdentity,
495
+ prepared.consumed,
496
+ prepared.automaticCommit,
497
+ );
294
498
  return row;
295
499
  },
296
500
  update,
297
501
  async delete(params): Promise<void> {
298
- await protocol.delete(mutationOptions(params));
502
+ const prepared = preparedMutation(params);
503
+ try {
504
+ await protocol.delete({ ...prepared.options, id: params.id });
505
+ } catch (error) {
506
+ abortReadSetCommit(readSetContext, prepared.automaticCommit);
507
+ throw error;
508
+ }
509
+ consumeReadSet(
510
+ readSetContext,
511
+ clientIdentity,
512
+ prepared.consumed,
513
+ prepared.automaticCommit,
514
+ );
299
515
  },
300
- claim: protocol.claim,
516
+ claim,
301
517
  track: (params) => protocol.track(params),
302
518
  };
303
519
  }
@@ -312,14 +528,20 @@ function createHttpModelClient<T, C = T>(
312
528
  export function createAbloHttpClient<S extends SchemaRecord>(
313
529
  options: AbloHttpClientOptions<S>,
314
530
  ): AbloHttpClient<S> {
315
- const { schema, ...rest } = options;
531
+ const { schema, onCommitReceipt, ...rest } = options;
532
+ const readSetContext = createReadSetContext();
316
533
  const transport: HttpTransport = createHttpTransport({
317
534
  ...rest,
318
535
  // Derived from the schema this client is bound to, never assembled by hand
319
536
  // — see `auth/capability.ts`.
320
537
  modelTypenames: modelWireNames(schema.models),
538
+ onCommitReceipt: (observation) => {
539
+ recordHttpCommitReceipt(readSetContext, observation);
540
+ onCommitReceipt?.(observation);
541
+ },
321
542
  });
322
543
  const schemaModels = new Set(Object.keys(schema.models));
544
+ const clientIdentity = {};
323
545
  const omittedModels = new Set(schema.omittedModels ?? []);
324
546
  const models = new Map<
325
547
  string,
@@ -331,7 +553,12 @@ export function createAbloHttpClient<S extends SchemaRecord>(
331
553
  ): HttpModelClient<Record<string, unknown>, Record<string, unknown>> => {
332
554
  const cached = models.get(name);
333
555
  if (cached) return cached;
334
- const created = createHttpModelClient(transport.model(name));
556
+ const created = createHttpModelClient(
557
+ transport.model(name),
558
+ name,
559
+ clientIdentity,
560
+ readSetContext,
561
+ );
335
562
  models.set(name, created);
336
563
  return created;
337
564
  };
@@ -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
  }