@abloatai/ablo 0.10.1 → 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 +10 -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/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
package/dist/client/Ablo.d.ts
CHANGED
|
@@ -20,15 +20,17 @@
|
|
|
20
20
|
*/
|
|
21
21
|
import type { Schema, SchemaRecord, InferModel, InferCreate } from '../schema/schema.js';
|
|
22
22
|
import type { SyncEngineConfig, SyncLogger, MutationExecutor, MutationDispatcher, SyncObservabilityProvider, SyncAnalytics, SessionErrorDetector, OnlineStatusProvider } from '../interfaces/index.js';
|
|
23
|
+
import type { ModelTarget, ModelClaim } from '../coordination/schema.js';
|
|
24
|
+
export type { ModelTarget, ModelClaim };
|
|
23
25
|
import { ObjectPool } from '../ObjectPool.js';
|
|
24
26
|
import type { SyncStoreContract } from '../react/context.js';
|
|
25
27
|
import type { SyncWebSocket } from '../sync/SyncWebSocket.js';
|
|
26
28
|
import type { SyncGroupInput } from '../schema/roles.js';
|
|
27
29
|
import { type SyncStatus } from '../BaseSyncedStore.js';
|
|
28
|
-
import type {
|
|
30
|
+
import type { ClaimStream, ClaimWaitOptions, PresenceStream, Snapshot } from '../types/streams.js';
|
|
29
31
|
import type { ParticipantManager } from '../sync/participants.js';
|
|
30
|
-
import type {
|
|
31
|
-
import { type AbloApi, type AbloApiClientOptions, type
|
|
32
|
+
import type { ClaimHandle, Duration, Claim } from '../types/streams.js';
|
|
33
|
+
import { type AbloApi, type AbloApiClientOptions, type AbloApiClaims } from './ApiClient.js';
|
|
32
34
|
import { type AbloHttpClient, type AbloHttpClientOptions } from './httpClient.js';
|
|
33
35
|
/**
|
|
34
36
|
* Async function that resolves an apiKey at request time. Use for
|
|
@@ -384,30 +386,9 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
|
|
|
384
386
|
* `claim({ id })` — durable claim handle for coordinated writes
|
|
385
387
|
*/
|
|
386
388
|
export type { ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ModelRetrieveParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ClaimHandle, ModelOperations, } from './createModelProxy.js';
|
|
387
|
-
import type { ModelOperations, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams,
|
|
389
|
+
import type { ModelOperations, ClaimOptions, ClaimParams, ClaimLookupParams, ClaimReorderParams, ModelLoadOptions } from './createModelProxy.js';
|
|
388
390
|
export type ModelOperationAction = 'create' | 'update' | 'delete' | 'archive' | 'unarchive';
|
|
389
391
|
export type CommitWait = 'queued' | 'confirmed';
|
|
390
|
-
export interface ModelTarget {
|
|
391
|
-
/** The model name — matches `ablo.<model>` and the schema's `model()`. */
|
|
392
|
-
readonly model: string;
|
|
393
|
-
readonly id: string;
|
|
394
|
-
readonly path?: string;
|
|
395
|
-
readonly range?: TargetRange;
|
|
396
|
-
readonly field?: string;
|
|
397
|
-
readonly meta?: Record<string, unknown>;
|
|
398
|
-
}
|
|
399
|
-
export interface ModelClaim {
|
|
400
|
-
readonly id: string;
|
|
401
|
-
readonly actor: string;
|
|
402
|
-
readonly participantKind: ActiveIntent['participantKind'];
|
|
403
|
-
readonly action: string;
|
|
404
|
-
readonly description?: string;
|
|
405
|
-
readonly field?: string;
|
|
406
|
-
readonly status?: 'active' | 'queued';
|
|
407
|
-
readonly position?: number;
|
|
408
|
-
readonly expiresAt: string;
|
|
409
|
-
readonly target: ModelTarget;
|
|
410
|
-
}
|
|
411
392
|
export interface ModelRead<T = Record<string, unknown>> {
|
|
412
393
|
readonly data: T;
|
|
413
394
|
readonly stamp: number;
|
|
@@ -431,18 +412,18 @@ export interface ClaimedOptions {
|
|
|
431
412
|
*/
|
|
432
413
|
readonly maxQueueDepth?: number;
|
|
433
414
|
}
|
|
434
|
-
export type {
|
|
415
|
+
export type { ClaimWaitOptions } from '../types/streams.js';
|
|
435
416
|
export interface ModelReadOptions extends ClaimedOptions {
|
|
436
417
|
}
|
|
437
|
-
export interface
|
|
418
|
+
export interface ClaimCreateOptions {
|
|
438
419
|
readonly target: ModelTarget;
|
|
439
420
|
readonly action: string;
|
|
440
421
|
readonly ttl?: Duration;
|
|
441
422
|
/**
|
|
442
423
|
* Join the server's fair FIFO queue when the target is already claimed,
|
|
443
424
|
* rather than failing immediately. `create` then resolves only once the
|
|
444
|
-
* lease is actually ours (the server pushes `
|
|
445
|
-
* was free, or `
|
|
425
|
+
* lease is actually ours (the server pushes `claim_acquired` if the target
|
|
426
|
+
* was free, or `claim_granted` when we reach the head of the line). Without
|
|
446
427
|
* this, a contended claim throws. Used by `ablo.<model>.claim` so writers
|
|
447
428
|
* serialize instead of racing.
|
|
448
429
|
*/
|
|
@@ -455,20 +436,6 @@ export interface IntentCreateOptions {
|
|
|
455
436
|
*/
|
|
456
437
|
readonly maxQueueDepth?: number;
|
|
457
438
|
}
|
|
458
|
-
export interface IntentHandle extends AsyncDisposable {
|
|
459
|
-
readonly id: string;
|
|
460
|
-
/**
|
|
461
|
-
* True when the lease was granted AFTER waiting in the server's FIFO
|
|
462
|
-
* queue behind a holder (`intent_granted`), false/absent for an
|
|
463
|
-
* immediate grant (`intent_acquired`). Consumers re-read the target
|
|
464
|
-
* row when this is set — the row may have changed while we queued, and
|
|
465
|
-
* the local coordination snapshot can't detect that for org-scoped
|
|
466
|
-
* subscriptions (intent fan-out is entity-scoped).
|
|
467
|
-
*/
|
|
468
|
-
readonly waited?: boolean;
|
|
469
|
-
release(): Promise<void>;
|
|
470
|
-
revoke(): void;
|
|
471
|
-
}
|
|
472
439
|
export interface CommitOperationInput {
|
|
473
440
|
readonly action: ModelOperationAction;
|
|
474
441
|
/** The model name — matches `ablo.<model>` and the schema's `model()`. */
|
|
@@ -481,7 +448,7 @@ export interface CommitOperationInput {
|
|
|
481
448
|
readonly onStale?: 'reject' | 'force' | 'flag' | 'merge' | null;
|
|
482
449
|
}
|
|
483
450
|
export interface CommitCreateOptions {
|
|
484
|
-
readonly
|
|
451
|
+
readonly claimRef?: string | {
|
|
485
452
|
readonly id: string;
|
|
486
453
|
} | null;
|
|
487
454
|
readonly idempotencyKey?: string | null;
|
|
@@ -508,13 +475,13 @@ export interface CommitReceipt {
|
|
|
508
475
|
export interface CommitResource {
|
|
509
476
|
create(options: CommitCreateOptions): Promise<CommitReceipt>;
|
|
510
477
|
}
|
|
511
|
-
export interface
|
|
512
|
-
create(options:
|
|
478
|
+
export interface ClaimResource extends ClaimStream {
|
|
479
|
+
create(options: ClaimCreateOptions): Promise<ClaimHandle>;
|
|
513
480
|
list(target?: Partial<ModelTarget>): readonly ModelClaim[];
|
|
514
|
-
waitFor(target: Partial<ModelTarget>, options?:
|
|
481
|
+
waitFor(target: Partial<ModelTarget>, options?: ClaimWaitOptions): Promise<void>;
|
|
515
482
|
}
|
|
516
483
|
export interface ModelMutationOptions extends ClaimedOptions {
|
|
517
|
-
readonly
|
|
484
|
+
readonly claimRef?: string | {
|
|
518
485
|
readonly id: string;
|
|
519
486
|
} | null;
|
|
520
487
|
readonly idempotencyKey?: string | null;
|
|
@@ -537,14 +504,14 @@ export interface HttpClaimApi<T> {
|
|
|
537
504
|
* Current holder of the lease on a row, or `null` when free. For UI badges,
|
|
538
505
|
* preflight checks, and operators.
|
|
539
506
|
*/
|
|
540
|
-
state(params: ClaimLookupParams<T>): Promise<
|
|
507
|
+
state(params: ClaimLookupParams<T>): Promise<Claim | null>;
|
|
541
508
|
/**
|
|
542
509
|
* FIFO wait line behind the holder. Advanced: useful for operator UIs and
|
|
543
510
|
* schedulers.
|
|
544
511
|
*/
|
|
545
512
|
queue(params: ClaimLookupParams<T>): Promise<{
|
|
546
513
|
readonly object: 'list';
|
|
547
|
-
readonly data: readonly
|
|
514
|
+
readonly data: readonly Claim[];
|
|
548
515
|
}>;
|
|
549
516
|
/**
|
|
550
517
|
* Re-rank the wait line. Advanced and permission-gated.
|
|
@@ -859,7 +826,7 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
859
826
|
* Canonical multiplayer participant surface. Joins a structured app
|
|
860
827
|
* target, derives the transport scope internally, opens a scoped
|
|
861
828
|
* claim on the existing WebSocket, and returns target-bound presence
|
|
862
|
-
* +
|
|
829
|
+
* + claim helpers.
|
|
863
830
|
*
|
|
864
831
|
* ```ts
|
|
865
832
|
* const participant = await ablo.participants.join({
|
|
@@ -869,7 +836,7 @@ export type Ablo<S extends SchemaRecord> = {
|
|
|
869
836
|
* range: { startLine: 10, endLine: 40 },
|
|
870
837
|
* });
|
|
871
838
|
* participant.presence.editing();
|
|
872
|
-
* const claim = participant.
|
|
839
|
+
* const claim = participant.claims.claim('rewrite imports');
|
|
873
840
|
* ```
|
|
874
841
|
*/
|
|
875
842
|
readonly participants: ParticipantManager;
|
|
@@ -1000,7 +967,7 @@ import type * as _SchemaTypes from '../schema/schema.js';
|
|
|
1000
967
|
export declare namespace Ablo {
|
|
1001
968
|
type Options<S extends SchemaRecord = SchemaRecord> = AbloOptions<S>;
|
|
1002
969
|
type Api = AbloApi;
|
|
1003
|
-
type
|
|
970
|
+
type ApiClaims = AbloApiClaims;
|
|
1004
971
|
type Capability = import('./ApiClient.js').Capability;
|
|
1005
972
|
type CapabilityCreateOptions = import('./ApiClient.js').CapabilityCreateOptions;
|
|
1006
973
|
type CapabilityRecord = import('./ApiClient.js').CapabilityRecord;
|
|
@@ -1015,13 +982,13 @@ export declare namespace Ablo {
|
|
|
1015
982
|
type TargetRange = _Streams.TargetRange;
|
|
1016
983
|
type Duration = _Streams.Duration;
|
|
1017
984
|
type PresenceStream = _Streams.PresenceStream;
|
|
1018
|
-
type
|
|
985
|
+
type ClaimStream = _Streams.ClaimStream;
|
|
1019
986
|
type Peer = _Streams.Peer;
|
|
1020
987
|
type Activity = _Streams.Activity;
|
|
1021
|
-
type
|
|
1022
|
-
type
|
|
1023
|
-
type
|
|
1024
|
-
type
|
|
988
|
+
type ActiveClaim = _Streams.ActiveClaim;
|
|
989
|
+
type ClaimHandle = _Streams.ClaimHandle;
|
|
990
|
+
type ClaimRejection = _Streams.ClaimRejection;
|
|
991
|
+
type ClaimLost = _Streams.ClaimLost;
|
|
1025
992
|
type Snapshot<TSchema extends _SchemaTypes.Schema = _SchemaTypes.Schema, K extends keyof TSchema['models'] = keyof TSchema['models']> = _Streams.Snapshot<TSchema, K>;
|
|
1026
993
|
namespace Auth {
|
|
1027
994
|
type Principal = _Streams.Principal;
|
|
@@ -1057,11 +1024,11 @@ export declare namespace Ablo {
|
|
|
1057
1024
|
type Receipt = import('./Ablo.js').CommitReceipt;
|
|
1058
1025
|
type Client = import('./Ablo.js').CommitResource;
|
|
1059
1026
|
}
|
|
1060
|
-
namespace
|
|
1061
|
-
type Handle = import('./Ablo.js').
|
|
1062
|
-
type CreateOptions = import('./Ablo.js').
|
|
1063
|
-
type WaitOptions = import('./Ablo.js').
|
|
1064
|
-
type Client = import('./Ablo.js').
|
|
1027
|
+
namespace Claim {
|
|
1028
|
+
type Handle = import('./Ablo.js').ClaimHandle;
|
|
1029
|
+
type CreateOptions = import('./Ablo.js').ClaimCreateOptions;
|
|
1030
|
+
type WaitOptions = import('./Ablo.js').ClaimWaitOptions;
|
|
1031
|
+
type Client = import('./Ablo.js').ClaimResource;
|
|
1065
1032
|
}
|
|
1066
1033
|
namespace Model {
|
|
1067
1034
|
type Target = import('./Ablo.js').ModelTarget;
|
package/dist/client/Ablo.js
CHANGED
|
@@ -19,7 +19,9 @@
|
|
|
19
19
|
* await sync.reports.delete({ id: reportId });
|
|
20
20
|
*/
|
|
21
21
|
import { z } from 'zod';
|
|
22
|
-
import {
|
|
22
|
+
import { baseFieldsSchema } from '../schema/schema.js';
|
|
23
|
+
import { AbloError, AbloAuthenticationError, AbloConnectionError, AbloValidationError, translateHttpError, toAbloError, claimedError } from '../errors.js';
|
|
24
|
+
import { descriptionFromMeta } from '../coordination/schema.js';
|
|
23
25
|
import { LoadStrategy, PropertyType } from '../types/index.js';
|
|
24
26
|
import { initSyncEngine } from '../context.js';
|
|
25
27
|
import { noopObservability, browserOnlineStatus, defaultSessionErrorDetector, noopAnalytics, } from '../SyncEngineContext.js';
|
|
@@ -32,8 +34,8 @@ import { resolveParticipantIdentity } from './identity.js';
|
|
|
32
34
|
import { Model } from '../Model.js';
|
|
33
35
|
import { BaseSyncedStore } from '../BaseSyncedStore.js';
|
|
34
36
|
import { createPresenceStream } from '../sync/createPresenceStream.js';
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
+
import { createClaimStream } from '../sync/createClaimStream.js';
|
|
38
|
+
import { awaitClaimGrant } from '../sync/awaitClaimGrant.js';
|
|
37
39
|
import { createSnapshot } from '../sync/createSnapshot.js';
|
|
38
40
|
import { createParticipantManager } from '../sync/participants.js';
|
|
39
41
|
import { createProtocolClient, } from './ApiClient.js';
|
|
@@ -240,7 +242,18 @@ function registerModelsFromSchema(schema, registry) {
|
|
|
240
242
|
}
|
|
241
243
|
// Create a dynamic Model subclass with JSON sub-property getters
|
|
242
244
|
const isLazy = modelDef.lazyObservable === true;
|
|
243
|
-
|
|
245
|
+
// Base provenance fields (`organizationId`, `createdBy`) live in
|
|
246
|
+
// `baseFieldsSchema`, not the per-model `shape`. The server stamps + emits
|
|
247
|
+
// them (camelCased on the wire), but hydration (`Model.assignFieldsFromData`)
|
|
248
|
+
// only assigns keys that already exist as an own/prototype property — so
|
|
249
|
+
// without a slot here, `deck.createdBy` / `deck.organizationId` silently read
|
|
250
|
+
// `undefined` (this is why the profile decks tab showed nothing: it filters
|
|
251
|
+
// `decks.filter(d => d.createdBy === userId)`). `id`/`createdAt`/`updatedAt`
|
|
252
|
+
// are already seeded by the base Model constructor, so they're excluded.
|
|
253
|
+
const fieldNames = [
|
|
254
|
+
...Object.keys(modelDef.shape),
|
|
255
|
+
...Object.keys(baseFieldsSchema.shape).filter((f) => f !== 'id' && f !== 'createdAt' && f !== 'updatedAt' && !(f in modelDef.shape)),
|
|
256
|
+
];
|
|
244
257
|
const computed = modelDef.computed;
|
|
245
258
|
const DynamicModel = createDynamicModelClass(modelName, jsonSubFields, fieldNames, computed, isLazy);
|
|
246
259
|
// Respect the schema's load strategy so lazy models skip IDB hydration + bootstrap
|
|
@@ -821,8 +834,8 @@ export function Ablo(options) {
|
|
|
821
834
|
// becomes null, so the first Ablo's commits start throwing
|
|
822
835
|
// `ws_not_ready` forever (terminal AgentJob writes hang on retry).
|
|
823
836
|
syncClient.getTransactionQueue().setMutationExecutor(executor);
|
|
824
|
-
// Presence +
|
|
825
|
-
// and `engine.
|
|
837
|
+
// Presence + claim streams — built eagerly so `engine.presence`
|
|
838
|
+
// and `engine.claims` return the same reference for the engine's
|
|
826
839
|
// lifetime. The transport doesn't exist yet (BaseSyncedStore.initialize
|
|
827
840
|
// creates it during ready()), so both streams are constructed in
|
|
828
841
|
// deferred-attach mode and wired after initialize() resolves below.
|
|
@@ -837,12 +850,12 @@ export function Ablo(options) {
|
|
|
837
850
|
syncGroups: internalOptions.syncGroups ?? [],
|
|
838
851
|
isAgent: internalOptions.kind === 'agent',
|
|
839
852
|
});
|
|
840
|
-
const
|
|
853
|
+
const claimStream = createClaimStream({ participantId });
|
|
841
854
|
const participantManager = createParticipantManager({
|
|
842
855
|
ready,
|
|
843
856
|
getTransport: () => store.getSyncWebSocket() ?? null,
|
|
844
857
|
presence: presenceStream,
|
|
845
|
-
|
|
858
|
+
claims: claimStream,
|
|
846
859
|
schema,
|
|
847
860
|
});
|
|
848
861
|
// 6. Validate options up front — fail loudly on obviously wrong inputs so
|
|
@@ -990,14 +1003,14 @@ export function Ablo(options) {
|
|
|
990
1003
|
code: 'bootstrap_fetch_timeout',
|
|
991
1004
|
});
|
|
992
1005
|
}
|
|
993
|
-
// Wire presence +
|
|
1006
|
+
// Wire presence + claims to the now-open transport.
|
|
994
1007
|
// `getSyncWebSocket()` returns non-null after a successful
|
|
995
1008
|
// initialize() — the WS is created during the generator's
|
|
996
1009
|
// connect step.
|
|
997
1010
|
const ws = store.getSyncWebSocket();
|
|
998
1011
|
if (ws) {
|
|
999
1012
|
presenceStream.attach(ws);
|
|
1000
|
-
|
|
1013
|
+
claimStream.attach(ws);
|
|
1001
1014
|
}
|
|
1002
1015
|
logger.info('Sync engine ready', { models: Object.keys(schema.models).length });
|
|
1003
1016
|
}
|
|
@@ -1073,10 +1086,10 @@ export function Ablo(options) {
|
|
|
1073
1086
|
? crypto.randomUUID()
|
|
1074
1087
|
: `id_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1075
1088
|
}
|
|
1076
|
-
function
|
|
1077
|
-
if (typeof
|
|
1078
|
-
return
|
|
1079
|
-
return
|
|
1089
|
+
function normalizeClaimId(claim) {
|
|
1090
|
+
if (typeof claim === 'string')
|
|
1091
|
+
return claim;
|
|
1092
|
+
return claim?.id;
|
|
1080
1093
|
}
|
|
1081
1094
|
function isClaimHandleValue(value) {
|
|
1082
1095
|
return (typeof value === 'object' &&
|
|
@@ -1113,82 +1126,72 @@ export function Ablo(options) {
|
|
|
1113
1126
|
}
|
|
1114
1127
|
return inputOperations.map((op) => normalizeCommitOperation(op, commitOptions));
|
|
1115
1128
|
}
|
|
1116
|
-
function modelClaimFromActive(
|
|
1117
|
-
const description =
|
|
1118
|
-
? intent.target.meta.description
|
|
1119
|
-
: undefined;
|
|
1129
|
+
function modelClaimFromActive(claim) {
|
|
1130
|
+
const description = descriptionFromMeta(claim.target.meta);
|
|
1120
1131
|
return {
|
|
1121
|
-
id:
|
|
1122
|
-
actor:
|
|
1123
|
-
participantKind:
|
|
1124
|
-
action:
|
|
1132
|
+
id: claim.id,
|
|
1133
|
+
actor: claim.heldBy,
|
|
1134
|
+
participantKind: claim.participantKind,
|
|
1135
|
+
action: claim.reason,
|
|
1125
1136
|
...(description ? { description } : {}),
|
|
1126
|
-
field:
|
|
1137
|
+
field: claim.target.field,
|
|
1127
1138
|
status: 'active',
|
|
1128
|
-
expiresAt:
|
|
1139
|
+
expiresAt: claim.expiresAt,
|
|
1129
1140
|
target: {
|
|
1130
|
-
model:
|
|
1131
|
-
id:
|
|
1132
|
-
path:
|
|
1133
|
-
range:
|
|
1134
|
-
field:
|
|
1135
|
-
meta:
|
|
1141
|
+
model: claim.target.type,
|
|
1142
|
+
id: claim.target.id,
|
|
1143
|
+
path: claim.target.path,
|
|
1144
|
+
range: claim.target.range,
|
|
1145
|
+
field: claim.target.field,
|
|
1146
|
+
meta: claim.target.meta,
|
|
1136
1147
|
},
|
|
1137
1148
|
};
|
|
1138
1149
|
}
|
|
1139
|
-
function modelClaimFromQueued(
|
|
1150
|
+
function modelClaimFromQueued(claim) {
|
|
1140
1151
|
return {
|
|
1141
|
-
id:
|
|
1142
|
-
actor:
|
|
1143
|
-
participantKind:
|
|
1144
|
-
action:
|
|
1145
|
-
...(
|
|
1146
|
-
field:
|
|
1152
|
+
id: claim.id,
|
|
1153
|
+
actor: claim.heldBy,
|
|
1154
|
+
participantKind: claim.participantKind,
|
|
1155
|
+
action: claim.action,
|
|
1156
|
+
...(claim.description ? { description: claim.description } : {}),
|
|
1157
|
+
field: claim.target.field,
|
|
1147
1158
|
status: 'queued',
|
|
1148
|
-
position:
|
|
1149
|
-
expiresAt:
|
|
1159
|
+
position: claim.position,
|
|
1160
|
+
expiresAt: claim.expiresAt,
|
|
1150
1161
|
target: {
|
|
1151
|
-
model:
|
|
1152
|
-
id:
|
|
1153
|
-
path:
|
|
1154
|
-
range:
|
|
1155
|
-
field:
|
|
1156
|
-
meta:
|
|
1162
|
+
model: claim.target.type,
|
|
1163
|
+
id: claim.target.id,
|
|
1164
|
+
path: claim.target.path,
|
|
1165
|
+
range: claim.target.range,
|
|
1166
|
+
field: claim.target.field,
|
|
1167
|
+
meta: claim.target.meta,
|
|
1157
1168
|
},
|
|
1158
1169
|
};
|
|
1159
1170
|
}
|
|
1160
|
-
function targetMatchesModel(target,
|
|
1171
|
+
function targetMatchesModel(target, claim) {
|
|
1161
1172
|
if (target.model &&
|
|
1162
|
-
|
|
1173
|
+
claim.target.type.toLowerCase() !== target.model.toLowerCase()) {
|
|
1163
1174
|
return false;
|
|
1164
1175
|
}
|
|
1165
|
-
if (target.id &&
|
|
1176
|
+
if (target.id && claim.target.id !== target.id)
|
|
1166
1177
|
return false;
|
|
1167
|
-
if (target.field &&
|
|
1178
|
+
if (target.field && claim.target.field !== target.field)
|
|
1168
1179
|
return false;
|
|
1169
1180
|
return true;
|
|
1170
1181
|
}
|
|
1171
1182
|
function listModelClaims(target) {
|
|
1172
|
-
return
|
|
1173
|
-
.filter((
|
|
1183
|
+
return claimStream.others
|
|
1184
|
+
.filter((claim) => (target ? targetMatchesModel(target, claim) : true))
|
|
1174
1185
|
.map(modelClaimFromActive);
|
|
1175
1186
|
}
|
|
1176
1187
|
function listModelClaimQueue(target) {
|
|
1177
1188
|
if (!target?.model || !target.id)
|
|
1178
1189
|
return [];
|
|
1179
|
-
return
|
|
1190
|
+
return publicClaims
|
|
1180
1191
|
.queueFor({ type: target.model, id: target.id })
|
|
1181
|
-
.filter((
|
|
1192
|
+
.filter((claim) => (target.field ? claim.target.field === target.field : true))
|
|
1182
1193
|
.map(modelClaimFromQueued);
|
|
1183
1194
|
}
|
|
1184
|
-
function claimedError(target, claims, code) {
|
|
1185
|
-
const label = [target.model, target.id, target.field].filter(Boolean).join('/');
|
|
1186
|
-
const holder = claims[0];
|
|
1187
|
-
const suffix = holder
|
|
1188
|
-
? ` held by ${holder.actor} (${holder.action})`
|
|
1189
|
-
: ' held by another participant';
|
|
1190
|
-
return new AbloClaimedError(`Model row is claimed: ${label || 'target'}${suffix}.`, { code, claims });
|
|
1191
|
-
}
|
|
1192
1195
|
function waitForModelUnclaimed(target, options) {
|
|
1193
1196
|
if (listModelClaims(target).length === 0)
|
|
1194
1197
|
return Promise.resolve();
|
|
@@ -1216,8 +1219,8 @@ export function Ablo(options) {
|
|
|
1216
1219
|
}
|
|
1217
1220
|
};
|
|
1218
1221
|
const onAbort = () => {
|
|
1219
|
-
finish(() => reject(new AbloConnectionError('
|
|
1220
|
-
code: '
|
|
1222
|
+
finish(() => reject(new AbloConnectionError('Claim wait aborted.', {
|
|
1223
|
+
code: 'claim_wait_aborted',
|
|
1221
1224
|
cause: options?.signal?.reason,
|
|
1222
1225
|
})));
|
|
1223
1226
|
};
|
|
@@ -1225,7 +1228,7 @@ export function Ablo(options) {
|
|
|
1225
1228
|
onAbort();
|
|
1226
1229
|
return;
|
|
1227
1230
|
}
|
|
1228
|
-
unsubscribe =
|
|
1231
|
+
unsubscribe = claimStream.onChange(check);
|
|
1229
1232
|
options?.signal?.addEventListener('abort', onAbort, { once: true });
|
|
1230
1233
|
if (options?.timeout != null) {
|
|
1231
1234
|
timeoutId = setTimeout(() => {
|
|
@@ -1250,58 +1253,61 @@ export function Ablo(options) {
|
|
|
1250
1253
|
}
|
|
1251
1254
|
await waitForModelUnclaimed(target, { timeout: options?.claimedTimeout });
|
|
1252
1255
|
}
|
|
1253
|
-
function
|
|
1256
|
+
function wrapClaimHandle(claim, waited = false) {
|
|
1254
1257
|
const release = async () => {
|
|
1255
1258
|
claim.revoke();
|
|
1256
1259
|
};
|
|
1257
1260
|
return {
|
|
1258
|
-
|
|
1261
|
+
object: 'claim',
|
|
1262
|
+
claimId: claim.claimId,
|
|
1263
|
+
action: claim.action,
|
|
1264
|
+
target: claim.target,
|
|
1259
1265
|
waited,
|
|
1260
1266
|
release,
|
|
1261
1267
|
revoke: claim.revoke,
|
|
1262
1268
|
[Symbol.asyncDispose]: release,
|
|
1263
1269
|
};
|
|
1264
1270
|
}
|
|
1265
|
-
const
|
|
1266
|
-
async create(
|
|
1271
|
+
const publicClaims = Object.assign(claimStream, {
|
|
1272
|
+
async create(claimOptions) {
|
|
1267
1273
|
await ready();
|
|
1268
|
-
const claim =
|
|
1269
|
-
type:
|
|
1270
|
-
id:
|
|
1271
|
-
path:
|
|
1272
|
-
range:
|
|
1273
|
-
field:
|
|
1274
|
-
meta:
|
|
1274
|
+
const claim = claimStream.claim({
|
|
1275
|
+
type: claimOptions.target.model,
|
|
1276
|
+
id: claimOptions.target.id,
|
|
1277
|
+
path: claimOptions.target.path,
|
|
1278
|
+
range: claimOptions.target.range,
|
|
1279
|
+
field: claimOptions.target.field,
|
|
1280
|
+
meta: claimOptions.target.meta,
|
|
1275
1281
|
}, {
|
|
1276
|
-
reason:
|
|
1277
|
-
ttl:
|
|
1278
|
-
queue:
|
|
1282
|
+
reason: claimOptions.action,
|
|
1283
|
+
ttl: claimOptions.ttl,
|
|
1284
|
+
queue: claimOptions.queue,
|
|
1279
1285
|
});
|
|
1280
1286
|
// With `queue`, the claim is only really *ours* once the server says
|
|
1281
|
-
// so (`
|
|
1287
|
+
// so (`claim_acquired` if the target was free, `claim_granted` once
|
|
1282
1288
|
// we reach the head of the FIFO line). Block here on that grant so
|
|
1283
1289
|
// callers — chiefly `ablo.<model>.claim` — get a handle that already
|
|
1284
1290
|
// holds the lease, never a half-claimed one racing the queue.
|
|
1285
1291
|
let waited = false;
|
|
1286
|
-
if (
|
|
1292
|
+
if (claimOptions.queue) {
|
|
1287
1293
|
const ws = store.getSyncWebSocket();
|
|
1288
1294
|
if (ws) {
|
|
1289
1295
|
try {
|
|
1290
|
-
({ waited } = await
|
|
1291
|
-
timeoutMs:
|
|
1292
|
-
maxQueueDepth:
|
|
1296
|
+
({ waited } = await awaitClaimGrant(ws, claim.claimId, {
|
|
1297
|
+
timeoutMs: claimOptions.waitTimeoutMs,
|
|
1298
|
+
maxQueueDepth: claimOptions.maxQueueDepth,
|
|
1293
1299
|
}));
|
|
1294
1300
|
}
|
|
1295
1301
|
catch (err) {
|
|
1296
1302
|
// Gave up waiting (queue too deep, timed out, or lost) — abandon
|
|
1297
|
-
// the queued
|
|
1303
|
+
// the queued claim so we don't leave a phantom entry in the
|
|
1298
1304
|
// line that would block or mislead other claimers.
|
|
1299
1305
|
claim.revoke();
|
|
1300
1306
|
throw err;
|
|
1301
1307
|
}
|
|
1302
1308
|
}
|
|
1303
1309
|
}
|
|
1304
|
-
return
|
|
1310
|
+
return wrapClaimHandle(claim, waited);
|
|
1305
1311
|
},
|
|
1306
1312
|
list(target) {
|
|
1307
1313
|
return listModelClaims(target);
|
|
@@ -1310,14 +1316,14 @@ export function Ablo(options) {
|
|
|
1310
1316
|
return waitForModelUnclaimed(target, options);
|
|
1311
1317
|
},
|
|
1312
1318
|
});
|
|
1313
|
-
// Build the typed proxy — one property per model. Done after
|
|
1319
|
+
// Build the typed proxy — one property per model. Done after publicClaims
|
|
1314
1320
|
// exists so model clients can expose workflow helpers such as
|
|
1315
1321
|
// `ablo.files.edit(...)` without importing protocol wiring.
|
|
1316
1322
|
const modelProxies = {};
|
|
1317
1323
|
for (const [schemaKey, modelDef] of Object.entries(schema.models)) {
|
|
1318
1324
|
const registeredModelName = modelDef.typename ?? schemaKey;
|
|
1319
1325
|
modelProxies[schemaKey] = createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, modelRegistry, hydration, {
|
|
1320
|
-
|
|
1326
|
+
createClaim: (claimOptions) => publicClaims.create(claimOptions),
|
|
1321
1327
|
createSnapshot: (modelKey, id) => createSnapshot({
|
|
1322
1328
|
pool: objectPool,
|
|
1323
1329
|
transport: store.getSyncWebSocket(),
|
|
@@ -1331,21 +1337,21 @@ export function Ablo(options) {
|
|
|
1331
1337
|
getLastSyncId: () => syncClient.position.readFloor,
|
|
1332
1338
|
entities: { [modelKey]: id },
|
|
1333
1339
|
}),
|
|
1334
|
-
queue: (target) =>
|
|
1335
|
-
reorder: (target, order) =>
|
|
1340
|
+
queue: (target) => publicClaims.queueFor({ type: target.model, id: target.id }),
|
|
1341
|
+
reorder: (target, order) => publicClaims.reorder({ type: target.model, id: target.id }, order),
|
|
1336
1342
|
observe: (target) => {
|
|
1337
|
-
// The live
|
|
1343
|
+
// The live claim stream only tracks *open* (active) claims;
|
|
1338
1344
|
// terminal states (committed / expired / canceled) drop out of
|
|
1339
1345
|
// the list entirely — exactly the ephemeral coordination model.
|
|
1340
1346
|
// So a present entry is, by definition, `status: 'active'`.
|
|
1341
|
-
const held =
|
|
1347
|
+
const held = publicClaims.list({
|
|
1342
1348
|
model: target.model,
|
|
1343
1349
|
id: target.id,
|
|
1344
1350
|
})[0];
|
|
1345
1351
|
if (!held)
|
|
1346
1352
|
return null;
|
|
1347
1353
|
return {
|
|
1348
|
-
object: '
|
|
1354
|
+
object: 'claim',
|
|
1349
1355
|
id: held.id,
|
|
1350
1356
|
status: 'active',
|
|
1351
1357
|
target: {
|
|
@@ -1362,7 +1368,7 @@ export function Ablo(options) {
|
|
|
1362
1368
|
expiresAt: held.expiresAt,
|
|
1363
1369
|
};
|
|
1364
1370
|
},
|
|
1365
|
-
waitFor: (target, waitOptions) =>
|
|
1371
|
+
waitFor: (target, waitOptions) => publicClaims.waitFor({ model: target.model, id: target.id }, waitOptions),
|
|
1366
1372
|
selfParticipantId: participantId,
|
|
1367
1373
|
});
|
|
1368
1374
|
}
|
|
@@ -1375,7 +1381,7 @@ export function Ablo(options) {
|
|
|
1375
1381
|
readAt: commitOptions.readAt,
|
|
1376
1382
|
onStale: commitOptions.onStale,
|
|
1377
1383
|
wait: commitOptions.wait,
|
|
1378
|
-
|
|
1384
|
+
claim: commitOptions.claim,
|
|
1379
1385
|
}, 'commits.create');
|
|
1380
1386
|
const clientTxId = createClientTxId(commitOptions.idempotencyKey);
|
|
1381
1387
|
// A claim handle supplies the batch stale-guard defaults — same
|
|
@@ -1388,8 +1394,8 @@ export function Ablo(options) {
|
|
|
1388
1394
|
onStale: commitOptions.onStale ?? (claim?.readAt !== undefined ? 'reject' : null),
|
|
1389
1395
|
});
|
|
1390
1396
|
const wait = commitOptions.wait ?? 'confirmed';
|
|
1391
|
-
const
|
|
1392
|
-
void
|
|
1397
|
+
const claimId = normalizeClaimId(commitOptions.claimRef) ?? claim?.claimId;
|
|
1398
|
+
void claimId; // The current wire clears claims by entity after commit.
|
|
1393
1399
|
// Route through the TransactionQueue's commit lane so the call
|
|
1394
1400
|
// tolerates WS disconnects: the envelope stays in memory until
|
|
1395
1401
|
// reconnect, mutationExecutor.commit() owns transport-level
|
|
@@ -1462,7 +1468,7 @@ export function Ablo(options) {
|
|
|
1462
1468
|
const id = params.id ?? createModelId();
|
|
1463
1469
|
await applyClaimedPolicy({ model: name, id }, params);
|
|
1464
1470
|
return commits.create({
|
|
1465
|
-
|
|
1471
|
+
claimRef: params.claimRef,
|
|
1466
1472
|
idempotencyKey: params.idempotencyKey,
|
|
1467
1473
|
readAt: params.readAt,
|
|
1468
1474
|
onStale: params.onStale,
|
|
@@ -1481,7 +1487,7 @@ export function Ablo(options) {
|
|
|
1481
1487
|
async update(params) {
|
|
1482
1488
|
await applyClaimedPolicy({ model: name, id: params.id }, params);
|
|
1483
1489
|
return commits.create({
|
|
1484
|
-
|
|
1490
|
+
claimRef: params.claimRef,
|
|
1485
1491
|
idempotencyKey: params.idempotencyKey,
|
|
1486
1492
|
readAt: params.readAt,
|
|
1487
1493
|
onStale: params.onStale,
|
|
@@ -1500,7 +1506,7 @@ export function Ablo(options) {
|
|
|
1500
1506
|
async delete(params) {
|
|
1501
1507
|
await applyClaimedPolicy({ model: name, id: params.id }, params);
|
|
1502
1508
|
return commits.create({
|
|
1503
|
-
|
|
1509
|
+
claimRef: params.claimRef,
|
|
1504
1510
|
idempotencyKey: params.idempotencyKey,
|
|
1505
1511
|
readAt: params.readAt,
|
|
1506
1512
|
onStale: params.onStale,
|
|
@@ -1659,7 +1665,7 @@ export function Ablo(options) {
|
|
|
1659
1665
|
logger.warn('Error during sync engine disposal', { error: err.message });
|
|
1660
1666
|
}
|
|
1661
1667
|
presenceStream.dispose();
|
|
1662
|
-
|
|
1668
|
+
claimStream.dispose();
|
|
1663
1669
|
syncClient.dispose();
|
|
1664
1670
|
},
|
|
1665
1671
|
/**
|
|
@@ -1711,8 +1717,8 @@ export function Ablo(options) {
|
|
|
1711
1717
|
/** Presence livestream — same socket as entity sync, no second
|
|
1712
1718
|
* connection. Stable reference across the engine's lifetime. */
|
|
1713
1719
|
presence: presenceStream,
|
|
1714
|
-
/**
|
|
1715
|
-
|
|
1720
|
+
/** Claim livestream — same socket. Stable reference. */
|
|
1721
|
+
claims: publicClaims,
|
|
1716
1722
|
commits,
|
|
1717
1723
|
model,
|
|
1718
1724
|
/** Structured multiplayer participation — target-first, no
|