@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 +58 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/react.d.ts +2 -2
- package/dist/react.d.ts.map +1 -1
- package/dist/react.js +2 -1
- package/dist/react.js.map +1 -1
- package/docs/examples/account-multiplayer.md +2 -2
- package/docs/examples/scoped-agent.md +2 -2
- package/docs/groups.md +4 -4
- package/docs/identity.md +1 -1
- package/docs/integration-guide.md +3 -3
- package/docs/migration.md +95 -2
- package/docs/react.md +138 -31
- package/package.json +5 -4
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
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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
|
|
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
|
package/dist/react.d.ts.map
CHANGED
|
@@ -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,
|
|
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
|
|
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;
|
|
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
|
-
|
|
122
|
-
|
|
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 `
|
|
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 =
|
|
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 =
|
|
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? | `
|
|
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 `
|
|
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 `
|
|
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; `
|
|
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
|
-
|
|
304
|
+
Call `useAbloClient()` during render, then use its client in callbacks and effects:
|
|
305
305
|
|
|
306
306
|
```tsx
|
|
307
|
-
const ablo =
|
|
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@
|
|
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 — `
|
|
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
|
-
`
|
|
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 {
|
|
193
|
+
import { useAblo } from '@abloatai/ablo/react';
|
|
145
194
|
|
|
146
195
|
function ConnectionIndicator() {
|
|
147
|
-
const status =
|
|
196
|
+
const status = useAblo(ablo => ablo.status);
|
|
148
197
|
return <span role="status">{
|
|
149
|
-
status
|
|
150
|
-
? 'Connecting…' : status
|
|
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
|
-
|
|
184
|
-
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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
|
-
##
|
|
408
|
+
## Presence: the same core read
|
|
343
409
|
|
|
344
|
-
`
|
|
345
|
-
|
|
346
|
-
|
|
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
|
-
|
|
350
|
-
|
|
351
|
-
import { usePeers } from '@abloatai/ablo/react';
|
|
415
|
+
import { useAblo } from '@abloatai/ablo/react';
|
|
352
416
|
|
|
353
|
-
|
|
354
|
-
const
|
|
355
|
-
const
|
|
356
|
-
|
|
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
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
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.
|
|
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": "
|
|
149
|
-
"@abloatai/transaction": "
|
|
149
|
+
"@abloatai/humans": "0.64.1",
|
|
150
|
+
"@abloatai/transaction": "0.64.1",
|
|
150
151
|
"zod": "^4.4.3"
|
|
151
152
|
},
|
|
152
153
|
"peerDependencies": {
|