@abloatai/ablo 0.6.0 → 0.7.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 +45 -0
- package/README.md +64 -35
- package/dist/BaseSyncedStore.d.ts +1 -1
- package/dist/BaseSyncedStore.js +1 -1
- package/dist/client/Ablo.d.ts +1 -0
- package/dist/client/Ablo.js +1 -0
- package/dist/client/createModelProxy.d.ts +26 -3
- package/dist/client/createModelProxy.js +4 -1
- package/dist/client/validateAbloOptions.js +2 -2
- package/dist/coordination/index.d.ts +6 -0
- package/dist/coordination/index.js +6 -0
- package/dist/coordination/schema.d.ts +329 -0
- package/dist/coordination/schema.js +209 -0
- package/dist/core/QueryView.d.ts +4 -1
- package/dist/core/QueryView.js +1 -1
- package/dist/core/query-utils.d.ts +7 -10
- package/dist/core/query-utils.js +2 -3
- package/dist/errorCodes.d.ts +264 -0
- package/dist/errorCodes.js +251 -0
- package/dist/errors.d.ts +51 -6
- package/dist/errors.js +56 -3
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -2
- package/dist/policy/index.d.ts +1 -1
- package/dist/policy/index.js +1 -1
- package/dist/policy/types.d.ts +31 -0
- package/dist/policy/types.js +15 -0
- package/dist/react/AbloProvider.d.ts +12 -0
- package/dist/react/AbloProvider.js +11 -3
- package/dist/schema/ddl.d.ts +62 -0
- package/dist/schema/ddl.js +317 -0
- package/dist/schema/diff.d.ts +6 -0
- package/dist/schema/diff.js +21 -3
- package/dist/schema/field.d.ts +16 -19
- package/dist/schema/field.js +30 -17
- package/dist/schema/index.d.ts +7 -4
- package/dist/schema/index.js +9 -3
- package/dist/schema/model.d.ts +87 -25
- package/dist/schema/model.js +33 -3
- package/dist/schema/relation.d.ts +17 -0
- package/dist/schema/roles.d.ts +148 -0
- package/dist/schema/roles.js +149 -0
- package/dist/schema/schema.d.ts +2 -112
- package/dist/schema/schema.js +50 -62
- package/dist/schema/select.d.ts +25 -0
- package/dist/schema/select.js +55 -0
- package/dist/schema/serialize.d.ts +13 -9
- package/dist/schema/serialize.js +14 -10
- package/dist/schema/sugar.d.ts +20 -3
- package/dist/schema/sugar.js +5 -1
- package/dist/schema/tenancy.d.ts +66 -0
- package/dist/schema/tenancy.js +58 -0
- package/dist/sync/HydrationCoordinator.d.ts +2 -0
- package/dist/sync/HydrationCoordinator.js +23 -17
- package/dist/sync/createIntentStream.d.ts +2 -1
- package/dist/sync/createIntentStream.js +46 -1
- package/dist/sync/participants.js +5 -14
- package/dist/types/streams.d.ts +53 -33
- package/docs/api-keys.md +44 -0
- package/docs/api.md +11 -22
- package/docs/cli.md +212 -0
- package/docs/client-behavior.md +1 -1
- package/docs/coordination.md +61 -12
- package/docs/data-sources.md +2 -2
- package/docs/examples/existing-python-backend.md +3 -3
- package/docs/examples/scoped-agent.md +78 -0
- package/docs/guarantees.md +5 -2
- package/docs/identity.md +139 -68
- package/docs/index.md +6 -0
- package/docs/integration-guide.md +31 -35
- package/docs/interaction-model.md +3 -0
- package/docs/react.md +3 -3
- package/docs/roadmap.md +14 -2
- package/package.json +8 -1
package/docs/cli.md
ADDED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# CLI
|
|
2
|
+
|
|
3
|
+
The `ablo` CLI gets you from an empty project to live-syncing data: scaffold a
|
|
4
|
+
schema, authenticate, push the schema, and watch it sync. Your
|
|
5
|
+
`defineSchema(...)` is the single source of truth — the CLI and the hosted
|
|
6
|
+
server lower it to **the same SQL** through one engine
|
|
7
|
+
(`generateProvisionPlan` / `generateMigrationPlan` in `@abloatai/ablo/schema`).
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx ablo init # scaffold ablo/schema.ts + client
|
|
11
|
+
npx ablo login # authorize in the browser
|
|
12
|
+
npx ablo dev # push schema to the test sandbox + watch
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Authenticate
|
|
16
|
+
|
|
17
|
+
`ablo login` runs the OAuth 2.0 device flow: it opens your browser, you choose
|
|
18
|
+
**log in** or **create an account** and approve, and the CLI provisions a
|
|
19
|
+
**test + live key pair** (90-day, restricted) and stores them locally. This
|
|
20
|
+
mirrors `stripe login`.
|
|
21
|
+
|
|
22
|
+
| Command | What it does |
|
|
23
|
+
| --- | --- |
|
|
24
|
+
| `ablo login` | Authorize in the browser; provisions + stores a test and a live key. |
|
|
25
|
+
| `ablo logout` | Remove the stored keys. |
|
|
26
|
+
| `ablo status` | Show the active org, mode, both keys (prefix + expiry), and server health. |
|
|
27
|
+
| `ablo mode [test\|live]` | Switch the active mode. With no argument, prompts. |
|
|
28
|
+
|
|
29
|
+
Keys are stored in `~/.config/ablo/config.json` (mode `0600`). In **CI**, don't
|
|
30
|
+
log in — set `ABLO_API_KEY`, which always overrides the stored key.
|
|
31
|
+
|
|
32
|
+
## Test vs live
|
|
33
|
+
|
|
34
|
+
Like Stripe, every account has a **test** mode and a **live** mode, and a key
|
|
35
|
+
belongs to one of them. Test keys are bound to an isolated sandbox: their reads
|
|
36
|
+
and writes never touch live data. Switch with `ablo mode`; `ablo dev` is always
|
|
37
|
+
test mode by design.
|
|
38
|
+
|
|
39
|
+
The schema, however, is **shared** across the org — pushing a schema (from
|
|
40
|
+
either mode) defines the same models test and live see; only the rows differ.
|
|
41
|
+
|
|
42
|
+
## Commands
|
|
43
|
+
|
|
44
|
+
| Command | What it does | Flags |
|
|
45
|
+
| --- | --- | --- |
|
|
46
|
+
| `ablo init` | Scaffold `ablo/` (`schema.ts`, client, optional Data Source / agent / component), write `.env`, install the SDK. Offers to log in at the end. | — |
|
|
47
|
+
| `ablo login` / `logout` / `status` | Authentication & status (above). | — |
|
|
48
|
+
| `ablo mode [test\|live]` | Switch active mode. | — |
|
|
49
|
+
| `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>` |
|
|
50
|
+
| `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` |
|
|
51
|
+
| `ablo schema 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` |
|
|
52
|
+
| `ablo pull` | **BYO** — generate `defineSchema(...)` from your existing tables (read-only, like `prisma db pull`). | `--out <path>`, `--app-schema <name>`, `--import <pkg>`, `--force` |
|
|
53
|
+
| `ablo check` | **BYO** — verify your *existing* tables fit the schema (read-only, no DDL). | `--schema <path>`, `--export <name>`, `--app-schema <name>` |
|
|
54
|
+
| `ablo generate` | Emit TypeScript types from the schema. | `--out <path>`, `--schema`, `--export` |
|
|
55
|
+
|
|
56
|
+
## `ablo dev`
|
|
57
|
+
|
|
58
|
+
The development loop. It pushes `ablo/schema.ts` to your **test sandbox**,
|
|
59
|
+
prints the env line your app needs, then watches the file and re-pushes on every
|
|
60
|
+
save (300 ms debounce). It refuses live keys so a tight save loop can never
|
|
61
|
+
churn production data.
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
npx ablo dev # push + watch
|
|
65
|
+
npx ablo dev --no-watch # push once and exit
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## `ablo logs`
|
|
69
|
+
|
|
70
|
+
Tail commit activity, like `stripe logs tail`. Scope comes from the key — a test
|
|
71
|
+
key streams only its sandbox's writes, a live key the org's — so you never pass
|
|
72
|
+
an org. Follows by default; `--no-follow` prints recent and exits.
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
npx ablo logs # last 50, then stream
|
|
76
|
+
npx ablo logs -n 100 --model task # backfill 100, one model
|
|
77
|
+
npx ablo logs --since 15m --json # last 15m as NDJSON, then stream
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
Each line is `time · op · model · id · actor`. `--json` emits one event per line
|
|
81
|
+
(NDJSON) for piping to `jq` or an agent.
|
|
82
|
+
|
|
83
|
+
## `ablo pull`
|
|
84
|
+
|
|
85
|
+
Generate `defineSchema(...)` from the tables you already have — the inverse of
|
|
86
|
+
provisioning, and read-only (like `prisma db pull`). It introspects
|
|
87
|
+
`DATABASE_URL`, emits a model per adoptable table (one that has `id` +
|
|
88
|
+
`organization_id`), maps Postgres types back to Zod, and writes `ablo/schema.ts`.
|
|
89
|
+
|
|
90
|
+
```bash
|
|
91
|
+
DATABASE_URL=postgres://… npx ablo pull
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
It never touches the database, and won't overwrite an existing schema without
|
|
95
|
+
`--force`. Introspection is lossy — enum members, JSON shape, relations, and
|
|
96
|
+
defaults can't be recovered from columns — so treat the output as a starting
|
|
97
|
+
point: review it, then run `ablo check`.
|
|
98
|
+
|
|
99
|
+
## `ablo check`
|
|
100
|
+
|
|
101
|
+
The BYO front door. Instead of migrating (DDL on your database), Ablo *adopts*
|
|
102
|
+
the tables you already have: `ablo check` introspects `DATABASE_URL`, compares it
|
|
103
|
+
to your `defineSchema(...)`, and reports — per model — whether the table is
|
|
104
|
+
adoptable. It never writes or alters anything.
|
|
105
|
+
|
|
106
|
+
A table is adoptable when it has a primary key `id` and (for org-scoped models)
|
|
107
|
+
an `organization_id` column — the tenancy marker the engine isolates on. Every
|
|
108
|
+
other table in your database is ignored.
|
|
109
|
+
|
|
110
|
+
**Why `organization_id`?** It's the one column that makes a table safe to
|
|
111
|
+
multiplayer-sync. Row-level security scopes every read and write by it (org A
|
|
112
|
+
can't see org B's rows), and the engine routes realtime deltas by `org:<id>`. A
|
|
113
|
+
table without a tenancy key has no isolation boundary, so Ablo excludes it
|
|
114
|
+
**by default** rather than risk exposing it across tenants. If your tenancy
|
|
115
|
+
column has a different name, keep that table behind a
|
|
116
|
+
[Data Source endpoint](/data-sources) for now.
|
|
117
|
+
|
|
118
|
+
```bash
|
|
119
|
+
DATABASE_URL=postgres://… npx ablo check
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
```text
|
|
123
|
+
✓ tasks → tasks (id, organization_id ok)
|
|
124
|
+
✗ projects → projects
|
|
125
|
+
• missing "organization_id" — add it, or move this model behind a Data Source
|
|
126
|
+
2 models · 1 ok · 1 error
|
|
127
|
+
12 other tables in your database — ignored by Ablo
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
If a table can't carry `organization_id` (or has business logic Ablo shouldn't
|
|
131
|
+
bypass), keep it behind a [Data Source endpoint](/data-sources) rather than
|
|
132
|
+
reshaping it. `ablo check` is read-only; it never proposes a migration.
|
|
133
|
+
|
|
134
|
+
## `migrate` vs `schema push`
|
|
135
|
+
|
|
136
|
+
Two front doors to the same engine. Use `migrate` when your app owns the
|
|
137
|
+
database (it applies to `DATABASE_URL`); use `schema push` (and `dev`) on the
|
|
138
|
+
hosted path (the server applies to Ablo-managed Postgres and version-gates
|
|
139
|
+
connecting clients).
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
ablo migrate --dry-run # preview the exact SQL
|
|
143
|
+
ablo migrate # apply to DATABASE_URL
|
|
144
|
+
ablo migrate --output schema.sql # write SQL to a file
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Zod → Postgres type mapping
|
|
148
|
+
|
|
149
|
+
The one type map, shared by both paths (there is no second mapping):
|
|
150
|
+
|
|
151
|
+
| Zod | Postgres |
|
|
152
|
+
| --- | --- |
|
|
153
|
+
| `z.string()` | `TEXT` |
|
|
154
|
+
| `z.number()` | `DOUBLE PRECISION` — never `INTEGER`; a Zod number may be fractional, and truncating is silent data loss |
|
|
155
|
+
| `z.boolean()` | `BOOLEAN` |
|
|
156
|
+
| `z.date()` | `TIMESTAMPTZ` |
|
|
157
|
+
| `z.enum([...])` | `TEXT` + a `CHECK (col IN (...))` constraint |
|
|
158
|
+
| `z.object` / `z.array` / `z.record` / `z.union` / `z.custom` | `JSONB` |
|
|
159
|
+
| `.optional()` / `.nullable()` | nullable column |
|
|
160
|
+
|
|
161
|
+
Each table also gets the platform columns (`id`, `organization_id`,
|
|
162
|
+
`created_by`, `created_at`, `updated_at`), an `organization_id` index, and
|
|
163
|
+
row-level security keyed on `current_setting('app.current_org_id')` for tenant
|
|
164
|
+
isolation.
|
|
165
|
+
|
|
166
|
+
`.default(...)` is **not** emitted as a SQL column default — Zod applies the
|
|
167
|
+
default at write time (`create`), in one place, so a DB default and a schema
|
|
168
|
+
default can't drift.
|
|
169
|
+
|
|
170
|
+
## Structured errors
|
|
171
|
+
|
|
172
|
+
A failed migration aborts the whole transaction (nothing partial lands) and
|
|
173
|
+
reports the same `migration_failed` shape on both paths — naming the statement
|
|
174
|
+
that broke and the Postgres SQLSTATE, not just "migration failed".
|
|
175
|
+
|
|
176
|
+
`ablo migrate` (local) logs it:
|
|
177
|
+
|
|
178
|
+
```txt
|
|
179
|
+
[migrate] migration plan failed {
|
|
180
|
+
code: 'migration_failed',
|
|
181
|
+
failedStatement: 'ALTER TABLE "public"."tasks" RENAME COLUMN a TO b;',
|
|
182
|
+
failedStatementIndex: 4,
|
|
183
|
+
pgCode: '42P01',
|
|
184
|
+
durationMs: 133
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`ablo schema push` (hosted) returns the canonical error envelope (HTTP 500),
|
|
189
|
+
which the SDK reconstructs as a typed `AbloServerError`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"type": "AbloServerError",
|
|
194
|
+
"code": "migration_failed",
|
|
195
|
+
"message": "schema migration failed: relation \"...\" does not exist",
|
|
196
|
+
"doc_url": "https://docs.abloatai.com/errors#migration_failed",
|
|
197
|
+
"failedStatement": "ALTER TABLE ... RENAME COLUMN a TO b;",
|
|
198
|
+
"pgCode": "42P01"
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
The pushed artifact is recorded `failed` and is never activated, so a broken
|
|
203
|
+
migration can't leave clients gated against tables that don't match.
|
|
204
|
+
|
|
205
|
+
## Environment
|
|
206
|
+
|
|
207
|
+
| Variable | Purpose | Default |
|
|
208
|
+
| --- | --- | --- |
|
|
209
|
+
| `ABLO_API_KEY` | Authenticate without `ablo login` (CI). Always overrides the stored key. | — |
|
|
210
|
+
| `ABLO_API_URL` | Control-plane / API host (`schema push`, `dev`, `status`). | `https://api.abloatai.com` |
|
|
211
|
+
| `ABLO_AUTH_URL` | Dashboard origin for `ablo login`'s device flow. | `https://abloatai.com` |
|
|
212
|
+
| `ABLO_CONFIG_DIR` / `XDG_CONFIG_HOME` | Where the credential file lives. | `~/.config/ablo` |
|
package/docs/client-behavior.md
CHANGED
|
@@ -28,7 +28,7 @@ Common options:
|
|
|
28
28
|
| `schema` | Required for typed model clients. |
|
|
29
29
|
| `apiKey` | Bearer credential for trusted server runtimes. Defaults to `ABLO_API_KEY` when available. |
|
|
30
30
|
| `baseURL` | Override the hosted sync endpoint for staging or private deployments. |
|
|
31
|
-
| `persistence` | `volatile` by default. Use `indexeddb` for
|
|
31
|
+
| `persistence` | `volatile` by default. Use `indexeddb` for a durable browser cache that survives reloads. |
|
|
32
32
|
| `fetch` | Custom fetch implementation for tests or non-standard runtimes. |
|
|
33
33
|
| `defaultHeaders` | Extra headers attached to every HTTP request. |
|
|
34
34
|
| `defaultQuery` | Extra query parameters attached to every HTTP request. |
|
package/docs/coordination.md
CHANGED
|
@@ -11,10 +11,61 @@ does not poll. Reads are open by default; reading a claimed row is allowed unles
|
|
|
11
11
|
the caller explicitly asks for claimed gating. A claim carries a TTL so a crashed
|
|
12
12
|
holder is auto-released and the queue advances.
|
|
13
13
|
|
|
14
|
-
This reference
|
|
15
|
-
|
|
16
|
-
[
|
|
17
|
-
|
|
14
|
+
This reference opens with [the model](#the-model--three-layers-one-decision) — the
|
|
15
|
+
one answer to "how do two agents not clobber each other" — then covers the
|
|
16
|
+
[claim state object](#the-claim-state-object), the SDK [methods](#methods)
|
|
17
|
+
(`claim` · `claimState` · `queue` · `release` · [writing under a
|
|
18
|
+
claim](#writing-under-a-claim)), and the [errors](#errors) you can catch.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## The model — three layers, one decision
|
|
23
|
+
|
|
24
|
+
Ablo has exactly **three** coordination layers. They are **not** three competing
|
|
25
|
+
answers to the same question — they stack, and only one of them is a decision you
|
|
26
|
+
make:
|
|
27
|
+
|
|
28
|
+
| layer | kind | what it does | enforces? |
|
|
29
|
+
|---|---|---|---|
|
|
30
|
+
| **Presence** (`claimState`, observers) | observation | Broadcasts who is working where, live. Renders cursors / "agent X is editing." | **No.** Advisory only — it never blocks or rejects a write. |
|
|
31
|
+
| **Claim** (`claim`/`queue`/`release`) | pessimistic | Reserves a row for one participant. Foreign writers are rejected server-side; contenders join a fair FIFO queue. | **Yes**, between participants — mutual exclusion. |
|
|
32
|
+
| **Stale-context** (`readAt` + `onStale`) | optimistic (LWW) | On commit, rejects a write whose snapshot is older than the row's latest delta. Last-writer-wins detection. | **Yes**, against time — lost-update detection. |
|
|
33
|
+
|
|
34
|
+
**The one decision: do you hold the row across a slow gap (read → LLM call →
|
|
35
|
+
write)?**
|
|
36
|
+
|
|
37
|
+
- **No** (the common case — a single quick `update`): do nothing. `ablo.<model>.update`
|
|
38
|
+
is optimistically guarded by stale-context already; it rejects with
|
|
39
|
+
`AbloStaleContextError` if the row moved under you. This is the default and
|
|
40
|
+
needs no ceremony.
|
|
41
|
+
- **Yes** (you'll reason for seconds while holding the row): `claim` it. The claim
|
|
42
|
+
excludes other participants for the duration, queues contenders fairly, and —
|
|
43
|
+
see below — your own writes under it stay stale-guarded too.
|
|
44
|
+
|
|
45
|
+
**How they compose (what wins):**
|
|
46
|
+
|
|
47
|
+
1. **Claim supersedes stale-context for *foreign* writers.** A non-holder writing
|
|
48
|
+
to a claimed row is rejected by the claim guard (`AbloClaimedError`,
|
|
49
|
+
`claim_conflict`/`entity_claimed`) *before* any watermark check — `readAt` is
|
|
50
|
+
irrelevant when you don't hold the lease. Pessimistic exclusion is the outer
|
|
51
|
+
gate.
|
|
52
|
+
2. **Stale-context is the always-on backstop for *unclaimed* writes.** No claim
|
|
53
|
+
held → the watermark check is the only protection, and it's automatic. This is
|
|
54
|
+
why the no-claim path is safe by default.
|
|
55
|
+
3. **Inside a claim, both apply.** A claim is not a license to clobber yourself:
|
|
56
|
+
writes under a held claim carry the claim's snapshot as `readAt` with
|
|
57
|
+
`onStale: 'reject'` (see [Writing under a claim](#writing-under-a-claim)), so a
|
|
58
|
+
`bypass` write or a row that moved between snapshot and write still rejects.
|
|
59
|
+
Claim = "no one else"; stale-context = "and not against a moved snapshot."
|
|
60
|
+
4. **Presence never decides.** It is the visualization of (1)–(3), not a fourth
|
|
61
|
+
gate. Never branch enforcement logic on `claimState` — read it to render, act
|
|
62
|
+
on the errors above.
|
|
63
|
+
|
|
64
|
+
Claims and stale-context are **orthogonal by construction**, not wired into each
|
|
65
|
+
other on the server: the claim guard runs pre-transaction; the watermark check
|
|
66
|
+
runs inside it. The SDK attaches `readAt`/`onStale` for you when writing under a
|
|
67
|
+
claim — that coupling lives in the SDK, deliberately, so the server's two checks
|
|
68
|
+
stay independent and individually testable.
|
|
18
69
|
|
|
19
70
|
---
|
|
20
71
|
|
|
@@ -36,7 +87,6 @@ a model row. It's what `claimState()` returns and what observers render.
|
|
|
36
87
|
| `expiresAt` | `string` | Ms-epoch the server reclaims it if the holder goes **silent**. Renewed automatically while the holder's connection stays alive — a crash-cleanup floor, not a duration you size. |
|
|
37
88
|
|
|
38
89
|
```jsonc
|
|
39
|
-
// A live claim state, as returned by claimState():
|
|
40
90
|
{
|
|
41
91
|
"id": "claim_8fJ2",
|
|
42
92
|
"status": "active",
|
|
@@ -95,7 +145,6 @@ release hook for manual scopes.
|
|
|
95
145
|
**Example**
|
|
96
146
|
|
|
97
147
|
```ts
|
|
98
|
-
// Callback form — works on any toolchain:
|
|
99
148
|
const forecast = await ablo.weatherReports.claim('report_stockholm', async (report) => {
|
|
100
149
|
const weather = await weatherAgent.getWeather(report.location);
|
|
101
150
|
await ablo.weatherReports.update(report.id, { forecast: weather });
|
|
@@ -114,8 +163,8 @@ held. Server/model reads can choose a claimed policy:
|
|
|
114
163
|
|
|
115
164
|
```ts
|
|
116
165
|
await ablo.model('weatherReports').retrieve('report_stockholm', {
|
|
117
|
-
ifClaimed: 'wait',
|
|
118
|
-
claimedTimeout: 30_000,
|
|
166
|
+
ifClaimed: 'wait',
|
|
167
|
+
claimedTimeout: 30_000,
|
|
119
168
|
});
|
|
120
169
|
```
|
|
121
170
|
|
|
@@ -145,11 +194,12 @@ is free.
|
|
|
145
194
|
|
|
146
195
|
```ts
|
|
147
196
|
const who = ablo.weatherReports.claimState('report_stockholm');
|
|
148
|
-
if (who) console.log(`${who.heldBy} is ${who.action}`);
|
|
197
|
+
if (who) console.log(`${who.heldBy} is ${who.action}`);
|
|
149
198
|
```
|
|
150
199
|
|
|
200
|
+
Returns the active claim state when the row is held, or `null` when it's free:
|
|
201
|
+
|
|
151
202
|
```jsonc
|
|
152
|
-
// Resolved value when the row is held:
|
|
153
203
|
{
|
|
154
204
|
"id": "claim_8fJ2",
|
|
155
205
|
"status": "active",
|
|
@@ -159,7 +209,6 @@ if (who) console.log(`${who.heldBy} is ${who.action}`); // 'agent:forecaster is
|
|
|
159
209
|
"participantKind": "agent",
|
|
160
210
|
"expiresAt": "1748160030000"
|
|
161
211
|
}
|
|
162
|
-
// → null when free.
|
|
163
212
|
```
|
|
164
213
|
|
|
165
214
|
### `queue`
|
|
@@ -190,7 +239,7 @@ the active holder; `[]` when no one is waiting.
|
|
|
190
239
|
```ts
|
|
191
240
|
const { data: waiting } = ablo.weatherReports.queue('report_stockholm');
|
|
192
241
|
console.log(`${waiting.length} ahead of you`);
|
|
193
|
-
console.log(waiting.map((i) => i.heldBy));
|
|
242
|
+
console.log(waiting.map((i) => i.heldBy));
|
|
194
243
|
```
|
|
195
244
|
|
|
196
245
|
### `release`
|
package/docs/data-sources.md
CHANGED
|
@@ -24,7 +24,7 @@ Use the SDK with an API key:
|
|
|
24
24
|
|
|
25
25
|
```ts
|
|
26
26
|
import Ablo from '@abloatai/ablo';
|
|
27
|
-
import { schema } from './ablo
|
|
27
|
+
import { schema } from './ablo/schema';
|
|
28
28
|
|
|
29
29
|
export const ablo = Ablo({
|
|
30
30
|
schema,
|
|
@@ -100,7 +100,7 @@ The shape is the same as a production webhook integration:
|
|
|
100
100
|
```ts
|
|
101
101
|
// app/api/ablo/source/route.ts
|
|
102
102
|
import { dataSource } from '@abloatai/ablo';
|
|
103
|
-
import { schema } from '@/ablo
|
|
103
|
+
import { schema } from '@/ablo/schema';
|
|
104
104
|
import { db } from '@/db';
|
|
105
105
|
|
|
106
106
|
export const POST = dataSource({
|
|
@@ -26,7 +26,7 @@ Browser UI
|
|
|
26
26
|
Create a schema for the records that need realtime coordination.
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
|
-
// web/ablo
|
|
29
|
+
// web/ablo/schema.ts
|
|
30
30
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
31
31
|
|
|
32
32
|
export const schema = defineSchema({
|
|
@@ -42,7 +42,7 @@ export const schema = defineSchema({
|
|
|
42
42
|
```ts
|
|
43
43
|
// web/ablo.ts
|
|
44
44
|
import Ablo from '@abloatai/ablo';
|
|
45
|
-
import { schema } from './ablo
|
|
45
|
+
import { schema } from './ablo/schema';
|
|
46
46
|
|
|
47
47
|
export const ablo = Ablo({
|
|
48
48
|
schema,
|
|
@@ -58,7 +58,7 @@ model clients without importing server credentials.
|
|
|
58
58
|
'use client';
|
|
59
59
|
|
|
60
60
|
import { AbloProvider } from '@abloatai/ablo/react';
|
|
61
|
-
import { schema } from '@/ablo
|
|
61
|
+
import { schema } from '@/ablo/schema';
|
|
62
62
|
|
|
63
63
|
export function Providers({ children }: { children: React.ReactNode }) {
|
|
64
64
|
return <AbloProvider schema={schema}>{children}</AbloProvider>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Agent Scoped to One Deck
|
|
2
|
+
|
|
3
|
+
An agent that edits **one deck** and receives realtime updates for **only that
|
|
4
|
+
deck** — not the whole org. Shows the sync-group model end to end: a scope root,
|
|
5
|
+
a containment (`parent`) edge, identity roles, and the model-form `scope`.
|
|
6
|
+
|
|
7
|
+
See [Identity & Sync Groups](../identity.md) for the full reference.
|
|
8
|
+
|
|
9
|
+
## 1. Schema — declare the scope, once
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import { defineSchema, identityRole, model, relation, z } from '@abloatai/ablo/schema';
|
|
13
|
+
|
|
14
|
+
export const schema = defineSchema(
|
|
15
|
+
{
|
|
16
|
+
// A scope root: deck rows form the group `deck:<id>`.
|
|
17
|
+
decks: model(
|
|
18
|
+
{ title: z.string() },
|
|
19
|
+
{},
|
|
20
|
+
{ orgScoped: true, scope: 'deck' },
|
|
21
|
+
),
|
|
22
|
+
// A child: no group of its own. It inherits its deck's group via the
|
|
23
|
+
// `parent` edge, so a slide write reaches everyone viewing the deck —
|
|
24
|
+
// even a slide edit that doesn't touch `deckId` (routing is keyed on the
|
|
25
|
+
// row's id, not the changed columns).
|
|
26
|
+
slides: model(
|
|
27
|
+
{ deckId: z.string(), body: z.string() },
|
|
28
|
+
{ deck: relation.belongsTo('decks', 'deckId', { parent: true }) },
|
|
29
|
+
{ orgScoped: true },
|
|
30
|
+
),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
// Humans get their full org scope automatically from these.
|
|
34
|
+
identityRoles: [
|
|
35
|
+
identityRole({ kind: 'org', source: 'organizationId' }),
|
|
36
|
+
identityRole({ kind: 'user', source: 'userId' }),
|
|
37
|
+
],
|
|
38
|
+
},
|
|
39
|
+
);
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 2. Dispatch — narrow the agent to the deck it's working on
|
|
43
|
+
|
|
44
|
+
The agent inherits the triggering user's identity (its ceiling) and is narrowed
|
|
45
|
+
to one deck (the floor). You pass the **model and id** — never a `deck:<id>`
|
|
46
|
+
string; the engine builds the group from the model's `scope`.
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const ablo = Ablo({
|
|
50
|
+
schema,
|
|
51
|
+
url: process.env.ABLO_URL,
|
|
52
|
+
kind: 'agent',
|
|
53
|
+
agentId: 'agent:slide-writer',
|
|
54
|
+
userId: triggeringUser.id, // ceiling: can't exceed this user's reach
|
|
55
|
+
organizationId: triggeringUser.organizationId,
|
|
56
|
+
scope: { decks: deckId }, // floor: just this deck → deck:<deckId>
|
|
57
|
+
});
|
|
58
|
+
await ablo.ready();
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## 3. Write — it fans out to everyone on that deck
|
|
62
|
+
|
|
63
|
+
```ts
|
|
64
|
+
// Other participants subscribed to deck:<deckId> — the human in the editor,
|
|
65
|
+
// a reviewer agent — receive this delta in realtime. Participants on other
|
|
66
|
+
// decks never see it.
|
|
67
|
+
await ablo.slides.update(slideId, { body: 'Q4 revenue up 12% YoY' });
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The slide's delta is stamped `deck:<deckId>` (derived server-side from the
|
|
71
|
+
slide → deck `parent` edge), so it reaches the deck's audience authoritatively —
|
|
72
|
+
regardless of which groups the agent happened to subscribe to. And `scope` only
|
|
73
|
+
ever *narrows*: the agent can't reach a deck its triggering user couldn't.
|
|
74
|
+
|
|
75
|
+
## See also
|
|
76
|
+
|
|
77
|
+
- [Identity & Sync Groups](../identity.md) — the full scope / parent / grants model.
|
|
78
|
+
- [Agent + Human](./agent-human.md) — yielding when a human edits the same row.
|
package/docs/guarantees.md
CHANGED
|
@@ -62,6 +62,9 @@ Advanced policies exist for controlled product flows:
|
|
|
62
62
|
|
|
63
63
|
## Claim Coordination
|
|
64
64
|
|
|
65
|
+
> The guarantee, not the how-to. Methods, the claim-state object, and the `queue`
|
|
66
|
+
> live in [Coordination](./coordination.md).
|
|
67
|
+
|
|
65
68
|
Claims are live coordination signals. They are not database locks.
|
|
66
69
|
|
|
67
70
|
Claims are **advisory** and **cooperative**. `ablo.<model>.claim(id, ...)`
|
|
@@ -95,10 +98,10 @@ authorized it, which run did it, and what state was it based on?"
|
|
|
95
98
|
|
|
96
99
|
## Persistence
|
|
97
100
|
|
|
98
|
-
Ablo defaults to volatile
|
|
101
|
+
Ablo defaults to volatile in-memory persistence. That keeps the SDK focused on
|
|
99
102
|
coordination and audit instead of silently becoming a browser storage product.
|
|
100
103
|
|
|
101
|
-
Opt into durable browser cache
|
|
104
|
+
Opt into a durable browser cache that survives reloads when you need it:
|
|
102
105
|
|
|
103
106
|
```ts
|
|
104
107
|
const ablo = Ablo({
|