@abloatai/ablo 0.28.0 → 0.29.1
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/CHANGELOG.md +36 -0
- package/dist/Database.js +9 -6
- package/dist/SyncClient.d.ts +2 -2
- package/dist/SyncClient.js +22 -10
- package/dist/adapters/inMemoryStorage.js +18 -14
- package/dist/ai-sdk/coordinatedTool.d.ts +15 -2
- package/dist/auth/schemas.d.ts +6 -0
- package/dist/auth/schemas.js +7 -0
- package/dist/cli.cjs +815 -400
- package/dist/client/Ablo.d.ts +29 -23
- package/dist/client/Ablo.js +8 -196
- package/dist/client/claimHeartbeatLoop.d.ts +1 -1
- package/dist/client/claimHeartbeatLoop.js +1 -1
- package/dist/client/createInternalComponents.d.ts +4 -2
- package/dist/client/createInternalComponents.js +3 -1
- package/dist/client/durableWrites.d.ts +21 -0
- package/dist/client/durableWrites.js +46 -0
- package/dist/client/httpClient.d.ts +19 -45
- package/dist/client/httpClient.js +104 -26
- package/dist/client/httpTransport.d.ts +8 -0
- package/dist/client/{ApiClient.js → httpTransport.js} +78 -291
- package/dist/client/options.d.ts +43 -19
- package/dist/client/options.js +3 -2
- package/dist/client/resourceTypes.d.ts +9 -85
- package/dist/client/resourceTypes.js +1 -1
- package/dist/client/sessionMint.d.ts +5 -6
- package/dist/client/sessionMint.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/coordination/schema.d.ts +30 -0
- package/dist/coordination/schema.js +14 -0
- package/dist/core/index.d.ts +1 -1
- package/dist/errors.d.ts +4 -40
- package/dist/errors.js +5 -5
- package/dist/index.d.ts +14 -8
- package/dist/index.js +7 -5
- package/dist/interfaces/index.d.ts +3 -0
- package/dist/mutators/defineMutators.d.ts +18 -0
- package/dist/mutators/defineMutators.js +4 -8
- package/dist/react/index.d.ts +1 -1
- package/dist/schema/index.d.ts +1 -1
- package/dist/schema/index.js +1 -1
- package/dist/schema/select.d.ts +15 -0
- package/dist/schema/select.js +27 -3
- package/dist/source/connector.d.ts +3 -3
- package/dist/source/factory.d.ts +4 -6
- package/dist/source/factory.js +4 -7
- package/dist/source/index.d.ts +4 -4
- package/dist/source/index.js +2 -2
- package/dist/source/signing.d.ts +0 -3
- package/dist/source/types.d.ts +0 -26
- package/dist/stores/ObjectStore.js +8 -8
- package/dist/surface.d.ts +1 -1
- package/dist/surface.js +1 -0
- package/dist/sync/BootstrapFetcher.d.ts +10 -0
- package/dist/sync/BootstrapFetcher.js +27 -4
- package/dist/sync/createClaimStream.d.ts +11 -2
- package/dist/sync/createClaimStream.js +4 -3
- package/dist/testing/mocks/FakeDatabase.d.ts +1 -1
- package/dist/transactions/TransactionQueue.d.ts +2 -2
- package/dist/transactions/TransactionQueue.js +3 -3
- package/dist/transactions/commitEnvelope.d.ts +1 -1
- package/dist/transactions/commitEnvelope.js +2 -2
- package/dist/transactions/commitOutboxStore.d.ts +19 -19
- package/dist/transactions/durableWriteStore.d.ts +123 -0
- package/dist/transactions/durableWriteStore.js +30 -0
- package/dist/transactions/httpCommitEnvelope.d.ts +1 -0
- package/dist/transactions/httpCommitEnvelope.js +14 -12
- package/dist/transactions/idempotencyKey.d.ts +10 -0
- package/dist/transactions/idempotencyKey.js +9 -0
- package/dist/types/global.d.ts +10 -29
- package/dist/types/global.js +4 -7
- package/dist/types/streams.d.ts +2 -28
- package/dist/wire/frames.d.ts +6 -257
- package/dist/wire/frames.js +4 -25
- package/dist/wire/index.d.ts +4 -3
- package/dist/wire/index.js +2 -2
- package/dist/wire/protocolVersion.d.ts +30 -17
- package/dist/wire/protocolVersion.js +34 -18
- package/docs/agents.md +8 -3
- package/docs/api.md +6 -4
- package/docs/client-behavior.md +6 -3
- package/docs/coordination.md +2 -3
- package/docs/data-sources.md +5 -8
- package/docs/guarantees.md +21 -0
- package/docs/integration-guide.md +1 -0
- package/docs/migration.md +21 -1
- package/docs/quickstart.md +11 -0
- package/docs/react.md +0 -46
- package/examples/README.md +6 -5
- package/llms.txt +15 -15
- package/package.json +2 -7
- package/dist/client/ApiClient.d.ts +0 -177
- package/dist/commit/contract.d.ts +0 -493
- package/dist/commit/contract.js +0 -187
- package/dist/commit/index.d.ts +0 -6
- package/dist/commit/index.js +0 -5
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* object pool, local database, or WebSocket
|
|
4
|
-
* and Commit
|
|
5
|
-
*
|
|
2
|
+
* Private HTTP protocol client behind `Ablo({ schema, transport: 'http' })`.
|
|
3
|
+
* It carries no object pool, local database, or WebSocket and maps Model,
|
|
4
|
+
* Claim, and Commit protocol shapes directly to server routes. The typed
|
|
5
|
+
* facade in `httpClient.ts` is the application boundary; this module owns
|
|
6
|
+
* transport envelopes, watermarks, replay, and route details.
|
|
6
7
|
*/
|
|
7
8
|
import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloIdempotencyError, AbloValidationError, AbloNotFoundError, claimedError, translateHttpError, } from '../errors.js';
|
|
8
9
|
import { z } from 'zod';
|
|
@@ -11,10 +12,11 @@ import { reconcileFunctionalUpdate, } from './functionalUpdate.js';
|
|
|
11
12
|
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, warnIfCliKeyMismatch, warnIfDatabaseUrlEnvIgnored, warnIfDatabaseUrlDeprecated, } from './auth.js';
|
|
12
13
|
import { registerDataSource } from './registerDataSource.js';
|
|
13
14
|
import { PROTOCOL_VERSION, PROTOCOL_VERSION_HEADER } from '../wire/protocolVersion.js';
|
|
14
|
-
import { toMs
|
|
15
|
+
import { toMs } from '../utils/duration.js';
|
|
15
16
|
import { heartbeatCadenceMs, resolveHeartbeatOptions, startClaimHeartbeatLoop, } from './claimHeartbeatLoop.js';
|
|
16
17
|
import { mintSession } from './sessionMint.js';
|
|
17
18
|
import { parseIdentityResolveResponse } from '../auth/schemas.js';
|
|
19
|
+
import { staleNotificationSchema } from '../coordination/schema.js';
|
|
18
20
|
/**
|
|
19
21
|
* Interpret a heartbeat reply for a lease this handle HOLDS: anything other
|
|
20
22
|
* than `held` means the lease is no longer ours (a holder cannot be `queued`;
|
|
@@ -33,7 +35,8 @@ function heldHeartbeatReply(reply, label) {
|
|
|
33
35
|
}
|
|
34
36
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
35
37
|
import { createDurableHttpCommitEnvelope, canonicalHttpCommitBody, durableHttpCommitEnvelopeSchema, httpCommitEnvelopeRecordId, isHttpCommitReplayExpired, } from '../transactions/httpCommitEnvelope.js';
|
|
36
|
-
|
|
38
|
+
import { resolveDurableWrites } from './durableWrites.js';
|
|
39
|
+
/** @internal Default per-request deadline for the private HTTP transport. */
|
|
37
40
|
export const DEFAULT_REQUEST_TIMEOUT_MS = 30_000;
|
|
38
41
|
const successfulCommitResponseSchema = z
|
|
39
42
|
.object({
|
|
@@ -45,6 +48,7 @@ const successfulCommitResponseSchema = z
|
|
|
45
48
|
success: z.literal(true),
|
|
46
49
|
lastSyncId: z.number().int().nonnegative().optional(),
|
|
47
50
|
ops: z.number().int().positive(),
|
|
51
|
+
notifications: z.array(staleNotificationSchema).optional(),
|
|
48
52
|
/** Ids of UPDATE/DELETE targets that matched zero rows (loud 0-row writes). */
|
|
49
53
|
missingIds: z.array(z.string().min(1)).optional(),
|
|
50
54
|
})
|
|
@@ -61,8 +65,30 @@ function parseSuccessfulCommitResponse(value, idempotencyKey) {
|
|
|
61
65
|
}
|
|
62
66
|
return parsed.data;
|
|
63
67
|
}
|
|
64
|
-
|
|
65
|
-
|
|
68
|
+
/** Decode the HTTP claim DTO into the one public Claim shape. */
|
|
69
|
+
function claimFromModelClaim(claim) {
|
|
70
|
+
return {
|
|
71
|
+
object: 'claim',
|
|
72
|
+
id: claim.id,
|
|
73
|
+
...(claim.status ? { status: claim.status } : {}),
|
|
74
|
+
reason: claim.reason,
|
|
75
|
+
...(claim.description ? { description: claim.description } : {}),
|
|
76
|
+
heldBy: claim.actor,
|
|
77
|
+
participantKind: claim.participantKind,
|
|
78
|
+
expiresAt: claim.expiresAt,
|
|
79
|
+
...(claim.position !== undefined ? { position: claim.position } : {}),
|
|
80
|
+
target: {
|
|
81
|
+
type: claim.target.model,
|
|
82
|
+
id: claim.target.id,
|
|
83
|
+
...(claim.target.path ? { path: claim.target.path } : {}),
|
|
84
|
+
...(claim.target.range ? { range: claim.target.range } : {}),
|
|
85
|
+
...(claim.target.field ? { field: claim.target.field } : {}),
|
|
86
|
+
...(claim.target.meta ? { meta: claim.target.meta } : {}),
|
|
87
|
+
},
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** @internal Constructed only by the typed HTTP facade. */
|
|
91
|
+
export function createHttpTransport(options) {
|
|
66
92
|
const env = readProcessEnv();
|
|
67
93
|
const authInput = { options, env };
|
|
68
94
|
const configuredApiKey = resolveApiKey(authInput);
|
|
@@ -123,7 +149,12 @@ export function createProtocolClient(options) {
|
|
|
123
149
|
url,
|
|
124
150
|
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
125
151
|
}).replace(/\/+$/, '');
|
|
126
|
-
const
|
|
152
|
+
const durableWrites = resolveDurableWrites(options);
|
|
153
|
+
// Internal replay code retains transactional-outbox terminology. The public
|
|
154
|
+
// constructor exposes the behavior as `durableWrites`.
|
|
155
|
+
const commitOutbox = durableWrites.store;
|
|
156
|
+
const durableWriteNamespace = durableWrites.namespace ?? 'http';
|
|
157
|
+
const legacyCommitOutboxScope = options.commitOutboxScope;
|
|
127
158
|
const httpOutboxPlaneNamespace = canonicalHttpCommitBody({
|
|
128
159
|
apiBaseUrl,
|
|
129
160
|
defaultQuery: Object.entries(options.defaultQuery ?? {}).sort(([a], [b]) => a.localeCompare(b)),
|
|
@@ -141,14 +172,19 @@ export function createProtocolClient(options) {
|
|
|
141
172
|
return null;
|
|
142
173
|
if (httpOutboxScopeNamespace)
|
|
143
174
|
return httpOutboxScopeNamespace;
|
|
144
|
-
let scope =
|
|
175
|
+
let scope = legacyCommitOutboxScope
|
|
176
|
+
? {
|
|
177
|
+
...legacyCommitOutboxScope,
|
|
178
|
+
namespace: durableWriteNamespace,
|
|
179
|
+
}
|
|
180
|
+
: undefined;
|
|
145
181
|
if (!scope) {
|
|
146
182
|
const rawIdentity = await requestJson('/auth/identity', { method: 'GET' }, true);
|
|
147
183
|
const identity = parseIdentityResolveResponse(rawIdentity);
|
|
148
184
|
scope = {
|
|
149
185
|
organizationId: identity.accountScope,
|
|
150
186
|
participantId: identity.participantId,
|
|
151
|
-
namespace:
|
|
187
|
+
namespace: durableWriteNamespace,
|
|
152
188
|
};
|
|
153
189
|
}
|
|
154
190
|
httpOutboxScopeNamespace = canonicalHttpCommitBody({
|
|
@@ -180,11 +216,11 @@ export function createProtocolClient(options) {
|
|
|
180
216
|
throw error;
|
|
181
217
|
}
|
|
182
218
|
}
|
|
183
|
-
async function authHeaders() {
|
|
219
|
+
async function authHeaders(sealedProtocolVersion) {
|
|
184
220
|
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
185
221
|
const token = apiKey ?? configuredAuthToken;
|
|
186
222
|
if (!token) {
|
|
187
|
-
throw new AbloAuthenticationError('
|
|
223
|
+
throw new AbloAuthenticationError('The HTTP client requires an API key. Pass `apiKey` or set ABLO_API_KEY.', { code: 'api_key_required' });
|
|
188
224
|
}
|
|
189
225
|
const headers = {
|
|
190
226
|
'Content-Type': 'application/json',
|
|
@@ -201,6 +237,12 @@ export function createProtocolClient(options) {
|
|
|
201
237
|
headers[key] = value;
|
|
202
238
|
}
|
|
203
239
|
}
|
|
240
|
+
// A durable write owns its wire version. Force the sealed value after
|
|
241
|
+
// caller defaults so a restarted (or rolled-back) SDK cannot rewrite the
|
|
242
|
+
// protocol identity of a request that may already have reached the server.
|
|
243
|
+
if (sealedProtocolVersion !== undefined) {
|
|
244
|
+
headers[PROTOCOL_VERSION_HEADER] = String(sealedProtocolVersion);
|
|
245
|
+
}
|
|
204
246
|
return headers;
|
|
205
247
|
}
|
|
206
248
|
function endpoint(path) {
|
|
@@ -215,8 +257,8 @@ export function createProtocolClient(options) {
|
|
|
215
257
|
async function requestJson(path, init, skipReady = false) {
|
|
216
258
|
if (!skipReady)
|
|
217
259
|
await ready();
|
|
218
|
-
const { idempotencyKey, ...requestInit } = init;
|
|
219
|
-
const headers = await authHeaders();
|
|
260
|
+
const { idempotencyKey, sealedProtocolVersion, ...requestInit } = init;
|
|
261
|
+
const headers = await authHeaders(sealedProtocolVersion);
|
|
220
262
|
if (idempotencyKey)
|
|
221
263
|
headers['Idempotency-Key'] = idempotencyKey;
|
|
222
264
|
// Deadline: abort the request after `timeoutMs` so a black-holed server
|
|
@@ -340,6 +382,7 @@ export function createProtocolClient(options) {
|
|
|
340
382
|
const raw = await requestJson(envelope.request.path, {
|
|
341
383
|
method: envelope.request.method,
|
|
342
384
|
idempotencyKey: envelope.idempotencyKey,
|
|
385
|
+
sealedProtocolVersion: envelope.protocolVersion,
|
|
343
386
|
body: envelope.request.body,
|
|
344
387
|
}, true);
|
|
345
388
|
const response = parseSuccessfulCommitResponse(raw, envelope.idempotencyKey);
|
|
@@ -366,7 +409,7 @@ export function createProtocolClient(options) {
|
|
|
366
409
|
return null;
|
|
367
410
|
const scopeNamespace = await resolveHttpOutboxScope();
|
|
368
411
|
if (!scopeNamespace) {
|
|
369
|
-
throw new AbloValidationError('HTTP
|
|
412
|
+
throw new AbloValidationError('HTTP durable-write scope was not resolved', {
|
|
370
413
|
code: 'write_options_invalid',
|
|
371
414
|
});
|
|
372
415
|
}
|
|
@@ -424,6 +467,9 @@ export function createProtocolClient(options) {
|
|
|
424
467
|
const raw = await requestJson(input.path, {
|
|
425
468
|
method: input.method,
|
|
426
469
|
idempotencyKey: input.idempotencyKey,
|
|
470
|
+
...(durableEnvelope
|
|
471
|
+
? { sealedProtocolVersion: durableEnvelope.protocolVersion }
|
|
472
|
+
: {}),
|
|
427
473
|
body: requestBody,
|
|
428
474
|
}, true);
|
|
429
475
|
response = parseSuccessfulCommitResponse(raw, input.idempotencyKey);
|
|
@@ -462,32 +508,11 @@ export function createProtocolClient(options) {
|
|
|
462
508
|
? crypto.randomUUID()
|
|
463
509
|
: `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
464
510
|
}
|
|
465
|
-
function childClient(authToken, scope) {
|
|
466
|
-
return createProtocolClient({
|
|
467
|
-
...options,
|
|
468
|
-
apiKey: null,
|
|
469
|
-
authToken,
|
|
470
|
-
schema: null,
|
|
471
|
-
...(scope
|
|
472
|
-
? {
|
|
473
|
-
commitOutboxScope: {
|
|
474
|
-
...scope,
|
|
475
|
-
namespace: options.commitOutboxScope?.namespace ?? 'http',
|
|
476
|
-
},
|
|
477
|
-
}
|
|
478
|
-
: { commitOutboxScope: undefined }),
|
|
479
|
-
});
|
|
480
|
-
}
|
|
481
511
|
function normalizeCommitOperation(op, defaults) {
|
|
482
|
-
const model = op.model ?? op.target?.model;
|
|
483
|
-
if (!model) {
|
|
484
|
-
throw new AbloValidationError('Commit operation requires `model` or `target.model`.', { code: 'commit_operation_model_required' });
|
|
485
|
-
}
|
|
486
|
-
const id = op.id ?? op.target?.id ?? null;
|
|
487
512
|
return {
|
|
488
513
|
action: op.action,
|
|
489
|
-
model,
|
|
490
|
-
id,
|
|
514
|
+
model: op.model,
|
|
515
|
+
id: op.id ?? null,
|
|
491
516
|
data: op.data ?? null,
|
|
492
517
|
transactionId: op.transactionId ?? null,
|
|
493
518
|
readAt: op.readAt ?? defaults.readAt ?? null,
|
|
@@ -495,20 +520,10 @@ export function createProtocolClient(options) {
|
|
|
495
520
|
};
|
|
496
521
|
}
|
|
497
522
|
function normalizeCommitOperations(commitOptions) {
|
|
498
|
-
if (commitOptions.
|
|
499
|
-
throw new AbloValidationError('
|
|
500
|
-
}
|
|
501
|
-
const inputOperations = commitOptions.operation
|
|
502
|
-
? [commitOptions.operation]
|
|
503
|
-
: commitOptions.operations ?? [];
|
|
504
|
-
if (inputOperations.length === 0) {
|
|
505
|
-
throw new AbloValidationError('Commit requires at least one operation.', { code: 'commit_operation_required' });
|
|
523
|
+
if (commitOptions.operations.length === 0) {
|
|
524
|
+
throw new AbloValidationError('Commit requires a non-empty `operations` array.', { code: 'commit_operation_required' });
|
|
506
525
|
}
|
|
507
|
-
return
|
|
508
|
-
}
|
|
509
|
-
async function listClaims(target) {
|
|
510
|
-
const state = await listClaimState(target);
|
|
511
|
-
return state.active;
|
|
526
|
+
return commitOptions.operations.map((op) => normalizeCommitOperation(op, commitOptions));
|
|
512
527
|
}
|
|
513
528
|
async function listClaimState(target) {
|
|
514
529
|
const params = new URLSearchParams();
|
|
@@ -525,54 +540,6 @@ export function createProtocolClient(options) {
|
|
|
525
540
|
queue: body.queue ?? [],
|
|
526
541
|
};
|
|
527
542
|
}
|
|
528
|
-
function delay(ms, signal) {
|
|
529
|
-
if (signal?.aborted) {
|
|
530
|
-
return Promise.reject(new AbloConnectionError('Claim wait aborted.', {
|
|
531
|
-
code: 'claim_wait_aborted',
|
|
532
|
-
cause: signal.reason,
|
|
533
|
-
}));
|
|
534
|
-
}
|
|
535
|
-
return new Promise((resolve, reject) => {
|
|
536
|
-
const timeout = setTimeout(done, ms);
|
|
537
|
-
function cleanup() {
|
|
538
|
-
clearTimeout(timeout);
|
|
539
|
-
signal?.removeEventListener('abort', onAbort);
|
|
540
|
-
}
|
|
541
|
-
function done() {
|
|
542
|
-
cleanup();
|
|
543
|
-
resolve();
|
|
544
|
-
}
|
|
545
|
-
function onAbort() {
|
|
546
|
-
cleanup();
|
|
547
|
-
reject(new AbloConnectionError('Claim wait aborted.', {
|
|
548
|
-
code: 'claim_wait_aborted',
|
|
549
|
-
cause: signal?.reason,
|
|
550
|
-
}));
|
|
551
|
-
}
|
|
552
|
-
signal?.addEventListener('abort', onAbort, { once: true });
|
|
553
|
-
});
|
|
554
|
-
}
|
|
555
|
-
async function waitForNoClaims(target, options) {
|
|
556
|
-
const startedAt = Date.now();
|
|
557
|
-
const pollInterval = options?.pollInterval;
|
|
558
|
-
for (;;) {
|
|
559
|
-
const claims = await listClaims(target);
|
|
560
|
-
if (claims.length === 0)
|
|
561
|
-
return;
|
|
562
|
-
if (pollInterval == null) {
|
|
563
|
-
throw new AbloValidationError('Cannot wait for claims over the HTTP client without `pollInterval`. ' +
|
|
564
|
-
'Use the schema client for event-driven claim waits, pass `ifClaimed: "return"`, ' +
|
|
565
|
-
'or provide an explicit poll interval for this runtime.', { code: 'claim_wait_poll_interval_required' });
|
|
566
|
-
}
|
|
567
|
-
if (options?.timeout != null && Date.now() - startedAt >= options.timeout) {
|
|
568
|
-
throw claimedError(target, claims, 'model_claimed_timeout');
|
|
569
|
-
}
|
|
570
|
-
const remaining = options?.timeout == null
|
|
571
|
-
? pollInterval
|
|
572
|
-
: Math.max(0, Math.min(pollInterval, options.timeout - (Date.now() - startedAt)));
|
|
573
|
-
await delay(remaining, options?.signal);
|
|
574
|
-
}
|
|
575
|
-
}
|
|
576
543
|
async function applyClaimedPolicy(target, options, defaultPolicy = 'return') {
|
|
577
544
|
const policy = options?.ifClaimed ?? defaultPolicy;
|
|
578
545
|
if (policy === 'return')
|
|
@@ -603,9 +570,6 @@ export function createProtocolClient(options) {
|
|
|
603
570
|
onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
604
571
|
});
|
|
605
572
|
const requestBody = {
|
|
606
|
-
clientTxId,
|
|
607
|
-
idempotencyKey: clientTxId,
|
|
608
|
-
claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.id,
|
|
609
573
|
operations,
|
|
610
574
|
reads: commitOptions.reads,
|
|
611
575
|
};
|
|
@@ -637,195 +601,15 @@ export function createProtocolClient(options) {
|
|
|
637
601
|
id: body.id ?? body.clientTxId,
|
|
638
602
|
status,
|
|
639
603
|
lastSyncId: body.lastSyncId,
|
|
604
|
+
...(body.notifications && body.notifications.length > 0
|
|
605
|
+
? { notifications: body.notifications }
|
|
606
|
+
: {}),
|
|
640
607
|
...(body.missingIds && body.missingIds.length > 0
|
|
641
608
|
? { missingIds: body.missingIds }
|
|
642
609
|
: {}),
|
|
643
610
|
};
|
|
644
611
|
},
|
|
645
612
|
};
|
|
646
|
-
const capabilities = {
|
|
647
|
-
async create(capabilityOptions) {
|
|
648
|
-
const ttlSeconds = capabilityOptions.ttlSeconds ?? // eslint-disable-line @typescript-eslint/no-deprecated -- compatibility alias
|
|
649
|
-
capabilityOptions.leaseSeconds ??
|
|
650
|
-
toSeconds(capabilityOptions.ttl ?? capabilityOptions.lease ?? DEFAULT_AGENT_LEASE); // eslint-disable-line @typescript-eslint/no-deprecated -- compatibility alias
|
|
651
|
-
const body = await requestJson('/v1/capabilities', {
|
|
652
|
-
method: 'POST',
|
|
653
|
-
body: JSON.stringify({
|
|
654
|
-
participantKind: capabilityOptions.participantKind ?? 'agent',
|
|
655
|
-
participantId: capabilityOptions.participantId,
|
|
656
|
-
syncGroups: capabilityOptions.syncGroups,
|
|
657
|
-
operations: capabilityOptions.operations,
|
|
658
|
-
ttlSeconds,
|
|
659
|
-
label: capabilityOptions.label,
|
|
660
|
-
wideScope: capabilityOptions.wideScope,
|
|
661
|
-
userMeta: capabilityOptions.userMeta,
|
|
662
|
-
}),
|
|
663
|
-
});
|
|
664
|
-
const id = body.capabilityId ?? body.id;
|
|
665
|
-
if (!id) {
|
|
666
|
-
throw new AbloValidationError('Capability create response did not include an id.', { code: 'capability_id_missing' });
|
|
667
|
-
}
|
|
668
|
-
return {
|
|
669
|
-
id,
|
|
670
|
-
token: body.token,
|
|
671
|
-
expiresAt: body.expiresAt,
|
|
672
|
-
organizationId: body.organizationId,
|
|
673
|
-
scope: body.scope,
|
|
674
|
-
userMeta: body.userMeta,
|
|
675
|
-
client: () => childClient(body.token, {
|
|
676
|
-
organizationId: body.organizationId,
|
|
677
|
-
participantId: body.scope.participantId,
|
|
678
|
-
}),
|
|
679
|
-
};
|
|
680
|
-
},
|
|
681
|
-
async retrieve(id) {
|
|
682
|
-
const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}`, { method: 'GET' });
|
|
683
|
-
return {
|
|
684
|
-
id: body.capabilityId ?? body.id ?? id,
|
|
685
|
-
organizationId: body.organizationId,
|
|
686
|
-
participantKind: body.participantKind,
|
|
687
|
-
participantId: body.participantId,
|
|
688
|
-
label: body.label,
|
|
689
|
-
status: body.status,
|
|
690
|
-
issuedAt: body.issuedAt,
|
|
691
|
-
expiresAt: body.expiresAt,
|
|
692
|
-
revokedAt: body.revokedAt,
|
|
693
|
-
lastUsedAt: body.lastUsedAt,
|
|
694
|
-
operations: body.operations ?? [],
|
|
695
|
-
syncGroups: body.syncGroups ?? [],
|
|
696
|
-
};
|
|
697
|
-
},
|
|
698
|
-
async revoke(id) {
|
|
699
|
-
const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
700
|
-
return {
|
|
701
|
-
id: body.capabilityId ?? body.id ?? id,
|
|
702
|
-
deleted: body.deleted ?? true,
|
|
703
|
-
activeSessionsClosed: body.activeSessionsClosed,
|
|
704
|
-
};
|
|
705
|
-
},
|
|
706
|
-
async rotate(id, rotateOptions = {}) {
|
|
707
|
-
const graceSeconds = rotateOptions.graceSeconds ??
|
|
708
|
-
(rotateOptions.grace !== undefined ? toSeconds(rotateOptions.grace) : undefined);
|
|
709
|
-
const leaseSeconds = rotateOptions.leaseSeconds ??
|
|
710
|
-
(rotateOptions.lease !== undefined ? toSeconds(rotateOptions.lease) : undefined);
|
|
711
|
-
const body = await requestJson(`/v1/capabilities/${encodeURIComponent(id)}/rotate`, {
|
|
712
|
-
method: 'POST',
|
|
713
|
-
body: JSON.stringify({
|
|
714
|
-
...(graceSeconds !== undefined ? { graceSeconds } : {}),
|
|
715
|
-
...(leaseSeconds !== undefined ? { ttlSeconds: leaseSeconds } : {}),
|
|
716
|
-
}),
|
|
717
|
-
});
|
|
718
|
-
const newId = body.capabilityId ?? body.id;
|
|
719
|
-
if (!newId) {
|
|
720
|
-
throw new AbloValidationError('Capability rotate response did not include an id.', { code: 'capability_id_missing' });
|
|
721
|
-
}
|
|
722
|
-
return {
|
|
723
|
-
id: newId,
|
|
724
|
-
token: body.token,
|
|
725
|
-
expiresAt: body.expiresAt,
|
|
726
|
-
organizationId: body.organizationId,
|
|
727
|
-
scope: body.scope,
|
|
728
|
-
rotatedFrom: {
|
|
729
|
-
id: body.rotatedFrom.capabilityId ?? body.rotatedFrom.id ?? id,
|
|
730
|
-
expiresAt: body.rotatedFrom.expiresAt,
|
|
731
|
-
},
|
|
732
|
-
client: () => childClient(body.token, {
|
|
733
|
-
organizationId: body.organizationId,
|
|
734
|
-
participantId: body.scope.participantId,
|
|
735
|
-
}),
|
|
736
|
-
};
|
|
737
|
-
},
|
|
738
|
-
mint(options) {
|
|
739
|
-
return capabilities.create(options);
|
|
740
|
-
},
|
|
741
|
-
};
|
|
742
|
-
const claims = {
|
|
743
|
-
async create(claimOptions) {
|
|
744
|
-
const claimId = createClaimId();
|
|
745
|
-
const body = await requestJson('/v1/claims', {
|
|
746
|
-
method: 'POST',
|
|
747
|
-
body: JSON.stringify({
|
|
748
|
-
claimId,
|
|
749
|
-
target: claimOptions.target,
|
|
750
|
-
reason: claimOptions.reason,
|
|
751
|
-
ttl: claimOptions.ttl,
|
|
752
|
-
queue: claimOptions.queue,
|
|
753
|
-
}),
|
|
754
|
-
});
|
|
755
|
-
// The fair-queue grant is pushed over a WebSocket (`claim_granted`), which
|
|
756
|
-
// this stateless HTTP client doesn't hold. Returning a handle here would be
|
|
757
|
-
// a phantom holder — a lease we can't confirm is ours. So a queued response
|
|
758
|
-
// is surfaced as a typed claimed signal; callers that need to wait in line
|
|
759
|
-
// use the realtime (WebSocket-backed) `ablo.<model>.claim`.
|
|
760
|
-
if (body.status === 'queued') {
|
|
761
|
-
throw new AbloClaimedError(`Target ${claimOptions.target.model}/${claimOptions.target.id} is held; ` +
|
|
762
|
-
`queued at position ${body.position ?? 0}. The HTTP client can't await ` +
|
|
763
|
-
`the grant (no socket) — use the realtime client to wait in line.`, { code: 'claim_queued' });
|
|
764
|
-
}
|
|
765
|
-
const id = body.claim?.id ?? claimId;
|
|
766
|
-
let released = false;
|
|
767
|
-
const release = async () => {
|
|
768
|
-
if (released)
|
|
769
|
-
return;
|
|
770
|
-
released = true;
|
|
771
|
-
await requestJson(`/v1/claims/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
772
|
-
};
|
|
773
|
-
// The by-id twin of the model-scoped heartbeat — same reply contract.
|
|
774
|
-
const heartbeat = async (beatOptions) => {
|
|
775
|
-
const resolved = resolveHeartbeatOptions(beatOptions);
|
|
776
|
-
const reply = await requestJson(`/v1/claims/${encodeURIComponent(id)}/heartbeat`, {
|
|
777
|
-
method: 'POST',
|
|
778
|
-
body: JSON.stringify({
|
|
779
|
-
...(resolved.ttl !== undefined ? { ttl: resolved.ttl } : {}),
|
|
780
|
-
...(resolved.details !== undefined
|
|
781
|
-
? { details: resolved.details }
|
|
782
|
-
: {}),
|
|
783
|
-
}),
|
|
784
|
-
});
|
|
785
|
-
return heldHeartbeatReply(reply, `claim ${id}`);
|
|
786
|
-
};
|
|
787
|
-
return {
|
|
788
|
-
object: 'claim',
|
|
789
|
-
id,
|
|
790
|
-
heartbeat,
|
|
791
|
-
reason: claimOptions.reason,
|
|
792
|
-
target: {
|
|
793
|
-
type: claimOptions.target.model,
|
|
794
|
-
id: claimOptions.target.id,
|
|
795
|
-
...(claimOptions.target.field ? { field: claimOptions.target.field } : {}),
|
|
796
|
-
...(claimOptions.target.path ? { path: claimOptions.target.path } : {}),
|
|
797
|
-
...(claimOptions.target.range ? { range: claimOptions.target.range } : {}),
|
|
798
|
-
...(claimOptions.target.meta ? { meta: claimOptions.target.meta } : {}),
|
|
799
|
-
},
|
|
800
|
-
release,
|
|
801
|
-
revoke: () => {
|
|
802
|
-
void release().catch(() => { });
|
|
803
|
-
},
|
|
804
|
-
[Symbol.asyncDispose]: release,
|
|
805
|
-
};
|
|
806
|
-
},
|
|
807
|
-
list: listClaims,
|
|
808
|
-
waitFor(target, options) {
|
|
809
|
-
return waitForNoClaims(target, options);
|
|
810
|
-
},
|
|
811
|
-
async heartbeatAll(options) {
|
|
812
|
-
const reply = await requestJson('/v1/claims/heartbeat', {
|
|
813
|
-
method: 'POST',
|
|
814
|
-
body: JSON.stringify(options?.ttl !== undefined ? { ttl: options.ttl } : {}),
|
|
815
|
-
});
|
|
816
|
-
return (reply.results ?? []).flatMap((entry) => typeof entry.claimId === 'string' && typeof entry.expiresAt === 'number'
|
|
817
|
-
? [
|
|
818
|
-
{
|
|
819
|
-
claimId: entry.claimId,
|
|
820
|
-
expiresAt: entry.expiresAt,
|
|
821
|
-
...(entry.queueDepth !== undefined
|
|
822
|
-
? { queueDepth: entry.queueDepth }
|
|
823
|
-
: {}),
|
|
824
|
-
},
|
|
825
|
-
]
|
|
826
|
-
: []);
|
|
827
|
-
},
|
|
828
|
-
};
|
|
829
613
|
async function listModel(modelName, options) {
|
|
830
614
|
const params = new URLSearchParams();
|
|
831
615
|
if (options?.limit !== undefined)
|
|
@@ -906,7 +690,6 @@ export function createProtocolClient(options) {
|
|
|
906
690
|
: undefined;
|
|
907
691
|
const readAt = options?.readAt ?? claimHandle?.readAt;
|
|
908
692
|
const requestBody = {
|
|
909
|
-
idempotencyKey: clientTxId,
|
|
910
693
|
claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.id,
|
|
911
694
|
onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
|
|
912
695
|
readAt,
|
|
@@ -1064,11 +847,14 @@ export function createProtocolClient(options) {
|
|
|
1064
847
|
state: async (params) => {
|
|
1065
848
|
const res = await claimsForEntity(params);
|
|
1066
849
|
const first = res.claims?.[0];
|
|
1067
|
-
return first
|
|
850
|
+
return first ? claimFromModelClaim(first) : null;
|
|
1068
851
|
},
|
|
1069
852
|
queue: async (params) => {
|
|
1070
853
|
const res = await claimsForEntity(params);
|
|
1071
|
-
return {
|
|
854
|
+
return {
|
|
855
|
+
object: 'list',
|
|
856
|
+
data: (res.queue ?? []).map(claimFromModelClaim),
|
|
857
|
+
};
|
|
1072
858
|
},
|
|
1073
859
|
reorder: async (params) => {
|
|
1074
860
|
await requestJson(`${claimPath(params.id)}/reorder`, {
|
|
@@ -1184,8 +970,6 @@ export function createProtocolClient(options) {
|
|
|
1184
970
|
}),
|
|
1185
971
|
async dispose() { },
|
|
1186
972
|
async purge() { },
|
|
1187
|
-
capabilities,
|
|
1188
|
-
claims,
|
|
1189
973
|
commits,
|
|
1190
974
|
model,
|
|
1191
975
|
sessions: {
|
|
@@ -1201,6 +985,9 @@ export function createProtocolClient(options) {
|
|
|
1201
985
|
apiKey,
|
|
1202
986
|
baseUrl: apiBaseUrl,
|
|
1203
987
|
...(options.fetch ? { fetch: options.fetch } : {}),
|
|
988
|
+
...(options.modelTypenames
|
|
989
|
+
? { modelTypenames: options.modelTypenames }
|
|
990
|
+
: {}),
|
|
1204
991
|
});
|
|
1205
992
|
},
|
|
1206
993
|
},
|
package/dist/client/options.d.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The option types for the {@link Ablo} client: the public {@link AbloOptions}
|
|
3
|
-
* bag callers pass, and the fuller {@link InternalAbloOptions}
|
|
4
|
-
* surface
|
|
3
|
+
* bag callers pass, and the fuller monorepo-only {@link InternalAbloOptions}
|
|
4
|
+
* construction surface exposed through `@abloatai/ablo/core`.
|
|
5
|
+
* This module holds only types and has no runtime imports.
|
|
5
6
|
*/
|
|
6
7
|
import type { Schema, SchemaRecord } from '../schema/schema.js';
|
|
7
8
|
import type { SyncEngineConfig, SyncLogger, MutationExecutor, SyncObservabilityProvider, SyncAnalytics, SessionErrorDetector, OnlineStatusProvider } from '../interfaces/index.js';
|
|
8
9
|
import type { AbloPersistence } from './persistence.js';
|
|
9
|
-
import type {
|
|
10
|
+
import type { DurableWriteStore, DurableWritesConfig } from '../transactions/durableWriteStore.js';
|
|
10
11
|
import type { CommitOutboxScope } from '../transactions/commitEnvelope.js';
|
|
11
12
|
/**
|
|
12
13
|
* An async function that resolves an apiKey at request time. Use it for credential
|
|
@@ -111,16 +112,38 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
111
112
|
*/
|
|
112
113
|
persistence?: AbloPersistence;
|
|
113
114
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
118
|
-
*
|
|
119
|
-
*
|
|
115
|
+
* Makes `create`, `update`, and `delete` survive process restarts and
|
|
116
|
+
* ambiguous network responses. Server-side agents and workers inject a
|
|
117
|
+
* workflow-, SQLite-, or filesystem-backed store; authenticated identity is
|
|
118
|
+
* used to fence replay to the actor that originally made the write.
|
|
119
|
+
*
|
|
120
|
+
* Most clients leave this unset and use Ablo's default in-memory cache.
|
|
121
|
+
* `durableWrites` is independent of that cache: it is the optional outbound
|
|
122
|
+
* write journal for workers that must recover automatically after a crash.
|
|
123
|
+
* Browsers only use IndexedDB when `persistence: 'indexeddb'` is explicitly
|
|
124
|
+
* enabled for offline/reload behavior.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* Ablo({
|
|
129
|
+
* schema,
|
|
130
|
+
* apiKey,
|
|
131
|
+
* durableWrites: { store, namespace: 'agent-worker' },
|
|
132
|
+
* })
|
|
133
|
+
* ```
|
|
120
134
|
*/
|
|
121
|
-
|
|
135
|
+
durableWrites?: DurableWritesConfig;
|
|
122
136
|
/**
|
|
123
|
-
*
|
|
137
|
+
* @deprecated Use `durableWrites: { store, namespace }`.
|
|
138
|
+
* Retained as a compatibility alias through the next major release.
|
|
139
|
+
*/
|
|
140
|
+
commitOutbox?: DurableWriteStore;
|
|
141
|
+
/**
|
|
142
|
+
* @deprecated Actor identity is resolved from authentication. Use
|
|
143
|
+
* `durableWrites.namespace` only when shared storage needs separate workflow
|
|
144
|
+
* or deployment lanes.
|
|
145
|
+
*
|
|
146
|
+
* Stable actor/server scope for an injected HTTP `commitOutbox`.
|
|
124
147
|
* Omit it to resolve `organizationId` and `participantId` from the authenticated
|
|
125
148
|
* `/auth/identity` endpoint. Supplying it avoids that one setup request in
|
|
126
149
|
* trusted worker environments; `namespace` distinguishes deployments or
|
|
@@ -137,8 +160,7 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
137
160
|
* {@link AbloHttpClient}, so stateful-only capabilities such as `get`, `getAll`,
|
|
138
161
|
* and `onChange` become compile errors instead of runtime gaps.
|
|
139
162
|
*
|
|
140
|
-
*
|
|
141
|
-
* client, not the HTTP client.
|
|
163
|
+
* Session minting through `sessions.create` is available on both transports.
|
|
142
164
|
*
|
|
143
165
|
* @default 'websocket'
|
|
144
166
|
*/
|
|
@@ -241,9 +263,9 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
241
263
|
/**
|
|
242
264
|
* TypeScript schema defined with `defineSchema()`.
|
|
243
265
|
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* only
|
|
266
|
+
* Required on every public `Ablo(...)` construction. It produces typed model
|
|
267
|
+
* clients such as `ablo.weatherReports.update(...)`; schema-agnostic routing
|
|
268
|
+
* exists only inside the private HTTP transport.
|
|
247
269
|
*/
|
|
248
270
|
schema: Schema<S>;
|
|
249
271
|
/**
|
|
@@ -299,9 +321,11 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
299
321
|
* reload-surviving local cache in a browser.
|
|
300
322
|
*/
|
|
301
323
|
persistence?: AbloPersistence;
|
|
302
|
-
/** Internal mirror of {@link AbloOptions.
|
|
303
|
-
|
|
304
|
-
/** Internal mirror of {@link AbloOptions.
|
|
324
|
+
/** Internal mirror of {@link AbloOptions.durableWrites}. */
|
|
325
|
+
durableWrites?: DurableWritesConfig;
|
|
326
|
+
/** @deprecated Internal mirror of {@link AbloOptions.commitOutbox}. */
|
|
327
|
+
commitOutbox?: DurableWriteStore;
|
|
328
|
+
/** @deprecated Internal mirror of {@link AbloOptions.commitOutboxScope}. */
|
|
305
329
|
commitOutboxScope?: CommitOutboxScope;
|
|
306
330
|
/** @deprecated Use `persistence: 'indexeddb'` for durable browser storage. */
|
|
307
331
|
offline?: boolean;
|
package/dist/client/options.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The option types for the {@link Ablo} client: the public {@link AbloOptions}
|
|
3
|
-
* bag callers pass, and the fuller {@link InternalAbloOptions}
|
|
4
|
-
* surface
|
|
3
|
+
* bag callers pass, and the fuller monorepo-only {@link InternalAbloOptions}
|
|
4
|
+
* construction surface exposed through `@abloatai/ablo/core`.
|
|
5
|
+
* This module holds only types and has no runtime imports.
|
|
5
6
|
*/
|
|
6
7
|
export {};
|