@abloatai/ablo 0.62.0 → 0.63.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 +21 -0
- package/docs/api.md +40 -0
- package/docs/integration-guide.md +11 -0
- package/docs/options.md +17 -1
- package/docs/react.md +76 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.63.0
|
|
4
|
+
|
|
5
|
+
Reactive model clients now expose live awareness beneath the model namespace.
|
|
6
|
+
`usePresence((ablo) => ablo.chats, chatId)` owns the mounted view's read
|
|
7
|
+
activity, refresh, reconnect announcement, and cleanup, and returns the human
|
|
8
|
+
and agent sessions active on that record. Multiple tabs remain distinct
|
|
9
|
+
authenticated sessions.
|
|
10
|
+
|
|
11
|
+
Transient collaboration now uses `ablo.<model>.events`. Applications can send
|
|
12
|
+
and subscribe to cursor, selection, and similar signals by record id without
|
|
13
|
+
putting routing or caller-authored identity in the payload. Ablo derives the
|
|
14
|
+
record sync group, routes only to other connections in that group, and supplies
|
|
15
|
+
the authenticated participant, presence session, and server timestamp to the
|
|
16
|
+
receiver.
|
|
17
|
+
|
|
18
|
+
Model events are deliberately lossy: disconnected sends are dropped and
|
|
19
|
+
events are not replayed after reconnect. Persist state that must recover as
|
|
20
|
+
ordinary model data, and throttle high-frequency pointer updates in the
|
|
21
|
+
application. The legacy store-level collaboration-event API remains available
|
|
22
|
+
and now receives optional authenticated context from updated servers.
|
|
23
|
+
|
|
3
24
|
## 0.62.0
|
|
4
25
|
|
|
5
26
|
Every live client now exposes one session-owned `ablo.presence` projection.
|
package/docs/api.md
CHANGED
|
@@ -66,6 +66,9 @@ Each schema model becomes a typed model on the client:
|
|
|
66
66
|
- `ablo.weatherReports.update({ id, data, ...options })` updates a row.
|
|
67
67
|
- `ablo.weatherReports.delete({ id, ...options })` deletes a row.
|
|
68
68
|
- `ablo.weatherReports.claim({ id, description })` acquires a durable write lease; the HTTP form is awaited.
|
|
69
|
+
- `ablo.weatherReports.presence(id)` reads the live session projection for one row on a reactive client.
|
|
70
|
+
- `ablo.weatherReports.events.send(id, name, payload)` sends a transient row-scoped event on a reactive client.
|
|
71
|
+
- `ablo.weatherReports.events.subscribe(id, name, handler)` subscribes to that transient event and returns a disposer.
|
|
69
72
|
|
|
70
73
|
`local.` narrows a query to what has already synced. `get({ id })`, `read({ id })`, and
|
|
71
74
|
`list({ where })` answer from the local graph and fall back to IndexedDB and
|
|
@@ -91,11 +94,48 @@ fallback removed — nothing to await, so they return a value.
|
|
|
91
94
|
| `claim.queue({ id })` | `Promise<ClaimQueueView>` on HTTP | You need the durable wait line. |
|
|
92
95
|
| `claim.release({ id })` | `Promise<void>` on HTTP | You need to release a claim early. |
|
|
93
96
|
| `claim.reorder({ id, order })` | `Promise<void>` on HTTP | A privileged coordinator needs to reorder the wait line. |
|
|
97
|
+
| `presence(id?)` | `readonly PresenceSession[]` on reactive clients | You need the sessions currently active on a model or row. |
|
|
98
|
+
| `events.send(id, name, payload)` | `void` on reactive clients | You need to send a cursor, selection, or other transient signal. |
|
|
99
|
+
| `events.subscribe(id, name, handler)` | `() => void` on reactive clients | You need transient signals for one row until cleanup. |
|
|
94
100
|
|
|
95
101
|
`get`, `read`, `list`, `create`, `update`, `delete`, and `claim` go
|
|
96
102
|
through the server. The `local` reads work off the rows a session has already
|
|
97
103
|
synced, so a cheap re-read needs no round-trip.
|
|
98
104
|
|
|
105
|
+
### Live presence and model events
|
|
106
|
+
|
|
107
|
+
Presence and events belong to the reactive WebSocket client. They are not on
|
|
108
|
+
the stateless HTTP client used by server-side agents and workers.
|
|
109
|
+
|
|
110
|
+
Use presence for who is active on a row. In React, `usePresence` also owns the
|
|
111
|
+
mounted component's read activity and cleanup:
|
|
112
|
+
|
|
113
|
+
```tsx
|
|
114
|
+
const viewers = usePresence((ablo) => ablo.chats, chatId);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Use model events for short-lived UI detail that should not become a database
|
|
118
|
+
field:
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const stop = ablo.files.events.subscribe(fileId, 'cursor', (cursor, context) => {
|
|
122
|
+
renderCursor(context.sender.presenceSessionId, cursor);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
ablo.files.events.send(fileId, 'cursor', { line: 12, column: 4 });
|
|
126
|
+
stop();
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The model and row determine the sync group. Ablo excludes the sending
|
|
130
|
+
connection, validates the wire envelope, and supplies authenticated
|
|
131
|
+
`context.sender` and `context.sentAt`; identity does not belong in the payload.
|
|
132
|
+
Event names are strings and payloads are objects. Payloads are not inferred
|
|
133
|
+
from the schema today.
|
|
134
|
+
|
|
135
|
+
Delivery is lossy: sending while disconnected is dropped, and events are not
|
|
136
|
+
replayed after reconnect. Cursor and live selection fit this contract. Persist
|
|
137
|
+
anything that must be recovered as ordinary model data.
|
|
138
|
+
|
|
99
139
|
## Atomic commits
|
|
100
140
|
|
|
101
141
|
Use one `ablo.commits.create` when several Ablo model writes must all land or
|
|
@@ -523,6 +523,8 @@ that guarantee as eventual completion with repair—not atomicity.
|
|
|
523
523
|
| `persistence: 'indexeddb'` | Durable browser cache that survives reloads, for apps that need it. |
|
|
524
524
|
| `durableWrites: { store, namespace? }` | Recover unacknowledged worker writes after a process restart. |
|
|
525
525
|
| `claim` / `claim.state` / `claim.queue` | Show active work and coordinate before a write. |
|
|
526
|
+
| `usePresence` / `<model>.presence` | Show the human and agent sessions active on a row. |
|
|
527
|
+
| `<model>.events` | Send lossy row-scoped cursor, selection, and similar UI signals. |
|
|
526
528
|
| `read` + `reads` | Reject writes based on stale state. |
|
|
527
529
|
| `mutable`, `readOnly`, `field`, `indexed` | Advanced schema and read tuning. |
|
|
528
530
|
|
|
@@ -546,7 +548,16 @@ them.
|
|
|
546
548
|
| `delete({ id, ...opts })` | Delete through the model client. |
|
|
547
549
|
| `claim.state({ id })` | See who is currently working on a row (synchronous). |
|
|
548
550
|
| `claim({ id, description?, ttl? })` | Acquire a disposable handle: wait for your turn, re-read, and hold the row. |
|
|
551
|
+
| `presence(id?)` | Read live human and agent sessions on a reactive client. |
|
|
552
|
+
| `events.send(id, name, payload)` | Send a transient row-scoped signal; disconnected sends are dropped. |
|
|
553
|
+
| `events.subscribe(id, name, handler)` | Listen until its returned disposer is called; events are not replayed. |
|
|
549
554
|
|
|
550
555
|
Keep first integrations on the model methods above. Every mutation and
|
|
551
556
|
server-read verb takes one options object; the synchronous `local.get(id)` stays
|
|
552
557
|
positional.
|
|
558
|
+
|
|
559
|
+
Presence and events require the reactive WebSocket client. Server-side agents
|
|
560
|
+
using the stateless HTTP client still coordinate through reads, claims, and
|
|
561
|
+
writes, but they do not open a cursor/selection event channel. Event payloads
|
|
562
|
+
carry application data only; Ablo derives the model scope and supplies
|
|
563
|
+
authenticated participant context to receivers.
|
package/docs/options.md
CHANGED
|
@@ -200,7 +200,23 @@ connection follows, but it cannot widen the authority granted by
|
|
|
200
200
|
|
|
201
201
|
## collaborationEvents
|
|
202
202
|
|
|
203
|
-
|
|
203
|
+
Legacy store-level WebSocket event names accepted by `subscribe()`. New React
|
|
204
|
+
and reactive-client code should prefer the record-scoped
|
|
205
|
+
`ablo.<model>.events.send(...)` and `.subscribe(...)` surface. A
|
|
206
|
+
collaboration-event handler receives the application's unchanged payload first
|
|
207
|
+
and optional server-authenticated context second:
|
|
208
|
+
|
|
209
|
+
```ts
|
|
210
|
+
store.subscribe('document:cursor', (cursor, context) => {
|
|
211
|
+
context?.sender.presenceSessionId;
|
|
212
|
+
context?.sender.participant; // { id, kind }
|
|
213
|
+
context?.sentAt;
|
|
214
|
+
});
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The context is optional while connecting to a server version that predates
|
|
218
|
+
authenticated collaboration-event attribution. Never put caller-authored user
|
|
219
|
+
identity in the application payload.
|
|
204
220
|
|
|
205
221
|
## cursorStore
|
|
206
222
|
|
package/docs/react.md
CHANGED
|
@@ -39,10 +39,10 @@ export const ablo = Ablo({
|
|
|
39
39
|
// The typed binding: capture the schema once, and every component imports
|
|
40
40
|
// born-typed hooks from this file — `useAblo()` takes no type arguments,
|
|
41
41
|
// and a selector's `ablo` parameter knows your models.
|
|
42
|
-
export const { AbloProvider, useAblo } = createAbloReact(schema);
|
|
42
|
+
export const { AbloProvider, useAblo, usePresence } = createAbloReact(schema);
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Import `AbloProvider` and `
|
|
45
|
+
Import `AbloProvider`, `useAblo`, and `usePresence` from `lib/ablo` rather than from the
|
|
46
46
|
package, and the schema generic never appears at a call site again — the
|
|
47
47
|
same one-binding-file convention as tRPC's `createTRPCReact` or
|
|
48
48
|
react-redux's typed hooks.
|
|
@@ -188,6 +188,80 @@ imperative work after an event or effect.
|
|
|
188
188
|
|
|
189
189
|
See [API reference](/docs/api) for the full options surface.
|
|
190
190
|
|
|
191
|
+
## usePresence: viewers and active participants
|
|
192
|
+
|
|
193
|
+
`usePresence` declares that the mounted component is reading one model record
|
|
194
|
+
and returns the live sessions active on that record. Use the selector form with
|
|
195
|
+
the schema-bound hook:
|
|
196
|
+
|
|
197
|
+
```tsx
|
|
198
|
+
import { usePresence } from '@/lib/ablo';
|
|
199
|
+
|
|
200
|
+
export function ChatView({ chatId }: { chatId: string }) {
|
|
201
|
+
const viewers = usePresence((ablo) => ablo.chats, chatId);
|
|
202
|
+
|
|
203
|
+
return viewers.map((session) => (
|
|
204
|
+
<Avatar
|
|
205
|
+
key={session.presenceSessionId}
|
|
206
|
+
participantId={session.participant.id}
|
|
207
|
+
kind={session.participant.kind}
|
|
208
|
+
/>
|
|
209
|
+
));
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
The component chooses the model and record. Ablo owns the authenticated
|
|
214
|
+
session identity, read lease, refresh, reconnect re-announcement, and removal
|
|
215
|
+
on cleanup. Multiple tabs remain separate sessions, and human and agent
|
|
216
|
+
participants use the same result shape. Do not build a separate `chat:view`
|
|
217
|
+
event, heartbeat, or stale-viewer timer in the app.
|
|
218
|
+
|
|
219
|
+
If you already have the client, the direct model form is equivalent:
|
|
220
|
+
|
|
221
|
+
```tsx
|
|
222
|
+
const viewers = usePresence(ablo.chats, chatId);
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
The hook returns the complete matching session projection, including the
|
|
226
|
+
current session. Use `session.participant` for identity and inspect
|
|
227
|
+
`session.activities` when the UI needs to distinguish reading from claiming or
|
|
228
|
+
writing.
|
|
229
|
+
|
|
230
|
+
## Model events: cursors and selections
|
|
231
|
+
|
|
232
|
+
Use the `events` namespace already attached to each model for transient UI
|
|
233
|
+
signals. The model and record choose the authorized sync group; the payload
|
|
234
|
+
does not need routing fields or caller-authored identity.
|
|
235
|
+
|
|
236
|
+
```tsx
|
|
237
|
+
const ablo = useAblo();
|
|
238
|
+
|
|
239
|
+
useEffect(() => {
|
|
240
|
+
if (!ablo) return;
|
|
241
|
+
return ablo.slideDecks.events.subscribe(deckId, 'cursor', (cursor, context) => {
|
|
242
|
+
drawRemoteCursor(context.sender.presenceSessionId, cursor);
|
|
243
|
+
});
|
|
244
|
+
}, [ablo, deckId]);
|
|
245
|
+
|
|
246
|
+
function onPointerMove(x: number, y: number) {
|
|
247
|
+
ablo?.slideDecks.events.send(deckId, 'cursor', { slideId, x, y });
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
The same shape works for code editors:
|
|
252
|
+
|
|
253
|
+
```ts
|
|
254
|
+
ablo.files.events.send(fileId, 'selection', { anchor, head });
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Events are lossy and are not replayed after reconnect, which fits cursor and
|
|
258
|
+
live-selection updates. Ablo enters and leaves the record scope with each
|
|
259
|
+
subscription, routes only inside that scope, and delivers authenticated
|
|
260
|
+
`sender` and `sentAt` context separately from the application payload. Use
|
|
261
|
+
durable model fields when state must survive reconnects. The sending connection
|
|
262
|
+
does not receive its own event. Coalesce or throttle pointer movement in the
|
|
263
|
+
application; model events do not currently declare a per-event `maxHz`.
|
|
264
|
+
|
|
191
265
|
## usePeers: read-only presence
|
|
192
266
|
|
|
193
267
|
`usePeers` reads the presence stream already flowing for the client's scoped
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.63.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.
|
|
149
|
-
"@abloatai/transaction": "^0.
|
|
148
|
+
"@abloatai/humans": "^0.63.0",
|
|
149
|
+
"@abloatai/transaction": "^0.63.0",
|
|
150
150
|
"zod": "^4.4.3"
|
|
151
151
|
},
|
|
152
152
|
"peerDependencies": {
|