@abloatai/ablo 0.63.1 → 0.64.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,63 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.1
4
+
5
+ The React read and write boundaries are now explicit: `useAbloClient()` returns the writable client and `useAblo(selector)` returns render snapshots. The zero-argument `useAblo()` overload is removed. `ablo upgrade` reports migration hints; see [Upgrade Guide](./docs/migration.md).
6
+
7
+ Public schema registration reaches Transaction and Humans across emitted package declarations. Ambient mutators without a registered schema now report a schema diagnostic; prefer explicit-schema overloads. Internal package dependencies are exact, and CLI/SDK compatibility is checked as one release family.
8
+
9
+ `usePresence(..., { excludeSelf: true })` and `presence.forModel(model, id, { excludeSelf: true })` omit the current session. `useMutationFailure` owns React subscription cleanup. Custom framework adapters use `getAbloStore(client)` from `/client`. Generic collaboration subscriptions preserve their declared event tuples; attributed handlers use `collaboration.subscribe` (or transport `subscribeCollaboration`).
10
+
11
+ `AbloProvider` is now the sole provider vocabulary. Advanced framework adapters use `useAbloStoreContext` and `AbloStoreContextValue`; the missing-provider error code is `ablo_context_missing_provider`.
12
+
13
+ ## 0.64.0
14
+
15
+ React reads the same core objects as ordinary Ablo code. Read connection state with `ablo.status` outside React and `useAblo(ablo => ablo.status)` inside React; its type is `Ablo.Status`. Presence reads through `useAblo(ablo => ablo.presence.others)` or `ablo.presence.forModel(model, id)` now react to activity changes. The unregistered-schema selector also preserves core properties and methods.
16
+
17
+ This is a breaking API cleanup for 0.64, with no compatibility aliases:
18
+
19
+ - Replace `useSyncStatus()` with `useAblo(ablo => ablo.status)`, and `SyncStatusSnapshot` with `Ablo.Status`. Replace the client's raw `syncStatus` with `status`; raw transport details remain internal to the store.
20
+ - Replace `useSync()` with `useAblo()`. The client is available during startup; await `client.ready()` before operations that require initialization.
21
+ - Replace `usePeers()` with `useAblo(ablo => ablo.presence.others)`. Replace scoped peer reads with `useAblo(ablo => ablo.presence.forModel(model, id))`; this returns sessions, including the current session when it has activity on that record. Filter by session ID when you specifically need other sessions.
22
+ - Replace `useMutationFailureListener(listener)` with `useEffect(() => ablo?.onMutationFailure(listener), [ablo, listener])`; the payload type is `Ablo.MutationFailure`.
23
+ - Handle provider startup errors with `<AbloProvider onError={...}>` instead of `useErrorListener()`.
24
+ - Read application identity from your authentication client or application context instead of `useCurrentUserId()`. The informational `AbloProvider.userId` prop is removed; authenticated Ablo authority still comes from the client's session.
25
+ - `GroupScope` leaves the React entry point with `usePeers`; presence filtering uses model and record arguments.
26
+ - `useSyncStore`, `SyncStoreContract`, and `useReactive` are no longer React entry-point exports. Use the core client; framework integration internals remain under the humans runtime boundary.
27
+ - `ClientSideSuspense` and `DefaultFallback` are no longer public components. Supply your own UI through the provider's `fallback` prop or render from `ablo.status`.
28
+
29
+ The React entry point explicitly exports its supported bindings instead of forwarding all implementation exports through a wildcard. Existing model operations, claims, and reading-activity lifetimes remain on their core owners.
30
+
31
+ React type annotations now follow their owners. The entry point has eight runtime exports (`Ablo`, `humans`, `AbloProvider`, `createAbloReact`, `useAblo`, `usePresence`, `useMutators`, `useUndoScope`) and no standalone type exports:
32
+
33
+ | Removed React type export | Replacement |
34
+ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
35
+ | `AbloOptions` | `Ablo.Options` |
36
+ | `AbloReads` | `Ablo.Reads` |
37
+ | `HumansSurface` | Infer the plugin result; low-level plugin authors can still import the type from `@abloatai/humans` |
38
+ | `AbloProviderProps` | `AbloProvider.Props` |
39
+ | `AbloReactBinding` | Infer the binding, or `ReturnType<typeof createAbloReact<Models>>` |
40
+ | `PresenceModelSelector` | Infer an inline selector against the bound schema |
41
+ | `UseAbloModelOptions` | `useAblo.Options<Row>` |
42
+ | `UseAbloModelResult` | `useAblo.Result<Row>` |
43
+ | `UseAbloHydratedModelResult` | `useAblo.Result<Row>`; handle optional data after local removal, even with `initial` |
44
+ | `InvokerFor` | `typeof mutations.model.operation` |
45
+ | `MutatorInvokers` | `useMutators.Result<typeof definitions>` |
46
+ | `UseMutatorsOptions` | `useMutators.Options<typeof schema>` |
47
+ | `UseUndoScopeResult` | `useUndoScope.Result<typeof schema>` |
48
+ | `ResolveSchema` | Existing core `Ablo.ResolveSchema`, when using module registration |
49
+ | `ResolveModelKey` | `keyof typeof schema.models` |
50
+ | `DefaultSyncShape`, `ResolveUserMeta`, `ResolveClaimMeta` | Internal registration details; use schema inference and the core presence/claim types |
51
+ | `MutateActions` | `Ablo.Mutator.Transaction<typeof schema>['mutate'][ModelKey]` |
52
+ | `ReaderActions` | `Ablo.Mutator.Transaction<typeof schema>['read'][ModelKey]` |
53
+ | `ReaderFindOptions` | Infer read options; advanced callers can import the existing type from `@abloatai/ablo/client` |
54
+
55
+ `ModelScope` is no longer re-exported by React; its existing advanced owner is `@abloatai/ablo/client`. Ordinary model reads use their `state` option.
56
+
57
+ `createAbloReact(schema)` now specializes only types and returns the existing module-level provider and hooks. It creates no React context or component identity. Hooks always read the nearest AbloProvider, including when bindings for different schemas are nested; do not call a binding's hooks below a provider for another schema. Recreating a binding no longer remounts its children. Keep the application binding at module scope for clear ownership.
58
+
59
+ Selectors now preserve equal snapshot identity, detach nested row data, and subscribe to claim events. Subscriptions survive suspended renders and catch changes between render and subscription. Initial rows match server rendering during hydration, then yield to local data; a later removal no longer restores the seed. Local cache absence remains distinct from server absence.
60
+
3
61
  ## 0.63.1
4
62
 
5
63
  React workspaces can render immediately while collaboration connects.
package/dist/index.d.ts CHANGED
@@ -8,4 +8,12 @@ export { Ablo as default } from '@abloatai/transaction';
8
8
  */
9
9
  export { noopLogger } from '@abloatai/transaction/logger';
10
10
  export type { Logger } from '@abloatai/transaction/logger';
11
+ /** Application registration, owned here so augmentation survives re-exports. */
12
+ export interface Register {
13
+ }
14
+ type PublicRegister = Register;
15
+ declare module '@abloatai/transaction/types/global' {
16
+ interface Register extends PublicRegister {
17
+ }
18
+ }
11
19
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAC;AACtC,OAAO,EAAE,IAAI,IAAI,OAAO,EAAE,MAAM,uBAAuB,CAAC;AAExD;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,8BAA8B,CAAC;AAC1D,YAAY,EAAE,MAAM,EAAE,MAAM,8BAA8B,CAAC;AAE3D,gFAAgF;AAEhF,MAAM,WAAW,QAAQ;CAAG;AAC5B,KAAK,cAAc,GAAG,QAAQ,CAAC;AAI/B,OAAO,QAAQ,oCAAoC,CAAC;IAClD,UAAU,QAAS,SAAQ,cAAc;KAAG;CAC7C"}
package/dist/react.d.ts CHANGED
@@ -1,4 +1,4 @@
1
+ /** Core client and the explicitly supported React bindings. */
1
2
  export { Ablo, humans } from '@abloatai/humans';
2
- export type { AbloOptions, AbloReads, HumansSurface } from '@abloatai/humans';
3
- export * from '@abloatai/humans/react';
3
+ export { AbloProvider, createAbloReact, useAblo, useAbloClient, useMutationFailure, usePresence, useMutators, useUndoScope, } from '@abloatai/humans/react';
4
4
  //# sourceMappingURL=react.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,YAAY,EAAE,WAAW,EAAE,SAAS,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAC9E,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"react.d.ts","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,OAAO,EACP,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,wBAAwB,CAAC"}
package/dist/react.js CHANGED
@@ -1,3 +1,4 @@
1
+ /** Core client and the explicitly supported React bindings. */
1
2
  export { Ablo, humans } from '@abloatai/humans';
2
- export * from '@abloatai/humans/react';
3
+ export { AbloProvider, createAbloReact, useAblo, useAbloClient, useMutationFailure, usePresence, useMutators, useUndoScope, } from '@abloatai/humans/react';
3
4
  //# sourceMappingURL=react.js.map
package/dist/react.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAEhD,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"react.js","sourceRoot":"","sources":["../src/react.ts"],"names":[],"mappings":"AAAA,+DAA+D;AAC/D,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAChD,OAAO,EACL,YAAY,EACZ,eAAe,EACf,OAAO,EACP,aAAa,EACb,kBAAkB,EAClB,WAAW,EACX,WAAW,EACX,YAAY,GACb,MAAM,wBAAwB,CAAC"}
@@ -118,8 +118,8 @@ The example claims both `executionOwner` and `executionState`, writes with the
118
118
  claim, simulates generating, then stays idle while still holding ownership.
119
119
  The `executionOwner` row value is historical metadata after release; the UI uses
120
120
  `useAblo(client => client.conversations, id).claimed` for reactive ownership.
121
- The row form subscribes to claim events; a selector-only call to `claim.state`
122
- is only a snapshot and can miss ownership changes. Applications decide how to recover
121
+ Both the row form and `useAblo(client => client.conversations.claim.state({ id }))`
122
+ subscribe to claim events. Applications decide how to recover
123
123
  an interrupted execution-state field; a lease is not proof of ongoing generation.
124
124
 
125
125
  Text buffering, tool execution and queued-message scheduling belong to the
@@ -115,11 +115,11 @@ the agent can never reach a workspace its triggering user couldn't.
115
115
 
116
116
  ## 3. Write: it fans out to everyone on that workspace
117
117
 
118
- Inside any component under the provider, grab the scoped client with `useAblo()`
118
+ Inside any component under the provider, grab the scoped client with `useAbloClient()`
119
119
  and write. The connection is already narrowed to `workspace:<workspaceId>` from Step 2.
120
120
 
121
121
  ```ts
122
- const ablo = useAblo();
122
+ const ablo = useAbloClient();
123
123
 
124
124
  // Other participants subscribed to workspace:<workspaceId> — a reviewer agent, a
125
125
  // person watching in the UI — receive this delta in realtime. Participants on
package/docs/groups.md CHANGED
@@ -46,7 +46,7 @@ question you have.
46
46
  ablo.records.onChange((docs) => render(docs));
47
47
 
48
48
  // In React: who else is visible on this client's scoped groups?
49
- const peers = usePeers({ records: documentId });
49
+ const peers = useAblo(ablo => ablo.presence.forModel('records', documentId)) ?? [];
50
50
 
51
51
  // Stop this write if the thing I read moved while I composed it.
52
52
  const record = await ablo.records.read({ id: 's-1' });
@@ -57,10 +57,10 @@ await ablo.blocks.update({ id, data, reads: [record] });
57
57
  | Question | Channel | Arrives |
58
58
  | --- | --- | --- |
59
59
  | What do the rows say right now? | `onChange` | As deltas land, on the socket |
60
- | Who else is working here? | `usePeers` over the session/client groups | As participants connect, disconnect, or change activity |
60
+ | Who else is working here? | `useAblo(ablo => ablo.presence.others)` over the session/client groups | As participants connect, disconnect, or change activity |
61
61
  | Did the premise for **this** write move? | `reads` on the write | On that write's receipt, before it applies |
62
62
 
63
- `onChange` and `usePeers` use the reactive client's socket. `reads` rides the
63
+ `onChange` and `useAblo(ablo => ablo.presence.others)` use the reactive client's socket. `reads` rides the
64
64
  commit, so it reaches a socketless actor over HTTP too. The row returned by
65
65
  `read` privately carries its model, id, and
66
66
  watermark; passing that row in `reads` is enough to protect a later write. Ablo
@@ -183,6 +183,6 @@ them coarse everywhere else.
183
183
  [`concurrency-convention.md`](./concurrency-convention.md) (§4 and §5).
184
184
  - **The mechanics**, the three coordination blocks underneath, are
185
185
  [`coordination.md`](./coordination.md).
186
- - **Presence** is read with `usePeers`; active exclusions remain on the
186
+ - **Presence** is read with `useAblo(ablo => ablo.presence.others)`; active exclusions remain on the
187
187
  `claim` namespace. See [`react.md`](./react.md) and
188
188
  [`coordination.md`](./coordination.md).
package/docs/identity.md CHANGED
@@ -585,7 +585,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
585
585
  So an agent's reachable set **accretes** as it works — no extra subscribe call.
586
586
 
587
587
  3. **Presence follows those same groups.** A reactive client announces one
588
- participant on its connection; `usePeers(scope)` filters the roster already
588
+ participant on its connection; `ablo.presence.forModel(model, id)` filters the roster already
589
589
  visible through the session and connection groups. There is no second
590
590
  membership lease or participant handle. Use `claim` separately when work needs
591
591
  exclusion. See [React](./react.md#usepeers-read-only-presence).
@@ -287,7 +287,7 @@ In React, selector `useAblo` is the public read API:
287
287
  ```tsx
288
288
  'use client';
289
289
 
290
- import { useAblo } from '@abloatai/ablo/react';
290
+ import { useAblo, useAbloClient } from '@abloatai/ablo/react';
291
291
 
292
292
  export function ReportRow({
293
293
  report: serverReport,
@@ -301,10 +301,10 @@ export function ReportRow({
301
301
  }
302
302
  ```
303
303
 
304
- Use zero-argument `useAblo()` only in callbacks and effects:
304
+ Call `useAbloClient()` during render, then use its client in callbacks and effects:
305
305
 
306
306
  ```tsx
307
- const ablo = useAblo();
307
+ const ablo = useAbloClient();
308
308
  ```
309
309
 
310
310
  ## 4. Write State
package/docs/migration.md CHANGED
@@ -3,7 +3,100 @@
3
3
  > A safe workflow for upgrading the pre-1.0 SDK.
4
4
 
5
5
  Ablo is pre-1.0, so a minor release may contain a breaking API change. Patch
6
- releases remain compatible within the same minor line.
6
+ releases normally remain compatible within the same minor line. The 0.64.1
7
+ release includes the explicit React and collaboration migrations below.
8
+
9
+ ## React and package boundaries after 0.64
10
+
11
+ Run `npx ablo upgrade` for migration hints before editing. It previews changes;
12
+ `--write` applies only mechanical rewrites. React changes requiring application
13
+ judgment are reported with their replacements.
14
+
15
+ | Previous API | Current API |
16
+ | --- | --- |
17
+ | `useSync()` or zero-argument `useAblo()` | `useAbloClient()` for actions; await `client.ready()` when initialization is required |
18
+ | `useSyncStatus()` | `useAblo(client => client.status)` |
19
+ | `usePeers()` | `useAblo(client => client.presence.others)` |
20
+ | Scoped peers | `usePresence(client => client.records, id, { excludeSelf: true })` |
21
+ | `useMutationFailureListener(listener)` | `useMutationFailure(listener)` |
22
+ | `useErrorListener()` | `AbloProvider`'s `onError` prop for startup errors |
23
+ | `useCurrentUserId()` | Your application's authentication context |
24
+ | `useSDKSyncStore()` / `useSyncStore()` | Public client operations; `getAbloStore(client)` from `/client` for custom local-store adapters |
25
+ | `ClientSideSuspense` / `DefaultFallback` | Application UI through the provider's `fallback` prop |
26
+ | `GroupScope` for presence | Model and record arguments |
27
+
28
+ `useAblo(selector)` returns detached snapshots for rendering. They have no model
29
+ methods or relation accessors. `useAbloClient()` returns the writable client and
30
+ does not subscribe to row changes. Do not perform writes inside a selector.
31
+
32
+ ### Presence, activities and claims
33
+
34
+ `Activity` is replaced by `Ablo.PresenceActivity`, including for agents. A session
35
+ is `Ablo.PresenceSession`: identify the execution with `presenceSessionId` and its
36
+ actor with `participant.id` and `participant.kind`. A participant can have several
37
+ sessions, so exclude self by session rather than user ID.
38
+
39
+ `presence.active` contains this session's activities; `presence.others` contains
40
+ other sessions, each with an `activities` array. `forModel(model, id)` includes
41
+ self by default; pass `{ excludeSelf: true }` as its third argument to omit self.
42
+ The same option on `usePresence` still announces the component's reading activity
43
+ and cleans it up on unmount.
44
+
45
+ An activity has `operation`, `target`, `source`, and ISO timestamp fields.
46
+ Read the model and row from `activity.target.model` and `.id`; field-specific
47
+ activity uses `.field` or `.fields` (never both, and always with a row ID).
48
+ Activities describe visible work; they do not grant write authority.
49
+
50
+ Claims are acquired through `await using claim = await client.records.claim({ id })`.
51
+ The protected row is `claim.data`; pass `claim` to the write. Claim lookup uses
52
+ `client.records.claim.state({ id })` and `.queue({ id })`. Claim metadata is on
53
+ `claim.target.meta`, and a multi-field claim uses `fields` in the claim target.
54
+ Do not reconstruct a claim handle from a presence activity. See [Claims](./claims.md).
55
+
56
+ ### Imports and schema ownership
57
+
58
+ The root factory creates a headless client; `/client` creates the reactive client.
59
+ Import the `Ablo` type namespace beside the factory you use. Both expose the
60
+ shared schema, presence-activity and claim types. Reactive-only types such as
61
+ `Ablo.Status`, `Ablo.Reads`, `Ablo.Store`, and `Ablo.Mutator` belong to `/client`
62
+ (or `/react` for its re-exported reactive factory).
63
+
64
+ Prefer explicit schema inference:
65
+
66
+ ```ts
67
+ import { defineMutators } from '@abloatai/ablo/client';
68
+ import { useMutators, useUndoScope } from '@abloatai/ablo/react';
69
+ import { schema } from './schema';
70
+
71
+ const definitions = defineMutators(schema, {
72
+ records: {
73
+ rename: async ({ tx, args }: {
74
+ tx: import('@abloatai/ablo/client').Ablo.Mutator.Transaction<typeof schema>;
75
+ args: { id: string; title: string };
76
+ }) => tx.mutations.records.update({ id: args.id, title: args.title }),
77
+ },
78
+ });
79
+
80
+ function useRecordActions() {
81
+ const { scope } = useUndoScope(schema, 'record-editor');
82
+ return useMutators(schema, definitions, { undoScope: scope });
83
+ }
84
+ ```
85
+
86
+ Ambient registration remains available for a single application. Its module must
87
+ be included in that TypeScript program and import `@abloatai/ablo` before
88
+ augmenting `Register`. The public registration now reaches the downstream
89
+ Transaction and Humans resolvers through the emitted declarations. A package
90
+ compiled independently cannot inherit a consuming app's ambient declaration.
91
+
92
+ ### Structural collaboration adapters
93
+
94
+ Generic `subscribe(event, handler)` preserves exactly the argument tuples in your
95
+ event map. Receive optional server attribution through the session's
96
+ `collaboration.subscribe(event, (payload, context) => ...)`; low-level transport
97
+ adapters use `subscribeCollaboration`. Model-scoped `client.records.events.on`
98
+ continues to supply authenticated context as its second callback argument.
99
+ Legacy servers may omit context on application events; handle `undefined` there.
7
100
 
8
101
  ## Upgrade safely
9
102
 
@@ -14,7 +107,7 @@ releases remain compatible within the same minor line.
14
107
  5. Run the three-state deployment plan, then push the exact reviewed plan.
15
108
 
16
109
  ```bash
17
- npm install @abloatai/ablo@0.48
110
+ npm install --save-exact @abloatai/ablo@<version> @abloatai/cli@<version>
18
111
  npx ablo docs
19
112
  npx ablo docs api
20
113
  npx ablo plan
package/docs/react.md CHANGED
@@ -13,9 +13,58 @@ agents, start with [Integration Guide](/docs/integration-guide).
13
13
  The React bindings ship with the main package — no extra install.
14
14
 
15
15
  ```ts
16
- import { useAblo } from '@abloatai/ablo/react';
16
+ import { useAblo, useAbloClient } from '@abloatai/ablo/react';
17
17
  ```
18
18
 
19
+ React uses the same objects and operations as core Ablo. Read `ablo.status`
20
+ with `useAblo(ablo => ablo.status)`, select `ablo.presence.others` for other
21
+ sessions, and call ordinary model methods from event handlers. React owns the
22
+ subscription or component lifetime; the client owns the behavior.
23
+
24
+ For mutation failures, let the hook own the subscription:
25
+
26
+ ```tsx
27
+ import { useMutationFailure } from '@abloatai/ablo/react';
28
+
29
+ useMutationFailure(reportFailure);
30
+ ```
31
+
32
+ `reportFailure` receives `Ablo.MutationFailure`. Read application identity from
33
+ your auth provider; Ablo's authenticated authority comes from its session.
34
+
35
+ ## Types follow their owners
36
+
37
+ Most calls infer their types from your schema. For wrapper components or explicit
38
+ annotations, use the name you already imported:
39
+
40
+ ```tsx
41
+ import { Ablo, AbloProvider, useAblo } from '@abloatai/ablo/react';
42
+ import { schema } from './schema';
43
+
44
+ type Models = (typeof schema)['models'];
45
+ type Chat = Ablo.Schema.InferRow<typeof schema, 'chats'>;
46
+
47
+ function Provider(props: AbloProvider.Props<Models>) {
48
+ return <AbloProvider {...props} />;
49
+ }
50
+
51
+ function ChatView({ result }: { result: useAblo.Result<Chat> }) {
52
+ return <p>{result.data?.title}</p>;
53
+ }
54
+ ```
55
+
56
+ The React annotation types are `AbloProvider.Props`, `useAblo.Options`,
57
+ `useAblo.Result`, `useMutators.Options`, `useMutators.Result` and
58
+ `useUndoScope.Result`. Core types remain under `Ablo`, including `Ablo.Options`,
59
+ `Ablo.Reads`, `Ablo.Status` and `Ablo.PresenceSession`. Import annotation owners
60
+ from the SDK entry point; functions destructured from an app binding infer their
61
+ call types but do not create new TypeScript namespaces in the app module.
62
+
63
+ `createAbloReact(schema)` specializes types and returns the existing provider and
64
+ hooks. Define your binding at module scope. It creates no client, React context
65
+ or component identity. Hooks always read the nearest provider: use the binding
66
+ that matches that provider's client schema.
67
+
19
68
  ## Building the client
20
69
 
21
70
  You build the Ablo client once — that's where the schema, the session endpoint,
@@ -37,12 +86,12 @@ export const ablo = Ablo({
37
86
  });
38
87
 
39
88
  // The typed binding: capture the schema once, and every component imports
40
- // born-typed hooks from this file — `useAblo()` takes no type arguments,
89
+ // born-typed hooks from this file — `useAbloClient()` takes no type arguments,
41
90
  // and a selector's `ablo` parameter knows your models.
42
- export const { AbloProvider, useAblo, usePresence } = createAbloReact(schema);
91
+ export const { AbloProvider, useAblo, useAbloClient, usePresence, useMutationFailure } = createAbloReact(schema);
43
92
  ```
44
93
 
45
- Import `AbloProvider`, `useAblo`, and `usePresence` from `lib/ablo` rather than from the
94
+ Import `AbloProvider`, `useAbloClient`, `useAblo`, and `usePresence` from `lib/ablo` rather than from the
46
95
  package, and the schema generic never appears at a call site again — the
47
96
  same one-binding-file convention as tRPC's `createTRPCReact` or
48
97
  react-redux's typed hooks.
@@ -135,19 +184,19 @@ for the complete ownership boundary and runnable component.
135
184
 
136
185
  ## Render immediately with connection status
137
186
 
138
- `useSyncStatus()` works during provider startup, in passthrough children and in
187
+ `useAblo(ablo => ablo.status)` works during provider startup, in passthrough children and in
139
188
  custom fallbacks. It observes the client's status before row scope is available.
140
189
  It still requires a provider. Data hooks that require authenticated scope must
141
190
  wait for readiness.
142
191
 
143
192
  ```tsx
144
- import { useSyncStatus } from '@abloatai/ablo/react';
193
+ import { useAblo } from '@abloatai/ablo/react';
145
194
 
146
195
  function ConnectionIndicator() {
147
- const status = useSyncStatus();
196
+ const status = useAblo(ablo => ablo.status);
148
197
  return <span role="status">{
149
- status.name === 'initial' || status.name === 'connecting'
150
- ? 'Connecting…' : status.name
198
+ status?.name === 'initial' || status?.name === 'connecting'
199
+ ? 'Connecting…' : status?.name
151
200
  }</span>;
152
201
  }
153
202
 
@@ -171,6 +220,7 @@ export function ReportView({ report: serverReport }: { report: { id: string; loc
171
220
  { initial: serverReport },
172
221
  );
173
222
 
223
+ if (!report) return <p>This report is not in the local cache.</p>;
174
224
  return <article>{report.location}{claimed && <span>Claimed</span>}</article>;
175
225
  }
176
226
  ```
@@ -180,14 +230,30 @@ The row form subscribes to both data and claim events. It returns `data`,
180
230
 
181
231
  For data-only reads, selectors such as
182
232
  `useAblo(ablo => ablo.weatherReports.local.get(id))` track model fields.
183
- A selector-only `claim.state({ id })` read does not subscribe to the claim event
184
- stream. Use the row form above when the UI displays ownership.
233
+ Claim selectors subscribe too: `useAblo(ablo => ablo.weatherReports.claim.state({ id }))`
234
+ updates when ownership changes. Use the row form when you want data and claims together.
235
+
236
+ Selectors must be pure synchronous reads. Ablo copies selected rows, arrays and plain
237
+ records into frozen snapshots, including nested data and computed fields. Unchanged
238
+ data retains its snapshot identity across renders. Treat copied dates as read-only;
239
+ model API handles and other class instances retain their original identity. Read
240
+ data inside the selector to subscribe to it. Selector errors reach React error boundaries.
241
+
242
+ `initial` supplies the server HTML and first hydration render; React then reads the
243
+ current local row. Once a local row has appeared, removing it returns `undefined`
244
+ instead of restoring the seed, so `data` remains optional even with `initial`.
245
+
246
+ A local cache miss (`undefined` or an empty list) does not prove that the server has
247
+ no matching data. Local reads do not fetch rows, and connection status does not
248
+ indicate query completeness. Use your route loader and an awaited server read, such
249
+ as `await ablo.weatherReports.get({ id })`, to establish server results and handle
250
+ request errors before passing an initial row to React.
185
251
 
186
252
  Use the zero-argument form only when you need the full client for callbacks,
187
253
  effects, or writes:
188
254
 
189
255
  ```tsx
190
- const abloClient = useAblo();
256
+ const abloClient = useAbloClient();
191
257
  ```
192
258
 
193
259
  Prefer selector reads like `useAblo((ablo) => ablo.<model>.local.get(id))`. Older hooks
@@ -237,7 +303,7 @@ For client event handlers, get the application-owned client and call the same
237
303
  model client:
238
304
 
239
305
  ```tsx
240
- const ablo = useAblo();
306
+ const ablo = useAbloClient();
241
307
 
242
308
  async function markReady() {
243
309
  if (!ablo) return;
@@ -311,7 +377,7 @@ signals. The model and record choose the authorized sync group; the payload
311
377
  does not need routing fields or caller-authored identity.
312
378
 
313
379
  ```tsx
314
- const ablo = useAblo();
380
+ const ablo = useAbloClient();
315
381
 
316
382
  useEffect(() => {
317
383
  if (!ablo) return;
@@ -339,32 +405,73 @@ durable model fields when state must survive reconnects. The sending connection
339
405
  does not receive its own event. Coalesce or throttle pointer movement in the
340
406
  application; model events do not currently declare a per-event `maxHz`.
341
407
 
342
- ## usePeers: read-only presence
408
+ ## Presence: the same core read
343
409
 
344
- `usePeers` reads the presence stream already flowing for the client's scoped
345
- groups. It does not create a second membership or lease and does not change the
346
- connection's subscriptions.
410
+ Select `ablo.presence.others` for the other sessions visible to this client, or
411
+ filter by a model and record. These reads subscribe to changes without starting
412
+ an activity or claiming ownership.
347
413
 
348
414
  ```tsx
349
- 'use client';
350
-
351
- import { usePeers } from '@abloatai/ablo/react';
415
+ import { useAblo } from '@abloatai/ablo/react';
352
416
 
353
- export function CursorBroadcaster({ workspaceId }: { workspaceId: string }) {
354
- const peers = usePeers({ entryCollections: workspaceId });
355
- const alone = !peers.some((p) => p.participantKind === 'user');
356
- // suppress live-cursor broadcasts while alone
417
+ function Readers({ conversationId }: { conversationId: string }) {
418
+ const sessions = useAblo(ablo => ablo.presence.forModel('conversations', conversationId)) ?? [];
419
+ const people = new Set(sessions.filter(session => session.participant.kind === 'user')
420
+ .map(session => session.participant.id));
421
+ return <span>{people.size} people, {sessions.length} sessions</span>;
357
422
  }
358
423
  ```
359
424
 
360
- Pass a schema-shaped group scope to filter the visible peers, or omit it for
361
- everyone on the client's groups. Returns `ReadonlyArray<Peer>`, where each `Peer` carries
362
- `participantKind` (`'user' | 'agent' | 'system'`), `participantId`, optional
363
- `label`, `syncGroups`, `activity`, `lastActive`, and optional `activeClaims`.
364
- Use `ablo.<model>.claim` when the caller needs exclusion; reading presence does
365
- not claim anything.
425
+ `usePresence(ablo => ablo.conversations, conversationId)` additionally owns a
426
+ reading activity for the component's lifetime. Use it when mounting the component
427
+ should announce that this session is reading the record. Core code owns the same
428
+ lifecycle explicitly with `ablo.conversations.presence.read(conversationId)` and
429
+ its returned cleanup function.
366
430
 
367
431
  ## Next.js
368
432
 
369
433
  The Next.js [App Router landing](./examples/nextjs.md) walks through Server Components
370
434
  + Server Actions + `useAblo` together.
435
+
436
+ ## Separate packages and monorepos
437
+
438
+ Put the schema in an application-owned package, with a public export for its
439
+ value and type. Put the React binding in a client module which imports that
440
+ schema, and export its inferred declarations. Components import their hooks from
441
+ that binding package. Build the schema package before its dependents and test the
442
+ emitted declarations without workspace source aliases.
443
+
444
+ ```ts
445
+ 'use client';
446
+ import { createAbloReact } from '@abloatai/ablo/react';
447
+ import { schema } from '@app/schema';
448
+
449
+ export const { AbloProvider, useAbloClient, useAblo, usePresence, useMutationFailure } =
450
+ createAbloReact(schema);
451
+ ```
452
+
453
+ Use one binding per schema, and mount its hooks under a provider for that schema.
454
+ The binding specializes types; it does not create an isolated runtime context or
455
+ validate that a different binding's provider has the same schema. Keep React and
456
+ the Ablo package family deduplicated across packages. Reusable libraries should
457
+ accept a schema or typed binding rather than declare an application-global schema.
458
+ An app's ambient registration cannot retroactively change a separately compiled
459
+ library. Use `defineMutators(schema, definitions)`, `useMutators(schema, definitions)`
460
+ and `useUndoScope(schema, name)` across those boundaries.
461
+
462
+ ## Mutation failures and scoped collaborators
463
+
464
+ ```tsx
465
+ const peers = usePresence(client => client.records, id, { excludeSelf: true });
466
+ useMutationFailure(({ error }) => showToast(error.message));
467
+ ```
468
+
469
+ `useMutationFailure` uses the latest committed callback, moves its subscription
470
+ when the provider client changes, and unsubscribes on unmount. For non-React
471
+ callers, `client.onMutationFailure` returns the cleanup function.
472
+
473
+ For a custom framework adapter, `getAbloStore(client)` from
474
+ `@abloatai/ablo/client` returns the supported `Ablo.Store` contract: local pool
475
+ access, mutation observation, and scope management. Ordinary writes, confirmation,
476
+ reconnection and model events already have public client methods; they do not
477
+ require a store adapter.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.63.1",
3
+ "version": "0.64.1",
4
4
  "description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -132,7 +132,8 @@
132
132
  "build:docs": "node scripts/build-blume-docs.mjs",
133
133
  "lint:docs-site": "node scripts/build-blume-docs.mjs --check",
134
134
  "lint:docs": "node scripts/check-doc-drift.mjs",
135
- "lint:pkg": "publint"
135
+ "lint:pkg": "publint",
136
+ "verify:package-contract": "node scripts/package-contract/index.mjs"
136
137
  },
137
138
  "publishConfig": {
138
139
  "access": "public",
@@ -145,8 +146,8 @@
145
146
  "directory": "packages/ablo"
146
147
  },
147
148
  "dependencies": {
148
- "@abloatai/humans": "^0.63.1",
149
- "@abloatai/transaction": "^0.63.1",
149
+ "@abloatai/humans": "0.64.1",
150
+ "@abloatai/transaction": "0.64.1",
150
151
  "zod": "^4.4.3"
151
152
  },
152
153
  "peerDependencies": {