@abloatai/ablo 0.5.1 → 0.6.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.
Files changed (94) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +217 -122
  3. package/dist/BaseSyncedStore.d.ts +2 -2
  4. package/dist/BaseSyncedStore.js +2 -2
  5. package/dist/api/index.d.ts +3 -3
  6. package/dist/api/index.js +1 -1
  7. package/dist/client/Ablo.d.ts +90 -93
  8. package/dist/client/Ablo.js +121 -60
  9. package/dist/client/ApiClient.d.ts +14 -14
  10. package/dist/client/ApiClient.js +81 -55
  11. package/dist/client/createInternalComponents.d.ts +2 -3
  12. package/dist/client/createInternalComponents.js +2 -3
  13. package/dist/client/createModelProxy.d.ts +90 -87
  14. package/dist/client/createModelProxy.js +124 -127
  15. package/dist/client/index.d.ts +6 -7
  16. package/dist/client/index.js +4 -5
  17. package/dist/client/validateAbloOptions.js +3 -3
  18. package/dist/core/index.d.ts +2 -0
  19. package/dist/core/index.js +7 -0
  20. package/dist/errors.d.ts +8 -8
  21. package/dist/errors.js +18 -10
  22. package/dist/index.d.ts +9 -8
  23. package/dist/index.js +7 -11
  24. package/dist/interfaces/index.d.ts +2 -10
  25. package/dist/mutators/Transaction.d.ts +2 -2
  26. package/dist/mutators/Transaction.js +2 -2
  27. package/dist/mutators/mutateActions.d.ts +44 -0
  28. package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
  29. package/dist/mutators/readerActions.d.ts +32 -0
  30. package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
  31. package/dist/query/types.d.ts +1 -1
  32. package/dist/react/AbloProvider.d.ts +1 -1
  33. package/dist/react/AbloProvider.js +3 -3
  34. package/dist/react/context.d.ts +4 -4
  35. package/dist/react/index.d.ts +4 -5
  36. package/dist/react/index.js +3 -7
  37. package/dist/react/useAblo.d.ts +14 -14
  38. package/dist/react/useAblo.js +26 -26
  39. package/dist/react/useIntent.d.ts +2 -2
  40. package/dist/react/useIntent.js +2 -2
  41. package/dist/react/useMutators.d.ts +1 -1
  42. package/dist/react/usePresence.d.ts +3 -3
  43. package/dist/react/usePresence.js +4 -4
  44. package/dist/react/useUndoScope.d.ts +1 -1
  45. package/dist/schema/diff.d.ts +161 -0
  46. package/dist/schema/diff.js +262 -0
  47. package/dist/schema/generate.d.ts +19 -0
  48. package/dist/schema/generate.js +87 -0
  49. package/dist/schema/index.d.ts +4 -1
  50. package/dist/schema/index.js +7 -1
  51. package/dist/schema/schema.d.ts +83 -32
  52. package/dist/schema/schema.js +58 -12
  53. package/dist/schema/serialize.d.ts +92 -0
  54. package/dist/schema/serialize.js +227 -0
  55. package/dist/sync/SyncWebSocket.d.ts +17 -0
  56. package/dist/sync/SyncWebSocket.js +46 -1
  57. package/dist/sync/awaitIntentGrant.d.ts +26 -0
  58. package/dist/sync/awaitIntentGrant.js +60 -0
  59. package/dist/sync/createIntentStream.js +43 -4
  60. package/dist/sync/createPresenceStream.js +1 -1
  61. package/dist/sync/participants.d.ts +2 -2
  62. package/dist/sync/participants.js +4 -4
  63. package/dist/types/global.d.ts +43 -52
  64. package/dist/types/global.js +16 -18
  65. package/dist/types/streams.d.ts +37 -9
  66. package/docs/api.md +68 -158
  67. package/docs/audit.md +5 -5
  68. package/docs/client-behavior.md +41 -42
  69. package/docs/coordination.md +294 -0
  70. package/docs/data-sources.md +14 -14
  71. package/docs/examples/agent-human.md +30 -32
  72. package/docs/examples/ai-sdk-tool.md +32 -33
  73. package/docs/examples/existing-python-backend.md +35 -33
  74. package/docs/examples/nextjs.md +24 -25
  75. package/docs/examples/server-agent.md +20 -61
  76. package/docs/guarantees.md +30 -55
  77. package/docs/identity.md +458 -0
  78. package/docs/index.md +12 -24
  79. package/docs/integration-guide.md +106 -116
  80. package/docs/interaction-model.md +29 -95
  81. package/docs/mcp/claude-code.md +3 -3
  82. package/docs/mcp/cursor.md +1 -1
  83. package/docs/mcp/windsurf.md +1 -1
  84. package/docs/mcp.md +11 -26
  85. package/docs/quickstart.md +43 -49
  86. package/docs/react.md +73 -23
  87. package/docs/roadmap.md +5 -7
  88. package/llms.txt +34 -39
  89. package/package.json +1 -1
  90. package/dist/react/useMutate.d.ts +0 -83
  91. package/dist/react/useQuery.d.ts +0 -123
  92. package/dist/react/useQuery.js +0 -145
  93. package/dist/react/useReader.d.ts +0 -69
  94. package/docs/capabilities.md +0 -163
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.6.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 0f663e7: Coordination surface: fair queue, reactive wait-line, and lease renewal.
8
+ - **Claims acquire through a server FIFO queue.** On contention a claim waits its turn and re-reads before proceeding; reads are never blocked. Writes blocked by another participant's claim throw a typed `AbloBusyError`.
9
+ - **`ablo.<model>.queue(id)`** — reactive read of the wait-line behind a row: who's queued, their action, and FIFO position. Synced to peers like `activity(id)`.
10
+ - **Backpressure on `claim`** — `{ wait: false }` skips instead of waiting if the row is already held (claim-or-skip dedup); `{ maxQueueDepth: n }` bails with `AbloBusyError('queue_too_deep')` rather than joining a line already that deep.
11
+ - **Lease renewal** — a held claim renews automatically while the holder's connection is alive, so you never size a TTL; it lapses only after the holder goes silent. A queued claim that's abandoned is dequeued (no ghost waiters).
12
+ - **Reads are never gated by a claim**, including for agents.
13
+ - Intent vocabulary cleanup: a waiting claim is an `Intent` with `status: 'queued'` (`position` carries its place in line). Removed the unbuilt `whenFree`.
14
+
15
+ - **BREAKING — API renames** (apply when upgrading from 0.5.1):
16
+ - Change-listeners renamed to `.onChange(...)`: `ablo.<model>.subscribe(cb)`, `presence.subscribe()`, `intents.subscribe()` → `.onChange(...)`. (`subscribe` is reserved for an upcoming scope-grant verb.)
17
+ - Row-access API renamed Resource → Model: `Ablo.Resource.*` → `Ablo.Model.*`, `ablo.resource(name)` → `ablo.model(name)`, `ModelTarget.resource` → `ModelTarget.model`, error code `resource_not_found` → `model_not_found`.
18
+
3
19
  ## 0.5.1
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Ablo
2
2
 
3
- Ablo Sync is a typed sync engine for shared app state — the kind that humans,
3
+ Ablo is a typed sync engine for shared app state — the kind that humans,
4
4
  server code, and AI agents all edit at once.
5
5
 
6
6
  Reach for it when those edits need to show up everywhere in real time, not
@@ -8,28 +8,32 @@ silently overwrite each other, expose who's working on what, and leave a record
8
8
  of who changed what.
9
9
 
10
10
  ```txt
11
- schema -> ablo.<model>.create/retrieve/load/update/intent(...)
11
+ schema -> ablo.<model>.create/retrieve/update/claim(...)
12
12
  ```
13
13
 
14
- ## Install
14
+ ## Set up
15
15
 
16
16
  ```bash
17
17
  npm install @abloatai/ablo
18
18
  ```
19
19
 
20
- Requires Node 22+ and TypeScript 5+.
20
+ The package ships an `llms.txt` — a precise map of the API — so a coding agent
21
+ integrates from the real surface instead of guessing it. Point Claude Code or
22
+ Cursor at it:
21
23
 
22
- ## Get a Test Key
24
+ > Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema and
25
+ > `<AbloProvider>`, wire my first create / retrieve / update, and use `claim`
26
+ > for anything an agent edits across a slow step (read → LLM → write).
23
27
 
24
- Create an Ablo sandbox and copy an `sk_test_*` API key. Keep API keys in trusted
25
- server runtimes only.
28
+ Or wire it by hand the [Quick Start](#quick-start) below is the shape it
29
+ produces. For production (React, an existing backend, Data Source, agents), the
30
+ [Integration Guide](./docs/integration-guide.md) is the deeper map.
26
31
 
27
- ```bash
28
- export ABLO_API_KEY=sk_test_...
29
- ```
30
-
31
- In the browser, connect through the React provider (`<AbloProvider>`), which
32
- authenticates with the signed-in user's session — never the raw API key.
32
+ **Keys & runtime.** Ablo is ESM-only (`import`, not `require`) and needs Node
33
+ 22+ and TypeScript 5+. Grab an `sk_test_*` key
34
+ for a sandbox (`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server
35
+ runtimes only. In the browser, `<AbloProvider>` authenticates with the signed-in
36
+ user's session never the raw key.
33
37
 
34
38
  ## Quick Start
35
39
 
@@ -73,9 +77,99 @@ Expected output:
73
77
  { id: '...', status: 'ready' }
74
78
  ```
75
79
 
76
- Pass `schema` to get typed models like `ablo.weatherReports.update(...)`. Omit it
77
- only for the lower-level client used by custom agents and MCP routes that can't
78
- import your app's schema.
80
+ Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
81
+
82
+ ## Reading
83
+
84
+ `retrieve(id)` returns one row from the local cache — synchronous, no round-trip.
85
+ `list(...)` filters and sorts what's already synced; it's also synchronous, and
86
+ reactive under `useAblo`/`subscribe`. `load(...)` fetches from the server when a
87
+ row may not be local yet.
88
+
89
+ ```ts
90
+ ablo.weatherReports.retrieve('report_stockholm'); // → row | undefined
91
+
92
+ // Synchronous, from the local cache → row[]
93
+ const pending = ablo.weatherReports.list({
94
+ where: { status: 'pending' }, // equality filter (an array value means IN)
95
+ orderBy: { location: 'asc' },
96
+ limit: 20,
97
+ });
98
+
99
+ // Server fetch → Promise<row[]>. 'complete' waits for the server; 'unknown'
100
+ // returns what's local now and refreshes in the background.
101
+ const ready = await ablo.weatherReports.load({
102
+ where: { status: 'ready' },
103
+ type: 'complete',
104
+ });
105
+ ```
106
+
107
+ ## Writing
108
+
109
+ `create` / `update` apply optimistically and resolve to the row. Two options
110
+ matter day to day:
111
+
112
+ | Option | Values | What it does |
113
+ | --- | --- | --- |
114
+ | `wait` | `'queued'` \| `'confirmed'` | `'confirmed'` resolves only after the server acks the write; `'queued'` resolves as soon as it's locally queued (fire-and-forget). |
115
+ | `idempotencyKey` | `string` | Auto-generated per call. Override only when you own the retry boundary (e.g. a job id) so a re-run dedupes server-side. |
116
+
117
+ ```ts
118
+ await ablo.weatherReports.update(id, { status: 'ready' }, { wait: 'confirmed' });
119
+ ```
120
+
121
+ To guard a write against a row that changed under you, pass `readAt` + `onStale`
122
+ — see [Coordinating long agent work](#coordinating-long-agent-work).
123
+
124
+ ## Coordinating long agent work
125
+
126
+ An agent reads a row, thinks for 30s, writes back — and clobbers whatever changed
127
+ meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
128
+
129
+ ```ts
130
+ await ablo.weatherReports.claim('report_stockholm', async (report) => {
131
+ // If someone else holds it, claim() WAITS in a fair queue, then re-reads —
132
+ // so `report` is the current row, never a stale snapshot. Reads stay open by
133
+ // default; only acting-on-the-row serializes.
134
+
135
+ const forecast = await weatherAgent.getWeather(report.location); // slow LLM gap
136
+ await ablo.weatherReports.update(report.id, { forecast, status: 'ready' });
137
+ }); // claim released here, whether the callback returns or throws
138
+ ```
139
+
140
+ See who's mid-edit before you act — decide to wait, or skip:
141
+
142
+ ```ts
143
+ ablo.weatherReports.claimState('report_stockholm'); // → the holder, or null
144
+ ablo.weatherReports.queue('report_stockholm'); // → { data: [{ heldBy, action, position }, …] }
145
+
146
+ await ablo.weatherReports.claim(id, async (report) => {
147
+ /* do the held work */
148
+ }, { wait: false }); // held? skip (dedup) — throws instead of waiting
149
+
150
+ await ablo.weatherReports.claim(id, async (report) => {
151
+ /* do the held work */
152
+ }, { maxQueueDepth: 2 }); // 2+ already ahead? bail
153
+ ```
154
+
155
+ Default reads keep working while a row is claimed. Server reads that need claimed
156
+ semantics can opt in with `ifClaimed: 'return' | 'wait' | 'fail'`.
157
+
158
+ Even an unclaimed write can't land on stale reasoning — the commit is guarded:
159
+
160
+ ```ts
161
+ try {
162
+ await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
163
+ } catch (e) {
164
+ if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
165
+ }
166
+ ```
167
+
168
+ > Prefer the callback form for ordinary held work. Manual scoped claims are
169
+ > available for wider lifetimes, but callback claims are the docs default.
170
+
171
+ See [Coordination](./docs/coordination.md) for the full `claim` / `claimState` /
172
+ `queue` / `release` reference.
79
173
 
80
174
  ## React
81
175
 
@@ -112,62 +206,39 @@ function Report({ id }: { id: string }) {
112
206
  }
113
207
  ```
114
208
 
115
- `<AbloProvider>` owns the connection and authenticates with the signed-in user's
116
- session no API key in the browser. That's the whole loop: read with
117
- `useAblo(selector)`, write with `ablo.<model>`, and every other client (human or
118
- agent) on that row sees it in real time. See [React](./docs/react.md) for
119
- `fallback`, `bootstrapMode`, presence, and status hooks.
120
-
121
- ## Set up with Claude Code
122
-
123
- The package ships an `llms.txt` — a compact, LLM-readable map of the whole API.
124
- Point your coding agent at it and let it do the integration:
125
-
126
- > Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema and
127
- > `<AbloProvider>` to this app and wire up my first create / retrieve / update.
128
-
129
- It scaffolds the schema, sets up the provider, and writes your first
130
- `ablo.<model>` calls — the same shape as the Quick Start above.
209
+ `<AbloProvider>` owns the connection no API key in the browser. That's the
210
+ whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
211
+ other client (human or agent) on that row sees it in real time. See
212
+ [React](./docs/react.md) for the full `<AbloProvider>` prop surface (`userId`,
213
+ `teamIds`, `syncGroups`, `fallback`, `bootstrapMode`) and status hooks.
131
214
 
132
- For a production integration with React, an existing backend, Data Source, and
133
- future agents, read [Integration Guide](./docs/integration-guide.md).
215
+ ## Identity & Sync Groups
134
216
 
135
- ## AI Activity on Existing State
217
+ Ablo is **not** an auth provider — you keep your own (Clerk, Auth0, NextAuth,
218
+ whatever). Ablo's job starts after you've authenticated a request: you tell it
219
+ *who* is connecting, and it scopes their realtime data to the right **sync
220
+ groups** (named channels like `org:acme` or `deck:abc123` that are both the unit
221
+ of fan-out and the unit of access).
136
222
 
137
- When AI or background work will spend real time on an existing row — not just a
138
- one-shot write coordinate through `ablo.<model>.intent(id)`, the coordination
139
- accessor that sits beside `create`/`update`/`retrieve`. It takes the row's `id` (the same
140
- id you pass to `retrieve(id)` / `update(id, …)`) and returns a handle
141
- synchronously, so you can see who's already working on a row before you start.
223
+ The model is a proxy: your `ABLO_API_KEY` stays on your trusted server, your
224
+ server resolves the signed-in user (org / team / user) from your own auth, and
225
+ the browser connects as an already-scoped participant it never holds the key
226
+ and can't widen its own scope. Your schema's `identityRoles` map that identity
227
+ to sync-group strings.
142
228
 
143
- ```ts
144
- // `report_stockholm` is this weather report's id — set at create time, or the
145
- // `created.id` returned above.
146
- const report = ablo.weatherReports.intent('report_stockholm');
147
-
148
- // Read side: is someone already on it? Wait for them to finish.
149
- if (report.current) {
150
- report.current.heldBy; // 'agent:forecaster'
151
- await report.whenFree();
152
- }
153
-
154
- // Write side: claim so other participants yield while we work.
155
- await report.claim({ action: 'checking_weather', field: 'forecast', ttl: '2m' });
156
-
157
- // Your existing weather tool or agent call. While this runs, other clients see
158
- // that report_stockholm is being checked.
159
- const row = ablo.weatherReports.retrieve('report_stockholm');
160
- const weather = await weatherAgent.getWeather(row.location);
161
-
162
- await report.update({
163
- status: 'ready',
164
- forecast: weather.summary,
165
- });
229
+ ```tsx
230
+ // userId / teamIds come from YOUR auth, resolved server-side
231
+ <AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
232
+ <App />
233
+ </AbloProvider>
166
234
  ```
167
235
 
168
- Ablo does not fetch the weather. It keeps the activity visible while the work
169
- runs, rejects `report.update(...)` with `AbloStaleContextError` if the row
170
- changed under you, and finishes the claim automatically once the write lands.
236
+ If it isn't obvious where org / team / user come from in the Quick Start above,
237
+ that's because they come from *your* app — see
238
+ [Identity & Sync Groups](./docs/identity.md) for the full picture: what a sync
239
+ group is, the two halves of scoping (`identityRoles` + per-model `orgScoped` /
240
+ `syncGroupFormat`), and how identity reaches Ablo without an API key in the
241
+ browser.
171
242
 
172
243
  ## Multiplayer
173
244
 
@@ -176,63 +247,32 @@ workers share the same schema and write through `ablo.<model>`, they all see
176
247
  each other's changes in real time — that's the default, not a feature you turn on.
177
248
 
178
249
  - `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
179
- - `useAblo(...)` gives React clients the live row plus active intents.
180
- - `ablo.<model>.intent(id)` lets humans and agents see and coordinate active work before a write lands.
181
-
182
- Always write through Ablo — the SDK (`ablo.<model>.create/update/delete`) or the
183
- HTTP API (`POST /v1/commits`). If you write straight to your own database
184
- instead, those changes won't reach connected clients. Use Ablo's endpoints and
185
- the fan-out is automatic.
186
-
187
- Under the hood, capabilities, tasks, leases, intents, commits, and receipts are
188
- real protocol primitives. They exist so agent work is scoped, coordinated,
189
- attributable, and cleaned up if a runtime disappears — but you don't touch them
190
- in a first integration. The default path above is enough.
191
-
192
- ## HTTP API
193
-
194
- The SDK is a typed wrapper over a signed HTTP API, the same way `stripe-node`
195
- wraps Stripe's REST API. Everything you do through `ablo.<model>` is reachable
196
- over HTTP, so backends in other languages and server-to-server callers work
197
- without the SDK.
198
-
199
- Writes — create, update, and delete — all go through one endpoint as a batch of
200
- operations:
201
-
202
- ```http
203
- POST /v1/commits
204
- Authorization: Bearer sk_live_...
205
- Idempotency-Key: <your-unique-id>
206
-
207
- {
208
- "operations": [
209
- {
210
- "type": "UPDATE",
211
- "model": "weatherReports",
212
- "id": "report_stockholm",
213
- "input": { "status": "ready" },
214
- "readAt": 1042,
215
- "onStale": "reject"
216
- }
217
- ]
218
- }
219
- ```
250
+ - `useAblo(...)` gives React clients the live row, kept current automatically.
251
+ - `ablo.<model>.claim(id)` / `claimState(id)` / `queue(id)` let humans and agents coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
220
252
 
221
- Each operation is one `CREATE` / `UPDATE` / `DELETE` (plus `ARCHIVE` /
222
- `UNARCHIVE`). Reads fetch a single row with `GET /v1/resources/{model}/{id}`.
223
- The same API key, scope, idempotency, and stale-write rules apply as in the SDK
224
- — the SDK just removes the boilerplate.
253
+ Always write through Ablo either the SDK model methods
254
+ (`ablo.<model>.create/update/delete`) or the HTTP write endpoint below. If you
255
+ write straight to your own database instead, those changes won't reach connected
256
+ clients.
225
257
 
226
- ## Load vs Retrieve
258
+ ## HTTP Writes
227
259
 
228
- Most reads are `retrieve` it's the everyday path, especially inside `useAblo(...)`:
260
+ Use the SDK when you are in JavaScript and want typed models or realtime. Use the
261
+ HTTP endpoint when a server-to-server caller needs to write without opening a
262
+ WebSocket:
229
263
 
230
- - `ablo.weatherReports.retrieve(id)` is sync. It returns the already-loaded row from
231
- the local pool (or `undefined` if it isn't loaded yet). In React,
232
- `useAblo((ablo) => ablo.weatherReports.retrieve(id))` keeps that read live.
233
- - `ablo.weatherReports.load({ where })` is async. Reach for it when you need to
234
- guarantee a row is hydrated before you read it — initial fetch, SSR, scripts, or
235
- any non-reactive runtime.
264
+ ```bash
265
+ curl https://api.ablo.dev/v1/commits \
266
+ -H "Authorization: Bearer sk_test_..." \
267
+ -H "Content-Type: application/json" \
268
+ -d '{ "operations": [
269
+ { "action": "update", "model": "weatherReports", "id": "report_stockholm", "data": { "status": "ready" } }
270
+ ] }'
271
+ ```
272
+
273
+ ```json
274
+ { "object": "commit_receipt", "status": "confirmed", "serverTxId": "tx_…", "lastSyncId": 1042, "ops": 1 }
275
+ ```
236
276
 
237
277
  ## Connect Your Database
238
278
 
@@ -245,18 +285,73 @@ Source: Ablo sends signed commit requests to an endpoint you host, and your app
245
285
  writes its own database. Your `DATABASE_URL` stays in your app — Ablo only ever
246
286
  sees the API key.
247
287
 
248
- See [Connect Your Database](./docs/data-sources.md) for the route and commit shape.
288
+ See [Connect Your Database](./docs/data-sources.md) for the integration shape.
289
+
290
+ ## Configuration
291
+
292
+ `Ablo({ ... })` takes one required option and a couple of transport overrides:
293
+
294
+ | Option | Type | Default | Purpose |
295
+ | --- | --- | --- | --- |
296
+ | `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
297
+ | `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
298
+ | `baseURL` | `string` | `wss://mesh.ablo.finance` | Point at a self-hosted or staging mesh |
299
+
300
+ Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
301
+ authenticates with the signed-in user's session; the raw-key path is gated
302
+ behind `dangerouslyAllowBrowser` for server-proxy setups only. Self-hosted
303
+ deployments can pass `authToken` instead of `apiKey`. Advanced hooks (custom
304
+ `fetch`, logging, observability) live in [Client Behavior](./docs/client-behavior.md).
305
+
306
+ ## Errors
307
+
308
+ Every SDK error extends `AbloError` and carries a `requestId` for support.
309
+ Discriminate with `instanceof` or the `type` string — the string form also
310
+ survives worker / `postMessage` boundaries, where `instanceof` does not:
311
+
312
+ ```ts
313
+ try {
314
+ await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
315
+ } catch (e) {
316
+ if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
317
+ if ((e as AbloError).type === 'AbloClaimedError') { /* another participant holds it */ }
318
+ }
319
+ ```
320
+
321
+ | Error | When |
322
+ | --- | --- |
323
+ | `AbloAuthenticationError` | Invalid / missing / expired credentials |
324
+ | `AbloPermissionError` / `CapabilityError` | Action forbidden by scope |
325
+ | `AbloRateLimitError` | Rate limited (carries `retryAfterSeconds`) |
326
+ | `AbloIdempotencyError` | Same `idempotencyKey` reused with a different body |
327
+ | `AbloValidationError` | Invalid request payload |
328
+ | `AbloStaleContextError` | Write carried `readAt`, but the row has newer changes (`conflicts`) |
329
+ | `AbloClaimedError` | Target is claimed by another participant (`claims`) |
330
+ | `AbloConnectionError` / `AbloServerError` | Transport failure / server 5xx |
331
+ | `SyncSessionError` | Session expired (prompts re-auth) |
332
+
333
+ ## Reconnect & retries
334
+
335
+ The client owns reconnection so your code doesn't have to. A dropped WebSocket
336
+ reconnects automatically with exponential backoff (1s → 30s, ±15% jitter, up to
337
+ ~7.5 minutes); session errors (401/403) suppress it so you re-authenticate
338
+ instead of looping. Commits are idempotent by client transaction id, and a
339
+ commit that times out is never silently rolled back — the client reconciles
340
+ against authoritative server state on reconnect. These defaults are the
341
+ contract; there are no retry or timeout knobs to tune.
249
342
 
250
343
  ## Production Reference
251
344
 
252
- - [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, intent coordination, and agent lifecycle.
345
+ - [Identity & Sync Groups](./docs/identity.md) — bring your own auth; tell Ablo who's connecting and how org / team / user map to sync-group scope.
346
+ - [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, claim coordination, and agent lifecycle.
253
347
  - [Integration Guide](./docs/integration-guide.md) — pick the backing mode and integrate React, Data Source, multiplayer, and agents.
254
348
  - [React](./docs/react.md) — `<AbloProvider>`, `useAblo`, presence, status, and bootstrap gating.
349
+ - [Coordination](./docs/coordination.md) — `claim` / `claimState` / `queue` / `release` reference: hold a row across slow agent work, and observe the line waiting behind it.
255
350
  - [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
256
351
  - [Connect Your Database](./docs/data-sources.md) — keep canonical rows in your app database without giving Ablo database credentials.
257
352
  - [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
258
353
  - [AI SDK Tool](./docs/examples/ai-sdk-tool.md) — use Ablo inside an AI SDK tool call.
259
- - [Server Agent](./docs/examples/server-agent.md) — schema-backed worker plus advanced schema-less run.
354
+ - [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
260
355
 
261
356
  ## License
262
357
 
@@ -21,7 +21,7 @@ import { QueryProcessor } from './core/QueryProcessor.js';
21
21
  import { Model } from './Model.js';
22
22
  import { ModelScope } from './ObjectPool.js';
23
23
  import type { Schema } from './schema/schema.js';
24
- import { type ReaderActions } from './react/useReader.js';
24
+ import { type ReaderActions } from './mutators/readerActions.js';
25
25
  /** Constructor type for Model subclasses (accepts abstract classes) */
26
26
  export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
27
27
  /** Concrete constructor type for instantiation */
@@ -139,7 +139,7 @@ export interface UserContext {
139
139
  * - `'full'` (default): pull every delta in scope before `ready()`
140
140
  * resolves. The standard browser/user replica behavior.
141
141
  * - `'none'`: open the WebSocket and process live deltas only.
142
- * Reads go through `resource.retrieve()` / filtered subscriptions
142
+ * Reads go through `model.retrieve()` / filtered subscriptions
143
143
  * backfilled by `Covering` deltas. Suitable for transactional
144
144
  * participants — agent-worker, video-pipeline, routine runners —
145
145
  * that don't need a local replica of the org's tenant plane.
@@ -22,7 +22,7 @@ import { getContext } from './context.js';
22
22
  import { SyncSessionError } from './errors.js';
23
23
  import { ModelScope } from './ObjectPool.js';
24
24
  import { LazyReferenceCollection } from './LazyReferenceCollection.js';
25
- import { createReaderActions } from './react/useReader.js';
25
+ import { createReaderActions } from './mutators/readerActions.js';
26
26
  /** Bootstrap timeout configuration */
27
27
  export const BOOTSTRAP_CONFIG = {
28
28
  OVERALL_TIMEOUT_MS: 15_000,
@@ -790,7 +790,7 @@ export class BaseSyncedStore {
790
790
  //
791
791
  // `bootstrapMode: 'none'` participants (agent-worker, headless
792
792
  // task runners) skip baseline replication — they read via
793
- // `resource.retrieve()` round-trips and rely on covering deltas
793
+ // `model.retrieve()` round-trips and rely on covering deltas
794
794
  // from filtered subscriptions to populate the pool lazily. The
795
795
  // WS is already open by `setupWebSocketSync` above, so live
796
796
  // delta flow works regardless of this branch.
@@ -2,9 +2,9 @@
2
2
  * Internal compatibility entrypoint for the stateless hosted protocol client.
3
3
  *
4
4
  * Use this build for serverless functions, scripts, and backends that want
5
- * Resource / Intent / Commit over HTTP without the realtime sync runtime.
5
+ * model reads/writes and commits over HTTP without the realtime sync runtime.
6
6
  */
7
- export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Agent, type AgentIntentInput, type AgentIntentOptions, type AgentOptions, type AgentResourceClient, type AgentResourceReadOptions, type AgentResourceMutationOptions, type AgentRunContext, type AgentRunDone, type AgentRunFailed, type AgentRunCancelled, type AgentRunOptions, type AgentRunResult, type AgentRunStatus, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, type Task, type TaskCloseOptions, type TaskCloseResult, type TaskCreateOptions, type TaskResource, } from '../client/ApiClient.js';
8
- export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, BusyOptions, BusyPolicy, ResourceClient, ResourceIntent, ResourceMutationOptions, ResourceReadOptions, ResourceRead, ResourceTarget, } from '../client/Ablo.js';
7
+ export { createProtocolClient, createProtocolClient as Ablo, type AbloApi, type AbloApiClientOptions, type AbloApiIntents, type Agent, type AgentIntentInput, type AgentIntentOptions, type AgentOptions, type AgentModelClient, type AgentModelReadOptions, type AgentModelMutationOptions, type AgentRunContext, type AgentRunDone, type AgentRunFailed, type AgentRunCancelled, type AgentRunOptions, type AgentRunResult, type AgentRunStatus, type Capability, type CapabilityCreateOptions, type CapabilityParticipantKind, type CapabilityRecord, type CapabilityResource, type CapabilityRevocation, type CapabilityScope, type Task, type TaskCloseOptions, type TaskCloseResult, type TaskCreateOptions, type TaskResource, } from '../client/ApiClient.js';
8
+ export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions, ClaimedOptions, IfClaimedPolicy, ModelClient, ModelClaim, ModelMutationOptions, ModelReadOptions, ModelRead, ModelTarget, } from '../client/Ablo.js';
9
9
  import { createProtocolClient } from '../client/ApiClient.js';
10
10
  export default createProtocolClient;
package/dist/api/index.js CHANGED
@@ -2,7 +2,7 @@
2
2
  * Internal compatibility entrypoint for the stateless hosted protocol client.
3
3
  *
4
4
  * Use this build for serverless functions, scripts, and backends that want
5
- * Resource / Intent / Commit over HTTP without the realtime sync runtime.
5
+ * model reads/writes and commits over HTTP without the realtime sync runtime.
6
6
  */
7
7
  export { createProtocolClient, createProtocolClient as Ablo, } from '../client/ApiClient.js';
8
8
  import { createProtocolClient } from '../client/ApiClient.js';