@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
@@ -1,8 +1,8 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import type { SchemaRecord } from '@abloatai/transaction/schema/schema';
3
3
  import type { AbloClient as Ablo } from '../client.js';
4
- import type { Claim, Duration, Peer } from '@abloatai/transaction/types/streams';
5
- import type { EngineParticipant, ParticipantScope, ParticipantStatus } from '../local/sync/participants.js';
4
+ import type { Peer } from '@abloatai/transaction/types/streams';
5
+ import type { GroupScope } from '../local/sync/scopeGroups.js';
6
6
  import { type SyncStoreContract } from './context.js';
7
7
  /**
8
8
  * Ablo umbrella provider — owns the sync engine, multiplayer, and
@@ -13,9 +13,8 @@ import { type SyncStoreContract } from './context.js';
13
13
  *
14
14
  * - **One component, one import.** Consumers write the provider
15
15
  * once at the root; nothing else needs to plumb the engine.
16
- * - **Multiplayer is default.** React consumers are always browsers doing
17
- * multiplayer UI, so `useJoin()` / `useAblo()` are always
18
- * available. No opt-in prop.
16
+ * - **Multiplayer is default.** React consumers share the client's scoped
17
+ * groups, presence stream, and model surface without another join step.
19
18
  * - **Declarative props for app glue.** `preventUnsavedChanges`,
20
19
  * `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
21
20
  * absorbs a class of integration code that previously lived in
@@ -35,7 +34,7 @@ import { type SyncStoreContract } from './context.js';
35
34
  * // Build once at module scope — a new instance per render tears down the socket.
36
35
  * // The endpoint string points at your session-mint route (`ablo init`
37
36
  * // scaffolds it); the SDK fetches it and keeps the token fresh.
38
- * const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
37
+ * const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
39
38
  *
40
39
  * <AbloProvider client={ablo}>
41
40
  * <App />
@@ -108,82 +107,11 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
108
107
  children: ReactNode;
109
108
  }
110
109
  export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProviderProps<R>): React.ReactElement;
111
- export type { EngineParticipant, ParticipantScope, ParticipantStatus };
112
- /**
113
- * Options for `useJoin`. The hook reuses the engine's single
114
- * WebSocket and opens a scoped claim on it when `scope` is provided:
115
- * one TCP connection, N logical sub-syncgroup participants.
116
- */
117
- export interface UseJoinOptions {
118
- readonly scope?: ParticipantScope;
119
- /**
120
- * Lease TTL for the participant claim, as a compact duration (`'5m'`) or a
121
- * number of seconds. The same dial and the same spelling as
122
- * `ablo.<model>.join(ids, { ttl })` and every other lease in the SDK.
123
- */
124
- readonly ttl?: Duration;
125
- /**
126
- * @deprecated Use `ttl`. Removed in 0.37.0.
127
- *
128
- * The same rename as on `ParticipantJoinOptions`: one lease, and the seconds
129
- * spelling was the one that misled, because it accepted duration strings too.
130
- */
131
- readonly ttlSeconds?: number | string | null;
132
- /** Tear down + don't re-join while true. */
133
- readonly paused?: boolean;
134
- /**
135
- * Acquire a write-claim CLAIM on the scope, in addition to read interest.
136
- *
137
- * Default `false`: opening a scope subscribes the connection to its deltas
138
- * (read interest, via `update_subscription`) but does NOT claim it — a
139
- * viewer is not a claimant. Set `true` when the participant intends to
140
- * WRITE (editing a report, an agent staking work): the claim is sent so peers
141
- * observe it, and the scope is pinned so it stays subscribed (never warms)
142
- * for as long as the claim is held.
143
- */
144
- readonly claim?: boolean;
145
- /**
146
- * Backfill the scope's CURRENT state into the pool on enter, in addition to
147
- * tailing live changes.
148
- *
149
- * Default `false`: entering a scope subscribes to its FUTURE deltas only — if
150
- * the scope's rows aren't already loaded, the view is empty until something
151
- * changes. Set `true` when opening an entity that may not be loaded yet (a
152
- * deep-linked report, a never-opened ledger) so its current rows are fetched and
153
- * injected once, then kept fresh by the live tail. The fetch is single-flight
154
- * and runs once per group; a failure soft-fails (the live tail still flows).
155
- */
156
- readonly hydrate?: boolean;
157
- }
158
- export interface UseJoinReturn {
159
- readonly participant: EngineParticipant | null;
160
- /** Everyone else on the engine's sync groups (`participant.presence.others`), bridged to React. */
161
- readonly peers: readonly Peer[];
162
- /** Active claim claims by peers (`participant.claims.others`), bridged to React. */
163
- readonly claims: readonly Claim[];
164
- readonly status: ParticipantStatus;
165
- readonly error: Error | null;
166
- }
167
- /**
168
- * Join multiplayer for a given scope. Returns the participant and its
169
- * lifecycle status. Auto-cleans up on unmount or when `paused`
170
- * flips to true.
171
- *
172
- * `useJoin` is the React form of `ablo.<model>.join` — scope-level
173
- * read-interest + presence; returns the reactive participant facade
174
- * (peers/claims/status).
175
- *
176
- * The returned `participant` is an `EngineParticipant` — `.presence`
177
- * + `.claims` only — backed by the engine's existing socket. For
178
- * headless-bot patterns (a separate identity in the same browser
179
- * tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
180
- */
181
- export declare function useJoin(opts: UseJoinOptions): UseJoinReturn;
110
+ export type { GroupScope };
182
111
  /**
183
112
  * Read-only presence: the OTHER participants currently visible to this
184
- * connection, bridged to React. Unlike {@link useJoin}, this does
185
- * NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
186
- * it is a pure reader of the engine's already-flowing presence stream.
113
+ * connection, bridged to React. This is a pure reader of the engine's
114
+ * already-flowing presence stream; it does not mutate connection groups.
187
115
  *
188
116
  * Pass `scope` to narrow to the peers on that scope's sync group(s); omit
189
117
  * it to get everyone on the engine's groups. Membership is driven entirely
@@ -191,18 +119,15 @@ export declare function useJoin(opts: UseJoinOptions): UseJoinReturn;
191
119
  * cursor/collaboration traffic), so reading it never affects what the
192
120
  * connection is subscribed to and can't deadlock against a gated channel.
193
121
  *
194
- * Use this to answer "is anyone else here?" e.g. suppressing live-cursor
195
- * broadcasts while alone — when some OTHER mount already owns the scope's
196
- * read interest (scope `leave` is not reference-counted, so a second
197
- * `useJoin` on the same scope would warm-drop the owner's
198
- * subscription on unmount).
122
+ * Use this to answer "is anyone else here?", for example to suppress
123
+ * live-cursor broadcasts while alone.
199
124
  *
200
125
  * ```ts
201
126
  * const peers = usePeers({ reports: reportId });
202
127
  * const alone = !peers.some((p) => p.participantKind === 'user');
203
128
  * ```
204
129
  */
205
- export declare function usePeers(scope?: ParticipantScope): readonly Peer[];
130
+ export declare function usePeers(scope?: GroupScope): readonly Peer[];
206
131
  /**
207
132
  * Returns the raw `SyncEngine` proxy. Typically you want the typed
208
133
  * hooks (`useQuery`, `useOne`, `useMutate`) — this is for rare cases
@@ -1,7 +1,7 @@
1
1
  'use client';
2
- import { Fragment as _Fragment, jsx as _jsx } from "react/jsx-runtime";
2
+ import { jsx as _jsx, Fragment as _Fragment } from "react/jsx-runtime";
3
3
  import { useCallback, useContext, useEffect, useMemo, useRef, useState, createContext, } from 'react';
4
- import { createParticipantClaimId, parseParticipantTtlSeconds, resolveParticipantSyncGroups, } from '../local/sync/participants.js';
4
+ import { resolveScopeGroups } from '../local/sync/scopeGroups.js';
5
5
  import { SyncContext } from './context.js';
6
6
  import { AbloInternalContext } from './internalContext.js';
7
7
  import { AbloValidationError } from '@abloatai/transaction/errors';
@@ -200,158 +200,10 @@ function BootstrapGate({ fallback, children, }) {
200
200
  return _jsx(_Fragment, { children: showFallback ? fallback : children });
201
201
  }
202
202
  const EMPTY_PRESENCE = Object.freeze([]);
203
- const EMPTY_INTENTS = Object.freeze([]);
204
- /**
205
- * Join multiplayer for a given scope. Returns the participant and its
206
- * lifecycle status. Auto-cleans up on unmount or when `paused`
207
- * flips to true.
208
- *
209
- * `useJoin` is the React form of `ablo.<model>.join` — scope-level
210
- * read-interest + presence; returns the reactive participant facade
211
- * (peers/claims/status).
212
- *
213
- * The returned `participant` is an `EngineParticipant` — `.presence`
214
- * + `.claims` only — backed by the engine's existing socket. For
215
- * headless-bot patterns (a separate identity in the same browser
216
- * tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
217
- */
218
- export function useJoin(opts) {
219
- const ctx = useContext(AbloInternalContext);
220
- const engine = ctx?.engine ?? null;
221
- const { paused = false } = opts;
222
- // Resolve the model-form scope ({ reports: id } / refs) THROUGH the schema, so a
223
- // model's declared `scope` kind is honored (typename `Report` → `report:<id>`,
224
- // not the `type:id` string fallback). Schema appears once the engine is ready;
225
- // until then refs resolve by convention, then re-resolve when it arrives.
226
- const scopeKey = JSON.stringify(resolveParticipantSyncGroups(opts.scope, engine?.schema).sort());
227
- const scopedSyncGroups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
228
- const [claimError, setClaimError] = useState(null);
229
- const [claimConnected, setClaimConnected] = useState(false);
230
- // Reference-stable participant facade — same socket as entity sync,
231
- // so there is no `connect()` / `disconnect()` lifecycle here. The
232
- // engine manages the connection; the hook is a thin window onto its
233
- // already-attached presence + claim streams.
234
- const participant = useMemo(() => {
235
- if (!engine)
236
- return null;
237
- return { presence: engine.presence, claims: engine.claims };
238
- }, [engine]);
239
- // Status maps to the engine's sync state. `connecting` while the
240
- // engine bootstraps; `connected` once `engine.ready()` resolves and
241
- // any scoped participant claim has acked; `error` if the claim
242
- // fails; `disconnected` while paused or before the engine exists.
243
- const syncStatus = useSyncStatus();
244
- // Only a write-claim participant waits on a claim ack. A pure reader
245
- // (the default) is `connected` as soon as the engine is — its read
246
- // interest is fire-and-forget `update_subscription`, not a claim.
247
- const needsClaim = !!opts.claim && scopedSyncGroups.length > 0;
248
- const status = paused || !engine
249
- ? 'disconnected'
250
- : claimError
251
- ? 'error'
252
- : syncStatus.name === 'connected'
253
- ? needsClaim && !claimConnected
254
- ? 'connecting'
255
- : 'connected'
256
- : syncStatus.name === 'disconnected' || syncStatus.name === 'needs-auth'
257
- ? 'disconnected'
258
- : 'connecting';
259
- const error = claimError;
260
- // ── Read interest (always) ───────────────────────────────────────
261
- // Subscribe the connection to the scope's sync groups while mounted +
262
- // connected — the area-of-interest navigation primitive. No claim, no
263
- // TTL: a viewer just receives the scope's deltas. Hysteresis (warm TTL)
264
- // lives in the store's SubscriptionManager, so a quick unmount/remount
265
- // (tab flip) doesn't re-bootstrap.
266
- useEffect(() => {
267
- const scope = opts.scope;
268
- if (paused || !engine || !scope || scopedSyncGroups.length === 0)
269
- return;
270
- if (syncStatus.name !== 'connected')
271
- return;
272
- const store = engine._store;
273
- // `hydrate` backfills the scope's current state after subscribing
274
- // (store handles subscribe-first ordering + single-flight). leaveScope
275
- // only moves read interest; the hydrated rows stay in the pool.
276
- void store.enterScope?.(scope, { hydrate: opts.hydrate });
277
- return () => {
278
- void store.leaveScope?.(scope);
279
- };
280
- // scopeKey is the stable proxy for the resolved groups; same idiom as
281
- // the claim effect below.
282
- }, [engine, paused, scopeKey, syncStatus.name, opts.hydrate]);
283
- // ── Write claim (opt-in: `claim: true`) ─────────────────────────
284
- // A claim is the write-claim primitive — distinct from read interest
285
- // above. Only sent when the caller opts in; it makes peers observe the
286
- // claim and pins the scope so it never warms while held.
287
- useEffect(() => {
288
- setClaimError(null);
289
- setClaimConnected(false);
290
- const scope = opts.scope;
291
- if (paused || !engine || !opts.claim || !scope || scopedSyncGroups.length === 0)
292
- return;
293
- if (syncStatus.name !== 'connected')
294
- return;
295
- const ws = engine._ws;
296
- const store = engine._store;
297
- let cancelled = false;
298
- const claimId = createParticipantClaimId();
299
- ws.sendClaim(claimId, scopedSyncGroups, {
300
- // Reading the retired spelling IS the compatibility path; it goes at 0.37.0.
301
- // eslint-disable-next-line @typescript-eslint/no-deprecated
302
- ttlSeconds: parseParticipantTtlSeconds(opts.ttl ?? opts.ttlSeconds),
303
- })
304
- .then(() => {
305
- if (!cancelled)
306
- setClaimConnected(true);
307
- })
308
- .catch((err) => {
309
- if (!cancelled) {
310
- setClaimError(err instanceof Error ? err : new Error(String(err)));
311
- }
312
- });
313
- // Prominence: hold the scope subscribed for as long as the claim lives.
314
- void store.pinScope?.(scope);
315
- return () => {
316
- cancelled = true;
317
- ws.sendRelease(claimId);
318
- void store.unpinScope?.(scope);
319
- };
320
- // eslint-disable-next-line @typescript-eslint/no-deprecated -- same compatibility read as above.
321
- }, [engine, paused, scopeKey, syncStatus.name, opts.ttl, opts.ttlSeconds, opts.claim]);
322
- // Bridge the engine's presence + claims streams into React state.
323
- // Plain useState + useEffect is sufficient — mid-frame tearing on a
324
- // peer list is harmless (users won't notice one frame of stale
325
- // presence). Queries and sync status use useSyncExternalStore
326
- // because transactions CAN tear visibly; presence can't.
327
- const [peers, setPeers] = useState(EMPTY_PRESENCE);
328
- const [claims, setClaims] = useState(EMPTY_INTENTS);
329
- useEffect(() => {
330
- if (!participant || paused) {
331
- setPeers(EMPTY_PRESENCE);
332
- setClaims(EMPTY_INTENTS);
333
- return;
334
- }
335
- setPeers(participant.presence.others);
336
- setClaims(participant.claims.others);
337
- const unsubPresence = participant.presence.onChange(() => {
338
- setPeers(participant.presence.others);
339
- });
340
- const unsubClaims = participant.claims.onChange(() => {
341
- setClaims(participant.claims.others);
342
- });
343
- return () => {
344
- unsubPresence();
345
- unsubClaims();
346
- };
347
- }, [participant, paused]);
348
- return { participant, peers, claims, status, error };
349
- }
350
203
  /**
351
204
  * Read-only presence: the OTHER participants currently visible to this
352
- * connection, bridged to React. Unlike {@link useJoin}, this does
353
- * NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
354
- * it is a pure reader of the engine's already-flowing presence stream.
205
+ * connection, bridged to React. This is a pure reader of the engine's
206
+ * already-flowing presence stream; it does not mutate connection groups.
355
207
  *
356
208
  * Pass `scope` to narrow to the peers on that scope's sync group(s); omit
357
209
  * it to get everyone on the engine's groups. Membership is driven entirely
@@ -359,11 +211,8 @@ export function useJoin(opts) {
359
211
  * cursor/collaboration traffic), so reading it never affects what the
360
212
  * connection is subscribed to and can't deadlock against a gated channel.
361
213
  *
362
- * Use this to answer "is anyone else here?" e.g. suppressing live-cursor
363
- * broadcasts while alone — when some OTHER mount already owns the scope's
364
- * read interest (scope `leave` is not reference-counted, so a second
365
- * `useJoin` on the same scope would warm-drop the owner's
366
- * subscription on unmount).
214
+ * Use this to answer "is anyone else here?", for example to suppress
215
+ * live-cursor broadcasts while alone.
367
216
  *
368
217
  * ```ts
369
218
  * const peers = usePeers({ reports: reportId });
@@ -373,9 +222,9 @@ export function useJoin(opts) {
373
222
  export function usePeers(scope) {
374
223
  const ctx = useContext(AbloInternalContext);
375
224
  const engine = ctx?.engine ?? null;
376
- // Resolve scope → groups through the schema (same idiom as useJoin).
225
+ // Resolve scope → groups through the schema.
377
226
  // The stringified, sorted key is the stable effect dependency.
378
- const scopeKey = JSON.stringify(resolveParticipantSyncGroups(scope, engine?.schema).sort());
227
+ const scopeKey = JSON.stringify(resolveScopeGroups(scope, engine?.schema).sort());
379
228
  const groups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
380
229
  const [peers, setPeers] = useState(EMPTY_PRESENCE);
381
230
  useEffect(() => {
@@ -387,13 +236,12 @@ export function usePeers(scope) {
387
236
  const compute = () => groups.length === 0
388
237
  ? presence.others
389
238
  : presence.others.filter((p) => p.syncGroups.some((g) => groups.includes(g)));
390
- // Plain useState + onChange — presence changes on join/leave/activity
239
+ // Plain useState + onChange — presence changes on connect/disconnect/activity
391
240
  // only (never on cursor traffic, a separate channel), so this fires
392
- // rarely; a frame of stale presence is harmless (same rationale as
393
- // useJoin's peers bridge).
241
+ // rarely; a frame of stale presence is harmless.
394
242
  setPeers(compute());
395
243
  return presence.onChange(() => { setPeers(compute()); });
396
- }, [engine, scopeKey]);
244
+ }, [engine, groups, scopeKey]);
397
245
  return peers;
398
246
  }
399
247
  // ── Escape-hatches: raw engine/store access ──────────────────────────
@@ -33,4 +33,4 @@ export interface ClientSideSuspenseProps {
33
33
  /** What to render once the subtree is cleared to render. */
34
34
  children: ReactNode;
35
35
  }
36
- export declare function ClientSideSuspense({ fallback, children }: ClientSideSuspenseProps): import("react/jsx-runtime").JSX.Element;
36
+ export declare function ClientSideSuspense({ fallback, children }: ClientSideSuspenseProps): import("react").JSX.Element;
@@ -21,4 +21,4 @@
21
21
  * pass `fallback={null}`. Consumers who want to skip the gate entirely
22
22
  * pass `fallback="passthrough"`.
23
23
  */
24
- export declare function DefaultFallback(): import("react/jsx-runtime").JSX.Element;
24
+ export declare function DefaultFallback(): import("react").JSX.Element;
@@ -38,7 +38,7 @@ export function createAbloReact(schema) {
38
38
  // implementation's internal-context fallback.
39
39
  const BoundClientContext = createContext(null);
40
40
  function BoundAbloProvider(props) {
41
- return createElement(BoundClientContext.Provider, { value: props.client }, createElement(AbloProvider, props));
41
+ return createElement(BoundClientContext.Provider, { value: props.client }, createElement((AbloProvider), props));
42
42
  }
43
43
  function useBoundAblo(modelOrSelect, id, options) {
44
44
  const bound = useContext(BoundClientContext);
package/dist/react.d.ts CHANGED
@@ -4,7 +4,7 @@ export { useCurrentUserId } from './react/useCurrentUserId.js';
4
4
  export { useErrorListener } from './react/useErrorListener.js';
5
5
  export { useSyncStatus, type SyncStatusSnapshot } from './react/useSyncStatus.js';
6
6
  export { useMutationFailureListener, type MutationFailurePayload, } from './react/useMutationFailureListener.js';
7
- export { AbloProvider, useJoin, usePeers, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseJoinOptions, type UseJoinReturn, } from './react/AbloProvider.js';
7
+ export { AbloProvider, usePeers, useSync, useSyncStore, type AbloProviderProps, type GroupScope, } from './react/AbloProvider.js';
8
8
  export { ClientSideSuspense, type ClientSideSuspenseProps, } from './react/ClientSideSuspense.js';
9
9
  export { DefaultFallback } from './react/DefaultFallback.js';
10
10
  export { createAbloReact, type AbloReactBinding, } from './react/createAbloReact.js';
package/dist/react.js CHANGED
@@ -4,7 +4,7 @@ export { useCurrentUserId } from './react/useCurrentUserId.js';
4
4
  export { useErrorListener } from './react/useErrorListener.js';
5
5
  export { useSyncStatus } from './react/useSyncStatus.js';
6
6
  export { useMutationFailureListener, } from './react/useMutationFailureListener.js';
7
- export { AbloProvider, useJoin, usePeers, useSync, useSyncStore, } from './react/AbloProvider.js';
7
+ export { AbloProvider, usePeers, useSync, useSyncStore, } from './react/AbloProvider.js';
8
8
  export { ClientSideSuspense, } from './react/ClientSideSuspense.js';
9
9
  export { DefaultFallback } from './react/DefaultFallback.js';
10
10
  export { createAbloReact, } from './react/createAbloReact.js';
package/dist/surface.d.ts CHANGED
@@ -19,18 +19,18 @@
19
19
  * tuple, so it is the one list of model-verb names a generated summary can
20
20
  * describe.
21
21
  */
22
- export declare const PUBLIC_MODEL_VERBS: readonly ['get', 'read', 'list', 'listAll', 'local', 'create', 'update', 'delete', 'claim', 'join', 'onChange'];
22
+ export declare const PUBLIC_MODEL_VERBS: readonly ["get", "read", "list", "listAll", "local", "create", "update", "delete", "claim", "onChange"];
23
23
  /**
24
24
  * The option keys accepted by `local.list` and `onChange`, matching the
25
25
  * keys of {@link LocalReadOptions}. Note that the lifecycle filter is named
26
26
  * `state`, not `scope`.
27
27
  */
28
- export declare const PUBLIC_LIST_OPTION_KEYS: readonly ['where', 'filter', 'orderBy', 'limit', 'offset', 'state'];
28
+ export declare const PUBLIC_LIST_OPTION_KEYS: readonly ["where", "filter", "orderBy", "limit", "offset", "state"];
29
29
  /**
30
30
  * The keys of the client constructor options, {@link AbloOptions}. Only
31
31
  * `schema` is required; every other key is optional.
32
32
  */
33
- export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ['schema', 'apiKey', 'projectId', 'branchId', 'authEndpoint', 'authTimeoutMs', 'allowCrossOriginAuthEndpoint', 'persistence', 'durableWrites', 'commitOutbox', 'commitOutboxScope', 'debug', 'logLevel', 'logger', 'authToken', 'baseURL', 'fetch', 'defaultHeaders', 'defaultQuery', 'dangerouslyAllowBrowser', 'collaborationEvents', 'plugins'];
33
+ export declare const PUBLIC_ABLO_OPTION_KEYS: readonly ["schema", "apiKey", "session", "projectId", "branchId", "persistence", "durableWrites", "commitOutbox", "commitOutboxScope", "debug", "logLevel", "logger", "authToken", "baseURL", "fetch", "defaultHeaders", "defaultQuery", "dangerouslyAllowBrowser", "collaborationEvents", "plugins"];
34
34
  export type ModelVerb = (typeof PUBLIC_MODEL_VERBS)[number];
35
35
  export type ListOptionKey = (typeof PUBLIC_LIST_OPTION_KEYS)[number];
36
36
  export type AbloOptionKey = (typeof PUBLIC_ABLO_OPTION_KEYS)[number];
package/dist/surface.js CHANGED
@@ -30,7 +30,6 @@ export const PUBLIC_MODEL_VERBS = [
30
30
  'update',
31
31
  'delete',
32
32
  'claim',
33
- 'join',
34
33
  'onChange',
35
34
  ];
36
35
  // ── the read/list query option surface ─────────────────────────────────────
@@ -55,11 +54,9 @@ export const PUBLIC_LIST_OPTION_KEYS = [
55
54
  export const PUBLIC_ABLO_OPTION_KEYS = [
56
55
  'schema',
57
56
  'apiKey',
57
+ 'session',
58
58
  'projectId',
59
59
  'branchId',
60
- 'authEndpoint',
61
- 'authTimeoutMs',
62
- 'allowCrossOriginAuthEndpoint',
63
60
  'persistence',
64
61
  'durableWrites',
65
62
  'commitOutbox',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/humans",
3
- "version": "0.59.2",
3
+ "version": "0.60.0",
4
4
  "description": "The optional human-facing local-state package for Ablo: presence, live queries, and React bindings.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -84,7 +84,7 @@
84
84
  "directory": "packages/humans"
85
85
  },
86
86
  "dependencies": {
87
- "@abloatai/transaction": "^0.59.2",
87
+ "@abloatai/transaction": "^0.60.0",
88
88
  "mobx": "^6.13.7",
89
89
  "uuid": "^11.1.0",
90
90
  "zod": "^4.4.3"
@@ -114,6 +114,7 @@
114
114
  "react": "^19.2.8",
115
115
  "react-dom": "^19.2.8",
116
116
  "ts-jest": "^29.4.5",
117
+ "ts-node": "^10.9.2",
117
118
  "typescript": "npm:@typescript/typescript6@^6.0.2",
118
119
  "ws": "^8.18.3"
119
120
  }
package/src/Ablo.ts CHANGED
@@ -46,9 +46,9 @@ import { resolveClientPrelude } from './local/client/clientPrelude.js';
46
46
  // The option types, the client's public shape, the pass over the options bag,
47
47
  // the resource-type surface, and the default WebSocket mutation executor each
48
48
  // live in their own module. The type-only ones (`options`, `abloClient`,
49
- // `resourceTypes`) carry no runtime imports, which lets the HTTP client and the
50
- // session-mint helpers reference the client types without importing this
51
- // factory back and creating an import cycle.
49
+ // `resourceTypes`) carry no runtime imports, which lets the HTTP client
50
+ // reference the client types without importing this factory back and creating
51
+ // an import cycle.
52
52
  import type { AbloOptions } from './local/client/options.js';
53
53
  // `AbloReads` is named by `Ablo.Reads` in the namespace below as well as
54
54
  // re-exported, so it needs the import too — same reason as `AbloOptions`.
@@ -92,11 +92,11 @@ export type Ablo<S extends SchemaRecord> = AbloClient<S>;
92
92
  * ```
93
93
  *
94
94
  * In the browser (or any client that shouldn't hold a secret key), point
95
- * `authEndpoint` at your session-mint route instead — the SDK fetches it, keeps the
95
+ * `session.endpoint` at your session-mint route instead — the SDK fetches it, keeps the
96
96
  * short-lived token fresh, and re-mints on expiry:
97
97
  *
98
98
  * ```ts
99
- * const ablo = Ablo({ schema, authEndpoint: '/api/ablo-session' });
99
+ * const ablo = Ablo({ schema, session: { endpoint: '/api/ablo-session' } });
100
100
  * ```
101
101
  *
102
102
  * Server-side agents, workers, and services use `@abloatai/transaction`.
@@ -214,7 +214,6 @@ export function Ablo<const S extends SchemaRecord>(
214
214
  transport,
215
215
  presence: humansSurface.presence,
216
216
  cluster,
217
- createSibling: (siblingOptions) => Ablo(siblingOptions),
218
217
  }), installedPlugins);
219
218
  }
220
219
 
@@ -232,7 +231,6 @@ export function Ablo<const S extends SchemaRecord>(
232
231
  // continue to work for callers who prefer them.
233
232
 
234
233
  import type * as _Streams from '@abloatai/transaction/types/streams';
235
- import type * as _Participants from './local/sync/participants.js';
236
234
  import type * as _Mutators from './local/mutators/defineMutators.js';
237
235
  import type * as _Tx from './local/mutators/Transaction.js';
238
236
  import type * as _Undo from './local/mutators/UndoManager.js';
@@ -297,16 +295,6 @@ export namespace Ablo {
297
295
  export type Actor = _Streams.ParticipantRef;
298
296
  }
299
297
 
300
- // ── Participant (sub-namespace — 5 names, shared concept) ─────────
301
- // eslint-disable-next-line @typescript-eslint/no-namespace
302
- export namespace Participant {
303
- export type Manager = _Participants.ParticipantManager;
304
- export type Joined = _Participants.JoinedParticipant;
305
- export type Scope = _Participants.ParticipantScope;
306
- export type Status = _Participants.ParticipantStatus;
307
- export type JoinOptions = _Participants.ParticipantJoinOptions;
308
- }
309
-
310
298
  // ── Schema (type + sub-namespace via declaration merge) ───────────
311
299
  export type Schema<S extends _SchemaTypes.SchemaRecord = _SchemaTypes.SchemaRecord> = _SchemaTypes.Schema<S>;
312
300
  /**
package/src/client.ts CHANGED
@@ -27,8 +27,6 @@ import type { ModelOperations } from './local/client/createModelOperations.js';
27
27
  import type {
28
28
  ClaimResource,
29
29
  CommitResource,
30
- CreateAgentClientParams,
31
- SessionResource,
32
30
  } from '@abloatai/transaction/client/resources/httpResources';
33
31
  import type { EffectiveAuthority } from '@abloatai/transaction/auth';
34
32
  import type { ReadDependency } from '@abloatai/transaction/coordination';
@@ -125,55 +123,6 @@ export type AbloClient<S extends SchemaRecord> = {
125
123
  */
126
124
  nudgeReconnect(): void;
127
125
 
128
- /**
129
- * Mint a short-lived, scoped session token for one end user. Call this on your
130
- * backend, where the `sk_` secret key lives, then hand the returned `token` to
131
- * that user's browser — typically through a token route the browser's `apiKey`
132
- * resolver fetches. The browser presents the token as its bearer, and the
133
- * server verifies it. The browser must never see the `sk_` key, only the
134
- * per-user session token.
135
- *
136
- * Pass `{ user: { id }, can: { items: ['read', 'update'] } }` for an end-user
137
- * session. It mints an `ek_` and attributes writes to a user (recorded as
138
- * `actor_kind` on the delta row). Pass `{ agent: { id }, can: {
139
- * items: ['update'] } }` for a scoped agent session, which mints an `rk_`.
140
- * Both kinds require `can`, typed against your schema's model names. This
141
- * always authenticates with the original `sk_`, never the client's exchanged
142
- * sync credential.
143
- */
144
- sessions: SessionResource<S>;
145
-
146
- /**
147
- * Mint a scoped **agent identity** and return a ready-to-use client bound to
148
- * it — the `ablo.<resource>.<verb>` shape for the agent use case. One call
149
- * replaces `sessions.create({ agent, can })` + constructing a second
150
- * `Ablo({ apiKey: token })`:
151
- *
152
- * ```ts
153
- * const agent = await ablo.agents.create({
154
- * name: 'researcher', // readable label (optional)
155
- * can: { records: ['read', 'update'] },
156
- * // id omitted → a fresh uuid: a distinct, independent participant
157
- * });
158
- * await agent.records.update({ id, data, claim });
159
- * await agent.dispose(); // when the agent is done
160
- * ```
161
- *
162
- * Server-side only: it requires the `sk_` secret key (like `sessions.create`)
163
- * and throws `AbloAuthenticationError` in the browser. The returned client
164
- * holds its own auto-refreshing `rk_`, so a long run never hits token expiry,
165
- * and the `sk_` never leaves this process. Each call is a distinct participant
166
- * by default (omit `id` for a fresh uuid), so even two agents sharing a `name`
167
- * queue behind one another on a contended row — `name` is display only and
168
- * never collapses identity. Humans don't get a server-built client; ship them a
169
- * token via `sessions.create({ user, can })`. If you need the raw token for
170
- * revocation, or a stable re-attachable id, use `sessions.create({ agent, can })`
171
- * or pass `id`.
172
- */
173
- agents: {
174
- create(params: CreateAgentClientParams<S>): Promise<AbloClient<S>>;
175
- };
176
-
177
126
  /**
178
127
  * The organization this client resolved to — `null` until `ready()`
179
128
  * completes. Use it instead of scraping CLI output or hardcoding env vars: