@abloatai/ablo 0.61.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,63 @@
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
+
24
+ ## 0.62.0
25
+
26
+ Every live client now exposes one session-owned `ablo.presence` projection.
27
+ `active` shows the current session's announced activity and `others` shows the
28
+ other visible sessions. The reactive client adds `forModel(model, id?)` to
29
+ narrow the same projection without opening another connection. Read, claim,
30
+ create, update, and delete activity share one typed, model-addressable contract
31
+ and remain attributable to their originating human or agent session.
32
+
33
+ Claims and presence now have separate owners: the durable claim authority
34
+ decides admission, queuing, fencing, expiry, and release, while presence
35
+ projects that lifecycle for live collaborators. Cross-replica HTTP callers
36
+ resolve and release claims against the shared authority instead of a
37
+ process-local roster. An object-form claim also returns its protected row
38
+ snapshot in the acquisition request, removing the post-grant read race and an
39
+ extra round trip.
40
+
41
+ This replaces the earlier participant-oriented presence stream and its raw
42
+ wire event vocabulary. Migrate `PresenceStream`, `Peer`, `Activity`,
43
+ `PresenceUpdate*`, and `PresenceKind` consumers to `Ablo.Presence`,
44
+ `Ablo.PresenceSession`, and `Ablo.PresenceActivity`; read the projection through
45
+ `ablo.presence.active`, `ablo.presence.others`, or
46
+ `ablo.presence.forModel(model, id?)`. The low-level `presence_update` event,
47
+ frame-handler members, presence schemas, and claim-stream participant setters
48
+ are removed because session identity and typed presence snapshot/patch frames
49
+ now own that lifecycle.
50
+
51
+ Admission and other transient failures now expose machine-actionable
52
+ `recovery`, `retryable`, and `retryAfterSeconds` fields through the branded
53
+ package, including session issuance and headless model requests. The headless
54
+ client automatically replays an admission-rejected request after the requested
55
+ delay, without restarting the surrounding claim workflow. Queued HTTP claims
56
+ heartbeat through their known model and row target, so one holder releasing
57
+ cannot make the next queued ticket appear lost during promotion. A visibility
58
+ miss while the fence is minted is retried only inside the ticket's last
59
+ server-acknowledged lease window.
60
+
3
61
  ## 0.61.0
4
62
 
5
63
  ### Existing database connections repair in place
@@ -2,9 +2,9 @@ export * from '@abloatai/transaction/coordination';
2
2
  /**
3
3
  * Coordination vocabulary that the streams module declares.
4
4
  *
5
- * A caller that holds a claim, watches presence, or types an activity feed
6
- * needs these names, and coordination is where they belong — so they are
5
+ * A caller that holds a claim needs these names, and coordination is where
6
+ * they belong — so they are
7
7
  * surfaced here rather than leaving callers to reach into the type module.
8
8
  */
9
- export type { Activity, Claim, ClaimTarget } from '@abloatai/transaction/types/streams';
9
+ export type { Claim, ClaimTarget } from '@abloatai/transaction/types/streams';
10
10
  //# sourceMappingURL=coordination.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../src/coordination.ts"],"names":[],"mappings":"AAAA,cAAc,oCAAoC,CAAC;AAEnD;;;;;;GAMG;AACH,YAAY,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC"}
1
+ {"version":3,"file":"coordination.d.ts","sourceRoot":"","sources":["../src/coordination.ts"],"names":[],"mappings":"AAAA,cAAc,oCAAoC,CAAC;AAEnD;;;;;;GAMG;AACH,YAAY,EAAE,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC"}
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
@@ -157,13 +157,20 @@ see. Options on the claim:
157
157
  - `{ maxQueueDepth }` rejects if the wait line is already too deep.
158
158
 
159
159
  While waiting, schema clients learn when the claim clears from the live claim
160
- stream, so they never poll.
160
+ stream, so they never poll. Headless HTTP clients poll the same durable queue;
161
+ the model client keeps the row target on each heartbeat, so a holder releasing
162
+ cannot make the queued ticket unresolvable by id. During fence minting, it also
163
+ keeps polling through a visibility miss only while the server's last enqueue or
164
+ heartbeat acknowledgement still guarantees that ticket is live.
161
165
 
162
166
  ## Errors
163
167
 
164
168
  All SDK errors extend `AbloError`. `type` is the class-name discriminator, such
165
169
  as `AbloStaleContextError`; `code` is the wire condition, such as
166
170
  `stale_context`. Use `instanceof` in-process and `type` after serialization.
171
+ Every error also exposes a typed `recovery` classification and `retryable`
172
+ boolean. When the server requests a minimum delay, `retryAfterSeconds` is
173
+ present on the same error for both 429 and 503 responses.
167
174
 
168
175
  | Error | Typical cause |
169
176
  |---|---|
@@ -196,8 +203,14 @@ Model writes are retry-safe by default because the SDK attaches an idempotency
196
203
  key. If you provide your own key, keep it stable for retries of the same logical
197
204
  operation and never reuse it for a different payload.
198
205
 
199
- Retry transport failures and 5xx with backoff. Do not blindly retry validation,
200
- permission, idempotency, or stale-context errors without changing the request.
206
+ Retry transport failures and 5xx with backoff. For example, an
207
+ `instance_at_capacity` error has `recovery === 'transient'`; wait at least
208
+ `retryAfterSeconds` before replaying the unchanged request. The headless HTTP
209
+ client performs that exact replay within `timeoutMs`; importantly, it does not
210
+ restart a larger claim/read/write workflow around the rejected request. If the
211
+ deadline is exhausted, the same actionable error reaches the caller. Do not
212
+ blindly retry validation, permission, idempotency, or stale-context errors
213
+ without changing the request.
201
214
 
202
215
  ## Logging
203
216
 
@@ -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.
@@ -242,7 +242,7 @@ examples/
242
242
  inngest-agent/ events, functions, steps, endpoint, AI composition
243
243
  ```
244
244
 
245
- Do not add Inngest to `packages/agent`. A dedicated `@abloatai/inngest` package
245
+ Do not add Inngest orchestration to the Ablo core. A dedicated `@abloatai/inngest` package
246
246
  is justified only after multiple real applications reveal substantial
247
247
  reusable behavior beyond a small function or step wrapper.
248
248
 
@@ -178,7 +178,7 @@ The repository's
178
178
  [`examples/temporal-agent`](../../../../examples/temporal-agent/README.md)
179
179
  contains a Workflow, Activities, Worker, client, durable AI SDK tool, and a
180
180
  simulated lost-response retry. It is a standalone application on purpose:
181
- Temporal stays out of Ablo's core packages and out of `packages/agent`.
181
+ Temporal stays out of Ablo's core packages; applications own their agent composition.
182
182
 
183
183
  A dedicated `@abloatai/temporal` package should be introduced only after
184
184
  multiple production integrations reveal substantial, stable behavior that
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.61.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.61.0",
149
- "@abloatai/transaction": "^0.61.0",
148
+ "@abloatai/humans": "^0.63.0",
149
+ "@abloatai/transaction": "^0.63.0",
150
150
  "zod": "^4.4.3"
151
151
  },
152
152
  "peerDependencies": {