@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
@@ -4,7 +4,7 @@ import {
4
4
  hasWireCode,
5
5
  translateHttpError,
6
6
  } from '../errors.js';
7
- import { capabilityScopeSchema } from './capability.js';
7
+ import { effectiveAuthoritySchema } from './capability.js';
8
8
  import type {
9
9
  RotateSessionParams,
10
10
  SessionRevocation,
@@ -27,7 +27,8 @@ export const capabilityRotationResponseSchema = z.object({
27
27
  token: z.string().trim().min(1),
28
28
  expiresAt: z.iso.datetime().nullable(),
29
29
  organizationId: z.string().min(1),
30
- scope: capabilityScopeSchema,
30
+ branchRoot: z.boolean().default(false),
31
+ scope: effectiveAuthoritySchema,
31
32
  rotatedFrom: z.object({
32
33
  capabilityId: z.string().min(1),
33
34
  expiresAt: z.iso.datetime(),
@@ -34,6 +34,7 @@ import type { AuthCredentialSource } from '../auth/credentialSource.js';
34
34
  import type { CredentialProvider } from './credentialResult.js';
35
35
  import { resolveApiKeyValue, resolveBootstrapBaseUrl } from './apiKey.js';
36
36
  import type { DeliveryPartitionRoute } from './deliveryPartition.js';
37
+ import type { EffectiveAuthority } from './capability.js';
37
38
 
38
39
  export interface IdentityResolveInput {
39
40
  readonly options: {
@@ -71,6 +72,8 @@ export interface ResolvedIdentity {
71
72
  readonly syncGroups: readonly string[] | undefined;
72
73
  readonly participantKind: ParticipantKind;
73
74
  readonly deliveryPartition: DeliveryPartitionRoute | null;
75
+ /** Effective authority confirmed by the server (or explicit self-hosted configuration). */
76
+ readonly authority: EffectiveAuthority;
74
77
  /** Set only on the hosted-cloud path; the caller keeps it to stop refreshes on shutdown. */
75
78
  readonly refreshScheduler: RefreshScheduler | null;
76
79
  }
@@ -195,6 +198,16 @@ export async function resolveParticipantIdentity(
195
198
  syncGroups: options.syncGroups,
196
199
  participantKind: kind,
197
200
  deliveryPartition: null,
201
+ authority: {
202
+ organizationId: accountScope,
203
+ projectId: accountScope,
204
+ branchId: internalOptions.branchId ?? null,
205
+ participantKind: kind,
206
+ participantId: userId,
207
+ operations: [],
208
+ syncGroups: options.syncGroups ?? [],
209
+ deliveryPartition: null,
210
+ },
198
211
  refreshScheduler: null,
199
212
  };
200
213
  }
@@ -246,6 +259,7 @@ async function resolveViaIdentity(
246
259
  syncGroups: mergedSyncGroups,
247
260
  participantKind: identity.participantKind,
248
261
  deliveryPartition: identity.deliveryPartition,
262
+ authority: identity.authority,
249
263
  refreshScheduler: null,
250
264
  };
251
265
  }
@@ -322,7 +336,7 @@ async function resolveHosted(input: HostedInput): Promise<ResolvedIdentity> {
322
336
  accountScope: exchange.scope.organizationId,
323
337
  projectId: exchange.scope.projectId ?? exchange.scope.organizationId,
324
338
  branchId: exchange.scope.branchId,
325
- branchRoot: exchange.scope.branchRoot,
339
+ branchRoot: exchange.branchRoot,
326
340
  // teamIds isn't needed because the server already encoded
327
341
  // team-level access into scope.syncGroups.
328
342
  teamIds: undefined,
@@ -330,6 +344,7 @@ async function resolveHosted(input: HostedInput): Promise<ResolvedIdentity> {
330
344
  syncGroups: exchange.scope.syncGroups,
331
345
  participantKind: input.kind,
332
346
  deliveryPartition: exchange.scope.deliveryPartition,
347
+ authority: exchange.scope,
333
348
  refreshScheduler,
334
349
  };
335
350
  }
package/src/auth/index.ts CHANGED
@@ -74,6 +74,7 @@ export {
74
74
  capabilityOperationSchema,
75
75
  capabilityCanSchema,
76
76
  capabilityCanSchemaFor,
77
+ effectiveAuthoritySchema,
77
78
  capabilityScopeSchema,
78
79
  capabilityRequestSchema,
79
80
  capabilityMintResponseSchema,
@@ -88,6 +89,7 @@ export type {
88
89
  CapabilityOperation,
89
90
  CapabilityCan,
90
91
  CapabilityGrant,
92
+ EffectiveAuthority,
91
93
  CapabilityScope,
92
94
  CapabilityMintResponse,
93
95
  CapabilityRequest,
@@ -2,6 +2,7 @@ import { z } from 'zod';
2
2
  import { participantKindSchema } from '../coordination/schema.js';
3
3
  import {
4
4
  capabilityMintResponseSchema,
5
+ effectiveAuthoritySchema,
5
6
  grantedOperationSchema,
6
7
  type CapabilityMintResponse,
7
8
  } from './capability.js';
@@ -26,6 +27,8 @@ export const IdentityResolveResponseSchema = z.object({
26
27
  branchRoot: z.boolean().default(false),
27
28
  syncGroups: z.array(z.string()),
28
29
  deliveryPartition: deliveryPartitionRouteSchema.nullable().default(null),
30
+ /** Canonical server-confirmed authority of the authenticated credential. */
31
+ authority: effectiveAuthoritySchema,
29
32
  userMeta: z.record(z.string(), z.unknown()),
30
33
  });
31
34
 
@@ -93,7 +93,6 @@ export async function mintSession<S extends SchemaRecord>(
93
93
  organizationId: res.organizationId,
94
94
  projectId: res.projectId,
95
95
  branchId: res.branchId,
96
- branchRoot: res.branchRoot,
97
96
  syncGroups: res.syncGroups,
98
97
  operations: res.operations,
99
98
  participantKind: 'user',
@@ -0,0 +1,245 @@
1
+ import {
2
+ DEFAULT_TRACK_ON_STALE,
3
+ type CommitReadSetEntry,
4
+ type ReadDependency,
5
+ type ReadSetEntry,
6
+ type TrackDependency,
7
+ } from './coordination/schema.js';
8
+ import {
9
+ commitRecordIdentity,
10
+ publishCommitRecord,
11
+ type ReadSetContext,
12
+ } from './readSetContext.js';
13
+ import {
14
+ commitRecordSchema,
15
+ type CommitOperationBody,
16
+ type CommitReceiptWire,
17
+ } from './wire/commit.js';
18
+
19
+ interface HttpCommitObservation {
20
+ readonly receipt: CommitReceiptWire;
21
+ readonly method: string;
22
+ readonly path: string;
23
+ readonly body: unknown;
24
+ }
25
+
26
+ interface WebSocketCommitObservation {
27
+ readonly receipt: CommitReceiptWire;
28
+ readonly operations: readonly {
29
+ readonly type: string;
30
+ readonly model: string;
31
+ readonly id: string;
32
+ readonly input?: Record<string, unknown>;
33
+ readonly claimId?: string | null;
34
+ readonly readAt?: number | null;
35
+ readonly onStale?: CommitOperationBody['onStale'] | null;
36
+ readonly fenceToken?: number | null;
37
+ }[];
38
+ readonly reads?: readonly ReadDependency[] | null;
39
+ readonly track?: readonly TrackDependency[] | null;
40
+ }
41
+
42
+ function commitEntry(dependency: ReadDependency): CommitReadSetEntry {
43
+ return {
44
+ target: 'group' in dependency
45
+ ? { scope: 'group', group: dependency.group }
46
+ : {
47
+ scope: 'row',
48
+ model: dependency.model,
49
+ id: dependency.id,
50
+ ...(dependency.fields ? { fields: dependency.fields } : {}),
51
+ },
52
+ watermark: dependency.readAt,
53
+ lifetime: 'commit',
54
+ onStale: dependency.onStale ?? 'reject',
55
+ };
56
+ }
57
+
58
+ function persistedEntry(
59
+ dependency: TrackDependency,
60
+ defaultWatermark: number,
61
+ ): ReadSetEntry {
62
+ return {
63
+ target: 'group' in dependency
64
+ ? { scope: 'group', group: dependency.group }
65
+ : { scope: 'row', model: dependency.model, id: dependency.id },
66
+ watermark: dependency.readAt ?? defaultWatermark,
67
+ lifetime: 'persisted',
68
+ onStale: dependency.onStale ?? DEFAULT_TRACK_ON_STALE,
69
+ };
70
+ }
71
+
72
+ function bodyRecord(value: unknown): Record<string, unknown> {
73
+ return typeof value === 'object' && value !== null
74
+ ? value as Record<string, unknown>
75
+ : {};
76
+ }
77
+
78
+ function retainedOperations(operations: readonly CommitOperationBody[]) {
79
+ return operations.map(({ data: _data, ...operation }) => ({
80
+ ...operation,
81
+ data: { retention: 'redacted' as const },
82
+ }));
83
+ }
84
+
85
+ function commitEvidence(
86
+ receipt: CommitReceiptWire,
87
+ operations: readonly CommitOperationBody[],
88
+ transport: 'http' | 'websocket',
89
+ ) {
90
+ return {
91
+ attempts: [{
92
+ id: receipt.clientTxId,
93
+ observedAt: receipt.createdAt,
94
+ transport,
95
+ kind: 'execution' as const,
96
+ }],
97
+ actor: {
98
+ kind: receipt.authority.participantKind,
99
+ id: receipt.authority.participantId,
100
+ },
101
+ // A receipt does not prove which live claim grant the server matched.
102
+ // Durable server records populate this from the claim gate instead.
103
+ claims: [],
104
+ };
105
+ }
106
+
107
+ function operationFromModelRequest(
108
+ observation: HttpCommitObservation,
109
+ body: Record<string, unknown>,
110
+ ): CommitOperationBody[] {
111
+ const segments = observation.path.split('/').filter(Boolean);
112
+ const modelIndex = segments.indexOf('models');
113
+ if (modelIndex < 0) return [];
114
+ const model = decodeURIComponent(segments[modelIndex + 1] ?? '');
115
+ const pathId = segments[modelIndex + 2];
116
+ const action = observation.method === 'POST'
117
+ ? 'create'
118
+ : observation.method === 'DELETE'
119
+ ? 'delete'
120
+ : 'update';
121
+ return [{
122
+ action,
123
+ model,
124
+ ...(pathId ? { id: decodeURIComponent(pathId) } : typeof body.id === 'string' ? { id: body.id } : {}),
125
+ ...(body.data && typeof body.data === 'object'
126
+ ? { data: body.data as Record<string, unknown> }
127
+ : {}),
128
+ ...(typeof body.readAt === 'number' ? { readAt: body.readAt } : {}),
129
+ ...(typeof body.onStale === 'string'
130
+ ? { onStale: body.onStale as CommitOperationBody['onStale'] }
131
+ : {}),
132
+ ...(typeof body.fenceToken === 'number' ? { fenceToken: body.fenceToken } : {}),
133
+ ...(typeof body.claim === 'string' ? { claimId: body.claim } : {}),
134
+ }];
135
+ }
136
+
137
+ /** @internal Joins exact HTTP request and receipt projections. */
138
+ export function recordHttpCommitReceipt(
139
+ context: ReadSetContext | undefined,
140
+ observation: HttpCommitObservation,
141
+ ): void {
142
+ if (!context?.getStore()) return;
143
+ const body = bodyRecord(observation.body);
144
+ const operations = Array.isArray(body.operations)
145
+ ? body.operations as CommitOperationBody[]
146
+ : operationFromModelRequest(observation, body);
147
+ const reads = Array.isArray(body.reads) ? body.reads as ReadDependency[] : [];
148
+ const track = Array.isArray(body.track) ? body.track as TrackDependency[] : [];
149
+ const readSet: ReadSetEntry[] = reads.map(commitEntry);
150
+ for (const operation of operations) {
151
+ if (typeof operation.readAt !== 'number' || !operation.id) continue;
152
+ readSet.push(commitEntry({
153
+ model: operation.model,
154
+ id: operation.id,
155
+ readAt: operation.readAt,
156
+ onStale: operation.onStale ?? 'reject',
157
+ }));
158
+ }
159
+ readSet.push(...track.map((entry) => persistedEntry(entry, observation.receipt.lastSyncId)));
160
+
161
+ const record = commitRecordSchema.parse({
162
+ id: commitRecordIdentity(context, observation.receipt.clientTxId).id,
163
+ createdAt: observation.receipt.createdAt,
164
+ status: observation.receipt.status,
165
+ statusAt: observation.receipt.statusAt,
166
+ lastSyncId: observation.receipt.lastSyncId,
167
+ ...(observation.receipt.correlationId
168
+ ? { correlationId: observation.receipt.correlationId }
169
+ : {}),
170
+ readSet,
171
+ operations: retainedOperations(operations),
172
+ ...commitEvidence(observation.receipt, operations, 'http'),
173
+ authority: observation.receipt.authority,
174
+ receipt: {
175
+ clientTxId: observation.receipt.clientTxId,
176
+ serverTxId: observation.receipt.serverTxId,
177
+ ops: observation.receipt.ops,
178
+ ...(observation.receipt.notifications
179
+ ? { notifications: observation.receipt.notifications }
180
+ : {}),
181
+ ...(observation.receipt.missingIds
182
+ ? { missingIds: observation.receipt.missingIds }
183
+ : {}),
184
+ },
185
+ });
186
+ publishCommitRecord(context, record);
187
+ }
188
+
189
+ /** @internal Joins an exact commit frame and its authoritative WS receipt. */
190
+ export function recordWebSocketCommitReceipt(
191
+ context: ReadSetContext | undefined,
192
+ observation: WebSocketCommitObservation,
193
+ ): void {
194
+ if (!context?.getStore()) return;
195
+ const operations: CommitOperationBody[] = observation.operations.map((operation) => ({
196
+ action: operation.type.toLowerCase() as CommitOperationBody['action'],
197
+ model: operation.model,
198
+ id: operation.id,
199
+ ...(operation.input ? { data: operation.input } : {}),
200
+ ...(operation.claimId ? { claimId: operation.claimId } : {}),
201
+ ...(typeof operation.readAt === 'number' ? { readAt: operation.readAt } : {}),
202
+ ...(operation.onStale ? { onStale: operation.onStale } : {}),
203
+ ...(typeof operation.fenceToken === 'number'
204
+ ? { fenceToken: operation.fenceToken }
205
+ : {}),
206
+ }));
207
+ const readSet: ReadSetEntry[] = (observation.reads ?? []).map(commitEntry);
208
+ for (const operation of operations) {
209
+ if (typeof operation.readAt !== 'number' || !operation.id) continue;
210
+ readSet.push(commitEntry({
211
+ model: operation.model,
212
+ id: operation.id,
213
+ readAt: operation.readAt,
214
+ onStale: operation.onStale ?? 'reject',
215
+ }));
216
+ }
217
+ readSet.push(...(observation.track ?? []).map((entry) =>
218
+ persistedEntry(entry, observation.receipt.lastSyncId)));
219
+ const record = commitRecordSchema.parse({
220
+ id: commitRecordIdentity(context, observation.receipt.clientTxId).id,
221
+ createdAt: observation.receipt.createdAt,
222
+ status: observation.receipt.status,
223
+ statusAt: observation.receipt.statusAt,
224
+ lastSyncId: observation.receipt.lastSyncId,
225
+ ...(observation.receipt.correlationId
226
+ ? { correlationId: observation.receipt.correlationId }
227
+ : {}),
228
+ readSet,
229
+ operations: retainedOperations(operations),
230
+ ...commitEvidence(observation.receipt, operations, 'websocket'),
231
+ authority: observation.receipt.authority,
232
+ receipt: {
233
+ clientTxId: observation.receipt.clientTxId,
234
+ serverTxId: observation.receipt.serverTxId,
235
+ ops: observation.receipt.ops,
236
+ ...(observation.receipt.notifications
237
+ ? { notifications: observation.receipt.notifications }
238
+ : {}),
239
+ ...(observation.receipt.missingIds
240
+ ? { missingIds: observation.receipt.missingIds }
241
+ : {}),
242
+ },
243
+ });
244
+ publishCommitRecord(context, record);
245
+ }
@@ -22,6 +22,17 @@ export {
22
22
  targetRefSchema,
23
23
  // Layer 3 — optimistic stale-context
24
24
  onStaleModeSchema,
25
+ MAX_READ_SET_ENTRIES,
26
+ readSetWatermarkSchema,
27
+ readSetRowTargetSchema,
28
+ readSetGroupTargetSchema,
29
+ commitReadSetTargetSchema,
30
+ persistedReadSetRowTargetSchema,
31
+ persistedReadSetTargetSchema,
32
+ commitReadSetEntrySchema,
33
+ persistedReadSetEntrySchema,
34
+ readSetEntrySchema,
35
+ readSetSchema,
25
36
  writeGuardSchema,
26
37
  staleNotificationSchema,
27
38
  rowStaleNotificationSchema,
@@ -29,7 +40,10 @@ export {
29
40
  stalePropagationSchema,
30
41
 
31
42
  readDependencySchema,
43
+ readDependencyListSchema,
44
+ readSetProjectionEntryCount,
32
45
  trackDependencySchema,
46
+ trackDependencyListSchema,
33
47
  // Layer 2 — pessimistic claim / claim-lease
34
48
  claimStatusSchema,
35
49
  wireClaimStatusSchema,
@@ -103,6 +117,15 @@ export type {
103
117
  TargetRef,
104
118
  // Layer 3 — optimistic stale-context
105
119
  OnStaleMode,
120
+ ReadSetWatermark,
121
+ ReadSetRowTarget,
122
+ ReadSetGroupTarget,
123
+ CommitReadSetTarget,
124
+ PersistedReadSetTarget,
125
+ CommitReadSetEntry,
126
+ PersistedReadSetEntry,
127
+ ReadSetEntry,
128
+ ReadSet,
106
129
  WriteGuard,
107
130
  StaleNotification,
108
131
  RowStaleNotification,
@@ -179,6 +202,7 @@ export type {
179
202
  } from './locator.js';
180
203
  export {
181
204
  batchFence,
205
+ claimIdFor,
182
206
  fenceTokenFor,
183
207
  isTargetTuple,
184
208
  subTarget,
@@ -186,4 +210,3 @@ export {
186
210
  modelTarget,
187
211
  streamTarget,
188
212
  } from './locator.js';
189
-
@@ -198,3 +198,20 @@ export function fenceTokenFor(
198
198
  ? fence.token
199
199
  : null;
200
200
  }
201
+
202
+ /**
203
+ * The claim identity an operation carries when it writes the row named by a
204
+ * held claim. Like the fence token, a claim id cannot travel to another row.
205
+ */
206
+ export function claimIdFor(
207
+ source: EntityLocator | null | undefined,
208
+ claimId: string | null | undefined,
209
+ model: string,
210
+ id: string | null,
211
+ ): string | null {
212
+ if (source == null || claimId == null || id === null) return null;
213
+ const target = modelTarget(source);
214
+ return model.toLowerCase() === target.model.toLowerCase() && id === target.id
215
+ ? claimId
216
+ : null;
217
+ }
@@ -267,6 +267,17 @@ const streamTargetSchema = targetRefSchema
267
267
  export const onStaleModeSchema = z.enum(['reject', 'overwrite', 'notify']);
268
268
  export type OnStaleMode = z.infer<typeof onStaleModeSchema>;
269
269
 
270
+ /** The exact authoritative position retained for one ReadSet entry. */
271
+ export const readSetWatermarkSchema = z.number().int().nonnegative();
272
+ export type ReadSetWatermark = z.infer<typeof readSetWatermarkSchema>;
273
+
274
+ /** Maximum decision-input entries one logical commit may ask the server to scan. */
275
+ export const MAX_READ_SET_ENTRIES = 500;
276
+
277
+ /** A persisted entry cannot overwrite: it observes future writes, not its own. */
278
+ export const trackOnStaleSchema = onStaleModeSchema.exclude(['overwrite']);
279
+ export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
280
+
270
281
  /**
271
282
  * The optimistic guard carried on a commit operation. `readAt` is the
272
283
  * snapshot watermark from `context.capture` (null/absent ⇒ unguarded write).
@@ -274,7 +285,7 @@ export type OnStaleMode = z.infer<typeof onStaleModeSchema>;
274
285
  * claim — see the claim layer below.
275
286
  */
276
287
  export const writeGuardSchema = z.object({
277
- readAt: z.number().nullish(),
288
+ readAt: readSetWatermarkSchema.nullish(),
278
289
  onStale: onStaleModeSchema.nullish(),
279
290
  bypass: z.boolean().optional(),
280
291
  });
@@ -445,42 +456,79 @@ export const staleNotificationSchema = z.discriminatedUnion('scope', [
445
456
  ]);
446
457
  export type StaleNotification = z.infer<typeof staleNotificationSchema>;
447
458
 
448
- /**
449
- * One entry in a commit's batch premise — a read it was based on, so the
450
- * server can ask "did anything I looked at change?" — broader than the
451
- * write-target check, which only validates the rows being written. The server
452
- * re-runs stale detection against each entry at its `readAt`; a moved premise
453
- * fires the entry's `onStale` disposition (default `reject`) across the whole
454
- * batch (`notify` holds every write and notifies, `reject` aborts, `overwrite`
455
- * proceeds silently). An entry comes at one of two granularities:
456
- *
457
- * • Row — `{ model, id, readAt, fields? }`: did this specific row, or these
458
- * specific fields, change?
459
- * • Group — `{ group, readAt }`: did anything in this sync group change?
460
- * `group` is a sync-group key such as `report:abc` or `section:s1`, the
461
- * same unit a participant watches and claims.
462
- *
463
- * See `packages/ablo/docs/concurrency-convention.md` (§4) for the
464
- * governing convention and the receive → reconcile loop.
465
- */
466
- const readRowDependencySchema = z.object({
459
+ /** One row named by a commit-lifetime ReadSet entry. */
460
+ export const readSetRowTargetSchema = z.object({
461
+ scope: z.literal('row'),
467
462
  model: z.string(),
468
463
  id: z.string(),
469
- readAt: z.number(),
470
464
  fields: z.array(z.string()).readonly().optional(),
471
- onStale: onStaleModeSchema.optional(),
472
465
  });
466
+ export type ReadSetRowTarget = z.infer<typeof readSetRowTargetSchema>;
473
467
 
474
- const readGroupDependencySchema = z.object({
475
- /**
476
- * The caller-facing view of the format: `reads: [{ group: 'report:abc' }]`
477
- * type-checks inline, `'nonsense'` does not. The
478
- * {@link groupStaleNotificationSchema} this premise fires uses the branded
479
- * view, because there Ablo is the author.
480
- */
468
+ /** One sync group named by a ReadSet entry. */
469
+ export const readSetGroupTargetSchema = z.object({
470
+ scope: z.literal('group'),
481
471
  group: syncGroupRefSchema,
482
- readAt: z.number(),
483
- onStale: onStaleModeSchema.optional(),
472
+ });
473
+ export type ReadSetGroupTarget = z.infer<typeof readSetGroupTargetSchema>;
474
+
475
+ export const commitReadSetTargetSchema = z.discriminatedUnion('scope', [
476
+ readSetRowTargetSchema,
477
+ readSetGroupTargetSchema,
478
+ ]);
479
+ export type CommitReadSetTarget = z.infer<typeof commitReadSetTargetSchema>;
480
+
481
+ /** Persisted row tracking is row-grained; it has no field storage axis. */
482
+ export const persistedReadSetRowTargetSchema = readSetRowTargetSchema.omit({ fields: true });
483
+ export const persistedReadSetTargetSchema = z.discriminatedUnion('scope', [
484
+ persistedReadSetRowTargetSchema,
485
+ readSetGroupTargetSchema,
486
+ ]);
487
+ export type PersistedReadSetTarget = z.infer<typeof persistedReadSetTargetSchema>;
488
+
489
+ /** One exact input to the decision made by this commit. */
490
+ export const commitReadSetEntrySchema = z.object({
491
+ target: commitReadSetTargetSchema,
492
+ watermark: readSetWatermarkSchema,
493
+ lifetime: z.literal('commit'),
494
+ onStale: onStaleModeSchema,
495
+ });
496
+ export type CommitReadSetEntry = z.infer<typeof commitReadSetEntrySchema>;
497
+
498
+ /** One exact input retained after the declaring commit settles. */
499
+ export const persistedReadSetEntrySchema = z.object({
500
+ target: persistedReadSetTargetSchema,
501
+ watermark: readSetWatermarkSchema,
502
+ lifetime: z.literal('persisted'),
503
+ onStale: trackOnStaleSchema,
504
+ });
505
+ export type PersistedReadSetEntry = z.infer<typeof persistedReadSetEntrySchema>;
506
+
507
+ export const readSetEntrySchema = z.discriminatedUnion('lifetime', [
508
+ commitReadSetEntrySchema,
509
+ persistedReadSetEntrySchema,
510
+ ]);
511
+ export type ReadSetEntry = z.infer<typeof readSetEntrySchema>;
512
+
513
+ export const readSetSchema = z
514
+ .array(readSetEntrySchema)
515
+ .max(MAX_READ_SET_ENTRIES)
516
+ .readonly();
517
+ export type ReadSet = z.infer<typeof readSetSchema>;
518
+
519
+ /**
520
+ * Wire projection of a commit-lifetime ReadSet entry. The public field names
521
+ * stay compatible (`readAt`, flat row/group target); every member is selected
522
+ * from the canonical entry rather than restated beside it.
523
+ */
524
+ const readRowDependencySchema = readSetRowTargetSchema.omit({ scope: true }).extend({
525
+ readAt: commitReadSetEntrySchema.shape.watermark,
526
+ onStale: commitReadSetEntrySchema.shape.onStale.optional(),
527
+ });
528
+
529
+ const readGroupDependencySchema = readSetGroupTargetSchema.omit({ scope: true }).extend({
530
+ readAt: commitReadSetEntrySchema.shape.watermark,
531
+ onStale: commitReadSetEntrySchema.shape.onStale.optional(),
484
532
  });
485
533
 
486
534
  export const readDependencySchema = z.union([
@@ -490,14 +538,14 @@ export const readDependencySchema = z.union([
490
538
  export type ReadDependency = z.infer<typeof readDependencySchema>;
491
539
 
492
540
  /**
493
- * A durable premise — what a participant is watching so that a later
541
+ * A durable ReadSet entry — what a participant is watching so that a later
494
542
  * change to it opens a {@link StaleNotification}. Where a {@link ReadDependency}
495
543
  * is checked once at commit and discarded, a `TrackDependency` is kept and
496
544
  * re-checked against every future delta. The row form watches one object; the
497
545
  * group form watches a whole sync group ("anything in `report:abc`"). See
498
546
  * `packages/ablo/docs/groups.md` for how it drives change propagation.
499
547
  *
500
- * It is a PROJECTION of the ephemeral premise, not a second declaration of the
548
+ * It is a projection of the canonical persisted entry, not a second declaration of the
501
549
  * same reference: a track names its target exactly as a read does, and the
502
550
  * three ways it differs are stated here as omissions the compiler holds.
503
551
  *
@@ -514,18 +562,10 @@ export type ReadDependency = z.infer<typeof readDependencySchema>;
514
562
  * registered the track, so a caller with nothing to say about when it last
515
563
  * looked gets "from here on".
516
564
  *
517
- * A field added to the premise reaches both halves; a field the durable half
565
+ * A field added to the ReadSet target reaches both halves; a field the durable half
518
566
  * genuinely cannot carry has to be omitted here on purpose, in one line, rather
519
567
  * than by being quietly left out of a copy.
520
568
  */
521
- /**
522
- * A track's disposition — the premise enum with `overwrite` subtracted, derived
523
- * from it rather than spelled again, so a mode added to the convention reaches
524
- * a track unless it is deliberately excluded here.
525
- */
526
- export const trackOnStaleSchema = onStaleModeSchema.exclude(['overwrite']);
527
- export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
528
-
529
569
  /**
530
570
  * What a track does when the caller says nothing — reporting, the behavior every
531
571
  * track had before the disposition existed, so an existing registration is
@@ -539,18 +579,32 @@ export type TrackOnStale = z.infer<typeof trackOnStaleSchema>;
539
579
  export const DEFAULT_TRACK_ON_STALE: TrackOnStale = 'notify';
540
580
 
541
581
  const trackBaseSchema = {
542
- readAt: z.number().optional(),
543
- onStale: trackOnStaleSchema.optional(),
582
+ readAt: persistedReadSetEntrySchema.shape.watermark.optional(),
583
+ onStale: persistedReadSetEntrySchema.shape.onStale.optional(),
544
584
  } as const;
545
585
 
546
586
  export const trackDependencySchema = z.union([
547
- readRowDependencySchema
548
- .omit({ onStale: true, fields: true, readAt: true })
549
- .extend(trackBaseSchema),
550
- readGroupDependencySchema.omit({ onStale: true, readAt: true }).extend(trackBaseSchema),
587
+ persistedReadSetRowTargetSchema.omit({ scope: true }).extend(trackBaseSchema),
588
+ readSetGroupTargetSchema.omit({ scope: true }).extend(trackBaseSchema),
551
589
  ]);
552
590
  export type TrackDependency = z.infer<typeof trackDependencySchema>;
553
591
 
592
+ /** Bounded wire projections of the two ReadSet lifetimes. */
593
+ export const readDependencyListSchema = z
594
+ .array(readDependencySchema)
595
+ .max(MAX_READ_SET_ENTRIES);
596
+ export const trackDependencyListSchema = z
597
+ .array(trackDependencySchema)
598
+ .max(MAX_READ_SET_ENTRIES);
599
+
600
+ /** Shared combined-cardinality rule for envelopes carrying both projections. */
601
+ export function readSetProjectionEntryCount(value: {
602
+ readonly reads?: readonly unknown[] | null;
603
+ readonly track?: readonly unknown[] | null;
604
+ }): number {
605
+ return (value.reads?.length ?? 0) + (value.track?.length ?? 0);
606
+ }
607
+
554
608
  // ─────────────────────────────────────────────────────────────────────────
555
609
  // Layer 2 — pessimistic claims and leases
556
610
  // ─────────────────────────────────────────────────────────────────────────
@@ -1328,7 +1382,12 @@ export type UpdateSubscriptionPayload = z.infer<
1328
1382
  export const subscriptionAckPayloadSchema = z.object({
1329
1383
  success: z.boolean(),
1330
1384
  syncGroups: z.array(z.string()),
1331
- error: z.object({ code: z.string(), message: z.string() }).optional(),
1385
+ error: z.object({
1386
+ code: z.string(),
1387
+ message: z.string(),
1388
+ request_id: z.string().optional(),
1389
+ event_id: z.string().optional(),
1390
+ }).optional(),
1332
1391
  });
1333
1392
  export type SubscriptionAckPayload = z.infer<
1334
1393
  typeof subscriptionAckPayloadSchema
@@ -1360,6 +1419,12 @@ export const commitOperationSchema = writeGuardSchema.extend({
1360
1419
  input: z.record(z.string(), z.unknown()).nullish(),
1361
1420
  /** Per-op client tx id, echoed on the broadcast delta. */
1362
1421
  transactionId: z.string().nullish(),
1422
+ /**
1423
+ * The server-issued claim identity this operation was performed under.
1424
+ * Distinct from `transactionId`: a claim attributes and fences coordinated
1425
+ * ownership, while a transaction id correlates the resulting source echo.
1426
+ */
1427
+ claimId: z.string().min(1).nullish(),
1363
1428
  /**
1364
1429
  * The fencing token from the held claim this write belongs to (Option B).
1365
1430
  * Present only on a write issued under a claim that was granted one; the