@abloatai/ablo 0.5.0 → 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.
- package/CHANGELOG.md +22 -0
- package/README.md +242 -135
- package/dist/BaseSyncedStore.d.ts +2 -2
- package/dist/BaseSyncedStore.js +2 -2
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +90 -93
- package/dist/client/Ablo.js +121 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +90 -87
- package/dist/client/createModelProxy.js +124 -127
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/errors.d.ts +8 -8
- package/dist/errors.js +18 -10
- package/dist/index.d.ts +9 -8
- package/dist/index.js +7 -11
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +1 -1
- package/dist/react/AbloProvider.js +3 -3
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/diff.d.ts +161 -0
- package/dist/schema/diff.js +262 -0
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +4 -1
- package/dist/schema/index.js +7 -1
- package/dist/schema/schema.d.ts +83 -32
- package/dist/schema/schema.js +58 -12
- package/dist/schema/serialize.d.ts +92 -0
- package/dist/schema/serialize.js +227 -0
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.js +43 -4
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +4 -4
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +37 -9
- package/docs/api.md +68 -158
- package/docs/audit.md +5 -5
- package/docs/client-behavior.md +41 -42
- package/docs/coordination.md +294 -0
- package/docs/data-sources.md +14 -14
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +35 -33
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +30 -55
- package/docs/identity.md +458 -0
- package/docs/index.md +12 -24
- package/docs/integration-guide.md +106 -116
- package/docs/interaction-model.md +29 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +73 -23
- package/docs/roadmap.md +5 -7
- package/llms.txt +34 -39
- package/package.json +1 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,27 @@
|
|
|
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
|
+
|
|
19
|
+
## 0.5.1
|
|
20
|
+
|
|
21
|
+
### Patch Changes
|
|
22
|
+
|
|
23
|
+
- Docs: add a React quick-start (provider + `useAblo`), plain-language rewrite, and a "Set up with Claude Code" section.
|
|
24
|
+
|
|
3
25
|
## 0.5.0
|
|
4
26
|
|
|
5
27
|
### Minor Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Ablo
|
|
2
2
|
|
|
3
|
-
Ablo
|
|
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,29 +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/
|
|
11
|
+
schema -> ablo.<model>.create/retrieve/update/claim(...)
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
##
|
|
14
|
+
## Set up
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install @abloatai/ablo
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
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
|
-
|
|
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
|
-
|
|
25
|
-
|
|
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
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
authenticates with the signed-in user's session — never the raw API key. Do not
|
|
33
|
-
ship `ABLO_API_KEY` in a browser bundle.
|
|
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.
|
|
34
37
|
|
|
35
38
|
## Quick Start
|
|
36
39
|
|
|
@@ -74,139 +77,202 @@ Expected output:
|
|
|
74
77
|
{ id: '...', status: 'ready' }
|
|
75
78
|
```
|
|
76
79
|
|
|
77
|
-
Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
|
|
78
|
-
only for the lower-level client used by custom agents and MCP routes that can't
|
|
79
|
-
import your app's schema.
|
|
80
|
+
Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
|
|
80
81
|
|
|
81
|
-
|
|
82
|
+
## Reading
|
|
82
83
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
+
});
|
|
86
105
|
```
|
|
87
106
|
|
|
88
|
-
|
|
89
|
-
future agents, read [Integration Guide](./docs/integration-guide.md).
|
|
107
|
+
## Writing
|
|
90
108
|
|
|
91
|
-
|
|
109
|
+
`create` / `update` apply optimistically and resolve to the row. Two options
|
|
110
|
+
matter day to day:
|
|
92
111
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
synchronously, so you can see who's already working on a row before you start.
|
|
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. |
|
|
98
116
|
|
|
99
117
|
```ts
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const report = ablo.weatherReports.intent('report_stockholm');
|
|
103
|
-
|
|
104
|
-
// Read side: is someone already on it? Wait for them to finish.
|
|
105
|
-
if (report.current) {
|
|
106
|
-
report.current.heldBy; // 'agent:forecaster'
|
|
107
|
-
await report.whenFree();
|
|
108
|
-
}
|
|
118
|
+
await ablo.weatherReports.update(id, { status: 'ready' }, { wait: 'confirmed' });
|
|
119
|
+
```
|
|
109
120
|
|
|
110
|
-
|
|
111
|
-
|
|
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).
|
|
112
123
|
|
|
113
|
-
|
|
114
|
-
// that report_stockholm is being checked.
|
|
115
|
-
const row = ablo.weatherReports.retrieve('report_stockholm');
|
|
116
|
-
const weather = await weatherAgent.getWeather(row.location);
|
|
124
|
+
## Coordinating long agent work
|
|
117
125
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
|
122
138
|
```
|
|
123
139
|
|
|
124
|
-
|
|
125
|
-
runs, rejects `report.update(...)` with `AbloStaleContextError` if the row
|
|
126
|
-
changed under you, and finishes the claim automatically once the write lands.
|
|
140
|
+
See who's mid-edit before you act — decide to wait, or skip:
|
|
127
141
|
|
|
128
|
-
|
|
142
|
+
```ts
|
|
143
|
+
ablo.weatherReports.claimState('report_stockholm'); // → the holder, or null
|
|
144
|
+
ablo.weatherReports.queue('report_stockholm'); // → { data: [{ heldBy, action, position }, …] }
|
|
129
145
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
146
|
+
await ablo.weatherReports.claim(id, async (report) => {
|
|
147
|
+
/* do the held work */
|
|
148
|
+
}, { wait: false }); // held? skip (dedup) — throws instead of waiting
|
|
133
149
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
The SDK is a typed wrapper over a signed HTTP API, the same way `stripe-node`
|
|
151
|
-
wraps Stripe's REST API. Everything you do through `ablo.<model>` is reachable
|
|
152
|
-
over HTTP, so backends in other languages and server-to-server callers work
|
|
153
|
-
without the SDK.
|
|
154
|
-
|
|
155
|
-
Writes — create, update, and delete — all go through one endpoint as a batch of
|
|
156
|
-
operations:
|
|
157
|
-
|
|
158
|
-
```http
|
|
159
|
-
POST /v1/commits
|
|
160
|
-
Authorization: Bearer sk_live_...
|
|
161
|
-
Idempotency-Key: <your-unique-id>
|
|
162
|
-
|
|
163
|
-
{
|
|
164
|
-
"operations": [
|
|
165
|
-
{
|
|
166
|
-
"type": "UPDATE",
|
|
167
|
-
"model": "weatherReports",
|
|
168
|
-
"id": "report_stockholm",
|
|
169
|
-
"input": { "status": "ready" },
|
|
170
|
-
"readAt": 1042,
|
|
171
|
-
"onStale": "reject"
|
|
172
|
-
}
|
|
173
|
-
]
|
|
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 */ }
|
|
174
165
|
}
|
|
175
166
|
```
|
|
176
167
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
The same API key, scope, idempotency, and stale-write rules apply as in the SDK
|
|
180
|
-
— the SDK just removes the boilerplate.
|
|
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.
|
|
181
170
|
|
|
182
|
-
|
|
171
|
+
See [Coordination](./docs/coordination.md) for the full `claim` / `claimState` /
|
|
172
|
+
`queue` / `release` reference.
|
|
183
173
|
|
|
184
|
-
|
|
174
|
+
## React
|
|
185
175
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
- `ablo.weatherReports.load({ where })` is async. Reach for it when you need to
|
|
190
|
-
guarantee a row is hydrated before you read it — initial fetch, SSR, scripts, or
|
|
191
|
-
any non-reactive runtime.
|
|
176
|
+
In a React app it's the **same `ablo.<model>` API** — just mounted through a
|
|
177
|
+
provider and read with hooks, from `@abloatai/ablo/react`. Wrap your tree once;
|
|
178
|
+
everything inside is live.
|
|
192
179
|
|
|
193
|
-
|
|
180
|
+
```tsx
|
|
181
|
+
import { AbloProvider, useAblo } from '@abloatai/ablo/react';
|
|
182
|
+
import { schema } from './ablo.schema';
|
|
194
183
|
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
184
|
+
function App() {
|
|
185
|
+
return (
|
|
186
|
+
<AbloProvider schema={schema}>
|
|
187
|
+
<Report id="report_stockholm" />
|
|
188
|
+
</AbloProvider>
|
|
189
|
+
);
|
|
190
|
+
}
|
|
198
191
|
|
|
199
|
-
|
|
192
|
+
function Report({ id }: { id: string }) {
|
|
193
|
+
// Reactive read: this re-renders whenever the row changes — whether you,
|
|
194
|
+
// a teammate, or an agent changed it.
|
|
195
|
+
const report = useAblo((ablo) => ablo.weatherReports.retrieve(id));
|
|
196
|
+
const ablo = useAblo();
|
|
200
197
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
198
|
+
if (!report) return null;
|
|
199
|
+
|
|
200
|
+
// Write: same method as the server example above. Optimistic; fans out.
|
|
201
|
+
return (
|
|
202
|
+
<button onClick={() => ablo?.weatherReports.update(id, { status: 'ready' })}>
|
|
203
|
+
{report.status}
|
|
204
|
+
</button>
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
207
|
```
|
|
208
208
|
|
|
209
|
-
|
|
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.
|
|
214
|
+
|
|
215
|
+
## Identity & Sync Groups
|
|
216
|
+
|
|
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).
|
|
222
|
+
|
|
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.
|
|
228
|
+
|
|
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>
|
|
234
|
+
```
|
|
235
|
+
|
|
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.
|
|
242
|
+
|
|
243
|
+
## Multiplayer
|
|
244
|
+
|
|
245
|
+
There is no separate multiplayer mode. When human UI, server actions, and agent
|
|
246
|
+
workers share the same schema and write through `ablo.<model>`, they all see
|
|
247
|
+
each other's changes in real time — that's the default, not a feature you turn on.
|
|
248
|
+
|
|
249
|
+
- `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
|
|
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.
|
|
252
|
+
|
|
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.
|
|
257
|
+
|
|
258
|
+
## HTTP Writes
|
|
259
|
+
|
|
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:
|
|
263
|
+
|
|
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
|
+
```
|
|
210
276
|
|
|
211
277
|
## Connect Your Database
|
|
212
278
|
|
|
@@ -216,35 +282,76 @@ write to Ablo-managed state.
|
|
|
216
282
|
|
|
217
283
|
If your existing database stays the source of truth, connect it as a Data
|
|
218
284
|
Source: Ablo sends signed commit requests to an endpoint you host, and your app
|
|
219
|
-
writes its own database.
|
|
220
|
-
API key
|
|
285
|
+
writes its own database. Your `DATABASE_URL` stays in your app — Ablo only ever
|
|
286
|
+
sees the API key.
|
|
221
287
|
|
|
222
|
-
|
|
223
|
-
# stays in your app — Ablo never receives this
|
|
224
|
-
DATABASE_URL=postgres://...
|
|
288
|
+
See [Connect Your Database](./docs/data-sources.md) for the integration shape.
|
|
225
289
|
|
|
226
|
-
|
|
227
|
-
ABLO_API_KEY=sk_live_...
|
|
228
|
-
```
|
|
290
|
+
## Configuration
|
|
229
291
|
|
|
230
|
-
|
|
292
|
+
`Ablo({ ... })` takes one required option and a couple of transport overrides:
|
|
231
293
|
|
|
232
|
-
|
|
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
|
+
```
|
|
233
320
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
`
|
|
237
|
-
|
|
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.
|
|
238
342
|
|
|
239
343
|
## Production Reference
|
|
240
344
|
|
|
241
|
-
- [
|
|
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.
|
|
242
347
|
- [Integration Guide](./docs/integration-guide.md) — pick the backing mode and integrate React, Data Source, multiplayer, and agents.
|
|
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.
|
|
243
350
|
- [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
|
|
244
351
|
- [Connect Your Database](./docs/data-sources.md) — keep canonical rows in your app database without giving Ablo database credentials.
|
|
245
352
|
- [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
|
|
246
353
|
- [AI SDK Tool](./docs/examples/ai-sdk-tool.md) — use Ablo inside an AI SDK tool call.
|
|
247
|
-
- [Server Agent](./docs/examples/server-agent.md) — schema-backed worker
|
|
354
|
+
- [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
|
|
248
355
|
|
|
249
356
|
## License
|
|
250
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 './
|
|
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 `
|
|
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.
|
package/dist/BaseSyncedStore.js
CHANGED
|
@@ -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 './
|
|
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
|
-
// `
|
|
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.
|
package/dist/api/index.d.ts
CHANGED
|
@@ -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
|
-
*
|
|
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
|
|
8
|
-
export type { CommitCreateOptions, CommitOperationInput, CommitReceipt, CommitWait, IntentCreateOptions, IntentHandle, IntentWaitOptions,
|
|
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
|
-
*
|
|
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';
|