@abloatai/transaction 0.59.2 → 0.61.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 (275) hide show
  1. package/dist/auth/apiKey.d.ts +10 -11
  2. package/dist/auth/apiKey.d.ts.map +1 -1
  3. package/dist/auth/apiKey.js +15 -29
  4. package/dist/auth/apiKey.js.map +1 -1
  5. package/dist/auth/browserCredentialSafety.js +1 -1
  6. package/dist/auth/browserCredentialSafety.js.map +1 -1
  7. package/dist/auth/capability.d.ts +15 -15
  8. package/dist/auth/capabilityLifecycle.d.ts +3 -3
  9. package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
  10. package/dist/auth/capabilityLifecycle.js +2 -1
  11. package/dist/auth/capabilityLifecycle.js.map +1 -1
  12. package/dist/auth/credentialEndpoint.js +1 -1
  13. package/dist/auth/credentialEndpoint.js.map +1 -1
  14. package/dist/auth/credentialResult.d.ts +2 -1
  15. package/dist/auth/credentialResult.d.ts.map +1 -1
  16. package/dist/auth/credentialResult.js.map +1 -1
  17. package/dist/auth/credentialSource.js.map +1 -1
  18. package/dist/auth/identity.js.map +1 -1
  19. package/dist/auth/schemas.d.ts +5 -5
  20. package/dist/auth/sessionIssuanceProtocol.d.ts +8 -8
  21. package/dist/auth/sessionIssuanceProtocol.d.ts.map +1 -1
  22. package/dist/branches.d.ts +38 -38
  23. package/dist/claims/admission.d.ts +4 -0
  24. package/dist/claims/admission.d.ts.map +1 -0
  25. package/dist/claims/admission.js +32 -0
  26. package/dist/claims/admission.js.map +1 -0
  27. package/dist/claims/awaitGrant.d.ts.map +1 -1
  28. package/dist/claims/awaitGrant.js +4 -38
  29. package/dist/claims/awaitGrant.js.map +1 -1
  30. package/dist/claims/conflict.d.ts +55 -0
  31. package/dist/claims/conflict.d.ts.map +1 -0
  32. package/dist/claims/conflict.js +71 -0
  33. package/dist/claims/conflict.js.map +1 -0
  34. package/dist/claims/contract.d.ts +39 -27
  35. package/dist/claims/contract.d.ts.map +1 -1
  36. package/dist/claims/contract.js +2 -0
  37. package/dist/claims/contract.js.map +1 -1
  38. package/dist/claims/eventContract.d.ts +18 -18
  39. package/dist/claims/events.d.ts +1 -1
  40. package/dist/claims/events.d.ts.map +1 -1
  41. package/dist/claims/index.d.ts +2 -0
  42. package/dist/claims/index.d.ts.map +1 -1
  43. package/dist/claims/index.js +1 -0
  44. package/dist/claims/index.js.map +1 -1
  45. package/dist/claims/routes.d.ts +9 -9
  46. package/dist/claims/routes.d.ts.map +1 -1
  47. package/dist/claims/trace.d.ts.map +1 -1
  48. package/dist/claims/trace.js.map +1 -1
  49. package/dist/client/ablo.d.ts +11 -8
  50. package/dist/client/ablo.d.ts.map +1 -1
  51. package/dist/client/ablo.js +5 -6
  52. package/dist/client/ablo.js.map +1 -1
  53. package/dist/client/resources/httpResources.d.ts +1 -169
  54. package/dist/client/resources/httpResources.d.ts.map +1 -1
  55. package/dist/client/resources/httpResources.js +1 -1
  56. package/dist/client/resources/httpResources.js.map +1 -1
  57. package/dist/client/resources/modelOperations.d.ts +6 -15
  58. package/dist/client/resources/modelOperations.d.ts.map +1 -1
  59. package/dist/client/resources/where.d.ts +12 -12
  60. package/dist/client/surface.d.ts +1 -1
  61. package/dist/client/surface.d.ts.map +1 -1
  62. package/dist/client/surface.js +2 -4
  63. package/dist/client/surface.js.map +1 -1
  64. package/dist/commit/confirmation/commitEnvelope.d.ts +7 -7
  65. package/dist/commit/confirmation/httpCommitEnvelope.d.ts +1 -1
  66. package/dist/commit/confirmation/pendingWrite.d.ts +5 -5
  67. package/dist/commit/contract.d.ts +249 -249
  68. package/dist/commit/contract.d.ts.map +1 -1
  69. package/dist/coordination/index.d.ts +2 -2
  70. package/dist/coordination/index.d.ts.map +1 -1
  71. package/dist/coordination/index.js +4 -5
  72. package/dist/coordination/index.js.map +1 -1
  73. package/dist/coordination/schema.d.ts +64 -101
  74. package/dist/coordination/schema.d.ts.map +1 -1
  75. package/dist/coordination/schema.js +14 -69
  76. package/dist/coordination/schema.js.map +1 -1
  77. package/dist/environment.d.ts +3 -3
  78. package/dist/environment.d.ts.map +1 -1
  79. package/dist/environment.js.map +1 -1
  80. package/dist/errorCodes.d.ts +4 -5
  81. package/dist/errorCodes.d.ts.map +1 -1
  82. package/dist/errorCodes.js +15 -9
  83. package/dist/errorCodes.js.map +1 -1
  84. package/dist/errorObservation.d.ts +14 -14
  85. package/dist/errors.d.ts +17 -11
  86. package/dist/errors.d.ts.map +1 -1
  87. package/dist/errors.js +101 -5
  88. package/dist/errors.js.map +1 -1
  89. package/dist/keys/index.d.ts +1 -1
  90. package/dist/keys/index.d.ts.map +1 -1
  91. package/dist/keys/index.js.map +1 -1
  92. package/dist/observation/contract.d.ts +34 -34
  93. package/dist/observation/feedContract.d.ts +86 -86
  94. package/dist/observation/persistence/syncDeltaRow.d.ts +23 -23
  95. package/dist/observation/persistence/syncDeltaRow.d.ts.map +1 -1
  96. package/dist/pricing.d.ts +12 -12
  97. package/dist/schema/audit.js.map +1 -1
  98. package/dist/schema/ddl.js.map +1 -1
  99. package/dist/schema/ddlLock.js.map +1 -1
  100. package/dist/schema/deployment/contracts.d.ts +64 -64
  101. package/dist/schema/deployment/index.js.map +1 -1
  102. package/dist/schema/deployment/reconcile.js.map +1 -1
  103. package/dist/schema/diff.js.map +1 -1
  104. package/dist/schema/field.d.ts +1 -1
  105. package/dist/schema/field.d.ts.map +1 -1
  106. package/dist/schema/loadStrategy.d.ts +2 -2
  107. package/dist/schema/loadStrategy.d.ts.map +1 -1
  108. package/dist/schema/openapi.js.map +1 -1
  109. package/dist/schema/relation.d.ts +3 -3
  110. package/dist/schema/relation.d.ts.map +1 -1
  111. package/dist/schema/residency.d.ts +1 -1
  112. package/dist/schema/roles.d.ts +1 -1
  113. package/dist/schema/roles.d.ts.map +1 -1
  114. package/dist/schema/roles.js.map +1 -1
  115. package/dist/schema/schema.d.ts +1 -1
  116. package/dist/schema/schema.d.ts.map +1 -1
  117. package/dist/schema/serialize.js.map +1 -1
  118. package/dist/schema/sugar.d.ts.map +1 -1
  119. package/dist/schema/tenancy.d.ts +2 -2
  120. package/dist/server/storageMode.d.ts +1 -1
  121. package/dist/sessions/client.d.ts +29 -0
  122. package/dist/sessions/client.d.ts.map +1 -0
  123. package/dist/sessions/client.js +69 -0
  124. package/dist/sessions/client.js.map +1 -0
  125. package/dist/sessions/contract.d.ts +97 -0
  126. package/dist/sessions/contract.d.ts.map +1 -0
  127. package/dist/sessions/contract.js +5 -0
  128. package/dist/sessions/contract.js.map +1 -0
  129. package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
  130. package/dist/sessions/create.d.ts.map +1 -0
  131. package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
  132. package/dist/sessions/create.js.map +1 -0
  133. package/dist/sessions/handler.d.ts +23 -0
  134. package/dist/sessions/handler.d.ts.map +1 -0
  135. package/dist/sessions/handler.js +55 -0
  136. package/dist/sessions/handler.js.map +1 -0
  137. package/dist/sessions/index.d.ts +14 -0
  138. package/dist/sessions/index.d.ts.map +1 -0
  139. package/dist/sessions/index.js +14 -0
  140. package/dist/sessions/index.js.map +1 -0
  141. package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
  142. package/dist/sessions/lifecycle.d.ts.map +1 -0
  143. package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
  144. package/dist/sessions/lifecycle.js.map +1 -0
  145. package/dist/sessions/source.d.ts +24 -0
  146. package/dist/sessions/source.d.ts.map +1 -0
  147. package/dist/sessions/source.js +56 -0
  148. package/dist/sessions/source.js.map +1 -0
  149. package/dist/source/adapters/adapterProfile.d.ts +6 -6
  150. package/dist/source/adapters/conformance.js.map +1 -1
  151. package/dist/source/adapters/contract.d.ts +14 -14
  152. package/dist/source/adapters/kysely.js.map +1 -1
  153. package/dist/source/adapters/memory.js.map +1 -1
  154. package/dist/source/adapters/prisma.js.map +1 -1
  155. package/dist/source/connector/client.d.ts.map +1 -1
  156. package/dist/source/connector/client.js.map +1 -1
  157. package/dist/source/connector/protocol.d.ts.map +1 -1
  158. package/dist/source/delivery/pushQueue.d.ts.map +1 -1
  159. package/dist/source/delivery/pushQueue.js.map +1 -1
  160. package/dist/source/endpoint/signing.d.ts +4 -4
  161. package/dist/source/endpoint/signing.d.ts.map +1 -1
  162. package/dist/source/outbox/contract.d.ts +4 -4
  163. package/dist/testing/fixtures/httpResponses.d.ts +2 -2
  164. package/dist/testing/fixtures/httpResponses.d.ts.map +1 -1
  165. package/dist/testing/fixtures/httpResponses.js.map +1 -1
  166. package/dist/transport/connection/index.d.ts +0 -1
  167. package/dist/transport/connection/index.d.ts.map +1 -1
  168. package/dist/transport/connection/index.js +0 -1
  169. package/dist/transport/connection/index.js.map +1 -1
  170. package/dist/transport/connection/manager.d.ts +1 -1
  171. package/dist/transport/connection/manager.d.ts.map +1 -1
  172. package/dist/transport/connection/manager.js.map +1 -1
  173. package/dist/transport/connection/networkProbe.d.ts +5 -18
  174. package/dist/transport/connection/networkProbe.d.ts.map +1 -1
  175. package/dist/transport/connection/networkProbe.js.map +1 -1
  176. package/dist/transport/http/claimWait.d.ts +9 -0
  177. package/dist/transport/http/claimWait.d.ts.map +1 -0
  178. package/dist/transport/http/claimWait.js +74 -0
  179. package/dist/transport/http/claimWait.js.map +1 -0
  180. package/dist/transport/http/client.d.ts +11 -19
  181. package/dist/transport/http/client.d.ts.map +1 -1
  182. package/dist/transport/http/client.js +15 -18
  183. package/dist/transport/http/client.js.map +1 -1
  184. package/dist/transport/http/contract.d.ts +57 -0
  185. package/dist/transport/http/contract.d.ts.map +1 -0
  186. package/dist/transport/http/contract.js +3 -0
  187. package/dist/transport/http/contract.js.map +1 -0
  188. package/dist/transport/http/index.d.ts +1 -0
  189. package/dist/transport/http/index.d.ts.map +1 -1
  190. package/dist/transport/http/index.js +1 -0
  191. package/dist/transport/http/index.js.map +1 -1
  192. package/dist/transport/http/options.d.ts +4 -1
  193. package/dist/transport/http/options.d.ts.map +1 -1
  194. package/dist/transport/http/transport.d.ts +1 -84
  195. package/dist/transport/http/transport.d.ts.map +1 -1
  196. package/dist/transport/http/transport.js +16 -134
  197. package/dist/transport/http/transport.js.map +1 -1
  198. package/dist/transport/websocket/commitFrames.d.ts +1 -2
  199. package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
  200. package/dist/transport/websocket/commitFrames.js.map +1 -1
  201. package/dist/transport/websocket/frameHandlers.d.ts +0 -14
  202. package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
  203. package/dist/transport/websocket/frameHandlers.js +4 -71
  204. package/dist/transport/websocket/frameHandlers.js.map +1 -1
  205. package/dist/transport/websocket/heartbeat.d.ts.map +1 -1
  206. package/dist/transport/websocket/heartbeat.js.map +1 -1
  207. package/dist/transport/websocket/session.d.ts.map +1 -1
  208. package/dist/transport/websocket/session.js +137 -64
  209. package/dist/transport/websocket/session.js.map +1 -1
  210. package/dist/transport/websocket/sessionContract.d.ts +4 -15
  211. package/dist/transport/websocket/sessionContract.d.ts.map +1 -1
  212. package/dist/transport/websocket/transport.d.ts +6 -42
  213. package/dist/transport/websocket/transport.d.ts.map +1 -1
  214. package/dist/transport/websocket/transport.js +14 -101
  215. package/dist/transport/websocket/transport.js.map +1 -1
  216. package/dist/wire/accountResponses.d.ts +31 -31
  217. package/dist/wire/auth.d.ts +3 -3
  218. package/dist/wire/bootstrapReason.d.ts +2 -2
  219. package/dist/wire/dataSourceResponses.d.ts +2 -2
  220. package/dist/wire/dataSourceResponses.d.ts.map +1 -1
  221. package/dist/wire/frames.d.ts +8 -8
  222. package/dist/wire/inboundFrames.d.ts +60 -63
  223. package/dist/wire/inboundFrames.d.ts.map +1 -1
  224. package/dist/wire/inboundFrames.js +5 -6
  225. package/dist/wire/inboundFrames.js.map +1 -1
  226. package/dist/wire/listEnvelope.js.map +1 -1
  227. package/dist/wire/modelResponses.d.ts +2 -2
  228. package/dist/wire/modelShape.d.ts +4 -4
  229. package/dist/wire/pricing.d.ts +3 -3
  230. package/dist/wire/protocolVersion.d.ts.map +1 -1
  231. package/package.json +6 -1
  232. package/src/auth/apiKey.ts +25 -44
  233. package/src/auth/browserCredentialSafety.ts +1 -1
  234. package/src/auth/capabilityLifecycle.ts +3 -2
  235. package/src/auth/credentialEndpoint.ts +1 -1
  236. package/src/auth/credentialResult.ts +2 -0
  237. package/src/claims/admission.ts +49 -0
  238. package/src/claims/awaitGrant.ts +3 -52
  239. package/src/claims/conflict.ts +103 -0
  240. package/src/claims/contract.ts +2 -0
  241. package/src/claims/events.ts +1 -2
  242. package/src/claims/index.ts +11 -0
  243. package/src/client/ablo.ts +15 -10
  244. package/src/client/resources/httpResources.ts +2 -174
  245. package/src/client/resources/modelOperations.ts +0 -10
  246. package/src/client/surface.ts +2 -4
  247. package/src/coordination/index.ts +4 -9
  248. package/src/coordination/schema.ts +18 -81
  249. package/src/errorCodes.ts +21 -13
  250. package/src/errors.ts +116 -4
  251. package/src/sessions/client.ts +121 -0
  252. package/src/sessions/contract.ts +116 -0
  253. package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
  254. package/src/sessions/handler.ts +96 -0
  255. package/src/sessions/index.ts +13 -0
  256. package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
  257. package/src/sessions/source.ts +94 -0
  258. package/src/transport/connection/index.ts +0 -1
  259. package/src/transport/connection/manager.ts +1 -1
  260. package/src/transport/http/claimWait.ts +108 -0
  261. package/src/transport/http/client.ts +27 -36
  262. package/src/transport/http/contract.ts +71 -0
  263. package/src/transport/http/index.ts +1 -0
  264. package/src/transport/http/options.ts +4 -1
  265. package/src/transport/http/transport.ts +22 -275
  266. package/src/transport/websocket/commitFrames.ts +0 -2
  267. package/src/transport/websocket/frameHandlers.ts +7 -102
  268. package/src/transport/websocket/session.ts +162 -73
  269. package/src/transport/websocket/sessionContract.ts +4 -13
  270. package/src/transport/websocket/transport.ts +15 -137
  271. package/src/wire/inboundFrames.ts +5 -6
  272. package/dist/auth/sessionMint.d.ts.map +0 -1
  273. package/dist/auth/sessionMint.js.map +0 -1
  274. package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
  275. package/dist/transport/connection/credentialLifecycle.js.map +0 -1
@@ -0,0 +1,94 @@
1
+ /**
2
+ * Runtime lifecycle for a renewable session supplied to `Ablo({ session })`.
3
+ *
4
+ * A session provider mints a credential, while this boundary decides when to
5
+ * reuse it. Keeping that policy here prevents HTTP requests and the WebSocket
6
+ * bootstrap from independently minting duplicate sessions for the same actor.
7
+ */
8
+
9
+ import type {
10
+ CredentialProvider,
11
+ CredentialProviderResult,
12
+ } from '../auth/credentialResult.js';
13
+ import type {
14
+ SessionProvider,
15
+ SessionProviderResult,
16
+ SessionSource,
17
+ } from './contract.js';
18
+ import { AbloValidationError } from '../errors.js';
19
+ import {
20
+ createEndpointCredentialResolver,
21
+ isCredentialEndpoint,
22
+ } from '../auth/credentialEndpoint.js';
23
+ import { protectBrowserCredentialProvider } from '../auth/browserCredentialSafety.js';
24
+
25
+ const SESSION_REFRESH_SKEW_MS = 30_000;
26
+
27
+ function remainsUsable(session: Exclude<SessionProviderResult, null>, now: number): boolean {
28
+ const expiresAt = Date.parse(session.expiresAt);
29
+ return Number.isFinite(expiresAt) && expiresAt - now > SESSION_REFRESH_SKEW_MS;
30
+ }
31
+
32
+ /** Cache one minted session until it approaches expiry; concurrent refreshes share one mint. */
33
+ export function cachedSessionProvider(provider: SessionProvider): CredentialProvider {
34
+ let current: Exclude<SessionProviderResult, null> | null = null;
35
+ let refreshing: Promise<SessionProviderResult> | null = null;
36
+
37
+ return async () => {
38
+ if (current && remainsUsable(current, Date.now())) return current;
39
+ refreshing ??= provider().then((next) => {
40
+ current = next;
41
+ return next;
42
+ }).finally(() => {
43
+ refreshing = null;
44
+ });
45
+ return refreshing;
46
+ };
47
+ }
48
+
49
+ /** The one credential input a live transport consumes. */
50
+ export interface SessionAccess {
51
+ readonly renewable: boolean;
52
+ credential(): Promise<CredentialProviderResult>;
53
+ }
54
+
55
+ /** Normalize a static or renewable session before it reaches a transport. */
56
+ export function createSessionAccess(
57
+ source: SessionSource | null | undefined,
58
+ credential: () => Promise<CredentialProviderResult>,
59
+ ): SessionAccess {
60
+ return {
61
+ renewable:
62
+ typeof source === 'function'
63
+ || (typeof source === 'object' && source !== null && 'endpoint' in source),
64
+ credential,
65
+ };
66
+ }
67
+
68
+ /** Resolve the public session forms into the credential shape auth consumes. */
69
+ export function resolveSessionCredential(
70
+ source: SessionSource,
71
+ options: {
72
+ readonly fetch?: typeof globalThis.fetch;
73
+ readonly dangerouslyAllowBrowser?: boolean;
74
+ },
75
+ ): string | CredentialProvider {
76
+ if (typeof source === 'function') {
77
+ return protectBrowserCredentialProvider(
78
+ cachedSessionProvider(source),
79
+ options.dangerouslyAllowBrowser,
80
+ );
81
+ }
82
+ if (!('endpoint' in source)) return source.token;
83
+ if (!isCredentialEndpoint(source.endpoint)) {
84
+ throw new AbloValidationError(
85
+ '`session.endpoint` expects a URL or path such as \'/api/ablo-session\'.',
86
+ { code: 'invalid_options', param: 'session.endpoint' },
87
+ );
88
+ }
89
+ return protectBrowserCredentialProvider(createEndpointCredentialResolver(source.endpoint, {
90
+ fetch: options.fetch,
91
+ timeoutMs: source.timeoutMs,
92
+ allowCrossOrigin: source.allowCrossOrigin,
93
+ }), options.dangerouslyAllowBrowser);
94
+ }
@@ -1,4 +1,3 @@
1
1
  /** Shared connection lifecycle boundary. */
2
2
  export * from './manager.js';
3
- export * from './credentialLifecycle.js';
4
3
  export * from './networkProbe.js';
@@ -42,7 +42,7 @@
42
42
 
43
43
  import { probeNetwork, type ProbeResult } from './networkProbe.js';
44
44
  import type { AuthTokenGetter } from '../../auth/credentialSource.js';
45
- import type { CredentialRefreshOutcome } from './credentialLifecycle.js';
45
+ import type { CredentialRefreshOutcome } from '../../sessions/lifecycle.js';
46
46
  import { noopLogger, type Logger } from '../../logger.js';
47
47
  import {
48
48
  noopSocketObservability,
@@ -0,0 +1,108 @@
1
+ import { normalizeClaimConflict } from '../../claims/conflict.js';
2
+ import type { ClaimQueuedResponse } from '../../claims/contract.js';
3
+ import type { HttpClaimsResource } from '../../client/resources/httpResources.js';
4
+ import {
5
+ emitClaimStatus,
6
+ type ResolvedClaimContentionOptions,
7
+ } from '../../client/resources/modelOperations.js';
8
+ import { AbloClaimedError } from '../../errors.js';
9
+
10
+ const GRANT_POLL_FIRST_MS = 250;
11
+ const GRANT_POLL_INTERVAL_MS = 1_000;
12
+ const ignoreBestEffortClaimReleaseFailure = (): undefined => undefined;
13
+
14
+ function sleep(ms: number, signal?: AbortSignal): Promise<void> {
15
+ return new Promise((resolve) => {
16
+ const done = (): void => {
17
+ clearTimeout(timer);
18
+ signal?.removeEventListener('abort', done);
19
+ resolve();
20
+ };
21
+ const timer = setTimeout(done, ms);
22
+ signal?.addEventListener('abort', done, { once: true });
23
+ });
24
+ }
25
+
26
+ /** Wait for a queued HTTP claim while keeping its server-side slot alive. */
27
+ export async function awaitClaimGrantOverHttp(
28
+ claims: HttpClaimsResource,
29
+ targetLabel: string,
30
+ queued: ClaimQueuedResponse,
31
+ options: ResolvedClaimContentionOptions,
32
+ ): Promise<{ id: string; fenceToken?: number }> {
33
+ const claimId = queued.id;
34
+ const { signal } = options;
35
+ const conflict = normalizeClaimConflict({
36
+ ...(queued.heldBy !== undefined ? { heldBy: queued.heldBy } : {}),
37
+ ...(queued.heldByKind !== undefined ? { heldByKind: queued.heldByKind } : {}),
38
+ ...(queued.heldByClaimId !== undefined ? { heldByClaimId: queued.heldByClaimId } : {}),
39
+ ...(queued.expiresAt !== undefined ? { heldByExpiresAt: queued.expiresAt } : {}),
40
+ ...(queued.heldByClaim !== undefined ? { heldByClaim: queued.heldByClaim } : {}),
41
+ });
42
+ const waitingError = (
43
+ message: string,
44
+ code: 'queue_too_deep' | 'claim_wait_aborted' | 'grant_timeout' | 'claim_lost',
45
+ ): AbloClaimedError => new AbloClaimedError(message, {
46
+ code,
47
+ claims: queued.heldByClaim ? [queued.heldByClaim] : undefined,
48
+ conflict,
49
+ });
50
+ const rejectAndLeave = async (error: AbloClaimedError): Promise<never> => {
51
+ await claims.release({ claimId }).catch(ignoreBestEffortClaimReleaseFailure);
52
+ throw error;
53
+ };
54
+
55
+ emitClaimStatus(options.onStatus, {
56
+ type: 'queued',
57
+ claimId,
58
+ position: queued.position,
59
+ ahead: queued.position + 1,
60
+ });
61
+ if (options.maxDepth !== undefined && queued.position >= options.maxDepth) {
62
+ return rejectAndLeave(waitingError(
63
+ `Claim queue for ${targetLabel} is ${queued.position} deep (max ${options.maxDepth}).`,
64
+ 'queue_too_deep',
65
+ ));
66
+ }
67
+
68
+ const deadline = options.timeoutMs !== undefined ? Date.now() + options.timeoutMs : undefined;
69
+ let delay = GRANT_POLL_FIRST_MS;
70
+ for (;;) {
71
+ if (signal?.aborted) {
72
+ return rejectAndLeave(waitingError(
73
+ `The wait for the claim on ${targetLabel} was aborted before the grant arrived.`,
74
+ 'claim_wait_aborted',
75
+ ));
76
+ }
77
+ if (deadline !== undefined && Date.now() >= deadline) {
78
+ return rejectAndLeave(waitingError(
79
+ `Timed out after ${options.timeoutMs}ms waiting for the queue grant on ${targetLabel}.`,
80
+ 'grant_timeout',
81
+ ));
82
+ }
83
+ await sleep(
84
+ deadline !== undefined ? Math.min(delay, Math.max(0, deadline - Date.now())) : delay,
85
+ signal,
86
+ );
87
+ if (signal?.aborted) {
88
+ return rejectAndLeave(waitingError(
89
+ `The wait for the claim on ${targetLabel} was aborted before the grant arrived.`,
90
+ 'claim_wait_aborted',
91
+ ));
92
+ }
93
+ delay = GRANT_POLL_INTERVAL_MS * (0.85 + Math.random() * 0.3);
94
+ const beat = await claims.heartbeat({ claimId });
95
+ if (beat.status !== 'held') continue;
96
+ const state = await claims.retrieve({ claimId });
97
+ if (state.status !== 'active') {
98
+ return rejectAndLeave(waitingError(
99
+ `Claim lost while queued for ${targetLabel}.`,
100
+ 'claim_lost',
101
+ ));
102
+ }
103
+ emitClaimStatus(options.onStatus, { type: 'granted', claimId, waited: true });
104
+ return state.fenceToken !== undefined
105
+ ? { id: claimId, fenceToken: state.fenceToken }
106
+ : { id: claimId };
107
+ }
108
+ }
@@ -19,9 +19,8 @@
19
19
  */
20
20
  import {
21
21
  createHttpTransport,
22
- type HttpTransport,
23
22
  } from './transport.js';
24
- import { modelWireNames } from '../../auth/capability.js';
23
+ import type { HttpTransport } from './contract.js';
25
24
  import type { HttpClientConfig } from './options.js';
26
25
  import type {
27
26
  CommitResource,
@@ -33,9 +32,6 @@ import type {
33
32
  ModelReadOptions,
34
33
  ModelMutationOptions,
35
34
  ModelList,
36
- CreateSessionParams,
37
- AbloSession,
38
- SessionResource,
39
35
  } from '../../client/resources/httpResources.js';
40
36
  import { collectModelList, modelList } from '../../client/resources/httpResources.js';
41
37
  import { resolveCreateId } from '../../client/resources/modelCreate.js';
@@ -91,8 +87,8 @@ export interface AbloHttpClientOptions<S extends SchemaRecord>
91
87
  * @default 30_000
92
88
  */
93
89
  readonly timeoutMs?: number;
94
- /** Initial area of interest for `transport: 'websocket'`. */
95
- readonly syncGroups?: readonly string[];
90
+ /** Initial groups observed by a session-backed WebSocket client. */
91
+ readonly groups?: readonly string[];
96
92
  /** Application frame names accepted by `subscribe` on the WebSocket transport. */
97
93
  readonly collaborationEvents?: readonly string[];
98
94
  /** Durable resume position for WebSocket observation. */
@@ -123,7 +119,7 @@ export type HttpModelMutationParams<P> = Omit<P, 'reads'> & {
123
119
  * coordinated writes.
124
120
  * It deliberately omits the stateful client's local-only
125
121
  * reads (the `local` namespace) and live subscriptions (`onChange`), which need
126
- * a resident graph and a persistent socket; those are absent from the type, so
122
+ * a local graph and a persistent socket; those are absent from the type, so
127
123
  * reaching for one is a compile error rather than a runtime gap.
128
124
  *
129
125
  * This is also the base the reactive per-model surface is composed from, rather
@@ -199,7 +195,7 @@ export interface HttpModelClient<T, C = T> {
199
195
 
200
196
  /**
201
197
  * The type of the stateless HTTP client: a typed {@link HttpModelClient} per
202
- * schema model, plus `commits`, `dispose`, and the session-mint surface. It
198
+ * schema model, plus `commits` and lifecycle operations. It
203
199
  * exposes only what request/response transport can do, so reaching for a
204
200
  * stateful-only capability — the `local` reads, `onChange`, or the synchronous
205
201
  * `claim.state`/`queue`/`reorder` reads — is a compile error rather than a value
@@ -235,13 +231,6 @@ export type AbloHttpClient<S extends SchemaRecord> = {
235
231
  dispose(): Promise<void>;
236
232
  /** Resolves the bearer credential this client authenticates with, or `null` if none is set. */
237
233
  getAuthToken(): Promise<string | null>;
238
- /**
239
- * Mints a short-lived, scoped session token. Minting is itself a stateless
240
- * request, so it is available here even though the local-cache reads are not.
241
- * Pass `{ user }` to mint an end-user key (`ek_`) or `{ agent, can }` to mint a
242
- * scoped agent key (`rk_`). See {@link CreateSessionParams}.
243
- */
244
- readonly sessions: SessionResource<S>;
245
234
  };
246
235
 
247
236
  /** The same typed resource client, with server-pushed capabilities carried by WebSocket. */
@@ -252,9 +241,9 @@ export type AbloWebSocketClient<S extends SchemaRecord> = AbloHttpClient<S> & {
252
241
  listener: (...args: CoreSyncEventMap[K]) => void,
253
242
  ): () => void;
254
243
  updateSubscription(
255
- syncGroups: readonly string[],
244
+ groups: readonly string[],
256
245
  options?: { timeoutMs?: number },
257
- ): Promise<{ syncGroups: string[] }>;
246
+ ): Promise<{ groups: string[] }>;
258
247
  readonly presence: {
259
248
  update(input?: {
260
249
  readonly status?: 'online' | 'away' | 'offline';
@@ -281,9 +270,10 @@ const PROTOCOL_MEMBERS = new Set<string>([
281
270
  'logs',
282
271
  'identity',
283
272
  'getAuthToken',
284
- 'sessions',
285
273
  ]);
286
274
 
275
+ const ignoreAlreadySurfacedSessionOpeningFailure = (): undefined => undefined;
276
+
287
277
  /** Narrows a bare property name to a transport key so the facade can index it typed. */
288
278
  function isProtocolMember(prop: string): prop is keyof HttpTransport {
289
279
  return PROTOCOL_MEMBERS.has(prop);
@@ -538,23 +528,20 @@ function createHttpModelClient<T, C = T>(
538
528
  }
539
529
 
540
530
  /**
541
- * Builds the stateless, typed HTTP client. Each `client.<model>` resolves to the
542
- * protocol client's model accessor, while `commits`, `dispose`, and the other
543
- * protocol members pass through unchanged. No socket is ever opened; the bearer
544
- * credential is the identity.
531
+ * Builds the typed headless client. An API-key identity uses HTTP. A session
532
+ * identity uses one lazily opened WebSocket by default for commits and live
533
+ * coordination until `dispose()`; point reads and administration remain HTTP.
545
534
  */
546
- /** @internal Constructed only through the public `Ablo({ transport: 'http' })` factory. */
535
+ /** @internal Constructed only through the public `Ablo()` factory. */
547
536
  export function createAbloHttpClient<S extends SchemaRecord>(
548
537
  options: AbloHttpClientOptions<S>,
549
538
  ): AbloHttpClient<S> | AbloWebSocketClient<S> {
550
539
  const { schema, onCommitReceipt, ...rest } = options;
551
- const usesWebSocket = options.transport === 'websocket';
540
+ const usesWebSocket = options.transport === 'websocket'
541
+ || (options.transport === undefined && options.session != null);
552
542
  const readSetContext = createReadSetContext();
553
543
  const transport: HttpTransport = createHttpTransport({
554
544
  ...rest,
555
- // Derived from the schema this client is bound to, never assembled by hand
556
- // — see `auth/capability.ts`.
557
- modelTypenames: modelWireNames(schema.models),
558
545
  onCommitReceipt: (observation) => {
559
546
  recordHttpCommitReceipt(readSetContext, observation);
560
547
  onCommitReceipt?.(observation);
@@ -570,9 +557,8 @@ export function createAbloHttpClient<S extends SchemaRecord>(
570
557
  return receipt;
571
558
  },
572
559
  dispatchClaim: async (input) => (await webSocketSession()).claim(input),
573
- releaseDispatchedClaim: (input) => {
574
- void webSocketSession().then((session) => session.release(input));
575
- },
560
+ releaseDispatchedClaim: async (input) =>
561
+ (await webSocketSession()).release(input),
576
562
  } : {}),
577
563
  });
578
564
  const schemaModels = new Set(Object.keys(schema.models));
@@ -597,8 +583,8 @@ export function createAbloHttpClient<S extends SchemaRecord>(
597
583
  await transport.ready();
598
584
  const session = await createWebSocketSession({
599
585
  baseUrl: rest.baseURL ?? undefined,
600
- getAuthToken: async () => (await transport.getAuthToken()) ?? undefined,
601
- syncGroups: options.syncGroups,
586
+ access: transport.access,
587
+ syncGroups: options.groups,
602
588
  collaborationEvents: options.collaborationEvents,
603
589
  cursorStore: options.cursorStore,
604
590
  cursorKey: options.cursorKey,
@@ -629,7 +615,7 @@ export function createAbloHttpClient<S extends SchemaRecord>(
629
615
  webSocketOpen.abort();
630
616
  const opening = webSocketPromise;
631
617
  if (webSocket) await webSocket.close();
632
- else await opening?.then((session) => session.close()).catch(() => undefined);
618
+ else await opening?.then((session) => session.close()).catch(ignoreAlreadySurfacedSessionOpeningFailure);
633
619
  await transport.dispose();
634
620
  };
635
621
 
@@ -707,8 +693,13 @@ export function createAbloHttpClient<S extends SchemaRecord>(
707
693
  };
708
694
  }
709
695
  if (usesWebSocket && prop === 'updateSubscription') {
710
- return async (groups: readonly string[], subscriptionOptions?: { timeoutMs?: number }) =>
711
- (await webSocketSession()).updateSubscription(groups, subscriptionOptions);
696
+ return async (groups: readonly string[], subscriptionOptions?: { timeoutMs?: number }) => {
697
+ const result = await (await webSocketSession()).updateSubscription(
698
+ groups,
699
+ subscriptionOptions,
700
+ );
701
+ return { groups: result.syncGroups };
702
+ };
712
703
  }
713
704
  if (usesWebSocket && prop === 'presence') {
714
705
  return {
@@ -0,0 +1,71 @@
1
+ /** Private contract owned by the stateless HTTP transport boundary. */
2
+
3
+ import type { HttpClientConfig } from './options.js';
4
+ import type { CommitReceiptWire } from '../../commit/contract.js';
5
+ import type {
6
+ CommitResource,
7
+ HttpClaimsResource,
8
+ HttpLogsResource,
9
+ HttpTransportModel,
10
+ } from '../../client/resources/httpResources.js';
11
+ import type {
12
+ ClaimAcquired,
13
+ ClaimBeginPayload,
14
+ ClaimGranted,
15
+ ClaimQueued,
16
+ } from '../../coordination/schema.js';
17
+ import type { CoordinationObservability } from '../../observability.js';
18
+ import type { EffectiveAuthority } from '../../auth/capability.js';
19
+ import type { SessionAccess } from '../../sessions/index.js';
20
+ import type { CommitFrameOperation } from '../websocket/commitFrames.js';
21
+ import type { HttpReadOnChange } from './subscription.js';
22
+
23
+ /** @internal Private options for the schema-agnostic HTTP protocol transport. */
24
+ export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
25
+ readonly bootstrapBaseUrl?: string | undefined;
26
+ /** Observability forwarded from `Ablo({ observability })`. */
27
+ readonly observability?: CoordinationObservability;
28
+ /** Per-request deadline in milliseconds. Pass `0` to disable it. */
29
+ readonly timeoutMs?: number;
30
+ /** @internal Routes writes through the selected duplex transport. */
31
+ readonly dispatchCommit?: ((input: {
32
+ readonly clientTxId: string;
33
+ readonly operations: readonly CommitFrameOperation[];
34
+ readonly reads?: readonly import('../../coordination/schema.js').ReadDependency[] | null;
35
+ }) => Promise<CommitReceiptWire>) | undefined;
36
+ /** @internal Routes held-claim acquisition through the shared socket. */
37
+ readonly dispatchClaim?: ((input: ClaimBeginPayload & {
38
+ readonly timeoutMs?: number;
39
+ readonly signal?: AbortSignal;
40
+ readonly onQueued?: (event: ClaimQueued) => Error | undefined;
41
+ }) => Promise<ClaimAcquired | ClaimGranted>) | undefined;
42
+ /** @internal Releases a socket-acquired claim on that same session. */
43
+ readonly releaseDispatchedClaim?: ((input: {
44
+ readonly claimId: string;
45
+ readonly entityType: string;
46
+ readonly entityId: string;
47
+ }) => Promise<void>) | undefined;
48
+ };
49
+
50
+ /** @internal Private protocol surface wrapped by `AbloHttpClient`. */
51
+ export interface HttpTransport {
52
+ ready(): Promise<void>;
53
+ waitForFlush(): Promise<void>;
54
+ /** Drains scheduled commits and active requests. */
55
+ dispose(): Promise<void>;
56
+ purge(): Promise<void>;
57
+ /** @internal Live check used only by `context().onChange`. */
58
+ readonly onChange: HttpReadOnChange;
59
+ readonly commits: CommitResource;
60
+ readonly claims: HttpClaimsResource;
61
+ readonly logs: HttpLogsResource;
62
+ /** Server-confirmed authority of the active bearer, populated by `ready()`. */
63
+ readonly identity: EffectiveAuthority | null;
64
+ model<T = Record<string, unknown>, Fields = T>(
65
+ name: string,
66
+ ): HttpTransportModel<T, Fields>;
67
+ /** Resolve the bearer credential used by this transport. */
68
+ getAuthToken(): Promise<string | null>;
69
+ /** @internal One normalized source shared by HTTP bootstrap and live transport. */
70
+ readonly access: SessionAccess;
71
+ }
@@ -1,5 +1,6 @@
1
1
  /** Stateless HTTP transport boundary. */
2
2
  export * from './options.js';
3
+ export * from './contract.js';
3
4
  export * from './client.js';
4
5
  export * from './transport.js';
5
6
  export * from './helpers.js';
@@ -37,7 +37,10 @@ export interface HttpClientConfig<S extends SchemaRecord = SchemaRecord>
37
37
  commitOutbox?: DurableWriteStore;
38
38
  /** Compatibility input and internal child-client identity seam. */
39
39
  commitOutboxScope?: CommitOutboxScope;
40
- /** Which transport carries this client's traffic. */
40
+ /**
41
+ * Which transport carries this client's traffic. Defaults to WebSocket for
42
+ * a scoped `session` and HTTP for an API key or direct auth token.
43
+ */
41
44
  transport?: 'websocket' | 'http' | undefined;
42
45
  /** @internal Exact receipt phases observed while dispatching one HTTP commit. */
43
46
  onCommitReceipt?: (observation: {