@abloatai/ablo 0.46.0 → 0.47.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/AGENTS.md +1 -1
- package/CHANGELOG.md +48 -0
- package/LICENSE +1 -1
- package/NOTICE +3 -3
- package/README.md +0 -2
- package/docs/agent-messaging.md +0 -1
- package/docs/agents.md +1 -1
- package/docs/api.md +6 -5
- package/docs/cli.md +4 -4
- package/docs/client-behavior.md +8 -7
- package/docs/coordination.md +2 -2
- package/docs/data-sources.md +93 -15
- package/docs/debugging.md +4 -5
- package/docs/examples/agent-human.md +0 -2
- package/docs/examples/existing-python-backend.md +0 -2
- package/docs/examples/nextjs.md +1 -2
- package/docs/examples/server-agent.md +2 -3
- package/docs/groups.md +1 -1
- package/docs/guarantees.md +4 -7
- package/docs/how-it-works.md +6 -6
- package/docs/idempotency.md +0 -1
- package/docs/identity.md +1 -1
- package/docs/index.md +1 -2
- package/docs/integration-guide.md +7 -10
- package/docs/integrations/inngest.md +1 -4
- package/docs/integrations/temporal.md +1 -3
- package/docs/internal/agent-orchestration.md +0 -1
- package/docs/internal/serializable-schema.md +1 -1
- package/docs/migration.md +3 -3
- package/docs/operating-on-your-database.md +2 -2
- package/docs/react.md +1 -3
- package/docs/schema-contract.md +2 -2
- package/examples/README.md +1 -1
- package/llms.txt +2 -3
- package/package.json +3 -3
package/AGENTS.md
CHANGED
|
@@ -79,7 +79,7 @@ await ablo.weatherReports.update({
|
|
|
79
79
|
|
|
80
80
|
## Coordination surface
|
|
81
81
|
|
|
82
|
-
Claims live on a callable namespace beside `create` / `update` / `
|
|
82
|
+
Claims live on a callable namespace beside `create` / `update` / `get`. Every member takes an options object:
|
|
83
83
|
|
|
84
84
|
- `await using claim = await ablo.<model>.claim({ id })` — acquire the row (waits if held); read it via `claim.data`; auto-releases on scope exit (or call `claim.release()`).
|
|
85
85
|
- `ablo.<model>.claim.state({ id })` — who is currently working on the row (synchronous; never blocks).
|
package/CHANGELOG.md
CHANGED
|
@@ -1,7 +1,42 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.47.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 101ca2c: Make schema model writes optimistic with one stable promise contract: local reactive state changes immediately, while awaiting `create`, `update`, or `delete` always waits for authoritative confirmation. Remove the model-level and client-level `wait` options; explicit queued-versus-confirmed receipt control remains on `commits.create`.
|
|
8
|
+
|
|
9
|
+
### Patch Changes
|
|
10
|
+
|
|
11
|
+
- Updated dependencies [101ca2c]
|
|
12
|
+
- @abloatai/transaction@0.47.0
|
|
13
|
+
- @abloatai/humans@0.47.0
|
|
14
|
+
|
|
3
15
|
## 0.46.0
|
|
4
16
|
|
|
17
|
+
### Provider branches isolate environments; schemas isolate projects
|
|
18
|
+
|
|
19
|
+
Several Ablo projects can now share one physical Postgres database safely when
|
|
20
|
+
each owns a separate schema. `ablo connect apply --schema mail` binds the
|
|
21
|
+
authenticated project branch to `(database, mail)` and derives an independent
|
|
22
|
+
slot, publication, replication role, and writer role. Publications enumerate
|
|
23
|
+
schema-qualified mapped tables and the idempotency ledger lives in the selected
|
|
24
|
+
schema. A provider branch's distinct direct URL remains the environment boundary
|
|
25
|
+
for Neon, Supabase, and similar hosts.
|
|
26
|
+
|
|
27
|
+
Registration enforces one owner per `(database, schema)` globally, without
|
|
28
|
+
revealing another organization's coordinates, and refuses a new binding when
|
|
29
|
+
`max_replication_slots` has no capacity. Runtime replication, rotate,
|
|
30
|
+
resnapshot, disconnect, and scan use the binding's stored/derived footprint;
|
|
31
|
+
legacy manual setups remain single-binding.
|
|
32
|
+
|
|
33
|
+
New clients can pin their intended project and branch with `projectId` /
|
|
34
|
+
`ABLO_PROJECT_ID` and `branchId` / `ABLO_BRANCH_ID`. `ablo dev` writes both
|
|
35
|
+
immutable coordinates beside its branch-bound key, and `ready()` compares them
|
|
36
|
+
with the key's server-resolved target before opening the sync connection. A mail
|
|
37
|
+
deployment carrying a slides key now fails with `project_scope_denied`; a
|
|
38
|
+
same-project key for the wrong environment fails with `branch_scope_denied`.
|
|
39
|
+
|
|
5
40
|
### Pre-existing rows arrive on their own
|
|
6
41
|
|
|
7
42
|
Connecting a database that already holds data no longer leaves those rows
|
|
@@ -13,6 +48,19 @@ snapshot completes, and `ablo status --json` exposes the progress as
|
|
|
13
48
|
`complete`. Row-touch backfill scripts are unnecessary; the snapshot is the
|
|
14
49
|
engine's job.
|
|
15
50
|
|
|
51
|
+
The snapshot reader now detects row-level security that would filter its
|
|
52
|
+
ordinary `SELECT` even though WAL carries every published row. New setup plans
|
|
53
|
+
give the read-only replication role `BYPASSRLS` (with `SELECT` still restricted
|
|
54
|
+
to published tables), and readiness refuses to call an RLS-filtered snapshot
|
|
55
|
+
safe. Existing connections can run `ablo connect resnapshot` after repairing
|
|
56
|
+
the role; it recreates only the slot and keeps the DataSource and credentials.
|
|
57
|
+
The same completion guard rejects an empty or partial snapshot mapping: if a
|
|
58
|
+
pushed model's table is absent from the publication, completion stays pending
|
|
59
|
+
until coverage is repaired and a fresh snapshot is requested. Snapshot and WAL
|
|
60
|
+
mapping now use the DataSource's configured Postgres schema as part of relation
|
|
61
|
+
identity, so an identically named table in another schema cannot be loaded into
|
|
62
|
+
the model or falsely satisfy publication coverage.
|
|
63
|
+
|
|
16
64
|
## 0.45.0
|
|
17
65
|
|
|
18
66
|
### Claim admission is authoritative
|
package/LICENSE
CHANGED
|
@@ -186,7 +186,7 @@
|
|
|
186
186
|
same "printed page" as the copyright notice for easier
|
|
187
187
|
identification within third-party archives.
|
|
188
188
|
|
|
189
|
-
Copyright 2025-2026
|
|
189
|
+
Copyright 2025-2026 Lukas Andersson
|
|
190
190
|
|
|
191
191
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
192
|
you may not use this file except in compliance with the License.
|
package/NOTICE
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
@ablo/ablo
|
|
2
|
-
Copyright 2025-2026
|
|
2
|
+
Copyright 2025-2026 Lukas Andersson
|
|
3
3
|
|
|
4
|
-
This product includes software developed by
|
|
4
|
+
This product includes software developed by Lukas Andersson
|
|
5
5
|
(https://ablo.finance).
|
|
6
6
|
|
|
7
|
-
"Ablo" is a trademark of
|
|
7
|
+
"Ablo" is a trademark of Lukas Andersson. This license does not grant
|
|
8
8
|
permission to use the Ablo name, logo, or trademarks. Third parties
|
|
9
9
|
may describe their use of or compatibility with Ablo factually (e.g.,
|
|
10
10
|
"built with @ablo/ablo") but may not use the Ablo name in a way
|
package/README.md
CHANGED
|
@@ -63,7 +63,6 @@ if (!order) throw new Error('Order not found');
|
|
|
63
63
|
await ablo.orders.update({
|
|
64
64
|
id: order.id,
|
|
65
65
|
data: { status: 'approved' },
|
|
66
|
-
wait: 'confirmed',
|
|
67
66
|
});
|
|
68
67
|
```
|
|
69
68
|
|
|
@@ -82,7 +81,6 @@ await ablo.orders.update({
|
|
|
82
81
|
id: claim.data.id,
|
|
83
82
|
data: { total: priced.total, status: 'repriced' },
|
|
84
83
|
claim,
|
|
85
|
-
wait: 'confirmed',
|
|
86
84
|
});
|
|
87
85
|
```
|
|
88
86
|
|
package/docs/agent-messaging.md
CHANGED
package/docs/agents.md
CHANGED
|
@@ -147,7 +147,7 @@ default caller here, not a bolt-on.
|
|
|
147
147
|
|
|
148
148
|
```text
|
|
149
149
|
something happens ──▶ your agent (HTTP, no socket)
|
|
150
|
-
(a job, a webhook, read context (list/
|
|
150
|
+
(a job, a webhook, read context (list/get)
|
|
151
151
|
a queue message) claim → work → commit
|
|
152
152
|
done — no held connection
|
|
153
153
|
```
|
package/docs/api.md
CHANGED
|
@@ -41,7 +41,7 @@ await ablo.ready();
|
|
|
41
41
|
const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
|
|
42
42
|
if (!report) throw new Error('Row not found');
|
|
43
43
|
|
|
44
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }
|
|
44
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
|
|
45
45
|
```
|
|
46
46
|
|
|
47
47
|
For end-to-end app setup across React, existing backends, Data Source, and
|
|
@@ -75,7 +75,7 @@ fallback removed — nothing to await, so they return a value.
|
|
|
75
75
|
| `update({ id, data, ...options })` | `Promise<T>` | You want to update through the schema model. |
|
|
76
76
|
| `delete({ id, ...options })` | `Promise<void>` | You want to delete through the schema model. |
|
|
77
77
|
|
|
78
|
-
`
|
|
78
|
+
`get`, `list`, `create`, `update`, and `delete` are the main path — they go
|
|
79
79
|
through the server. The `local` reads work off the rows a session has already
|
|
80
80
|
synced, so a cheap re-read needs no round-trip.
|
|
81
81
|
|
|
@@ -91,17 +91,18 @@ await ablo.weatherReports.update({
|
|
|
91
91
|
data: { status: 'ready' },
|
|
92
92
|
readAt: snap.stamp,
|
|
93
93
|
onStale: 'reject',
|
|
94
|
-
wait: 'confirmed',
|
|
95
94
|
});
|
|
96
95
|
```
|
|
97
96
|
|
|
97
|
+
Reactive local state changes optimistically at call time; awaiting the model
|
|
98
|
+
write waits for authoritative confirmation.
|
|
99
|
+
|
|
98
100
|
Protected write options:
|
|
99
101
|
|
|
100
102
|
| Option | Purpose |
|
|
101
103
|
|---|---|
|
|
102
104
|
| `readAt` | The state cursor the write was based on. |
|
|
103
105
|
| `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
|
|
104
|
-
| `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
|
|
105
106
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
106
107
|
| `timeout` | Maximum time to wait for the write call. |
|
|
107
108
|
|
|
@@ -113,7 +114,7 @@ row, `claim` waits for them, re-reads the fresh row, then hands it to you — so
|
|
|
113
114
|
two writers serialize instead of clobbering. A claim is temporary: it expires
|
|
114
115
|
on its own if the holder stops, and is never saved as a row.
|
|
115
116
|
|
|
116
|
-
You coordinate a row with calls on its model, beside `create`/`update`/`
|
|
117
|
+
You coordinate a row with calls on its model, beside `create`/`update`/`get`:
|
|
117
118
|
`ablo.<model>.claim({ id })` takes the claim and returns a handle,
|
|
118
119
|
`ablo.<model>.claim.state({ id })` reads who currently holds it (synchronous, never
|
|
119
120
|
blocks), and `ablo.<model>.claim.release({ id })` releases it early. The full
|
package/docs/cli.md
CHANGED
|
@@ -145,9 +145,9 @@ npx ablo docs --json # the page list, machine-readable
|
|
|
145
145
|
|
|
146
146
|
These pages ship inside the npm package, so they describe the code beside them
|
|
147
147
|
and stay reachable with no network — isolated agent environments and CI runners
|
|
148
|
-
often have none. That matters most when a project is pinned: `
|
|
149
|
-
|
|
150
|
-
the newest release, so an agent on an earlier version reads the new
|
|
148
|
+
often have none. That matters most when a project is pinned: `claim` took a
|
|
149
|
+
callback before it returned a disposable handle, and a website always describes
|
|
150
|
+
the newest release, so an agent on an earlier version reads the new shape and
|
|
151
151
|
writes a call its own package doesn't have.
|
|
152
152
|
|
|
153
153
|
Pass a slug (`coordination`), a path (`docs/coordination.md`), or a file name
|
|
@@ -330,5 +330,5 @@ migration can't leave clients gated against tables that don't match.
|
|
|
330
330
|
| ------------------------------------- | ------------------------------------------------------------------------ | -------------------------- |
|
|
331
331
|
| `ABLO_API_KEY` | Authenticate without `ablo login` (CI). Always overrides the stored key. |: |
|
|
332
332
|
| `ABLO_API_URL` | Control-plane / API host (`push`, `dev`, `status`). | `https://api.abloatai.com` |
|
|
333
|
-
| `ABLO_AUTH_URL` | Dashboard origin for `ablo login`'s device flow. | `https://abloatai.com`
|
|
333
|
+
| `ABLO_AUTH_URL` | Dashboard origin for `ablo login`'s device flow. | `https://www.abloatai.com` |
|
|
334
334
|
| `ABLO_CONFIG_DIR` / `XDG_CONFIG_HOME` | Where the credential file lives. | `~/.config/ablo` |
|
package/docs/client-behavior.md
CHANGED
|
@@ -57,11 +57,15 @@ const report = await ablo.weatherReports.get({ id: 'report_stockholm' });
|
|
|
57
57
|
const local = ablo.weatherReports.local.get('report_stockholm');
|
|
58
58
|
|
|
59
59
|
await ablo.weatherReports.create({ data: { location: 'Stockholm', status: 'pending' } });
|
|
60
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }
|
|
61
|
-
await ablo.weatherReports.delete({ id: 'report_stockholm'
|
|
60
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
|
|
61
|
+
await ablo.weatherReports.delete({ id: 'report_stockholm' });
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
-
|
|
64
|
+
On the reactive client, each model write changes local state optimistically
|
|
65
|
+
before the call returns. Its promise always waits for authoritative
|
|
66
|
+
confirmation, so `await update(...)` is the confirmation barrier.
|
|
67
|
+
|
|
68
|
+
Call `get`/`list` first — they fetch from the server and you `await` them.
|
|
65
69
|
After that, `local.get`/`local.list`/`local.count` read the already-synced data instantly with
|
|
66
70
|
no `await`, and stay reactive in render. Use the async pair to load, the sync trio
|
|
67
71
|
to read.
|
|
@@ -87,7 +91,6 @@ await ablo.weatherReports.update({
|
|
|
87
91
|
data: patch,
|
|
88
92
|
readAt: snap.stamp,
|
|
89
93
|
onStale: 'reject',
|
|
90
|
-
wait: 'confirmed',
|
|
91
94
|
});
|
|
92
95
|
```
|
|
93
96
|
|
|
@@ -109,7 +112,6 @@ it for reads, but it bypasses claims and ordering.
|
|
|
109
112
|
await ablo.weatherReports.update({
|
|
110
113
|
id: 'report_stockholm',
|
|
111
114
|
data: { status: 'ready' },
|
|
112
|
-
wait: 'confirmed',
|
|
113
115
|
readAt: snap.stamp,
|
|
114
116
|
onStale: 'reject',
|
|
115
117
|
idempotencyKey: 'report_stockholm:mark-ready:v1',
|
|
@@ -118,7 +120,6 @@ await ablo.weatherReports.update({
|
|
|
118
120
|
|
|
119
121
|
| Option | Purpose |
|
|
120
122
|
|---|---|
|
|
121
|
-
| `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
|
|
122
123
|
| `readAt` | State cursor the write was based on. |
|
|
123
124
|
| `onStale` | Policy when the target changed after `readAt`. Prefer `reject`. |
|
|
124
125
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
@@ -180,7 +181,7 @@ All SDK errors extend `AbloError` and carry a stable `type`.
|
|
|
180
181
|
import { AbloClaimedError } from '@abloatai/ablo';
|
|
181
182
|
|
|
182
183
|
try {
|
|
183
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }
|
|
184
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
|
|
184
185
|
} catch (error) {
|
|
185
186
|
if (error instanceof AbloClaimedError) {
|
|
186
187
|
return { status: 'claimed' };
|
package/docs/coordination.md
CHANGED
|
@@ -313,7 +313,7 @@ four axes. `claim({ id })` alone is a complete call; each axis is opt-in.
|
|
|
313
313
|
|
|
314
314
|
| name | type | required | description |
|
|
315
315
|
|---|---|---|---|
|
|
316
|
-
| `id` | `string` | yes | The row id: same id as `
|
|
316
|
+
| `id` | `string` | yes | The row id: same id as `get` / `update`. |
|
|
317
317
|
| `options.fields` | field selector | no | Claim fields declared by the model's Zod schema instead of the whole row: `fields: (task) => task.status`, or `fields: (task) => [task.status, task.title]` for several. The model supplies its own fields, so autocomplete is exact, a typo does not compile, and a schema rename is a compile error at every use. Two sets conflict where they intersect, so holders of disjoint fields do not wait for each other; see [claiming part of a row](#claiming-part-of-a-row). |
|
|
318
318
|
|
|
319
319
|
*What others see* — the presence half:
|
|
@@ -510,7 +510,7 @@ summary into the same snapshot immediately.
|
|
|
510
510
|
|
|
511
511
|
**You don't subscribe to anything first.** Reading or claiming a row
|
|
512
512
|
automatically enrolls you in that row's sync group: reading it (including
|
|
513
|
-
`
|
|
513
|
+
`get`, or `claim.state` itself) gives you **read-interest**, and
|
|
514
514
|
`claim`-ing it gives you a **pinned write-intent**. So `claim.state({ id })`
|
|
515
515
|
observes co-participants on that row from **any** client — a browser, a Server
|
|
516
516
|
Action, or a Node agent — and a holder sees its own claim, with no manual
|
package/docs/data-sources.md
CHANGED
|
@@ -36,6 +36,36 @@ npx ablo connect apply --env-file .env.local --yes
|
|
|
36
36
|
The explicit flag makes the credential choice visible and loads both the
|
|
37
37
|
branch-bound key and database URL. Shell environment variables take precedence.
|
|
38
38
|
|
|
39
|
+
### One database, several projects
|
|
40
|
+
|
|
41
|
+
Provider database URLs and Postgres schemas solve different isolation jobs:
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
database URL = production, staging, or preview environment
|
|
45
|
+
schema = application/project inside that database
|
|
46
|
+
ABLO_API_KEY = exact Ablo project branch to bind
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
It is safe to keep several apps in one production database when each app has its
|
|
50
|
+
own schema:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
ABLO_API_KEY="$MAIL_KEY" DATABASE_URL="$PRODUCTION_URL" \
|
|
54
|
+
npx ablo connect apply --schema mail --yes
|
|
55
|
+
|
|
56
|
+
ABLO_API_KEY="$SLIDES_KEY" DATABASE_URL="$PRODUCTION_URL" \
|
|
57
|
+
npx ablo connect apply --schema slides --yes
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
For a Neon or Supabase preview branch, use that branch's direct URL and keep the
|
|
61
|
+
schema name stable. Ablo binds one plane to `(database, schema)`: the same
|
|
62
|
+
database may add `billing`, but a second project cannot also claim `mail`.
|
|
63
|
+
Cross-organization conflicts reveal only that the binding is occupied.
|
|
64
|
+
|
|
65
|
+
Push the Ablo schema before connecting, or pass `--tables`. The publication is
|
|
66
|
+
an explicit list of schema-qualified mapped tables; Ablo never uses a
|
|
67
|
+
database-wide `FOR ALL TABLES` publication for this multi-project path.
|
|
68
|
+
|
|
39
69
|
If scoped roles already exist but their passwords are unavailable, do not drop
|
|
40
70
|
them or run `DROP OWNED`. Rotate them in place and re-register the fresh
|
|
41
71
|
credentials:
|
|
@@ -60,11 +90,12 @@ without printing the secret. Retire the old variable after the move.
|
|
|
60
90
|
## Connect in one command
|
|
61
91
|
|
|
62
92
|
```bash
|
|
63
|
-
npx ablo connect apply --url postgres://admin:...@host:5432/db
|
|
93
|
+
npx ablo connect apply --url postgres://admin:...@host:5432/db --schema mail
|
|
64
94
|
```
|
|
65
95
|
|
|
66
|
-
Pass an admin connection string with `--url` and
|
|
67
|
-
|
|
96
|
+
Pass an admin connection string with `--url` and select the application namespace
|
|
97
|
+
with `--schema` (default `public`). It creates a per-binding publication, two
|
|
98
|
+
per-binding scoped roles, and the grants, turns on logical decoding where it can, registers
|
|
68
99
|
both scoped roles with Ablo, and proves the setup by reconnecting and reading back.
|
|
69
100
|
The admin credential is used on this machine only and never persisted — nothing is
|
|
70
101
|
written to your `.env`, which keeps holding only `ABLO_API_KEY`. Pass `--show-sql`
|
|
@@ -78,7 +109,7 @@ to run it by hand or review exactly what changes.
|
|
|
78
109
|
|
|
79
110
|
When Ablo creates the replication slot, it takes a consistent initial snapshot of
|
|
80
111
|
every mapped table in the publication before following new changes. Rows that
|
|
81
|
-
predate `ablo connect` therefore become available to `
|
|
112
|
+
predate `ablo connect` therefore become available to `get`, `list`, and
|
|
82
113
|
reactive `local.*` reads without an application backfill.
|
|
83
114
|
|
|
84
115
|
Run `ablo connect check` before removing an existing HTTP/database read fallback.
|
|
@@ -87,6 +118,24 @@ write a script that updates every row to make it visible: an Ablo update require
|
|
|
87
118
|
the row to be visible already, and touching application rows is neither necessary
|
|
88
119
|
nor a safe bootstrap mechanism.
|
|
89
120
|
|
|
121
|
+
If a connection was snapshotted with an older replication role whose row-level
|
|
122
|
+
security hid historical rows, repair that role and request the load again without
|
|
123
|
+
deregistering or rotating credentials:
|
|
124
|
+
|
|
125
|
+
```bash
|
|
126
|
+
npx ablo connect rotate # reasserts BYPASSRLS and safely re-registers both roles
|
|
127
|
+
npx ablo connect resnapshot # recreates only the slot; the load is asynchronous
|
|
128
|
+
npx ablo connect check # repeat until the existing-row load is complete
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Use the same `resnapshot` step after adding an existing populated table to the
|
|
132
|
+
publication. Following its future WAL changes is not enough to load rows written
|
|
133
|
+
before publication membership; the snapshot coverage guard therefore refuses to
|
|
134
|
+
record completion when even one mapped table is absent. Relation matching is
|
|
135
|
+
schema-qualified using the DataSource's configured `schema` (default `public`):
|
|
136
|
+
an identically named table in another Postgres schema neither counts as coverage
|
|
137
|
+
nor enters the snapshot or WAL stream for your model.
|
|
138
|
+
|
|
90
139
|
## The setup, step by step
|
|
91
140
|
|
|
92
141
|
### 1. Enable logical decoding
|
|
@@ -112,24 +161,34 @@ npx ablo connect
|
|
|
112
161
|
`ablo connect` prints the exact, copy-pasteable setup SQL for **your** Postgres.
|
|
113
162
|
Run it against your database as a superuser or the DB owner. It creates:
|
|
114
163
|
|
|
115
|
-
- **A publication** naming the
|
|
116
|
-
|
|
164
|
+
- **A per-binding publication** naming only the schema-qualified mapped tables
|
|
165
|
+
Ablo reads and confirms against. Its suffix is derived from the authenticated
|
|
166
|
+
Ablo plane and is stable across re-runs:
|
|
117
167
|
|
|
118
168
|
```sql
|
|
119
|
-
CREATE PUBLICATION "
|
|
169
|
+
CREATE PUBLICATION "ablo_publication_<suffix>"
|
|
170
|
+
FOR TABLE "mail"."messages", "mail"."threads";
|
|
120
171
|
```
|
|
121
172
|
|
|
122
|
-
|
|
173
|
+
Override the pushed model set with `--tables a,b,c`.
|
|
123
174
|
|
|
124
175
|
- **A replication role:** it streams the WAL and `SELECT`s, nothing more. This is
|
|
125
176
|
the role Ablo reads and confirms through. You choose the password; it never
|
|
126
177
|
passes through Ablo's CLI or servers:
|
|
127
178
|
|
|
128
179
|
```sql
|
|
129
|
-
CREATE ROLE "
|
|
130
|
-
|
|
180
|
+
CREATE ROLE "ablo_replicator_<suffix>" WITH
|
|
181
|
+
NOSUPERUSER BYPASSRLS NOCREATEDB NOCREATEROLE REPLICATION NOINHERIT
|
|
182
|
+
LOGIN PASSWORD '<password>';
|
|
183
|
+
GRANT SELECT ON TABLE "mail"."messages", "mail"."threads"
|
|
184
|
+
TO "ablo_replicator_<suffix>";
|
|
131
185
|
```
|
|
132
186
|
|
|
187
|
+
`BYPASSRLS` is required because the initial load is an ordinary `SELECT`,
|
|
188
|
+
while logical replication already exposes every row in the publication
|
|
189
|
+
independently of row-level-security policies. Keep this role's `SELECT`
|
|
190
|
+
grants scoped to the published tables; it has no write or DDL privileges.
|
|
191
|
+
|
|
133
192
|
On Amazon RDS the `REPLICATION` attribute is granted, not set directly:
|
|
134
193
|
`GRANT rds_replication TO "ablo_replicator";`.
|
|
135
194
|
|
|
@@ -139,15 +198,23 @@ Run it against your database as a superuser or the DB owner. It creates:
|
|
|
139
198
|
can change rows in your tables; it cannot change your database:
|
|
140
199
|
|
|
141
200
|
```sql
|
|
142
|
-
CREATE ROLE "
|
|
201
|
+
CREATE ROLE "ablo_writer_<suffix>" WITH LOGIN PASSWORD '<write-password>'
|
|
143
202
|
NOSUPERUSER NOBYPASSRLS NOCREATEDB NOCREATEROLE NOREPLICATION NOINHERIT;
|
|
144
|
-
GRANT SELECT, INSERT, UPDATE, DELETE
|
|
203
|
+
GRANT SELECT, INSERT, UPDATE, DELETE
|
|
204
|
+
ON TABLE "mail"."messages", "mail"."threads"
|
|
205
|
+
TO "ablo_writer_<suffix>";
|
|
145
206
|
```
|
|
146
207
|
|
|
147
208
|
Rename either role with `--role <name>` / `--write-role <name>`.
|
|
148
209
|
|
|
149
|
-
The
|
|
150
|
-
|
|
210
|
+
The schema-local `ablo_idempotency` ledger lives beside that app's tables. The
|
|
211
|
+
**replication slot** (`ablo_slot_<suffix>`) is created and owned by Ablo's runtime
|
|
212
|
+
when it first subscribes — you don't pre-create it. Registration checks
|
|
213
|
+
`max_replication_slots` first and explains how to free or add capacity.
|
|
214
|
+
|
|
215
|
+
`ablo connect --manual` retains the legacy canonical object names for
|
|
216
|
+
compatibility and is therefore single-binding within a physical database. Use
|
|
217
|
+
`connect apply --schema …` when several projects share that database.
|
|
151
218
|
|
|
152
219
|
### 3. Register the database with Ablo
|
|
153
220
|
|
|
@@ -197,6 +264,8 @@ Your **app** holds only the API key — never a connection string:
|
|
|
197
264
|
```bash
|
|
198
265
|
# .env — server runtime only, never the browser
|
|
199
266
|
ABLO_API_KEY=sk_...
|
|
267
|
+
ABLO_PROJECT_ID=proj_...
|
|
268
|
+
ABLO_BRANCH_ID=br_...
|
|
200
269
|
```
|
|
201
270
|
|
|
202
271
|
```ts
|
|
@@ -206,9 +275,18 @@ import { schema } from './ablo/schema';
|
|
|
206
275
|
export const ablo = Ablo({
|
|
207
276
|
schema,
|
|
208
277
|
apiKey: process.env.ABLO_API_KEY,
|
|
278
|
+
projectId: process.env.ABLO_PROJECT_ID,
|
|
279
|
+
branchId: process.env.ABLO_BRANCH_ID,
|
|
209
280
|
});
|
|
210
281
|
```
|
|
211
282
|
|
|
283
|
+
`ABLO_PROJECT_ID` and `ABLO_BRANCH_ID` are safety assertions, not routing inputs.
|
|
284
|
+
The API key still selects the project and branch; during `ready()` Ablo asks the
|
|
285
|
+
server what the key actually targets and refuses startup when either coordinate
|
|
286
|
+
differs. `ablo dev` writes all three values together, so accidentally exporting
|
|
287
|
+
a slides key into the mail app—or a mail development key into production—fails
|
|
288
|
+
before any read, write, or subscription begins.
|
|
289
|
+
|
|
212
290
|
The Ablo schema describes **only your synced, collaborative models** — the rows
|
|
213
291
|
Ablo coordinates and fans out in realtime. It is _not_ your whole-database schema
|
|
214
292
|
and does _not_ replace your `schema.prisma` (or Drizzle schema). Your auth,
|
|
@@ -227,7 +305,7 @@ landed:
|
|
|
227
305
|
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
|
|
228
306
|
|
|
229
307
|
// Block until your database has it and the WAL echo confirms.
|
|
230
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 }
|
|
308
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { high: 21 } });
|
|
231
309
|
|
|
232
310
|
// Reads are live off the same stream.
|
|
233
311
|
const report = ablo.weatherReports.local.get('report_stockholm');
|
package/docs/debugging.md
CHANGED
|
@@ -267,7 +267,6 @@ import { AbloError } from '@abloatai/ablo';
|
|
|
267
267
|
try {
|
|
268
268
|
await ablo.documents.create({
|
|
269
269
|
data,
|
|
270
|
-
wait: 'confirmed',
|
|
271
270
|
});
|
|
272
271
|
} catch (error) {
|
|
273
272
|
if (error instanceof AbloError) {
|
|
@@ -276,10 +275,10 @@ try {
|
|
|
276
275
|
}
|
|
277
276
|
```
|
|
278
277
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
278
|
+
An awaited model write rejects with that complete typed error.
|
|
279
|
+
`onMutationFailure` remains the notification channel for deliberately
|
|
280
|
+
unawaited optimistic writes; it is not required to recover details from an
|
|
281
|
+
awaited write.
|
|
283
282
|
|
|
284
283
|
### Local reads versus a confirmed server read
|
|
285
284
|
|
|
@@ -75,7 +75,6 @@ export async function markDone(taskId: string) {
|
|
|
75
75
|
// ablo.tasks.update({
|
|
76
76
|
// id: claim.data.id,
|
|
77
77
|
// data: { status: 'done' },
|
|
78
|
-
// wait: 'confirmed',
|
|
79
78
|
// readAt: <claim snapshot version>,
|
|
80
79
|
// onStale: 'reject',
|
|
81
80
|
// });
|
|
@@ -86,7 +85,6 @@ export async function markDone(taskId: string) {
|
|
|
86
85
|
const updated = await ablo.tasks.update({
|
|
87
86
|
id: claim.data.id,
|
|
88
87
|
data: { status: 'done' },
|
|
89
|
-
wait: 'confirmed',
|
|
90
88
|
});
|
|
91
89
|
|
|
92
90
|
return { status: 'done', task: updated };
|
|
@@ -255,7 +255,6 @@ await ablo.weatherReports.update({
|
|
|
255
255
|
data: { status: 'ready' },
|
|
256
256
|
readAt: snap.stamp,
|
|
257
257
|
onStale: 'reject',
|
|
258
|
-
wait: 'confirmed',
|
|
259
258
|
});
|
|
260
259
|
```
|
|
261
260
|
|
|
@@ -294,7 +293,6 @@ await ablo.weatherReports.update({
|
|
|
294
293
|
data: { status: 'ready' },
|
|
295
294
|
readAt: snap.stamp,
|
|
296
295
|
onStale: 'reject',
|
|
297
|
-
wait: 'confirmed',
|
|
298
296
|
});
|
|
299
297
|
```
|
|
300
298
|
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -23,7 +23,7 @@ app/
|
|
|
23
23
|
route.ts # mints a per-user ek_ token for the browser
|
|
24
24
|
tasks/
|
|
25
25
|
[id]/
|
|
26
|
-
page.tsx # RSC:
|
|
26
|
+
page.tsx # RSC: get + render
|
|
27
27
|
actions.ts # Server Action: claim, then write
|
|
28
28
|
TaskEditor.tsx # Client: live updates
|
|
29
29
|
lib/
|
|
@@ -168,7 +168,6 @@ export async function markDone(id: string) {
|
|
|
168
168
|
id,
|
|
169
169
|
data: { status: 'done' },
|
|
170
170
|
claim,
|
|
171
|
-
wait: 'confirmed',
|
|
172
171
|
});
|
|
173
172
|
|
|
174
173
|
return { status: 'done', task };
|
|
@@ -69,7 +69,6 @@ export async function completeTask(taskId: string, workerId: string) {
|
|
|
69
69
|
const updated = await ablo.tasks.update({
|
|
70
70
|
id: claim.data.id,
|
|
71
71
|
data: { status: 'done' },
|
|
72
|
-
wait: 'confirmed',
|
|
73
72
|
});
|
|
74
73
|
|
|
75
74
|
return { status: 'done', task: updated };
|
|
@@ -79,8 +78,8 @@ export async function completeTask(taskId: string, workerId: string) {
|
|
|
79
78
|
|
|
80
79
|
`get({ id })` is an async server read — it hits the server and returns the
|
|
81
80
|
row (or `undefined`, which the early `not_found` guard handles). The update runs
|
|
82
|
-
while the claim is held
|
|
83
|
-
|
|
81
|
+
while the claim is held; awaiting it resolves only once your database has
|
|
82
|
+
confirmed the row landed.
|
|
84
83
|
|
|
85
84
|
The two options on the claim:
|
|
86
85
|
|
package/docs/groups.md
CHANGED
|
@@ -266,7 +266,7 @@ try {
|
|
|
266
266
|
await ablo.tasks.update({ id, data });
|
|
267
267
|
} catch (err) {
|
|
268
268
|
if (err.code === 'stale_context') {
|
|
269
|
-
const fresh = await ablo.documents.
|
|
269
|
+
const fresh = await ablo.documents.get({ id: 's-1' }); // read
|
|
270
270
|
await ablo.documents.track({ id: 's-1', onStale: 'reject' }); // acknowledge
|
|
271
271
|
await ablo.tasks.update({ id, data: reconsider(fresh) }); // now it lands
|
|
272
272
|
}
|
package/docs/guarantees.md
CHANGED
|
@@ -14,14 +14,13 @@ of clobbering.
|
|
|
14
14
|
|
|
15
15
|
## Confirmed Writes
|
|
16
16
|
|
|
17
|
-
|
|
18
|
-
the
|
|
17
|
+
Awaiting a schema model write resolves only after authoritative confirmation
|
|
18
|
+
and returns the updated row.
|
|
19
19
|
|
|
20
20
|
```ts
|
|
21
21
|
const updated = await ablo.weatherReports.update({
|
|
22
22
|
id: 'report_stockholm',
|
|
23
23
|
data: { status: 'ready' },
|
|
24
|
-
wait: 'confirmed',
|
|
25
24
|
});
|
|
26
25
|
```
|
|
27
26
|
|
|
@@ -37,9 +36,8 @@ Schema model writes return the updated model row.
|
|
|
37
36
|
Schema model writes update local state optimistically. This keeps UI and agent
|
|
38
37
|
tools responsive while the commit is sent to the server.
|
|
39
38
|
|
|
40
|
-
-
|
|
41
|
-
|
|
42
|
-
- With `wait: 'confirmed'`, the promise waits for server confirmation.
|
|
39
|
+
- The local model changes immediately, before the promise settles.
|
|
40
|
+
- The promise always waits for authoritative confirmation.
|
|
43
41
|
- If the server rejects the write, the SDK rolls back the optimistic change and
|
|
44
42
|
raises a typed error.
|
|
45
43
|
|
|
@@ -58,7 +56,6 @@ await ablo.weatherReports.update({
|
|
|
58
56
|
data: { status: 'ready' },
|
|
59
57
|
readAt: snap.stamp,
|
|
60
58
|
onStale: 'reject',
|
|
61
|
-
wait: 'confirmed',
|
|
62
59
|
});
|
|
63
60
|
```
|
|
64
61
|
|
package/docs/how-it-works.md
CHANGED
|
@@ -7,7 +7,7 @@ whole model — everything below explains what it means and how to use it.
|
|
|
7
7
|
|
|
8
8
|
```ts
|
|
9
9
|
// You call Ablo. Ablo lands the change in your database and confirms it.
|
|
10
|
-
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }
|
|
10
|
+
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
|
|
11
11
|
|
|
12
12
|
// Reads come back live, kept current from your database.
|
|
13
13
|
const task = ablo.tasks.local.get('task_42');
|
|
@@ -42,10 +42,10 @@ echo confirms it → everyone connected sees it live.**
|
|
|
42
42
|
| Primitive | Plane | Purpose |
|
|
43
43
|
|---|---|---|
|
|
44
44
|
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
45
|
-
| `Model` | State | The generated `ablo.<model>` model. Use `
|
|
45
|
+
| `Model` | State | The generated `ablo.<model>` model. Use `get`/`list` (async reads), `local.get`/`local.list`/`local.count` (the same verbs, synchronous and local-only), `create`, `update`, and `delete`. |
|
|
46
46
|
| `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim({ id })` and read via `ablo.<model>.claim.state({ id })`. Ephemeral, never persisted. |
|
|
47
47
|
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
48
|
-
| `Receipt` | Protocol | The lower-level durable result for custom runtimes.
|
|
48
|
+
| `Receipt` | Protocol | The lower-level durable result for custom runtimes. Awaiting a schema write waits for confirmation. |
|
|
49
49
|
|
|
50
50
|
### Why each primitive is separate
|
|
51
51
|
|
|
@@ -102,9 +102,9 @@ import Ablo from '@abloatai/ablo';
|
|
|
102
102
|
import { schema } from './ablo/schema';
|
|
103
103
|
export const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
104
104
|
|
|
105
|
-
// 5. Write through Ablo.
|
|
106
|
-
//
|
|
107
|
-
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' }
|
|
105
|
+
// 5. Write through Ablo. Local state changes immediately; await blocks until
|
|
106
|
+
// the authoritative feed proves the row is there.
|
|
107
|
+
await ablo.tasks.update({ id: 'task_42', data: { status: 'done' } });
|
|
108
108
|
|
|
109
109
|
// 6. Read — live, no fetch loop.
|
|
110
110
|
const task = ablo.tasks.local.get('task_42');
|
package/docs/idempotency.md
CHANGED
package/docs/identity.md
CHANGED
|
@@ -517,7 +517,7 @@ an agent pointed at the entities it's working on. You **never hand-write**
|
|
|
517
517
|
const ablo = Ablo({ schema, apiKey: session.token });
|
|
518
518
|
```
|
|
519
519
|
|
|
520
|
-
2. **Automatically, on read or claim.** Reading a row (`
|
|
520
|
+
2. **Automatically, on read or claim.** Reading a row (`get`/
|
|
521
521
|
`claim.state`) auto-enrolls the client in that row's entity group
|
|
522
522
|
(**read-interest**), and `claim`-ing it pins a **write-intent** subscription.
|
|
523
523
|
So an agent's reachable set **accretes** as it works — no extra subscribe call.
|
package/docs/index.md
CHANGED
|
@@ -14,7 +14,6 @@ await using claim = await ablo.reports.claim({ id: reportId });
|
|
|
14
14
|
await ablo.reports.update({
|
|
15
15
|
id: claim.data.id,
|
|
16
16
|
data: { forecast: await generateForecast(claim.data) },
|
|
17
|
-
wait: 'confirmed',
|
|
18
17
|
});
|
|
19
18
|
```
|
|
20
19
|
|
|
@@ -90,7 +89,7 @@ based on a row that has since changed is turned away rather than applied.
|
|
|
90
89
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY, transport: 'http' });
|
|
91
90
|
```
|
|
92
91
|
|
|
93
|
-
Read with `list` / `
|
|
92
|
+
Read with `list` / `get`, coordinate with `claim`, write with `create` / `update` /
|
|
94
93
|
`delete`. See [Agents](./agents.md) for the loop and [API Reference](./api.md) for the shape.
|
|
95
94
|
</Step>
|
|
96
95
|
|
|
@@ -77,8 +77,7 @@ When handing this to a coding agent, give it a concrete target:
|
|
|
77
77
|
```txt
|
|
78
78
|
Add Ablo to this app for one model your agents edit.
|
|
79
79
|
Run npx ablo dev and use its branch-bound key. Declare schema, add the Ablo client, replace
|
|
80
|
-
one write with ablo.<model>.update(..., { readAt, onStale: 'reject',
|
|
81
|
-
wait: 'confirmed' }), and add a smoke test for two concurrent writers.
|
|
80
|
+
one write with ablo.<model>.update(..., { readAt, onStale: 'reject' }), and add a smoke test for two concurrent writers.
|
|
82
81
|
```
|
|
83
82
|
|
|
84
83
|
## 1. Declare A Schema
|
|
@@ -261,9 +260,9 @@ Reads come in two flavors, and you pick based on whether you can wait.
|
|
|
261
260
|
store) — they're async, so you `await` them. `local.get(id)`,
|
|
262
261
|
`local.list({ where })`, and `local.count({ where })` read the already-synced local
|
|
263
262
|
graph synchronously, so they're the ones you call in render — and the ones you
|
|
264
|
-
use inside a `useAblo` selector, never the async `
|
|
263
|
+
use inside a `useAblo` selector, never the async `get`/`list`.
|
|
265
264
|
|
|
266
|
-
Use `
|
|
265
|
+
Use `get` when the row may not be local yet — it fetches from the server
|
|
267
266
|
and waits.
|
|
268
267
|
|
|
269
268
|
```ts
|
|
@@ -316,7 +315,7 @@ const ablo = useAblo();
|
|
|
316
315
|
For simple writes:
|
|
317
316
|
|
|
318
317
|
```ts
|
|
319
|
-
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }
|
|
318
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' } });
|
|
320
319
|
```
|
|
321
320
|
|
|
322
321
|
For writes based on state the user or agent already read, snapshot first and
|
|
@@ -330,12 +329,12 @@ await ablo.weatherReports.update({
|
|
|
330
329
|
data: { status: 'ready' },
|
|
331
330
|
readAt: snap.stamp,
|
|
332
331
|
onStale: 'reject',
|
|
333
|
-
wait: 'confirmed',
|
|
334
332
|
});
|
|
335
333
|
```
|
|
336
334
|
|
|
337
|
-
|
|
338
|
-
|
|
335
|
+
The local row changes optimistically at once. Awaiting the model write waits for
|
|
336
|
+
authoritative confirmation; a rejection rolls the optimistic state back and
|
|
337
|
+
throws a typed `AbloError`.
|
|
339
338
|
|
|
340
339
|
## 5. Multiplayer Is Automatic
|
|
341
340
|
|
|
@@ -468,7 +467,6 @@ if (!claimed) return;
|
|
|
468
467
|
await ablo.weatherReports.update({
|
|
469
468
|
id: claimed.id,
|
|
470
469
|
data: { status: 'ready', forecast: await getForecast(claimed) },
|
|
471
|
-
wait: 'confirmed',
|
|
472
470
|
});
|
|
473
471
|
```
|
|
474
472
|
|
|
@@ -489,7 +487,6 @@ const completeReport = tool({
|
|
|
489
487
|
data: { status: 'ready', forecast },
|
|
490
488
|
readAt: snap.stamp,
|
|
491
489
|
onStale: 'reject',
|
|
492
|
-
wait: 'confirmed',
|
|
493
490
|
});
|
|
494
491
|
},
|
|
495
492
|
});
|
|
@@ -58,7 +58,6 @@ export const approveOrder = inngest.createFunction(
|
|
|
58
58
|
id: orderId,
|
|
59
59
|
data: { status: 'approved', approvalNote },
|
|
60
60
|
idempotencyKey: `${operationId}:approve-order:${orderId}`,
|
|
61
|
-
wait: 'confirmed',
|
|
62
61
|
}),
|
|
63
62
|
);
|
|
64
63
|
},
|
|
@@ -99,7 +98,7 @@ For every mutating step:
|
|
|
99
98
|
1. Accept a stable business operation ID in the triggering event.
|
|
100
99
|
2. Derive a separate key for each logical effect.
|
|
101
100
|
3. Reuse the key and mutation body for every retry.
|
|
102
|
-
4.
|
|
101
|
+
4. Await every model write before a later step depends on its authoritative result.
|
|
103
102
|
5. Treat reuse of a key with another body as an application bug.
|
|
104
103
|
6. Keep the complete retry horizon within Ablo's documented
|
|
105
104
|
[idempotency retention window](../idempotency.md).
|
|
@@ -133,7 +132,6 @@ await step.run('apply-order-review', () =>
|
|
|
133
132
|
id: orderId,
|
|
134
133
|
data: { status: 'approved', approvalNote },
|
|
135
134
|
idempotencyKey: `${operationId}:apply-order-review:${orderId}`,
|
|
136
|
-
wait: 'confirmed',
|
|
137
135
|
}),
|
|
138
136
|
);
|
|
139
137
|
```
|
|
@@ -167,7 +165,6 @@ await step.run('apply-review-with-claim', async () => {
|
|
|
167
165
|
data: { status: 'approved', approvalNote },
|
|
168
166
|
claim,
|
|
169
167
|
idempotencyKey: `${operationId}:apply-review:${orderId}`,
|
|
170
|
-
wait: 'confirmed',
|
|
171
168
|
});
|
|
172
169
|
} finally {
|
|
173
170
|
await claim.release();
|
|
@@ -46,7 +46,6 @@ export async function approveOrder(input: ApproveOrderInput) {
|
|
|
46
46
|
approvalNote: input.approvalNote,
|
|
47
47
|
},
|
|
48
48
|
idempotencyKey: input.idempotencyKey,
|
|
49
|
-
wait: 'confirmed',
|
|
50
49
|
});
|
|
51
50
|
}
|
|
52
51
|
```
|
|
@@ -135,7 +134,7 @@ For every mutating Activity:
|
|
|
135
134
|
1. Create the key in deterministic Workflow code or accept a stable operation
|
|
136
135
|
ID in the Workflow input.
|
|
137
136
|
2. Reuse the key and mutation body for every retry of the same logical effect.
|
|
138
|
-
3.
|
|
137
|
+
3. Await each model write before later Workflow steps depend on the
|
|
139
138
|
authoritative database result.
|
|
140
139
|
4. Treat reuse of a key with a different body as an application bug.
|
|
141
140
|
5. Keep the Temporal retry horizon within Ablo's documented
|
|
@@ -165,7 +164,6 @@ export async function applyReview(orderId: string, note: string) {
|
|
|
165
164
|
id: claim.data.id,
|
|
166
165
|
data: { approvalNote: note },
|
|
167
166
|
claim,
|
|
168
|
-
wait: 'confirmed',
|
|
169
167
|
});
|
|
170
168
|
}
|
|
171
169
|
```
|
|
@@ -69,7 +69,7 @@ FNV-1a content hash used for connect-time gating. Round-trip tested in
|
|
|
69
69
|
error, createdBy, createdAt, activatedAt)`, unique `(orgId, version)`. State
|
|
70
70
|
`pending|validated|active|overwritten|failed`, ≤1 active per tenant (Convex
|
|
71
71
|
`_schemas` machine; Zero's "row in the operational DB"). *Migration written,
|
|
72
|
-
not applied — 0 users
|
|
72
|
+
not applied — 0 users at the time.*
|
|
73
73
|
- **`pgSchemaStore` / `memorySchemaStore`** (`schemaStore.ts`) — mirrors
|
|
74
74
|
`pgApiKeyStore`. `insertPending` assigns `MAX(version)+1`; `activate` is a
|
|
75
75
|
transaction that demotes the current active → `overwritten` then promotes the
|
package/docs/migration.md
CHANGED
|
@@ -293,7 +293,7 @@ helper, and the agent/task type family (`Agent`, `AgentOptions`,
|
|
|
293
293
|
+ const { token } = await server.sessions.create({ agent: { id: agentId } });
|
|
294
294
|
+ const agent = Ablo({ schema, apiKey: token });
|
|
295
295
|
+ await using claim = await agent.tasks.claim({ id });
|
|
296
|
-
+ await agent.tasks.update({ id, data: { status: 'done' }
|
|
296
|
+
+ await agent.tasks.update({ id, data: { status: 'done' } });
|
|
297
297
|
```
|
|
298
298
|
|
|
299
299
|
Per-run token/cost now lives in Langfuse, not an `agent_tasks` table. The only
|
|
@@ -324,10 +324,10 @@ modifier are named siblings. Reactive local reads stay on the synchronous
|
|
|
324
324
|
|
|
325
325
|
```diff
|
|
326
326
|
- await ablo.tasks.update(id, { status: 'done' }, { wait: 'confirmed' })
|
|
327
|
-
+ await ablo.tasks.update({ id, data: { status: 'done' }
|
|
327
|
+
+ await ablo.tasks.update({ id, data: { status: 'done' } })
|
|
328
328
|
|
|
329
329
|
- await ablo.tasks.retrieve(id)
|
|
330
|
-
+ await ablo.tasks.
|
|
330
|
+
+ await ablo.tasks.get({ id })
|
|
331
331
|
|
|
332
332
|
- useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask
|
|
333
333
|
+ useAblo((ablo) => ablo.tasks.local.get(id)) ?? serverTask
|
|
@@ -46,7 +46,7 @@ Sort any action you're about to take into one of these, and the right move
|
|
|
46
46
|
follows.
|
|
47
47
|
|
|
48
48
|
**Run freely — read-only or reversible.**
|
|
49
|
-
Reads (`
|
|
49
|
+
Reads (`get`, `list`), `ablo check`, and `ablo pull` observe and
|
|
50
50
|
never change anything. Previews — `--show-sql`, `--dry-run` — print the exact
|
|
51
51
|
SQL a command would run without executing it. Model writes through
|
|
52
52
|
`ablo.<model>.create` / `update` are claim-checked, optimistic, rolled back if
|
|
@@ -95,7 +95,7 @@ read-only checks:
|
|
|
95
95
|
Read-only, like `prisma db pull`.
|
|
96
96
|
- `--show-sql` / `--dry-run` on `connect` and `migrate` — the exact statements,
|
|
97
97
|
printed and unexecuted, so you approve the SQL before it runs.
|
|
98
|
-
- Read the row and its claim state before you write — `
|
|
98
|
+
- Read the row and its claim state before you write — `get` / `list`, and
|
|
99
99
|
`ablo.<model>.claim.state({ id })` for who is already working on it.
|
|
100
100
|
|
|
101
101
|
The pattern underneath all of it is steady: reads and model writes flow freely
|
package/docs/react.md
CHANGED
|
@@ -143,7 +143,7 @@ const reports = useAblo((ablo) =>
|
|
|
143
143
|
const report = await ablo.weatherReports.get({ id });
|
|
144
144
|
```
|
|
145
145
|
|
|
146
|
-
Use `
|
|
146
|
+
Use `get` in Server Components when the row may not be in the local pool
|
|
147
147
|
yet — it hydrates from the local store and the server, and returns a Promise, so
|
|
148
148
|
`await` it. (Server reads come in two shapes: `get({ id })` for one row and
|
|
149
149
|
`list({ where })` for many; both are async. The synchronous local reads are
|
|
@@ -162,7 +162,6 @@ await ablo.weatherReports.update({
|
|
|
162
162
|
data: patch,
|
|
163
163
|
readAt: snap.stamp,
|
|
164
164
|
onStale: 'reject',
|
|
165
|
-
wait: 'confirmed',
|
|
166
165
|
});
|
|
167
166
|
```
|
|
168
167
|
|
|
@@ -180,7 +179,6 @@ async function markReady() {
|
|
|
180
179
|
data: { status: 'ready' },
|
|
181
180
|
readAt: snap.stamp,
|
|
182
181
|
onStale: 'reject',
|
|
183
|
-
wait: 'confirmed',
|
|
184
182
|
});
|
|
185
183
|
}
|
|
186
184
|
```
|
package/docs/schema-contract.md
CHANGED
|
@@ -6,7 +6,7 @@ Ablo's schema is the integration contract. Define it once, pass it to `Ablo(...)
|
|
|
6
6
|
and every actor gets the same typed model surface:
|
|
7
7
|
|
|
8
8
|
```txt
|
|
9
|
-
defineSchema(...) -> ablo.<model>.create/
|
|
9
|
+
defineSchema(...) -> ablo.<model>.create/get/update/claim(...)
|
|
10
10
|
```
|
|
11
11
|
|
|
12
12
|
That one object drives:
|
|
@@ -84,7 +84,7 @@ const pending = ablo.weatherReports.local.list({ where: { status: 'pending' } })
|
|
|
84
84
|
Use model writes for every actor:
|
|
85
85
|
|
|
86
86
|
```ts
|
|
87
|
-
await ablo.weatherReports.update({ id: reportId, data: { status: 'ready' }
|
|
87
|
+
await ablo.weatherReports.update({ id: reportId, data: { status: 'ready' } });
|
|
88
88
|
```
|
|
89
89
|
|
|
90
90
|
## Coordination
|
package/examples/README.md
CHANGED
|
@@ -21,7 +21,7 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
|
21
21
|
Then:
|
|
22
22
|
|
|
23
23
|
- create with `ablo.weatherReports.create`
|
|
24
|
-
- read with `ablo.weatherReports.
|
|
24
|
+
- read with `ablo.weatherReports.get` or `ablo.weatherReports.list`
|
|
25
25
|
- coordinate long-running work with `ablo.weatherReports.claim`
|
|
26
26
|
- write with `ablo.weatherReports.update`
|
|
27
27
|
- dispose the client when the worker finishes
|
package/llms.txt
CHANGED
|
@@ -61,11 +61,10 @@ await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
|
61
61
|
const updated = await ablo.weatherReports.update({
|
|
62
62
|
id: claim.data.id,
|
|
63
63
|
data: { status: 'ready', forecast: await getForecast(claim.data) },
|
|
64
|
-
wait: 'confirmed',
|
|
65
64
|
});
|
|
66
65
|
```
|
|
67
66
|
|
|
68
|
-
That is the normal app path: declare models in a schema, then use `ablo.<model>.
|
|
67
|
+
That is the normal app path: declare models in a schema, then use `ablo.<model>.get({ id })`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`. Every verb takes a single options object.
|
|
69
68
|
|
|
70
69
|
Treat the schema as the integration contract. It drives typed model clients,
|
|
71
70
|
React selectors, server and agent writes, Data Source request/response shape,
|
|
@@ -137,7 +136,7 @@ frees. Use `ifClaimed: 'fail'` when you'd rather refuse to read a claimed row.
|
|
|
137
136
|
|
|
138
137
|
## Guarantees
|
|
139
138
|
|
|
140
|
-
|
|
139
|
+
Schema model writes update local state optimistically, while their returned promise always waits for authoritative confirmation. 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`.
|
|
141
140
|
|
|
142
141
|
Claims coordinate writers; they do not block readers. Most users should stay on
|
|
143
142
|
schema-backed reads/writes and `claim(...)`; manual protocol bookkeeping is not
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@abloatai/ablo",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.47.0",
|
|
4
4
|
"description": "The public Ablo SDK for coordinated reads, commits, claims, observation, and reactive applications.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -124,8 +124,8 @@
|
|
|
124
124
|
"directory": "packages/ablo"
|
|
125
125
|
},
|
|
126
126
|
"dependencies": {
|
|
127
|
-
"@abloatai/humans": "^0.
|
|
128
|
-
"@abloatai/transaction": "^0.
|
|
127
|
+
"@abloatai/humans": "^0.47.0",
|
|
128
|
+
"@abloatai/transaction": "^0.47.0"
|
|
129
129
|
},
|
|
130
130
|
"peerDependencies": {
|
|
131
131
|
"ai": "^6.0.0 || ^7.0.0",
|