@abloatai/ablo 0.8.0 → 0.9.1
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 +46 -1
- package/README.md +33 -28
- package/dist/BaseSyncedStore.d.ts +83 -0
- package/dist/BaseSyncedStore.js +194 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +158 -50
- package/dist/mutators/UndoManager.js +345 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/context.d.ts +31 -0
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +34 -3
- package/dist/schema/ddl.js +162 -4
- package/dist/schema/index.d.ts +5 -1
- package/dist/schema/index.js +13 -1
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +4 -0
- package/dist/schema/serialize.js +4 -0
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +65 -0
- package/dist/source/adapter.js +20 -0
- package/dist/source/adapters/drizzle.d.ts +43 -0
- package/dist/source/adapters/drizzle.js +185 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +176 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +144 -0
- package/dist/source/contract.js +99 -0
- package/dist/source/index.d.ts +62 -10
- package/dist/source/index.js +99 -0
- package/dist/source/migrations.d.ts +14 -0
- package/dist/source/migrations.js +39 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +2 -2
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- package/package.json +43 -3
package/docs/mcp/claude-code.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
## Install
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
claude mcp add --transport http ablo
|
|
6
|
+
claude mcp add --transport http ablo https://<your-app>/api/mcp
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
That's it — no token or header needed. The endpoint is public and serves
|
|
@@ -18,14 +18,14 @@ In Claude Code, run:
|
|
|
18
18
|
/mcp list
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
-
You should see `ablo
|
|
21
|
+
You should see `ablo` with the integration tools enumerated:
|
|
22
22
|
`search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
|
|
23
23
|
`scaffold_app`.
|
|
24
24
|
|
|
25
25
|
## Removing
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
claude mcp remove ablo
|
|
28
|
+
claude mcp remove ablo
|
|
29
29
|
```
|
|
30
30
|
|
|
31
31
|
## More
|
package/docs/mcp/cursor.md
CHANGED
package/docs/mcp/windsurf.md
CHANGED
|
@@ -7,7 +7,7 @@ Add the Ablo Sync MCP server to Windsurf's MCP config:
|
|
|
7
7
|
```json
|
|
8
8
|
{
|
|
9
9
|
"mcpServers": {
|
|
10
|
-
"ablo
|
|
10
|
+
"ablo": {
|
|
11
11
|
"transport": "http",
|
|
12
12
|
"url": "https://<your-app>/api/mcp"
|
|
13
13
|
}
|
|
@@ -22,7 +22,7 @@ the endpoint is public and serves only docs, schema lint, and scaffolds.
|
|
|
22
22
|
## Verify
|
|
23
23
|
|
|
24
24
|
Cascade's MCP panel lists every configured server with its tools. You
|
|
25
|
-
should see `ablo
|
|
25
|
+
should see `ablo` with the integration tools enumerated:
|
|
26
26
|
`search_ablo_docs`, `get_recipe`, `get_api_surface`, `validate_schema`,
|
|
27
27
|
`scaffold_app`.
|
|
28
28
|
|
package/docs/mcp.md
CHANGED
|
@@ -33,10 +33,10 @@ Each tool mirrors an SDK verb, scoped to a model + id:
|
|
|
33
33
|
|---|---|---|
|
|
34
34
|
| `get_model` | `ablo.<model>.get(id)` | read latest state + active claims |
|
|
35
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 |
|
|
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
40
|
| `release_claim` | — | release the lease so others proceed |
|
|
41
41
|
|
|
42
42
|
The agent-facing contract — the safe loop, the "derive idempotency keys from
|
|
@@ -64,7 +64,7 @@ server above, never here.
|
|
|
64
64
|
Point your assistant at the hosted endpoint — no auth, no token:
|
|
65
65
|
|
|
66
66
|
```bash
|
|
67
|
-
claude mcp add --transport http ablo
|
|
67
|
+
claude mcp add --transport http ablo https://<your-app>/api/mcp
|
|
68
68
|
```
|
|
69
69
|
|
|
70
70
|
Per-client walkthroughs:
|
|
@@ -87,7 +87,7 @@ Per-client walkthroughs:
|
|
|
87
87
|
|
|
88
88
|
#### Resources
|
|
89
89
|
|
|
90
|
-
Every doc file is addressable at `ablo
|
|
90
|
+
Every doc file is addressable at `ablo://docs/{name}`, so a
|
|
91
91
|
client can list the corpus and fetch individual files on demand instead of
|
|
92
92
|
loading everything into context.
|
|
93
93
|
|
package/docs/quickstart.md
CHANGED
|
@@ -41,13 +41,18 @@ export const ablo = Ablo({
|
|
|
41
41
|
await ablo.ready();
|
|
42
42
|
|
|
43
43
|
const created = await ablo.weatherReports.create({
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
data: {
|
|
45
|
+
location: 'Stockholm',
|
|
46
|
+
status: 'pending',
|
|
47
|
+
},
|
|
46
48
|
});
|
|
47
49
|
|
|
48
|
-
const updated = await ablo.weatherReports.update(
|
|
49
|
-
|
|
50
|
-
|
|
50
|
+
const updated = await ablo.weatherReports.update({
|
|
51
|
+
id: created.id,
|
|
52
|
+
data: {
|
|
53
|
+
status: 'ready',
|
|
54
|
+
forecast: 'Light rain, 13C',
|
|
55
|
+
},
|
|
51
56
|
});
|
|
52
57
|
|
|
53
58
|
console.log({ id: updated.id, status: updated.status });
|
|
@@ -69,40 +74,45 @@ ABLO_API_KEY=sk_test_... npx tsx examples/quickstart.ts
|
|
|
69
74
|
## Add coordination for slow work
|
|
70
75
|
|
|
71
76
|
When AI or background work will touch an existing row for more than a quick
|
|
72
|
-
write, coordinate through `claim(id
|
|
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,
|
|
77
|
+
write, coordinate through `claim({ id })`. It claims the row and hands a handle
|
|
78
|
+
back; `claim.state({ id })` reads who is currently working on it without blocking;
|
|
79
|
+
and you write the usual way with `ablo.<model>.update({ id, data })`.
|
|
75
80
|
|
|
76
81
|
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
77
82
|
re-reads the fresh row, then hands it to you — so two writers serialize instead
|
|
78
83
|
of clobbering. Normal reads still work while the claim is held. If a server read
|
|
79
84
|
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.
|
|
81
|
-
|
|
85
|
+
to wait for the claim to clear, or `ifClaimed: 'fail'` to error out instead.
|
|
86
|
+
Call `handle.release()` when your work is done.
|
|
82
87
|
|
|
83
88
|
```ts
|
|
84
89
|
// Claim the row so other participants serialize behind us while we work.
|
|
85
|
-
await ablo.weatherReports.claim(
|
|
86
|
-
'weather_stockholm',
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
90
|
+
const handle = await ablo.weatherReports.claim({
|
|
91
|
+
id: 'weather_stockholm',
|
|
92
|
+
action: 'checking_weather',
|
|
93
|
+
ttl: '2m',
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// Your existing weather tool or agent call. While this runs, other clients
|
|
97
|
+
// see that weather_stockholm is being checked.
|
|
98
|
+
const weather = await weatherAgent.getWeather(handle.data.location);
|
|
99
|
+
|
|
100
|
+
await ablo.weatherReports.update({
|
|
101
|
+
id: handle.data.id,
|
|
102
|
+
data: {
|
|
103
|
+
status: 'ready',
|
|
104
|
+
forecast: weather.summary,
|
|
96
105
|
},
|
|
97
|
-
|
|
98
|
-
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
await handle.release();
|
|
99
109
|
```
|
|
100
110
|
|
|
101
111
|
Ablo does not fetch the weather. If another participant already holds the row,
|
|
102
112
|
`claim` waits for them to finish, re-reads, and then hands you the fresh row.
|
|
103
|
-
While you hold the claim, `update(id,
|
|
113
|
+
While you hold the claim, `update({ id, data })` rejects with `AbloStaleContextError`
|
|
104
114
|
if someone else changed the row first — so you never overwrite work you didn't
|
|
105
|
-
see.
|
|
115
|
+
see. Call `handle.release()` once your work is done.
|
|
106
116
|
|
|
107
117
|
## Multiplayer and claimed work
|
|
108
118
|
|
|
@@ -110,19 +120,19 @@ There is no separate multiplayer mode. Use the same schema client for human UI,
|
|
|
110
120
|
server actions, and agents; Ablo fans out confirmed writes and keeps active
|
|
111
121
|
claims visible on the same model row.
|
|
112
122
|
|
|
113
|
-
`claim.state(id)` tells you when another human or agent is active on the same row.
|
|
114
|
-
For schema clients, `claim(id
|
|
123
|
+
`claim.state({ id })` tells you when another human or agent is active on the same row.
|
|
124
|
+
For schema clients, `claim({ id })` waits fairly, re-reads, and then lets you
|
|
115
125
|
write through the model.
|
|
116
126
|
|
|
117
127
|
```ts
|
|
118
|
-
const active = ablo.weatherReports.claim.state('weather_stockholm');
|
|
128
|
+
const active = ablo.weatherReports.claim.state({ id: 'weather_stockholm' });
|
|
119
129
|
if (active) {
|
|
120
130
|
console.log(`${active.heldBy} is ${active.action}`);
|
|
121
131
|
}
|
|
122
132
|
|
|
123
|
-
await ablo.weatherReports.claim('weather_stockholm'
|
|
124
|
-
|
|
125
|
-
|
|
133
|
+
const handle = await ablo.weatherReports.claim({ id: 'weather_stockholm' });
|
|
134
|
+
await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
|
|
135
|
+
await handle.release();
|
|
126
136
|
```
|
|
127
137
|
|
|
128
138
|
Use `{ wait: false }` on `claim` when work should be skipped instead of queued
|
package/docs/react.md
CHANGED
|
@@ -75,7 +75,7 @@ import { useAblo } from '@abloatai/ablo/react';
|
|
|
75
75
|
|
|
76
76
|
export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
|
|
77
77
|
const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
|
|
78
|
-
const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
|
|
78
|
+
const active = useAblo((ablo) => ablo.weatherReports.claim.state({ id: serverReport.id }));
|
|
79
79
|
const claimed = Boolean(active);
|
|
80
80
|
|
|
81
81
|
return <article>{report.location}</article>;
|
|
@@ -90,7 +90,7 @@ The hook:
|
|
|
90
90
|
deltas arrive.
|
|
91
91
|
3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
|
|
92
92
|
parent already loaded the row.
|
|
93
|
-
4. Works for coordination state too, such as `ablo.weatherReports.claim.state(id)`.
|
|
93
|
+
4. Works for coordination state too, such as `ablo.weatherReports.claim.state({ id })`.
|
|
94
94
|
|
|
95
95
|
Use the zero-argument form only when you need the full client for callbacks,
|
|
96
96
|
effects, or writes:
|
|
@@ -117,12 +117,12 @@ const reports = useAblo((ablo) =>
|
|
|
117
117
|
## Server Load
|
|
118
118
|
|
|
119
119
|
```tsx
|
|
120
|
-
const report = await ablo.weatherReports.retrieve(id);
|
|
120
|
+
const report = await ablo.weatherReports.retrieve({ id });
|
|
121
121
|
```
|
|
122
122
|
|
|
123
123
|
Use `retrieve` in Server Components when the row may not be in the local pool
|
|
124
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
|
|
125
|
+
`await` it. (Server reads come in two shapes: `retrieve({ id })` for one row and
|
|
126
126
|
`list({ where })` for many; both are async. The synchronous local reads are
|
|
127
127
|
`get`/`getAll`/`getCount`, used in render below.)
|
|
128
128
|
|
|
@@ -134,7 +134,9 @@ For Server Actions and route handlers, call the SDK directly:
|
|
|
134
134
|
import { ablo } from '@/lib/ablo';
|
|
135
135
|
|
|
136
136
|
const snap = ablo.snapshot({ weatherReports: id });
|
|
137
|
-
await ablo.weatherReports.update(
|
|
137
|
+
await ablo.weatherReports.update({
|
|
138
|
+
id,
|
|
139
|
+
data: patch,
|
|
138
140
|
readAt: snap.stamp,
|
|
139
141
|
onStale: 'reject',
|
|
140
142
|
wait: 'confirmed',
|
|
@@ -150,11 +152,13 @@ const ablo = useAblo();
|
|
|
150
152
|
async function markReady() {
|
|
151
153
|
if (!ablo) return;
|
|
152
154
|
const snap = ablo.snapshot({ weatherReports: id });
|
|
153
|
-
await ablo.weatherReports.update(
|
|
155
|
+
await ablo.weatherReports.update({
|
|
154
156
|
id,
|
|
155
|
-
{ status: 'ready' },
|
|
156
|
-
|
|
157
|
-
|
|
157
|
+
data: { status: 'ready' },
|
|
158
|
+
readAt: snap.stamp,
|
|
159
|
+
onStale: 'reject',
|
|
160
|
+
wait: 'confirmed',
|
|
161
|
+
});
|
|
158
162
|
}
|
|
159
163
|
```
|
|
160
164
|
|
package/docs/schema-contract.md
CHANGED
|
@@ -37,8 +37,10 @@ export const ablo = Ablo({
|
|
|
37
37
|
await ablo.ready();
|
|
38
38
|
|
|
39
39
|
const report = await ablo.weatherReports.create({
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
data: {
|
|
41
|
+
location: 'Stockholm',
|
|
42
|
+
status: 'pending',
|
|
43
|
+
},
|
|
42
44
|
});
|
|
43
45
|
```
|
|
44
46
|
|
|
@@ -52,7 +54,7 @@ data.
|
|
|
52
54
|
Use async reads when the row may not be local:
|
|
53
55
|
|
|
54
56
|
```ts
|
|
55
|
-
const report = await ablo.weatherReports.retrieve(reportId);
|
|
57
|
+
const report = await ablo.weatherReports.retrieve({ id: reportId });
|
|
56
58
|
const ready = await ablo.weatherReports.list({ where: { status: 'ready' } });
|
|
57
59
|
```
|
|
58
60
|
|
|
@@ -66,7 +68,7 @@ const pending = ablo.weatherReports.getAll({ where: { status: 'pending' } });
|
|
|
66
68
|
Use model writes for every actor:
|
|
67
69
|
|
|
68
70
|
```ts
|
|
69
|
-
await ablo.weatherReports.update(reportId, { status: 'ready' },
|
|
71
|
+
await ablo.weatherReports.update({ id: reportId, data: { status: 'ready' }, wait: 'confirmed' });
|
|
70
72
|
```
|
|
71
73
|
|
|
72
74
|
## Coordination
|
|
@@ -75,14 +77,14 @@ Agents and background jobs often read, call a tool or model, then write later.
|
|
|
75
77
|
Wrap that slow span in `claim`:
|
|
76
78
|
|
|
77
79
|
```ts
|
|
78
|
-
await ablo.weatherReports.claim(reportId
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
80
|
+
const handle = await ablo.weatherReports.claim({ id: reportId });
|
|
81
|
+
const forecast = await getForecast(handle.data.location);
|
|
82
|
+
await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready', forecast } });
|
|
83
|
+
await handle.release();
|
|
82
84
|
```
|
|
83
85
|
|
|
84
|
-
If another writer already holds the row, `claim` waits, re-reads, and
|
|
85
|
-
|
|
86
|
+
If another writer already holds the row, `claim` waits, re-reads, and hands you
|
|
87
|
+
the fresh row. Reads stay open; only acting on the row serializes.
|
|
86
88
|
|
|
87
89
|
## Storage boundary
|
|
88
90
|
|
package/docs/the-loop.md
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# The loop: how your data flows
|
|
2
|
+
|
|
3
|
+
This explainer moved to the canonical, maintained docs:
|
|
4
|
+
|
|
5
|
+
**→ https://abloatai.com/docs/webhooks**
|
|
6
|
+
|
|
7
|
+
The short version: Ablo has the same two-sided shape as Stripe — **you call Ablo to make changes (the client), and Ablo calls you to persist them (a signed webhook)** — plus realtime sync to every connected client.
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
your app ──write──▶ Ablo (hosted) ──realtime sync──▶ other clients
|
|
11
|
+
(the client) the transaction log (live, optimistic)
|
|
12
|
+
│
|
|
13
|
+
└──signed event──▶ /api/ablo/[...all] ──▶ YOUR database
|
|
14
|
+
(the webhook route)
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Ablo owns the ordered transaction log (the source of truth); your database is a
|
|
18
|
+
materialized copy you keep via the webhook. See the link above for the full
|
|
19
|
+
guide: scaffolding the handler (`ablo init`), local testing (`ablo dev`),
|
|
20
|
+
registering an endpoint (`ablo webhooks create`), signature verification, the
|
|
21
|
+
delivery/retry model, and best practices.
|
|
@@ -38,9 +38,9 @@ signed bytes — flip the API key and you'll see a 401.
|
|
|
38
38
|
3. **The customer DB stays canonical.** Ablo never sees rows
|
|
39
39
|
directly; it only sees the response payload from the customer's
|
|
40
40
|
handler.
|
|
41
|
-
4. **The outbox feed.**
|
|
42
|
-
|
|
43
|
-
|
|
41
|
+
4. **The outbox feed.** Every committed app-row change gets an outbox marker.
|
|
42
|
+
Ablo filters markers for commits it already appended and uses the same feed
|
|
43
|
+
to repair a failed post-commit append.
|
|
44
44
|
|
|
45
45
|
## Production wiring
|
|
46
46
|
|
|
@@ -96,8 +96,7 @@ data layer. The handler shape stays the same:
|
|
|
96
96
|
- `tasks.load({ id })` -> `db.task.findUnique({ where: { id } })`
|
|
97
97
|
- `tasks.list({ query })` -> `db.task.findMany({ take, cursor })`
|
|
98
98
|
- `tasks.commit({ operations, clientTxId })` -> `db.$transaction` that
|
|
99
|
-
applies each `op` and
|
|
100
|
-
retries
|
|
99
|
+
applies each `op` and writes an outbox marker with `clientTxId` before commit
|
|
101
100
|
- `events({ cursor, limit })` -> read from your outbox table, return
|
|
102
101
|
rows with their `clientTxId` (Ablo dedupes its own commits) and the
|
|
103
102
|
resume cursor
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* inside a transaction. The shape of the handlers stays identical.
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
|
-
import Ablo, { dataSource } from '@abloatai/ablo';
|
|
18
|
+
import Ablo, { dataSource, sourceEventForOperation } from '@abloatai/ablo';
|
|
19
19
|
import { schema } from './schema';
|
|
20
20
|
|
|
21
21
|
type TaskRow = {
|
|
@@ -29,16 +29,10 @@ type TaskRow = {
|
|
|
29
29
|
const taskStore = new Map<string, TaskRow>();
|
|
30
30
|
|
|
31
31
|
// Outbox table. In production this is a `tasks_outbox` Postgres table
|
|
32
|
-
// populated
|
|
33
|
-
// out changes that
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
entityId: string;
|
|
37
|
-
type: Ablo.Source.Operation['type'];
|
|
38
|
-
data: TaskRow | null;
|
|
39
|
-
clientTxId?: string;
|
|
40
|
-
};
|
|
41
|
-
const outbox: OutboxRow[] = [];
|
|
32
|
+
// populated in the same transaction as the app-row write. Ablo polls `events`
|
|
33
|
+
// to fan out changes that bypassed Ablo, and to repair SDK-origin writes if
|
|
34
|
+
// Ablo's immediate post-commit append failed.
|
|
35
|
+
const outbox: Ablo.Source.Event[] = [];
|
|
42
36
|
let outboxSequence = 0;
|
|
43
37
|
|
|
44
38
|
// Seed one row so the example's first `load` returns something.
|
|
@@ -132,19 +126,14 @@ export const handleAbloSource = dataSource({
|
|
|
132
126
|
const start = cursor ? Number(cursor) : 0;
|
|
133
127
|
const cap = limit ?? 100;
|
|
134
128
|
const slice = outbox.slice(start, start + cap);
|
|
135
|
-
const events = slice.map((row) => ({
|
|
136
|
-
id: row.id,
|
|
137
|
-
model: 'tasks',
|
|
138
|
-
entityId: row.entityId,
|
|
139
|
-
type: row.type,
|
|
140
|
-
data: row.data,
|
|
141
|
-
...(row.clientTxId ? { clientTxId: row.clientTxId } : {}),
|
|
142
|
-
}));
|
|
143
129
|
const nextCursor =
|
|
144
130
|
start + slice.length < outbox.length
|
|
145
131
|
? String(start + slice.length)
|
|
146
132
|
: undefined;
|
|
147
|
-
return {
|
|
133
|
+
return {
|
|
134
|
+
events: slice,
|
|
135
|
+
...(nextCursor !== undefined ? { nextCursor } : {}),
|
|
136
|
+
};
|
|
148
137
|
},
|
|
149
138
|
});
|
|
150
139
|
|
|
@@ -166,7 +155,7 @@ function applyOperation(
|
|
|
166
155
|
: {}),
|
|
167
156
|
};
|
|
168
157
|
taskStore.set(id, row);
|
|
169
|
-
appendOutbox({
|
|
158
|
+
appendOutbox({ operation: op, entityId: id, data: row, clientTxId });
|
|
170
159
|
return row;
|
|
171
160
|
}
|
|
172
161
|
|
|
@@ -175,7 +164,7 @@ function applyOperation(
|
|
|
175
164
|
if (!existing) return null;
|
|
176
165
|
const next: TaskRow = { ...existing, ...(op.input as Partial<TaskRow>) };
|
|
177
166
|
taskStore.set(id, next);
|
|
178
|
-
appendOutbox({
|
|
167
|
+
appendOutbox({ operation: op, entityId: id, data: next, clientTxId });
|
|
179
168
|
return next;
|
|
180
169
|
}
|
|
181
170
|
|
|
@@ -183,16 +172,29 @@ function applyOperation(
|
|
|
183
172
|
const existing = taskStore.get(id);
|
|
184
173
|
if (!existing) return null;
|
|
185
174
|
taskStore.delete(id);
|
|
186
|
-
appendOutbox({
|
|
175
|
+
appendOutbox({ operation: op, entityId: id, data: null, clientTxId });
|
|
187
176
|
return existing;
|
|
188
177
|
}
|
|
189
178
|
|
|
190
179
|
return null;
|
|
191
180
|
}
|
|
192
181
|
|
|
193
|
-
function appendOutbox(input:
|
|
182
|
+
function appendOutbox(input: {
|
|
183
|
+
operation: Ablo.Source.Operation;
|
|
184
|
+
entityId: string;
|
|
185
|
+
data: TaskRow | null;
|
|
186
|
+
clientTxId: string | undefined;
|
|
187
|
+
}): void {
|
|
194
188
|
outboxSequence += 1;
|
|
195
|
-
outbox.push(
|
|
189
|
+
outbox.push(
|
|
190
|
+
sourceEventForOperation({
|
|
191
|
+
eventId: `evt_${outboxSequence}`,
|
|
192
|
+
operation: input.operation,
|
|
193
|
+
entityId: input.entityId,
|
|
194
|
+
data: input.data,
|
|
195
|
+
...(input.clientTxId ? { clientTxId: input.clientTxId } : {}),
|
|
196
|
+
}),
|
|
197
|
+
);
|
|
196
198
|
}
|
|
197
199
|
|
|
198
200
|
// Exposed for the orchestrator's `run.ts`. A real customer doesn't
|
package/llms.txt
CHANGED
|
@@ -109,14 +109,24 @@ A schema is model fields and relations. Advanced schema helpers such as `mutable
|
|
|
109
109
|
|
|
110
110
|
Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
|
|
111
111
|
|
|
112
|
-
Every schema model has a backing store. By default, Ablo stores rows for declared models, so `ablo.<model>.create/update/delete` write to Ablo-managed state. If the customer database is canonical, expose a Data Source endpoint
|
|
113
|
-
|
|
114
|
-
Use `dataSource` from the root import:
|
|
112
|
+
Every schema model has a backing store. By default, Ablo stores rows for declared models, so `ablo.<model>.create/update/delete` write to Ablo-managed state. If the customer database is canonical, expose a Data Source endpoint. With Prisma or Drizzle this is ONE line — pass an ORM `adapter` and it owns the transaction, idempotency, and outbox (no hand-written `commit`/`events`):
|
|
115
113
|
|
|
116
114
|
```ts
|
|
117
|
-
|
|
115
|
+
// app/api/ablo/source/route.ts
|
|
116
|
+
import { dataSourceNext } from '@abloatai/ablo/source/next';
|
|
117
|
+
import { prismaDataSource } from '@abloatai/ablo/source';
|
|
118
|
+
import { schema } from '@/ablo/schema';
|
|
119
|
+
import { prisma } from '@/lib/prisma';
|
|
120
|
+
|
|
121
|
+
export const { POST } = dataSourceNext({
|
|
122
|
+
schema,
|
|
123
|
+
apiKey: process.env.ABLO_API_KEY!,
|
|
124
|
+
adapter: prismaDataSource(prisma, schema), // or drizzleDataSource(db, tables)
|
|
125
|
+
});
|
|
118
126
|
```
|
|
119
127
|
|
|
128
|
+
`npx ablo init` generates this file for you (see CLI below). Customer-owned app database credentials stay private — Ablo only calls the endpoint.
|
|
129
|
+
|
|
120
130
|
## Sandboxes
|
|
121
131
|
|
|
122
132
|
Public `/sandbox` is a deterministic visual demo. It should teach shared state,
|
|
@@ -143,7 +153,20 @@ Import from these public paths only:
|
|
|
143
153
|
- `@abloatai/ablo/schema` — schema DSL.
|
|
144
154
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
145
155
|
- `@abloatai/ablo/testing` — test harnesses and mocks.
|
|
156
|
+
- `@abloatai/ablo/source` — `dataSource`, the `DataSourceAdapter` spine, `prismaDataSource`. For a customer-canonical Data Source endpoint.
|
|
157
|
+
- `@abloatai/ablo/source/next` — `dataSourceNext` (Next.js App Router `{ POST }`).
|
|
158
|
+
- `@abloatai/ablo/source/drizzle` — `drizzleDataSource`.
|
|
159
|
+
- `@abloatai/ablo/source/conformance` — `runDataSourceTests` to prove a custom adapter/handler.
|
|
160
|
+
|
|
161
|
+
Do not teach `/api`, `/agent`, `/ai-sdk`, `/core`, `/realtime`, or internal subpaths. (`/source` IS public — it's the Data Source endpoint surface above.)
|
|
162
|
+
|
|
163
|
+
## CLI — agents run it NON-INTERACTIVELY
|
|
164
|
+
|
|
165
|
+
`ablo init` and other prompts need a TTY; an agent/CI run has none and will HANG. Always:
|
|
146
166
|
|
|
147
|
-
|
|
167
|
+
- `npx ablo init --yes` (flags: `--framework`, `--auth`, `--storage`, `--no-agent`, `--no-pull`, `--no-install`, `--no-login`). Generates `ablo/schema.ts` + the `ablo/data-source.ts` endpoint above.
|
|
168
|
+
- Authenticate with the `ABLO_API_KEY` env var. Do NOT run `ablo login` (opens a browser).
|
|
169
|
+
- Adopt an existing DB: `npx ablo pull prisma [path]` / `npx ablo pull drizzle <module>`.
|
|
170
|
+
- `npx ablo dev --no-watch` (default watches forever); `npx ablo logs --no-follow` (default tails forever); `npx ablo mode test|live` (always pass the arg). `npx ablo push`/`status`/`pull`/`check`/`generate` are one-shot.
|
|
148
171
|
|
|
149
172
|
Canonical docs to read before integrating: `quickstart`, `schema-contract`, `integration-guide`, `guarantees`, `client-behavior`, `data-sources`, `examples/existing-python-backend`, `api`, `examples/ai-sdk-tool`, and `examples/server-agent`.
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "State control API for AI agents and collaborative apps.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"engines": {
|
|
8
|
-
"node": ">=
|
|
8
|
+
"node": ">=24.0.0"
|
|
9
9
|
},
|
|
10
10
|
"main": "./dist/index.js",
|
|
11
11
|
"types": "./dist/index.d.ts",
|
|
@@ -55,10 +55,45 @@
|
|
|
55
55
|
"import": "./dist/source/index.js",
|
|
56
56
|
"default": "./dist/source/index.js"
|
|
57
57
|
},
|
|
58
|
+
"./source/conformance": {
|
|
59
|
+
"types": "./dist/source/conformance.d.ts",
|
|
60
|
+
"import": "./dist/source/conformance.js",
|
|
61
|
+
"default": "./dist/source/conformance.js"
|
|
62
|
+
},
|
|
63
|
+
"./source/drizzle": {
|
|
64
|
+
"types": "./dist/source/adapters/drizzle.d.ts",
|
|
65
|
+
"import": "./dist/source/adapters/drizzle.js",
|
|
66
|
+
"default": "./dist/source/adapters/drizzle.js"
|
|
67
|
+
},
|
|
68
|
+
"./source/next": {
|
|
69
|
+
"types": "./dist/source/next.d.ts",
|
|
70
|
+
"import": "./dist/source/next.js",
|
|
71
|
+
"default": "./dist/source/next.js"
|
|
72
|
+
},
|
|
58
73
|
"./keys": {
|
|
59
74
|
"types": "./dist/keys/index.d.ts",
|
|
60
75
|
"import": "./dist/keys/index.js",
|
|
61
76
|
"default": "./dist/keys/index.js"
|
|
77
|
+
},
|
|
78
|
+
"./wire": {
|
|
79
|
+
"types": "./dist/wire/index.d.ts",
|
|
80
|
+
"import": "./dist/wire/index.js",
|
|
81
|
+
"default": "./dist/wire/index.js"
|
|
82
|
+
},
|
|
83
|
+
"./server": {
|
|
84
|
+
"types": "./dist/server/index.d.ts",
|
|
85
|
+
"import": "./dist/server/index.js",
|
|
86
|
+
"default": "./dist/server/index.js"
|
|
87
|
+
},
|
|
88
|
+
"./server/next": {
|
|
89
|
+
"types": "./dist/server/next.d.ts",
|
|
90
|
+
"import": "./dist/server/next.js",
|
|
91
|
+
"default": "./dist/server/next.js"
|
|
92
|
+
},
|
|
93
|
+
"./webhooks": {
|
|
94
|
+
"types": "./dist/webhooks/index.d.ts",
|
|
95
|
+
"import": "./dist/webhooks/index.js",
|
|
96
|
+
"default": "./dist/webhooks/index.js"
|
|
62
97
|
}
|
|
63
98
|
},
|
|
64
99
|
"files": [
|
|
@@ -123,11 +158,15 @@
|
|
|
123
158
|
"url": "https://github.com/Abloatai/ablo/issues"
|
|
124
159
|
},
|
|
125
160
|
"peerDependencies": {
|
|
126
|
-
"react": "^19.0.0"
|
|
161
|
+
"react": "^19.0.0",
|
|
162
|
+
"drizzle-orm": ">=0.30.0"
|
|
127
163
|
},
|
|
128
164
|
"peerDependenciesMeta": {
|
|
129
165
|
"react": {
|
|
130
166
|
"optional": true
|
|
167
|
+
},
|
|
168
|
+
"drizzle-orm": {
|
|
169
|
+
"optional": true
|
|
131
170
|
}
|
|
132
171
|
},
|
|
133
172
|
"dependencies": {
|
|
@@ -145,6 +184,7 @@
|
|
|
145
184
|
"@testing-library/react": "^16.0.0",
|
|
146
185
|
"@testing-library/jest-dom": "^6.6.0",
|
|
147
186
|
"ai": "^6.0.0",
|
|
187
|
+
"drizzle-orm": "^0.45.2",
|
|
148
188
|
"fake-indexeddb": "^6.0.0",
|
|
149
189
|
"fast-check": "^3.0.0",
|
|
150
190
|
"jest": "^29.7.0",
|