@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 +58 -0
- package/dist/coordination.d.ts +3 -3
- package/dist/coordination.d.ts.map +1 -1
- package/docs/api.md +40 -0
- package/docs/client-behavior.md +16 -3
- package/docs/integration-guide.md +11 -0
- package/docs/integrations/inngest.md +1 -1
- package/docs/integrations/temporal.md +1 -1
- package/docs/options.md +17 -1
- package/docs/react.md +76 -2
- package/package.json +3 -3
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
|
package/dist/coordination.d.ts
CHANGED
|
@@ -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
|
|
6
|
-
*
|
|
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 {
|
|
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,
|
|
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
|
package/docs/client-behavior.md
CHANGED
|
@@ -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.
|
|
200
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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": {
|