@abloatai/humans 0.59.2 → 0.60.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +1 -1
  2. package/dist/Ablo.d.ts +2 -10
  3. package/dist/Ablo.js +0 -1
  4. package/dist/client.d.ts +1 -48
  5. package/dist/humans.d.ts +1 -1
  6. package/dist/local/BaseSyncedStore.d.ts +5 -5
  7. package/dist/local/BaseSyncedStore.js +2 -2
  8. package/dist/local/Database.d.ts +2 -2
  9. package/dist/local/LazyReferenceCollection.d.ts +1 -1
  10. package/dist/local/SyncClient.d.ts +7 -7
  11. package/dist/local/SyncClient.js +25 -10
  12. package/dist/local/client/createModelOperations.d.ts +3 -27
  13. package/dist/local/client/createModelOperations.js +14 -17
  14. package/dist/local/client/options.d.ts +14 -39
  15. package/dist/local/client/reactiveEngine.d.ts +3 -9
  16. package/dist/local/client/reactiveEngine.js +6 -151
  17. package/dist/local/client/storeLifecycle.js +5 -1
  18. package/dist/local/storeContract.d.ts +5 -5
  19. package/dist/local/stores/syncAction.d.ts +4 -4
  20. package/dist/local/sync/credentialLifecycle.d.ts +4 -5
  21. package/dist/local/sync/credentialLifecycle.js +4 -5
  22. package/dist/local/sync/schemas.d.ts +10 -10
  23. package/dist/local/sync/scopeGroups.d.ts +11 -0
  24. package/dist/local/sync/scopeGroups.js +75 -0
  25. package/dist/local/sync/wsFrameHandlers.d.ts +1 -1
  26. package/dist/local/transactions/mutations/MutationQueue.d.ts +3 -3
  27. package/dist/local/transactions/mutations/commitPayload.d.ts +1 -1
  28. package/dist/local/transactions/mutations/replayValidation.d.ts +15 -15
  29. package/dist/react/AbloProvider.d.ts +11 -86
  30. package/dist/react/AbloProvider.js +11 -163
  31. package/dist/react/ClientSideSuspense.d.ts +1 -1
  32. package/dist/react/DefaultFallback.d.ts +1 -1
  33. package/dist/react/createAbloReact.js +1 -1
  34. package/dist/react.d.ts +1 -1
  35. package/dist/react.js +1 -1
  36. package/dist/surface.d.ts +3 -3
  37. package/dist/surface.js +1 -4
  38. package/package.json +3 -2
  39. package/src/Ablo.ts +5 -17
  40. package/src/client.ts +0 -51
  41. package/src/local/BaseSyncedStore.ts +9 -9
  42. package/src/local/SyncClient.ts +41 -14
  43. package/src/local/client/createModelOperations.ts +23 -60
  44. package/src/local/client/options.ts +20 -43
  45. package/src/local/client/reactiveEngine.ts +7 -179
  46. package/src/local/client/storeLifecycle.ts +6 -1
  47. package/src/local/storeContract.ts +5 -5
  48. package/src/local/sync/credentialLifecycle.ts +4 -5
  49. package/src/local/sync/scopeGroups.ts +91 -0
  50. package/src/local/sync/wsFrameHandlers.ts +0 -1
  51. package/src/react/AbloProvider.tsx +17 -249
  52. package/src/react.ts +1 -5
  53. package/src/surface.ts +1 -4
  54. package/dist/local/sync/participants.d.ts +0 -132
  55. package/dist/local/sync/participants.js +0 -342
  56. package/src/local/sync/participants.ts +0 -564
@@ -12,21 +12,9 @@ import {
12
12
  } from 'react';
13
13
  import type { Schema, SchemaRecord } from '@abloatai/transaction/schema/schema';
14
14
  import type { AbloClient as Ablo } from '../client.js';
15
- import type {
16
- Claim,
17
- Duration,
18
- Peer,
19
- } from '@abloatai/transaction/types/streams';
20
- import type {
21
- EngineParticipant,
22
- ParticipantScope,
23
- ParticipantStatus,
24
- } from '../local/sync/participants.js';
25
- import {
26
- createParticipantClaimId,
27
- parseParticipantTtlSeconds,
28
- resolveParticipantSyncGroups,
29
- } from '../local/sync/participants.js';
15
+ import type { Peer } from '@abloatai/transaction/types/streams';
16
+ import type { GroupScope } from '../local/sync/scopeGroups.js';
17
+ import { resolveScopeGroups } from '../local/sync/scopeGroups.js';
30
18
  import { SyncContext, type SyncStoreContract } from './context.js';
31
19
  import { AbloInternalContext, type AbloInternalContextValue } from './internalContext.js';
32
20
  import { AbloValidationError } from '@abloatai/transaction/errors';
@@ -42,9 +30,8 @@ import { DefaultFallback } from './DefaultFallback.js';
42
30
  *
43
31
  * - **One component, one import.** Consumers write the provider
44
32
  * once at the root; nothing else needs to plumb the engine.
45
- * - **Multiplayer is default.** React consumers are always browsers doing
46
- * multiplayer UI, so `useJoin()` / `useAblo()` are always
47
- * available. No opt-in prop.
33
+ * - **Multiplayer is default.** React consumers share the client's scoped
34
+ * groups, presence stream, and model surface without another join step.
48
35
  * - **Declarative props for app glue.** `preventUnsavedChanges`,
49
36
  * `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
50
37
  * absorbs a class of integration code that previously lived in
@@ -67,7 +54,7 @@ import { DefaultFallback } from './DefaultFallback.js';
67
54
  * // Build once at module scope — a new instance per render tears down the socket.
68
55
  * // The endpoint string points at your session-mint route (`ablo init`
69
56
  * // scaffolds it); the SDK fetches it and keeps the token fresh.
70
- * const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
57
+ * const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
71
58
  *
72
59
  * <AbloProvider client={ablo}>
73
60
  * <App />
@@ -393,229 +380,14 @@ function BootstrapGate({
393
380
  }
394
381
 
395
382
 
396
- export type { EngineParticipant, ParticipantScope, ParticipantStatus };
397
-
398
- /**
399
- * Options for `useJoin`. The hook reuses the engine's single
400
- * WebSocket and opens a scoped claim on it when `scope` is provided:
401
- * one TCP connection, N logical sub-syncgroup participants.
402
- */
403
- export interface UseJoinOptions {
404
- readonly scope?: ParticipantScope;
405
- /**
406
- * Lease TTL for the participant claim, as a compact duration (`'5m'`) or a
407
- * number of seconds. The same dial and the same spelling as
408
- * `ablo.<model>.join(ids, { ttl })` and every other lease in the SDK.
409
- */
410
- readonly ttl?: Duration;
411
- /**
412
- * @deprecated Use `ttl`. Removed in 0.37.0.
413
- *
414
- * The same rename as on `ParticipantJoinOptions`: one lease, and the seconds
415
- * spelling was the one that misled, because it accepted duration strings too.
416
- */
417
- readonly ttlSeconds?: number | string | null;
418
- /** Tear down + don't re-join while true. */
419
- readonly paused?: boolean;
420
- /**
421
- * Acquire a write-claim CLAIM on the scope, in addition to read interest.
422
- *
423
- * Default `false`: opening a scope subscribes the connection to its deltas
424
- * (read interest, via `update_subscription`) but does NOT claim it — a
425
- * viewer is not a claimant. Set `true` when the participant intends to
426
- * WRITE (editing a report, an agent staking work): the claim is sent so peers
427
- * observe it, and the scope is pinned so it stays subscribed (never warms)
428
- * for as long as the claim is held.
429
- */
430
- readonly claim?: boolean;
431
- /**
432
- * Backfill the scope's CURRENT state into the pool on enter, in addition to
433
- * tailing live changes.
434
- *
435
- * Default `false`: entering a scope subscribes to its FUTURE deltas only — if
436
- * the scope's rows aren't already loaded, the view is empty until something
437
- * changes. Set `true` when opening an entity that may not be loaded yet (a
438
- * deep-linked report, a never-opened ledger) so its current rows are fetched and
439
- * injected once, then kept fresh by the live tail. The fetch is single-flight
440
- * and runs once per group; a failure soft-fails (the live tail still flows).
441
- */
442
- readonly hydrate?: boolean;
443
- }
444
-
445
- export interface UseJoinReturn {
446
- readonly participant: EngineParticipant | null;
447
- /** Everyone else on the engine's sync groups (`participant.presence.others`), bridged to React. */
448
- readonly peers: readonly Peer[];
449
- /** Active claim claims by peers (`participant.claims.others`), bridged to React. */
450
- readonly claims: readonly Claim[];
451
- readonly status: ParticipantStatus;
452
- readonly error: Error | null;
453
- }
454
-
455
383
  const EMPTY_PRESENCE: readonly Peer[] = Object.freeze([]);
456
- const EMPTY_INTENTS: readonly Claim[] = Object.freeze([]);
457
384
 
458
- /**
459
- * Join multiplayer for a given scope. Returns the participant and its
460
- * lifecycle status. Auto-cleans up on unmount or when `paused`
461
- * flips to true.
462
- *
463
- * `useJoin` is the React form of `ablo.<model>.join` — scope-level
464
- * read-interest + presence; returns the reactive participant facade
465
- * (peers/claims/status).
466
- *
467
- * The returned `participant` is an `EngineParticipant` — `.presence`
468
- * + `.claims` only — backed by the engine's existing socket. For
469
- * headless-bot patterns (a separate identity in the same browser
470
- * tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
471
- */
472
- export function useJoin(opts: UseJoinOptions): UseJoinReturn {
473
- const ctx = useContext(AbloInternalContext);
474
- const engine = ctx?.engine ?? null;
475
- const { paused = false } = opts;
476
- // Resolve the model-form scope ({ reports: id } / refs) THROUGH the schema, so a
477
- // model's declared `scope` kind is honored (typename `Report` → `report:<id>`,
478
- // not the `type:id` string fallback). Schema appears once the engine is ready;
479
- // until then refs resolve by convention, then re-resolve when it arrives.
480
- const scopeKey = JSON.stringify(
481
- resolveParticipantSyncGroups(opts.scope, engine?.schema).sort(),
482
- );
483
- const scopedSyncGroups = useMemo(
484
- () => JSON.parse(scopeKey) as string[],
485
- [scopeKey],
486
- );
487
- const [claimError, setClaimError] = useState<Error | null>(null);
488
- const [claimConnected, setClaimConnected] = useState(false);
489
-
490
- // Reference-stable participant facade — same socket as entity sync,
491
- // so there is no `connect()` / `disconnect()` lifecycle here. The
492
- // engine manages the connection; the hook is a thin window onto its
493
- // already-attached presence + claim streams.
494
- const participant: EngineParticipant | null = useMemo(() => {
495
- if (!engine) return null;
496
- return { presence: engine.presence, claims: engine.claims };
497
- }, [engine]);
498
-
499
- // Status maps to the engine's sync state. `connecting` while the
500
- // engine bootstraps; `connected` once `engine.ready()` resolves and
501
- // any scoped participant claim has acked; `error` if the claim
502
- // fails; `disconnected` while paused or before the engine exists.
503
- const syncStatus = useSyncStatus();
504
- // Only a write-claim participant waits on a claim ack. A pure reader
505
- // (the default) is `connected` as soon as the engine is — its read
506
- // interest is fire-and-forget `update_subscription`, not a claim.
507
- const needsClaim = !!opts.claim && scopedSyncGroups.length > 0;
508
- const status: ParticipantStatus = paused || !engine
509
- ? 'disconnected'
510
- : claimError
511
- ? 'error'
512
- : syncStatus.name === 'connected'
513
- ? needsClaim && !claimConnected
514
- ? 'connecting'
515
- : 'connected'
516
- : syncStatus.name === 'disconnected' || syncStatus.name === 'needs-auth'
517
- ? 'disconnected'
518
- : 'connecting';
519
- const error: Error | null = claimError;
520
-
521
- // ── Read interest (always) ───────────────────────────────────────
522
- // Subscribe the connection to the scope's sync groups while mounted +
523
- // connected — the area-of-interest navigation primitive. No claim, no
524
- // TTL: a viewer just receives the scope's deltas. Hysteresis (warm TTL)
525
- // lives in the store's SubscriptionManager, so a quick unmount/remount
526
- // (tab flip) doesn't re-bootstrap.
527
- useEffect(() => {
528
- const scope = opts.scope;
529
- if (paused || !engine || !scope || scopedSyncGroups.length === 0) return;
530
- if (syncStatus.name !== 'connected') return;
531
- const store = engine._store;
532
- // `hydrate` backfills the scope's current state after subscribing
533
- // (store handles subscribe-first ordering + single-flight). leaveScope
534
- // only moves read interest; the hydrated rows stay in the pool.
535
- void store.enterScope?.(scope, { hydrate: opts.hydrate });
536
- return () => {
537
- void store.leaveScope?.(scope);
538
- };
539
- // scopeKey is the stable proxy for the resolved groups; same idiom as
540
- // the claim effect below.
541
- }, [engine, paused, scopeKey, syncStatus.name, opts.hydrate]);
542
-
543
- // ── Write claim (opt-in: `claim: true`) ─────────────────────────
544
- // A claim is the write-claim primitive — distinct from read interest
545
- // above. Only sent when the caller opts in; it makes peers observe the
546
- // claim and pins the scope so it never warms while held.
547
- useEffect(() => {
548
- setClaimError(null);
549
- setClaimConnected(false);
550
- const scope = opts.scope;
551
- if (paused || !engine || !opts.claim || !scope || scopedSyncGroups.length === 0)
552
- return;
553
- if (syncStatus.name !== 'connected') return;
554
- const ws = engine._ws;
555
- const store = engine._store;
556
-
557
- let cancelled = false;
558
- const claimId = createParticipantClaimId();
559
- ws.sendClaim(claimId, scopedSyncGroups, {
560
- // Reading the retired spelling IS the compatibility path; it goes at 0.37.0.
561
- // eslint-disable-next-line @typescript-eslint/no-deprecated
562
- ttlSeconds: parseParticipantTtlSeconds(opts.ttl ?? opts.ttlSeconds),
563
- })
564
- .then(() => {
565
- if (!cancelled) setClaimConnected(true);
566
- })
567
- .catch((err) => {
568
- if (!cancelled) {
569
- setClaimError(err instanceof Error ? err : new Error(String(err)));
570
- }
571
- });
572
- // Prominence: hold the scope subscribed for as long as the claim lives.
573
- void store.pinScope?.(scope);
574
-
575
- return () => {
576
- cancelled = true;
577
- ws.sendRelease(claimId);
578
- void store.unpinScope?.(scope);
579
- };
580
- // eslint-disable-next-line @typescript-eslint/no-deprecated -- same compatibility read as above.
581
- }, [engine, paused, scopeKey, syncStatus.name, opts.ttl, opts.ttlSeconds, opts.claim]);
582
-
583
- // Bridge the engine's presence + claims streams into React state.
584
- // Plain useState + useEffect is sufficient — mid-frame tearing on a
585
- // peer list is harmless (users won't notice one frame of stale
586
- // presence). Queries and sync status use useSyncExternalStore
587
- // because transactions CAN tear visibly; presence can't.
588
- const [peers, setPeers] = useState<readonly Peer[]>(EMPTY_PRESENCE);
589
- const [claims, setClaims] = useState<readonly Claim[]>(EMPTY_INTENTS);
590
-
591
- useEffect(() => {
592
- if (!participant || paused) {
593
- setPeers(EMPTY_PRESENCE);
594
- setClaims(EMPTY_INTENTS);
595
- return;
596
- }
597
- setPeers(participant.presence.others);
598
- setClaims(participant.claims.others);
599
- const unsubPresence = participant.presence.onChange(() => {
600
- setPeers(participant.presence.others);
601
- });
602
- const unsubClaims = participant.claims.onChange(() => {
603
- setClaims(participant.claims.others);
604
- });
605
- return () => {
606
- unsubPresence();
607
- unsubClaims();
608
- };
609
- }, [participant, paused]);
610
-
611
- return { participant, peers, claims, status, error };
612
- }
385
+ export type { GroupScope };
613
386
 
614
387
  /**
615
388
  * Read-only presence: the OTHER participants currently visible to this
616
- * connection, bridged to React. Unlike {@link useJoin}, this does
617
- * NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
618
- * it is a pure reader of the engine's already-flowing presence stream.
389
+ * connection, bridged to React. This is a pure reader of the engine's
390
+ * already-flowing presence stream; it does not mutate connection groups.
619
391
  *
620
392
  * Pass `scope` to narrow to the peers on that scope's sync group(s); omit
621
393
  * it to get everyone on the engine's groups. Membership is driven entirely
@@ -623,25 +395,22 @@ export function useJoin(opts: UseJoinOptions): UseJoinReturn {
623
395
  * cursor/collaboration traffic), so reading it never affects what the
624
396
  * connection is subscribed to and can't deadlock against a gated channel.
625
397
  *
626
- * Use this to answer "is anyone else here?" e.g. suppressing live-cursor
627
- * broadcasts while alone — when some OTHER mount already owns the scope's
628
- * read interest (scope `leave` is not reference-counted, so a second
629
- * `useJoin` on the same scope would warm-drop the owner's
630
- * subscription on unmount).
398
+ * Use this to answer "is anyone else here?", for example to suppress
399
+ * live-cursor broadcasts while alone.
631
400
  *
632
401
  * ```ts
633
402
  * const peers = usePeers({ reports: reportId });
634
403
  * const alone = !peers.some((p) => p.participantKind === 'user');
635
404
  * ```
636
405
  */
637
- export function usePeers(scope?: ParticipantScope): readonly Peer[] {
406
+ export function usePeers(scope?: GroupScope): readonly Peer[] {
638
407
  const ctx = useContext(AbloInternalContext);
639
408
  const engine = ctx?.engine ?? null;
640
409
 
641
- // Resolve scope → groups through the schema (same idiom as useJoin).
410
+ // Resolve scope → groups through the schema.
642
411
  // The stringified, sorted key is the stable effect dependency.
643
412
  const scopeKey = JSON.stringify(
644
- resolveParticipantSyncGroups(scope, engine?.schema).sort(),
413
+ resolveScopeGroups(scope, engine?.schema).sort(),
645
414
  );
646
415
  const groups = useMemo(() => JSON.parse(scopeKey) as string[], [scopeKey]);
647
416
 
@@ -659,13 +428,12 @@ export function usePeers(scope?: ParticipantScope): readonly Peer[] {
659
428
  : presence.others.filter((p) =>
660
429
  p.syncGroups.some((g) => groups.includes(g)),
661
430
  );
662
- // Plain useState + onChange — presence changes on join/leave/activity
431
+ // Plain useState + onChange — presence changes on connect/disconnect/activity
663
432
  // only (never on cursor traffic, a separate channel), so this fires
664
- // rarely; a frame of stale presence is harmless (same rationale as
665
- // useJoin's peers bridge).
433
+ // rarely; a frame of stale presence is harmless.
666
434
  setPeers(compute());
667
435
  return presence.onChange(() => { setPeers(compute()); });
668
- }, [engine, scopeKey]);
436
+ }, [engine, groups, scopeKey]);
669
437
 
670
438
  return peers;
671
439
  }
package/src/react.ts CHANGED
@@ -10,15 +10,11 @@ export {
10
10
 
11
11
  export {
12
12
  AbloProvider,
13
- useJoin,
14
13
  usePeers,
15
14
  useSync,
16
15
  useSyncStore,
17
16
  type AbloProviderProps,
18
- type ParticipantScope,
19
- type ParticipantStatus,
20
- type UseJoinOptions,
21
- type UseJoinReturn,
17
+ type GroupScope,
22
18
  } from './react/AbloProvider.js';
23
19
 
24
20
  export {
package/src/surface.ts CHANGED
@@ -41,7 +41,6 @@ export const PUBLIC_MODEL_VERBS = [
41
41
  'update',
42
42
  'delete',
43
43
  'claim',
44
- 'join',
45
44
  'onChange',
46
45
  ] as const;
47
46
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
@@ -76,11 +75,9 @@ type _ListOptionKeysExact = Expect<
76
75
  export const PUBLIC_ABLO_OPTION_KEYS = [
77
76
  'schema',
78
77
  'apiKey',
78
+ 'session',
79
79
  'projectId',
80
80
  'branchId',
81
- 'authEndpoint',
82
- 'authTimeoutMs',
83
- 'allowCrossOriginAuthEndpoint',
84
81
  'persistence',
85
82
  'durableWrites',
86
83
  'commitOutbox',
@@ -1,132 +0,0 @@
1
- import type { JoinOptions } from '@abloatai/transaction/client/resources/modelOperations';
2
- import type { SyncWebSocket } from './SyncWebSocket.js';
3
- import type { Schema } from '@abloatai/transaction/schema/schema';
4
- import type { Claim, Activity, ClaimTarget, ClaimStream, Peer, PresenceStream, PresenceTarget } from '@abloatai/transaction/types/streams';
5
- import type { AttachableClaimStream } from './createClaimStream.js';
6
- /**
7
- * The scope a participant can be joined to. The usual form is an entity target
8
- * (`{ type, id }`); raw sync-group strings are an advanced escape hatch for
9
- * addressing a transport scope directly.
10
- */
11
- export type ParticipantScope = ClaimTarget | readonly ClaimTarget[] | string | readonly string[] | {
12
- readonly syncGroup: string;
13
- } | {
14
- readonly syncGroups: readonly string[];
15
- } | Record<string, string | readonly string[] | undefined>;
16
- export type ParticipantStatus = 'idle' | 'connecting' | 'connected' | 'error' | 'disconnected';
17
- export interface EngineParticipant {
18
- readonly presence: PresenceStream;
19
- readonly claims: ClaimStream;
20
- }
21
- /**
22
- * The options for a participant join. It extends the public per-model
23
- * {@link JoinOptions} rather than restating its members, so the lease dial is
24
- * declared once, in the core, and this surface adds only what a lower-level
25
- * join can additionally say.
26
- */
27
- export interface ParticipantJoinOptions extends JoinOptions {
28
- /**
29
- * The initial focus target, named in your schema's vocabulary and optionally
30
- * narrowed to a field. When `scope` is omitted, this target also becomes the
31
- * routing scope.
32
- */
33
- readonly target?: PresenceTarget;
34
- /** Alias for `target` when the participant is joined to a broader scope. */
35
- readonly focus?: PresenceTarget;
36
- /**
37
- * The routing scope: one entity, many entities, or a raw sync-group escape
38
- * hatch. Use it for "joined to the folder, focused on one file" shapes,
39
- * where the participant listens more broadly than its focus target.
40
- */
41
- readonly scope?: ParticipantScope;
42
- /** Present a narrower capability for this logical participant. */
43
- readonly capabilityToken?: string;
44
- /**
45
- * @deprecated Use `ttl`. Removed in 0.37.0.
46
- *
47
- * One lease, spelled two ways, and the seconds spelling was the one that
48
- * misled: it accepted a duration string, so `ablo.<model>.join(ids, { ttl:
49
- * '5m' })` reached this surface as `ttlSeconds: '5m'` — a field whose name
50
- * asserts a unit its value did not carry. `ttl` takes the same values and is
51
- * the spelling every other lease in the SDK already uses (`claim`'s `ttl`,
52
- * `ClaimLeaseOptions.ttl`). The wire is unchanged: it has always carried
53
- * seconds, and still does.
54
- */
55
- readonly ttlSeconds?: number | string | null;
56
- /**
57
- * The activity to announce as soon as the claim is acknowledged. Defaults to
58
- * `reading` when a `target` is present. Pass `false` to join without
59
- * announcing anything.
60
- */
61
- readonly activity?: 'reading' | 'viewing' | 'editing' | false;
62
- readonly detail?: string;
63
- }
64
- export interface ScopedPresence {
65
- readonly self: Peer;
66
- readonly focus: ClaimTarget | null;
67
- readonly others: readonly Peer[];
68
- update(activity: Activity): void;
69
- reading(detail?: string): void;
70
- reading(target: PresenceTarget, detail?: string): void;
71
- viewing(detail?: string): void;
72
- viewing(target: PresenceTarget, detail?: string): void;
73
- editing(detail?: string): void;
74
- editing(target: PresenceTarget, detail?: string): void;
75
- idle(): void;
76
- onChange(listener: () => void): () => void;
77
- }
78
- export interface ScopedClaimOptions {
79
- /** Override the participant's focus target for this one claim. */
80
- readonly target?: PresenceTarget;
81
- /** Peer-visible description of the work. Defaults to `'editing'`. */
82
- readonly description?: string;
83
- /** How long the claim lives; the server expires it automatically after this. */
84
- readonly ttl?: import('@abloatai/transaction/types/streams').Duration;
85
- }
86
- export interface ScopedClaims {
87
- readonly focus: ClaimTarget | null;
88
- readonly others: readonly Claim[];
89
- /**
90
- * Takes an exclusive claim on the participant's focus target, or on an
91
- * explicit target passed via `opts.target`. While the claim is held, other
92
- * participants that request an overlapping target are rejected.
93
- */
94
- claim(opts?: ScopedClaimOptions): Claim;
95
- onRejected(listener: Parameters<ClaimStream['onRejected']>[0]): () => void;
96
- onChange(listener: () => void): () => void;
97
- }
98
- export interface ParticipantFocusOptions {
99
- readonly activity?: 'reading' | 'viewing' | 'editing' | false;
100
- readonly detail?: string;
101
- }
102
- export interface JoinedParticipant {
103
- /** The exact entity this participant is currently reading or editing. */
104
- readonly target: ClaimTarget | null;
105
- readonly focusTarget: ClaimTarget | null;
106
- /** The transport scopes this participant is joined to, which govern what it sees and receives. */
107
- readonly syncGroups: readonly string[];
108
- readonly presence: ScopedPresence;
109
- readonly claims: ScopedClaims;
110
- readonly peers: readonly Peer[];
111
- readonly activeClaims: readonly Claim[];
112
- focus(target: PresenceTarget, options?: ParticipantFocusOptions): JoinedParticipant;
113
- leave(): void;
114
- [Symbol.asyncDispose](): Promise<void>;
115
- }
116
- export interface ParticipantManager {
117
- join(target: PresenceTarget, options?: Omit<ParticipantJoinOptions, 'target'>): Promise<JoinedParticipant>;
118
- join(options: ParticipantJoinOptions): Promise<JoinedParticipant>;
119
- }
120
- export interface ParticipantManagerConfig {
121
- readonly ready: () => Promise<void>;
122
- /** The connection, host-built and stable for the client's lifetime. */
123
- readonly transport: SyncWebSocket;
124
- readonly presence: PresenceStream;
125
- readonly claims: AttachableClaimStream;
126
- readonly schema?: Schema;
127
- }
128
- export declare function createParticipantManager(config: ParticipantManagerConfig): ParticipantManager;
129
- export declare function resolveParticipantSyncGroups(scope: ParticipantScope | undefined, schema?: Schema): string[];
130
- export declare function syncGroupFromEntityRef(ref: ClaimTarget, schema?: Schema): string;
131
- export declare function parseParticipantTtlSeconds(value: number | string | null | undefined): number | undefined;
132
- export declare function createParticipantClaimId(): string;