@abloatai/ablo 0.11.2 → 0.13.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 (70) hide show
  1. package/AGENTS.md +2 -2
  2. package/CHANGELOG.md +34 -0
  3. package/README.md +3 -3
  4. package/dist/ai-sdk/claim-broadcast.d.ts +4 -3
  5. package/dist/ai-sdk/claim-broadcast.js +2 -2
  6. package/dist/ai-sdk/wrap.d.ts +5 -4
  7. package/dist/ai-sdk/wrap.js +3 -3
  8. package/dist/cli.cjs +152 -41
  9. package/dist/client/Ablo.d.ts +25 -3
  10. package/dist/client/Ablo.js +5 -5
  11. package/dist/client/ApiClient.js +26 -11
  12. package/dist/client/createModelProxy.d.ts +15 -7
  13. package/dist/client/createModelProxy.js +12 -12
  14. package/dist/coordination/schema.d.ts +1 -1
  15. package/dist/coordination/schema.js +3 -1
  16. package/dist/errors.d.ts +3 -1
  17. package/dist/errors.js +6 -1
  18. package/dist/react/AbloProvider.d.ts +11 -7
  19. package/dist/react/AbloProvider.js +9 -5
  20. package/dist/react/context.d.ts +9 -14
  21. package/dist/react/context.js +10 -15
  22. package/dist/react/index.d.ts +8 -4
  23. package/dist/react/index.js +8 -4
  24. package/dist/react/useMutators.js +3 -2
  25. package/dist/react/useUndoScope.js +3 -2
  26. package/dist/schema/index.d.ts +2 -2
  27. package/dist/schema/index.js +2 -2
  28. package/dist/schema/model.d.ts +38 -77
  29. package/dist/schema/model.js +12 -12
  30. package/dist/schema/roles.d.ts +49 -0
  31. package/dist/schema/roles.js +21 -0
  32. package/dist/schema/schema.d.ts +1 -1
  33. package/dist/schema/schema.js +1 -1
  34. package/dist/schema/serialize.d.ts +4 -2
  35. package/dist/schema/serialize.js +4 -2
  36. package/dist/schema/sugar.d.ts +7 -28
  37. package/dist/schema/sugar.js +2 -7
  38. package/dist/schema/sync-delta-row.d.ts +2 -0
  39. package/dist/schema/sync-delta-row.js +2 -1
  40. package/dist/schema/tenancy.d.ts +67 -28
  41. package/dist/schema/tenancy.js +93 -23
  42. package/dist/server/commit.d.ts +8 -3
  43. package/dist/sync/createClaimStream.js +5 -4
  44. package/dist/sync/participants.js +1 -1
  45. package/dist/types/streams.d.ts +17 -7
  46. package/docs/api.md +1 -1
  47. package/docs/cli.md +43 -4
  48. package/docs/client-behavior.md +2 -2
  49. package/docs/coordination.md +1 -1
  50. package/docs/examples/agent-human.md +6 -6
  51. package/docs/examples/ai-sdk-tool.md +1 -1
  52. package/docs/examples/existing-python-backend.md +0 -2
  53. package/docs/examples/nextjs.md +2 -2
  54. package/docs/examples/scoped-agent.md +3 -3
  55. package/docs/examples/server-agent.md +4 -4
  56. package/docs/identity.md +27 -20
  57. package/docs/index.md +0 -1
  58. package/docs/integration-guide.md +12 -9
  59. package/docs/interaction-model.md +1 -1
  60. package/docs/mcp.md +17 -5
  61. package/docs/migration.md +2 -1
  62. package/docs/quickstart.md +3 -3
  63. package/llms.txt +2 -3
  64. package/package.json +3 -2
  65. package/docs/mcp/claude-code.md +0 -35
  66. package/docs/mcp/cursor.md +0 -35
  67. package/docs/mcp/windsurf.md +0 -33
  68. package/docs/roadmap.md +0 -55
  69. package/docs/the-loop.md +0 -21
  70. package/llms-full.txt +0 -396
@@ -11,6 +11,18 @@ import { registerDataSource } from './registerDataSource.js';
11
11
  import { toSeconds } from '../utils/duration.js';
12
12
  import { mintSession } from './sessionMint.js';
13
13
  import { assertWriteOptions } from './writeOptionsSchema.js';
14
+ /**
15
+ * The `/v1/claims` and model-query routes still emit the wire field `action`
16
+ * for the claim phase; the public `Claim` / `ModelClaim` expose it as `reason`.
17
+ * Heal on read so the SDK shape is consistent without a coordinated server
18
+ * deploy — `reason ?? action`. When the server adopts `reason`, this is a no-op.
19
+ */
20
+ function healClaimPhase(claim) {
21
+ const raw = claim;
22
+ if (raw.reason !== undefined)
23
+ return claim;
24
+ return { ...claim, reason: raw.action ?? 'editing' };
25
+ }
14
26
  const DEFAULT_AGENT_LEASE = '10m';
15
27
  export function createProtocolClient(options) {
16
28
  const env = readProcessEnv();
@@ -173,8 +185,8 @@ export function createProtocolClient(options) {
173
185
  const suffix = params.toString();
174
186
  const body = await requestJson(`/v1/claims${suffix ? `?${suffix}` : ''}`, { method: 'GET' });
175
187
  return {
176
- active: body.claims ?? [],
177
- queue: body.queue ?? [],
188
+ active: (body.claims ?? []).map(healClaimPhase),
189
+ queue: (body.queue ?? []).map(healClaimPhase),
178
190
  };
179
191
  }
180
192
  function delay(ms, signal) {
@@ -374,7 +386,8 @@ export function createProtocolClient(options) {
374
386
  body: JSON.stringify({
375
387
  claimId,
376
388
  target: claimOptions.target,
377
- action: claimOptions.action,
389
+ // Wire field stays `action`; public option is `reason`.
390
+ action: claimOptions.reason,
378
391
  ttl: claimOptions.ttl,
379
392
  queue: claimOptions.queue,
380
393
  }),
@@ -400,7 +413,7 @@ export function createProtocolClient(options) {
400
413
  return {
401
414
  object: 'claim',
402
415
  claimId: id,
403
- action: claimOptions.action,
416
+ reason: claimOptions.reason,
404
417
  target: claimOptions.target,
405
418
  release,
406
419
  revoke: () => {
@@ -451,7 +464,7 @@ export function createProtocolClient(options) {
451
464
  return {
452
465
  data,
453
466
  stamp: query.stamp ?? 0,
454
- claims: query.claims ?? [],
467
+ claims: (query.claims ?? []).map(healClaimPhase),
455
468
  };
456
469
  }
457
470
  /**
@@ -533,13 +546,14 @@ export function createProtocolClient(options) {
533
546
  const body = await requestJson(claimPath(params.id), {
534
547
  method: 'POST',
535
548
  body: JSON.stringify({
536
- action: params.action ?? 'editing',
549
+ // Wire field stays `action`; public option is `reason`.
550
+ action: params.reason ?? 'editing',
537
551
  ...(params.ttl !== undefined ? { ttl: params.ttl } : {}),
538
552
  ...(params.description !== undefined ? { description: params.description } : {}),
539
553
  ...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
540
- // `wait` (default true) → queue behind the holder; false → fail-fast
554
+ // `queue` (default true) → queue behind the holder; false → fail-fast
541
555
  // with AbloClaimedError (work-distribution dedup).
542
- queue: params.wait ?? true,
556
+ queue: params.queue ?? true,
543
557
  }),
544
558
  });
545
559
  if (body.status === 'queued') {
@@ -565,7 +579,7 @@ export function createProtocolClient(options) {
565
579
  ...(params.range ? { range: params.range } : {}),
566
580
  ...(claimMeta(params) ? { meta: claimMeta(params) } : {}),
567
581
  },
568
- action: params.action ?? 'editing',
582
+ reason: params.reason ?? 'editing',
569
583
  ...(params.description ? { description: params.description } : {}),
570
584
  data,
571
585
  release,
@@ -580,11 +594,12 @@ export function createProtocolClient(options) {
580
594
  release: releaseClaim,
581
595
  state: async (params) => {
582
596
  const res = await claimsForEntity(params);
583
- return res.claims?.[0] ?? null;
597
+ const first = res.claims?.[0];
598
+ return first ? healClaimPhase(first) : null;
584
599
  },
585
600
  queue: async (params) => {
586
601
  const res = await claimsForEntity(params);
587
- return { object: 'list', data: res.queue ?? [] };
602
+ return { object: 'list', data: (res.queue ?? []).map(healClaimPhase) };
588
603
  },
589
604
  reorder: async (params) => {
590
605
  await requestJson(`${claimPath(params.id)}/reorder`, {
@@ -90,7 +90,8 @@ export interface ModelCollaboration<T> {
90
90
  range?: TargetRange;
91
91
  meta?: Record<string, unknown>;
92
92
  };
93
- action: string;
93
+ /** Human-readable phase (`'editing'`); wire field is `action`. */
94
+ reason: string;
94
95
  ttl?: Duration;
95
96
  /**
96
97
  * Block on the server's fair FIFO queue when the target is held, rather
@@ -178,8 +179,9 @@ export interface ModelCollaboration<T> {
178
179
  createWatch?(modelKey: string, ids: string | readonly string[], options?: WatchOptions): Promise<JoinedParticipant>;
179
180
  }
180
181
  export interface ClaimTargetOptions<T = Record<string, unknown>> {
181
- /** Phase shown to observers while held. Defaults to `'editing'`. */
182
- action?: string;
182
+ /** Human-readable phase shown to observers while held. Defaults to
183
+ * `'editing'`. The same word on every claim surface; wire field is `action`. */
184
+ reason?: string;
183
185
  /** Peer-visible explanation of the work being performed. */
184
186
  description?: string;
185
187
  /** Field-level target, for fine-grained claimed-state badges. */
@@ -199,8 +201,14 @@ export interface ClaimTargetOptions<T = Record<string, unknown>> {
199
201
  * `AbloClaimedError` instead of waiting (claim-or-skip). Use `false` for
200
202
  * work-distribution dedup ("if someone else has this job, skip it") where
201
203
  * waiting would mean double-processing.
204
+ *
205
+ * Named `queue` to match every other claim surface (low-level
206
+ * `claims.claim`, HTTP `claim.create`, and the wire). The high-level typed
207
+ * claim defaults it ON because it serializes writers; the low-level lease
208
+ * and HTTP default it OFF — they return/resolve immediately and can't
209
+ * transparently wait for a grant.
202
210
  */
203
- wait?: boolean;
211
+ queue?: boolean;
204
212
  /**
205
213
  * Backpressure: willing to queue, but not behind too many. If the server
206
214
  * reports `position >= maxQueueDepth` when we join the line, reject with
@@ -226,7 +234,7 @@ export interface ClaimReorderParams<T = Record<string, unknown>> extends ClaimLo
226
234
  * ```ts
227
235
  * const claim = await ablo.weatherReports.claim({
228
236
  * id: 'report_stockholm',
229
- * action: 'forecasting',
237
+ * reason: 'forecasting',
230
238
  * description: 'Fetching current weather before writing the forecast.',
231
239
  * });
232
240
  * try {
@@ -258,7 +266,7 @@ export type ClaimOptions<T = Record<string, unknown>> = ClaimTargetOptions<T>;
258
266
  * data: { title },
259
267
  * claim: {
260
268
  * field: 'title',
261
- * action: 'renaming',
269
+ * reason: 'renaming',
262
270
  * description: 'Renaming the task to match the project brief.',
263
271
  * },
264
272
  * });
@@ -379,7 +387,7 @@ export interface ModelOperations<T, CreateInput> {
379
387
  * ```ts
380
388
  * const claim = await ablo.weatherReports.claim({
381
389
  * id: 'report_stockholm',
382
- * action: 'forecasting',
390
+ * reason: 'forecasting',
383
391
  * description: 'Fetching fresh weather before updating the report.',
384
392
  * });
385
393
  * const weather = await getWeather(claim.data.location);
@@ -77,7 +77,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
77
77
  // `release({ id })` and `update({ id, data })` find the lease + snapshot a `claim({ id })`
78
78
  // took — no per-call handle. Released on dispose, explicit release, or TTL.
79
79
  //
80
- // `target` / `action` / `expiresAt` are kept alongside the lease so
80
+ // `target` / `reason` / `expiresAt` are kept alongside the lease so
81
81
  // `claim.state` can synthesize a self-claim: the server excludes a holder's
82
82
  // own presence frames, so the local proxy is the ONLY place that knows "I
83
83
  // hold this." `expiresAt` is the client's best estimate from the requested
@@ -103,7 +103,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
103
103
  id: claim.id,
104
104
  actor: claim.heldBy,
105
105
  participantKind: claim.participantKind,
106
- action: claim.action,
106
+ reason: claim.reason,
107
107
  ...(description ? { description } : {}),
108
108
  field: claim.target.field,
109
109
  status: claim.status,
@@ -143,8 +143,8 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
143
143
  // claim (a free / already-mine target can't have changed under us).
144
144
  const held = collaboration.observe({ model: wireModel, id });
145
145
  const contended = !!held && held.heldBy !== collaboration.selfParticipantId;
146
- const failFast = options?.wait === false;
147
- // Fail-fast (`wait: false`): if another participant already holds it,
146
+ const failFast = options?.queue === false;
147
+ // Fail-fast (`queue: false`): if another participant already holds it,
148
148
  // reject now instead of queuing. Best-effort at the client (a racing
149
149
  // claim not yet synced into our snapshot slips through here) — the
150
150
  // commit-time claim guard is the authoritative backstop that rejects
@@ -176,7 +176,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
176
176
  // in the group when `createClaim` lands. Awaited because the broadcast
177
177
  // ordering depends on it; still soft (the store swallows reconcile errors).
178
178
  await collaboration.pinScope?.({ [schemaKey]: id });
179
- // Acquire the lease. Default (`wait` !== false) goes through the server's
179
+ // Acquire the lease. Default (`queue` !== false) goes through the server's
180
180
  // fair FIFO queue — `queue: true` resolves only once the lease is genuinely
181
181
  // ours, blocking behind any current holder, with no TOCTOU gap (the server
182
182
  // orders contenders). Fail-fast skips the queue: we already rejected an
@@ -190,7 +190,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
190
190
  ...(options?.range ? { range: options.range } : {}),
191
191
  ...(claimMeta(options) ? { meta: claimMeta(options) } : {}),
192
192
  },
193
- action: options?.action ?? 'editing',
193
+ reason: options?.reason ?? 'editing',
194
194
  ttl: options?.ttl,
195
195
  queue: !failFast,
196
196
  maxQueueDepth: options?.maxQueueDepth,
@@ -213,7 +213,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
213
213
  model = objectPool.get(id) ?? model;
214
214
  }
215
215
  const snapshot = collaboration.createSnapshot(schemaKey, id);
216
- const action = options?.action ?? 'editing';
216
+ const reason = options?.reason ?? 'editing';
217
217
  // The self-claim's `EntityRef` mirrors what a peer's `claim.state` would
218
218
  // report (`observe` maps `held.target.model` → `type`), so a holder and a
219
219
  // peer see the SAME target.type for one row — the wire model token.
@@ -231,7 +231,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
231
231
  lease,
232
232
  snapshot,
233
233
  target: selfTarget,
234
- action,
234
+ reason,
235
235
  expiresAt,
236
236
  });
237
237
  const target = {
@@ -248,7 +248,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
248
248
  claimId: lease.claimId,
249
249
  readAt: snapshot.stamp,
250
250
  target,
251
- action,
251
+ reason,
252
252
  ...(options?.description ? { description: options.description } : {}),
253
253
  data: modelAsRow(model),
254
254
  release,
@@ -281,7 +281,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
281
281
  id: own.lease.claimId,
282
282
  status: 'active',
283
283
  target: own.target,
284
- action: own.action,
284
+ reason: own.reason,
285
285
  heldBy: collaboration?.selfParticipantId ?? '',
286
286
  participantKind: collaboration?.selfParticipantKind ?? 'user',
287
287
  expiresAt: own.expiresAt,
@@ -381,9 +381,9 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
381
381
  ...(claim.range ? { range: claim.range } : {}),
382
382
  ...(claimMeta(claim) ? { meta: claimMeta(claim) } : {}),
383
383
  },
384
- action: claim.action ?? 'creating',
384
+ reason: claim.reason ?? 'creating',
385
385
  ttl: claim.ttl,
386
- queue: claim.wait !== false,
386
+ queue: claim.queue !== false,
387
387
  maxQueueDepth: claim.maxQueueDepth,
388
388
  });
389
389
  }
@@ -281,7 +281,7 @@ export declare const modelClaimSchema: z.ZodReadonly<z.ZodObject<{
281
281
  agent: "agent";
282
282
  system: "system";
283
283
  }>>;
284
- action: z.ZodString;
284
+ reason: z.ZodString;
285
285
  description: z.ZodOptional<z.ZodString>;
286
286
  field: z.ZodOptional<z.ZodString>;
287
287
  status: z.ZodOptional<z.ZodEnum<{
@@ -203,7 +203,9 @@ export const modelClaimSchema = z
203
203
  id: z.string(),
204
204
  actor: z.string(),
205
205
  participantKind: wireParticipantKindSchema,
206
- action: z.string(),
206
+ /** Human-readable phase (`'editing'`). The public SDK field; the WS/HTTP
207
+ * wire carries the same value as `action` (healed on read). */
208
+ reason: z.string(),
207
209
  description: z.string().optional(),
208
210
  field: z.string().optional(),
209
211
  status: z.enum(['active', 'queued']).optional(),
package/dist/errors.d.ts CHANGED
@@ -161,7 +161,9 @@ export interface ClaimContext {
161
161
  readonly claimId?: string;
162
162
  readonly actor?: string;
163
163
  readonly participantKind?: ParticipantKind;
164
- readonly action?: string;
164
+ /** Human-readable phase the holder is in (`'editing'`). Matches the public
165
+ * claim surface; the wire summary carries the same value as `action`. */
166
+ readonly reason?: string;
165
167
  readonly description?: string;
166
168
  readonly field?: string;
167
169
  readonly status?: string;
package/dist/errors.js CHANGED
@@ -162,7 +162,12 @@ export class AbloStaleContextError extends AbloError {
162
162
  }
163
163
  }
164
164
  function claimAction(claim) {
165
- return claim?.action;
165
+ if (!claim)
166
+ return undefined;
167
+ // The public `ClaimContext` exposes the phase as `reason`; the wire
168
+ // `WireClaimSummary` projection still carries it under `action`. Read both.
169
+ const c = claim;
170
+ return c.reason ?? c.action;
166
171
  }
167
172
  function claimDescription(claim) {
168
173
  if (!claim)
@@ -15,7 +15,7 @@ import { type SyncStoreContract } from './context.js';
15
15
  * - **One component, one import.** Consumers write the provider
16
16
  * once at the root; nothing else needs to plumb the engine.
17
17
  * - **Multiplayer is default.** React consumers are always browsers doing
18
- * multiplayer UI, so `useParticipant()` / `useAblo()` are always
18
+ * multiplayer UI, so `useWatch()` / `useAblo()` are always
19
19
  * available. No opt-in prop.
20
20
  * - **Declarative props for app glue.** `preventUnsavedChanges`,
21
21
  * `onSessionExpired`, `postBootstrap`, `resolveUsers` — each
@@ -114,11 +114,11 @@ export interface AbloProviderProps<R extends SchemaRecord = SchemaRecord> {
114
114
  export declare function AbloProvider<R extends SchemaRecord = SchemaRecord>(props: AbloProviderProps<R>): React.ReactElement;
115
115
  export type { EngineParticipant, ParticipantScope, ParticipantStatus };
116
116
  /**
117
- * Options for `useParticipant`. The hook reuses the engine's single
117
+ * Options for `useWatch`. The hook reuses the engine's single
118
118
  * WebSocket and opens a scoped claim on it when `scope` is provided:
119
119
  * one TCP connection, N logical sub-syncgroup participants.
120
120
  */
121
- export interface UseParticipantOptions {
121
+ export interface UseWatchOptions {
122
122
  readonly scope?: ParticipantScope;
123
123
  readonly ttlSeconds?: number | string | null;
124
124
  /** Tear down + don't re-join while true. */
@@ -149,7 +149,7 @@ export interface UseParticipantOptions {
149
149
  }
150
150
  /** @deprecated Use `ParticipantStatus`. */
151
151
  export type MeshParticipantStatus = ParticipantStatus;
152
- export interface UseParticipantReturn {
152
+ export interface UseWatchReturn {
153
153
  readonly participant: EngineParticipant | null;
154
154
  /** Everyone else on the engine's sync groups (`participant.presence.others`), bridged to React. */
155
155
  readonly peers: ReadonlyArray<Peer>;
@@ -163,15 +163,19 @@ export interface UseParticipantReturn {
163
163
  * lifecycle status. Auto-cleans up on unmount or when `paused`
164
164
  * flips to true.
165
165
  *
166
+ * `useWatch` is the React form of `ablo.<model>.watch` — scope-level
167
+ * read-interest + presence; returns the reactive participant facade
168
+ * (peers/claims/status).
169
+ *
166
170
  * The returned `participant` is an `EngineParticipant` — `.presence`
167
171
  * + `.claims` only — backed by the engine's existing socket. For
168
172
  * headless-bot patterns (a separate identity in the same browser
169
173
  * tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
170
174
  */
171
- export declare function useParticipant(opts: UseParticipantOptions): UseParticipantReturn;
175
+ export declare function useWatch(opts: UseWatchOptions): UseWatchReturn;
172
176
  /**
173
177
  * Read-only presence: the OTHER participants currently visible to this
174
- * connection, bridged to React. Unlike {@link useParticipant}, this does
178
+ * connection, bridged to React. Unlike {@link useWatch}, this does
175
179
  * NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
176
180
  * it is a pure reader of the engine's already-flowing presence stream.
177
181
  *
@@ -184,7 +188,7 @@ export declare function useParticipant(opts: UseParticipantOptions): UseParticip
184
188
  * Use this to answer "is anyone else here?" — e.g. suppressing live-cursor
185
189
  * broadcasts while alone — when some OTHER mount already owns the scope's
186
190
  * read interest (scope `leave` is not reference-counted, so a second
187
- * `useParticipant` on the same scope would warm-drop the owner's
191
+ * `useWatch` on the same scope would warm-drop the owner's
188
192
  * subscription on unmount).
189
193
  *
190
194
  * ```ts
@@ -204,12 +204,16 @@ const EMPTY_INTENTS = Object.freeze([]);
204
204
  * lifecycle status. Auto-cleans up on unmount or when `paused`
205
205
  * flips to true.
206
206
  *
207
+ * `useWatch` is the React form of `ablo.<model>.watch` — scope-level
208
+ * read-interest + presence; returns the reactive participant facade
209
+ * (peers/claims/status).
210
+ *
207
211
  * The returned `participant` is an `EngineParticipant` — `.presence`
208
212
  * + `.claims` only — backed by the engine's existing socket. For
209
213
  * headless-bot patterns (a separate identity in the same browser
210
214
  * tab), construct a second `Ablo({ kind: 'agent', ... })` directly.
211
215
  */
212
- export function useParticipant(opts) {
216
+ export function useWatch(opts) {
213
217
  const ctx = useContext(AbloInternalContext);
214
218
  const engine = ctx?.engine ?? null;
215
219
  const { paused = false } = opts;
@@ -342,7 +346,7 @@ export function useParticipant(opts) {
342
346
  }
343
347
  /**
344
348
  * Read-only presence: the OTHER participants currently visible to this
345
- * connection, bridged to React. Unlike {@link useParticipant}, this does
349
+ * connection, bridged to React. Unlike {@link useWatch}, this does
346
350
  * NOT enter/leave a scope (no `update_subscription`, no warm-TTL churn) —
347
351
  * it is a pure reader of the engine's already-flowing presence stream.
348
352
  *
@@ -355,7 +359,7 @@ export function useParticipant(opts) {
355
359
  * Use this to answer "is anyone else here?" — e.g. suppressing live-cursor
356
360
  * broadcasts while alone — when some OTHER mount already owns the scope's
357
361
  * read interest (scope `leave` is not reference-counted, so a second
358
- * `useParticipant` on the same scope would warm-drop the owner's
362
+ * `useWatch` on the same scope would warm-drop the owner's
359
363
  * subscription on unmount).
360
364
  *
361
365
  * ```ts
@@ -366,7 +370,7 @@ export function useParticipant(opts) {
366
370
  export function usePeers(scope) {
367
371
  const ctx = useContext(AbloInternalContext);
368
372
  const engine = ctx?.engine ?? null;
369
- // Resolve scope → groups through the schema (same idiom as useParticipant).
373
+ // Resolve scope → groups through the schema (same idiom as useWatch).
370
374
  // The stringified, sorted key is the stable effect dependency.
371
375
  const scopeKey = JSON.stringify(resolveParticipantSyncGroups(scope, engine?.schema).sort());
372
376
  const groups = useMemo(() => JSON.parse(scopeKey), [scopeKey]);
@@ -383,7 +387,7 @@ export function usePeers(scope) {
383
387
  // Plain useState + onChange — presence changes on join/leave/activity
384
388
  // only (never on cursor traffic, a separate channel), so this fires
385
389
  // rarely; a frame of stale presence is harmless (same rationale as
386
- // useParticipant's peers bridge).
390
+ // useWatch's peers bridge).
387
391
  setPeers(compute());
388
392
  return presence.onChange(() => setPeers(compute()));
389
393
  }, [engine, scopeKey]);
@@ -140,8 +140,9 @@ export interface SyncReactContext {
140
140
  }
141
141
  export declare const SyncContext: import("react").Context<SyncReactContext | null>;
142
142
  /**
143
- * Access the sync store from React components.
144
- * Must be used within a SyncProvider.
143
+ * Access the sync store from React components. The context is provided by
144
+ * `<AbloProvider>` (which renders the internal {@link SyncProvider}); public
145
+ * consumers wire `<AbloProvider client={ablo}>`, never this directly.
145
146
  */
146
147
  export declare function useSyncContext(): SyncReactContext;
147
148
  /**
@@ -162,18 +163,12 @@ export interface SyncProviderProps {
162
163
  children?: ReactNode;
163
164
  }
164
165
  /**
165
- * SyncProvider wires the sync store into React so SDK hooks
166
- * (useModel, useModels, useMutations) can access it.
166
+ * SyncProvider the INTERNAL low-level provider that wires a built sync store
167
+ * into React so SDK hooks (useModel, useModels, useMutations) can reach it.
167
168
  *
168
- * @example
169
- * import { SyncProvider } from '@abloatai/ablo/react';
170
- *
171
- * function App() {
172
- * return (
173
- * <SyncProvider store={syncStore} organizationId={orgId}>
174
- * <YourApp />
175
- * </SyncProvider>
176
- * );
177
- * }
169
+ * Public consumers do NOT use this directly (it is not exported from
170
+ * `@abloatai/ablo/react`). `<AbloProvider client={ablo}>` constructs the
171
+ * store from your `Ablo({ schema, apiKey })` client and renders this provider
172
+ * underneath reach for `<AbloProvider>`.
178
173
  */
179
174
  export declare function SyncProvider({ store, organizationId, schema, children, }: SyncProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SyncReactContext | null>>;
@@ -3,32 +3,27 @@ import { createContext, createElement, useContext } from 'react';
3
3
  import { AbloValidationError } from '../errors.js';
4
4
  export const SyncContext = createContext(null);
5
5
  /**
6
- * Access the sync store from React components.
7
- * Must be used within a SyncProvider.
6
+ * Access the sync store from React components. The context is provided by
7
+ * `<AbloProvider>` (which renders the internal {@link SyncProvider}); public
8
+ * consumers wire `<AbloProvider client={ablo}>`, never this directly.
8
9
  */
9
10
  export function useSyncContext() {
10
11
  const ctx = useContext(SyncContext);
11
12
  if (!ctx) {
12
- throw new AbloValidationError('useSyncContext must be used within a SyncProvider', {
13
+ throw new AbloValidationError('Sync hooks must be used within an <AbloProvider>.', {
13
14
  code: 'sync_context_missing_provider',
14
15
  });
15
16
  }
16
17
  return ctx;
17
18
  }
18
19
  /**
19
- * SyncProvider wires the sync store into React so SDK hooks
20
- * (useModel, useModels, useMutations) can access it.
20
+ * SyncProvider the INTERNAL low-level provider that wires a built sync store
21
+ * into React so SDK hooks (useModel, useModels, useMutations) can reach it.
21
22
  *
22
- * @example
23
- * import { SyncProvider } from '@abloatai/ablo/react';
24
- *
25
- * function App() {
26
- * return (
27
- * <SyncProvider store={syncStore} organizationId={orgId}>
28
- * <YourApp />
29
- * </SyncProvider>
30
- * );
31
- * }
23
+ * Public consumers do NOT use this directly (it is not exported from
24
+ * `@abloatai/ablo/react`). `<AbloProvider client={ablo}>` constructs the
25
+ * store from your `Ablo({ schema, apiKey })` client and renders this provider
26
+ * underneath reach for `<AbloProvider>`.
32
27
  */
33
28
  export function SyncProvider({ store, organizationId, schema, children, }) {
34
29
  return createElement(SyncContext.Provider, { value: { store, organizationId, schema } }, children);
@@ -2,8 +2,12 @@
2
2
  * @abloatai/ablo/react — React bindings (v0.3.0)
3
3
  *
4
4
  * Umbrella provider:
5
- * <AbloProvider schema={...} userId={...} orgId={...} fallback={<Skeleton/>}>
6
- * owns sync engine + multiplayer lifecycle; the `fallback` prop
5
+ * const ablo = Ablo({ schema, apiKey }) // build once — module scope or useMemo
6
+ * <AbloProvider client={ablo} fallback={<Skeleton/>}>
7
+ * — `client` is the only required prop (construct it yourself; the provider
8
+ * is the thin reactive binding, like `<Elements stripe={...}>`). `userId`
9
+ * is optional + informational. Owns sync engine + multiplayer lifecycle;
10
+ * the `fallback` prop
7
11
  * gates children on first bootstrap. Pass `fallback="passthrough"`
8
12
  * to disable the gate.
9
13
  * <ClientSideSuspense fallback={<Skeleton/>}> — NESTED gate inside an
@@ -27,7 +31,7 @@
27
31
  *
28
32
  * Multiplayer (always available — `<AbloProvider>` always constructs a client):
29
33
  * useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
30
- * useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
34
+ * useWatch({ scope }) — join multiplayer for a scope, get peers/claims
31
35
  *
32
36
  * ── Breaking changes from v0.2.x ───────────────────────────────────
33
37
  * Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
@@ -41,7 +45,7 @@
41
45
  * migration notes in CHANGELOG.md.
42
46
  */
43
47
  export type { DefaultSyncShape, ResolveSchema, ResolvePresence, ResolveClaims, ResolveUserMeta, ResolveModelKey, } from '../types/global.js';
44
- export { AbloProvider, useParticipant, usePeers, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseParticipantOptions, type UseParticipantReturn, type MeshParticipantStatus, } from './AbloProvider.js';
48
+ export { AbloProvider, useWatch, usePeers, useSync, useSyncStore, type AbloProviderProps, type ParticipantScope, type ParticipantStatus, type UseWatchOptions, type UseWatchReturn, type MeshParticipantStatus, } from './AbloProvider.js';
45
49
  export { ClientSideSuspense, type ClientSideSuspenseProps, } from './ClientSideSuspense.js';
46
50
  export { DefaultFallback } from './DefaultFallback.js';
47
51
  export type { SyncStoreContract } from './context.js';
@@ -2,8 +2,12 @@
2
2
  * @abloatai/ablo/react — React bindings (v0.3.0)
3
3
  *
4
4
  * Umbrella provider:
5
- * <AbloProvider schema={...} userId={...} orgId={...} fallback={<Skeleton/>}>
6
- * owns sync engine + multiplayer lifecycle; the `fallback` prop
5
+ * const ablo = Ablo({ schema, apiKey }) // build once — module scope or useMemo
6
+ * <AbloProvider client={ablo} fallback={<Skeleton/>}>
7
+ * — `client` is the only required prop (construct it yourself; the provider
8
+ * is the thin reactive binding, like `<Elements stripe={...}>`). `userId`
9
+ * is optional + informational. Owns sync engine + multiplayer lifecycle;
10
+ * the `fallback` prop
7
11
  * gates children on first bootstrap. Pass `fallback="passthrough"`
8
12
  * to disable the gate.
9
13
  * <ClientSideSuspense fallback={<Skeleton/>}> — NESTED gate inside an
@@ -27,7 +31,7 @@
27
31
  *
28
32
  * Multiplayer (always available — `<AbloProvider>` always constructs a client):
29
33
  * useAblo((ablo) => ablo.<model>.claim.state(...)) — reactive coordination reads
30
- * useParticipant({ scope }) — join multiplayer for a scope, get peers/claims
34
+ * useWatch({ scope }) — join multiplayer for a scope, get peers/claims
31
35
  *
32
36
  * ── Breaking changes from v0.2.x ───────────────────────────────────
33
37
  * Removed: <SyncProvider>, SyncContext, useSyncContext — folded into
@@ -41,7 +45,7 @@
41
45
  * migration notes in CHANGELOG.md.
42
46
  */
43
47
  // ── Umbrella provider + lifecycle hooks ────────────────────────────
44
- export { AbloProvider, useParticipant, usePeers, useSync, useSyncStore, } from './AbloProvider.js';
48
+ export { AbloProvider, useWatch, usePeers, useSync, useSyncStore, } from './AbloProvider.js';
45
49
  export { ClientSideSuspense, } from './ClientSideSuspense.js';
46
50
  export { DefaultFallback } from './DefaultFallback.js';
47
51
  // ── Status + errors + identity ─────────────────────────────────────
@@ -17,8 +17,9 @@ export function useMutators(schemaOrMutators, mutatorsOrOptions, maybeOptions) {
17
17
  const mutators = (isExplicit ? mutatorsOrOptions : schemaOrMutators);
18
18
  const options = (isExplicit ? maybeOptions : mutatorsOrOptions);
19
19
  if (!schema) {
20
- throw new AbloValidationError('useMutators: no schema available. Pass the schema as the first arg ' +
21
- 'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'mutators_schema_missing' });
20
+ throw new AbloValidationError('useMutators: no schema available. Pass the schema as the first arg, ' +
21
+ 'or build the <AbloProvider> above with `Ablo({ schema })` so the ' +
22
+ 'zero-arg overload can read it from context.', { code: 'mutators_schema_missing' });
22
23
  }
23
24
  const { undoScope } = options ?? {};
24
25
  return useMemo(() => {
@@ -34,8 +34,9 @@ export function useUndoScope(schemaOrName, nameOrOptions, maybeOptions) {
34
34
  const name = isExplicit ? nameOrOptions : schemaOrName;
35
35
  const options = (isExplicit ? maybeOptions : nameOrOptions);
36
36
  if (!schema) {
37
- throw new AbloValidationError('useUndoScope: no schema available. Pass the schema as the first arg ' +
38
- 'or wire SyncProvider with a `schema` prop when using the zero-arg overload.', { code: 'undo_scope_schema_missing' });
37
+ throw new AbloValidationError('useUndoScope: no schema available. Pass the schema as the first arg, ' +
38
+ 'or build the <AbloProvider> above with `Ablo({ schema })` so the ' +
39
+ 'zero-arg overload can read it from context.', { code: 'undo_scope_schema_missing' });
39
40
  }
40
41
  const scope = useMemo(() => {
41
42
  // Store is the identity for the manager — one per SyncProvider.
@@ -23,13 +23,13 @@
23
23
  export { z } from 'zod';
24
24
  export { field, indexed, getFieldMeta, type FieldBuilder, type FieldMeta } from './field.js';
25
25
  export { relation, type RelationDef, type RelationType } from './relation.js';
26
- export { tenancySchema, scopedViaRefSchema, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, type Tenancy, type ScopedViaRef, type TenancyInput, } from './tenancy.js';
26
+ export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, type Tenancy, type ScopedViaRef, type PolicyInput, } from './tenancy.js';
27
27
  export { planeSchema, DEFAULT_PLANE, type SchemaPlane } from './plane.js';
28
28
  export { syncDeltaCoreSchema, deltaAttributionSchema, deltaProvenanceSchema, syncDeltaRowSchema, participantKindSchema, confirmationStateSchema, backfillProvenanceSchema, DELTA_PLANES, type SyncDeltaCore, type DeltaAttribution, type DeltaProvenance, type SyncDeltaRow, type ParticipantKind, type ConfirmationState, type BackfillProvenance, } from './sync-delta-row.js';
29
29
  export { syncDeltaActionSchema, wireDeltaDataSchema, participantRefSchema, syncDeltaWireCoreSchema, clientSyncDeltaSchema, serverSyncDeltaSchema, type SyncDeltaAction, type WireDeltaData, type ParticipantRef, type SyncDeltaWireCore, type ClientSyncDelta, type ServerSyncDelta, } from './sync-delta-wire.js';
30
30
  export { model, scopeKindOf, type ModelDef, type ModelOptions, type LoadStrategy, type PersistOptions, type RelationRecord, type GrantsRef, } from './model.js';
31
31
  export { mutable, readOnly, type SugarOptions } from './sugar.js';
32
- export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
32
+ export { defineSchema, composeIdentitySyncGroups, type Schema, type SchemaRecord, type Model, type InferModel, type InferCreate, type InferModelNames, type BaseModelFields, type InsertValue, type UpsertValue, type UpdateValue, type DeleteId, type DefineSchemaOptions, type Casing, type CasingConvention, type CasingFn, composeEntitySyncGroups, type IdentityRole, type IdentityContext, type IdentityRoleSource, type EntityRole, type EntityContext, type EntityRoleSource, type RoleSource, type RoleContext, type SyncGroup, type SyncGroupInput, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, type GroupsInput, } from './schema.js';
33
33
  export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, type SchemaJSON, type ModelJSON, type RelationJSON, } from './serialize.js';
34
34
  export { selectModels } from './select.js';
35
35
  export { generateProvisionPlan, generateMigrationPlan, appSchemaName, camelToSnake, snakeToCamel, q, sqlType, type ProvisionPlan, type MigrationPlan, } from './ddl.js';
@@ -27,7 +27,7 @@ export { field, indexed, getFieldMeta } from './field.js';
27
27
  // Relation builders
28
28
  export { relation } from './relation.js';
29
29
  // Tenancy — the single source of truth for how a model's rows are tenant-scoped.
30
- export { tenancySchema, scopedViaRefSchema, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
30
+ export { tenancySchema, scopedViaRefSchema, policyInputSchema, resolvePolicy, resolveTenancy, tenancyColumn, DEFAULT_ORG_COLUMN, } from './tenancy.js';
31
31
  // Database plane — which DB a model's rows live in (`tenant` portable to a BYO
32
32
  // customer DB, `control` = Ablo's own). Sibling axis to `tenancy`.
33
33
  export { planeSchema, DEFAULT_PLANE } from './plane.js';
@@ -46,7 +46,7 @@ export { model, scopeKindOf, } from './model.js';
46
46
  // falls back to sensible defaults. See sugar.ts for the full pattern.
47
47
  export { mutable, readOnly } from './sugar.js';
48
48
  // Schema definition + type inference
49
- export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, } from './schema.js';
49
+ export { defineSchema, composeIdentitySyncGroups, composeEntitySyncGroups, identityRole, entityRole, extractIdentityIds, extractEntityIds, syncGroup, syncGroupSchema, syncGroupInputSchema, isSyncGroupInput, identityRoleSchema, entityRoleSchema, roleSchema, roleSourceSchema, scopeSchema, grantsRefSchema, groupsInputSchema, } from './schema.js';
50
50
  // Schema ⇄ JSON (control-plane transport for hosted multi-tenant)
51
51
  export { serializeSchema, parseSchema, toSchemaJSON, fromSchemaJSON, schemaHash, } from './serialize.js';
52
52
  // Schema projection — derive an app's subset from one canonical schema.