@abloatai/ablo 0.7.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +72 -1
- package/README.md +80 -66
- package/dist/BaseSyncedStore.d.ts +73 -0
- package/dist/BaseSyncedStore.js +179 -5
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/SyncEngineContext.d.ts +2 -1
- package/dist/SyncEngineContext.js +5 -3
- package/dist/agent/session.js +6 -5
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +26 -36
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +259 -33
- package/dist/client/Ablo.js +276 -73
- package/dist/client/ApiClient.d.ts +52 -4
- package/dist/client/ApiClient.js +236 -66
- package/dist/client/auth.d.ts +21 -2
- package/dist/client/auth.js +77 -5
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +187 -79
- package/dist/client/createModelProxy.js +203 -68
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +63 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +19 -0
- package/dist/client/registerDataSource.js +59 -0
- package/dist/client/validateAbloOptions.d.ts +2 -1
- package/dist/client/validateAbloOptions.js +8 -7
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +92 -1
- package/dist/errorCodes.js +139 -7
- package/dist/errors.d.ts +54 -3
- package/dist/errors.js +192 -44
- package/dist/index.d.ts +23 -10
- package/dist/index.js +21 -8
- package/dist/keys/index.d.ts +76 -0
- package/dist/keys/index.js +171 -0
- package/dist/mutators/UndoManager.d.ts +86 -50
- package/dist/mutators/UndoManager.js +129 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +22 -14
- package/dist/react/AbloProvider.d.ts +23 -101
- package/dist/react/AbloProvider.js +61 -103
- package/dist/react/ClientSideSuspense.d.ts +1 -1
- package/dist/react/DefaultFallback.d.ts +1 -1
- package/dist/react/SyncGroupProvider.d.ts +1 -1
- package/dist/react/index.d.ts +3 -2
- package/dist/react/index.js +3 -2
- package/dist/react/useAblo.d.ts +4 -4
- package/dist/react/useAblo.js +10 -5
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/react/useReactive.js +16 -3
- package/dist/schema/ddl.d.ts +26 -3
- package/dist/schema/ddl.js +152 -4
- package/dist/schema/index.d.ts +4 -0
- package/dist/schema/index.js +12 -0
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +7 -3
- package/dist/schema/serialize.js +6 -2
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +59 -0
- package/dist/source/adapter.js +19 -0
- package/dist/source/adapters/drizzle.d.ts +34 -0
- package/dist/source/adapters/drizzle.js +147 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +199 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +143 -0
- package/dist/source/contract.js +98 -0
- package/dist/source/index.d.ts +61 -10
- package/dist/source/index.js +98 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +56 -42
- package/dist/sync/ConnectionManager.d.ts +57 -1
- package/dist/sync/ConnectionManager.js +186 -11
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +241 -41
- package/dist/sync/NetworkProbe.d.ts +60 -18
- package/dist/sync/NetworkProbe.js +121 -23
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +113 -89
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/sync/participants.js +5 -2
- package/dist/transactions/TransactionQueue.js +13 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api-keys.md +5 -5
- package/docs/api.md +125 -65
- package/docs/audit.md +16 -9
- package/docs/cli.md +57 -47
- package/docs/client-behavior.md +54 -40
- package/docs/coordination.md +66 -80
- package/docs/data-sources.md +56 -34
- package/docs/examples/agent-human.md +74 -28
- package/docs/examples/ai-sdk-tool.md +29 -22
- package/docs/examples/existing-python-backend.md +41 -26
- package/docs/examples/nextjs.md +32 -17
- package/docs/examples/scoped-agent.md +43 -28
- package/docs/examples/server-agent.md +40 -15
- package/docs/guarantees.md +38 -27
- package/docs/identity.md +65 -59
- package/docs/index.md +30 -19
- package/docs/integration-guide.md +78 -78
- package/docs/interaction-model.md +43 -35
- package/docs/mcp/claude-code.md +11 -19
- package/docs/mcp/cursor.md +7 -25
- package/docs/mcp/windsurf.md +7 -20
- package/docs/mcp.md +103 -26
- package/docs/quickstart.md +63 -61
- package/docs/react.md +24 -16
- package/docs/roadmap.md +13 -13
- package/docs/schema-contract.md +111 -0
- package/docs/the-loop.md +21 -0
- package/examples/README.md +8 -4
- package/examples/data-source/README.md +10 -7
- package/examples/data-source/customer-server.ts +27 -25
- package/examples/data-source/run.ts +4 -3
- package/examples/quickstart.ts +1 -1
- package/llms.txt +55 -21
- package/package.json +48 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,76 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0
|
|
4
|
+
|
|
5
|
+
A single options object for every model verb, and a disposable `claim` handle.
|
|
6
|
+
|
|
7
|
+
### Breaking Changes
|
|
8
|
+
|
|
9
|
+
- **One options object per verb.** `create`, `update`, `delete`, and the async
|
|
10
|
+
server `retrieve` each take a single options object instead of positional
|
|
11
|
+
arguments, so the id, the data, and every modifier live as named siblings:
|
|
12
|
+
`create({ data, id? })`, `update({ id, data, ...options })`,
|
|
13
|
+
`delete({ id, ...options })`, `retrieve({ id, ...options })`. Reactive local
|
|
14
|
+
reads stay on `get(id)` (synchronous) —
|
|
15
|
+
`useAblo((ablo) => ablo.tasks.get(id))`.
|
|
16
|
+
|
|
17
|
+
```diff
|
|
18
|
+
- await ablo.tasks.update(id, { status: 'done' }, { wait: 'confirmed' })
|
|
19
|
+
+ await ablo.tasks.update({ id, data: { status: 'done' }, wait: 'confirmed' })
|
|
20
|
+
|
|
21
|
+
- await ablo.tasks.retrieve(id)
|
|
22
|
+
+ await ablo.tasks.retrieve({ id })
|
|
23
|
+
|
|
24
|
+
- useAblo((ablo) => ablo.tasks.retrieve(id)) ?? serverTask
|
|
25
|
+
+ useAblo((ablo) => ablo.tasks.get(id)) ?? serverTask
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
- **`claim` returns a disposable handle** instead of taking a callback. The
|
|
29
|
+
handle exposes the fresh row on `.data` and is released on scope exit
|
|
30
|
+
(`await using`) or explicitly via `.release()`. `claim.state`, `claim.queue`,
|
|
31
|
+
`claim.release`, and `claim.reorder` also take the options object.
|
|
32
|
+
|
|
33
|
+
```diff
|
|
34
|
+
- await ablo.tasks.claim(id, async (task) => {
|
|
35
|
+
- await ablo.tasks.update(task.id, { status: 'in_review' })
|
|
36
|
+
- })
|
|
37
|
+
+ await using claim = await ablo.tasks.claim({ id })
|
|
38
|
+
+ const task = claim.data
|
|
39
|
+
+ await ablo.tasks.update({ id: task.id, data: { status: 'in_review' } })
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## 0.8.0
|
|
43
|
+
|
|
44
|
+
A callable `claim` coordination namespace and bring-your-own-database support
|
|
45
|
+
via a new `databaseUrl` option.
|
|
46
|
+
|
|
47
|
+
### Minor Changes
|
|
48
|
+
|
|
49
|
+
- **Callable `claim` coordination namespace.** Taking a claim and inspecting its
|
|
50
|
+
state now live under one accessor: `claim(id, work)` acquires a claim and runs
|
|
51
|
+
`work` while it's held, and `claim.state(id)`, `claim.queue(id)`,
|
|
52
|
+
`claim.release(id)`, and `claim.reorder(id, order)` cover the surrounding
|
|
53
|
+
lifecycle. The README leads with the problem (who is allowed to act, and in
|
|
54
|
+
what order) and the Quick Start now demonstrates `claim` directly.
|
|
55
|
+
|
|
56
|
+
- **Bring-your-own-database via `databaseUrl`.** Point a project at your own
|
|
57
|
+
Postgres with `Ablo({ schema, apiKey, databaseUrl })`. Ablo writes synced rows
|
|
58
|
+
back into your database, so your data stays canonical. Server-side only;
|
|
59
|
+
defaults to `process.env.DATABASE_URL`. See the data-sources guide for setup
|
|
60
|
+
and role requirements.
|
|
61
|
+
|
|
62
|
+
### Breaking
|
|
63
|
+
|
|
64
|
+
- The flat coordination methods `claimState`, `queue`, `release`, and `reorder`
|
|
65
|
+
are removed in favor of the `claim` namespace above.
|
|
66
|
+
|
|
67
|
+
```diff
|
|
68
|
+
- await ablo.task.claimState(id)
|
|
69
|
+
- await ablo.task.release(id)
|
|
70
|
+
+ await ablo.task.claim.state(id)
|
|
71
|
+
+ await ablo.task.claim.release(id)
|
|
72
|
+
```
|
|
73
|
+
|
|
3
74
|
## 0.7.0
|
|
4
75
|
|
|
5
76
|
### Minor Changes
|
|
@@ -293,7 +364,7 @@ The SDK covers exactly three integration shapes. Each has a canonical example in
|
|
|
293
364
|
### Env / config
|
|
294
365
|
|
|
295
366
|
- `ABLO_API_KEY` — required for server-side use.
|
|
296
|
-
- `
|
|
367
|
+
- `baseURL` — optional override for private deployments / local-dev (defaults to `wss://api.abloatai.com`).
|
|
297
368
|
- `organizationId` — **no longer required** in `createMesh`. The API key or session binds the caller to one org; the capability mint response echoes it back.
|
|
298
369
|
- `createMeshFromEnv` — removed. `new Ablo({ schema })` auto-reads env.
|
|
299
370
|
|
package/README.md
CHANGED
|
@@ -5,35 +5,40 @@
|
|
|
5
5
|
[](#)
|
|
6
6
|
[](#keys--runtime)
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
server code, and AI agents all edit at once.
|
|
8
|
+
**Let people and AI agents work on the same data without overwriting each other.**
|
|
10
9
|
|
|
11
|
-
|
|
12
|
-
silently
|
|
13
|
-
|
|
10
|
+
When an agent and a person change the same thing at once, work gets lost: one
|
|
11
|
+
edit silently clobbers another, or the agent acts on data that already moved.
|
|
12
|
+
Ablo gives them one shared, typed write path so people, server actions, and
|
|
13
|
+
agents can all work on the same rows without working blind.
|
|
14
|
+
|
|
15
|
+
The core idea is a **claim**. An agent's work is rarely one instant write; it
|
|
16
|
+
reads something, thinks, calls an LLM or tool, then writes back. While that is
|
|
17
|
+
happening, the row can change underneath it. So before slow work starts, the
|
|
18
|
+
agent claims the row. If someone else is already working on it, `claim` waits,
|
|
19
|
+
re-reads the fresh row, then hands it over. No stale overwrite, no separate
|
|
20
|
+
agent mutation path.
|
|
21
|
+
|
|
22
|
+
Under the hood, you define a Zod schema once and get typed model clients for
|
|
23
|
+
every actor:
|
|
14
24
|
|
|
15
25
|
```txt
|
|
16
26
|
schema -> ablo.<model>.create/retrieve/update/claim(...)
|
|
17
27
|
```
|
|
18
28
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
groups* from your existing identity, and can leave your database as the source
|
|
33
|
-
of truth via a Data Source.
|
|
34
|
-
|
|
35
|
-
**Built for:** collaborative editors, AI agent workflows, internal tools, and any
|
|
36
|
-
app where multiple actors mutate shared state and everyone must see it live.
|
|
29
|
+
The schema is the public contract. It gives you typed model methods, realtime
|
|
30
|
+
fanout, React selectors, agent writes, and the HTTP/Data Source shape for
|
|
31
|
+
non-JavaScript services. Every confirmed change shows up everywhere, and active
|
|
32
|
+
claims are visible while the work is still in progress.
|
|
33
|
+
|
|
34
|
+
[Get started ↓](#quick-start) · point your coding agent at the shipped `llms.txt`
|
|
35
|
+
|
|
36
|
+
It works with the auth and database you already have: realtime data is scoped to
|
|
37
|
+
*sync groups* from your own identity, and your database can stay the source of
|
|
38
|
+
truth via a Data Source.
|
|
39
|
+
|
|
40
|
+
**Built for** collaborative editors, AI agent workflows, and internal tools —
|
|
41
|
+
anywhere people and agents change shared state and everyone has to see it live.
|
|
37
42
|
|
|
38
43
|
## Set up
|
|
39
44
|
|
|
@@ -79,16 +84,22 @@ const ablo = Ablo({
|
|
|
79
84
|
await ablo.ready();
|
|
80
85
|
|
|
81
86
|
const created = await ablo.weatherReports.create({
|
|
82
|
-
|
|
83
|
-
|
|
87
|
+
data: {
|
|
88
|
+
location: 'Stockholm',
|
|
89
|
+
status: 'pending',
|
|
90
|
+
},
|
|
84
91
|
});
|
|
85
92
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
});
|
|
93
|
+
// An agent claims the row, does its slow work, then writes back. While the
|
|
94
|
+
// claim is held nobody else can overwrite it; anyone else who tries waits in
|
|
95
|
+
// line and re-reads the result. This is the whole point of Ablo.
|
|
96
|
+
await using claim = await ablo.weatherReports.claim({ id: created.id });
|
|
97
|
+
const report = claim.data;
|
|
98
|
+
const forecast = await fetchForecast(report.location); // slow: API or LLM call
|
|
99
|
+
await ablo.weatherReports.update({ id: report.id, data: { status: 'ready', forecast } });
|
|
90
100
|
|
|
91
|
-
|
|
101
|
+
const ready = ablo.weatherReports.get(created.id);
|
|
102
|
+
console.log({ id: ready.id, status: ready.status });
|
|
92
103
|
|
|
93
104
|
await ablo.dispose();
|
|
94
105
|
```
|
|
@@ -99,31 +110,30 @@ Expected output:
|
|
|
99
110
|
{ id: '...', status: 'ready' }
|
|
100
111
|
```
|
|
101
112
|
|
|
102
|
-
Pass `schema` to get typed models like `ablo.weatherReports.update(...)`.
|
|
103
|
-
|
|
104
113
|
## Reading
|
|
105
114
|
|
|
106
|
-
|
|
107
|
-
`
|
|
108
|
-
|
|
109
|
-
|
|
115
|
+
Two ways to read, depending on whether you can wait. `get(id)` / `getAll({ where })`
|
|
116
|
+
/ `getCount({ where })` are instant — they read what's already local and re-render
|
|
117
|
+
on their own when it changes, so they're what your UI uses. `retrieve(id)` /
|
|
118
|
+
`list({ where })` go ask the server and return a `Promise`, for when you need the
|
|
119
|
+
authoritative answer right now.
|
|
110
120
|
|
|
111
121
|
```ts
|
|
112
|
-
ablo.weatherReports.
|
|
122
|
+
ablo.weatherReports.get('report_stockholm');
|
|
113
123
|
|
|
114
|
-
const pending = ablo.weatherReports.
|
|
124
|
+
const pending = ablo.weatherReports.getAll({
|
|
115
125
|
where: { status: 'pending' },
|
|
116
126
|
orderBy: { location: 'asc' },
|
|
117
127
|
limit: 20,
|
|
118
128
|
});
|
|
119
129
|
|
|
120
|
-
const ready = await ablo.weatherReports.
|
|
130
|
+
const ready = await ablo.weatherReports.list({
|
|
121
131
|
where: { status: 'ready' },
|
|
122
132
|
type: 'complete',
|
|
123
133
|
});
|
|
124
134
|
```
|
|
125
135
|
|
|
126
|
-
An array value in `where` means `IN`. On `
|
|
136
|
+
An array value in `where` means `IN`. On `list`, `type: 'complete'` waits for
|
|
127
137
|
the server; `'unknown'` returns what's local now and refreshes in the background.
|
|
128
138
|
|
|
129
139
|
## Writing
|
|
@@ -137,7 +147,7 @@ matter day to day:
|
|
|
137
147
|
| `idempotencyKey` | `string` | Auto-generated per call. Override only when you own the retry boundary (e.g. a job id) so a re-run dedupes server-side. |
|
|
138
148
|
|
|
139
149
|
```ts
|
|
140
|
-
await ablo.weatherReports.update(id, { status: 'ready' },
|
|
150
|
+
await ablo.weatherReports.update({ id, data: { status: 'ready' }, wait: 'confirmed' });
|
|
141
151
|
```
|
|
142
152
|
|
|
143
153
|
To guard a write against a row that changed under you, pass `readAt` + `onStale`
|
|
@@ -149,33 +159,35 @@ An agent reads a row, thinks for 30s, writes back — and clobbers whatever chan
|
|
|
149
159
|
meanwhile, or worse, acts on stale state. `claim` holds the row across that gap:
|
|
150
160
|
|
|
151
161
|
```ts
|
|
152
|
-
await ablo.weatherReports.claim('report_stockholm'
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
});
|
|
162
|
+
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
163
|
+
const report = claim.data;
|
|
164
|
+
const forecast = await weatherAgent.getWeather(report.location);
|
|
165
|
+
await ablo.weatherReports.update({ id: report.id, data: { forecast, status: 'ready' } });
|
|
156
166
|
```
|
|
157
167
|
|
|
158
168
|
If someone else holds the row, `claim()` waits in a fair queue, then re-reads —
|
|
159
169
|
so `report` is the current row, never a stale snapshot. Reads stay open by
|
|
160
|
-
default; only acting on the row serializes. The claim releases when the
|
|
161
|
-
|
|
170
|
+
default; only acting on the row serializes. The claim releases when the `await
|
|
171
|
+
using` scope exits.
|
|
162
172
|
|
|
163
173
|
See who's mid-edit before you act — decide to wait, or skip:
|
|
164
174
|
|
|
165
175
|
```ts
|
|
166
|
-
ablo.weatherReports.
|
|
167
|
-
ablo.weatherReports.queue('report_stockholm');
|
|
176
|
+
ablo.weatherReports.claim.state({ id: 'report_stockholm' });
|
|
177
|
+
ablo.weatherReports.claim.queue({ id: 'report_stockholm' });
|
|
168
178
|
|
|
169
|
-
|
|
179
|
+
{
|
|
180
|
+
await using claim = await ablo.weatherReports.claim({ id, wait: false });
|
|
170
181
|
/* do the held work */
|
|
171
|
-
}
|
|
182
|
+
}
|
|
172
183
|
|
|
173
|
-
|
|
184
|
+
{
|
|
185
|
+
await using claim = await ablo.weatherReports.claim({ id, maxQueueDepth: 2 });
|
|
174
186
|
/* do the held work */
|
|
175
|
-
}
|
|
187
|
+
}
|
|
176
188
|
```
|
|
177
189
|
|
|
178
|
-
`
|
|
190
|
+
`claim.state` returns the holder (or `null`); `claim.queue` returns the line waiting
|
|
179
191
|
behind it. `wait: false` skips rather than waiting when the row is held;
|
|
180
192
|
`maxQueueDepth: 2` bails when two or more are already ahead.
|
|
181
193
|
|
|
@@ -186,17 +198,18 @@ Even an unclaimed write can't land on stale reasoning — the commit is guarded:
|
|
|
186
198
|
|
|
187
199
|
```ts
|
|
188
200
|
try {
|
|
189
|
-
await ablo.weatherReports.update(id, { status: 'ready' },
|
|
201
|
+
await ablo.weatherReports.update({ id, data: { status: 'ready' }, readAt, onStale: 'reject' });
|
|
190
202
|
} catch (e) {
|
|
191
203
|
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
192
204
|
}
|
|
193
205
|
```
|
|
194
206
|
|
|
195
|
-
>
|
|
196
|
-
>
|
|
207
|
+
> Use `await using` for ordinary held work — the claim releases when the scope
|
|
208
|
+
> exits. Call `claim.release({ id })` only to give a manually held claim back
|
|
209
|
+
> early.
|
|
197
210
|
|
|
198
|
-
See [Coordination](./docs/coordination.md) for the full `claim` / `
|
|
199
|
-
`queue` / `release` reference.
|
|
211
|
+
See [Coordination](./docs/coordination.md) for the full `claim` / `claim.state` /
|
|
212
|
+
`claim.queue` / `claim.release` reference.
|
|
200
213
|
|
|
201
214
|
## React
|
|
202
215
|
|
|
@@ -217,13 +230,13 @@ function App() {
|
|
|
217
230
|
}
|
|
218
231
|
|
|
219
232
|
function Report({ id }: { id: string }) {
|
|
220
|
-
const report = useAblo((ablo) => ablo.weatherReports.
|
|
233
|
+
const report = useAblo((ablo) => ablo.weatherReports.get(id));
|
|
221
234
|
const ablo = useAblo();
|
|
222
235
|
|
|
223
236
|
if (!report) return null;
|
|
224
237
|
|
|
225
238
|
return (
|
|
226
|
-
<button onClick={() => ablo?.weatherReports.update(id, { status: 'ready' })}>
|
|
239
|
+
<button onClick={() => ablo?.weatherReports.update({ id, data: { status: 'ready' } })}>
|
|
227
240
|
{report.status}
|
|
228
241
|
</button>
|
|
229
242
|
);
|
|
@@ -277,7 +290,7 @@ each other's changes in real time — that's the default, not a feature you turn
|
|
|
277
290
|
|
|
278
291
|
- `ablo.<model>.create/update/delete` fan out confirmed deltas to subscribers.
|
|
279
292
|
- `useAblo(...)` gives React clients the live row, kept current automatically.
|
|
280
|
-
- `ablo.<model>.claim(id)` / `
|
|
293
|
+
- `ablo.<model>.claim({ id })` / `claim.state({ id })` / `claim.queue({ id })` let humans and agents coordinate (and observe) active work on a row — and the line waiting behind it — before a write lands.
|
|
281
294
|
|
|
282
295
|
Always write through Ablo — either the SDK model methods
|
|
283
296
|
(`ablo.<model>.create/update/delete`) or the HTTP write endpoint below. If you
|
|
@@ -306,7 +319,7 @@ curl https://api.abloatai.com/v1/commits \
|
|
|
306
319
|
## Connect Your Database
|
|
307
320
|
|
|
308
321
|
Every schema model has a backing store. By default, Ablo stores rows for the
|
|
309
|
-
models you declare, so `ablo.weatherReports.create(
|
|
322
|
+
models you declare, so `ablo.weatherReports.create({ data })` and `ablo.weatherReports.update({ id, data })`
|
|
310
323
|
write to Ablo-managed state.
|
|
311
324
|
|
|
312
325
|
If your existing database stays the source of truth, connect it as a Data
|
|
@@ -324,7 +337,7 @@ See [Connect Your Database](./docs/data-sources.md) for the integration shape.
|
|
|
324
337
|
| --- | --- | --- | --- |
|
|
325
338
|
| `schema` | `Schema` | — (required) | Typed model proxies (`ablo.<model>.*`) |
|
|
326
339
|
| `apiKey` | `string \| ApiKeySetter \| null` | `process.env.ABLO_API_KEY` | Server key — a string, or an async function for rotation |
|
|
327
|
-
| `baseURL` | `string` | `wss://
|
|
340
|
+
| `baseURL` | `string` | `wss://api.abloatai.com` | Point at a self-hosted or private API |
|
|
328
341
|
|
|
329
342
|
Keep `apiKey` in trusted server runtimes. In the browser, `<AbloProvider>`
|
|
330
343
|
authenticates with the signed-in user's session; the raw-key path is gated
|
|
@@ -340,7 +353,7 @@ survives worker / `postMessage` boundaries, where `instanceof` does not:
|
|
|
340
353
|
|
|
341
354
|
```ts
|
|
342
355
|
try {
|
|
343
|
-
await ablo.weatherReports.update(id, { status: 'ready' },
|
|
356
|
+
await ablo.weatherReports.update({ id, data: { status: 'ready' }, readAt, onStale: 'reject' });
|
|
344
357
|
} catch (e) {
|
|
345
358
|
if (e instanceof AbloStaleContextError) { /* row moved under you — re-read, retry */ }
|
|
346
359
|
if ((e as AbloError).type === 'AbloClaimedError') { /* another participant holds it */ }
|
|
@@ -372,10 +385,11 @@ contract; there are no retry or timeout knobs to tune.
|
|
|
372
385
|
## Production Reference
|
|
373
386
|
|
|
374
387
|
- [Identity & Sync Groups](./docs/identity.md) — bring your own auth; tell Ablo who's connecting and how org / team / user map to sync-group scope.
|
|
388
|
+
- [Schema Contract](./docs/schema-contract.md) — one schema becomes typed model clients, React reads, agent writes, Data Source shape, and schema push.
|
|
375
389
|
- [Guarantees](./docs/guarantees.md) — confirmed writes, stale-write protection, claim coordination, and agent lifecycle.
|
|
376
390
|
- [Integration Guide](./docs/integration-guide.md) — pick the backing mode and integrate React, Data Source, multiplayer, and agents.
|
|
377
391
|
- [React](./docs/react.md) — `<AbloProvider>`, `useAblo`, presence, status, and bootstrap gating.
|
|
378
|
-
- [Coordination](./docs/coordination.md) — `claim` / `
|
|
392
|
+
- [Coordination](./docs/coordination.md) — `claim` / `claim.state` / `claim.queue` / `claim.release` reference: hold a row across slow agent work, and observe the line waiting behind it.
|
|
379
393
|
- [Client Behavior](./docs/client-behavior.md) — options, errors, retries, timeouts, and public imports.
|
|
380
394
|
- [Connect Your Database](./docs/data-sources.md) — keep canonical rows in your app database without giving Ablo database credentials.
|
|
381
395
|
- [Existing Python Backend](./docs/examples/existing-python-backend.md) — migrate existing Python endpoints to multiplayer and agent-safe writes gradually.
|
|
@@ -22,6 +22,7 @@ import { Model } from './Model.js';
|
|
|
22
22
|
import { ModelScope } from './ObjectPool.js';
|
|
23
23
|
import type { Schema } from './schema/schema.js';
|
|
24
24
|
import { type ReaderActions } from './mutators/readerActions.js';
|
|
25
|
+
import type { AuthCredentialSource } from './auth/credentialSource.js';
|
|
25
26
|
/** Constructor type for Model subclasses (accepts abstract classes) */
|
|
26
27
|
export type ModelConstructor<T extends Model> = abstract new (...args: never[]) => T;
|
|
27
28
|
/** Concrete constructor type for instantiation */
|
|
@@ -240,6 +241,7 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
240
241
|
protected readonly database: Database;
|
|
241
242
|
protected readonly objectPool: ObjectPool;
|
|
242
243
|
protected readonly modelRegistry: ModelRegistry;
|
|
244
|
+
protected readonly auth?: AuthCredentialSource;
|
|
243
245
|
/**
|
|
244
246
|
* Schema the store was constructed with. Persisted so the `query`
|
|
245
247
|
* accessor namespace can build typed per-model reader actions lazily
|
|
@@ -308,6 +310,8 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
308
310
|
schema?: TSchema;
|
|
309
311
|
/** Sync server URL for WebSocket connection. Converted to wss:// automatically. */
|
|
310
312
|
url?: string;
|
|
313
|
+
/** Shared bearer credential source for every auth-aware transport. */
|
|
314
|
+
auth?: AuthCredentialSource;
|
|
311
315
|
}, config?: SyncedStoreConfig);
|
|
312
316
|
/**
|
|
313
317
|
* Register foreign key indexes for O(1) lookups.
|
|
@@ -355,6 +359,24 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
355
359
|
* return null.
|
|
356
360
|
*/
|
|
357
361
|
protected connectionManager: import('./sync/ConnectionManager.js').ConnectionManager | null;
|
|
362
|
+
/**
|
|
363
|
+
* Re-mint hook for the short-lived access credential (the Stripe-style
|
|
364
|
+
* `ek_`/`rk_`). Wired by the React provider from its `getToken`/`authEndpoint`
|
|
365
|
+
* — the engine owns WHEN to refresh (a stale-credential probe / an external
|
|
366
|
+
* nudge), the integrator owns HOW to mint. Mirrors the `getToken` contract:
|
|
367
|
+
* resolves a token string on success, `null` when the long-lived login is
|
|
368
|
+
* gone (terminal), and THROWS on a transient/offline failure. Used by
|
|
369
|
+
* {@link performCredentialRefresh}. Absent ⇒ no silent re-mint (e.g. a static
|
|
370
|
+
* `apiKey` deployment whose credential source refreshes out-of-band).
|
|
371
|
+
*/
|
|
372
|
+
private credentialRefresher;
|
|
373
|
+
/** Single-flight guard so a wake nudge + an in-flight request + a probe don't
|
|
374
|
+
* all mint at once (the classic "token thrash → random logout" bug). */
|
|
375
|
+
private inFlightCredentialRefresh;
|
|
376
|
+
/** Teardown for the proactive credential lifecycle (refresh timer + wake/
|
|
377
|
+
* online/focus listeners) installed by {@link startCredentialLifecycle};
|
|
378
|
+
* cleared on {@link disconnect}. Null when no resolver is wired. */
|
|
379
|
+
private credentialLifecycleTeardown;
|
|
358
380
|
/**
|
|
359
381
|
* Listeners registered via `subscribeSessionError()`. Fired when the
|
|
360
382
|
* WebSocket closes with a session-invalid code (1008/4001/4003) or a
|
|
@@ -404,6 +426,57 @@ export declare class BaseSyncedStore<TCollaboration extends EventMap<TCollaborat
|
|
|
404
426
|
protected resetBootstrapState(): void;
|
|
405
427
|
/** Perform reconnect: bootstrap + WS reconnect. Returns outcome for state machine. */
|
|
406
428
|
performReconnect(): Promise<'success' | 'session_error' | 'network_error'>;
|
|
429
|
+
/**
|
|
430
|
+
* Register the access-credential re-mint hook. Called by the React provider
|
|
431
|
+
* with a thunk that mints a fresh `ek_`/`rk_` (typically its `getToken`).
|
|
432
|
+
* See {@link credentialRefresher}.
|
|
433
|
+
*/
|
|
434
|
+
setCredentialRefresher(refresher: (() => Promise<string | null>) | null): void;
|
|
435
|
+
/**
|
|
436
|
+
* Re-mint the short-lived access credential and push it into the credential
|
|
437
|
+
* source, reporting a tri-state outcome the {@link ConnectionManager} maps to
|
|
438
|
+
* its FSM. The contract mirrors `getToken` (and PowerSync's `fetchCredentials`
|
|
439
|
+
* / Liveblocks' `authEndpoint`, but made explicit instead of overloading
|
|
440
|
+
* return/throw):
|
|
441
|
+
* - token string → `'refreshed'` (fresh key in place; re-probe & reconnect)
|
|
442
|
+
* - `null` → `'session_error'` (login itself is gone → terminal, sign out)
|
|
443
|
+
* - throw → `'network_error'` (couldn't reach the mint endpoint → transient)
|
|
444
|
+
*
|
|
445
|
+
* SINGLE-FLIGHT: concurrent callers (a wake nudge, an in-flight request, the
|
|
446
|
+
* probe) share one in-flight promise so we never double-mint — the canonical
|
|
447
|
+
* fix for the "every 401 mints a token → thrash → spurious logout" anti-pattern.
|
|
448
|
+
*
|
|
449
|
+
* No refresher wired ⇒ `'refreshed'` (a no-op re-probe): a static-`apiKey`
|
|
450
|
+
* deployment has no session to re-mint from; its credential source refreshes
|
|
451
|
+
* out-of-band, so we just re-probe with whatever it currently holds.
|
|
452
|
+
*/
|
|
453
|
+
performCredentialRefresh(): Promise<'refreshed' | 'session_error' | 'network_error'>;
|
|
454
|
+
/**
|
|
455
|
+
* Nudge the connection FSM to re-probe with the current credential. Idempotent
|
|
456
|
+
* and safe in any state (ignored while `connected`). Call after pushing a
|
|
457
|
+
* freshly-minted token via `setAuthToken`, or on an OS-wake signal, so a
|
|
458
|
+
* connection parked in `offline` / `backoff` / `auth_blocked` picks the new
|
|
459
|
+
* credential up immediately instead of waiting for the 30s watchdog.
|
|
460
|
+
*/
|
|
461
|
+
nudgeReconnect(): void;
|
|
462
|
+
/**
|
|
463
|
+
* Install the access-credential lifecycle the CLIENT owns (this used to live
|
|
464
|
+
* in the React provider — wrong layer). Two parts:
|
|
465
|
+
* 1. REACTIVE — register `getToken` as the re-mint hook the FSM calls when a
|
|
466
|
+
* probe finds the key stale (`credential_stale`) or on a nudge.
|
|
467
|
+
* 2. PROACTIVE — keep the short-lived key fresh ahead of trouble: a refresh
|
|
468
|
+
* timer inside the TTL, plus re-mint on OS wake / network-online / tab
|
|
469
|
+
* focus. Browser-only triggers are env-gated, so Node/agent hosts get
|
|
470
|
+
* only the timer (a no-op there — agents use a static `apiKey`, no
|
|
471
|
+
* resolver, so this is never called for them).
|
|
472
|
+
*
|
|
473
|
+
* Config-driven and invisible, like Supabase's `autoRefreshToken` — consumers
|
|
474
|
+
* never call a refresh method. Idempotent (a second call replaces the first);
|
|
475
|
+
* torn down on {@link disconnect}.
|
|
476
|
+
*/
|
|
477
|
+
startCredentialLifecycle(getToken: () => Promise<string | null>): void;
|
|
478
|
+
/** Tear down the proactive credential lifecycle (idempotent). */
|
|
479
|
+
private stopCredentialLifecycle;
|
|
407
480
|
/**
|
|
408
481
|
* Handle an actionType 'G' delta.
|
|
409
482
|
*
|