@abloatai/ablo 0.7.0 → 0.9.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 +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- 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 +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- 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 +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- 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 +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -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 +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- 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/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- 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 +7 -3
- package/dist/schema/serialize.js +6 -2
- 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 +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -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 +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -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 +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -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-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
|
@@ -0,0 +1,111 @@
|
|
|
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
|
+
data: {
|
|
41
|
+
location: 'Stockholm',
|
|
42
|
+
status: 'pending',
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
The model key (`weatherReports`) becomes the client namespace
|
|
48
|
+
(`ablo.weatherReports`). The Zod fields become the create/update/read type
|
|
49
|
+
contract. You should not create a parallel string-keyed write path for the same
|
|
50
|
+
data.
|
|
51
|
+
|
|
52
|
+
## Reads and writes
|
|
53
|
+
|
|
54
|
+
Use async reads when the row may not be local:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
const report = await ablo.weatherReports.retrieve({ id: reportId });
|
|
58
|
+
const ready = await ablo.weatherReports.list({ where: { status: 'ready' } });
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Use synchronous local reads in render after data has synced:
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
const report = ablo.weatherReports.get(reportId);
|
|
65
|
+
const pending = ablo.weatherReports.getAll({ where: { status: 'pending' } });
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Use model writes for every actor:
|
|
69
|
+
|
|
70
|
+
```ts
|
|
71
|
+
await ablo.weatherReports.update({ id: reportId, data: { status: 'ready' }, wait: 'confirmed' });
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Coordination
|
|
75
|
+
|
|
76
|
+
Agents and background jobs often read, call a tool or model, then write later.
|
|
77
|
+
Wrap that slow span in `claim`:
|
|
78
|
+
|
|
79
|
+
```ts
|
|
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();
|
|
84
|
+
```
|
|
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.
|
|
88
|
+
|
|
89
|
+
## Storage boundary
|
|
90
|
+
|
|
91
|
+
Every schema model needs a backing store:
|
|
92
|
+
|
|
93
|
+
- Use Ablo-managed state when the row can live in Ablo.
|
|
94
|
+
- Use a Data Source when your app database remains canonical.
|
|
95
|
+
|
|
96
|
+
Do not pass a database URL to `Ablo(...)`. Trusted runtimes use `ABLO_API_KEY`.
|
|
97
|
+
Browser code goes through `<AbloProvider>` or a scoped session route, never a raw
|
|
98
|
+
API key.
|
|
99
|
+
|
|
100
|
+
## Rules of thumb
|
|
101
|
+
|
|
102
|
+
- Start with fields and relations before load/index tuning.
|
|
103
|
+
- Import one schema into app code, server actions, agents, and Data Source routes.
|
|
104
|
+
- Keep direct database writes out of the coordinated path unless they are reported
|
|
105
|
+
back through Data Source events.
|
|
106
|
+
- Use `claim` for slow read -> think -> write spans.
|
|
107
|
+
- Use `readAt` + `onStale: 'reject'` when a write must fail if the row changed
|
|
108
|
+
after it was read.
|
|
109
|
+
|
|
110
|
+
For the shortest runnable path, start with [Quickstart](./quickstart.md). For a
|
|
111
|
+
production app, continue with [Integration Guide](./integration-guide.md).
|
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.
|
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.
|
|
@@ -34,9 +38,9 @@ signed bytes — flip the API key and you'll see a 401.
|
|
|
34
38
|
3. **The customer DB stays canonical.** Ablo never sees rows
|
|
35
39
|
directly; it only sees the response payload from the customer's
|
|
36
40
|
handler.
|
|
37
|
-
4. **The outbox feed.**
|
|
38
|
-
|
|
39
|
-
|
|
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.
|
|
40
44
|
|
|
41
45
|
## Production wiring
|
|
42
46
|
|
|
@@ -92,8 +96,7 @@ data layer. The handler shape stays the same:
|
|
|
92
96
|
- `tasks.load({ id })` -> `db.task.findUnique({ where: { id } })`
|
|
93
97
|
- `tasks.list({ query })` -> `db.task.findMany({ take, cursor })`
|
|
94
98
|
- `tasks.commit({ operations, clientTxId })` -> `db.$transaction` that
|
|
95
|
-
applies each `op` and
|
|
96
|
-
retries
|
|
99
|
+
applies each `op` and writes an outbox marker with `clientTxId` before commit
|
|
97
100
|
- `events({ cursor, limit })` -> read from your outbox table, return
|
|
98
101
|
rows with their `clientTxId` (Ablo dedupes its own commits) and the
|
|
99
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
|
|
@@ -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
|
*
|
package/examples/quickstart.ts
CHANGED
package/llms.txt
CHANGED
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
Ablo is the state coordination layer for apps where humans and agents edit the same data.
|
|
4
4
|
|
|
5
|
+
Here is the problem it solves. Two writers touch `report_stockholm` at once. The agent claims the row, does slow work (an LLM call, a fetch), and commits; the human's UI sees the claim live and never clobbers it. Claims don't lock. If another writer holds the row, `claim` waits for them, re-reads the fresh row, then hands it to you — so two writers serialize instead of clobbering.
|
|
6
|
+
|
|
5
7
|
Use AI SDK for the agent loop. Use Ablo when agent reads and writes must persist, coordinate with concurrent work, and leave an audit trail.
|
|
6
8
|
|
|
7
9
|
## Use this API
|
|
@@ -21,7 +23,7 @@ const schema = defineSchema({
|
|
|
21
23
|
|
|
22
24
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
23
25
|
|
|
24
|
-
const
|
|
26
|
+
const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
25
27
|
if (!report) throw new Error('Row not found');
|
|
26
28
|
|
|
27
29
|
const updated = await ablo.weatherReports.claim('report_stockholm', async (report) => {
|
|
@@ -33,20 +35,29 @@ const updated = await ablo.weatherReports.claim('report_stockholm', async (repor
|
|
|
33
35
|
});
|
|
34
36
|
```
|
|
35
37
|
|
|
36
|
-
That is the normal app path: declare models in a schema, then use `ablo.<model>.
|
|
38
|
+
That is the normal app path: declare models in a schema, then use `ablo.<model>.retrieve(...)`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`.
|
|
39
|
+
|
|
40
|
+
Treat the schema as the integration contract. It drives typed model clients,
|
|
41
|
+
React selectors, server and agent writes, Data Source request/response shape,
|
|
42
|
+
hosted schema push, and schema-version gating. Do not invent a parallel
|
|
43
|
+
string-keyed write path for rows that belong to a schema model.
|
|
37
44
|
|
|
38
45
|
For full integrations, use `integration-guide` as the canonical doc. It covers
|
|
39
46
|
the same model API across Ablo-managed state, Data Source-backed app databases,
|
|
40
47
|
React selectors, multiplayer, and future agent workers.
|
|
41
48
|
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
49
|
+
Reads come in two flavors, and you pick by whether you can wait. `retrieve(id)`
|
|
50
|
+
(one row) and `list({ where })` (many) are async — they hit the server and return
|
|
51
|
+
a Promise, so await them. `get(id)`, `getAll({ where })`, and `getCount({ where })`
|
|
52
|
+
are synchronous — they read the local graph and are reactive in render, so no
|
|
53
|
+
await. The query reads accept `where`, `filter`, `orderBy`, `limit`, `offset`,
|
|
54
|
+
and `state`; state defaults to `'live'`, with `'archived'` and `'all'` to include
|
|
55
|
+
retired rows.
|
|
45
56
|
|
|
46
|
-
|
|
47
|
-
|
|
57
|
+
Workers that can't import the app schema can use a schema-less mode (covered in
|
|
58
|
+
`integration-guide`).
|
|
48
59
|
|
|
49
|
-
React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.
|
|
60
|
+
React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.get(id))` (synchronous local read, reactive in render).
|
|
50
61
|
Use zero-argument `useAblo()` only when a component needs the client for an
|
|
51
62
|
event handler or effect. Treat `useQuery`, `useOne`, `useReader`, and
|
|
52
63
|
`useMutate` as compatibility hooks for older string-keyed integrations, not the
|
|
@@ -57,7 +68,7 @@ first integration path.
|
|
|
57
68
|
Multiplayer is not a separate mode. When human UI, server actions, and agents use
|
|
58
69
|
the same schema client and write through `ablo.<model>`, Ablo coordinates the
|
|
59
70
|
shared model stream: confirmed deltas fan out to subscribers, active claims are
|
|
60
|
-
visible through `
|
|
71
|
+
visible through `claim.state(id)`, and stale writes can be rejected with `readAt`.
|
|
61
72
|
|
|
62
73
|
If an app writes directly to its own database outside Ablo, that write bypasses
|
|
63
74
|
coordination until the app reports it through Data Source events.
|
|
@@ -65,7 +76,7 @@ coordination until the app reports it through Data Source events.
|
|
|
65
76
|
## Nouns
|
|
66
77
|
|
|
67
78
|
- `Model client` is the typed `ablo.<model>` object generated from schema.
|
|
68
|
-
- `Claim` holds a model row while slow work runs; `
|
|
79
|
+
- `Claim` holds a model row while slow work runs; `claim.state(id)` observes it.
|
|
69
80
|
- `Commit` is the durable protocol write behind `ablo.<model>.update(...)`.
|
|
70
81
|
- `Receipt` confirms the commit.
|
|
71
82
|
|
|
@@ -76,36 +87,46 @@ Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` to
|
|
|
76
87
|
receive active claims, `ifClaimed: 'fail'` to throw `AbloClaimedError`, or
|
|
77
88
|
`ifClaimed: 'wait'` to wait until the active claim clears.
|
|
78
89
|
|
|
79
|
-
Schema clients
|
|
90
|
+
Schema clients learn when a claim clears by listening to the live claim stream, so they don't need to poll. Schema-less HTTP callers must provide an explicit `claimedPollInterval` when using `ifClaimed: 'wait'`; Ablo does not hide a hard-coded polling loop.
|
|
80
91
|
|
|
81
92
|
Use `claimedTimeout` only as a maximum wait, not as the coordination mechanism.
|
|
82
93
|
|
|
83
94
|
## Guarantees
|
|
84
95
|
|
|
85
|
-
`wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state.
|
|
96
|
+
`wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state. To prevent lost updates, read with `snapshot(...)` to capture a `readAt`, then write with `onStale: 'reject'` — the server rejects your update if someone else changed the row after that `readAt`.
|
|
86
97
|
|
|
87
98
|
Claims coordinate writers; they do not block readers. Most users should stay on
|
|
88
|
-
schema-backed reads/writes and `claim(...)`;
|
|
89
|
-
|
|
99
|
+
schema-backed reads/writes and `claim(...)`; manual protocol bookkeeping is not
|
|
100
|
+
part of the happy path.
|
|
90
101
|
|
|
91
102
|
All SDK errors extend `AbloError`. Important classes: `AbloClaimedError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`.
|
|
92
103
|
|
|
93
104
|
## Schema Scope
|
|
94
105
|
|
|
95
|
-
|
|
106
|
+
A schema is model fields and relations. Advanced schema helpers such as `mutable`, `readOnly`, `field`, `indexed`, queries, and load strategies exist for offline/cache/indexing-heavy apps; reach for them only after the basic field/relation schema is working.
|
|
96
107
|
|
|
97
108
|
## Storage Boundary
|
|
98
109
|
|
|
99
110
|
Do not add `databaseURL` to `Ablo(...)`. Application and agent code use `ABLO_API_KEY`.
|
|
100
111
|
|
|
101
|
-
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
|
|
102
|
-
|
|
103
|
-
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`):
|
|
104
113
|
|
|
105
114
|
```ts
|
|
106
|
-
|
|
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
|
+
});
|
|
107
126
|
```
|
|
108
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
|
+
|
|
109
130
|
## Sandboxes
|
|
110
131
|
|
|
111
132
|
Public `/sandbox` is a deterministic visual demo. It should teach shared state,
|
|
@@ -132,7 +153,20 @@ Import from these public paths only:
|
|
|
132
153
|
- `@abloatai/ablo/schema` — schema DSL.
|
|
133
154
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
134
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:
|
|
135
166
|
|
|
136
|
-
|
|
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.
|
|
137
171
|
|
|
138
|
-
Canonical docs to read before integrating: `quickstart`, `integration-guide`, `guarantees`, `client-behavior`, `data-sources`, `examples/existing-python-backend`, `api`, `examples/ai-sdk-tool`, and `examples/server-agent`.
|
|
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.0",
|
|
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",
|
|
@@ -54,6 +54,46 @@
|
|
|
54
54
|
"types": "./dist/source/index.d.ts",
|
|
55
55
|
"import": "./dist/source/index.js",
|
|
56
56
|
"default": "./dist/source/index.js"
|
|
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
|
+
},
|
|
73
|
+
"./keys": {
|
|
74
|
+
"types": "./dist/keys/index.d.ts",
|
|
75
|
+
"import": "./dist/keys/index.js",
|
|
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"
|
|
57
97
|
}
|
|
58
98
|
},
|
|
59
99
|
"files": [
|
|
@@ -118,11 +158,15 @@
|
|
|
118
158
|
"url": "https://github.com/Abloatai/ablo/issues"
|
|
119
159
|
},
|
|
120
160
|
"peerDependencies": {
|
|
121
|
-
"react": "^19.0.0"
|
|
161
|
+
"react": "^19.0.0",
|
|
162
|
+
"drizzle-orm": ">=0.30.0"
|
|
122
163
|
},
|
|
123
164
|
"peerDependenciesMeta": {
|
|
124
165
|
"react": {
|
|
125
166
|
"optional": true
|
|
167
|
+
},
|
|
168
|
+
"drizzle-orm": {
|
|
169
|
+
"optional": true
|
|
126
170
|
}
|
|
127
171
|
},
|
|
128
172
|
"dependencies": {
|
|
@@ -140,6 +184,7 @@
|
|
|
140
184
|
"@testing-library/react": "^16.0.0",
|
|
141
185
|
"@testing-library/jest-dom": "^6.6.0",
|
|
142
186
|
"ai": "^6.0.0",
|
|
187
|
+
"drizzle-orm": "^0.45.2",
|
|
143
188
|
"fake-indexeddb": "^6.0.0",
|
|
144
189
|
"fast-check": "^3.0.0",
|
|
145
190
|
"jest": "^29.7.0",
|