@abloatai/ablo 0.10.0 → 0.11.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/CHANGELOG.md +16 -0
- package/README.md +2 -1
- package/dist/BaseSyncedStore.d.ts +75 -0
- package/dist/BaseSyncedStore.js +193 -8
- package/dist/Database.d.ts +10 -2
- package/dist/Database.js +15 -1
- package/dist/SyncClient.d.ts +12 -1
- package/dist/SyncClient.js +110 -26
- package/dist/agent/Agent.d.ts +9 -9
- package/dist/agent/Agent.js +16 -16
- package/dist/agent/index.d.ts +1 -1
- package/dist/agent/index.js +2 -2
- package/dist/agent/types.d.ts +1 -1
- package/dist/agent/types.js +1 -1
- package/dist/ai-sdk/{intent-broadcast.d.ts → claim-broadcast.d.ts} +10 -10
- package/dist/ai-sdk/{intent-broadcast.js → claim-broadcast.js} +6 -6
- package/dist/ai-sdk/coordination-context.d.ts +9 -9
- package/dist/ai-sdk/coordination-context.js +8 -8
- package/dist/ai-sdk/index.d.ts +1 -1
- package/dist/ai-sdk/index.js +1 -1
- package/dist/ai-sdk/wrap.d.ts +4 -4
- package/dist/ai-sdk/wrap.js +4 -4
- package/dist/api/index.d.ts +2 -2
- package/dist/cli.cjs +254 -48
- package/dist/client/Ablo.d.ts +30 -63
- package/dist/client/Ablo.js +108 -102
- package/dist/client/ApiClient.d.ts +6 -5
- package/dist/client/ApiClient.js +83 -62
- package/dist/client/createModelProxy.d.ts +16 -54
- package/dist/client/createModelProxy.js +44 -16
- package/dist/client/httpClient.d.ts +2 -0
- package/dist/client/httpClient.js +1 -1
- package/dist/client/index.d.ts +3 -3
- package/dist/client/writeOptionsSchema.d.ts +4 -4
- package/dist/client/writeOptionsSchema.js +4 -4
- package/dist/coordination/schema.d.ts +249 -38
- package/dist/coordination/schema.js +172 -39
- package/dist/core/index.d.ts +2 -2
- package/dist/core/index.js +4 -4
- package/dist/errorCodes.d.ts +9 -9
- package/dist/errorCodes.js +15 -15
- package/dist/errors.d.ts +51 -2
- package/dist/errors.js +94 -5
- package/dist/interfaces/index.d.ts +8 -4
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/types.d.ts +13 -13
- package/dist/policy/types.js +8 -8
- package/dist/react/AbloProvider.d.ts +51 -4
- package/dist/react/AbloProvider.js +95 -11
- package/dist/react/context.d.ts +26 -9
- package/dist/react/context.js +2 -2
- package/dist/react/index.d.ts +4 -4
- package/dist/react/index.js +4 -4
- package/dist/react/useAblo.js +5 -5
- package/dist/react/{useIntent.d.ts → useClaim.d.ts} +9 -9
- package/dist/react/useClaim.js +42 -0
- package/dist/schema/index.js +1 -1
- package/dist/schema/sugar.d.ts +3 -3
- package/dist/schema/sugar.js +3 -3
- package/dist/schema/sync-delta-wire.d.ts +8 -8
- package/dist/server/commit.d.ts +2 -2
- package/dist/sync/AreaOfInterestManager.d.ts +162 -0
- package/dist/sync/AreaOfInterestManager.js +233 -0
- package/dist/sync/BootstrapHelper.d.ts +9 -1
- package/dist/sync/BootstrapHelper.js +15 -5
- package/dist/sync/NetworkProbe.d.ts +1 -1
- package/dist/sync/NetworkProbe.js +1 -1
- package/dist/sync/SyncWebSocket.d.ts +59 -25
- package/dist/sync/SyncWebSocket.js +123 -26
- package/dist/sync/awaitClaimGrant.d.ts +40 -0
- package/dist/sync/awaitClaimGrant.js +86 -0
- package/dist/sync/createClaimStream.d.ts +34 -0
- package/dist/sync/{createIntentStream.js → createClaimStream.js} +92 -81
- package/dist/sync/createPresenceStream.js +3 -2
- package/dist/sync/participants.d.ts +10 -10
- package/dist/sync/participants.js +17 -10
- package/dist/sync/schemas.d.ts +8 -8
- package/dist/transactions/TransactionQueue.d.ts +12 -0
- package/dist/transactions/TransactionQueue.js +126 -8
- package/dist/types/global.d.ts +10 -10
- package/dist/types/global.js +3 -3
- package/dist/types/index.d.ts +9 -7
- package/dist/types/index.js +2 -2
- package/dist/types/streams.d.ts +114 -98
- package/dist/types/streams.js +1 -1
- package/dist/utils/asyncIterator.d.ts +1 -1
- package/dist/utils/asyncIterator.js +1 -1
- package/dist/wire/frames.d.ts +2 -2
- package/docs/migration.md +52 -0
- package/package.json +3 -2
- package/dist/react/useIntent.js +0 -42
- package/dist/sync/awaitIntentGrant.d.ts +0 -40
- package/dist/sync/awaitIntentGrant.js +0 -62
- package/dist/sync/createIntentStream.d.ts +0 -34
|
@@ -5,16 +5,17 @@
|
|
|
5
5
|
* IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
|
-
import type { AbloOptions, CommitResource,
|
|
8
|
+
import type { AbloOptions, CommitResource, ClaimCreateOptions, ClaimWaitOptions, ModelClient, ModelClaim, ModelTarget } from './Ablo.js';
|
|
9
|
+
import type { ClaimHandle } from './createModelProxy.js';
|
|
9
10
|
import type { Duration } from '../utils/duration.js';
|
|
10
11
|
export type AbloApiClientOptions = Omit<AbloOptions, 'schema'> & {
|
|
11
12
|
readonly schema?: null | undefined;
|
|
12
13
|
readonly bootstrapBaseUrl?: string | undefined;
|
|
13
14
|
};
|
|
14
|
-
export interface
|
|
15
|
-
create(options:
|
|
15
|
+
export interface AbloApiClaims {
|
|
16
|
+
create(options: ClaimCreateOptions): Promise<ClaimHandle>;
|
|
16
17
|
list(target?: Partial<ModelTarget>): Promise<readonly ModelClaim[]>;
|
|
17
|
-
waitFor(target: Partial<ModelTarget>, options?:
|
|
18
|
+
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
|
|
18
19
|
}
|
|
19
20
|
export type CapabilityParticipantKind = 'agent' | 'system';
|
|
20
21
|
export interface CapabilityCreateBaseOptions {
|
|
@@ -121,7 +122,7 @@ export interface AbloApi {
|
|
|
121
122
|
dispose(): Promise<void>;
|
|
122
123
|
purge(): Promise<void>;
|
|
123
124
|
readonly capabilities: CapabilityResource;
|
|
124
|
-
readonly
|
|
125
|
+
readonly claims: AbloApiClaims;
|
|
125
126
|
readonly commits: CommitResource;
|
|
126
127
|
model<T = Record<string, unknown>>(name: string): ModelClient<T>;
|
|
127
128
|
/**
|
package/dist/client/ApiClient.js
CHANGED
|
@@ -5,8 +5,9 @@
|
|
|
5
5
|
* IndexedDB, no WebSocket. It maps the public Model / Claim / Commit
|
|
6
6
|
* nouns directly to HTTP routes on sync-server.
|
|
7
7
|
*/
|
|
8
|
-
import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, } from '../errors.js';
|
|
9
|
-
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, } from './auth.js';
|
|
8
|
+
import { AbloClaimedError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, claimedError, translateHttpError, } from '../errors.js';
|
|
9
|
+
import { assertBrowserSafety, readProcessEnv, resolveApiKey, resolveApiKeyValue, resolveAuthToken, resolveBaseURL, resolveBootstrapBaseUrl, resolveDatabaseUrl, } from './auth.js';
|
|
10
|
+
import { registerDataSource } from './registerDataSource.js';
|
|
10
11
|
import { toSeconds } from '../utils/duration.js';
|
|
11
12
|
import { assertWriteOptions } from './writeOptionsSchema.js';
|
|
12
13
|
const DEFAULT_AGENT_LEASE = '10m';
|
|
@@ -15,8 +16,10 @@ export function createProtocolClient(options) {
|
|
|
15
16
|
const authInput = { options, env };
|
|
16
17
|
const configuredApiKey = resolveApiKey(authInput);
|
|
17
18
|
const configuredAuthToken = resolveAuthToken(authInput);
|
|
19
|
+
const configuredDatabaseUrl = resolveDatabaseUrl(authInput);
|
|
18
20
|
assertBrowserSafety({
|
|
19
21
|
apiKey: configuredApiKey,
|
|
22
|
+
databaseUrl: configuredDatabaseUrl,
|
|
20
23
|
dangerouslyAllowBrowser: options.dangerouslyAllowBrowser,
|
|
21
24
|
});
|
|
22
25
|
const fetchImpl = options.fetch ?? globalThis.fetch;
|
|
@@ -28,6 +31,28 @@ export function createProtocolClient(options) {
|
|
|
28
31
|
url,
|
|
29
32
|
bootstrapBaseUrl: options.bootstrapBaseUrl,
|
|
30
33
|
}).replace(/\/+$/, '');
|
|
34
|
+
let readyPromise = null;
|
|
35
|
+
async function ready() {
|
|
36
|
+
if (readyPromise)
|
|
37
|
+
return readyPromise;
|
|
38
|
+
readyPromise = (async () => {
|
|
39
|
+
if (!configuredDatabaseUrl)
|
|
40
|
+
return;
|
|
41
|
+
await registerDataSource({
|
|
42
|
+
baseUrl: apiBaseUrl,
|
|
43
|
+
apiKey: await resolveApiKeyValue(configuredApiKey),
|
|
44
|
+
databaseUrl: configuredDatabaseUrl,
|
|
45
|
+
...(options.fetch ? { fetchImpl: options.fetch } : {}),
|
|
46
|
+
});
|
|
47
|
+
})();
|
|
48
|
+
try {
|
|
49
|
+
await readyPromise;
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
readyPromise = null;
|
|
53
|
+
throw error;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
31
56
|
async function authHeaders() {
|
|
32
57
|
const apiKey = await resolveApiKeyValue(configuredApiKey);
|
|
33
58
|
const token = apiKey ?? configuredAuthToken;
|
|
@@ -57,6 +82,7 @@ export function createProtocolClient(options) {
|
|
|
57
82
|
return target.toString();
|
|
58
83
|
}
|
|
59
84
|
async function requestJson(path, init) {
|
|
85
|
+
await ready();
|
|
60
86
|
const { idempotencyKey, ...requestInit } = init;
|
|
61
87
|
const headers = await authHeaders();
|
|
62
88
|
if (idempotencyKey)
|
|
@@ -82,7 +108,7 @@ export function createProtocolClient(options) {
|
|
|
82
108
|
? crypto.randomUUID()
|
|
83
109
|
: `tx_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
84
110
|
}
|
|
85
|
-
function
|
|
111
|
+
function createClaimId() {
|
|
86
112
|
return typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function'
|
|
87
113
|
? `int_${crypto.randomUUID()}`
|
|
88
114
|
: `int_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
@@ -128,7 +154,7 @@ export function createProtocolClient(options) {
|
|
|
128
154
|
}
|
|
129
155
|
return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
|
|
130
156
|
}
|
|
131
|
-
async function
|
|
157
|
+
async function listClaims(target) {
|
|
132
158
|
const state = await listClaimState(target);
|
|
133
159
|
return state.active;
|
|
134
160
|
}
|
|
@@ -141,24 +167,16 @@ export function createProtocolClient(options) {
|
|
|
141
167
|
if (target?.field)
|
|
142
168
|
params.set('field', target.field);
|
|
143
169
|
const suffix = params.toString();
|
|
144
|
-
const body = await requestJson(`/v1/
|
|
170
|
+
const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
|
|
145
171
|
return {
|
|
146
|
-
active: body.
|
|
172
|
+
active: body.claims ?? [],
|
|
147
173
|
queue: body.queue ?? [],
|
|
148
174
|
};
|
|
149
175
|
}
|
|
150
|
-
function claimedError(target, claims, code) {
|
|
151
|
-
const label = [target.model, target.id, target.field].filter(Boolean).join('/');
|
|
152
|
-
const holder = claims[0];
|
|
153
|
-
const suffix = holder
|
|
154
|
-
? ` held by ${holder.actor} (${holder.action})`
|
|
155
|
-
: ' held by another participant';
|
|
156
|
-
return new AbloClaimedError(`Model row is claimed: ${label || 'target'}${suffix}.`, { code, claims });
|
|
157
|
-
}
|
|
158
176
|
function delay(ms, signal) {
|
|
159
177
|
if (signal?.aborted) {
|
|
160
|
-
return Promise.reject(new AbloConnectionError('
|
|
161
|
-
code: '
|
|
178
|
+
return Promise.reject(new AbloConnectionError('Claim wait aborted.', {
|
|
179
|
+
code: 'claim_wait_aborted',
|
|
162
180
|
cause: signal.reason,
|
|
163
181
|
}));
|
|
164
182
|
}
|
|
@@ -174,28 +192,28 @@ export function createProtocolClient(options) {
|
|
|
174
192
|
}
|
|
175
193
|
function onAbort() {
|
|
176
194
|
cleanup();
|
|
177
|
-
reject(new AbloConnectionError('
|
|
178
|
-
code: '
|
|
195
|
+
reject(new AbloConnectionError('Claim wait aborted.', {
|
|
196
|
+
code: 'claim_wait_aborted',
|
|
179
197
|
cause: signal?.reason,
|
|
180
198
|
}));
|
|
181
199
|
}
|
|
182
200
|
signal?.addEventListener('abort', onAbort, { once: true });
|
|
183
201
|
});
|
|
184
202
|
}
|
|
185
|
-
async function
|
|
203
|
+
async function waitForNoClaims(target, options) {
|
|
186
204
|
const startedAt = Date.now();
|
|
187
205
|
const pollInterval = options?.pollInterval;
|
|
188
206
|
for (;;) {
|
|
189
|
-
const
|
|
190
|
-
if (
|
|
207
|
+
const claims = await listClaims(target);
|
|
208
|
+
if (claims.length === 0)
|
|
191
209
|
return;
|
|
192
210
|
if (pollInterval == null) {
|
|
193
211
|
throw new AbloValidationError('Cannot wait for claims over the HTTP client without `pollInterval`. ' +
|
|
194
212
|
'Use the schema client for event-driven claim waits, pass `ifClaimed: "return"`, ' +
|
|
195
|
-
'or provide an explicit poll interval for this runtime.', { code: '
|
|
213
|
+
'or provide an explicit poll interval for this runtime.', { code: 'claim_wait_poll_interval_required' });
|
|
196
214
|
}
|
|
197
215
|
if (options?.timeout != null && Date.now() - startedAt >= options.timeout) {
|
|
198
|
-
throw claimedError(target,
|
|
216
|
+
throw claimedError(target, claims, 'model_claimed_timeout');
|
|
199
217
|
}
|
|
200
218
|
const remaining = options?.timeout == null
|
|
201
219
|
? pollInterval
|
|
@@ -217,7 +235,7 @@ export function createProtocolClient(options) {
|
|
|
217
235
|
state.queue.length >= options.maxQueueDepth) {
|
|
218
236
|
throw claimedError(target, state.active, 'queue_too_deep');
|
|
219
237
|
}
|
|
220
|
-
await
|
|
238
|
+
await waitForNoClaims(target, {
|
|
221
239
|
timeout: options?.claimedTimeout,
|
|
222
240
|
pollInterval: options?.claimedPollInterval,
|
|
223
241
|
});
|
|
@@ -230,7 +248,7 @@ export function createProtocolClient(options) {
|
|
|
230
248
|
readAt: commitOptions.readAt,
|
|
231
249
|
onStale: commitOptions.onStale,
|
|
232
250
|
wait: commitOptions.wait,
|
|
233
|
-
|
|
251
|
+
claim: commitOptions.claim,
|
|
234
252
|
}, 'commits.create');
|
|
235
253
|
const clientTxId = createClientTxId(commitOptions.idempotencyKey);
|
|
236
254
|
// Same claim vocabulary as the WS client's `commits.create`: a handle
|
|
@@ -247,7 +265,7 @@ export function createProtocolClient(options) {
|
|
|
247
265
|
body: JSON.stringify({
|
|
248
266
|
clientTxId,
|
|
249
267
|
idempotencyKey: clientTxId,
|
|
250
|
-
|
|
268
|
+
claim: normalizeClaimId(commitOptions.claimRef) ?? claim?.claimId,
|
|
251
269
|
operations,
|
|
252
270
|
}),
|
|
253
271
|
});
|
|
@@ -353,39 +371,42 @@ export function createProtocolClient(options) {
|
|
|
353
371
|
return capabilities.create(options);
|
|
354
372
|
},
|
|
355
373
|
};
|
|
356
|
-
const
|
|
357
|
-
async create(
|
|
358
|
-
const
|
|
359
|
-
const body = await requestJson('/v1/
|
|
374
|
+
const claims = {
|
|
375
|
+
async create(claimOptions) {
|
|
376
|
+
const claimId = createClaimId();
|
|
377
|
+
const body = await requestJson('/v1/claims', {
|
|
360
378
|
method: 'POST',
|
|
361
379
|
body: JSON.stringify({
|
|
362
|
-
|
|
363
|
-
target:
|
|
364
|
-
action:
|
|
365
|
-
ttl:
|
|
366
|
-
queue:
|
|
380
|
+
claimId,
|
|
381
|
+
target: claimOptions.target,
|
|
382
|
+
action: claimOptions.action,
|
|
383
|
+
ttl: claimOptions.ttl,
|
|
384
|
+
queue: claimOptions.queue,
|
|
367
385
|
}),
|
|
368
386
|
});
|
|
369
|
-
// The fair-queue grant is PUSHED over a WebSocket (`
|
|
387
|
+
// The fair-queue grant is PUSHED over a WebSocket (`claim_granted`),
|
|
370
388
|
// which this stateless HTTP client doesn't hold. Returning a handle here
|
|
371
389
|
// would be a phantom holder — a lease we can't confirm is ours. So a
|
|
372
390
|
// queued response is surfaced as a typed claimed signal; callers that need
|
|
373
391
|
// to *wait* in line use the realtime (WS-backed) `ablo.<model>.claim`.
|
|
374
392
|
if (body.status === 'queued') {
|
|
375
|
-
throw new AbloClaimedError(`Target ${
|
|
393
|
+
throw new AbloClaimedError(`Target ${claimOptions.target.model}/${claimOptions.target.id} is held; ` +
|
|
376
394
|
`queued at position ${body.position ?? 0}. The HTTP client can't await ` +
|
|
377
|
-
`the grant (no socket) — use the realtime client to wait in line.`, { code: '
|
|
395
|
+
`the grant (no socket) — use the realtime client to wait in line.`, { code: 'claim_queued' });
|
|
378
396
|
}
|
|
379
|
-
const id = body.
|
|
397
|
+
const id = body.claim?.id ?? claimId;
|
|
380
398
|
let released = false;
|
|
381
399
|
const release = async () => {
|
|
382
400
|
if (released)
|
|
383
401
|
return;
|
|
384
402
|
released = true;
|
|
385
|
-
await requestJson(`/v1/
|
|
403
|
+
await requestJson(`/v1/claims/${encodeURIComponent(id)}`, { method: 'DELETE' });
|
|
386
404
|
};
|
|
387
405
|
return {
|
|
388
|
-
|
|
406
|
+
object: 'claim',
|
|
407
|
+
claimId: id,
|
|
408
|
+
action: claimOptions.action,
|
|
409
|
+
target: claimOptions.target,
|
|
389
410
|
release,
|
|
390
411
|
revoke: () => {
|
|
391
412
|
void release().catch(() => { });
|
|
@@ -393,9 +414,9 @@ export function createProtocolClient(options) {
|
|
|
393
414
|
[Symbol.asyncDispose]: release,
|
|
394
415
|
};
|
|
395
416
|
},
|
|
396
|
-
list:
|
|
417
|
+
list: listClaims,
|
|
397
418
|
waitFor(target, options) {
|
|
398
|
-
return
|
|
419
|
+
return waitForNoClaims(target, options);
|
|
399
420
|
},
|
|
400
421
|
};
|
|
401
422
|
async function listModel(modelName, options) {
|
|
@@ -456,7 +477,7 @@ export function createProtocolClient(options) {
|
|
|
456
477
|
readAt: options.readAt,
|
|
457
478
|
onStale: options.onStale,
|
|
458
479
|
wait: options.wait,
|
|
459
|
-
|
|
480
|
+
claim: options.claim,
|
|
460
481
|
}, `${modelName} ${action}`);
|
|
461
482
|
const clientTxId = createClientTxId(options?.idempotencyKey);
|
|
462
483
|
const encModel = encodeURIComponent(modelName);
|
|
@@ -475,7 +496,7 @@ export function createProtocolClient(options) {
|
|
|
475
496
|
const readAt = options?.readAt ?? claimHandle?.readAt;
|
|
476
497
|
const requestBody = {
|
|
477
498
|
idempotencyKey: clientTxId,
|
|
478
|
-
|
|
499
|
+
claim: normalizeClaimId(options?.claimRef) ?? claimHandle?.claimId,
|
|
479
500
|
onStale: options?.onStale ?? (claimHandle?.readAt !== undefined ? 'reject' : undefined),
|
|
480
501
|
readAt,
|
|
481
502
|
};
|
|
@@ -528,9 +549,9 @@ export function createProtocolClient(options) {
|
|
|
528
549
|
});
|
|
529
550
|
if (body.status === 'queued') {
|
|
530
551
|
throw new AbloClaimedError(`Target ${name}/${params.id} is held; queued at position ${body.position ?? 0}. ` +
|
|
531
|
-
`The HTTP client cannot await the grant without a WebSocket.`, { code: '
|
|
552
|
+
`The HTTP client cannot await the grant without a WebSocket.`, { code: 'claim_queued' });
|
|
532
553
|
}
|
|
533
|
-
return body.
|
|
554
|
+
return body.claim?.id ?? body.id ?? body.claimId ?? createClaimId();
|
|
534
555
|
};
|
|
535
556
|
const releaseClaim = (params) => requestJson(claimPath(isClaimHandle(params) ? params.target.id : params.id), { method: 'DELETE' }).then(() => undefined);
|
|
536
557
|
async function claimImpl(params) {
|
|
@@ -559,23 +580,23 @@ export function createProtocolClient(options) {
|
|
|
559
580
|
[Symbol.asyncDispose]: release,
|
|
560
581
|
};
|
|
561
582
|
}
|
|
562
|
-
const
|
|
583
|
+
const claimsForEntity = async (params) => requestJson(`/v1/claims?model=${encodeURIComponent(name)}&id=${encodeURIComponent(params.id)}${params.field ? `&field=${encodeURIComponent(params.field)}` : ''}`, { method: 'GET' });
|
|
563
584
|
const claim = Object.assign(claimImpl, {
|
|
564
585
|
release: releaseClaim,
|
|
565
586
|
state: async (params) => {
|
|
566
|
-
const res = await
|
|
567
|
-
return res.
|
|
587
|
+
const res = await claimsForEntity(params);
|
|
588
|
+
return res.claims?.[0] ?? null;
|
|
568
589
|
},
|
|
569
590
|
queue: async (params) => {
|
|
570
|
-
const res = await
|
|
591
|
+
const res = await claimsForEntity(params);
|
|
571
592
|
return { object: 'list', data: res.queue ?? [] };
|
|
572
593
|
},
|
|
573
594
|
reorder: async (params) => {
|
|
574
595
|
await requestJson(`${claimPath(params.id)}/reorder`, {
|
|
575
596
|
method: 'POST',
|
|
576
|
-
// The reorder route's payload is `{ heldBy,
|
|
577
|
-
// IS the
|
|
578
|
-
body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy,
|
|
597
|
+
// The reorder route's payload is `{ heldBy, claimId }[]` — Claim's id
|
|
598
|
+
// IS the claimId.
|
|
599
|
+
body: JSON.stringify({ order: params.order.map((i) => ({ heldBy: i.heldBy, claimId: i.id })) }),
|
|
579
600
|
});
|
|
580
601
|
},
|
|
581
602
|
});
|
|
@@ -584,11 +605,11 @@ export function createProtocolClient(options) {
|
|
|
584
605
|
if (!claimInput)
|
|
585
606
|
return run(input);
|
|
586
607
|
if (isClaimHandle(claimInput)) {
|
|
587
|
-
return run({ ...input,
|
|
608
|
+
return run({ ...input, claimRef: { id: claimInput.claimId }, claim: undefined });
|
|
588
609
|
}
|
|
589
610
|
const claimId = await acquireClaim({ id, ...claimInput });
|
|
590
611
|
try {
|
|
591
|
-
return await run({ ...input,
|
|
612
|
+
return await run({ ...input, claimRef: { id: claimId }, claim: undefined });
|
|
592
613
|
}
|
|
593
614
|
finally {
|
|
594
615
|
await releaseClaim({ id }).catch(() => { });
|
|
@@ -624,12 +645,12 @@ export function createProtocolClient(options) {
|
|
|
624
645
|
};
|
|
625
646
|
}
|
|
626
647
|
return {
|
|
627
|
-
|
|
648
|
+
ready,
|
|
628
649
|
async waitForFlush() { },
|
|
629
650
|
async dispose() { },
|
|
630
651
|
async purge() { },
|
|
631
652
|
capabilities,
|
|
632
|
-
|
|
653
|
+
claims,
|
|
633
654
|
commits,
|
|
634
655
|
model,
|
|
635
656
|
async getAuthToken() {
|
|
@@ -639,10 +660,10 @@ export function createProtocolClient(options) {
|
|
|
639
660
|
},
|
|
640
661
|
};
|
|
641
662
|
}
|
|
642
|
-
function
|
|
643
|
-
if (typeof
|
|
644
|
-
return
|
|
645
|
-
return
|
|
663
|
+
function normalizeClaimId(claim) {
|
|
664
|
+
if (typeof claim === 'string')
|
|
665
|
+
return claim;
|
|
666
|
+
return claim?.id;
|
|
646
667
|
}
|
|
647
668
|
function parseBody(bodyText) {
|
|
648
669
|
if (bodyText.length === 0)
|
|
@@ -21,7 +21,7 @@ import type { SyncClient } from '../SyncClient.js';
|
|
|
21
21
|
import type { HydrationCoordinator } from '../sync/HydrationCoordinator.js';
|
|
22
22
|
import type { LoadWhere } from '../query/types.js';
|
|
23
23
|
import { ModelScope } from '../types/index.js';
|
|
24
|
-
import type { Duration,
|
|
24
|
+
import type { Duration, Claim, ClaimHandle, ClaimWaitOptions, Snapshot, TargetRange } from '../types/streams.js';
|
|
25
25
|
export interface ModelClientMeta {
|
|
26
26
|
readonly key: string;
|
|
27
27
|
readonly typename: string;
|
|
@@ -73,21 +73,8 @@ export interface ModelLoadOptions<T> {
|
|
|
73
73
|
/** Options for the single-row async server read `retrieve({ id })`. A subset of
|
|
74
74
|
* {@link ModelLoadOptions} — `where`/`limit`/`orderBy` are fixed by the id. */
|
|
75
75
|
export type ModelRetrieveOptions = Pick<ModelLoadOptions<unknown>, 'type' | 'expand'>;
|
|
76
|
-
export interface IntentLeaseHandle {
|
|
77
|
-
readonly id: string;
|
|
78
|
-
/**
|
|
79
|
-
* True when the grant came AFTER waiting in the server's FIFO line
|
|
80
|
-
* (`intent_granted`) — the authoritative "the row may have changed
|
|
81
|
-
* underneath us" signal. The local `observe()` snapshot can't stand in
|
|
82
|
-
* for this: intent fan-out is entity-scoped, so org-wide subscriptions
|
|
83
|
-
* (the default hosted client) never see peers' claims at all.
|
|
84
|
-
*/
|
|
85
|
-
readonly waited?: boolean;
|
|
86
|
-
release(): Promise<void>;
|
|
87
|
-
revoke(): void;
|
|
88
|
-
}
|
|
89
76
|
export interface ModelCollaboration<T> {
|
|
90
|
-
|
|
77
|
+
createClaim(options: {
|
|
91
78
|
target: {
|
|
92
79
|
model: string;
|
|
93
80
|
id: string;
|
|
@@ -106,11 +93,11 @@ export interface ModelCollaboration<T> {
|
|
|
106
93
|
queue?: boolean;
|
|
107
94
|
/** Reject (don't wait) if the queue is already this deep when we join. */
|
|
108
95
|
maxQueueDepth?: number;
|
|
109
|
-
}): Promise<
|
|
96
|
+
}): Promise<ClaimHandle>;
|
|
110
97
|
createSnapshot(modelKey: string, id: string): Snapshot;
|
|
111
98
|
/**
|
|
112
99
|
* Current coordination state on a target — who (if anyone) holds it.
|
|
113
|
-
* Synchronous reactive snapshot read off the presence/
|
|
100
|
+
* Synchronous reactive snapshot read off the presence/claim stream;
|
|
114
101
|
* `null` when the target is free. The wiring site computes it because
|
|
115
102
|
* only it knows the local participant id (needed to distinguish "I
|
|
116
103
|
* hold it" from "someone else holds it").
|
|
@@ -118,15 +105,15 @@ export interface ModelCollaboration<T> {
|
|
|
118
105
|
observe(target: {
|
|
119
106
|
model: string;
|
|
120
107
|
id: string;
|
|
121
|
-
}):
|
|
108
|
+
}): Claim | null;
|
|
122
109
|
/**
|
|
123
|
-
* The reactive wait queue on a target — the FIFO line of queued
|
|
124
|
-
* behind the holder. Synchronous snapshot off the synced
|
|
110
|
+
* The reactive wait queue on a target — the FIFO line of queued claims
|
|
111
|
+
* behind the holder. Synchronous snapshot off the synced claim stream.
|
|
125
112
|
*/
|
|
126
113
|
queue(target: {
|
|
127
114
|
model: string;
|
|
128
115
|
id: string;
|
|
129
|
-
}): readonly
|
|
116
|
+
}): readonly Claim[];
|
|
130
117
|
/**
|
|
131
118
|
* Re-rank the wait queue on a target (privileged — server-gated). `order` is
|
|
132
119
|
* the desired front-of-line ordering, taken from `queue(target)`.
|
|
@@ -134,16 +121,16 @@ export interface ModelCollaboration<T> {
|
|
|
134
121
|
reorder(target: {
|
|
135
122
|
model: string;
|
|
136
123
|
id: string;
|
|
137
|
-
}, order: readonly
|
|
124
|
+
}, order: readonly Claim[]): void;
|
|
138
125
|
/**
|
|
139
|
-
* Resolve once no participant holds an active
|
|
140
|
-
* The contender's "wait until it's free" — delegates to the
|
|
126
|
+
* Resolve once no participant holds an active claim on the target.
|
|
127
|
+
* The contender's "wait until it's free" — delegates to the claim
|
|
141
128
|
* stream's `waitFor`.
|
|
142
129
|
*/
|
|
143
130
|
waitFor(target: {
|
|
144
131
|
model: string;
|
|
145
132
|
id: string;
|
|
146
|
-
}, options?:
|
|
133
|
+
}, options?: ClaimWaitOptions): Promise<void>;
|
|
147
134
|
/**
|
|
148
135
|
* The local participant's id. Used to distinguish "I already hold this"
|
|
149
136
|
* from "someone else holds it" in `claimOrWait`.
|
|
@@ -191,7 +178,7 @@ export interface ClaimLookupParams<T = Record<string, unknown>> {
|
|
|
191
178
|
readonly field?: string;
|
|
192
179
|
}
|
|
193
180
|
export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLookupParams<T> {
|
|
194
|
-
readonly order: readonly
|
|
181
|
+
readonly order: readonly Claim[];
|
|
195
182
|
}
|
|
196
183
|
/**
|
|
197
184
|
* A claim handle: the held entity data plus an explicit release hook, so
|
|
@@ -217,32 +204,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
|
|
|
217
204
|
* `ablo.<model>.update({ id, data, claim })` verb — the handle carries the
|
|
218
205
|
* lease id and snapshot watermark for attribution + stale protection.
|
|
219
206
|
*/
|
|
220
|
-
export
|
|
221
|
-
readonly object: 'claim';
|
|
222
|
-
readonly claimId: string;
|
|
223
|
-
/**
|
|
224
|
-
* Sync watermark of the held snapshot (`data` was read at this stamp).
|
|
225
|
-
* Writes that carry the handle — `update({ id, data, claim })` or
|
|
226
|
-
* `commits.create({ claim, ... })` — use it as the `readAt` stale guard,
|
|
227
|
-
* so a concurrent commit between snapshot and write is rejected instead
|
|
228
|
-
* of clobbered. Optional for wire/duck-type compat with externally
|
|
229
|
-
* constructed handles.
|
|
230
|
-
*/
|
|
231
|
-
readonly readAt?: number;
|
|
232
|
-
readonly target: {
|
|
233
|
-
readonly model: string;
|
|
234
|
-
readonly id: string;
|
|
235
|
-
readonly field?: string;
|
|
236
|
-
readonly path?: string;
|
|
237
|
-
readonly range?: TargetRange;
|
|
238
|
-
readonly meta?: Record<string, unknown>;
|
|
239
|
-
};
|
|
240
|
-
readonly action: string;
|
|
241
|
-
readonly description?: string;
|
|
242
|
-
readonly data: T;
|
|
243
|
-
release(): Promise<void>;
|
|
244
|
-
revoke(): void;
|
|
245
|
-
}
|
|
207
|
+
export type { ClaimHandle };
|
|
246
208
|
export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
|
|
247
209
|
/**
|
|
248
210
|
* The coordination surface for a model, exposed as a callable namespace.
|
|
@@ -273,14 +235,14 @@ export interface ClaimApi<T> {
|
|
|
273
235
|
* Current holder for a row, or `null` when free. Use this for UI badges and
|
|
274
236
|
* preflight checks, not for the normal write path.
|
|
275
237
|
*/
|
|
276
|
-
state(params: ClaimLookupParams<T>):
|
|
238
|
+
state(params: ClaimLookupParams<T>): Claim | null;
|
|
277
239
|
/**
|
|
278
240
|
* FIFO wait line behind the current holder. Advanced: useful for operator
|
|
279
241
|
* UIs and schedulers.
|
|
280
242
|
*/
|
|
281
243
|
queue(params: ClaimLookupParams<T>): {
|
|
282
244
|
readonly object: 'list';
|
|
283
|
-
readonly data: readonly
|
|
245
|
+
readonly data: readonly Claim[];
|
|
284
246
|
};
|
|
285
247
|
/**
|
|
286
248
|
* Re-rank the wait line. Advanced and permission-gated.
|