@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 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
- Application-defined WebSocket event names accepted by `subscribe()`.
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 `useAblo` from `lib/ablo` rather than from the
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.62.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.62.0",
149
- "@abloatai/transaction": "^0.62.0",
148
+ "@abloatai/humans": "^0.63.0",
149
+ "@abloatai/transaction": "^0.63.0",
150
150
  "zod": "^4.4.3"
151
151
  },
152
152
  "peerDependencies": {