@abloatai/ablo 0.6.0 → 0.8.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 +77 -0
- package/README.md +95 -57
- package/dist/BaseSyncedStore.d.ts +1 -1
- package/dist/BaseSyncedStore.js +8 -4
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +3 -2
- package/dist/auth/index.js +39 -11
- package/dist/client/Ablo.d.ts +112 -3
- package/dist/client/Ablo.js +144 -10
- package/dist/client/ApiClient.d.ts +32 -0
- package/dist/client/ApiClient.js +76 -44
- package/dist/client/auth.d.ts +11 -1
- package/dist/client/auth.js +21 -2
- package/dist/client/createModelProxy.d.ts +120 -53
- package/dist/client/createModelProxy.js +66 -31
- package/dist/client/identity.js +14 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +57 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- 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/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +286 -0
- package/dist/errorCodes.js +284 -0
- package/dist/errors.d.ts +103 -7
- package/dist/errors.js +192 -41
- package/dist/index.d.ts +11 -6
- package/dist/index.js +10 -6
- package/dist/keys/index.d.ts +61 -0
- package/dist/keys/index.js +151 -0
- 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/client.js +19 -8
- package/dist/react/AbloProvider.d.ts +37 -0
- package/dist/react/AbloProvider.js +107 -4
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +6 -0
- package/dist/schema/diff.js +21 -3
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/index.d.ts +7 -4
- package/dist/schema/index.js +9 -3
- 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 +2 -112
- package/dist/schema/schema.js +50 -62
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +16 -12
- package/dist/schema/serialize.js +16 -12
- 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/BootstrapHelper.js +46 -27
- package/dist/sync/ConnectionManager.d.ts +3 -1
- package/dist/sync/ConnectionManager.js +37 -1
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +26 -19
- package/dist/sync/NetworkProbe.d.ts +8 -0
- package/dist/sync/NetworkProbe.js +24 -2
- package/dist/sync/SyncWebSocket.d.ts +1 -1
- package/dist/sync/SyncWebSocket.js +43 -53
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +46 -1
- package/dist/sync/participants.js +10 -16
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +53 -33
- package/docs/api-keys.md +47 -3
- package/docs/api.md +103 -57
- package/docs/audit.md +16 -9
- package/docs/cli.md +222 -0
- package/docs/client-behavior.md +35 -21
- package/docs/coordination.md +74 -36
- package/docs/data-sources.md +23 -21
- package/docs/examples/agent-human.md +72 -28
- package/docs/examples/ai-sdk-tool.md +14 -11
- package/docs/examples/existing-python-backend.md +30 -19
- package/docs/examples/nextjs.md +21 -8
- package/docs/examples/scoped-agent.md +93 -0
- package/docs/examples/server-agent.md +27 -5
- package/docs/guarantees.md +29 -17
- package/docs/identity.md +198 -121
- package/docs/index.md +35 -18
- package/docs/integration-guide.md +79 -83
- package/docs/interaction-model.md +40 -25
- package/docs/mcp/claude-code.md +9 -17
- package/docs/mcp/cursor.md +6 -24
- package/docs/mcp/windsurf.md +6 -19
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +31 -39
- package/docs/react.md +18 -14
- package/docs/roadmap.md +15 -3
- package/docs/schema-contract.md +109 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +6 -2
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +27 -16
- package/package.json +13 -1
package/docs/mcp.md
CHANGED
|
@@ -1,44 +1,121 @@
|
|
|
1
1
|
# Model Context Protocol
|
|
2
2
|
|
|
3
|
-
Ablo
|
|
4
|
-
assistant — Claude Code, Cursor, Windsurf — and your sync models become
|
|
5
|
-
typed, callable tools.
|
|
3
|
+
Ablo publishes **two** MCP servers for two different jobs. Don't confuse them:
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+
| Server | Purpose | Auth | Tools |
|
|
6
|
+
|---|---|---|---|
|
|
7
|
+
| **Coordination** (`@ablo/mcp`) | Let an agent safely read & mutate your application data | API key (`sk_…` / `rk_…`) | per-model `get` / `list` / `create` / `update` / `delete` / `claim` / `release` |
|
|
8
|
+
| **Integration-helper** (hosted `/api/mcp`) | Help an AI coding assistant write SDK integration code that compiles | none (public docs) | doc search, export surface, schema lint, scaffold |
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
The coordination server **is the data plane** — it is how an agent changes
|
|
11
|
+
state. The integration-helper server only serves docs, schema lint, and
|
|
12
|
+
scaffolds; it does **not** read or write application data (there are no
|
|
13
|
+
per-model data tools on it). Pick by what you're doing: shipping an agent that
|
|
14
|
+
edits rows → coordination; teaching your IDE assistant the SDK → helper.
|
|
15
|
+
|
|
16
|
+
## Coordination server (`@ablo/mcp`)
|
|
17
|
+
|
|
18
|
+
The coordination server is the MCP projection of the model-scoped API
|
|
19
|
+
(`/v1/models/...`) — the same surface as `ablo.<model>.create/update/claim` and
|
|
20
|
+
the REST routes, rendered as tools. An agent connects with your API key and
|
|
21
|
+
gets one safe loop: **claim → read → commit → release.**
|
|
22
|
+
|
|
23
|
+
Install over stdio; set your key in the host's MCP env:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
claude mcp add ablo -- npx -y @ablo/mcp
|
|
27
|
+
# env: ABLO_API_KEY=sk_… (ABLO_API_URL optional; defaults to the hosted API)
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Each tool mirrors an SDK verb, scoped to a model + id:
|
|
31
|
+
|
|
32
|
+
| Tool | Mirrors | Does |
|
|
33
|
+
|---|---|---|
|
|
34
|
+
| `get_model` | `ablo.<model>.get(id)` | read latest state + active claims |
|
|
35
|
+
| `list_models` | `ablo.<model>.list({…})` | cursor-paginated list with filters |
|
|
36
|
+
| `create_model` | `ablo.<model>.create(data)` | guarded create |
|
|
37
|
+
| `update_model` | `ablo.<model>.update(id, …)` | guarded update |
|
|
38
|
+
| `delete_model` | `ablo.<model>.delete(id)` | guarded delete |
|
|
39
|
+
| `claim_model` | `ablo.<model>.claim(id)` | acquire / queue a coordination lease |
|
|
40
|
+
| `release_claim` | — | release the lease so others proceed |
|
|
41
|
+
|
|
42
|
+
The agent-facing contract — the safe loop, the "derive idempotency keys from
|
|
43
|
+
the business event" rule, and the error-code playbook — ships as a loadable
|
|
44
|
+
skill at `@ablo/mcp/skill.md`. Lives in `packages/mcp/`
|
|
45
|
+
(`createCoordinationMcpServer`, `src/tools.ts`).
|
|
46
|
+
|
|
47
|
+
## Integration-helper server
|
|
48
|
+
|
|
49
|
+
If you're integrating `@abloatai/ablo` with the help of an AI coding
|
|
50
|
+
assistant (Claude Code, Cursor, Windsurf, Codex), you don't want it guessing
|
|
51
|
+
at the API. This hosted server lets the assistant search the real docs,
|
|
52
|
+
inspect the actual export surface, lint your schema, and scaffold a starter —
|
|
53
|
+
so the code it writes uses APIs that exist. It serves docs only and returns
|
|
54
|
+
nothing org-specific; data access happens through the SDK or the coordination
|
|
55
|
+
server above, never here.
|
|
56
|
+
|
|
57
|
+
> The `@abloatai/ablo` npm package itself bundles neither server — it has
|
|
58
|
+
> no `@modelcontextprotocol/sdk` dependency. The helper is a feature of Ablo's
|
|
59
|
+
> hosted app, mounted at `/api/mcp`; the coordination server is the separate
|
|
60
|
+
> `@ablo/mcp` package.
|
|
61
|
+
|
|
62
|
+
### Install
|
|
63
|
+
|
|
64
|
+
Point your assistant at the hosted endpoint — no auth, no token:
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
claude mcp add --transport http ablo-sync https://<your-app>/api/mcp
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Per-client walkthroughs:
|
|
10
71
|
|
|
11
72
|
- [Claude Code](/docs/mcp/claude-code)
|
|
12
73
|
- [Cursor](/docs/mcp/cursor)
|
|
13
74
|
- [Windsurf](/docs/mcp/windsurf)
|
|
14
75
|
|
|
15
|
-
|
|
76
|
+
### What it exposes
|
|
16
77
|
|
|
17
|
-
|
|
78
|
+
#### Tools
|
|
18
79
|
|
|
19
|
-
|
|
|
20
|
-
|
|
21
|
-
| `
|
|
22
|
-
| `
|
|
23
|
-
| `
|
|
24
|
-
|
|
|
80
|
+
| Tool | What it does |
|
|
81
|
+
|---|---|
|
|
82
|
+
| `search_ablo_docs` | Keyword search across the docs corpus. Returns ranked matches with excerpts. Follow up with `get_recipe` on the top hit. |
|
|
83
|
+
| `get_recipe` | Returns the full markdown of one doc by name (e.g. `readme`, `quickstart`, `schema-contract`, `integration-guide`, `api`, `guarantees`). |
|
|
84
|
+
| `get_api_surface` | Returns the structured export list for an SDK subpath (`@abloatai/ablo`, `./react`, `./schema`, `./testing`, …). Call with no argument to list every subpath. |
|
|
85
|
+
| `validate_schema` | Lints `defineSchema` source against the DSL rules (camelCase fields, lowercase model keys, `scope`/`grants` sync groups, valid `load` strategies, no legacy builders) and returns a structured issue list. Runs no code. |
|
|
86
|
+
| `scaffold_app` | Emits a starter file tree for a schema-first integration — `next`, `node-agent`, or `plain`, with `managed` or `data-source` storage. |
|
|
87
|
+
|
|
88
|
+
#### Resources
|
|
89
|
+
|
|
90
|
+
Every doc file is addressable at `ablo-sync-engine://docs/{name}`, so a
|
|
91
|
+
client can list the corpus and fetch individual files on demand instead of
|
|
92
|
+
loading everything into context.
|
|
93
|
+
|
|
94
|
+
#### Prompts
|
|
25
95
|
|
|
26
|
-
|
|
27
|
-
rejections when it writes stale state. No invention, no hallucinated IDs.
|
|
96
|
+
Reusable, parameterised templates that drive an end-to-end flow:
|
|
28
97
|
|
|
29
|
-
|
|
98
|
+
- `integrate-sync-engine` — wire the SDK into an existing project.
|
|
99
|
+
- `add-agent` — add an agent worker that coordinates via intents and
|
|
100
|
+
conflict-safe writes.
|
|
101
|
+
- `define-schema` — design a Zod-first schema from a description, then run
|
|
102
|
+
`validate_schema` before committing.
|
|
30
103
|
|
|
31
|
-
|
|
32
|
-
token into the MCP client's auth header configuration. See your client's setup
|
|
33
|
-
guide for the exact mechanism.
|
|
104
|
+
### Transport and limits
|
|
34
105
|
|
|
35
|
-
|
|
106
|
+
The endpoint uses the stateless Streamable HTTP transport (`POST /api/mcp`;
|
|
107
|
+
`GET` returns 405 — SSE is not supported in stateless mode). A fresh
|
|
108
|
+
server is built per request, which suits serverless and horizontally-scaled
|
|
109
|
+
deployments.
|
|
36
110
|
|
|
37
|
-
|
|
38
|
-
|
|
111
|
+
There is **no authentication**: the server only serves docs, schema lint,
|
|
112
|
+
and scaffolds, so there's nothing org-scoped to protect. Abuse is bounded
|
|
113
|
+
by IP-based rate limiting — 120 requests per minute per IP. Rate-limit
|
|
114
|
+
headers are echoed on every response.
|
|
39
115
|
|
|
40
|
-
|
|
116
|
+
### Where it lives
|
|
41
117
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
118
|
+
- Route handler: `apps/sync-web/src/app/api/mcp/route.ts`
|
|
119
|
+
- Server setup: `apps/sync-web/src/lib/mcp-ablo/server.ts`
|
|
120
|
+
(`createSyncEngineMcpServer`)
|
|
121
|
+
- Tools: `apps/sync-web/src/lib/mcp-ablo/tools/`
|
package/docs/quickstart.md
CHANGED
|
@@ -1,29 +1,26 @@
|
|
|
1
1
|
# Quickstart
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Start building with Ablo in two steps: install, then declare one model and write
|
|
4
|
+
through its generated client.
|
|
4
5
|
|
|
5
6
|
If you already have a backend and database, still start here. The SDK call shape
|
|
6
7
|
is the same; [Integration Guide](./integration-guide.md) explains when to use
|
|
7
8
|
Ablo-managed state versus a Data Source that calls your existing API service.
|
|
8
9
|
|
|
9
|
-
## 1. Install
|
|
10
|
+
## 1. Install and set a sandbox key
|
|
10
11
|
|
|
11
12
|
```bash
|
|
12
13
|
npm install @abloatai/ablo
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
## 2. Set a Sandbox Key
|
|
16
|
-
|
|
17
|
-
Use an Ablo sandbox key while integrating.
|
|
18
|
-
|
|
19
|
-
```bash
|
|
20
14
|
export ABLO_API_KEY=sk_test_...
|
|
21
15
|
```
|
|
22
16
|
|
|
23
17
|
`ABLO_API_KEY` is for trusted server runtimes. Browser apps should use the React
|
|
24
18
|
provider with a scoped session route, not a bundled API key.
|
|
25
19
|
|
|
26
|
-
##
|
|
20
|
+
## 2. Declare schema and write state
|
|
21
|
+
|
|
22
|
+
Your schema is the contract. It generates `ablo.<model>` methods for app code,
|
|
23
|
+
server actions, agents, React reads, and Data Source requests.
|
|
27
24
|
|
|
28
25
|
```ts
|
|
29
26
|
import Ablo from '@abloatai/ablo';
|
|
@@ -41,15 +38,6 @@ export const ablo = Ablo({
|
|
|
41
38
|
schema,
|
|
42
39
|
apiKey: process.env.ABLO_API_KEY,
|
|
43
40
|
});
|
|
44
|
-
```
|
|
45
|
-
|
|
46
|
-
Customer apps should always pass `schema`. Treat it like Prisma's schema file:
|
|
47
|
-
it is the source of truth for typed model clients, realtime subscriptions,
|
|
48
|
-
agent writes, and Data Source requests.
|
|
49
|
-
|
|
50
|
-
## 4. Create and Update
|
|
51
|
-
|
|
52
|
-
```ts
|
|
53
41
|
await ablo.ready();
|
|
54
42
|
|
|
55
43
|
const created = await ablo.weatherReports.create({
|
|
@@ -71,23 +59,26 @@ Expected output:
|
|
|
71
59
|
{ id: '...', status: 'ready' }
|
|
72
60
|
```
|
|
73
61
|
|
|
74
|
-
##
|
|
62
|
+
## Run the example
|
|
75
63
|
|
|
76
64
|
```bash
|
|
77
|
-
cd
|
|
78
|
-
ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
|
|
65
|
+
cd packages/sync-engine
|
|
66
|
+
ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
|
|
79
67
|
```
|
|
80
68
|
|
|
81
|
-
##
|
|
69
|
+
## Add coordination for slow work
|
|
82
70
|
|
|
83
71
|
When AI or background work will touch an existing row for more than a quick
|
|
84
|
-
write, coordinate through
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
72
|
+
write, coordinate through `claim(id, work)`. It claims the row and hands the row
|
|
73
|
+
back; `claim.state(id)` reads who is currently working on it without blocking;
|
|
74
|
+
and you write the usual way with `ablo.<model>.update(id, ...)`.
|
|
75
|
+
|
|
76
|
+
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
77
|
+
re-reads the fresh row, then hands it to you — so two writers serialize instead
|
|
78
|
+
of clobbering. Normal reads still work while the claim is held. If a server read
|
|
79
|
+
should not return a row while someone else is mid-edit, pass `ifClaimed: 'wait'`
|
|
80
|
+
to wait for the claim to clear, or `ifClaimed: 'fail'` to error out instead. The
|
|
81
|
+
callback form releases the claim when the callback returns or throws.
|
|
91
82
|
|
|
92
83
|
```ts
|
|
93
84
|
// Claim the row so other participants serialize behind us while we work.
|
|
@@ -107,24 +98,24 @@ await ablo.weatherReports.claim(
|
|
|
107
98
|
);
|
|
108
99
|
```
|
|
109
100
|
|
|
110
|
-
Ablo does not fetch the weather.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
Ablo does not fetch the weather. If another participant already holds the row,
|
|
102
|
+
`claim` waits for them to finish, re-reads, and then hands you the fresh row.
|
|
103
|
+
While you hold the claim, `update(id, ...)` rejects with `AbloStaleContextError`
|
|
104
|
+
if someone else changed the row first — so you never overwrite work you didn't
|
|
105
|
+
see. The claim releases automatically once the callback returns or throws.
|
|
115
106
|
|
|
116
|
-
##
|
|
107
|
+
## Multiplayer and claimed work
|
|
117
108
|
|
|
118
109
|
There is no separate multiplayer mode. Use the same schema client for human UI,
|
|
119
110
|
server actions, and agents; Ablo fans out confirmed writes and keeps active
|
|
120
111
|
claims visible on the same model row.
|
|
121
112
|
|
|
122
|
-
`
|
|
113
|
+
`claim.state(id)` tells you when another human or agent is active on the same row.
|
|
123
114
|
For schema clients, `claim(id, work)` waits fairly, re-reads, and then lets you
|
|
124
115
|
write through the model.
|
|
125
116
|
|
|
126
117
|
```ts
|
|
127
|
-
const active = ablo.weatherReports.
|
|
118
|
+
const active = ablo.weatherReports.claim.state('weather_stockholm');
|
|
128
119
|
if (active) {
|
|
129
120
|
console.log(`${active.heldBy} is ${active.action}`);
|
|
130
121
|
}
|
|
@@ -137,11 +128,12 @@ await ablo.weatherReports.claim('weather_stockholm', async (report) => {
|
|
|
137
128
|
Use `{ wait: false }` on `claim` when work should be skipped instead of queued
|
|
138
129
|
behind an active holder.
|
|
139
130
|
|
|
140
|
-
##
|
|
131
|
+
## Next steps
|
|
141
132
|
|
|
142
133
|
Keep using the schema client for app and agent writes.
|
|
143
134
|
|
|
144
135
|
- [Integration Guide](./integration-guide.md) explains the full app, React, Data Source, multiplayer, and agent path.
|
|
136
|
+
- [Schema Contract](./schema-contract.md) explains what the schema drives across SDK, React, agents, Data Source, and schema push.
|
|
145
137
|
- [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
|
|
146
138
|
- [Client Behavior](./client-behavior.md) covers errors, retries, and public imports.
|
|
147
139
|
- [Connect Your Database](./data-sources.md) covers the optional route for teams keeping rows in their own database.
|
package/docs/react.md
CHANGED
|
@@ -23,7 +23,7 @@ pool, and the engine lifecycle; everything below it reads with `useAblo`.
|
|
|
23
23
|
'use client';
|
|
24
24
|
|
|
25
25
|
import { AbloProvider } from '@abloatai/ablo/react';
|
|
26
|
-
import { schema } from '@/ablo
|
|
26
|
+
import { schema } from '@/ablo/schema';
|
|
27
27
|
|
|
28
28
|
export function Providers({
|
|
29
29
|
children,
|
|
@@ -56,8 +56,8 @@ export function Providers({
|
|
|
56
56
|
| `url` | hosted endpoint | WebSocket URL of the sync server (`wss://…`). Hosted apps omit it. |
|
|
57
57
|
| `apiKey` | session/cookie | Bootstrap auth. Browser apps **omit this** — the key stays server-side. See Identity below. |
|
|
58
58
|
| `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
|
|
59
|
-
| `bootstrapMode` | `'full'` | `'full'`
|
|
60
|
-
| `persistence` | `'volatile'` | `'indexeddb'` opts into
|
|
59
|
+
| `bootstrapMode` | `'full'` | `'full'` loads existing rows before the app renders, so reads are populated on first paint; `'none'` skips that initial load and only streams changes as they happen.|
|
|
60
|
+
| `persistence` | `'volatile'` | `'indexeddb'` opts into a durable browser cache that survives reloads. |
|
|
61
61
|
| `onSessionExpired` | — | Fired after the engine has already purged on a rejected session — use for redirect-to-sign-in. |
|
|
62
62
|
| `onError` | — | Engine / WebSocket / `postBootstrap` errors. Wire to Sentry / Datadog. |
|
|
63
63
|
|
|
@@ -74,8 +74,8 @@ reaches the browser, is the whole of
|
|
|
74
74
|
import { useAblo } from '@abloatai/ablo/react';
|
|
75
75
|
|
|
76
76
|
export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
|
|
77
|
-
const report = useAblo((ablo) => ablo.weatherReports.
|
|
78
|
-
const active = useAblo((ablo) => ablo.weatherReports.
|
|
77
|
+
const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
|
|
78
|
+
const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
|
|
79
79
|
const claimed = Boolean(active);
|
|
80
80
|
|
|
81
81
|
return <article>{report.location}</article>;
|
|
@@ -84,12 +84,13 @@ export function ReportView({ report: serverReport }: { report: { id: string; loc
|
|
|
84
84
|
|
|
85
85
|
The hook:
|
|
86
86
|
|
|
87
|
-
1.
|
|
87
|
+
1. Uses the same `ablo.<model>.get(id)` / `.getAll()` methods you'd call anywhere
|
|
88
|
+
else in the SDK — the hook just makes them reactive.
|
|
88
89
|
2. Tracks the model fields read by the selector and re-renders when confirmed
|
|
89
90
|
deltas arrive.
|
|
90
91
|
3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
|
|
91
92
|
parent already loaded the row.
|
|
92
|
-
4. Works for coordination state too, such as `ablo.weatherReports.
|
|
93
|
+
4. Works for coordination state too, such as `ablo.weatherReports.claim.state(id)`.
|
|
93
94
|
|
|
94
95
|
Use the zero-argument form only when you need the full client for callbacks,
|
|
95
96
|
effects, or writes:
|
|
@@ -98,18 +99,17 @@ effects, or writes:
|
|
|
98
99
|
const abloClient = useAblo();
|
|
99
100
|
```
|
|
100
101
|
|
|
101
|
-
Prefer selector reads like `useAblo((ablo) => ablo.<model>.
|
|
102
|
-
|
|
103
|
-
should use the same model-client shape as the rest of the SDK.
|
|
102
|
+
Prefer selector reads like `useAblo((ablo) => ablo.<model>.get(id))`. Older hooks
|
|
103
|
+
also accept a string model name; prefer the selector form shown above.
|
|
104
104
|
|
|
105
105
|
For collections, keep the selector on the model client too:
|
|
106
106
|
|
|
107
107
|
```tsx
|
|
108
108
|
const reports = useAblo((ablo) =>
|
|
109
|
-
ablo.weatherReports.
|
|
109
|
+
ablo.weatherReports.getAll({
|
|
110
110
|
where: { projectId },
|
|
111
111
|
filter: (report) => report.status !== 'ready',
|
|
112
|
-
|
|
112
|
+
state: 'live',
|
|
113
113
|
}),
|
|
114
114
|
);
|
|
115
115
|
```
|
|
@@ -117,10 +117,14 @@ const reports = useAblo((ablo) =>
|
|
|
117
117
|
## Server Load
|
|
118
118
|
|
|
119
119
|
```tsx
|
|
120
|
-
const
|
|
120
|
+
const report = await ablo.weatherReports.retrieve(id);
|
|
121
121
|
```
|
|
122
122
|
|
|
123
|
-
Use `
|
|
123
|
+
Use `retrieve` in Server Components when the row may not be in the local pool
|
|
124
|
+
yet — it hydrates from the local store and the server, and returns a Promise, so
|
|
125
|
+
`await` it. (Server reads come in two shapes: `retrieve(id)` for one row and
|
|
126
|
+
`list({ where })` for many; both are async. The synchronous local reads are
|
|
127
|
+
`get`/`getAll`/`getCount`, used in render below.)
|
|
124
128
|
|
|
125
129
|
## Writes
|
|
126
130
|
|
package/docs/roadmap.md
CHANGED
|
@@ -5,19 +5,31 @@ What is shipped, what is next, and what we will not build.
|
|
|
5
5
|
## Shipped
|
|
6
6
|
|
|
7
7
|
- **Models, claims, commits** — the core API.
|
|
8
|
-
- **Audit log** — hash-chained
|
|
8
|
+
- **Audit log** — a tamper-evident, hash-chained record of who did what,
|
|
9
|
+
per principal.
|
|
9
10
|
- **MCP transport** — HTTP server at `/api/mcp`.
|
|
10
11
|
- **TypeScript SDK** — `@abloatai/ablo`, with React bindings.
|
|
11
12
|
- **Dashboard** — keys, audit, metrics, allowed origins.
|
|
13
|
+
- **Schema migrations** — change a model's shape after it already has data.
|
|
14
|
+
Ablo plans the migration, tells you which changes are safe to auto-apply,
|
|
15
|
+
and backfills the rest; the server applies and activates a migration only
|
|
16
|
+
if it succeeds. `ablo generate` emits typed clients from a pushed schema.
|
|
17
|
+
- **Structured errors** — every error has a stable code and a request id,
|
|
18
|
+
and the same codes work whether you reach Ablo over HTTP, WebSocket, or MCP.
|
|
19
|
+
- **Sync groups** — clients automatically receive only the records they have
|
|
20
|
+
access to, based on relationships you declare in the schema.
|
|
21
|
+
- **Agent coordination** — when several agents work on the same model, they
|
|
22
|
+
take turns instead of overwriting each other, via `intent(id)` handles
|
|
23
|
+
that claim, wait, and commit.
|
|
12
24
|
|
|
13
25
|
## In flight
|
|
14
26
|
|
|
15
|
-
- **Real-time presence** — see who else is viewing/editing a model
|
|
27
|
+
- **Real-time presence** — see who else is viewing/editing a model
|
|
28
|
+
(coordination primitives landed; presence surface in progress).
|
|
16
29
|
- **Cross-instance fan-out via Redis** — pub/sub deltas at scale.
|
|
17
30
|
|
|
18
31
|
## On deck
|
|
19
32
|
|
|
20
|
-
- **Schema migrations** — declarative model schema changes.
|
|
21
33
|
- **Field-level subscriptions** — subscribe to one path, not the whole row.
|
|
22
34
|
- **Bulk import/export** — CSV/JSON round-trip with chain verification.
|
|
23
35
|
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Schema Contract
|
|
2
|
+
|
|
3
|
+
Ablo's schema is the integration contract. Define it once, pass it to `Ablo(...)`,
|
|
4
|
+
and every actor gets the same typed model surface:
|
|
5
|
+
|
|
6
|
+
```txt
|
|
7
|
+
defineSchema(...) -> ablo.<model>.create/retrieve/update/claim(...)
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
That one object drives:
|
|
11
|
+
|
|
12
|
+
- typed model clients in trusted server runtimes,
|
|
13
|
+
- React selectors through `useAblo((ablo) => ablo.<model>.get(id))`,
|
|
14
|
+
- agent and background-worker writes,
|
|
15
|
+
- Data Source request/response shape when your database stays canonical,
|
|
16
|
+
- hosted schema push, migration planning, and schema-version gating.
|
|
17
|
+
|
|
18
|
+
## Minimal shape
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
import Ablo from '@abloatai/ablo';
|
|
22
|
+
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
23
|
+
|
|
24
|
+
export const schema = defineSchema({
|
|
25
|
+
weatherReports: model({
|
|
26
|
+
location: z.string(),
|
|
27
|
+
status: z.enum(['pending', 'ready']),
|
|
28
|
+
forecast: z.string().optional(),
|
|
29
|
+
}),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
export const ablo = Ablo({
|
|
33
|
+
schema,
|
|
34
|
+
apiKey: process.env.ABLO_API_KEY,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
await ablo.ready();
|
|
38
|
+
|
|
39
|
+
const report = await ablo.weatherReports.create({
|
|
40
|
+
location: 'Stockholm',
|
|
41
|
+
status: 'pending',
|
|
42
|
+
});
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
The model key (`weatherReports`) becomes the client namespace
|
|
46
|
+
(`ablo.weatherReports`). The Zod fields become the create/update/read type
|
|
47
|
+
contract. You should not create a parallel string-keyed write path for the same
|
|
48
|
+
data.
|
|
49
|
+
|
|
50
|
+
## Reads and writes
|
|
51
|
+
|
|
52
|
+
Use async reads when the row may not be local:
|
|
53
|
+
|
|
54
|
+
```ts
|
|
55
|
+
const report = await ablo.weatherReports.retrieve(reportId);
|
|
56
|
+
const ready = await ablo.weatherReports.list({ where: { status: 'ready' } });
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
Use synchronous local reads in render after data has synced:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
const report = ablo.weatherReports.get(reportId);
|
|
63
|
+
const pending = ablo.weatherReports.getAll({ where: { status: 'pending' } });
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Use model writes for every actor:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
await ablo.weatherReports.update(reportId, { status: 'ready' }, { wait: 'confirmed' });
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## Coordination
|
|
73
|
+
|
|
74
|
+
Agents and background jobs often read, call a tool or model, then write later.
|
|
75
|
+
Wrap that slow span in `claim`:
|
|
76
|
+
|
|
77
|
+
```ts
|
|
78
|
+
await ablo.weatherReports.claim(reportId, async (report) => {
|
|
79
|
+
const forecast = await getForecast(report.location);
|
|
80
|
+
await ablo.weatherReports.update(report.id, { status: 'ready', forecast });
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
If another writer already holds the row, `claim` waits, re-reads, and gives your
|
|
85
|
+
callback the fresh row. Reads stay open; only acting on the row serializes.
|
|
86
|
+
|
|
87
|
+
## Storage boundary
|
|
88
|
+
|
|
89
|
+
Every schema model needs a backing store:
|
|
90
|
+
|
|
91
|
+
- Use Ablo-managed state when the row can live in Ablo.
|
|
92
|
+
- Use a Data Source when your app database remains canonical.
|
|
93
|
+
|
|
94
|
+
Do not pass a database URL to `Ablo(...)`. Trusted runtimes use `ABLO_API_KEY`.
|
|
95
|
+
Browser code goes through `<AbloProvider>` or a scoped session route, never a raw
|
|
96
|
+
API key.
|
|
97
|
+
|
|
98
|
+
## Rules of thumb
|
|
99
|
+
|
|
100
|
+
- Start with fields and relations before load/index tuning.
|
|
101
|
+
- Import one schema into app code, server actions, agents, and Data Source routes.
|
|
102
|
+
- Keep direct database writes out of the coordinated path unless they are reported
|
|
103
|
+
back through Data Source events.
|
|
104
|
+
- Use `claim` for slow read -> think -> write spans.
|
|
105
|
+
- Use `readAt` + `onStale: 'reject'` when a write must fail if the row changed
|
|
106
|
+
after it was read.
|
|
107
|
+
|
|
108
|
+
For the shortest runnable path, start with [Quickstart](./quickstart.md). For a
|
|
109
|
+
production app, continue with [Integration Guide](./integration-guide.md).
|
package/examples/README.md
CHANGED
|
@@ -31,9 +31,13 @@ intentionally cannot import the app schema.
|
|
|
31
31
|
|
|
32
32
|
## Running
|
|
33
33
|
|
|
34
|
+
Run from the package root, not `examples/` — the `examples/` folder has
|
|
35
|
+
no `package.json`, so Node resolves the entry path against the package
|
|
36
|
+
root and a bare `quickstart.ts` won't be found.
|
|
37
|
+
|
|
34
38
|
```bash
|
|
35
|
-
cd packages/sync-engine
|
|
36
|
-
ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
|
|
39
|
+
cd packages/sync-engine
|
|
40
|
+
ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
|
|
37
41
|
```
|
|
38
42
|
|
|
39
43
|
## Data Source (customer-owned database)
|
|
@@ -45,8 +49,8 @@ orchestrator drives the customer handler in-process so signer and
|
|
|
45
49
|
verifier exchange real signed bytes without leaving the process.
|
|
46
50
|
|
|
47
51
|
```bash
|
|
48
|
-
cd packages/sync-engine
|
|
49
|
-
npx tsx data-source/run.ts
|
|
52
|
+
cd packages/sync-engine
|
|
53
|
+
npx tsx examples/data-source/run.ts
|
|
50
54
|
```
|
|
51
55
|
|
|
52
56
|
See `data-source/README.md` for what each file teaches and the
|
|
@@ -16,10 +16,14 @@ when they want Ablo to coordinate writes against rows stored in
|
|
|
16
16
|
## Run
|
|
17
17
|
|
|
18
18
|
```bash
|
|
19
|
-
cd packages/sync-engine
|
|
20
|
-
npx tsx data-source/run.ts
|
|
19
|
+
cd packages/sync-engine
|
|
20
|
+
npx tsx examples/data-source/run.ts
|
|
21
21
|
```
|
|
22
22
|
|
|
23
|
+
Run from the package root, not `examples/`: the `examples/` folder has
|
|
24
|
+
no `package.json`, so Node resolves the entry path against the package
|
|
25
|
+
root and a bare `data-source/run.ts` won't be found.
|
|
26
|
+
|
|
23
27
|
No network port, no env vars, no cloud credentials. The orchestrator
|
|
24
28
|
calls the handler in-process. Signer and verifier still exchange
|
|
25
29
|
signed bytes — flip the API key and you'll see a 401.
|
|
@@ -1,10 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* End-to-end Data Source demo.
|
|
3
3
|
*
|
|
4
|
-
* Run
|
|
4
|
+
* Run (from the package root — the examples/ folder has no package.json
|
|
5
|
+
* of its own, so Node resolves module paths against the package root):
|
|
5
6
|
*
|
|
6
|
-
* cd packages/sync-engine
|
|
7
|
-
* npx tsx data-source/run.ts
|
|
7
|
+
* cd packages/sync-engine
|
|
8
|
+
* npx tsx examples/data-source/run.ts
|
|
8
9
|
*
|
|
9
10
|
* What this proves:
|
|
10
11
|
*
|