@abloatai/ablo 0.5.1 → 0.7.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 +61 -0
- package/README.md +248 -124
- package/dist/BaseSyncedStore.d.ts +3 -3
- package/dist/BaseSyncedStore.js +3 -3
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +91 -93
- package/dist/client/Ablo.js +122 -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 +116 -90
- package/dist/client/createModelProxy.js +128 -128
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +5 -5
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +264 -0
- package/dist/errorCodes.js +251 -0
- package/dist/errors.d.ts +59 -14
- package/dist/errors.js +73 -12
- package/dist/index.d.ts +11 -9
- package/dist/index.js +8 -12
- 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/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +13 -1
- package/dist/react/AbloProvider.js +14 -6
- 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/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +167 -0
- package/dist/schema/diff.js +280 -0
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +9 -3
- package/dist/schema/index.js +14 -2
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +10 -69
- package/dist/schema/schema.js +58 -24
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +96 -0
- package/dist/schema/serialize.js +231 -0
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +23 -17
- 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.d.ts +2 -1
- package/dist/sync/createIntentStream.js +89 -5
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +9 -18
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +90 -42
- package/docs/api-keys.md +44 -0
- package/docs/api.md +72 -173
- package/docs/audit.md +5 -5
- package/docs/cli.md +212 -0
- package/docs/client-behavior.md +42 -43
- package/docs/coordination.md +343 -0
- package/docs/data-sources.md +16 -16
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +38 -36
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/scoped-agent.md +78 -0
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +34 -56
- package/docs/identity.md +529 -0
- package/docs/index.md +18 -24
- package/docs/integration-guide.md +130 -144
- package/docs/interaction-model.md +32 -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 +74 -24
- package/docs/roadmap.md +17 -7
- package/llms.txt +34 -39
- package/package.json +8 -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,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.7.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- Structured error contract, schema/migration engine, and a full `ablo` CLI.
|
|
8
|
+
- **Structured error contract across HTTP + WS planes.** A closed, canonical
|
|
9
|
+
error-code registry is now the `code` tier of a Stripe-style error model. A
|
|
10
|
+
single HTTP egress funnel converts every throw to a canonical
|
|
11
|
+
`{ type, code, message, doc_url, request_id, ...details }` envelope; the WS
|
|
12
|
+
plane narrows mutation/claim error codes to the same union.
|
|
13
|
+
- **Versioned contract + drift guard.** `ERROR_CONTRACT_VERSION` (date-based)
|
|
14
|
+
ships in `errors.json` and on the `Ablo-Version` response header, so consumers
|
|
15
|
+
detect contract changes without diffing docs. Generated `errors.mdx` /
|
|
16
|
+
`errors.json` plus a CI drift guard keep the docs, OpenAPI spec, and SDK from
|
|
17
|
+
silently diverging from the registry.
|
|
18
|
+
- **Always-on request correlation.** Every response carries a `req_…` request id
|
|
19
|
+
(honoring an inbound `x-request-id`), stamped into the envelope's `request_id`.
|
|
20
|
+
- **OpenAPI parity.** The stale `{ error, reason }` schema is replaced by the
|
|
21
|
+
canonical envelope plus a generated `ErrorCode` enum.
|
|
22
|
+
|
|
23
|
+
CLI + schema:
|
|
24
|
+
- **Schema diff + migration planning engine** (`generateProvisionPlan` /
|
|
25
|
+
`generateMigrationPlan` in `@abloatai/ablo/schema`) — pure diff, classify,
|
|
26
|
+
apply, and constant-value backfill for required-field migrations.
|
|
27
|
+
- **`ablo generate`** — emit TypeScript types from the pushed schema.
|
|
28
|
+
- **Full `ablo` CLI suite**, Stripe-CLI-shaped: `init`, `login` / `logout` /
|
|
29
|
+
`status`, `mode [test|live]`, `dev` (push schema to the test sandbox + watch),
|
|
30
|
+
`logs` (tail your scope's commit activity), and the data-source commands below.
|
|
31
|
+
Authentication is the OAuth 2.0 device flow; `login` provisions and stores a
|
|
32
|
+
test and a live key, and `mode` switches the active one.
|
|
33
|
+
- **Database-URL structure (bring-your-own-database).** The CLI is split by where
|
|
34
|
+
it writes:
|
|
35
|
+
- `ablo pull` / `ablo check` / `ablo migrate` operate on **your own
|
|
36
|
+
`DATABASE_URL`** — `pull` introspects it to emit `defineSchema(...)` from
|
|
37
|
+
existing tables (read-only, like `prisma db pull`), `check` verifies tables
|
|
38
|
+
fit the schema with no DDL, and `migrate` applies DDL to `DATABASE_URL`.
|
|
39
|
+
- `ablo schema push` / `ablo dev` target the **hosted** test/live sandbox; the
|
|
40
|
+
server diffs, migrates, and activates the uploaded schema. `dev` never
|
|
41
|
+
touches live data.
|
|
42
|
+
|
|
43
|
+
**BREAKING** — removed the legacy React hooks `useQuery` / `useOne` / `useMutate`
|
|
44
|
+
/ `useReader`. Use `useAblo()` + `ablo.<model>.*` instead. The `MutateActions`,
|
|
45
|
+
`ReaderActions`, and `ReaderFindOptions` types are still re-exported for callers
|
|
46
|
+
that referenced them.
|
|
47
|
+
|
|
48
|
+
## 0.6.0
|
|
49
|
+
|
|
50
|
+
### Minor Changes
|
|
51
|
+
|
|
52
|
+
- 0f663e7: Coordination surface: fair queue, reactive wait-line, and lease renewal.
|
|
53
|
+
- **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`.
|
|
54
|
+
- **`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)`.
|
|
55
|
+
- **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.
|
|
56
|
+
- **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).
|
|
57
|
+
- **Reads are never gated by a claim**, including for agents.
|
|
58
|
+
- Intent vocabulary cleanup: a waiting claim is an `Intent` with `status: 'queued'` (`position` carries its place in line). Removed the unbuilt `whenFree`.
|
|
59
|
+
|
|
60
|
+
- **BREAKING — API renames** (apply when upgrading from 0.5.1):
|
|
61
|
+
- Change-listeners renamed to `.onChange(...)`: `ablo.<model>.subscribe(cb)`, `presence.subscribe()`, `intents.subscribe()` → `.onChange(...)`. (`subscribe` is reserved for an upcoming scope-grant verb.)
|
|
62
|
+
- 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`.
|
|
63
|
+
|
|
3
64
|
## 0.5.1
|
|
4
65
|
|
|
5
66
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
# Ablo
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@abloatai/ablo)
|
|
4
|
+
[](./LICENSE)
|
|
5
|
+
[](#)
|
|
6
|
+
[](#keys--runtime)
|
|
7
|
+
|
|
8
|
+
Ablo is a typed sync engine for shared app state — the kind that humans,
|
|
4
9
|
server code, and AI agents all edit at once.
|
|
5
10
|
|
|
6
11
|
Reach for it when those edits need to show up everywhere in real time, not
|
|
@@ -8,28 +13,49 @@ silently overwrite each other, expose who's working on what, and leave a record
|
|
|
8
13
|
of who changed what.
|
|
9
14
|
|
|
10
15
|
```txt
|
|
11
|
-
schema -> ablo.<model>.create/retrieve/
|
|
16
|
+
schema -> ablo.<model>.create/retrieve/update/claim(...)
|
|
12
17
|
```
|
|
13
18
|
|
|
14
|
-
##
|
|
19
|
+
## Why Ablo
|
|
20
|
+
|
|
21
|
+
- **Real-time by default.** Every `create` / `update` / `delete` fans out
|
|
22
|
+
confirmed deltas to all subscribers — humans and agents — with no separate
|
|
23
|
+
"multiplayer mode" to switch on.
|
|
24
|
+
- **No silent clobbers.** Writes are guarded against stale reads, and `claim`
|
|
25
|
+
holds a row across a slow read → LLM → write gap so concurrent edits queue
|
|
26
|
+
instead of overwriting.
|
|
27
|
+
- **Built for agents.** See who's mid-edit (`claimState` / `queue`), coordinate a
|
|
28
|
+
fair line, and ship an `llms.txt` so coding agents integrate from the real API.
|
|
29
|
+
- **Typed end to end.** Your Zod schema produces typed model proxies
|
|
30
|
+
(`ablo.<model>.update(...)`), optimistic local reads, and reactive React hooks.
|
|
31
|
+
- **Bring your own auth and database.** Ablo scopes realtime data to *sync
|
|
32
|
+
groups* from your existing identity, and can leave your database as the source
|
|
33
|
+
of truth via a Data Source.
|
|
34
|
+
|
|
35
|
+
**Built for:** collaborative editors, AI agent workflows, internal tools, and any
|
|
36
|
+
app where multiple actors mutate shared state and everyone must see it live.
|
|
37
|
+
|
|
38
|
+
## Set up
|
|
15
39
|
|
|
16
40
|
```bash
|
|
17
41
|
npm install @abloatai/ablo
|
|
18
42
|
```
|
|
19
43
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
44
|
+
**Keys & runtime.** Ablo needs Node 22+ and TypeScript 5+. Grab an `sk_test_*`
|
|
45
|
+
key for a sandbox
|
|
46
|
+
(`export ABLO_API_KEY=sk_test_...`); keep keys in trusted server runtimes only.
|
|
47
|
+
In the browser, `<AbloProvider>` authenticates with the signed-in user's
|
|
48
|
+
session — never the raw key.
|
|
23
49
|
|
|
24
|
-
|
|
25
|
-
|
|
50
|
+
Then wire it by hand — the [Quick Start](#quick-start) below is the shape to
|
|
51
|
+
copy. For production (React, an existing backend, Data Source, agents), the
|
|
52
|
+
[Integration Guide](./docs/integration-guide.md) is the deeper map.
|
|
26
53
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
54
|
+
**Prefer to let an agent wire it?** The package ships an `llms.txt` — a precise
|
|
55
|
+
map of the API — so Claude Code or Cursor integrates from the real surface
|
|
56
|
+
instead of guessing:
|
|
30
57
|
|
|
31
|
-
|
|
32
|
-
authenticates with the signed-in user's session — never the raw API key.
|
|
58
|
+
> Read `node_modules/@abloatai/ablo/llms.txt`, then add an Ablo schema, a `<AbloProvider>`, and my first create / retrieve / update.
|
|
33
59
|
|
|
34
60
|
## Quick Start
|
|
35
61
|
|
|
@@ -73,9 +99,104 @@ Expected output:
|
|
|
73
99
|
{ id: '...', status: 'ready' }
|
|
74
100
|
```
|
|
75
101
|
|
|
76
|
-
Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
|
|
77
|
-
|
|
78
|
-
|
|
102
|
+
Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
|
|
103
|
+
|
|
104
|
+
## Reading
|
|
105
|
+
|
|
106
|
+
`retrieve(id)` returns one row from the local cache — synchronous, no round-trip.
|
|
107
|
+
`list(...)` filters and sorts what's already synced; it's also synchronous, and
|
|
108
|
+
reactive under `useAblo`/`subscribe`. `load(...)` fetches from the server when a
|
|
109
|
+
row may not be local yet.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
ablo.weatherReports.retrieve('report_stockholm');
|
|
113
|
+
|
|
114
|
+
const pending = ablo.weatherReports.list({
|
|
115
|
+
where: { status: 'pending' },
|
|
116
|
+
orderBy: { location: 'asc' },
|
|
117
|
+
limit: 20,
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
const ready = await ablo.weatherReports.load({
|
|
121
|
+
where: { status: 'ready' },
|
|
122
|
+
type: 'complete',
|
|
123
|
+
});
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
An array value in `where` means `IN`. On `load`, `type: 'complete'` waits for
|
|
127
|
+
the server; `'unknown'` returns what's local now and refreshes in the background.
|
|
128
|
+
|
|
129
|
+
## Writing
|
|
130
|
+
|
|
131
|
+
`create` / `update` apply optimistically and resolve to the row. Two options
|
|
132
|
+
matter day to day:
|
|
133
|
+
|
|
134
|
+
| Option | Values | What it does |
|
|
135
|
+
| --- | --- | --- |
|
|
136
|
+
| `wait` | `'queued'` \| `'confirmed'` | `'confirmed'` resolves only after the server acks the write; `'queued'` resolves as soon as it's locally queued (fire-and-forget). |
|
|
137
|
+
| `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. |
|
|
138
|
+
|
|
139
|
+
```ts
|
|
140
|
+
await ablo.weatherReports.update(id, { status: 'ready' }, { wait: 'confirmed' });
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
To guard a write against a row that changed under you, pass `readAt` + `onStale`
|
|
144
|
+
— see [Coordinating long agent work](#coordinating-long-agent-work).
|
|
145
|
+
|
|
146
|
+
## Coordinating long agent work
|
|
147
|
+
|
|
148
|
+
An agent reads a row, thinks for 30s, writes back — and clobbers whatever changed
|
|
149
|
+
meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
|
|
150
|
+
|
|
151
|
+
```ts
|
|
152
|
+
await ablo.weatherReports.claim('report_stockholm', async (report) => {
|
|
153
|
+
const forecast = await weatherAgent.getWeather(report.location);
|
|
154
|
+
await ablo.weatherReports.update(report.id, { forecast, status: 'ready' });
|
|
155
|
+
});
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
If someone else holds the row, `claim()` waits in a fair queue, then re-reads —
|
|
159
|
+
so `report` is the current row, never a stale snapshot. Reads stay open by
|
|
160
|
+
default; only acting on the row serializes. The claim releases when the callback
|
|
161
|
+
returns or throws.
|
|
162
|
+
|
|
163
|
+
See who's mid-edit before you act — decide to wait, or skip:
|
|
164
|
+
|
|
165
|
+
```ts
|
|
166
|
+
ablo.weatherReports.claimState('report_stockholm');
|
|
167
|
+
ablo.weatherReports.queue('report_stockholm');
|
|
168
|
+
|
|
169
|
+
await ablo.weatherReports.claim(id, async (report) => {
|
|
170
|
+
/* do the held work */
|
|
171
|
+
}, { wait: false });
|
|
172
|
+
|
|
173
|
+
await ablo.weatherReports.claim(id, async (report) => {
|
|
174
|
+
/* do the held work */
|
|
175
|
+
}, { maxQueueDepth: 2 });
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
`claimState` returns the holder (or `null`); `queue` returns the line waiting
|
|
179
|
+
behind it. `wait: false` skips rather than waiting when the row is held;
|
|
180
|
+
`maxQueueDepth: 2` bails when two or more are already ahead.
|
|
181
|
+
|
|
182
|
+
Default reads keep working while a row is claimed. Server reads that need claimed
|
|
183
|
+
semantics can opt in with `ifClaimed: 'return' | 'wait' | 'fail'`.
|
|
184
|
+
|
|
185
|
+
Even an unclaimed write can't land on stale reasoning — the commit is guarded:
|
|
186
|
+
|
|
187
|
+
```ts
|
|
188
|
+
try {
|
|
189
|
+
await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
|
|
190
|
+
} catch (e) {
|
|
191
|
+
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
192
|
+
}
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
> Prefer the callback form for ordinary held work. Manual scoped claims are
|
|
196
|
+
> available for wider lifetimes, but callback claims are the docs default.
|
|
197
|
+
|
|
198
|
+
See [Coordination](./docs/coordination.md) for the full `claim` / `claimState` /
|
|
199
|
+
`queue` / `release` reference.
|
|
79
200
|
|
|
80
201
|
## React
|
|
81
202
|
|
|
@@ -85,7 +206,7 @@ everything inside is live.
|
|
|
85
206
|
|
|
86
207
|
```tsx
|
|
87
208
|
import { AbloProvider, useAblo } from '@abloatai/ablo/react';
|
|
88
|
-
import { schema } from './ablo
|
|
209
|
+
import { schema } from './ablo/schema';
|
|
89
210
|
|
|
90
211
|
function App() {
|
|
91
212
|
return (
|
|
@@ -96,14 +217,11 @@ function App() {
|
|
|
96
217
|
}
|
|
97
218
|
|
|
98
219
|
function Report({ id }: { id: string }) {
|
|
99
|
-
// Reactive read: this re-renders whenever the row changes — whether you,
|
|
100
|
-
// a teammate, or an agent changed it.
|
|
101
220
|
const report = useAblo((ablo) => ablo.weatherReports.retrieve(id));
|
|
102
221
|
const ablo = useAblo();
|
|
103
222
|
|
|
104
223
|
if (!report) return null;
|
|
105
224
|
|
|
106
|
-
// Write: same method as the server example above. Optimistic; fans out.
|
|
107
225
|
return (
|
|
108
226
|
<button onClick={() => ablo?.weatherReports.update(id, { status: 'ready' })}>
|
|
109
227
|
{report.status}
|
|
@@ -112,62 +230,44 @@ function Report({ id }: { id: string }) {
|
|
|
112
230
|
}
|
|
113
231
|
```
|
|
114
232
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
agent) on that row sees it in real time. See [React](./docs/react.md) for
|
|
119
|
-
`fallback`, `bootstrapMode`, presence, and status hooks.
|
|
233
|
+
The `useAblo(selector)` read re-renders whenever the row changes — whether you,
|
|
234
|
+
a teammate, or an agent changed it. The write is the same optimistic, fan-out
|
|
235
|
+
method as the server example above.
|
|
120
236
|
|
|
121
|
-
|
|
237
|
+
`<AbloProvider>` owns the connection — no API key in the browser. That's the
|
|
238
|
+
whole loop: read with `useAblo(selector)`, write with `ablo.<model>`, and every
|
|
239
|
+
other client (human or agent) on that row sees it in real time. See
|
|
240
|
+
[React](./docs/react.md) for the full `<AbloProvider>` prop surface (`userId`,
|
|
241
|
+
`teamIds`, `syncGroups`, `fallback`, `bootstrapMode`) and status hooks.
|
|
122
242
|
|
|
123
|
-
|
|
124
|
-
Point your coding agent at it and let it do the integration:
|
|
243
|
+
## Identity & Sync Groups
|
|
125
244
|
|
|
126
|
-
|
|
127
|
-
|
|
245
|
+
Ablo is **not** an auth provider — you keep your own (Clerk, Auth0, NextAuth,
|
|
246
|
+
whatever). Ablo's job starts after you've authenticated a request: you tell it
|
|
247
|
+
*who* is connecting, and it scopes their realtime data to the right **sync
|
|
248
|
+
groups** (named channels like `org:acme` or `deck:abc123` that are both the unit
|
|
249
|
+
of fan-out and the unit of access).
|
|
128
250
|
|
|
129
|
-
|
|
130
|
-
|
|
251
|
+
The model is a proxy: your `ABLO_API_KEY` stays on your trusted server, your
|
|
252
|
+
server resolves the signed-in user (org / team / user) from your own auth, and
|
|
253
|
+
the browser connects as an already-scoped participant — it never holds the key
|
|
254
|
+
and can't widen its own scope. Your schema's `identityRoles` map that identity
|
|
255
|
+
to sync-group strings.
|
|
131
256
|
|
|
132
|
-
|
|
133
|
-
future agents, read [Integration Guide](./docs/integration-guide.md).
|
|
257
|
+
`userId` / `teamIds` come from your auth, resolved server-side:
|
|
134
258
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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.
|
|
142
|
-
|
|
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
|
-
});
|
|
259
|
+
```tsx
|
|
260
|
+
<AbloProvider schema={schema} userId={user.id} teamIds={user.teamIds}>
|
|
261
|
+
<App />
|
|
262
|
+
</AbloProvider>
|
|
166
263
|
```
|
|
167
264
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
265
|
+
If it isn't obvious where org / team / user come from in the Quick Start above,
|
|
266
|
+
that's because they come from *your* app — see
|
|
267
|
+
[Identity & Sync Groups](./docs/identity.md) for the full picture: what a sync
|
|
268
|
+
group is, the two halves of scoping (`identityRoles` + per-model `orgScoped` /
|
|
269
|
+
`syncGroupFormat`), and how identity reaches Ablo without an API key in the
|
|
270
|
+
browser.
|
|
171
271
|
|
|
172
272
|
## Multiplayer
|
|
173
273
|
|
|
@@ -176,63 +276,32 @@ workers share the same schema and write through `ablo.<model>`, they all see
|
|
|
176
276
|
each other's changes in real time — that's the default, not a feature you turn on.
|
|
177
277
|
|
|
178
278
|
- `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
|
|
179
|
-
- `useAblo(...)` gives React clients the live row
|
|
180
|
-
- `ablo.<model>.
|
|
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
|
-
```
|
|
279
|
+
- `useAblo(...)` gives React clients the live row, kept current automatically.
|
|
280
|
+
- `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
281
|
|
|
221
|
-
|
|
222
|
-
`
|
|
223
|
-
|
|
224
|
-
|
|
282
|
+
Always write through Ablo — either the SDK model methods
|
|
283
|
+
(`ablo.<model>.create/update/delete`) or the HTTP write endpoint below. If you
|
|
284
|
+
write straight to your own database instead, those changes won't reach connected
|
|
285
|
+
clients.
|
|
225
286
|
|
|
226
|
-
##
|
|
287
|
+
## HTTP Writes
|
|
227
288
|
|
|
228
|
-
|
|
289
|
+
Use the SDK when you are in JavaScript and want typed models or realtime. Use the
|
|
290
|
+
HTTP endpoint when a server-to-server caller needs to write without opening a
|
|
291
|
+
WebSocket:
|
|
229
292
|
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
-
|
|
234
|
-
|
|
235
|
-
|
|
293
|
+
```bash
|
|
294
|
+
curl https://api.abloatai.com/v1/commits \
|
|
295
|
+
-H "Authorization: Bearer sk_test_..." \
|
|
296
|
+
-H "Content-Type: application/json" \
|
|
297
|
+
-d '{ "operations": [
|
|
298
|
+
{ "action": "update", "model": "weatherReports", "id": "report_stockholm", "data": { "status": "ready" } }
|
|
299
|
+
] }'
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
```json
|
|
303
|
+
{ "object": "commit_receipt", "status": "confirmed", "serverTxId": "tx_…", "lastSyncId": 1042, "ops": 1 }
|
|
304
|
+
```
|
|
236
305
|
|
|
237
306
|
## Connect Your Database
|
|
238
307
|
|
|
@@ -245,18 +314,73 @@ Source: Ablo sends signed commit requests to an endpoint you host, and your app
|
|
|
245
314
|
writes its own database. Your `DATABASE_URL` stays in your app — Ablo only ever
|
|
246
315
|
sees the API key.
|
|
247
316
|
|
|
248
|
-
See [Connect Your Database](./docs/data-sources.md) for the
|
|
317
|
+
See [Connect Your Database](./docs/data-sources.md) for the integration shape.
|
|
318
|
+
|
|
319
|
+
## Configuration
|
|
320
|
+
|
|
321
|
+
`Ablo({ ... })` takes one required option and a couple of transport overrides:
|
|
322
|
+
|
|
323
|
+
| Option | Type | Default | Purpose |
|
|
324
|
+
| --- | --- | --- | --- |
|
|
325
|
+
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
326
|
+
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
327
|
+
| `baseURL` | `string` | `wss://mesh.ablo.finance` | Point at a self-hosted or staging mesh |
|
|
328
|
+
|
|
329
|
+
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
330
|
+
authenticates with the signed-in user's session; the raw-key path is gated
|
|
331
|
+
behind `dangerouslyAllowBrowser` for server-proxy setups only. Self-hosted
|
|
332
|
+
deployments can pass `authToken` instead of `apiKey`. Advanced hooks (custom
|
|
333
|
+
`fetch`, logging, observability) live in [Client Behavior](./docs/client-behavior.md).
|
|
334
|
+
|
|
335
|
+
## Errors
|
|
336
|
+
|
|
337
|
+
Every SDK error extends `AbloError` and carries a `requestId` for support.
|
|
338
|
+
Discriminate with `instanceof` or the `type` string — the string form also
|
|
339
|
+
survives worker / `postMessage` boundaries, where `instanceof` does not:
|
|
340
|
+
|
|
341
|
+
```ts
|
|
342
|
+
try {
|
|
343
|
+
await ablo.weatherReports.update(id, { status: 'ready' }, { readAt, onStale: 'reject' });
|
|
344
|
+
} catch (e) {
|
|
345
|
+
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
346
|
+
if ((e as AbloError).type === 'AbloClaimedError') { /* another participant holds it */ }
|
|
347
|
+
}
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
| Error | When |
|
|
351
|
+
| --- | --- |
|
|
352
|
+
| `AbloAuthenticationError` | Invalid / missing / expired credentials |
|
|
353
|
+
| `AbloPermissionError` / `CapabilityError` | Action forbidden by scope |
|
|
354
|
+
| `AbloRateLimitError` | Rate limited (carries `retryAfterSeconds`) |
|
|
355
|
+
| `AbloIdempotencyError` | Same `idempotencyKey` reused with a different body |
|
|
356
|
+
| `AbloValidationError` | Invalid request payload |
|
|
357
|
+
| `AbloStaleContextError` | Write carried `readAt`, but the row has newer changes (`conflicts`) |
|
|
358
|
+
| `AbloClaimedError` | Target is claimed by another participant (`claims`) |
|
|
359
|
+
| `AbloConnectionError` / `AbloServerError` | Transport failure / server 5xx |
|
|
360
|
+
| `SyncSessionError` | Session expired (prompts re-auth) |
|
|
361
|
+
|
|
362
|
+
## Reconnect & retries
|
|
363
|
+
|
|
364
|
+
The client owns reconnection so your code doesn't have to. A dropped WebSocket
|
|
365
|
+
reconnects automatically with exponential backoff (1s → 30s, ±15% jitter, up to
|
|
366
|
+
~7.5 minutes); session errors (401/403) suppress it so you re-authenticate
|
|
367
|
+
instead of looping. Commits are idempotent by client transaction id, and a
|
|
368
|
+
commit that times out is never silently rolled back — the client reconciles
|
|
369
|
+
against authoritative server state on reconnect. These defaults are the
|
|
370
|
+
contract; there are no retry or timeout knobs to tune.
|
|
249
371
|
|
|
250
372
|
## Production Reference
|
|
251
373
|
|
|
252
|
-
- [
|
|
374
|
+
- [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.
|
|
375
|
+
- [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, claim coordination, and agent lifecycle.
|
|
253
376
|
- [Integration Guide](./docs/integration-guide.md) — pick the backing mode and integrate React, Data Source, multiplayer, and agents.
|
|
254
377
|
- [React](./docs/react.md) — `<AbloProvider>`, `useAblo`, presence, status, and bootstrap gating.
|
|
378
|
+
- [Coordination](./docs/coordination.md) — `claim` / `claimState` / `queue` / `release` reference: hold a row across slow agent work, and observe the line waiting behind it.
|
|
255
379
|
- [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
|
|
256
380
|
- [Connect Your Database](./docs/data-sources.md) — keep canonical rows in your app database without giving Ablo database credentials.
|
|
257
381
|
- [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
|
|
258
382
|
- [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
|
|
383
|
+
- [Server Agent](./docs/examples/server-agent.md) — schema-backed worker.
|
|
260
384
|
|
|
261
385
|
## License
|
|
262
386
|
|
|
@@ -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.
|
|
@@ -662,7 +662,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
662
662
|
*/
|
|
663
663
|
queryByClass(modelClass: ModelConstructor<Model>, options?: {
|
|
664
664
|
predicate?: (model: Model) => boolean;
|
|
665
|
-
|
|
665
|
+
state?: ModelScope;
|
|
666
666
|
orderBy?: keyof Model;
|
|
667
667
|
order?: 'asc' | 'desc';
|
|
668
668
|
limit?: number;
|
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.
|
|
@@ -1688,7 +1688,7 @@ export class BaseSyncedStore {
|
|
|
1688
1688
|
const modelName = this.objectPool.registry.getModelNameFromConstructor(modelClass);
|
|
1689
1689
|
if (!modelName)
|
|
1690
1690
|
return { data: [], total: 0, hasMore: false };
|
|
1691
|
-
let allModels = this.objectPool.getByType(modelClass, options?.
|
|
1691
|
+
let allModels = this.objectPool.getByType(modelClass, options?.state ?? ModelScope.live);
|
|
1692
1692
|
// Filter out pending deletes
|
|
1693
1693
|
allModels = allModels.filter((m) => !this.pendingDeletes.has(m.id));
|
|
1694
1694
|
// Apply predicate
|
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';
|