@abloatai/ablo 0.63.1 → 0.64.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,53 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.64.0
4
+
5
+ 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.
6
+
7
+ This is a breaking API cleanup for 0.64, with no compatibility aliases:
8
+
9
+ - 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.
10
+ - Replace `useSync()` with `useAblo()`. The client is available during startup; await `client.ready()` before operations that require initialization.
11
+ - 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.
12
+ - Replace `useMutationFailureListener(listener)` with `useEffect(() => ablo?.onMutationFailure(listener), [ablo, listener])`; the payload type is `Ablo.MutationFailure`.
13
+ - Handle provider startup errors with `<AbloProvider onError={...}>` instead of `useErrorListener()`.
14
+ - 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.
15
+ - `GroupScope` leaves the React entry point with `usePeers`; presence filtering uses model and record arguments.
16
+ - `useSyncStore`, `SyncStoreContract`, and `useReactive` are no longer React entry-point exports. Use the core client; framework integration internals remain under the humans runtime boundary.
17
+ - `ClientSideSuspense` and `DefaultFallback` are no longer public components. Supply your own UI through the provider's `fallback` prop or render from `ablo.status`.
18
+
19
+ 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.
20
+
21
+ 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:
22
+
23
+ | Removed React type export | Replacement |
24
+ | --------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
25
+ | `AbloOptions` | `Ablo.Options` |
26
+ | `AbloReads` | `Ablo.Reads` |
27
+ | `HumansSurface` | Infer the plugin result; low-level plugin authors can still import the type from `@abloatai/humans` |
28
+ | `AbloProviderProps` | `AbloProvider.Props` |
29
+ | `AbloReactBinding` | Infer the binding, or `ReturnType<typeof createAbloReact<Models>>` |
30
+ | `PresenceModelSelector` | Infer an inline selector against the bound schema |
31
+ | `UseAbloModelOptions` | `useAblo.Options<Row>` |
32
+ | `UseAbloModelResult` | `useAblo.Result<Row>` |
33
+ | `UseAbloHydratedModelResult` | `useAblo.Result<Row>`; handle optional data after local removal, even with `initial` |
34
+ | `InvokerFor` | `typeof mutations.model.operation` |
35
+ | `MutatorInvokers` | `useMutators.Result<typeof definitions>` |
36
+ | `UseMutatorsOptions` | `useMutators.Options<typeof schema>` |
37
+ | `UseUndoScopeResult` | `useUndoScope.Result<typeof schema>` |
38
+ | `ResolveSchema` | Existing core `Ablo.ResolveSchema`, when using module registration |
39
+ | `ResolveModelKey` | `keyof typeof schema.models` |
40
+ | `DefaultSyncShape`, `ResolveUserMeta`, `ResolveClaimMeta` | Internal registration details; use schema inference and the core presence/claim types |
41
+ | `MutateActions` | `Ablo.Mutator.Transaction<typeof schema>['mutate'][ModelKey]` |
42
+ | `ReaderActions` | `Ablo.Mutator.Transaction<typeof schema>['read'][ModelKey]` |
43
+ | `ReaderFindOptions` | Infer read options; advanced callers can import the existing type from `@abloatai/ablo/client` |
44
+
45
+ `ModelScope` is no longer re-exported by React; its existing advanced owner is `@abloatai/ablo/client`. Ordinary model reads use their `state` option.
46
+
47
+ `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.
48
+
49
+ 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.
50
+
3
51
  ## 0.63.1
4
52
 
5
53
  React workspaces can render immediately while collaboration connects.
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, 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,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, 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,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
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).
package/docs/react.md CHANGED
@@ -16,6 +16,54 @@ The React bindings ship with the main package — no extra install.
16
16
  import { useAblo } 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, subscribe to the core event in an effect:
25
+
26
+ ```tsx
27
+ const ablo = useAblo();
28
+ useEffect(() => ablo?.onMutationFailure(reportFailure), [ablo, reportFailure]);
29
+ ```
30
+
31
+ `reportFailure` receives `Ablo.MutationFailure`. Read application identity from
32
+ your auth provider; Ablo's authenticated authority comes from its session.
33
+
34
+ ## Types follow their owners
35
+
36
+ Most calls infer their types from your schema. For wrapper components or explicit
37
+ annotations, use the name you already imported:
38
+
39
+ ```tsx
40
+ import { Ablo, AbloProvider, useAblo } from '@abloatai/ablo/react';
41
+ import { schema } from './schema';
42
+
43
+ type Models = (typeof schema)['models'];
44
+ type Chat = Ablo.Schema.InferRow<typeof schema, 'chats'>;
45
+
46
+ function Provider(props: AbloProvider.Props<Models>) {
47
+ return <AbloProvider {...props} />;
48
+ }
49
+
50
+ function ChatView({ result }: { result: useAblo.Result<Chat> }) {
51
+ return <p>{result.data?.title}</p>;
52
+ }
53
+ ```
54
+
55
+ The React annotation types are `AbloProvider.Props`, `useAblo.Options`,
56
+ `useAblo.Result`, `useMutators.Options`, `useMutators.Result` and
57
+ `useUndoScope.Result`. Core types remain under `Ablo`, including `Ablo.Options`,
58
+ `Ablo.Reads`, `Ablo.Status` and `Ablo.PresenceSession`. Import annotation owners
59
+ from the SDK entry point; functions destructured from an app binding infer their
60
+ call types but do not create new TypeScript namespaces in the app module.
61
+
62
+ `createAbloReact(schema)` specializes types and returns the existing provider and
63
+ hooks. Define your binding at module scope. It creates no client, React context
64
+ or component identity. Hooks always read the nearest provider: use the binding
65
+ that matches that provider's client schema.
66
+
19
67
  ## Building the client
20
68
 
21
69
  You build the Ablo client once — that's where the schema, the session endpoint,
@@ -135,19 +183,19 @@ for the complete ownership boundary and runnable component.
135
183
 
136
184
  ## Render immediately with connection status
137
185
 
138
- `useSyncStatus()` works during provider startup, in passthrough children and in
186
+ `useAblo(ablo => ablo.status)` works during provider startup, in passthrough children and in
139
187
  custom fallbacks. It observes the client's status before row scope is available.
140
188
  It still requires a provider. Data hooks that require authenticated scope must
141
189
  wait for readiness.
142
190
 
143
191
  ```tsx
144
- import { useSyncStatus } from '@abloatai/ablo/react';
192
+ import { useAblo } from '@abloatai/ablo/react';
145
193
 
146
194
  function ConnectionIndicator() {
147
- const status = useSyncStatus();
195
+ const status = useAblo(ablo => ablo.status);
148
196
  return <span role="status">{
149
- status.name === 'initial' || status.name === 'connecting'
150
- ? 'Connecting…' : status.name
197
+ status?.name === 'initial' || status?.name === 'connecting'
198
+ ? 'Connecting…' : status?.name
151
199
  }</span>;
152
200
  }
153
201
 
@@ -171,6 +219,7 @@ export function ReportView({ report: serverReport }: { report: { id: string; loc
171
219
  { initial: serverReport },
172
220
  );
173
221
 
222
+ if (!report) return <p>This report is not in the local cache.</p>;
174
223
  return <article>{report.location}{claimed && <span>Claimed</span>}</article>;
175
224
  }
176
225
  ```
@@ -180,8 +229,24 @@ The row form subscribes to both data and claim events. It returns `data`,
180
229
 
181
230
  For data-only reads, selectors such as
182
231
  `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.
232
+ Claim selectors subscribe too: `useAblo(ablo => ablo.weatherReports.claim.state({ id }))`
233
+ updates when ownership changes. Use the row form when you want data and claims together.
234
+
235
+ Selectors must be pure synchronous reads. Ablo copies selected rows, arrays and plain
236
+ records into frozen snapshots, including nested data and computed fields. Unchanged
237
+ data retains its snapshot identity across renders. Treat copied dates as read-only;
238
+ model API handles and other class instances retain their original identity. Read
239
+ data inside the selector to subscribe to it. Selector errors reach React error boundaries.
240
+
241
+ `initial` supplies the server HTML and first hydration render; React then reads the
242
+ current local row. Once a local row has appeared, removing it returns `undefined`
243
+ instead of restoring the seed, so `data` remains optional even with `initial`.
244
+
245
+ A local cache miss (`undefined` or an empty list) does not prove that the server has
246
+ no matching data. Local reads do not fetch rows, and connection status does not
247
+ indicate query completeness. Use your route loader and an awaited server read, such
248
+ as `await ablo.weatherReports.get({ id })`, to establish server results and handle
249
+ request errors before passing an initial row to React.
185
250
 
186
251
  Use the zero-argument form only when you need the full client for callbacks,
187
252
  effects, or writes:
@@ -339,30 +404,28 @@ durable model fields when state must survive reconnects. The sending connection
339
404
  does not receive its own event. Coalesce or throttle pointer movement in the
340
405
  application; model events do not currently declare a per-event `maxHz`.
341
406
 
342
- ## usePeers: read-only presence
407
+ ## Presence: the same core read
343
408
 
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.
409
+ Select `ablo.presence.others` for the other sessions visible to this client, or
410
+ filter by a model and record. These reads subscribe to changes without starting
411
+ an activity or claiming ownership.
347
412
 
348
413
  ```tsx
349
- 'use client';
350
-
351
- import { usePeers } from '@abloatai/ablo/react';
414
+ import { useAblo } from '@abloatai/ablo/react';
352
415
 
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
416
+ function Readers({ conversationId }: { conversationId: string }) {
417
+ const sessions = useAblo(ablo => ablo.presence.forModel('conversations', conversationId)) ?? [];
418
+ const people = new Set(sessions.filter(session => session.participant.kind === 'user')
419
+ .map(session => session.participant.id));
420
+ return <span>{people.size} people, {sessions.length} sessions</span>;
357
421
  }
358
422
  ```
359
423
 
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.
424
+ `usePresence(ablo => ablo.conversations, conversationId)` additionally owns a
425
+ reading activity for the component's lifetime. Use it when mounting the component
426
+ should announce that this session is reading the record. Core code owns the same
427
+ lifecycle explicitly with `ablo.conversations.presence.read(conversationId)` and
428
+ its returned cleanup function.
366
429
 
367
430
  ## Next.js
368
431
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abloatai/ablo",
3
- "version": "0.63.1",
3
+ "version": "0.64.0",
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",
@@ -145,8 +145,8 @@
145
145
  "directory": "packages/ablo"
146
146
  },
147
147
  "dependencies": {
148
- "@abloatai/humans": "^0.63.1",
149
- "@abloatai/transaction": "^0.63.1",
148
+ "@abloatai/humans": "^0.64.0",
149
+ "@abloatai/transaction": "^0.64.0",
150
150
  "zod": "^4.4.3"
151
151
  },
152
152
  "peerDependencies": {