@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/cli.md
CHANGED
|
@@ -12,12 +12,13 @@ npx ablo login # authorize in the browser
|
|
|
12
12
|
npx ablo dev # push schema to the test sandbox + watch
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
**Two
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
15
|
+
**Two setup styles, and they pick your commands.** If your app database is the
|
|
16
|
+
source of truth, expose a [Data Source endpoint](./data-sources.md) and keep DB
|
|
17
|
+
credentials in your app. If you explicitly want Ablo to open a Postgres
|
|
18
|
+
connection, use the **Direct Postgres connector** commands: `ablo migrate`
|
|
19
|
+
applies changes to your own `DATABASE_URL`, and `ablo check` / `ablo pull`
|
|
20
|
+
adopt tables you already have. Hosted sandbox commands are tagged **Hosted**;
|
|
21
|
+
direct-connector commands are tagged **Direct Postgres**.
|
|
21
22
|
|
|
22
23
|
## Authenticate
|
|
23
24
|
|
|
@@ -26,12 +27,12 @@ tell which apply to you.
|
|
|
26
27
|
**test + live key pair** (90-day, restricted) and stores them locally. This
|
|
27
28
|
mirrors `stripe login`.
|
|
28
29
|
|
|
29
|
-
| Command
|
|
30
|
-
|
|
|
31
|
-
| `ablo login`
|
|
32
|
-
| `ablo logout`
|
|
33
|
-
| `ablo status`
|
|
34
|
-
| `ablo mode [test\|live]` | Switch the active mode. With no argument, prompts.
|
|
30
|
+
| Command | What it does |
|
|
31
|
+
| ------------------------ | -------------------------------------------------------------------------- |
|
|
32
|
+
| `ablo login` | Authorize in the browser; provisions + stores a test and a live key. |
|
|
33
|
+
| `ablo logout` | Remove the stored keys. |
|
|
34
|
+
| `ablo status` | Show the active org, mode, both keys (prefix + expiry), and server health. |
|
|
35
|
+
| `ablo mode [test\|live]` | Switch the active mode. With no argument, prompts. |
|
|
35
36
|
|
|
36
37
|
Keys are stored in `~/.config/ablo/config.json` (mode `0600`). In **CI**, don't
|
|
37
38
|
log in — set `ABLO_API_KEY`, which always overrides the stored key.
|
|
@@ -48,18 +49,18 @@ either mode) defines the same models test and live see; only the rows differ.
|
|
|
48
49
|
|
|
49
50
|
## Commands
|
|
50
51
|
|
|
51
|
-
| Command
|
|
52
|
-
|
|
|
53
|
-
| `ablo init`
|
|
54
|
-
| `ablo login` / `logout` / `status` | Authentication & status (above).
|
|
55
|
-
| `ablo mode [test\|live]`
|
|
56
|
-
| `ablo dev`
|
|
57
|
-
| `ablo logs`
|
|
58
|
-
| `ablo push`
|
|
59
|
-
| `ablo migrate`
|
|
60
|
-
| `ablo pull`
|
|
61
|
-
| `ablo check`
|
|
62
|
-
| `ablo generate`
|
|
52
|
+
| Command | What it does | Flags |
|
|
53
|
+
| ---------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
|
|
54
|
+
| `ablo init` | Scaffold `ablo/` (`schema.ts`, client, optional Data Source / agent / component), write `.env`, install the SDK. Offers to log in at the end. | — |
|
|
55
|
+
| `ablo login` / `logout` / `status` | Authentication & status (above). | — |
|
|
56
|
+
| `ablo mode [test\|live]` | Switch active mode. | — |
|
|
57
|
+
| `ablo dev` | **Hosted** — push the schema to your test sandbox, then watch `ablo/schema.ts` and re-push on save. | `--no-watch`, `--schema <path>`, `--export <name>`, `--url <url>` |
|
|
58
|
+
| `ablo logs` | Tail your scope's commit activity (`stripe logs tail`). Follows by default. | `-n, --tail <N>`, `--since <dur\|ts>`, `--model`, `--op`, `--json`, `--no-follow`, `--mode test\|live` |
|
|
59
|
+
| `ablo push` | **Hosted** — upload the schema to Ablo; the server diffs, migrates, and activates it. | `--force`, `--rename old:new`, `--backfill model.field=value`, `--schema`, `--export`, `--url` |
|
|
60
|
+
| `ablo migrate` | **Direct Postgres** — apply the schema to your own `DATABASE_URL` (you run the table-creation SQL). | `--dry-run`, `--output <file>`, `--schema`, `--export` |
|
|
61
|
+
| `ablo pull` | **Direct Postgres** — generate `defineSchema(...)` from your existing tables (read-only, like `prisma db pull`). | `--out <path>`, `--app-schema <name>`, `--import <pkg>`, `--force` |
|
|
62
|
+
| `ablo check` | **Direct Postgres** — verify your _existing_ tables fit the schema (read-only, no schema changes). | `--schema <path>`, `--export <name>`, `--app-schema <name>` |
|
|
63
|
+
| `ablo generate` | Emit TypeScript types from the schema. | `--out <path>`, `--schema`, `--export` |
|
|
63
64
|
|
|
64
65
|
## `ablo dev`
|
|
65
66
|
|
|
@@ -140,13 +141,12 @@ If a table can't carry `organization_id` (or has business logic Ablo shouldn't
|
|
|
140
141
|
bypass), keep it behind a [Data Source endpoint](/data-sources) rather than
|
|
141
142
|
reshaping it. `ablo check` is read-only; it never proposes a migration.
|
|
142
143
|
|
|
143
|
-
## `migrate` (
|
|
144
|
+
## `migrate` (Direct Postgres) vs `push` (Hosted)
|
|
144
145
|
|
|
145
|
-
Same engine, two setups. If you
|
|
146
|
+
Same engine, two setups. If you use the **Direct Postgres connector**, use
|
|
146
147
|
`ablo migrate` — it applies the schema to your own `DATABASE_URL`, and you run
|
|
147
|
-
the
|
|
148
|
-
|
|
149
|
-
and version-gates connecting clients.
|
|
148
|
+
the table-creation SQL. If Ablo manages the sandbox/hosted store, use `ablo push` and
|
|
149
|
+
`ablo dev` — the server applies the change and version-gates connecting clients.
|
|
150
150
|
|
|
151
151
|
```bash
|
|
152
152
|
ablo migrate --dry-run # preview the exact SQL
|
|
@@ -158,15 +158,15 @@ ablo migrate --output schema.sql # write SQL to a file
|
|
|
158
158
|
|
|
159
159
|
The one type map, shared by both paths (there is no second mapping):
|
|
160
160
|
|
|
161
|
-
| Zod
|
|
162
|
-
|
|
|
163
|
-
| `z.string()`
|
|
164
|
-
| `z.number()`
|
|
165
|
-
| `z.boolean()`
|
|
166
|
-
| `z.date()`
|
|
167
|
-
| `z.enum([...])`
|
|
168
|
-
| `z.object` / `z.array` / `z.record` / `z.union` / `z.custom` | `JSONB`
|
|
169
|
-
| `.optional()` / `.nullable()`
|
|
161
|
+
| Zod | Postgres |
|
|
162
|
+
| ------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------- |
|
|
163
|
+
| `z.string()` | `TEXT` |
|
|
164
|
+
| `z.number()` | `DOUBLE PRECISION` — never `INTEGER`; a Zod number may be fractional, and truncating is silent data loss |
|
|
165
|
+
| `z.boolean()` | `BOOLEAN` |
|
|
166
|
+
| `z.date()` | `TIMESTAMPTZ` |
|
|
167
|
+
| `z.enum([...])` | `TEXT` + a `CHECK (col IN (...))` constraint |
|
|
168
|
+
| `z.object` / `z.array` / `z.record` / `z.union` / `z.custom` | `JSONB` |
|
|
169
|
+
| `.optional()` / `.nullable()` | nullable column |
|
|
170
170
|
|
|
171
171
|
Each table also gets the platform columns (`id`, `organization_id`,
|
|
172
172
|
`created_by`, `created_at`, `updated_at`), an `organization_id` index, and
|
|
@@ -214,9 +214,9 @@ migration can't leave clients gated against tables that don't match.
|
|
|
214
214
|
|
|
215
215
|
## Environment
|
|
216
216
|
|
|
217
|
-
| Variable
|
|
218
|
-
|
|
|
219
|
-
| `ABLO_API_KEY`
|
|
220
|
-
| `ABLO_API_URL`
|
|
221
|
-
| `ABLO_AUTH_URL`
|
|
222
|
-
| `ABLO_CONFIG_DIR` / `XDG_CONFIG_HOME` | Where the credential file lives.
|
|
217
|
+
| Variable | Purpose | Default |
|
|
218
|
+
| ------------------------------------- | ------------------------------------------------------------------------ | -------------------------- |
|
|
219
|
+
| `ABLO_API_KEY` | Authenticate without `ablo login` (CI). Always overrides the stored key. | — |
|
|
220
|
+
| `ABLO_API_URL` | Control-plane / API host (`push`, `dev`, `status`). | `https://api.abloatai.com` |
|
|
221
|
+
| `ABLO_AUTH_URL` | Dashboard origin for `ablo login`'s device flow. | `https://abloatai.com` |
|
|
222
|
+
| `ABLO_CONFIG_DIR` / `XDG_CONFIG_HOME` | Where the credential file lives. | `~/.config/ablo` |
|
package/docs/client-behavior.md
CHANGED
|
@@ -47,12 +47,12 @@ Each schema model becomes a typed model:
|
|
|
47
47
|
```ts
|
|
48
48
|
await ablo.ready();
|
|
49
49
|
|
|
50
|
-
const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
50
|
+
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
51
51
|
const local = ablo.weatherReports.get('report_stockholm');
|
|
52
52
|
|
|
53
|
-
await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
|
|
54
|
-
await ablo.weatherReports.update('report_stockholm', { status: 'ready' },
|
|
55
|
-
await ablo.weatherReports.delete('report_stockholm',
|
|
53
|
+
await ablo.weatherReports.create({ data: { location: 'Stockholm', status: 'pending' } });
|
|
54
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
|
|
55
|
+
await ablo.weatherReports.delete({ id: 'report_stockholm', wait: 'confirmed' });
|
|
56
56
|
```
|
|
57
57
|
|
|
58
58
|
Call `retrieve`/`list` first — they fetch from the server and you `await` them.
|
|
@@ -73,10 +73,12 @@ through the same model client path. A human Server Action, a browser view, and a
|
|
|
73
73
|
agent worker can all use `ablo.weatherReports`:
|
|
74
74
|
|
|
75
75
|
```ts
|
|
76
|
-
const report = await ablo.weatherReports.retrieve(id);
|
|
76
|
+
const report = await ablo.weatherReports.retrieve({ id });
|
|
77
77
|
const snap = ablo.snapshot({ weatherReports: id });
|
|
78
78
|
|
|
79
|
-
await ablo.weatherReports.update(
|
|
79
|
+
await ablo.weatherReports.update({
|
|
80
|
+
id,
|
|
81
|
+
data: patch,
|
|
80
82
|
readAt: snap.stamp,
|
|
81
83
|
onStale: 'reject',
|
|
82
84
|
wait: 'confirmed',
|
|
@@ -86,7 +88,7 @@ await ablo.weatherReports.update(id, patch, {
|
|
|
86
88
|
Once the server accepts the write, every other connected client gets the new row
|
|
87
89
|
automatically — no polling or manual refresh on your side. React clients that use
|
|
88
90
|
`useAblo((ablo) => ablo.weatherReports.get(id))` receive the new row, and selectors
|
|
89
|
-
such as `useAblo((ablo) => ablo.weatherReports.claim.state(id))`
|
|
91
|
+
such as `useAblo((ablo) => ablo.weatherReports.claim.state({ id }))`
|
|
90
92
|
receive active claim state. There is
|
|
91
93
|
no extra multiplayer setup beyond routing shared state through Ablo.
|
|
92
94
|
|
|
@@ -96,17 +98,15 @@ until the app reports it through Data Source events.
|
|
|
96
98
|
## Per-Write Options
|
|
97
99
|
|
|
98
100
|
```ts
|
|
99
|
-
await ablo.weatherReports.update(
|
|
100
|
-
'report_stockholm',
|
|
101
|
-
{ status: 'ready' },
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
);
|
|
101
|
+
await ablo.weatherReports.update({
|
|
102
|
+
id: 'report_stockholm',
|
|
103
|
+
data: { status: 'ready' },
|
|
104
|
+
wait: 'confirmed',
|
|
105
|
+
readAt: snap.stamp,
|
|
106
|
+
onStale: 'reject',
|
|
107
|
+
idempotencyKey: 'report_stockholm:mark-ready:v1',
|
|
108
|
+
timeout: 20_000,
|
|
109
|
+
});
|
|
110
110
|
```
|
|
111
111
|
|
|
112
112
|
| Option | Purpose |
|
|
@@ -121,27 +121,27 @@ await ablo.weatherReports.update(
|
|
|
121
121
|
|
|
122
122
|
If your update involves a slow step — an API call, an LLM round-trip — and someone
|
|
123
123
|
else might write the same record meanwhile, claiming the record stops you from
|
|
124
|
-
overwriting their change. Check who holds the record with `claim.state(id)`, then
|
|
125
|
-
take it with `claim(id
|
|
124
|
+
overwriting their change. Check who holds the record with `claim.state({ id })`, then
|
|
125
|
+
take it with `claim({ id })`:
|
|
126
126
|
|
|
127
127
|
```ts
|
|
128
|
-
const active = ablo.weatherReports.claim.state('report_stockholm');
|
|
128
|
+
const active = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
129
129
|
|
|
130
130
|
if (active) {
|
|
131
131
|
return { status: 'claimed', active };
|
|
132
132
|
}
|
|
133
133
|
|
|
134
|
-
await ablo.weatherReports.claim('report_stockholm'
|
|
135
|
-
|
|
136
|
-
|
|
134
|
+
const handle = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
135
|
+
await ablo.weatherReports.update({ id: handle.data.id, data: { status: 'ready' } });
|
|
136
|
+
await handle.release();
|
|
137
137
|
```
|
|
138
138
|
|
|
139
|
-
`claim.state(id)` returns the current holder (or nothing) without ever blocking.
|
|
140
|
-
When you call `claim(id
|
|
141
|
-
the latest row, then
|
|
142
|
-
see. Options on the
|
|
139
|
+
`claim.state({ id })` returns the current holder (or nothing) without ever blocking.
|
|
140
|
+
When you call `claim({ id })`, the SDK queues other claimers behind you, re-reads
|
|
141
|
+
the latest row, then hands you the fresh row — so you can't overwrite a change you didn't
|
|
142
|
+
see. Options on the claim:
|
|
143
143
|
|
|
144
|
-
- default `claim` waits in the fair queue and re-reads before
|
|
144
|
+
- default `claim` waits in the fair queue and re-reads before handing you the row;
|
|
145
145
|
- `{ wait: false }` rejects with `AbloClaimedError` instead of queuing;
|
|
146
146
|
- `{ maxQueueDepth }` rejects if the wait line is already too deep.
|
|
147
147
|
|
|
@@ -168,7 +168,7 @@ All SDK errors extend `AbloError` and carry a stable `type`.
|
|
|
168
168
|
import { AbloClaimedError } from '@abloatai/ablo';
|
|
169
169
|
|
|
170
170
|
try {
|
|
171
|
-
await ablo.weatherReports.update('report_stockholm', { status: 'ready' },
|
|
171
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
|
|
172
172
|
} catch (error) {
|
|
173
173
|
if (error instanceof AbloClaimedError) {
|
|
174
174
|
return { status: 'claimed' };
|
package/docs/coordination.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Coordination Reference
|
|
2
2
|
|
|
3
3
|
Coordinate long-running work on a row so humans and agents don't clobber each
|
|
4
|
-
other. Most writes need none of this — `ablo.<model>.update(id,
|
|
4
|
+
other. Most writes need none of this — `ablo.<model>.update({ id, data })` is optimistic
|
|
5
5
|
and the server rejects it if the row moved. Reach for `claim` only when you'll
|
|
6
6
|
**hold a row across a slow gap** (read → LLM call → write).
|
|
7
7
|
|
|
@@ -98,8 +98,7 @@ parameters · returns · example**.
|
|
|
98
98
|
### `claim`
|
|
99
99
|
|
|
100
100
|
```ts
|
|
101
|
-
ablo.<model>.claim(id,
|
|
102
|
-
ablo.<model>.claim(id, options?): Promise<ClaimedRow<T>>
|
|
101
|
+
ablo.<model>.claim({ id, ...options }): Promise<ClaimHandle<T>> // handle; AsyncDisposable, auto-releases with `await using`
|
|
103
102
|
```
|
|
104
103
|
|
|
105
104
|
Claim a row so other writers serialize behind you until you're done; reads stay
|
|
@@ -120,38 +119,36 @@ so two claimers can't both think they won.
|
|
|
120
119
|
| `options.wait` | `boolean` | no | `true` (default) queues and waits for the lease. `false` is fail-fast — if another participant holds the row, reject immediately with `AbloClaimedError('entity_claimed')` instead of queuing (claim-or-skip, for work dedup where waiting would double-process). |
|
|
121
120
|
| `options.maxQueueDepth` | `number` | no | Backpressure: reject with `AbloClaimedError('queue_too_deep')` instead of joining a line already `>= maxQueueDepth` deep. Omit to wait however deep the queue is. |
|
|
122
121
|
| `options.ttl` | `Duration` | no | Crash-cleanup floor. Rarely set — the lease renews while your connection is alive, so it only matters once you go silent. |
|
|
123
|
-
| `work` | `(row) => …` | no | Callback form: hold the claim for the callback, release when it returns. |
|
|
124
122
|
|
|
125
123
|
The high-level `claim` queues by default, so on contention you either get the row
|
|
126
124
|
when your turn arrives or one of the [queue errors](#errors) (`claim_lost`,
|
|
127
125
|
`grant_timeout`).
|
|
128
126
|
|
|
129
|
-
**Returns** —
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
127
|
+
**Returns** — a `ClaimHandle<T>` (an `AsyncDisposable`): `handle.data` is the
|
|
128
|
+
fresh row snapshot taken once the lease is yours, and `handle.release()` gives
|
|
129
|
+
the claim back. Bind it with `await using` so the claim auto-releases when the
|
|
130
|
+
scope exits.
|
|
133
131
|
|
|
134
132
|
**Example**
|
|
135
133
|
|
|
136
134
|
```ts
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
});
|
|
135
|
+
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
136
|
+
const report = claim.data;
|
|
137
|
+
const weather = await weatherAgent.getWeather(report.location);
|
|
138
|
+
await ablo.weatherReports.update({ id: report.id, data: { forecast: weather } });
|
|
142
139
|
```
|
|
143
140
|
|
|
144
|
-
The
|
|
145
|
-
held work should use the callback form above.
|
|
141
|
+
The claim releases when the `await using` scope exits — on return or throw.
|
|
146
142
|
|
|
147
143
|
### Claim-gated reads
|
|
148
144
|
|
|
149
|
-
`claim.state(id)` always returns immediately. Model reads such as
|
|
145
|
+
`claim.state({ id })` always returns immediately. Model reads such as
|
|
150
146
|
`ablo.<model>.get(id)` are local reads and stay available while a claim is
|
|
151
147
|
held. Server/model reads can choose a claimed policy:
|
|
152
148
|
|
|
153
149
|
```ts
|
|
154
|
-
await ablo.
|
|
150
|
+
await ablo.weatherReports.retrieve({
|
|
151
|
+
id: 'report_stockholm',
|
|
155
152
|
ifClaimed: 'wait',
|
|
156
153
|
claimedTimeout: 30_000,
|
|
157
154
|
});
|
|
@@ -164,7 +161,7 @@ await ablo.model('weatherReports').retrieve('report_stockholm', {
|
|
|
164
161
|
### `claim.state`
|
|
165
162
|
|
|
166
163
|
```ts
|
|
167
|
-
ablo.<model>.claim.state(id)
|
|
164
|
+
ablo.<model>.claim.state({ id })
|
|
168
165
|
```
|
|
169
166
|
|
|
170
167
|
Read who's currently working on a row, for observers and UI. Synchronous and
|
|
@@ -182,7 +179,7 @@ is free.
|
|
|
182
179
|
**Example**
|
|
183
180
|
|
|
184
181
|
```ts
|
|
185
|
-
const who = ablo.weatherReports.claim.state('report_stockholm');
|
|
182
|
+
const who = ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
186
183
|
if (who) console.log(`${who.heldBy} is ${who.action}`);
|
|
187
184
|
```
|
|
188
185
|
|
|
@@ -203,7 +200,7 @@ Returns the active claim state when the row is held, or `null` when it's free:
|
|
|
203
200
|
### `claim.queue`
|
|
204
201
|
|
|
205
202
|
```ts
|
|
206
|
-
ablo.<model>.claim.queue(id)
|
|
203
|
+
ablo.<model>.claim.queue({ id })
|
|
207
204
|
```
|
|
208
205
|
|
|
209
206
|
Read the **wait line** behind a row — the FIFO of claims queued behind the
|
|
@@ -226,7 +223,7 @@ the active holder; `[]` when no one is waiting.
|
|
|
226
223
|
**Example**
|
|
227
224
|
|
|
228
225
|
```ts
|
|
229
|
-
const { data: waiting } = ablo.weatherReports.claim.queue('report_stockholm');
|
|
226
|
+
const { data: waiting } = ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
230
227
|
console.log(`${waiting.length} ahead of you`);
|
|
231
228
|
console.log(waiting.map((i) => i.heldBy));
|
|
232
229
|
```
|
|
@@ -234,11 +231,11 @@ console.log(waiting.map((i) => i.heldBy));
|
|
|
234
231
|
### `claim.release`
|
|
235
232
|
|
|
236
233
|
```ts
|
|
237
|
-
ablo.<model>.claim.release(id): Promise<void>
|
|
234
|
+
ablo.<model>.claim.release({ id }): Promise<void>
|
|
238
235
|
```
|
|
239
236
|
|
|
240
|
-
Release a claim you hold. Usually **implicit** — the
|
|
241
|
-
for you, and TTL cleans up a crashed holder.
|
|
237
|
+
Release a claim you hold. Usually **implicit** — the `await using` scope exiting
|
|
238
|
+
releases for you, and TTL cleans up a crashed holder.
|
|
242
239
|
Call this only to give a manually held claim back early (claimed, then decided
|
|
243
240
|
not to write).
|
|
244
241
|
Releasing **promotes the head of the queue**: the next waiter receives the claim.
|
|
@@ -254,28 +251,28 @@ Releasing **promotes the head of the queue**: the next waiter receives the claim
|
|
|
254
251
|
**Example**
|
|
255
252
|
|
|
256
253
|
```ts
|
|
257
|
-
const
|
|
254
|
+
const claim = await ablo.weatherReports.claim({ id: 'report_stockholm', action: 'reviewing' });
|
|
255
|
+
const report = claim.data;
|
|
258
256
|
try {
|
|
259
257
|
const ok = await reviewExternally(report);
|
|
260
258
|
if (!ok) return; // abandon, no write
|
|
261
|
-
await ablo.weatherReports.update(report.id, { status: 'ready' });
|
|
259
|
+
await ablo.weatherReports.update({ id: report.id, data: { status: 'ready' } });
|
|
262
260
|
} finally {
|
|
263
|
-
await ablo.weatherReports.claim.release(report.id);
|
|
261
|
+
await ablo.weatherReports.claim.release({ id: report.id });
|
|
264
262
|
}
|
|
265
263
|
```
|
|
266
264
|
|
|
267
265
|
### Writing under a claim
|
|
268
266
|
|
|
269
267
|
There is no separate "write" method on a claim — use the normal
|
|
270
|
-
`ablo.<model>.update(id, data)`. While you hold a claim on `id`, that `update` is
|
|
268
|
+
`ablo.<model>.update({ id, data })`. While you hold a claim on `id`, that `update` is
|
|
271
269
|
automatically stale-guarded against the snapshot the claim took (`readAt` =
|
|
272
270
|
snapshot watermark, `onStale: 'reject'`) and attributed to the claim's lease, so
|
|
273
271
|
it rejects with [`AbloStaleContextError`](#errors) if the row changed under you.
|
|
274
272
|
|
|
275
273
|
```ts
|
|
276
|
-
await ablo.weatherReports.claim(id
|
|
277
|
-
|
|
278
|
-
});
|
|
274
|
+
await using claim = await ablo.weatherReports.claim({ id });
|
|
275
|
+
await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready' } }); // guarded by the claim
|
|
279
276
|
```
|
|
280
277
|
|
|
281
278
|
Claims are **enforced server-side**: if you `update`/`delete` a row that *another*
|
|
@@ -286,7 +283,7 @@ on fresh data. You never conflict with your own claim, and reads are never gated
|
|
|
286
283
|
|
|
287
284
|
```ts
|
|
288
285
|
try {
|
|
289
|
-
await ablo.weatherReports.update(id, { status: 'ready' });
|
|
286
|
+
await ablo.weatherReports.update({ id, data: { status: 'ready' } });
|
|
290
287
|
} catch (err) {
|
|
291
288
|
if (err instanceof AbloClaimedError) {
|
|
292
289
|
// someone else holds it — claim the row and retry from fresh state
|
|
@@ -318,10 +315,10 @@ that moved during your generation window — use it for selective regeneration
|
|
|
318
315
|
|
|
319
316
|
```ts
|
|
320
317
|
try {
|
|
321
|
-
await ablo.weatherReports.claim('report_stockholm'
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
});
|
|
318
|
+
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
319
|
+
const report = claim.data;
|
|
320
|
+
const weather = await weatherAgent.getWeather(report.location); // slow gap
|
|
321
|
+
await ablo.weatherReports.update({ id: report.id, data: { forecast: weather } });
|
|
325
322
|
} catch (err) {
|
|
326
323
|
if (err instanceof AbloClaimedError && err.code === 'claim_lost') {
|
|
327
324
|
// Our lease lapsed mid-flight (we stalled past the TTL). Re-claim and retry.
|
package/docs/data-sources.md
CHANGED
|
@@ -54,8 +54,8 @@ ABLO_API_KEY=sk_live_...
|
|
|
54
54
|
The SDK call is the same in both modes:
|
|
55
55
|
|
|
56
56
|
```ts
|
|
57
|
-
await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
|
|
58
|
-
await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
|
|
57
|
+
await ablo.weatherReports.create({ data: { location: 'Stockholm', status: 'pending' } });
|
|
58
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
|
|
59
59
|
const report = ablo.weatherReports.get('report_stockholm');
|
|
60
60
|
```
|
|
61
61
|
|
|
@@ -96,13 +96,13 @@ The shape is the same as a production webhook integration:
|
|
|
96
96
|
2. Store `ABLO_API_KEY` in your app.
|
|
97
97
|
3. Verify signed HTTP calls before opening a database transaction.
|
|
98
98
|
4. Keep your database credentials in your app.
|
|
99
|
-
5. Write an outbox row
|
|
99
|
+
5. Write an outbox row in the same transaction as every app-row change.
|
|
100
100
|
|
|
101
101
|
## Route
|
|
102
102
|
|
|
103
103
|
```ts
|
|
104
104
|
// app/api/ablo/source/route.ts
|
|
105
|
-
import { dataSource } from '@abloatai/ablo';
|
|
105
|
+
import { dataSource, sourceEventForOperation } from '@abloatai/ablo';
|
|
106
106
|
import { schema } from '@/ablo/schema';
|
|
107
107
|
import { db } from '@/db';
|
|
108
108
|
|
|
@@ -117,7 +117,23 @@ export const POST = dataSource({
|
|
|
117
117
|
async commit({ operations, clientTxId, context }) {
|
|
118
118
|
const rows = await context.auth.db.transaction(async (tx) => {
|
|
119
119
|
await tx.idempotency.upsert({ key: clientTxId, operations });
|
|
120
|
-
|
|
120
|
+
const changes = await applyOperations(tx, operations);
|
|
121
|
+
await tx.outbox.createMany({
|
|
122
|
+
data: changes.map(({ eventId, operation, entityId, data }) =>
|
|
123
|
+
sourceEventForOperation({
|
|
124
|
+
eventId,
|
|
125
|
+
operation,
|
|
126
|
+
entityId,
|
|
127
|
+
data,
|
|
128
|
+
...(clientTxId ? { clientTxId } : {}),
|
|
129
|
+
...(context.scope?.organizationId
|
|
130
|
+
? { organizationId: context.scope.organizationId }
|
|
131
|
+
: {}),
|
|
132
|
+
occurredAt: Date.now(),
|
|
133
|
+
}),
|
|
134
|
+
),
|
|
135
|
+
});
|
|
136
|
+
return changes.map(({ row }) => row);
|
|
121
137
|
});
|
|
122
138
|
|
|
123
139
|
return { rows };
|
|
@@ -140,11 +156,13 @@ export const POST = dataSource({
|
|
|
140
156
|
Your app code still writes through the normal model API:
|
|
141
157
|
|
|
142
158
|
```ts
|
|
143
|
-
await ablo.weatherReports.update(
|
|
144
|
-
'report_stockholm',
|
|
145
|
-
{ status: 'ready' },
|
|
146
|
-
|
|
147
|
-
|
|
159
|
+
await ablo.weatherReports.update({
|
|
160
|
+
id: 'report_stockholm',
|
|
161
|
+
data: { status: 'ready' },
|
|
162
|
+
wait: 'confirmed',
|
|
163
|
+
readAt: snap.stamp,
|
|
164
|
+
onStale: 'reject',
|
|
165
|
+
});
|
|
148
166
|
```
|
|
149
167
|
|
|
150
168
|
## Commit Request
|
|
@@ -188,10 +206,12 @@ Return canonical rows:
|
|
|
188
206
|
Use explicit `deltas` only when your source already computes canonical change
|
|
189
207
|
events.
|
|
190
208
|
|
|
191
|
-
##
|
|
209
|
+
## Outbox Events
|
|
192
210
|
|
|
193
|
-
|
|
194
|
-
|
|
211
|
+
Return your outbox feed from an `events` handler so connected humans and agents
|
|
212
|
+
stay current. Include SDK-origin events too. If Ablo already appended the commit
|
|
213
|
+
directly, `clientTxId` lets Ablo filter the echo; if the direct append failed,
|
|
214
|
+
the same outbox row repairs it on the next poll or push.
|
|
195
215
|
|
|
196
216
|
```ts
|
|
197
217
|
export const POST = dataSource({
|
|
@@ -218,7 +238,6 @@ export const POST = dataSource({
|
|
|
218
238
|
});
|
|
219
239
|
```
|
|
220
240
|
|
|
221
|
-
`clientTxId` lets Ablo drop SDK echoes that already produced a realtime update.
|
|
222
241
|
Events without `clientTxId` are treated as external writes.
|
|
223
242
|
|
|
224
243
|
## Production Checklist
|
|
@@ -230,7 +249,8 @@ Before using a customer-owned database in production:
|
|
|
230
249
|
- Verify signatures before opening a database transaction.
|
|
231
250
|
- Store `clientTxId` in an idempotency table before applying writes.
|
|
232
251
|
- Return canonical rows after each commit.
|
|
233
|
-
- Write outbox events in the same transaction as
|
|
252
|
+
- Write outbox events in the same transaction as every app-row write, including
|
|
253
|
+
Data Source `commit` writes.
|
|
234
254
|
- Dedupe outbox events by event `id`.
|
|
235
255
|
- Monitor last success, last error, retry count, event lag, and cursor.
|
|
236
256
|
|