@abloatai/ablo 0.5.1 → 0.6.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 +16 -0
- package/README.md +217 -122
- package/dist/BaseSyncedStore.d.ts +2 -2
- package/dist/BaseSyncedStore.js +2 -2
- package/dist/api/index.d.ts +3 -3
- package/dist/api/index.js +1 -1
- package/dist/client/Ablo.d.ts +90 -93
- package/dist/client/Ablo.js +121 -60
- package/dist/client/ApiClient.d.ts +14 -14
- package/dist/client/ApiClient.js +81 -55
- package/dist/client/createInternalComponents.d.ts +2 -3
- package/dist/client/createInternalComponents.js +2 -3
- package/dist/client/createModelProxy.d.ts +90 -87
- package/dist/client/createModelProxy.js +124 -127
- package/dist/client/index.d.ts +6 -7
- package/dist/client/index.js +4 -5
- package/dist/client/validateAbloOptions.js +3 -3
- package/dist/core/index.d.ts +2 -0
- package/dist/core/index.js +7 -0
- package/dist/errors.d.ts +8 -8
- package/dist/errors.js +18 -10
- package/dist/index.d.ts +9 -8
- package/dist/index.js +7 -11
- package/dist/interfaces/index.d.ts +2 -10
- package/dist/mutators/Transaction.d.ts +2 -2
- package/dist/mutators/Transaction.js +2 -2
- package/dist/mutators/mutateActions.d.ts +44 -0
- package/dist/{react/useMutate.js → mutators/mutateActions.js} +11 -28
- package/dist/mutators/readerActions.d.ts +32 -0
- package/dist/{react/useReader.js → mutators/readerActions.js} +2 -18
- package/dist/query/types.d.ts +1 -1
- package/dist/react/AbloProvider.d.ts +1 -1
- package/dist/react/AbloProvider.js +3 -3
- package/dist/react/context.d.ts +4 -4
- package/dist/react/index.d.ts +4 -5
- package/dist/react/index.js +3 -7
- package/dist/react/useAblo.d.ts +14 -14
- package/dist/react/useAblo.js +26 -26
- package/dist/react/useIntent.d.ts +2 -2
- package/dist/react/useIntent.js +2 -2
- package/dist/react/useMutators.d.ts +1 -1
- package/dist/react/usePresence.d.ts +3 -3
- package/dist/react/usePresence.js +4 -4
- package/dist/react/useUndoScope.d.ts +1 -1
- package/dist/schema/diff.d.ts +161 -0
- package/dist/schema/diff.js +262 -0
- package/dist/schema/generate.d.ts +19 -0
- package/dist/schema/generate.js +87 -0
- package/dist/schema/index.d.ts +4 -1
- package/dist/schema/index.js +7 -1
- package/dist/schema/schema.d.ts +83 -32
- package/dist/schema/schema.js +58 -12
- package/dist/schema/serialize.d.ts +92 -0
- package/dist/schema/serialize.js +227 -0
- package/dist/sync/SyncWebSocket.d.ts +17 -0
- package/dist/sync/SyncWebSocket.js +46 -1
- package/dist/sync/awaitIntentGrant.d.ts +26 -0
- package/dist/sync/awaitIntentGrant.js +60 -0
- package/dist/sync/createIntentStream.js +43 -4
- package/dist/sync/createPresenceStream.js +1 -1
- package/dist/sync/participants.d.ts +2 -2
- package/dist/sync/participants.js +4 -4
- package/dist/types/global.d.ts +43 -52
- package/dist/types/global.js +16 -18
- package/dist/types/streams.d.ts +37 -9
- package/docs/api.md +68 -158
- package/docs/audit.md +5 -5
- package/docs/client-behavior.md +41 -42
- package/docs/coordination.md +294 -0
- package/docs/data-sources.md +14 -14
- package/docs/examples/agent-human.md +30 -32
- package/docs/examples/ai-sdk-tool.md +32 -33
- package/docs/examples/existing-python-backend.md +35 -33
- package/docs/examples/nextjs.md +24 -25
- package/docs/examples/server-agent.md +20 -61
- package/docs/guarantees.md +30 -55
- package/docs/identity.md +458 -0
- package/docs/index.md +12 -24
- package/docs/integration-guide.md +106 -116
- package/docs/interaction-model.md +29 -95
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +1 -1
- package/docs/mcp.md +11 -26
- package/docs/quickstart.md +43 -49
- package/docs/react.md +73 -23
- package/docs/roadmap.md +5 -7
- package/llms.txt +34 -39
- package/package.json +1 -1
- package/dist/react/useMutate.d.ts +0 -83
- package/dist/react/useQuery.d.ts +0 -123
- package/dist/react/useQuery.js +0 -145
- package/dist/react/useReader.d.ts +0 -69
- package/docs/capabilities.md +0 -163
package/docs/api.md
CHANGED
|
@@ -11,30 +11,30 @@ import Ablo from '@abloatai/ablo';
|
|
|
11
11
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
12
12
|
|
|
13
13
|
const schema = defineSchema({
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
status: z.enum(['
|
|
14
|
+
weatherReports: model({
|
|
15
|
+
location: z.string(),
|
|
16
|
+
status: z.enum(['pending', 'ready']),
|
|
17
17
|
}),
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
21
21
|
|
|
22
22
|
await ablo.ready();
|
|
23
|
-
const [
|
|
24
|
-
if (!
|
|
23
|
+
const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
24
|
+
if (!report) throw new Error('Row not found');
|
|
25
25
|
|
|
26
|
-
await ablo.
|
|
26
|
+
await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
|
|
27
27
|
```
|
|
28
28
|
|
|
29
29
|
## Model Methods
|
|
30
30
|
|
|
31
|
-
Each schema model becomes a typed
|
|
31
|
+
Each schema model becomes a typed model on the client:
|
|
32
32
|
|
|
33
|
-
- `ablo.
|
|
34
|
-
- `ablo.
|
|
35
|
-
- `ablo.
|
|
36
|
-
- `ablo.
|
|
37
|
-
- `ablo.
|
|
33
|
+
- `ablo.weatherReports.load({ where })` hydrates rows asynchronously.
|
|
34
|
+
- `ablo.weatherReports.retrieve(id)` reads one already-loaded row synchronously.
|
|
35
|
+
- `ablo.weatherReports.create(data)` creates a row.
|
|
36
|
+
- `ablo.weatherReports.update(id, data, options?)` updates a row.
|
|
37
|
+
- `ablo.weatherReports.delete(id, options?)` deletes a row.
|
|
38
38
|
|
|
39
39
|
`load` and `retrieve` are not aliases. Use `load` when the row may not be in the
|
|
40
40
|
local pool yet. Use `retrieve` after `ready()` or `load()` when you want a cheap
|
|
@@ -53,9 +53,9 @@ local read.
|
|
|
53
53
|
`list` and `count` read the local pool. They default to live rows and accept:
|
|
54
54
|
|
|
55
55
|
```ts
|
|
56
|
-
const
|
|
57
|
-
where: { status: '
|
|
58
|
-
filter: (
|
|
56
|
+
const readyReports = ablo.weatherReports.list({
|
|
57
|
+
where: { status: 'ready' },
|
|
58
|
+
filter: (report) => !report.location.startsWith('[archived]'),
|
|
59
59
|
orderBy: { updatedAt: 'desc' },
|
|
60
60
|
limit: 20,
|
|
61
61
|
scope: 'live', // 'live' | 'archived' | 'all'
|
|
@@ -67,11 +67,11 @@ const activeDoneTasks = ablo.tasks.list({
|
|
|
67
67
|
Use `snapshot` when a write should reject if the row changed mid-flight:
|
|
68
68
|
|
|
69
69
|
```ts
|
|
70
|
-
const snap = ablo.snapshot({
|
|
70
|
+
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
71
71
|
|
|
72
|
-
await ablo.
|
|
73
|
-
'
|
|
74
|
-
{ status: '
|
|
72
|
+
await ablo.weatherReports.update(
|
|
73
|
+
'report_stockholm',
|
|
74
|
+
{ status: 'ready' },
|
|
75
75
|
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
|
|
76
76
|
);
|
|
77
77
|
```
|
|
@@ -82,62 +82,43 @@ Protected write options:
|
|
|
82
82
|
|---|---|
|
|
83
83
|
| `readAt` | The state cursor the write was based on. |
|
|
84
84
|
| `onStale` | Stale-state policy. Prefer `reject` for agent writes. |
|
|
85
|
-
| `intent` | Active work claim associated with the write. |
|
|
86
85
|
| `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
|
|
87
86
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
88
87
|
| `timeout` | Maximum time to wait for the write call. |
|
|
89
88
|
|
|
90
|
-
##
|
|
89
|
+
## Claims
|
|
91
90
|
|
|
92
|
-
|
|
93
|
-
|
|
91
|
+
A claim tells humans and agents who is working on a target before the write
|
|
92
|
+
lands. One self-describing object carries the lifecycle in a single `status`
|
|
93
|
+
field. It lives on the coordination plane: ephemeral, TTL'd, broadcast to peers
|
|
94
|
+
in real time, and never persisted as a row.
|
|
94
95
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
ifBusy: 'return',
|
|
100
|
-
});
|
|
101
|
-
```
|
|
102
|
-
|
|
103
|
-
`stamp` is the state watermark. Pass it as `readAt` to reject stale writes.
|
|
96
|
+
Coordinate one through flat verbs on the model, beside `create`/`update`/`retrieve`:
|
|
97
|
+
`ablo.<model>.claim(id, ...)` to claim a row, `ablo.<model>.claimState(id)` to read
|
|
98
|
+
who holds it (synchronous; never blocks), and `ablo.<model>.release(id)` to release
|
|
99
|
+
early. Claims are **advisory** — they serialize on contention rather than locking.
|
|
104
100
|
|
|
105
|
-
|
|
106
|
-
that work to clear, or `ifBusy: 'fail'` to throw `AbloBusyError`.
|
|
107
|
-
|
|
108
|
-
## Intent
|
|
109
|
-
|
|
110
|
-
Intent is the coordination object — it tells humans and agents who is working on
|
|
111
|
-
a target before the write lands. Like Stripe's `PaymentIntent`, one
|
|
112
|
-
self-describing object carries the whole lifecycle in a single `status` field.
|
|
113
|
-
It lives on the **coordination plane**: ephemeral, TTL'd, broadcast to peers in
|
|
114
|
-
real time, and never persisted as a row.
|
|
115
|
-
|
|
116
|
-
Read or open one through the model accessor — `ablo.<model>.intent(id)` — which
|
|
117
|
-
sits beside `create`/`update`/`retrieve` and returns a handle **synchronously**,
|
|
118
|
-
so you can inspect who holds a target without awaiting.
|
|
119
|
-
|
|
120
|
-
### The Intent object
|
|
101
|
+
### The Claim State Object
|
|
121
102
|
|
|
122
103
|
| Field | Type | Description |
|
|
123
104
|
|---|---|---|
|
|
124
|
-
| `object` | `'
|
|
125
|
-
| `id` | string | Unique identifier for the
|
|
105
|
+
| `object` | `'claim'` | String representing the object's type. |
|
|
106
|
+
| `id` | string | Unique identifier for the claim. |
|
|
126
107
|
| `status` | `'active' \| 'committed' \| 'expired' \| 'canceled'` | The whole lifecycle, in one field. |
|
|
127
108
|
| `target` | `{ type, id, field? }` | What is being coordinated. |
|
|
128
109
|
| `action` | string | Human-readable phase — `'editing'`, `'writing'`, `'reviewing'`. |
|
|
129
|
-
| `heldBy` | string | Participant id holding the
|
|
110
|
+
| `heldBy` | string | Participant id holding the claim. |
|
|
130
111
|
| `participantKind` | `'human' \| 'agent'` | Whether a human session or an agent holds it. |
|
|
131
112
|
| `expiresAt` | string | Ms-epoch at which the server auto-expires it if the holder doesn't finish. |
|
|
132
113
|
|
|
133
114
|
```json
|
|
134
115
|
{
|
|
135
|
-
"object": "
|
|
136
|
-
"id": "
|
|
116
|
+
"object": "claim",
|
|
117
|
+
"id": "claim_3MtwBwLkdIwHu7ix",
|
|
137
118
|
"status": "active",
|
|
138
|
-
"target": { "type": "
|
|
119
|
+
"target": { "type": "weatherReports", "id": "report_stockholm", "field": "status" },
|
|
139
120
|
"action": "editing",
|
|
140
|
-
"heldBy": "agent:
|
|
121
|
+
"heldBy": "agent:report-writer",
|
|
141
122
|
"participantKind": "agent",
|
|
142
123
|
"expiresAt": "1716580000000"
|
|
143
124
|
}
|
|
@@ -146,128 +127,57 @@ so you can inspect who holds a target without awaiting.
|
|
|
146
127
|
### Lifecycle
|
|
147
128
|
|
|
148
129
|
```
|
|
149
|
-
claim()
|
|
130
|
+
claim(id) update(id) lands
|
|
150
131
|
(free) ───────────▶ active ───────────────────────▶ committed
|
|
151
132
|
│
|
|
152
133
|
┌───────────┴───────────┐
|
|
153
134
|
▼ ▼
|
|
154
135
|
canceled expired
|
|
155
|
-
|
|
136
|
+
(release w/o write) (TTL; holder died)
|
|
156
137
|
```
|
|
157
138
|
|
|
158
|
-
A target is free when `ablo.<model>.
|
|
159
|
-
states drop out of the live stream
|
|
160
|
-
`active`.
|
|
139
|
+
A target is free when `ablo.<model>.claimState(id)` is `null`. Terminal
|
|
140
|
+
states drop out of the live stream, so a present claim is active.
|
|
161
141
|
|
|
162
142
|
### Reading and claiming
|
|
163
143
|
|
|
164
|
-
|
|
165
|
-
|
|
144
|
+
`claimState(id)` is the read side for observers: synchronous, never blocks, and
|
|
145
|
+
returns the live claim state object (or `null`). `claim(id, ...)` is the write side:
|
|
146
|
+
it claims the row and returns the row. Because the claim is **advisory**, if
|
|
147
|
+
someone else already holds the row, `claim` waits for them to finish, then
|
|
148
|
+
re-reads the row before handing it back — so you always proceed from fresh state.
|
|
149
|
+
Default reads stay open; server/model reads can opt into `ifClaimed: 'wait'` or
|
|
150
|
+
`ifClaimed: 'fail'` when they should not read through active work.
|
|
166
151
|
|
|
152
|
+
```ts
|
|
167
153
|
// Read side — who is working on this target right now?
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
154
|
+
const claim = ablo.weatherReports.claimState('report_stockholm');
|
|
155
|
+
if (claim) {
|
|
156
|
+
claim.heldBy; // 'agent:report-writer'
|
|
157
|
+
claim.action; // 'editing'
|
|
172
158
|
}
|
|
173
159
|
|
|
174
|
-
// Write side — claim
|
|
175
|
-
await
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
with `AbloStaleContextError` if the row advanced past your claim point, so you
|
|
182
|
-
re-read before retrying. The intent releases automatically when `update`
|
|
183
|
-
resolves; call `task.finish()` if the work ends without a write.
|
|
184
|
-
|
|
185
|
-
`task.whenFree({ timeout })` waits until the target is free. Pass `timeout` only
|
|
186
|
-
when your product needs an upper bound.
|
|
187
|
-
|
|
188
|
-
### Cross-resource coordination
|
|
189
|
-
|
|
190
|
-
For lower-level coordination that isn't scoped to a single model row, the
|
|
191
|
-
top-level `ablo.intents` resource (`create`, `list`, `waitFor`) remains
|
|
192
|
-
available. Most callers should prefer `ablo.<model>.intent(id)`.
|
|
193
|
-
|
|
194
|
-
## Advanced Commit API
|
|
195
|
-
|
|
196
|
-
Most callers use `ablo.<model>.update(...)` or `resource.update(...)`. For atomic
|
|
197
|
-
batches or custom runtimes, use `commits.create`.
|
|
198
|
-
|
|
199
|
-
```ts
|
|
200
|
-
await ablo.commits.create({
|
|
201
|
-
wait: 'confirmed',
|
|
202
|
-
operations: [
|
|
203
|
-
{
|
|
204
|
-
action: 'update',
|
|
205
|
-
resource: 'tasks',
|
|
206
|
-
id: 'task_123',
|
|
207
|
-
data: { status: 'done' },
|
|
208
|
-
readAt: stamp,
|
|
209
|
-
onStale: 'reject',
|
|
210
|
-
},
|
|
211
|
-
],
|
|
212
|
-
});
|
|
160
|
+
// Write side — claim for the duration of the callback.
|
|
161
|
+
const updated = await ablo.weatherReports.claim(
|
|
162
|
+
'report_stockholm',
|
|
163
|
+
async (report) => ablo.weatherReports.update(report.id, { status: 'ready' }),
|
|
164
|
+
{ action: 'editing', ttl: '2m' },
|
|
165
|
+
);
|
|
166
|
+
updated.status; // 'ready'
|
|
213
167
|
```
|
|
214
168
|
|
|
215
|
-
|
|
216
|
-
|
|
169
|
+
Writes go through the normal flat `ablo.<model>.update(id, data)`. While you hold
|
|
170
|
+
a claim on `id`, that `update` is automatically stale-guarded: it rejects with
|
|
171
|
+
`AbloStaleContextError` if the row advanced past your claim point, so you re-read
|
|
172
|
+
before retrying. The callback form releases automatically when the callback
|
|
173
|
+
returns or throws, or call `ablo.weatherReports.release(id)` if you claimed manually and
|
|
174
|
+
need to release early.
|
|
217
175
|
|
|
218
176
|
## Agent
|
|
219
177
|
|
|
220
178
|
Most agents should import the same schema as the app and call
|
|
221
|
-
`ablo.<model>.load(...)
|
|
222
|
-
`
|
|
223
|
-
the app schema; it creates the capability and task internally and returns
|
|
224
|
-
`done`, `failed`, or `cancelled`.
|
|
225
|
-
|
|
226
|
-
## Data Source
|
|
227
|
-
|
|
228
|
-
Use `dataSource(...)` only when the customer's app database remains canonical
|
|
229
|
-
and Ablo should call a signed endpoint instead of storing customer rows itself.
|
|
230
|
-
|
|
231
|
-
```ts
|
|
232
|
-
import { dataSource } from '@abloatai/ablo';
|
|
233
|
-
import { schema } from './ablo.schema';
|
|
234
|
-
|
|
235
|
-
export const POST = dataSource({
|
|
236
|
-
schema,
|
|
237
|
-
apiKey: process.env.ABLO_API_KEY,
|
|
238
|
-
async commit({ operations, clientTxId, context }) {
|
|
239
|
-
// Write operations to the customer's database transaction.
|
|
240
|
-
return { rows: [] };
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
```
|
|
244
|
-
|
|
245
|
-
The SDK still uses `ablo.<model>.update(...)`. The Data Source endpoint is a
|
|
246
|
-
server-to-server storage adapter. See [Connect Your Database](./data-sources.md).
|
|
247
|
-
|
|
248
|
-
## Capability
|
|
249
|
-
|
|
250
|
-
Capabilities are the lower-level permission boundary. Most apps should let
|
|
251
|
-
`agent.run(...)` create and revoke them.
|
|
252
|
-
|
|
253
|
-
```ts
|
|
254
|
-
const capability = await api.capabilities.create({
|
|
255
|
-
participantKind: 'agent',
|
|
256
|
-
participantId: 'agent:task-writer',
|
|
257
|
-
// Strings derive from the schema's `identityRoles` templates
|
|
258
|
-
// (see integration-guide.md §1) or a model's `syncGroupFormat`.
|
|
259
|
-
syncGroups: ['org:acme', 'user:agent:task-writer'],
|
|
260
|
-
operations: ['tasks.retrieve', 'tasks.update'],
|
|
261
|
-
lease: '10m',
|
|
262
|
-
});
|
|
263
|
-
```
|
|
264
|
-
|
|
265
|
-
Use `lease` as a crash cleanup window. Normal agent runs still close when the
|
|
266
|
-
handler returns, fails, or is cancelled.
|
|
267
|
-
|
|
268
|
-
For the design rationale — why capabilities instead of static API keys,
|
|
269
|
-
why the lease + signature + revocation triple, and how this maps to AWS
|
|
270
|
-
STS / Vault / Auth0 Token Vault — see [Capabilities](./capabilities.md#why-capabilities-not-api-keys).
|
|
179
|
+
`ablo.<model>.load(...)`, `ablo.<model>.claim(...)`, and
|
|
180
|
+
`ablo.<model>.update(...)`.
|
|
271
181
|
|
|
272
182
|
## Errors
|
|
273
183
|
|
|
@@ -283,6 +193,6 @@ All SDK errors extend `AbloError` and expose a stable `type` string.
|
|
|
283
193
|
| `AbloValidationError` | Invalid input. |
|
|
284
194
|
| `AbloServerError` | Server-side 5xx. |
|
|
285
195
|
| `AbloStaleContextError` | `readAt` no longer matches current state. |
|
|
286
|
-
| `
|
|
196
|
+
| `AbloClaimedError` | Active claim conflict or claim wait timeout. |
|
|
287
197
|
|
|
288
198
|
See [Client Behavior](./client-behavior.md) for retry and timeout guidance.
|
package/docs/audit.md
CHANGED
|
@@ -12,11 +12,11 @@ tamper-evident, queryable, exportable.
|
|
|
12
12
|
actorId: string,
|
|
13
13
|
onBehalfOfKind: 'user' | 'agent' | 'system' | null,
|
|
14
14
|
onBehalfOfId: string | null,
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
credentialId: string | null,
|
|
16
|
+
credentialLabel: string | null,
|
|
17
17
|
delegationChainRoot: string | null, // always points at a human
|
|
18
|
-
|
|
19
|
-
actionType: string, // e.g. '
|
|
18
|
+
causedByRunId: string | null,
|
|
19
|
+
actionType: string, // e.g. 'weatherReport.update'
|
|
20
20
|
modelName: string | null, // e.g. 'claude-opus-4-7'
|
|
21
21
|
diffSummary: unknown,
|
|
22
22
|
// tamper-evident
|
|
@@ -37,7 +37,7 @@ a thing in this system; every chain starts with a person.
|
|
|
37
37
|
```bash
|
|
38
38
|
curl https://<your-app>/api/orgs/<slug>/audit/verify-chain?\
|
|
39
39
|
principalKind=agent\
|
|
40
|
-
&principalId=
|
|
40
|
+
&principalId=weather-agent-v3
|
|
41
41
|
```
|
|
42
42
|
|
|
43
43
|
Returns either:
|
package/docs/client-behavior.md
CHANGED
|
@@ -9,9 +9,9 @@ import Ablo from '@abloatai/ablo';
|
|
|
9
9
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
10
10
|
|
|
11
11
|
const schema = defineSchema({
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
status: z.enum(['
|
|
12
|
+
weatherReports: model({
|
|
13
|
+
location: z.string(),
|
|
14
|
+
status: z.enum(['pending', 'ready']),
|
|
15
15
|
}),
|
|
16
16
|
});
|
|
17
17
|
|
|
@@ -25,7 +25,7 @@ Common options:
|
|
|
25
25
|
|
|
26
26
|
| Option | Purpose |
|
|
27
27
|
|---|---|
|
|
28
|
-
| `schema` | Required for typed model
|
|
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
31
|
| `persistence` | `volatile` by default. Use `indexeddb` for browser durable cache and offline queueing. |
|
|
@@ -40,17 +40,17 @@ endpoint.
|
|
|
40
40
|
|
|
41
41
|
## Model Methods
|
|
42
42
|
|
|
43
|
-
Each schema model becomes a typed
|
|
43
|
+
Each schema model becomes a typed model:
|
|
44
44
|
|
|
45
45
|
```ts
|
|
46
46
|
await ablo.ready();
|
|
47
47
|
|
|
48
|
-
const [
|
|
49
|
-
const local = ablo.
|
|
48
|
+
const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
49
|
+
const local = ablo.weatherReports.retrieve('report_stockholm');
|
|
50
50
|
|
|
51
|
-
await ablo.
|
|
52
|
-
await ablo.
|
|
53
|
-
await ablo.
|
|
51
|
+
await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
|
|
52
|
+
await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
|
|
53
|
+
await ablo.weatherReports.delete('report_stockholm', { wait: 'confirmed' });
|
|
54
54
|
```
|
|
55
55
|
|
|
56
56
|
`load` is async hydration from local store and server. `retrieve`, `list`, and
|
|
@@ -63,15 +63,15 @@ rows.
|
|
|
63
63
|
|
|
64
64
|
## Multiplayer Behavior
|
|
65
65
|
|
|
66
|
-
Multiplayer works when every participant uses the same model
|
|
66
|
+
Multiplayer works when every participant uses the same model client path. A
|
|
67
67
|
human Server Action, a browser view, and an agent worker can all use
|
|
68
|
-
`ablo.
|
|
68
|
+
`ablo.weatherReports`:
|
|
69
69
|
|
|
70
70
|
```ts
|
|
71
|
-
const [
|
|
72
|
-
const snap = ablo.snapshot({
|
|
71
|
+
const [report] = await ablo.weatherReports.load({ where: { id } });
|
|
72
|
+
const snap = ablo.snapshot({ weatherReports: id });
|
|
73
73
|
|
|
74
|
-
await ablo.
|
|
74
|
+
await ablo.weatherReports.update(id, patch, {
|
|
75
75
|
readAt: snap.stamp,
|
|
76
76
|
onStale: 'reject',
|
|
77
77
|
wait: 'confirmed',
|
|
@@ -79,9 +79,9 @@ await ablo.tasks.update(id, patch, {
|
|
|
79
79
|
```
|
|
80
80
|
|
|
81
81
|
The confirmed write fans out over realtime subscriptions. React clients that use
|
|
82
|
-
`useAblo((ablo) => ablo.
|
|
83
|
-
such as `useAblo((ablo) => ablo.
|
|
84
|
-
receive active
|
|
82
|
+
`useAblo((ablo) => ablo.weatherReports.retrieve(id))` receive the new row, and selectors
|
|
83
|
+
such as `useAblo((ablo) => ablo.weatherReports.claimState(id))`
|
|
84
|
+
receive active claim state. There is
|
|
85
85
|
no extra multiplayer setup beyond routing shared state through Ablo.
|
|
86
86
|
|
|
87
87
|
If an app writes directly to its database, Ablo cannot coordinate that write
|
|
@@ -90,15 +90,14 @@ until the app reports it through Data Source events.
|
|
|
90
90
|
## Per-Write Options
|
|
91
91
|
|
|
92
92
|
```ts
|
|
93
|
-
await ablo.
|
|
94
|
-
'
|
|
95
|
-
{ status: '
|
|
93
|
+
await ablo.weatherReports.update(
|
|
94
|
+
'report_stockholm',
|
|
95
|
+
{ status: 'ready' },
|
|
96
96
|
{
|
|
97
97
|
wait: 'confirmed',
|
|
98
98
|
readAt: snap.stamp,
|
|
99
99
|
onStale: 'reject',
|
|
100
|
-
|
|
101
|
-
idempotencyKey: 'task_123:mark-done:v1',
|
|
100
|
+
idempotencyKey: 'report_stockholm:mark-ready:v1',
|
|
102
101
|
timeout: 20_000,
|
|
103
102
|
},
|
|
104
103
|
);
|
|
@@ -109,31 +108,31 @@ await ablo.tasks.update(
|
|
|
109
108
|
| `wait` | `queued` resolves after local queueing; `confirmed` waits for server acceptance. |
|
|
110
109
|
| `readAt` | State cursor the write was based on. |
|
|
111
110
|
| `onStale` | Policy when the target changed after `readAt`. Prefer `reject`. |
|
|
112
|
-
| `intent` | Active work claim associated with this write. |
|
|
113
111
|
| `idempotencyKey` | Stable key for retry-safe writes. The SDK generates one when omitted. |
|
|
114
112
|
| `timeout` | Maximum time for the write call. |
|
|
115
113
|
|
|
116
|
-
##
|
|
114
|
+
## Claimed Behavior
|
|
117
115
|
|
|
118
116
|
```ts
|
|
119
|
-
const
|
|
117
|
+
const active = ablo.weatherReports.claimState('report_stockholm');
|
|
120
118
|
|
|
121
|
-
if (
|
|
122
|
-
|
|
123
|
-
{ resource: 'tasks', id: 'task_123' },
|
|
124
|
-
{ timeout: 30_000 },
|
|
125
|
-
);
|
|
119
|
+
if (active) {
|
|
120
|
+
return { status: 'claimed', active };
|
|
126
121
|
}
|
|
122
|
+
|
|
123
|
+
await ablo.weatherReports.claim('report_stockholm', async (report) => {
|
|
124
|
+
await ablo.weatherReports.update(report.id, { status: 'ready' });
|
|
125
|
+
});
|
|
127
126
|
```
|
|
128
127
|
|
|
129
|
-
Reads never silently block. For
|
|
128
|
+
Reads never silently block. For schema model calls, use `claimState(id)` to observe
|
|
129
|
+
current work and `claim(id, work)` to serialize a write across a slow step:
|
|
130
130
|
|
|
131
|
-
- `
|
|
132
|
-
- `wait`
|
|
133
|
-
- `
|
|
131
|
+
- default `claim` waits in the fair queue and re-reads before invoking `work`;
|
|
132
|
+
- `{ wait: false }` rejects with `AbloClaimedError` instead of queuing;
|
|
133
|
+
- `{ maxQueueDepth }` rejects if the wait line is already too deep.
|
|
134
134
|
|
|
135
|
-
Schema clients use the realtime stream for waits.
|
|
136
|
-
provide `busyPollInterval` when using `ifBusy: 'wait'`.
|
|
135
|
+
Schema clients use the realtime stream for waits.
|
|
137
136
|
|
|
138
137
|
## Errors
|
|
139
138
|
|
|
@@ -149,16 +148,16 @@ All SDK errors extend `AbloError` and carry a stable `type`.
|
|
|
149
148
|
| `AbloValidationError` | Invalid input or unsupported request shape. |
|
|
150
149
|
| `AbloServerError` | Server-side 5xx. Retry with backoff if the operation is idempotent. |
|
|
151
150
|
| `AbloStaleContextError` | Write was based on stale `readAt` state. Re-read and retry. |
|
|
152
|
-
| `
|
|
151
|
+
| `AbloClaimedError` | An active claim conflicted with `{ wait: false }`, the queue was too deep, or a claim wait timed out. |
|
|
153
152
|
|
|
154
153
|
```ts
|
|
155
|
-
import {
|
|
154
|
+
import { AbloClaimedError } from '@abloatai/ablo';
|
|
156
155
|
|
|
157
156
|
try {
|
|
158
|
-
await ablo.
|
|
157
|
+
await ablo.weatherReports.update('report_stockholm', { status: 'ready' }, { wait: 'confirmed' });
|
|
159
158
|
} catch (error) {
|
|
160
|
-
if (error instanceof
|
|
161
|
-
return { status: '
|
|
159
|
+
if (error instanceof AbloClaimedError) {
|
|
160
|
+
return { status: 'claimed' };
|
|
162
161
|
}
|
|
163
162
|
throw error;
|
|
164
163
|
}
|