@abloatai/transaction 0.59.1 → 0.60.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.
- package/dist/auth/apiKey.d.ts +10 -11
- package/dist/auth/apiKey.d.ts.map +1 -1
- package/dist/auth/apiKey.js +15 -29
- package/dist/auth/apiKey.js.map +1 -1
- package/dist/auth/browserCredentialSafety.js +1 -1
- package/dist/auth/browserCredentialSafety.js.map +1 -1
- package/dist/auth/capabilityLifecycle.d.ts +1 -1
- package/dist/auth/capabilityLifecycle.d.ts.map +1 -1
- package/dist/auth/capabilityLifecycle.js +2 -1
- package/dist/auth/capabilityLifecycle.js.map +1 -1
- package/dist/auth/credentialEndpoint.js +1 -1
- package/dist/auth/credentialEndpoint.js.map +1 -1
- package/dist/auth/credentialResult.d.ts +2 -1
- package/dist/auth/credentialResult.d.ts.map +1 -1
- package/dist/auth/credentialResult.js.map +1 -1
- package/dist/claims/events.d.ts +1 -1
- package/dist/claims/events.d.ts.map +1 -1
- package/dist/client/ablo.d.ts +15 -11
- package/dist/client/ablo.d.ts.map +1 -1
- package/dist/client/ablo.js +5 -14
- package/dist/client/ablo.js.map +1 -1
- package/dist/client/resources/httpResources.d.ts +1 -169
- package/dist/client/resources/httpResources.d.ts.map +1 -1
- package/dist/client/resources/httpResources.js +1 -1
- package/dist/client/resources/httpResources.js.map +1 -1
- package/dist/client/resources/modelOperations.d.ts +0 -9
- package/dist/client/resources/modelOperations.d.ts.map +1 -1
- package/dist/client/surface.d.ts +2 -2
- package/dist/client/surface.d.ts.map +1 -1
- package/dist/client/surface.js +8 -3
- package/dist/client/surface.js.map +1 -1
- package/dist/commit/contract.d.ts +10 -10
- package/dist/commit/recordRuntime.d.ts +3 -2
- package/dist/commit/recordRuntime.d.ts.map +1 -1
- package/dist/commit/recordRuntime.js +1 -0
- package/dist/commit/recordRuntime.js.map +1 -1
- package/dist/coordination/index.d.ts +2 -2
- package/dist/coordination/index.d.ts.map +1 -1
- package/dist/coordination/index.js +3 -4
- package/dist/coordination/index.js.map +1 -1
- package/dist/coordination/schema.d.ts +3 -47
- package/dist/coordination/schema.d.ts.map +1 -1
- package/dist/coordination/schema.js +7 -69
- package/dist/coordination/schema.js.map +1 -1
- package/dist/errorCodes.d.ts +2 -2
- package/dist/errorCodes.d.ts.map +1 -1
- package/dist/errorCodes.js +5 -9
- package/dist/errorCodes.js.map +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +16 -1
- package/dist/errors.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/sessions/client.d.ts +29 -0
- package/dist/sessions/client.d.ts.map +1 -0
- package/dist/sessions/client.js +69 -0
- package/dist/sessions/client.js.map +1 -0
- package/dist/sessions/contract.d.ts +97 -0
- package/dist/sessions/contract.d.ts.map +1 -0
- package/dist/sessions/contract.js +5 -0
- package/dist/sessions/contract.js.map +1 -0
- package/dist/{auth/sessionMint.d.ts → sessions/create.d.ts} +4 -4
- package/dist/sessions/create.d.ts.map +1 -0
- package/dist/{auth/sessionMint.js → sessions/create.js} +11 -10
- package/dist/sessions/create.js.map +1 -0
- package/dist/sessions/handler.d.ts +23 -0
- package/dist/sessions/handler.d.ts.map +1 -0
- package/dist/sessions/handler.js +55 -0
- package/dist/sessions/handler.js.map +1 -0
- package/dist/sessions/index.d.ts +14 -0
- package/dist/sessions/index.d.ts.map +1 -0
- package/dist/sessions/index.js +14 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.d.ts → sessions/lifecycle.d.ts} +6 -6
- package/dist/sessions/lifecycle.d.ts.map +1 -0
- package/dist/{transport/connection/credentialLifecycle.js → sessions/lifecycle.js} +5 -5
- package/dist/sessions/lifecycle.js.map +1 -0
- package/dist/sessions/source.d.ts +24 -0
- package/dist/sessions/source.d.ts.map +1 -0
- package/dist/sessions/source.js +56 -0
- package/dist/sessions/source.js.map +1 -0
- package/dist/transport/connection/index.d.ts +0 -1
- package/dist/transport/connection/index.d.ts.map +1 -1
- package/dist/transport/connection/index.js +0 -1
- package/dist/transport/connection/index.js.map +1 -1
- package/dist/transport/connection/manager.d.ts +1 -1
- package/dist/transport/connection/manager.d.ts.map +1 -1
- package/dist/transport/http/client.d.ts +44 -16
- package/dist/transport/http/client.d.ts.map +1 -1
- package/dist/transport/http/client.js +125 -12
- package/dist/transport/http/client.js.map +1 -1
- package/dist/transport/http/options.d.ts +4 -1
- package/dist/transport/http/options.d.ts.map +1 -1
- package/dist/transport/http/transport.d.ts +25 -10
- package/dist/transport/http/transport.d.ts.map +1 -1
- package/dist/transport/http/transport.js +102 -56
- package/dist/transport/http/transport.js.map +1 -1
- package/dist/transport/websocket/commitFrames.d.ts +5 -5
- package/dist/transport/websocket/commitFrames.d.ts.map +1 -1
- package/dist/transport/websocket/commitFrames.js +1 -0
- package/dist/transport/websocket/commitFrames.js.map +1 -1
- package/dist/transport/websocket/contextSubscription.d.ts +6 -0
- package/dist/transport/websocket/contextSubscription.d.ts.map +1 -0
- package/dist/transport/websocket/contextSubscription.js +45 -0
- package/dist/transport/websocket/contextSubscription.js.map +1 -0
- package/dist/transport/websocket/frameHandlers.d.ts +0 -14
- package/dist/transport/websocket/frameHandlers.d.ts.map +1 -1
- package/dist/transport/websocket/frameHandlers.js +1 -71
- package/dist/transport/websocket/frameHandlers.js.map +1 -1
- package/dist/transport/websocket/session.d.ts +4 -0
- package/dist/transport/websocket/session.d.ts.map +1 -0
- package/dist/transport/websocket/session.js +425 -0
- package/dist/transport/websocket/session.js.map +1 -0
- package/dist/transport/websocket/sessionContract.d.ts +78 -0
- package/dist/transport/websocket/sessionContract.d.ts.map +1 -0
- package/dist/transport/websocket/sessionContract.js +2 -0
- package/dist/transport/websocket/sessionContract.js.map +1 -0
- package/dist/transport/websocket/transport.d.ts +27 -42
- package/dist/transport/websocket/transport.d.ts.map +1 -1
- package/dist/transport/websocket/transport.js +77 -102
- package/dist/transport/websocket/transport.js.map +1 -1
- package/dist/wire/inboundFrames.d.ts +0 -10
- package/dist/wire/inboundFrames.d.ts.map +1 -1
- package/dist/wire/inboundFrames.js +1 -6
- package/dist/wire/inboundFrames.js.map +1 -1
- package/package.json +7 -1
- package/src/auth/apiKey.ts +25 -44
- package/src/auth/browserCredentialSafety.ts +1 -1
- package/src/auth/capabilityLifecycle.ts +3 -2
- package/src/auth/credentialEndpoint.ts +1 -1
- package/src/auth/credentialResult.ts +2 -0
- package/src/claims/events.ts +1 -2
- package/src/client/ablo.ts +23 -13
- package/src/client/resources/httpResources.ts +2 -174
- package/src/client/resources/modelOperations.ts +0 -10
- package/src/client/surface.ts +9 -4
- package/src/commit/recordRuntime.ts +4 -2
- package/src/coordination/index.ts +2 -9
- package/src/coordination/schema.ts +7 -81
- package/src/errorCodes.ts +8 -13
- package/src/errors.ts +18 -1
- package/src/index.ts +1 -0
- package/src/sessions/client.ts +121 -0
- package/src/sessions/contract.ts +116 -0
- package/src/{auth/sessionMint.ts → sessions/create.ts} +13 -12
- package/src/sessions/handler.ts +96 -0
- package/src/sessions/index.ts +13 -0
- package/src/{transport/connection/credentialLifecycle.ts → sessions/lifecycle.ts} +5 -5
- package/src/sessions/source.ts +94 -0
- package/src/transport/connection/index.ts +0 -1
- package/src/transport/connection/manager.ts +1 -1
- package/src/transport/http/client.ts +183 -26
- package/src/transport/http/options.ts +4 -1
- package/src/transport/http/transport.ts +140 -78
- package/src/transport/websocket/commitFrames.ts +5 -5
- package/src/transport/websocket/contextSubscription.ts +55 -0
- package/src/transport/websocket/frameHandlers.ts +0 -102
- package/src/transport/websocket/session.ts +522 -0
- package/src/transport/websocket/sessionContract.ts +106 -0
- package/src/transport/websocket/transport.ts +85 -137
- package/src/wire/inboundFrames.ts +0 -6
- package/dist/auth/sessionMint.d.ts.map +0 -1
- package/dist/auth/sessionMint.js.map +0 -1
- package/dist/transport/connection/credentialLifecycle.d.ts.map +0 -1
- package/dist/transport/connection/credentialLifecycle.js.map +0 -1
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
readProcessEnv,
|
|
29
29
|
resolveApiKey,
|
|
30
30
|
resolveApiKeyValue,
|
|
31
|
+
resolveCredentialValue,
|
|
31
32
|
resolveAuthToken,
|
|
32
33
|
resolveBaseURL,
|
|
33
34
|
resolveBootstrapBaseUrl,
|
|
@@ -86,15 +87,10 @@ import type {
|
|
|
86
87
|
HttpTransportList,
|
|
87
88
|
HttpLogsResource,
|
|
88
89
|
ModelTarget,
|
|
89
|
-
CreateSessionParams,
|
|
90
|
-
AbloSession,
|
|
91
|
-
SessionResource,
|
|
92
90
|
} from '../../client/resources/httpResources.js';
|
|
93
|
-
import {
|
|
94
|
-
import {
|
|
95
|
-
|
|
96
|
-
rotateCapability,
|
|
97
|
-
} from '../../auth/capabilityLifecycle.js';
|
|
91
|
+
import type { CredentialProviderResult } from '../../auth/credentialResult.js';
|
|
92
|
+
import { credentialToken } from '../../auth/credentialResult.js';
|
|
93
|
+
import { createSessionAccess, type SessionAccess } from '../../sessions/index.js';
|
|
98
94
|
import { parseIdentityResolveResponse } from '../../auth/schemas.js';
|
|
99
95
|
import type { EffectiveAuthority } from '../../auth/capability.js';
|
|
100
96
|
|
|
@@ -103,7 +99,6 @@ import {
|
|
|
103
99
|
heldHeartbeatReply,
|
|
104
100
|
parseSuccessfulCommitResponse,
|
|
105
101
|
} from './helpers.js';
|
|
106
|
-
import type { SchemaRecord } from '../../schema/schema.js';
|
|
107
102
|
import type {
|
|
108
103
|
ClaimLookupParams,
|
|
109
104
|
ClaimOptions,
|
|
@@ -124,6 +119,12 @@ import {
|
|
|
124
119
|
} from '../../client/resources/modelOperations.js';
|
|
125
120
|
import type { Duration } from '../../utils/duration.js';
|
|
126
121
|
import { claimDescription, partName } from '../../coordination/schema.js';
|
|
122
|
+
import type {
|
|
123
|
+
ClaimAcquired,
|
|
124
|
+
ClaimBeginPayload,
|
|
125
|
+
ClaimGranted,
|
|
126
|
+
ClaimQueued,
|
|
127
|
+
} from '../../coordination/schema.js';
|
|
127
128
|
import type { BatchFence } from '../../claims/locator.js';
|
|
128
129
|
import {
|
|
129
130
|
subTarget,
|
|
@@ -169,12 +170,11 @@ import {
|
|
|
169
170
|
} from '../../commit/httpRequest.js';
|
|
170
171
|
import { resolveDurableWrites } from '../../commit/durableWrites.js';
|
|
171
172
|
import { createHttpReadOnChange, type HttpReadOnChange } from './subscription.js';
|
|
173
|
+
import type { CommitFrameOperation } from '../websocket/commitFrames.js';
|
|
172
174
|
|
|
173
175
|
/** @internal Private options for the schema-agnostic HTTP protocol transport. */
|
|
174
176
|
export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
|
|
175
177
|
readonly bootstrapBaseUrl?: string | undefined;
|
|
176
|
-
/** Schema-key to wire-typename mapping used only when minting agent sessions. */
|
|
177
|
-
readonly modelTypenames?: Readonly<Record<string, string>> | undefined;
|
|
178
178
|
/**
|
|
179
179
|
* The observability provider forwarded from `Ablo({ observability })`. The HTTP
|
|
180
180
|
* transport emits the same claim and conflict events as the WebSocket transport,
|
|
@@ -191,14 +191,29 @@ export type HttpTransportOptions = Omit<HttpClientConfig, 'schema'> & {
|
|
|
191
191
|
* @default 30_000
|
|
192
192
|
*/
|
|
193
193
|
readonly timeoutMs?: number;
|
|
194
|
+
/** @internal Routes writes through the selected duplex transport. */
|
|
195
|
+
readonly dispatchCommit?: ((input: {
|
|
196
|
+
readonly clientTxId: string;
|
|
197
|
+
readonly operations: readonly CommitFrameOperation[];
|
|
198
|
+
readonly reads?: readonly import('../../coordination/schema.js').ReadDependency[] | null;
|
|
199
|
+
}) => Promise<CommitReceiptWire>) | undefined;
|
|
200
|
+
/** @internal Routes held-claim acquisition through the shared socket. */
|
|
201
|
+
readonly dispatchClaim?: ((input: ClaimBeginPayload & {
|
|
202
|
+
readonly timeoutMs?: number;
|
|
203
|
+
readonly signal?: AbortSignal;
|
|
204
|
+
readonly onQueued?: (event: ClaimQueued) => Error | undefined;
|
|
205
|
+
}) => Promise<ClaimAcquired | ClaimGranted>) | undefined;
|
|
206
|
+
/** @internal Releases a socket-acquired claim on that same session. */
|
|
207
|
+
readonly releaseDispatchedClaim?: ((input: {
|
|
208
|
+
readonly claimId: string;
|
|
209
|
+
readonly entityType: string;
|
|
210
|
+
readonly entityId: string;
|
|
211
|
+
}) => void) | undefined;
|
|
194
212
|
};
|
|
195
213
|
|
|
196
214
|
/** @internal Default per-request deadline for the private HTTP transport. */
|
|
197
215
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
198
216
|
const HTTP_CONFIRMATION_POLL_INTERVAL_MS = 250;
|
|
199
|
-
// NOTE: end-user / agent session minting is `ablo.sessions.create(...)` (typed
|
|
200
|
-
// against the schema, see Ablo.ts `CreateSessionParams`). There is no separate
|
|
201
|
-
// `ephemeralKeys` resource — `sessions` is the one front door for both.
|
|
202
217
|
|
|
203
218
|
/** @internal Private protocol surface wrapped by `AbloHttpClient`. */
|
|
204
219
|
export interface HttpTransport {
|
|
@@ -230,12 +245,8 @@ export interface HttpTransport {
|
|
|
230
245
|
* server with the credential this client already holds — no re-mint.
|
|
231
246
|
*/
|
|
232
247
|
getAuthToken(): Promise<string | null>;
|
|
233
|
-
/**
|
|
234
|
-
|
|
235
|
-
* socket), so it lives on this stateless client too, not only the realtime one.
|
|
236
|
-
* `{ user }` mints an `ek_`; `{ agent, can }` mints an `rk_`.
|
|
237
|
-
*/
|
|
238
|
-
readonly sessions: SessionResource<SchemaRecord>;
|
|
248
|
+
/** @internal One normalized source shared by HTTP bootstrap and live transport. */
|
|
249
|
+
readonly access: SessionAccess;
|
|
239
250
|
}
|
|
240
251
|
|
|
241
252
|
type CommitResponse = CommitReceiptWire;
|
|
@@ -441,6 +452,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
441
452
|
return headers;
|
|
442
453
|
}
|
|
443
454
|
|
|
455
|
+
async function activeCredential(): Promise<CredentialProviderResult> {
|
|
456
|
+
return (await resolveCredentialValue(configuredApiKey)) ?? configuredAuthToken ?? null;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const access = createSessionAccess(options.session, activeCredential);
|
|
460
|
+
|
|
444
461
|
function endpoint(path: string): string {
|
|
445
462
|
const target = new URL(`${apiBaseUrl}${path.startsWith('/') ? path : `/${path}`}`);
|
|
446
463
|
for (const [key, value] of Object.entries(options.defaultQuery ?? {})) {
|
|
@@ -981,11 +998,22 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
981
998
|
path: string;
|
|
982
999
|
body: unknown;
|
|
983
1000
|
wait: CommitWait;
|
|
1001
|
+
operations: readonly CommitFrameOperation[];
|
|
1002
|
+
reads?: readonly import('../../coordination/schema.js').ReadDependency[] | null;
|
|
984
1003
|
},
|
|
985
1004
|
beforeConfirmation?: (response: CommitResponse) => Promise<void>
|
|
986
1005
|
): Promise<CommitResponse> {
|
|
987
1006
|
return runInHttpCommitLane(async () => {
|
|
988
1007
|
await prepare();
|
|
1008
|
+
if (options.dispatchCommit) {
|
|
1009
|
+
const response = await options.dispatchCommit({
|
|
1010
|
+
clientTxId: input.idempotencyKey,
|
|
1011
|
+
operations: input.operations,
|
|
1012
|
+
...(input.reads !== undefined ? { reads: input.reads } : {}),
|
|
1013
|
+
});
|
|
1014
|
+
if (response.status === 'confirmed') await beforeConfirmation?.(response);
|
|
1015
|
+
return response;
|
|
1016
|
+
}
|
|
989
1017
|
// Startup preparation covers replay. Re-draining here makes every later write wait
|
|
990
1018
|
// behind an ambiguous predecessor from this same process.
|
|
991
1019
|
const replayed = await replayHttpCommitOutbox();
|
|
@@ -1296,6 +1324,17 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1296
1324
|
reads: commitOptions.reads,
|
|
1297
1325
|
};
|
|
1298
1326
|
const wait = commitOptions.wait ?? 'confirmed';
|
|
1327
|
+
const frameOperations: CommitFrameOperation[] = operations.map((operation) => ({
|
|
1328
|
+
type: operation.action.toUpperCase(),
|
|
1329
|
+
model: operation.model,
|
|
1330
|
+
id: operation.id ?? null,
|
|
1331
|
+
input: operation.data ?? null,
|
|
1332
|
+
where: operation.where ?? null,
|
|
1333
|
+
transactionId: operation.transactionId ?? null,
|
|
1334
|
+
claimId: operation.claimId ?? null,
|
|
1335
|
+
readAt: operation.readAt ?? null,
|
|
1336
|
+
fenceToken: operation.fenceToken ?? null,
|
|
1337
|
+
}));
|
|
1299
1338
|
let body: CommitResponse;
|
|
1300
1339
|
try {
|
|
1301
1340
|
body = await dispatchHttpCommit({
|
|
@@ -1304,6 +1343,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1304
1343
|
idempotencyKey: clientTxId,
|
|
1305
1344
|
body: requestBody,
|
|
1306
1345
|
wait,
|
|
1346
|
+
operations: frameOperations,
|
|
1347
|
+
reads: commitOptions.reads,
|
|
1307
1348
|
});
|
|
1308
1349
|
} catch (error) {
|
|
1309
1350
|
// Coordination collision over HTTP — surface it to observability on the
|
|
@@ -1529,6 +1570,17 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1529
1570
|
if (action === 'create') requestBody.id = id;
|
|
1530
1571
|
if (data !== undefined) requestBody.data = data;
|
|
1531
1572
|
|
|
1573
|
+
const frameOperation: CommitFrameOperation = {
|
|
1574
|
+
type: action.toUpperCase(),
|
|
1575
|
+
model: modelName,
|
|
1576
|
+
id: action === 'create' ? (id || null) : id,
|
|
1577
|
+
input: data ?? null,
|
|
1578
|
+
transactionId: null,
|
|
1579
|
+
claimId: normalizeClaimId(options?.claimRef) ?? claimHandle?.id ?? null,
|
|
1580
|
+
readAt: readAt ?? null,
|
|
1581
|
+
fenceToken: options?.fenceToken ?? claimHandle?.fenceToken ?? null,
|
|
1582
|
+
};
|
|
1583
|
+
|
|
1532
1584
|
let body: CommitResponse;
|
|
1533
1585
|
try {
|
|
1534
1586
|
body = await dispatchHttpCommit(
|
|
@@ -1541,6 +1593,8 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1541
1593
|
// confirmation. Early queued receipts remain available through the
|
|
1542
1594
|
// lower-level `commits.create` lifecycle surface.
|
|
1543
1595
|
wait: 'confirmed',
|
|
1596
|
+
operations: [frameOperation],
|
|
1597
|
+
reads: options?.reads,
|
|
1544
1598
|
},
|
|
1545
1599
|
beforeConfirmation
|
|
1546
1600
|
);
|
|
@@ -1579,6 +1633,59 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1579
1633
|
params: ClaimParams<Fields>
|
|
1580
1634
|
): Promise<{ id: string; fenceToken?: number }> => {
|
|
1581
1635
|
const contention = resolveClaimContentionOptions(params);
|
|
1636
|
+
if (options.dispatchClaim) {
|
|
1637
|
+
const claimId = createClientTxId();
|
|
1638
|
+
let waited = false;
|
|
1639
|
+
try {
|
|
1640
|
+
const grant = await options.dispatchClaim({
|
|
1641
|
+
claimId,
|
|
1642
|
+
entityType: name,
|
|
1643
|
+
entityId: params.id,
|
|
1644
|
+
...subTarget(params, name),
|
|
1645
|
+
description: claimDescription(params),
|
|
1646
|
+
...(params.ttl !== undefined ? { estimatedMs: toMs(params.ttl) } : {}),
|
|
1647
|
+
queue: contention.wait,
|
|
1648
|
+
...(contention.timeoutMs !== undefined ? { timeoutMs: contention.timeoutMs } : {}),
|
|
1649
|
+
...(contention.signal !== undefined ? { signal: contention.signal } : {}),
|
|
1650
|
+
onQueued: (event) => {
|
|
1651
|
+
waited = true;
|
|
1652
|
+
emitClaimStatus(contention.onStatus, {
|
|
1653
|
+
type: 'queued',
|
|
1654
|
+
claimId: event.claimId,
|
|
1655
|
+
position: event.position,
|
|
1656
|
+
ahead: event.position + 1,
|
|
1657
|
+
});
|
|
1658
|
+
if (
|
|
1659
|
+
contention.maxDepth !== undefined
|
|
1660
|
+
&& event.position >= contention.maxDepth
|
|
1661
|
+
) {
|
|
1662
|
+
return new AbloClaimedError(
|
|
1663
|
+
`Claim queue for ${name}/${params.id} is ${event.position} deep (max ${contention.maxDepth}).`,
|
|
1664
|
+
{ code: 'queue_too_deep' },
|
|
1665
|
+
);
|
|
1666
|
+
}
|
|
1667
|
+
return undefined;
|
|
1668
|
+
},
|
|
1669
|
+
});
|
|
1670
|
+
emitClaimStatus(contention.onStatus, {
|
|
1671
|
+
type: 'granted',
|
|
1672
|
+
claimId: grant.claimId,
|
|
1673
|
+
waited,
|
|
1674
|
+
});
|
|
1675
|
+
return grant.fenceToken !== undefined
|
|
1676
|
+
? { id: grant.claimId, fenceToken: grant.fenceToken }
|
|
1677
|
+
: { id: grant.claimId };
|
|
1678
|
+
} catch (error) {
|
|
1679
|
+
const normalized = error instanceof AbloError
|
|
1680
|
+
? error
|
|
1681
|
+
: new AbloConnectionError(String(error));
|
|
1682
|
+
emitClaimStatus(
|
|
1683
|
+
contention.onStatus,
|
|
1684
|
+
claimAttemptFailure(contention.wait, normalized),
|
|
1685
|
+
);
|
|
1686
|
+
throw error;
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1582
1689
|
// The row is named by the URL, so `target` carries only the narrowing a
|
|
1583
1690
|
// claim adds below it. Sending it is what makes a field-scoped claim
|
|
1584
1691
|
// actually field-scoped: the server's conflict rule reads `path`,
|
|
@@ -1656,10 +1763,17 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1656
1763
|
};
|
|
1657
1764
|
const releaseClaim = (
|
|
1658
1765
|
params: ClaimLookupParams<T> | ClaimParams<Fields> | Claim<T>,
|
|
1659
|
-
|
|
1660
|
-
|
|
1766
|
+
claimId?: string,
|
|
1767
|
+
): Promise<void> => {
|
|
1768
|
+
const entityId = isClaimHandle(params) ? params.target.id : params.id;
|
|
1769
|
+
if (claimId && options.releaseDispatchedClaim) {
|
|
1770
|
+
options.releaseDispatchedClaim({ claimId, entityType: name, entityId });
|
|
1771
|
+
return Promise.resolve();
|
|
1772
|
+
}
|
|
1773
|
+
return requestRaw(claimPath(entityId), {
|
|
1661
1774
|
method: 'DELETE',
|
|
1662
1775
|
}).then(() => undefined);
|
|
1776
|
+
};
|
|
1663
1777
|
|
|
1664
1778
|
// One beat on the held lease. A lapsed lease answers `claim_lost`
|
|
1665
1779
|
// (409), which the wire error mapping surfaces as AbloClaimedError —
|
|
@@ -1763,7 +1877,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1763
1877
|
|
|
1764
1878
|
const release = () => {
|
|
1765
1879
|
stopHeartbeatLoop?.();
|
|
1766
|
-
return releaseClaim(params);
|
|
1880
|
+
return releaseClaim(params, claimId);
|
|
1767
1881
|
};
|
|
1768
1882
|
// The handle handed back is a public claim, so its `meta` is the declared
|
|
1769
1883
|
// shape — the same crossing the two decodes above make, spelled the same
|
|
@@ -1904,7 +2018,7 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
1904
2018
|
claim: undefined,
|
|
1905
2019
|
});
|
|
1906
2020
|
} finally {
|
|
1907
|
-
await releaseClaim({ id }).catch(() => {});
|
|
2021
|
+
await releaseClaim({ id }, claimId).catch(() => {});
|
|
1908
2022
|
}
|
|
1909
2023
|
};
|
|
1910
2024
|
|
|
@@ -2100,64 +2214,12 @@ export function createHttpTransport(options: HttpTransportOptions): HttpTranspor
|
|
|
2100
2214
|
claims,
|
|
2101
2215
|
logs,
|
|
2102
2216
|
model,
|
|
2103
|
-
sessions: {
|
|
2104
|
-
async create(params: CreateSessionParams<SchemaRecord>): Promise<AbloSession> {
|
|
2105
|
-
// Stateless mint: the configured key is the control-plane credential here
|
|
2106
|
-
// (no startup `rk_` exchange runs on this client). It reuses the resolved
|
|
2107
|
-
// base URL and fetch; the shared `mintSession` handles the two server routes.
|
|
2108
|
-
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
2109
|
-
if (!apiKey) {
|
|
2110
|
-
throw new AbloAuthenticationError(
|
|
2111
|
-
'sessions.create requires a secret (sk_) API key — call it from your backend, not the browser.',
|
|
2112
|
-
{ code: 'apikey_missing' }
|
|
2113
|
-
);
|
|
2114
|
-
}
|
|
2115
|
-
// A transport built without a schema has no way to translate `can`'s
|
|
2116
|
-
// schema keys into the type names the server gates on. Minting anyway
|
|
2117
|
-
// would spell every override wrong and surface as
|
|
2118
|
-
// `capability_scope_denied` on the agent's first write, so refuse here
|
|
2119
|
-
// instead of guessing.
|
|
2120
|
-
if (!options.modelTypenames) {
|
|
2121
|
-
throw new AbloValidationError(
|
|
2122
|
-
'sessions.create needs the schema this client is bound to. Construct it ' +
|
|
2123
|
-
"through Ablo({ schema, apiKey, transport: 'http' }) rather than the " +
|
|
2124
|
-
'bare transport.',
|
|
2125
|
-
{ code: 'invalid_options', param: 'schema' },
|
|
2126
|
-
);
|
|
2127
|
-
}
|
|
2128
|
-
return mintSession(params, {
|
|
2129
|
-
apiKey,
|
|
2130
|
-
baseUrl: apiBaseUrl,
|
|
2131
|
-
modelTypenames: options.modelTypenames,
|
|
2132
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
2133
|
-
});
|
|
2134
|
-
},
|
|
2135
|
-
async revoke({ id }) {
|
|
2136
|
-
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
2137
|
-
return revokeCapability({
|
|
2138
|
-
apiKey: apiKey ?? '',
|
|
2139
|
-
baseUrl: apiBaseUrl,
|
|
2140
|
-
id,
|
|
2141
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
2142
|
-
});
|
|
2143
|
-
},
|
|
2144
|
-
async rotate({ id, graceSeconds, ttlSeconds }) {
|
|
2145
|
-
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
2146
|
-
return rotateCapability({
|
|
2147
|
-
apiKey: apiKey ?? '',
|
|
2148
|
-
baseUrl: apiBaseUrl,
|
|
2149
|
-
id,
|
|
2150
|
-
...(graceSeconds !== undefined ? { graceSeconds } : {}),
|
|
2151
|
-
...(ttlSeconds !== undefined ? { ttlSeconds } : {}),
|
|
2152
|
-
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
2153
|
-
});
|
|
2154
|
-
},
|
|
2155
|
-
},
|
|
2156
2217
|
async getAuthToken(): Promise<string | null> {
|
|
2157
2218
|
// Mirror `authHeaders()`: a configured API key wins, else the
|
|
2158
2219
|
// construction-time auth token. Resolve the (possibly async) key setter.
|
|
2159
|
-
return (await
|
|
2220
|
+
return credentialToken(await activeCredential());
|
|
2160
2221
|
},
|
|
2222
|
+
access,
|
|
2161
2223
|
};
|
|
2162
2224
|
}
|
|
2163
2225
|
|
|
@@ -17,7 +17,6 @@ import type {
|
|
|
17
17
|
ClaimQueued,
|
|
18
18
|
ClaimLost,
|
|
19
19
|
ClaimRejection,
|
|
20
|
-
ClaimExpired,
|
|
21
20
|
} from '../../coordination/schema.js';
|
|
22
21
|
import type { ClaimEvent, ClaimCounterparty } from '../../claims/events.js';
|
|
23
22
|
import type { AssertExact } from '../../types/assertExact.js';
|
|
@@ -37,9 +36,10 @@ export type CommitAck = CanonicalCommitAck;
|
|
|
37
36
|
export interface CommitFrameOperation {
|
|
38
37
|
readonly type: string;
|
|
39
38
|
readonly model: string;
|
|
40
|
-
readonly id: string;
|
|
41
|
-
readonly input?: Record<string, unknown
|
|
42
|
-
readonly
|
|
39
|
+
readonly id: string | null;
|
|
40
|
+
readonly input?: Record<string, unknown> | null;
|
|
41
|
+
readonly where?: Record<string, unknown> | null;
|
|
42
|
+
readonly transactionId?: string | null;
|
|
43
43
|
readonly claimId?: string | null;
|
|
44
44
|
readonly readAt?: number | null;
|
|
45
45
|
readonly fenceToken?: number | null;
|
|
@@ -65,6 +65,7 @@ export function buildCommitFrame(
|
|
|
65
65
|
model: op.model,
|
|
66
66
|
id: op.id,
|
|
67
67
|
input: op.input,
|
|
68
|
+
where: op.where,
|
|
68
69
|
transactionId: op.transactionId,
|
|
69
70
|
claimId: op.claimId,
|
|
70
71
|
readAt: op.readAt,
|
|
@@ -103,7 +104,6 @@ interface ClaimFrameByPhase {
|
|
|
103
104
|
queued: ClaimQueued;
|
|
104
105
|
lost: ClaimLost;
|
|
105
106
|
rejected: ClaimRejection;
|
|
106
|
-
expired: ClaimExpired;
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
// The phases this map covers and the phases a ClaimEvent can report are the
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ReadDependency } from '../../coordination/schema.js';
|
|
2
|
+
import { AbloStaleContextError } from '../../errors.js';
|
|
3
|
+
import type { AbloWebSocketSession } from './sessionContract.js';
|
|
4
|
+
|
|
5
|
+
/** Watch captured reads over the client's already-open WebSocket. */
|
|
6
|
+
export function subscribeWebSocketReadChanges(
|
|
7
|
+
session: AbloWebSocketSession,
|
|
8
|
+
reads: readonly ReadDependency[],
|
|
9
|
+
listener: (error: AbloStaleContextError) => void,
|
|
10
|
+
): () => void {
|
|
11
|
+
if (reads.length === 0) return () => undefined;
|
|
12
|
+
const watchedRows = new Map(
|
|
13
|
+
reads
|
|
14
|
+
.filter((read): read is Extract<ReadDependency, { model: string }> => 'model' in read)
|
|
15
|
+
.map((read) => [`${read.model.toLowerCase()}:${read.id}`, read.readAt]),
|
|
16
|
+
);
|
|
17
|
+
const watchedGroups = new Map<string, number>(
|
|
18
|
+
reads
|
|
19
|
+
.filter((read): read is Extract<ReadDependency, { group: string }> => 'group' in read)
|
|
20
|
+
.map((read) => [read.group, read.readAt]),
|
|
21
|
+
);
|
|
22
|
+
let stopped = false;
|
|
23
|
+
let unsubscribe = (): void => undefined;
|
|
24
|
+
unsubscribe = session.subscribe('delta', (delta) => {
|
|
25
|
+
if (stopped) return;
|
|
26
|
+
const readAt = watchedRows.get(`${delta.modelName.toLowerCase()}:${delta.modelId}`)
|
|
27
|
+
?? delta.syncGroups.reduce<number | undefined>((latest, group) => {
|
|
28
|
+
const groupReadAt = watchedGroups.get(group);
|
|
29
|
+
return groupReadAt === undefined
|
|
30
|
+
? latest
|
|
31
|
+
: Math.max(latest ?? groupReadAt, groupReadAt);
|
|
32
|
+
}, undefined);
|
|
33
|
+
if (readAt === undefined || delta.id <= readAt) return;
|
|
34
|
+
stopped = true;
|
|
35
|
+
unsubscribe();
|
|
36
|
+
listener(new AbloStaleContextError(
|
|
37
|
+
`Captured context changed: ${delta.modelName}/${delta.modelId}.`,
|
|
38
|
+
{
|
|
39
|
+
code: 'stale_context',
|
|
40
|
+
details: {
|
|
41
|
+
conflicts: [{
|
|
42
|
+
model: delta.modelName,
|
|
43
|
+
id: delta.modelId,
|
|
44
|
+
readAt,
|
|
45
|
+
current: delta.id,
|
|
46
|
+
}],
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
));
|
|
50
|
+
});
|
|
51
|
+
return () => {
|
|
52
|
+
stopped = true;
|
|
53
|
+
unsubscribe();
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -45,16 +45,6 @@ export interface PendingCommit {
|
|
|
45
45
|
readonly returnReceipt?: boolean;
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
/**
|
|
49
|
-
* In-flight `claim` request record, keyed by claimId. Resolved when the
|
|
50
|
-
* matching `claim_ack` arrives, or rejected on timeout/disconnect.
|
|
51
|
-
*/
|
|
52
|
-
export interface PendingClaim {
|
|
53
|
-
resolve: (value: { syncGroups: string[]; ttlSeconds?: number }) => void;
|
|
54
|
-
reject: (err: Error) => void;
|
|
55
|
-
timeout: ReturnType<typeof setTimeout>;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
48
|
/**
|
|
59
49
|
* An in-flight `update_subscription` request awaiting its
|
|
60
50
|
* `subscription_ack`. The wire carries no correlation id, so requests are
|
|
@@ -109,8 +99,6 @@ export interface WsSession {
|
|
|
109
99
|
observability: SocketObservability;
|
|
110
100
|
/** In-flight commit acks keyed by clientTxId. */
|
|
111
101
|
pendingMutations: Map<string, PendingCommit>;
|
|
112
|
-
/** In-flight claim acks keyed by claimId. */
|
|
113
|
-
pendingClaims: Map<string, PendingClaim>;
|
|
114
102
|
/** Removes and returns the oldest in-flight `update_subscription` request. */
|
|
115
103
|
shiftPendingSubscription(): PendingSubscription | undefined;
|
|
116
104
|
/** Connection options subset the handlers write back (acked sync groups). */
|
|
@@ -290,84 +278,6 @@ const handleMutationResult: WsFrameHandler = (session, message) => {
|
|
|
290
278
|
}
|
|
291
279
|
};
|
|
292
280
|
|
|
293
|
-
/**
|
|
294
|
-
* Handles the acknowledgement of a `claim` request. The frame has the shape
|
|
295
|
-
* `{ type: 'claim_ack', payload: { claimId, success, syncGroups?,
|
|
296
|
-
* ttlSeconds?, error? } }`.
|
|
297
|
-
*/
|
|
298
|
-
const handleClaimAck: WsFrameHandler = (session, message) => {
|
|
299
|
-
const p = (message.payload ?? {}) as Record<string, unknown>;
|
|
300
|
-
const { claimId, success, syncGroups, ttlSeconds, error } = p;
|
|
301
|
-
const pending =
|
|
302
|
-
typeof claimId === 'string'
|
|
303
|
-
? session.pendingClaims.get(claimId)
|
|
304
|
-
: undefined;
|
|
305
|
-
if (!pending) return;
|
|
306
|
-
clearTimeout(pending.timeout);
|
|
307
|
-
// `pending` exists ⇒ claimId was a string key (the guard above).
|
|
308
|
-
session.pendingClaims.delete(claimId as string);
|
|
309
|
-
if (success) {
|
|
310
|
-
pending.resolve({
|
|
311
|
-
syncGroups: Array.isArray(syncGroups) ? syncGroups : [],
|
|
312
|
-
ttlSeconds: typeof ttlSeconds === 'number' ? ttlSeconds : undefined,
|
|
313
|
-
});
|
|
314
|
-
} else {
|
|
315
|
-
const err = error as
|
|
316
|
-
| {
|
|
317
|
-
code?: unknown;
|
|
318
|
-
message?: unknown;
|
|
319
|
-
request_id?: unknown;
|
|
320
|
-
event_id?: unknown;
|
|
321
|
-
}
|
|
322
|
-
| undefined;
|
|
323
|
-
const code =
|
|
324
|
-
err?.code && typeof err.code === 'string'
|
|
325
|
-
? err.code
|
|
326
|
-
: 'claim_rejected';
|
|
327
|
-
const msg =
|
|
328
|
-
err?.message && typeof err.message === 'string'
|
|
329
|
-
? err.message
|
|
330
|
-
: 'claim rejected by server';
|
|
331
|
-
const requestId =
|
|
332
|
-
typeof err?.request_id === 'string' ? err.request_id : undefined;
|
|
333
|
-
const eventId =
|
|
334
|
-
typeof err?.event_id === 'string' ? err.event_id : undefined;
|
|
335
|
-
const details = eventId === undefined ? undefined : { event_id: eventId };
|
|
336
|
-
// Capability denials get the typed CapabilityError so
|
|
337
|
-
// callers can read `.requiredCapability` and attenuate-
|
|
338
|
-
// and-retry the claim with a narrower token.
|
|
339
|
-
if (
|
|
340
|
-
code === 'capability_scope_denied' ||
|
|
341
|
-
code === 'capability_invalid'
|
|
342
|
-
) {
|
|
343
|
-
const rc = (error as { requiredCapability?: unknown } | undefined)
|
|
344
|
-
?.requiredCapability;
|
|
345
|
-
const requiredCapability =
|
|
346
|
-
rc != null &&
|
|
347
|
-
typeof rc === 'object' &&
|
|
348
|
-
typeof (rc as { scope?: unknown }).scope === 'string'
|
|
349
|
-
? (rc as RequiredCapability)
|
|
350
|
-
: undefined;
|
|
351
|
-
pending.reject(
|
|
352
|
-
new CapabilityError(code, msg, requiredCapability, {
|
|
353
|
-
...(requestId !== undefined ? { requestId } : {}),
|
|
354
|
-
...(details !== undefined ? { details } : {}),
|
|
355
|
-
}),
|
|
356
|
-
);
|
|
357
|
-
} else {
|
|
358
|
-
// Route through the shared factory so a failed claim_ack is a
|
|
359
|
-
// typed AbloError (registry code → right subclass), symmetric
|
|
360
|
-
// with the commit `mutation_result` path — never a bare Error.
|
|
361
|
-
pending.reject(
|
|
362
|
-
errorFromWire(msg, {
|
|
363
|
-
code,
|
|
364
|
-
requestId,
|
|
365
|
-
details,
|
|
366
|
-
}),
|
|
367
|
-
);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
};
|
|
371
281
|
|
|
372
282
|
/**
|
|
373
283
|
* Handles the acknowledgement of an `update_subscription` request. The wire
|
|
@@ -448,7 +358,6 @@ export const wsFrameHandlers: Record<string, WsFrameHandler> = {
|
|
|
448
358
|
bootstrap_response: (session, message) => { session.handleBootstrapResponse(message.payload); },
|
|
449
359
|
presence_update: (session, message) => { session.handlePresenceUpdate(message); },
|
|
450
360
|
mutation_result: handleMutationResult,
|
|
451
|
-
claim_ack: handleClaimAck,
|
|
452
361
|
subscription_ack: handleSubscriptionAck,
|
|
453
362
|
delta: handleDeltaFrame,
|
|
454
363
|
};
|
|
@@ -497,17 +406,6 @@ const validatedFrameHandlers: Record<
|
|
|
497
406
|
session.handlePresenceUpdate({ payload });
|
|
498
407
|
},
|
|
499
408
|
),
|
|
500
|
-
claim_expired: validating(
|
|
501
|
-
WS_INBOUND_FRAMES.claim_expired.payload,
|
|
502
|
-
'claim_expired',
|
|
503
|
-
(session, payload) => {
|
|
504
|
-
// Server-initiated expiry. The claim is already inactive by the time
|
|
505
|
-
// this arrives, so a consumer either re-claims with a fresh credential
|
|
506
|
-
// or accepts the drop.
|
|
507
|
-
recordClaim(tracePorts(session), 'expired', payload);
|
|
508
|
-
session.emit('claim_expired', payload);
|
|
509
|
-
},
|
|
510
|
-
),
|
|
511
409
|
claim_rejected: validating(
|
|
512
410
|
WS_INBOUND_FRAMES.claim_rejected.payload,
|
|
513
411
|
'claim_rejected',
|