@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
package/README.md CHANGED
@@ -25,7 +25,7 @@ import { Ablo } from '@abloatai/ablo/client';
25
25
 
26
26
  const ablo = Ablo({
27
27
  schema,
28
- authEndpoint: '/api/ablo-session',
28
+ session: { endpoint: '/api/ablo-session' },
29
29
  });
30
30
 
31
31
  await ablo.ready();
package/dist/Ablo.d.ts CHANGED
@@ -50,11 +50,11 @@ export type Ablo<S extends SchemaRecord> = AbloClient<S>;
50
50
  * ```
51
51
  *
52
52
  * In the browser (or any client that shouldn't hold a secret key), point
53
- * `authEndpoint` at your session-mint route instead — the SDK fetches it, keeps the
53
+ * `session.endpoint` at your session-mint route instead — the SDK fetches it, keeps the
54
54
  * short-lived token fresh, and re-mints on expiry:
55
55
  *
56
56
  * ```ts
57
- * const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
57
+ * const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
58
58
  * ```
59
59
  *
60
60
  * Server-side agents, workers, and services use `@abloatai/transaction`.
@@ -64,7 +64,6 @@ export declare function Ablo<const S extends SchemaRecord, const P extends reado
64
64
  }): Ablo<S> & MergedSurface<P>;
65
65
  export declare function Ablo<const S extends SchemaRecord>(options: AbloOptions<S>): Ablo<S>;
66
66
  import type * as _Streams from '@abloatai/transaction/types/streams';
67
- import type * as _Participants from './local/sync/participants.js';
68
67
  import type * as _Mutators from './local/mutators/defineMutators.js';
69
68
  import type * as _Tx from './local/mutators/Transaction.js';
70
69
  import type * as _Undo from './local/mutators/UndoManager.js';
@@ -110,13 +109,6 @@ export declare namespace Ablo {
110
109
  namespace Auth {
111
110
  type Actor = _Streams.ParticipantRef;
112
111
  }
113
- namespace Participant {
114
- type Manager = _Participants.ParticipantManager;
115
- type Joined = _Participants.JoinedParticipant;
116
- type Scope = _Participants.ParticipantScope;
117
- type Status = _Participants.ParticipantStatus;
118
- type JoinOptions = _Participants.ParticipantJoinOptions;
119
- }
120
112
  type Schema<S extends _SchemaTypes.SchemaRecord = _SchemaTypes.SchemaRecord> = _SchemaTypes.Schema<S>;
121
113
  /**
122
114
  * The schema this program has registered via `interface Register { Schema }`
package/dist/Ablo.js CHANGED
@@ -115,6 +115,5 @@ export function Ablo(options) {
115
115
  transport,
116
116
  presence: humansSurface.presence,
117
117
  cluster,
118
- createSibling: (siblingOptions) => Ablo(siblingOptions),
119
118
  }), installedPlugins);
120
119
  }
package/dist/client.d.ts CHANGED
@@ -17,7 +17,7 @@ import type { SyncStoreContract } from './react/context.js';
17
17
  import type { SyncWebSocket, CoreSyncEventMap } from './local/sync/SyncWebSocket.js';
18
18
  import type { SyncStatus } from './local/BaseSyncedStore.js';
19
19
  import type { ModelOperations } from './local/client/createModelOperations.js';
20
- import type { ClaimResource, CommitResource, CreateAgentClientParams, SessionResource } from '@abloatai/transaction/client/resources/httpResources';
20
+ import type { ClaimResource, CommitResource } from '@abloatai/transaction/client/resources/httpResources';
21
21
  import type { EffectiveAuthority } from '@abloatai/transaction/auth';
22
22
  import type { ReadDependency } from '@abloatai/transaction/coordination';
23
23
  import type { CapturedRow } from '@abloatai/transaction/transport/http';
@@ -102,53 +102,6 @@ export type AbloClient<S extends SchemaRecord> = {
102
102
  * the watchdog.
103
103
  */
104
104
  nudgeReconnect(): void;
105
- /**
106
- * Mint a short-lived, scoped session token for one end user. Call this on your
107
- * backend, where the `sk_` secret key lives, then hand the returned `token` to
108
- * that user's browser — typically through a token route the browser's `apiKey`
109
- * resolver fetches. The browser presents the token as its bearer, and the
110
- * server verifies it. The browser must never see the `sk_` key, only the
111
- * per-user session token.
112
- *
113
- * Pass `{ user: { id }, can: { items: ['read', 'update'] } }` for an end-user
114
- * session. It mints an `ek_` and attributes writes to a user (recorded as
115
- * `actor_kind` on the delta row). Pass `{ agent: { id }, can: {
116
- * items: ['update'] } }` for a scoped agent session, which mints an `rk_`.
117
- * Both kinds require `can`, typed against your schema's model names. This
118
- * always authenticates with the original `sk_`, never the client's exchanged
119
- * sync credential.
120
- */
121
- sessions: SessionResource<S>;
122
- /**
123
- * Mint a scoped **agent identity** and return a ready-to-use client bound to
124
- * it — the `ablo.<resource>.<verb>` shape for the agent use case. One call
125
- * replaces `sessions.create({ agent, can })` + constructing a second
126
- * `Ablo({ apiKey: token })`:
127
- *
128
- * ```ts
129
- * const agent = await ablo.agents.create({
130
- * name: 'researcher', // readable label (optional)
131
- * can: { records: ['read', 'update'] },
132
- * // id omitted → a fresh uuid: a distinct, independent participant
133
- * });
134
- * await agent.records.update({ id, data, claim });
135
- * await agent.dispose(); // when the agent is done
136
- * ```
137
- *
138
- * Server-side only: it requires the `sk_` secret key (like `sessions.create`)
139
- * and throws `AbloAuthenticationError` in the browser. The returned client
140
- * holds its own auto-refreshing `rk_`, so a long run never hits token expiry,
141
- * and the `sk_` never leaves this process. Each call is a distinct participant
142
- * by default (omit `id` for a fresh uuid), so even two agents sharing a `name`
143
- * queue behind one another on a contended row — `name` is display only and
144
- * never collapses identity. Humans don't get a server-built client; ship them a
145
- * token via `sessions.create({ user, can })`. If you need the raw token for
146
- * revocation, or a stable re-attachable id, use `sessions.create({ agent, can })`
147
- * or pass `id`.
148
- */
149
- agents: {
150
- create(params: CreateAgentClientParams<S>): Promise<AbloClient<S>>;
151
- };
152
105
  /**
153
106
  * The organization this client resolved to — `null` until `ready()`
154
107
  * completes. Use it instead of scraping CLI output or hardcoding env vars:
package/dist/humans.d.ts CHANGED
@@ -13,7 +13,7 @@ export interface HumansSurface {
13
13
  readonly [kStoreCluster]?: StoreCluster;
14
14
  }
15
15
  export declare function humans(): {
16
- readonly id: 'humans';
16
+ readonly id: "humans";
17
17
  readonly requires: {
18
18
  readonly duplex: true;
19
19
  };
@@ -13,7 +13,7 @@
13
13
  import type { RecoveryClass } from '@abloatai/transaction/errorCodes';
14
14
  import { ConnectionManager } from './sync/ConnectionManager.js';
15
15
  import { SubscriptionManager } from './sync/SubscriptionManager.js';
16
- import { type ParticipantScope } from './sync/participants.js';
16
+ import { type GroupScope } from './sync/scopeGroups.js';
17
17
  import type { SyncClient } from './SyncClient.js';
18
18
  import type { Database, BootstrapResult, BootstrapRequirements } from './Database.js';
19
19
  import type { InstanceCache } from './InstanceCache.js';
@@ -254,7 +254,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
254
254
  * Hydration is best-effort — a failed backfill never rejects `enterScope`,
255
255
  * and the live delta stream keeps flowing regardless.
256
256
  */
257
- enterScope(scope: ParticipantScope, opts?: {
257
+ enterScope(scope: GroupScope, opts?: {
258
258
  hydrate?: boolean;
259
259
  }): Promise<void>;
260
260
  /**
@@ -266,11 +266,11 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
266
266
  */
267
267
  protected hydrateGroups(syncGroups: readonly string[]): Promise<void>;
268
268
  /** Leave a scope → its groups go warm (hysteresis), then drop on sweep. */
269
- leaveScope(scope: ParticipantScope): Promise<void>;
269
+ leaveScope(scope: GroupScope): Promise<void>;
270
270
  /** Pin a scope (active claim / prominence) → never warms while pinned. */
271
- pinScope(scope: ParticipantScope): Promise<void>;
271
+ pinScope(scope: GroupScope): Promise<void>;
272
272
  /** Release a pin → the group transitions to warm rather than dropping. */
273
- unpinScope(scope: ParticipantScope): Promise<void>;
273
+ unpinScope(scope: GroupScope): Promise<void>;
274
274
  protected readonly queryProcessor: QueryProcessor;
275
275
  /**
276
276
  * Runtime behavior flags only — the schema/config arrays
@@ -15,7 +15,7 @@ import { AbloConnectionError, AbloValidationError, toAbloError } from '@abloatai
15
15
  import { ConnectionManager } from './sync/ConnectionManager.js';
16
16
  import { contextLogger, contextSocketObservability } from './sync/contextPorts.js';
17
17
  import { SubscriptionManager } from './sync/SubscriptionManager.js';
18
- import { resolveParticipantSyncGroups, } from './sync/participants.js';
18
+ import { resolveScopeGroups, } from './sync/scopeGroups.js';
19
19
  import { ModelRegistry } from './ModelRegistry.js';
20
20
  import { PropertyType } from '@abloatai/transaction/types';
21
21
  import { SyncWebSocket, } from './sync/SyncWebSocket.js';
@@ -180,7 +180,7 @@ export class BaseSyncedStore {
180
180
  // {@link SubscriptionManager.reconcile}); the on-connect `resync` pushes
181
181
  // whatever interest accumulated.
182
182
  scopeToGroups(scope) {
183
- return resolveParticipantSyncGroups(scope, this.schema);
183
+ return resolveScopeGroups(scope, this.schema);
184
184
  }
185
185
  /**
186
186
  * Bring a scope into view and subscribe to its sync groups. With
@@ -101,7 +101,7 @@ export declare class Database {
101
101
  * where a missing store points to silent data loss. Callers that
102
102
  * already expect optional behavior (e.g. lazy lookups) can omit it.
103
103
  */
104
- getStore(modelName: string, context?: string): InMemoryObjectStore | import("./stores/ObjectStore.js").ObjectStore | undefined;
104
+ getStore(modelName: string, context?: string): import("./stores/ObjectStore.js").ObjectStore | InMemoryObjectStore | undefined;
105
105
  /** Get store or throw if not found (for operations that require the store). */
106
106
  private getRequiredStore;
107
107
  /** Log preserved fields during partial UPDATE merge (debug helper) */
@@ -274,7 +274,7 @@ export declare class Database {
274
274
  * `getStore(modelName, context?)` is defined near the top of this
275
275
  * class — single accessor for both inMemory and IDB modes.
276
276
  */
277
- getAllStores(): Map<string, InMemoryObjectStore> | Map<string, import("./stores/ObjectStore.js").ObjectStore>;
277
+ getAllStores(): Map<string, import("./stores/ObjectStore.js").ObjectStore> | Map<string, InMemoryObjectStore>;
278
278
  /**
279
279
  * Model persistence tracking
280
280
  */
@@ -65,7 +65,7 @@ export declare class LazyReferenceCollection<T extends Model> {
65
65
  private get database();
66
66
  /** Get objectPool from static dependencies */
67
67
  private get objectPool();
68
- constructor(modelName: string, parent: Model, foreignKey: string, customQuery?: any, options?: LazyCollectionOptions);
68
+ constructor(modelName: string, parent: Model, foreignKey: string, customQuery?: any | undefined, options?: LazyCollectionOptions);
69
69
  /**
70
70
  * Set up MobX observation lifecycle hooks
71
71
  * When React components observe this collection, we prevent GC of the parent model
@@ -186,9 +186,9 @@ export declare class SyncClient extends EventEmitter {
186
186
  */
187
187
  private captureModelChanges;
188
188
  /** Add new model (CREATE) - works offline */
189
- add(model: Model, options?: WriteOptions): void;
189
+ add(model: Model, options?: WriteOptions): Promise<void> | undefined;
190
190
  /** Update existing model (UPDATE) - works offline */
191
- update(model: Model, options?: WriteOptions): void;
191
+ update(model: Model, options?: WriteOptions, capturedChanges?: Record<string, unknown>): Promise<void> | undefined;
192
192
  /**
193
193
  * Update existing model with pre-computed changes.
194
194
  * Used by saveManyOptimized when incoming models have empty change-tracking
@@ -200,7 +200,7 @@ export declare class SyncClient extends EventEmitter {
200
200
  * but still need optimistic pool updates at the sync layer. */
201
201
  get gql(): import("./interfaces/index.js").MutationExecutor;
202
202
  /** Delete model (DELETE) - works offline */
203
- delete(model: Model, options?: WriteOptions): void;
203
+ delete(model: Model, options?: WriteOptions): Promise<void> | undefined;
204
204
  /**
205
205
  * Upload a file and create its attachment record. The upload runs through
206
206
  * the {@link MutationQueue}, and a model is built from the server's
@@ -409,14 +409,14 @@ export declare class SyncClient extends EventEmitter {
409
409
  * Get detailed debug info for the sync debug page
410
410
  */
411
411
  getDebugInfo(): {
412
- connectionState: "connected" | "connecting" | "disconnected";
412
+ connectionState: "connected" | "disconnected" | "connecting";
413
413
  pendingMutationsCount: number;
414
414
  mutationQueue: {
415
415
  lastSeenSyncId: number;
416
416
  awaitingDeltaCount: number;
417
417
  awaitingDeltaTransactions: {
418
418
  id: string;
419
- type: "archive" | "create" | "delete" | "unarchive" | "update";
419
+ type: "update" | "create" | "delete" | "archive" | "unarchive";
420
420
  modelName: string;
421
421
  modelId: string;
422
422
  syncIdNeeded: number | undefined;
@@ -425,13 +425,13 @@ export declare class SyncClient extends EventEmitter {
425
425
  }[];
426
426
  pendingTransactions: {
427
427
  id: string;
428
- type: "archive" | "create" | "delete" | "unarchive" | "update";
428
+ type: "update" | "create" | "delete" | "archive" | "unarchive";
429
429
  modelName: string;
430
430
  modelId: string;
431
431
  }[];
432
432
  executingTransactions: {
433
433
  id: string;
434
- type: "archive" | "create" | "delete" | "unarchive" | "update";
434
+ type: "update" | "create" | "delete" | "archive" | "unarchive";
435
435
  modelName: string;
436
436
  modelId: string;
437
437
  }[];
@@ -758,7 +758,7 @@ export class SyncClient extends EventEmitter {
758
758
  * capturing first ensures those changes are never lost. The captured set is
759
759
  * frozen and handed to {@link queueMutation}.
760
760
  */
761
- mutate(type, model, poolAction, writeOptions) {
761
+ mutate(type, model, poolAction, writeOptions, capturedChangesOverride) {
762
762
  // No-op UPDATE guard (O(1)). An update with no dirty fields would travel
763
763
  // to the server, get dropped by `coalesceOperations` Rule 4 (empty input),
764
764
  // and — if it was the only op — come back as `lastSyncId: 0`. That trips
@@ -772,21 +772,28 @@ export class SyncClient extends EventEmitter {
772
772
  // is false → we fall through to the normal path rather than risk dropping a
773
773
  // real write. Only a genuine Model with an empty dirty-set is skipped.
774
774
  const hasChanges = model.hasChanges;
775
- if (type === 'update' && hasChanges === false) {
776
- return;
775
+ if (type === 'update' &&
776
+ hasChanges === false &&
777
+ capturedChangesOverride === undefined) {
778
+ return Promise.resolve();
777
779
  }
778
780
  // Capture changes before the pool action runs. Pool operations —
779
781
  // upsert in particular — can clear the model's local changes, so
780
782
  // capturing first ensures they are never lost.
781
- const capturedChanges = type === 'update' || type === 'create' ? this.captureModelChanges(model) : undefined;
783
+ const capturedChanges = capturedChangesOverride !== undefined
784
+ ? Object.freeze({ ...capturedChangesOverride })
785
+ : type === 'update' || type === 'create'
786
+ ? this.captureModelChanges(model)
787
+ : undefined;
782
788
  poolAction();
783
- this.stageMutation(type, model, capturedChanges, writeOptions);
789
+ const confirmation = this.stageMutation(type, model, capturedChanges, writeOptions);
784
790
  this.notifyObservers({
785
791
  type,
786
792
  modelType: model.getModelName(),
787
793
  model: type !== 'delete' ? model : undefined,
788
794
  modelId: model.id,
789
795
  });
796
+ return confirmation;
790
797
  // QueryProcessor uses `models:changed` to invalidate caches. Coalesce
791
798
  // to one event per microtask: a paste of 100 rows should re-run
792
799
  // affected queries ONCE, not 100×.
@@ -821,11 +828,11 @@ export class SyncClient extends EventEmitter {
821
828
  }
822
829
  /** Add new model (CREATE) - works offline */
823
830
  add(model, options) {
824
- this.mutate('create', model, () => { this.objectPool.add(model, ModelScope.live); }, options);
831
+ return this.mutate('create', model, () => { this.objectPool.add(model, ModelScope.live); }, options);
825
832
  }
826
833
  /** Update existing model (UPDATE) - works offline */
827
- update(model, options) {
828
- this.mutate('update', model, () => { this.objectPool.upsert(model, ModelScope.live); }, options);
834
+ update(model, options, capturedChanges) {
835
+ return this.mutate('update', model, () => { this.objectPool.upsert(model, ModelScope.live); }, options, capturedChanges);
829
836
  }
830
837
  /**
831
838
  * Update existing model with pre-computed changes.
@@ -867,7 +874,7 @@ export class SyncClient extends EventEmitter {
867
874
  delete(model, options) {
868
875
  // Clear pending mutations first to prevent "not found" errors on fast delete
869
876
  this.mutationQueue.cancelTransactionsForModel(model.id);
870
- this.mutate('delete', model, () => this.objectPool.remove(model.id), options);
877
+ return this.mutate('delete', model, () => this.objectPool.remove(model.id), options);
871
878
  }
872
879
  /**
873
880
  * Upload a file and create its attachment record. The upload runs through
@@ -979,13 +986,20 @@ export class SyncClient extends EventEmitter {
979
986
  /** Stage one mutation through the queue, which owns durability and execution. */
980
987
  stageMutation(type, model, capturedChanges, writeOptions) {
981
988
  if (this.isDisposed)
982
- return;
989
+ return Promise.resolve();
983
990
  if (!this.userId || !this.organizationId) {
984
991
  this.mutationQueue.deferMutation(type, model, capturedChanges, writeOptions);
985
992
  return;
986
993
  }
987
994
  const context = { userId: this.userId, organizationId: this.organizationId };
988
995
  const staging = this.mutationQueue.enqueueModelMutation(type, model, context, capturedChanges, writeOptions);
996
+ const confirmation = staging.then(async (transaction) => {
997
+ await transaction.confirmation;
998
+ });
999
+ // Most internal callers intentionally use fire-and-forget writes. Observe
1000
+ // their rejection without replacing the exact promise returned to model
1001
+ // operations that need authoritative per-transaction confirmation.
1002
+ void confirmation.catch(() => undefined);
989
1003
  const pending = staging.then(() => undefined).catch((error) => {
990
1004
  this.runtime.observability.captureMutationFailure({
991
1005
  context: `stage-mutation-${type}`,
@@ -996,6 +1010,7 @@ export class SyncClient extends EventEmitter {
996
1010
  });
997
1011
  this.pendingStages.add(pending);
998
1012
  void pending.finally(() => this.pendingStages.delete(pending));
1013
+ return confirmation;
999
1014
  }
1000
1015
  scheduleSync() {
1001
1016
  if (!this.runtime.onlineStatus.isOnline() || this.isDisposed)
@@ -6,7 +6,7 @@
6
6
  * `read` and `list`, with the same point lookup restricted to the local graph under
7
7
  * `local`, the writes `create`, `update`, and `delete`, the coordination
8
8
  * namespace `claim` (callable as `claim({ id })`, plus `claim.state`,
9
- * `claim.queue`, `claim.release`, and `claim.reorder`), `join`, and `onChange`.
9
+ * `claim.queue`, `claim.release`, and `claim.reorder`), and `onChange`.
10
10
  * The factory returns a plain object; the client assembles the `ablo.<model>`
11
11
  * lookup table from one of these per model.
12
12
  */
@@ -16,11 +16,10 @@ import type { ModelRegistry } from '../ModelRegistry.js';
16
16
  import type { InstanceCache } from '../InstanceCache.js';
17
17
  import type { SyncClient } from '../SyncClient.js';
18
18
  import type { OnDemandLoader } from '../sync/OnDemandLoader.js';
19
- import type { JoinedParticipant } from '../sync/participants.js';
20
19
  import type { Duration, Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease, ClaimWaitOptions } from '@abloatai/transaction/types/streams';
21
- export type { ModelListScope, LocalReadOptions, LocalCountOptions, ServerReadOptions, ListAllOptions, ServerPointReadOptions, ClaimTargetOptions, ClaimParams, ClaimContentionOptions, ClaimAttemptEvent, ClaimQueueView, ClaimSkipOptions, ClaimSkipParams, ClaimLookupParams, ClaimReorderParams, ClaimOptions, ClaimReadApi, AwaitedClaimMethod, ClaimApi, ModelReadParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, JoinOptions, } from '@abloatai/transaction/client/resources/modelOperations';
20
+ export type { ModelListScope, LocalReadOptions, LocalCountOptions, ServerReadOptions, ListAllOptions, ServerPointReadOptions, ClaimTargetOptions, ClaimParams, ClaimContentionOptions, ClaimAttemptEvent, ClaimQueueView, ClaimSkipOptions, ClaimSkipParams, ClaimLookupParams, ClaimReorderParams, ClaimOptions, ClaimReadApi, AwaitedClaimMethod, ClaimApi, ModelReadParams, ModelCreateParams, ModelUpdateParams, ModelDeleteParams, } from '@abloatai/transaction/client/resources/modelOperations';
22
21
  export type { Claim, ClaimHeartbeat, ClaimHeartbeatOptions, HeldClaim, HeldLease };
23
- import type { ClaimApi, ClaimAttemptEvent, JoinOptions, LocalCountOptions, LocalReadOptions } from '@abloatai/transaction/client/resources/modelOperations';
22
+ import type { ClaimApi, ClaimAttemptEvent, LocalCountOptions, LocalReadOptions } from '@abloatai/transaction/client/resources/modelOperations';
24
23
  import type { HttpModelClient } from '@abloatai/transaction/transport/http';
25
24
  import type { ParticipantKind } from '@abloatai/transaction/types/participant';
26
25
  import { type ReadSetContext } from '@abloatai/transaction/internal/read-set';
@@ -142,13 +141,6 @@ export interface ModelCollaboration {
142
141
  * fire-and-forget, best-effort semantics as `enterScope`.
143
142
  */
144
143
  pinScope?(scope: Record<string, string>): void | Promise<void>;
145
- /**
146
- * Opens a presence and claim subscription on this model's sync group(s) and
147
- * returns the live participant handle. Backs `ablo.<model>.join(ids)`.
148
- * WebSocket only, since presence needs a live socket; it is absent on other
149
- * client constructions, where the surface throws a clear error.
150
- */
151
- createJoin?(modelKey: string, ids: string | readonly string[], options?: JoinOptions): Promise<JoinedParticipant>;
152
144
  }
153
145
  /**
154
146
  * The synchronous, local-only reads — reached as `ablo.<model>.local.*`.
@@ -222,22 +214,6 @@ interface ReactiveModelSurface<T, Fields = T> {
222
214
  * ```
223
215
  */
224
216
  claim: ClaimApi<T, Fields>;
225
- /**
226
- * Joins the sync group(s) for one or more rows of this model and returns a
227
- * live participant handle — presence (`.peers`), the scoped claim stream
228
- * (`.claims`), and `.leave()` / `await using` disposal. This is a presence
229
- * subscription: it reports who else is here and what they hold, not row
230
- * values changing — for the latter, use `onChange`.
231
- *
232
- * WebSocket only: presence needs a live socket, so this is absent on HTTP
233
- * clients and throws on any non-WebSocket construction.
234
- *
235
- * ```ts
236
- * await using participant = await ablo.sections.join(sectionIds, { ttl: '5m' });
237
- * participant.peers; // who else is here
238
- * ```
239
- */
240
- join(ids: string | readonly string[], options?: JoinOptions): Promise<JoinedParticipant>;
241
217
  /** Subscribe to changes; the callback runs on every change. */
242
218
  onChange(callback: (entities: T[]) => void, options?: LocalReadOptions<T>): () => void;
243
219
  }
@@ -6,7 +6,7 @@
6
6
  * `read` and `list`, with the same point lookup restricted to the local graph under
7
7
  * `local`, the writes `create`, `update`, and `delete`, the coordination
8
8
  * namespace `claim` (callable as `claim({ id })`, plus `claim.state`,
9
- * `claim.queue`, `claim.release`, and `claim.reorder`), `join`, and `onChange`.
9
+ * `claim.queue`, `claim.release`, and `claim.reorder`), and `onChange`.
10
10
  * The factory returns a plain object; the client assembles the `ablo.<model>`
11
11
  * lookup table from one of these per model.
12
12
  */
@@ -130,7 +130,7 @@ hydration, collaboration, readSetContext) {
130
130
  const rows = await hydration.fetch(schemaKey, options);
131
131
  return rows.map((row) => modelAsRow(row));
132
132
  };
133
- const waitForMutation = async (model) => {
133
+ const waitForMutation = async (model, exactConfirmation) => {
134
134
  // Model writes are optimistic locally, but their promise has one stable
135
135
  // meaning: authoritative confirmation. Callers that do not need the
136
136
  // barrier can keep using the row immediately and leave the promise to the
@@ -142,7 +142,10 @@ hydration, collaboration, readSetContext) {
142
142
  // coalescer and producing one SQL transaction per delta.
143
143
  await Promise.resolve();
144
144
  await syncClient.syncNow();
145
- await syncClient.waitForConfirmation(model.getModelName(), model.id);
145
+ if (exactConfirmation)
146
+ await exactConfirmation;
147
+ else
148
+ await syncClient.waitForConfirmation(model.getModelName(), model.id);
146
149
  };
147
150
  const activeClaims = new Map();
148
151
  const claimIdsByEntity = new Map();
@@ -811,8 +814,8 @@ hydration, collaboration, readSetContext) {
811
814
  }
812
815
  : {}),
813
816
  };
814
- syncClient.add(model, effective);
815
- await waitForMutation(model);
817
+ const confirmation = syncClient.add(model, effective);
818
+ await waitForMutation(model, confirmation);
816
819
  return modelAsRow(model);
817
820
  }
818
821
  finally {
@@ -888,8 +891,8 @@ hydration, collaboration, readSetContext) {
888
891
  : {}),
889
892
  };
890
893
  model.applyChanges(patch);
891
- syncClient.update(model, effective);
892
- await waitForMutation(model);
894
+ const confirmation = syncClient.update(model, effective, patch);
895
+ await waitForMutation(model, confirmation);
893
896
  return modelAsRow(model);
894
897
  },
895
898
  });
@@ -939,8 +942,8 @@ hydration, collaboration, readSetContext) {
939
942
  // the server. (`updateFromData` is the hydration path and would discard
940
943
  // the tracking, producing an empty `input: {}` no-op mutation.)
941
944
  model.applyChanges(params.data);
942
- syncClient.update(model, effective);
943
- await waitForMutation(model);
945
+ const confirmation = syncClient.update(model, effective, params.data);
946
+ await waitForMutation(model, confirmation);
944
947
  const updated = modelAsRow(model);
945
948
  await settleClaimsAfterWrite(id, handle);
946
949
  return updated;
@@ -997,19 +1000,13 @@ hydration, collaboration, readSetContext) {
997
1000
  ...opts,
998
1001
  ...(selected ? { claimRef: { id: selected.id } } : {}),
999
1002
  };
1000
- syncClient.delete(model, effective);
1001
- await waitForMutation(model);
1003
+ const confirmation = syncClient.delete(model, effective);
1004
+ await waitForMutation(model, confirmation);
1002
1005
  await settleClaimsAfterWrite(id, handle);
1003
1006
  }),
1004
1007
  // `claim` is a callable namespace (take a claim) carrying the coordination
1005
1008
  // readers (`claim.state` / `claim.queue` / `claim.release` / `claim.reorder`).
1006
1009
  claim: claimApi,
1007
- join: guard((ids, options) => {
1008
- if (!collaboration?.createJoin) {
1009
- throw new AbloValidationError(`Model "${schemaKey}" was built without a WebSocket runtime, so join() is unavailable here. Presence needs a live socket — use the standard Ablo({ schema, apiKey }) client (not the HTTP transport).`, { code: 'model_join_not_configured' });
1010
- }
1011
- return collaboration.createJoin(schemaKey, ids, options);
1012
- }),
1013
1010
  onChange(callback, options) {
1014
1011
  return autorun(() => {
1015
1012
  callback(local.list(options));
@@ -17,6 +17,7 @@ import type { CommitOutboxScope } from '@abloatai/transaction/commit';
17
17
  */
18
18
  export type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
19
19
  import type { CredentialProvider } from '@abloatai/transaction/auth/apiKey';
20
+ import type { SessionCredential, SessionEndpoint, SessionProvider } from '@abloatai/transaction/sessions';
20
21
  import type { AbloPlugin } from '../../plugin.js';
21
22
  import type { ParticipantKind } from '@abloatai/transaction/types/participant';
22
23
  /**
@@ -56,18 +57,19 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
56
57
  * `ABLO_API_KEY` environment variable, so you usually pass nothing. A
57
58
  * long-lived key needs no refresh; the client uses it as-is.
58
59
  *
59
- * - **An async resolver** `() => Promise<string | null>` the escape hatch for
60
- * when the exchange needs custom headers, a request body, or a non-HTTP mint
61
- * (vault rotation, a cloud token service, an existing auth session). It uses
62
- * the same renewal machinery as the endpoint form.
63
- *
64
- * The endpoint and resolver forms share one contract: return a token; return
65
- * `null` when the login itself is gone (terminal — the client signs out and
66
- * fails `ready()` with `session_expired`); or throw on a transient failure, which
67
- * backs off and retries without signing out. The endpoint form maps HTTP onto
68
- * this for you: only a structured `401 session_expired` means signed out.
60
+ * - **An async resolver** for advanced process-owned key rotation, such as a
61
+ * vault or workload-identity exchange. Scoped actor renewal belongs in
62
+ * `session` instead.
69
63
  */
70
64
  apiKey?: string | CredentialProvider | null | undefined;
65
+ /**
66
+ * Scoped actor identity. Pass a session returned by `sessions.create()` for
67
+ * bounded work, a provider that re-mints it for a long-lived client, or
68
+ * `{ endpoint: '/api/ablo-session' }` in a browser. Endpoint responses use
69
+ * the canonical credential protocol; only a structured `401
70
+ * session_expired` ends the underlying login.
71
+ */
72
+ session?: SessionCredential | SessionProvider | SessionEndpoint | null | undefined;
71
73
  /**
72
74
  * Pins this client to one Ablo project. During `ready()` the server resolves
73
75
  * the API key's actual project and the client refuses to start when it differs.
@@ -84,30 +86,6 @@ export interface AbloOptions<S extends SchemaRecord = SchemaRecord> {
84
86
  * environment could reach this process.
85
87
  */
86
88
  branchId?: string | null | undefined;
87
- /**
88
- * The session-mint endpoint — the browser-side auth field, and the named
89
- * endpoint for the route that mints the signed-in user's short-lived token:
90
- *
91
- * ```ts
92
- * const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
93
- * ```
94
- *
95
- * The client owns the whole exchange: it POSTs the route (same-origin, cookies
96
- * included), validates the canonical auth response contract, keeps it fresh
97
- * ahead of expiry, and re-mints when the server reports the token stale. Only
98
- * a structured `401 session_expired` response means signed out. It also
99
- * accepts an async resolver `() => Promise<string | null>` when the exchange
100
- * needs custom headers or a body — the same contract as the resolver form of
101
- * `apiKey`.
102
- *
103
- * Mutually exclusive with `apiKey`: a server holds a key, a browser holds a mint
104
- * route, and passing both is a validation error.
105
- */
106
- authEndpoint?: string | CredentialProvider | null | undefined;
107
- /** Timeout for a session-mint request. @default 10000 */
108
- authTimeoutMs?: number | undefined;
109
- /** Explicit opt-in for a cross-origin session-mint endpoint. */
110
- allowCrossOriginAuthEndpoint?: boolean | undefined;
111
89
  /**
112
90
  * Local persistence mode. Pass `indexeddb` only when you want offline
113
91
  * queueing and a reload-surviving browser cache.
@@ -222,13 +200,10 @@ export interface InternalAbloOptions<S extends SchemaRecord = SchemaRecord> {
222
200
  * the original available as `cause`.
223
201
  */
224
202
  apiKey?: string | CredentialProvider | null | undefined;
203
+ /** A scoped session, or a provider that re-mints it for a long-lived client. */
204
+ session?: SessionCredential | SessionProvider | SessionEndpoint | null | undefined;
225
205
  /** Expected project assertion; see {@link AbloOptions.projectId}. */
226
206
  projectId?: string | null | undefined;
227
- /**
228
- * Session-mint endpoint (string or async resolver) — see
229
- * {@link AbloOptions.authEndpoint}. Mutually exclusive with `apiKey`.
230
- */
231
- authEndpoint?: string | CredentialProvider | null | undefined;
232
207
  /**
233
208
  * A bearer auth token, sent as `Authorization: Bearer <token>` on every request.
234
209
  *
@@ -2,9 +2,9 @@
2
2
  * The reactive engine assembly (ADR 0016). `Ablo({ ... })` resolves auth and
3
3
  * capabilities; `humans().init` constructs the store cluster; the lifecycle
4
4
  * — first mint, identity, ready() — lives in `./storeLifecycle.ts`. What
5
- * remains here is assembly around those parts: the claim stream and
6
- * participant manager, options validation, the typed model proxies, and the
7
- * commit/claim/session resources — composed into the reactive client.
5
+ * remains here is assembly around those parts: the claim and presence streams,
6
+ * options validation, the typed model proxies, and the
7
+ * commit and claim resources — composed into the reactive client.
8
8
  *
9
9
  * Extracted from the factory so the composition root stays a root: resolve,
10
10
  * dispatch, return. The remaining assembly converts to decoration of a
@@ -43,11 +43,5 @@ export interface ReactiveEngineInputs<S extends SchemaRecord> extends ClientPrel
43
43
  * assembles around it and constructs none of it.
44
44
  */
45
45
  cluster: StoreCluster;
46
- /**
47
- * Constructs a sibling client (`ablo.agents.create(...)` mints a scoped key
48
- * and builds a second engine with it). Injected by the factory — a direct
49
- * import back into it would close a runtime cycle.
50
- */
51
- createSibling: (options: AbloOptions<S>) => Ablo<S>;
52
46
  }
53
47
  export declare function buildReactiveEngine<const S extends SchemaRecord>(inputs: ReactiveEngineInputs<S>): Ablo<S>;