@abloatai/ablo 0.8.0 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +46 -1
- package/README.md +33 -28
- package/dist/BaseSyncedStore.d.ts +83 -0
- package/dist/BaseSyncedStore.js +194 -2
- package/dist/Model.d.ts +42 -0
- package/dist/Model.js +103 -44
- package/dist/agent/session.js +3 -3
- package/dist/ai-sdk/coordination-context.js +4 -0
- package/dist/ai-sdk/index.d.ts +56 -47
- package/dist/ai-sdk/index.js +56 -47
- package/dist/ai-sdk/intent-broadcast.d.ts +5 -0
- package/dist/ai-sdk/intent-broadcast.js +11 -4
- package/dist/ai-sdk/wrap.d.ts +14 -11
- package/dist/ai-sdk/wrap.js +11 -13
- package/dist/auth/credentialSource.d.ts +34 -0
- package/dist/auth/credentialSource.js +63 -0
- package/dist/auth/index.d.ts +2 -22
- package/dist/auth/index.js +4 -42
- package/dist/auth/schemas.d.ts +35 -0
- package/dist/auth/schemas.js +53 -0
- package/dist/client/Ablo.d.ts +160 -42
- package/dist/client/Ablo.js +145 -75
- package/dist/client/ApiClient.d.ts +20 -4
- package/dist/client/ApiClient.js +166 -28
- package/dist/client/auth.d.ts +14 -5
- package/dist/client/auth.js +60 -7
- package/dist/client/createInternalComponents.d.ts +2 -0
- package/dist/client/createInternalComponents.js +8 -1
- package/dist/client/createModelProxy.d.ts +130 -66
- package/dist/client/createModelProxy.js +152 -49
- package/dist/client/httpClient.d.ts +71 -0
- package/dist/client/httpClient.js +69 -0
- package/dist/client/identity.d.ts +2 -6
- package/dist/client/identity.js +49 -11
- package/dist/client/index.d.ts +1 -0
- package/dist/client/index.js +1 -0
- package/dist/client/registerDataSource.d.ts +3 -3
- package/dist/client/registerDataSource.js +11 -9
- package/dist/client/validateAbloOptions.js +1 -1
- package/dist/core/DatabaseManager.js +30 -2
- package/dist/core/openIDBWithTimeout.d.ts +36 -0
- package/dist/core/openIDBWithTimeout.js +88 -1
- package/dist/errorCodes.d.ts +70 -1
- package/dist/errorCodes.js +108 -9
- package/dist/errors.d.ts +2 -2
- package/dist/errors.js +72 -22
- package/dist/index.d.ts +17 -8
- package/dist/index.js +15 -6
- package/dist/keys/index.d.ts +16 -1
- package/dist/keys/index.js +26 -6
- package/dist/mutators/UndoManager.d.ts +158 -50
- package/dist/mutators/UndoManager.js +345 -22
- package/dist/mutators/inverseOp.d.ts +129 -0
- package/dist/mutators/inverseOp.js +74 -0
- package/dist/mutators/readerActions.d.ts +1 -1
- package/dist/mutators/undoApply.d.ts +42 -0
- package/dist/mutators/undoApply.js +143 -0
- package/dist/query/client.d.ts +10 -9
- package/dist/query/client.js +3 -6
- package/dist/react/AbloProvider.d.ts +23 -126
- package/dist/react/AbloProvider.js +62 -199
- package/dist/react/context.d.ts +31 -0
- package/dist/react/useAblo.d.ts +2 -2
- package/dist/react/useCurrentUserId.d.ts +1 -1
- package/dist/react/useCurrentUserId.js +1 -1
- package/dist/react/useMutators.js +19 -12
- package/dist/schema/ddl.d.ts +34 -3
- package/dist/schema/ddl.js +162 -4
- package/dist/schema/index.d.ts +5 -1
- package/dist/schema/index.js +13 -1
- package/dist/schema/model.d.ts +11 -0
- package/dist/schema/model.js +2 -0
- package/dist/schema/openapi.d.ts +28 -0
- package/dist/schema/openapi.js +118 -0
- package/dist/schema/plane.d.ts +23 -0
- package/dist/schema/plane.js +19 -0
- package/dist/schema/relation.d.ts +20 -0
- package/dist/schema/serialize.d.ts +4 -0
- package/dist/schema/serialize.js +4 -0
- package/dist/schema/sync-delta-row.d.ts +157 -0
- package/dist/schema/sync-delta-row.js +102 -0
- package/dist/schema/sync-delta-wire.d.ts +180 -0
- package/dist/schema/sync-delta-wire.js +102 -0
- package/dist/server/adapter.d.ts +156 -0
- package/dist/server/adapter.js +19 -0
- package/dist/server/commit.d.ts +82 -0
- package/dist/server/commit.js +1 -0
- package/dist/server/index.d.ts +14 -0
- package/dist/server/index.js +1 -0
- package/dist/server/next.d.ts +51 -0
- package/dist/server/next.js +47 -0
- package/dist/server/read-config.d.ts +60 -0
- package/dist/server/read-config.js +8 -0
- package/dist/server/storage-mode.d.ts +17 -0
- package/dist/server/storage-mode.js +12 -0
- package/dist/source/adapter.d.ts +65 -0
- package/dist/source/adapter.js +20 -0
- package/dist/source/adapters/drizzle.d.ts +43 -0
- package/dist/source/adapters/drizzle.js +185 -0
- package/dist/source/adapters/memory.d.ts +12 -0
- package/dist/source/adapters/memory.js +114 -0
- package/dist/source/adapters/prisma.d.ts +57 -0
- package/dist/source/adapters/prisma.js +176 -0
- package/dist/source/conformance.d.ts +32 -0
- package/dist/source/conformance.js +134 -0
- package/dist/source/contract.d.ts +144 -0
- package/dist/source/contract.js +99 -0
- package/dist/source/index.d.ts +62 -10
- package/dist/source/index.js +99 -0
- package/dist/source/migrations.d.ts +14 -0
- package/dist/source/migrations.js +39 -0
- package/dist/source/next.d.ts +33 -0
- package/dist/source/next.js +26 -0
- package/dist/sync/BootstrapHelper.d.ts +10 -0
- package/dist/sync/BootstrapHelper.js +10 -15
- package/dist/sync/ConnectionManager.d.ts +55 -1
- package/dist/sync/ConnectionManager.js +155 -16
- package/dist/sync/HydrationCoordinator.d.ts +93 -17
- package/dist/sync/HydrationCoordinator.js +238 -39
- package/dist/sync/NetworkProbe.d.ts +58 -24
- package/dist/sync/NetworkProbe.js +118 -42
- package/dist/sync/SyncWebSocket.d.ts +45 -70
- package/dist/sync/SyncWebSocket.js +70 -36
- package/dist/sync/createIntentStream.js +10 -1
- package/dist/types/streams.d.ts +9 -0
- package/dist/utils/mobx-setup.js +1 -0
- package/dist/webhooks/events.d.ts +38 -0
- package/dist/webhooks/events.js +40 -0
- package/dist/webhooks/index.d.ts +10 -0
- package/dist/webhooks/index.js +10 -0
- package/dist/wire/errorEnvelope.d.ts +34 -0
- package/dist/wire/errorEnvelope.js +86 -0
- package/dist/wire/frames.d.ts +119 -0
- package/dist/wire/frames.js +1 -0
- package/dist/wire/index.d.ts +24 -0
- package/dist/wire/index.js +21 -0
- package/dist/wire/listEnvelope.d.ts +45 -0
- package/dist/wire/listEnvelope.js +17 -0
- package/docs/api.md +47 -44
- package/docs/cli.md +44 -44
- package/docs/client-behavior.md +30 -30
- package/docs/coordination.md +33 -36
- package/docs/data-sources.md +35 -15
- package/docs/examples/agent-human.md +45 -43
- package/docs/examples/ai-sdk-tool.md +20 -16
- package/docs/examples/existing-python-backend.md +16 -12
- package/docs/examples/nextjs.md +14 -12
- package/docs/examples/scoped-agent.md +1 -1
- package/docs/examples/server-agent.md +24 -21
- package/docs/guarantees.md +15 -13
- package/docs/index.md +2 -2
- package/docs/integration-guide.md +30 -30
- package/docs/interaction-model.md +19 -23
- package/docs/mcp/claude-code.md +3 -3
- package/docs/mcp/cursor.md +1 -1
- package/docs/mcp/windsurf.md +2 -2
- package/docs/mcp.md +6 -6
- package/docs/quickstart.md +41 -31
- package/docs/react.md +13 -9
- package/docs/schema-contract.md +12 -10
- package/docs/the-loop.md +21 -0
- package/examples/data-source/README.md +4 -5
- package/examples/data-source/customer-server.ts +27 -25
- package/llms.txt +28 -5
- package/package.json +43 -3
|
@@ -12,16 +12,17 @@ The same reports are edited by both humans and agents. They must not collide:
|
|
|
12
12
|
the human's newer edit.
|
|
13
13
|
|
|
14
14
|
A **claim** does both jobs. Claims don't lock — if another writer holds the row,
|
|
15
|
-
`claim` waits for them, re-reads the fresh row, then hands it to you
|
|
16
|
-
writers serialize instead of clobbering.
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
you
|
|
15
|
+
`claim` waits for them, re-reads the fresh row, then hands it back to you on
|
|
16
|
+
`claim.data`, so two writers serialize instead of clobbering. The handle is an
|
|
17
|
+
`AsyncDisposable`: hold it with `await using` and it releases on scope exit. And
|
|
18
|
+
once you hold a claim, any `update` you make while it's held is stale-checked for
|
|
19
|
+
free: the SDK records the row version you were handed and rejects the write with
|
|
20
|
+
a typed error if the row moved underneath you while the agent was busy.
|
|
20
21
|
|
|
21
22
|
## Schema-Backed Worker
|
|
22
23
|
|
|
23
24
|
The worker uses the same schema client the app uses. It reads the report from
|
|
24
|
-
the server with `retrieve(id)`, claims the row, and writes through
|
|
25
|
+
the server with `retrieve({ id })`, claims the row, and writes through
|
|
25
26
|
`ablo.weatherReports.update(...)` with a stale-check so a human's concurrent edit
|
|
26
27
|
can't be overwritten.
|
|
27
28
|
|
|
@@ -41,42 +42,43 @@ const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
|
41
42
|
export async function markReady(reportId: string) {
|
|
42
43
|
await ablo.ready();
|
|
43
44
|
|
|
44
|
-
// retrieve(id) is an async server read — await it.
|
|
45
|
-
const report = await ablo.weatherReports.retrieve(reportId);
|
|
45
|
+
// retrieve({ id }) is an async server read — await it.
|
|
46
|
+
const report = await ablo.weatherReports.retrieve({ id: reportId });
|
|
46
47
|
if (!report) return { status: 'not_found' };
|
|
47
48
|
|
|
48
49
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
},
|
|
79
|
-
|
|
50
|
+
// wait: false → don't queue behind a current holder. If a human already
|
|
51
|
+
// holds the row, claim rejects with AbloClaimedError (caught below), so the
|
|
52
|
+
// agent yields instead of waiting. Omit it, or pass wait: true, to queue
|
|
53
|
+
// behind them. action → the label observers see while we work.
|
|
54
|
+
await using claim = await ablo.weatherReports.claim({
|
|
55
|
+
id: reportId,
|
|
56
|
+
wait: false,
|
|
57
|
+
action: 'marking_ready',
|
|
58
|
+
});
|
|
59
|
+
const claimed = claim.data;
|
|
60
|
+
|
|
61
|
+
// Inside an active claim, `update` is stale-checked automatically: the SDK
|
|
62
|
+
// attaches the claim's snapshot version as `readAt` and sets
|
|
63
|
+
// `onStale: 'reject'`. The write below is therefore equivalent to passing
|
|
64
|
+
// those options yourself:
|
|
65
|
+
//
|
|
66
|
+
// ablo.weatherReports.update({
|
|
67
|
+
// id: claimed.id,
|
|
68
|
+
// data: { status: 'ready' },
|
|
69
|
+
// wait: 'confirmed',
|
|
70
|
+
// readAt: <claim snapshot version>,
|
|
71
|
+
// onStale: 'reject',
|
|
72
|
+
// });
|
|
73
|
+
//
|
|
74
|
+
// If a human saved a newer version mid-run, the row no longer matches
|
|
75
|
+
// `readAt`, so the server rejects this commit with AbloStaleContextError
|
|
76
|
+
// (caught below) instead of clobbering their edit.
|
|
77
|
+
const updated = await ablo.weatherReports.update({
|
|
78
|
+
id: claimed.id,
|
|
79
|
+
data: { status: 'ready' },
|
|
80
|
+
wait: 'confirmed',
|
|
81
|
+
});
|
|
80
82
|
|
|
81
83
|
return { status: 'ready', report: updated };
|
|
82
84
|
} catch (err) {
|
|
@@ -101,7 +103,7 @@ import { useAblo } from '@abloatai/ablo/react';
|
|
|
101
103
|
|
|
102
104
|
export function ReportRow({ report: serverReport }: Props) {
|
|
103
105
|
const data = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
|
|
104
|
-
const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
|
|
106
|
+
const active = useAblo((ablo) => ablo.weatherReports.claim.state({ id: serverReport.id }));
|
|
105
107
|
const agentActive = active?.participantKind === 'agent';
|
|
106
108
|
|
|
107
109
|
return (
|
|
@@ -116,10 +118,10 @@ export function ReportRow({ report: serverReport }: Props) {
|
|
|
116
118
|
## Why It Works
|
|
117
119
|
|
|
118
120
|
- The claim is visible to everyone: the UI reads it synchronously with
|
|
119
|
-
`claim.state(id)`, and it also arrives over the live stream.
|
|
120
|
-
- `claim(id
|
|
121
|
+
`claim.state({ id })`, and it also arrives over the live stream.
|
|
122
|
+
- `claim({ id })` makes writers take turns instead of racing — with
|
|
121
123
|
`wait: false`, the agent simply yields when a human already holds the row.
|
|
122
|
-
- The `update`
|
|
124
|
+
- The `update` made while the claim is held is stale-checked automatically, so a human's
|
|
123
125
|
edit landing mid-run rejects the agent's write with a typed
|
|
124
126
|
`AbloStaleContextError` instead of overwriting it.
|
|
125
127
|
- That same write carries the claim, so each accepted change is attributed to
|
|
@@ -34,26 +34,30 @@ const updateReport = tool({
|
|
|
34
34
|
await ablo.ready();
|
|
35
35
|
|
|
36
36
|
// retrieve hits the server for the latest row (async — await it).
|
|
37
|
-
const report = await ablo.weatherReports.retrieve(reportId);
|
|
37
|
+
const report = await ablo.weatherReports.retrieve({ id: reportId });
|
|
38
38
|
if (!report) return { ok: false, reason: 'not_found' };
|
|
39
39
|
|
|
40
40
|
// If another agent or user already holds this row, claim waits for them
|
|
41
|
-
// to finish, re-reads the fresh row, then
|
|
42
|
-
// is released when
|
|
43
|
-
|
|
44
|
-
reportId,
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
status: status ?? claimed.status,
|
|
50
|
-
forecast: forecast ?? claimed.forecast,
|
|
51
|
-
});
|
|
41
|
+
// to finish, re-reads the fresh row, then hands it back on `claim.data`.
|
|
42
|
+
// The claim is released automatically when it goes out of scope.
|
|
43
|
+
await using claim = await ablo.weatherReports.claim({
|
|
44
|
+
id: reportId,
|
|
45
|
+
action: 'editing',
|
|
46
|
+
ttl: '2m',
|
|
47
|
+
});
|
|
48
|
+
const claimed = claim.data;
|
|
52
49
|
|
|
53
|
-
|
|
50
|
+
// Because you hold the claim, this update is rejected if the row
|
|
51
|
+
// changed underneath you, instead of silently overwriting it.
|
|
52
|
+
const updated = await ablo.weatherReports.update({
|
|
53
|
+
id: claimed.id,
|
|
54
|
+
data: {
|
|
55
|
+
status: status ?? claimed.status,
|
|
56
|
+
forecast: forecast ?? claimed.forecast,
|
|
54
57
|
},
|
|
55
|
-
|
|
56
|
-
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
return { ok: true, report: updated };
|
|
57
61
|
},
|
|
58
62
|
});
|
|
59
63
|
|
|
@@ -73,5 +77,5 @@ The model provider is interchangeable. What matters is that the tool:
|
|
|
73
77
|
- reads the latest weather report with `retrieve` (a server read),
|
|
74
78
|
- claims the row — if someone else holds it, the claim waits for them, then re-reads,
|
|
75
79
|
- writes through `update`, which is rejected if the row changed underneath you,
|
|
76
|
-
- releases the claim automatically when the
|
|
80
|
+
- releases the claim automatically when the handle goes out of scope,
|
|
77
81
|
- waits for server confirmation.
|
|
@@ -87,7 +87,7 @@ export function ReportRow({
|
|
|
87
87
|
report: { id: string; location: string; status: string };
|
|
88
88
|
}) {
|
|
89
89
|
const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
|
|
90
|
-
const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
|
|
90
|
+
const active = useAblo((ablo) => ablo.weatherReports.claim.state({ id: serverReport.id }));
|
|
91
91
|
const claimed = Boolean(active);
|
|
92
92
|
|
|
93
93
|
return (
|
|
@@ -213,11 +213,13 @@ The app does not need a flag-day rewrite. Move one model at a time.
|
|
|
213
213
|
```ts
|
|
214
214
|
const snap = ablo.snapshot({ weatherReports: reportId });
|
|
215
215
|
|
|
216
|
-
await ablo.weatherReports.update(
|
|
217
|
-
reportId,
|
|
218
|
-
{ status: 'ready' },
|
|
219
|
-
|
|
220
|
-
|
|
216
|
+
await ablo.weatherReports.update({
|
|
217
|
+
id: reportId,
|
|
218
|
+
data: { status: 'ready' },
|
|
219
|
+
readAt: snap.stamp,
|
|
220
|
+
onStale: 'reject',
|
|
221
|
+
wait: 'confirmed',
|
|
222
|
+
});
|
|
221
223
|
```
|
|
222
224
|
|
|
223
225
|
Use `readAt` and `onStale: 'reject'` for actions that depend on state the user
|
|
@@ -247,14 +249,16 @@ and timestamp. If the change originated from an Ablo commit, include the same
|
|
|
247
249
|
Agents use the same model API as the UI:
|
|
248
250
|
|
|
249
251
|
```ts
|
|
250
|
-
const report = await ablo.weatherReports.retrieve(reportId);
|
|
252
|
+
const report = await ablo.weatherReports.retrieve({ id: reportId });
|
|
251
253
|
const snap = ablo.snapshot({ weatherReports: reportId });
|
|
252
254
|
|
|
253
|
-
await ablo.weatherReports.update(
|
|
254
|
-
reportId,
|
|
255
|
-
{ status: 'ready' },
|
|
256
|
-
|
|
257
|
-
|
|
255
|
+
await ablo.weatherReports.update({
|
|
256
|
+
id: reportId,
|
|
257
|
+
data: { status: 'ready' },
|
|
258
|
+
readAt: snap.stamp,
|
|
259
|
+
onStale: 'reject',
|
|
260
|
+
wait: 'confirmed',
|
|
261
|
+
});
|
|
258
262
|
```
|
|
259
263
|
|
|
260
264
|
Agents reach for the exact same calls the UI does — the same write contract
|
package/docs/examples/nextjs.md
CHANGED
|
@@ -38,7 +38,7 @@ export default async function ReportPage({
|
|
|
38
38
|
params,
|
|
39
39
|
}: { params: { id: string } }) {
|
|
40
40
|
await ablo.ready();
|
|
41
|
-
const report = await ablo.weatherReports.retrieve(params.id);
|
|
41
|
+
const report = await ablo.weatherReports.retrieve({ id: params.id });
|
|
42
42
|
if (!report) return null;
|
|
43
43
|
|
|
44
44
|
return <ReportEditor report={report} />;
|
|
@@ -54,22 +54,24 @@ export default async function ReportPage({
|
|
|
54
54
|
import { ablo } from '@/lib/ablo';
|
|
55
55
|
|
|
56
56
|
export async function markReady(id: string) {
|
|
57
|
-
|
|
57
|
+
await using claim = await ablo.weatherReports.claim({
|
|
58
58
|
id,
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
59
|
+
wait: false,
|
|
60
|
+
action: 'marking_ready',
|
|
61
|
+
});
|
|
62
|
+
const claimed = claim.data;
|
|
63
|
+
|
|
64
|
+
const report = await ablo.weatherReports.update({
|
|
65
|
+
id: claimed.id,
|
|
66
|
+
data: { status: 'ready' },
|
|
67
|
+
wait: 'confirmed',
|
|
68
|
+
});
|
|
67
69
|
|
|
68
70
|
return { status: 'ready', report };
|
|
69
71
|
}
|
|
70
72
|
```
|
|
71
73
|
|
|
72
|
-
The write runs
|
|
74
|
+
The write runs while the `claim` is held. If another participant commits
|
|
73
75
|
between the read and the write, the commit is rejected because the row changed
|
|
74
76
|
underneath you. The action can re-fetch and ask the user to retry.
|
|
75
77
|
|
|
@@ -82,7 +84,7 @@ import { useAblo } from '@abloatai/ablo/react';
|
|
|
82
84
|
|
|
83
85
|
export function ReportEditor({ report: serverReport }: Props) {
|
|
84
86
|
const data = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
|
|
85
|
-
const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
|
|
87
|
+
const active = useAblo((ablo) => ablo.weatherReports.claim.state({ id: serverReport.id }));
|
|
86
88
|
const claimed = Boolean(active);
|
|
87
89
|
|
|
88
90
|
return (
|
|
@@ -78,7 +78,7 @@ const ablo = useAblo<(typeof schema)['models']>();
|
|
|
78
78
|
// Other participants subscribed to deck:<deckId> — the human in the editor,
|
|
79
79
|
// a reviewer agent — receive this delta in realtime. Participants on other
|
|
80
80
|
// decks never see it.
|
|
81
|
-
await ablo.slides.update(slideId, { body: 'Q4 revenue up 12% YoY' });
|
|
81
|
+
await ablo.slides.update({ id: slideId, data: { body: 'Q4 revenue up 12% YoY' } });
|
|
82
82
|
```
|
|
83
83
|
|
|
84
84
|
The slide's delta is stamped `deck:<deckId>`, derived server-side from the
|
|
@@ -5,12 +5,13 @@ reads and writes your app's records outside the browser. The hard part is doing
|
|
|
5
5
|
it without racing the live UI: if your worker and a user edit the same report at
|
|
6
6
|
once, one write clobbers the other. This is what `claim()` is for. Below, a
|
|
7
7
|
worker finishes a weather report by claiming it, writing the result, and
|
|
8
|
-
releasing it
|
|
8
|
+
releasing it automatically when the claim goes out of scope.
|
|
9
9
|
|
|
10
|
-
`claim(id
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
`claim({ id })` takes the record for your worker and returns a disposable handle:
|
|
11
|
+
the fresh post-lease row is on `claim.data`, and holding the handle with
|
|
12
|
+
`await using` releases the claim on scope exit (or call `claim.release()`). Claims
|
|
13
|
+
don't lock. If another writer holds the row, `claim` waits for them, re-reads the
|
|
14
|
+
fresh row, then hands it to you — so two writers serialize instead of clobbering.
|
|
14
15
|
|
|
15
16
|
```ts
|
|
16
17
|
import Ablo from '@abloatai/ablo';
|
|
@@ -32,35 +33,37 @@ const ablo = Ablo({
|
|
|
32
33
|
export async function completeReport(reportId: string) {
|
|
33
34
|
await ablo.ready();
|
|
34
35
|
|
|
35
|
-
const report = await ablo.weatherReports.retrieve(reportId);
|
|
36
|
+
const report = await ablo.weatherReports.retrieve({ id: reportId });
|
|
36
37
|
if (!report) return { status: 'not_found' };
|
|
37
38
|
|
|
38
|
-
|
|
39
|
-
reportId,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
39
|
+
await using claim = await ablo.weatherReports.claim({
|
|
40
|
+
id: reportId,
|
|
41
|
+
wait: false,
|
|
42
|
+
action: 'completing',
|
|
43
|
+
});
|
|
44
|
+
const claimed = claim.data;
|
|
45
|
+
|
|
46
|
+
const updated = await ablo.weatherReports.update({
|
|
47
|
+
id: claimed.id,
|
|
48
|
+
data: { status: 'ready' },
|
|
49
|
+
wait: 'confirmed',
|
|
50
|
+
});
|
|
48
51
|
|
|
49
52
|
return { status: 'ready', report: updated };
|
|
50
53
|
}
|
|
51
54
|
```
|
|
52
55
|
|
|
53
|
-
`retrieve(id)` is an async server read — it hits the server and returns the
|
|
54
|
-
(or `null`, which the early `not_found` guard handles). The update runs
|
|
55
|
-
the claim
|
|
56
|
-
|
|
56
|
+
`retrieve({ id })` is an async server read — it hits the server and returns the
|
|
57
|
+
row (or `null`, which the early `not_found` guard handles). The update runs while
|
|
58
|
+
the claim is held, and `wait: 'confirmed'` makes that update resolve only once
|
|
59
|
+
the server has accepted it.
|
|
57
60
|
|
|
58
61
|
The two options on the claim:
|
|
59
62
|
|
|
60
63
|
- `wait: false` — skip this record if another claim is already in progress,
|
|
61
64
|
rather than queueing behind it. (The default queues.)
|
|
62
65
|
- `action: 'completing'` — a human-readable label for what your worker is doing,
|
|
63
|
-
visible to anyone reading `claim.state(id)`.
|
|
66
|
+
visible to anyone reading `claim.state({ id })`.
|
|
64
67
|
|
|
65
68
|
Because the worker uses the same schema and `claim()` as the UI, its writes sync
|
|
66
69
|
to every connected client in real time and never collide with edits already in
|
package/docs/guarantees.md
CHANGED
|
@@ -16,11 +16,11 @@ of clobbering.
|
|
|
16
16
|
the authoritative sync cursor.
|
|
17
17
|
|
|
18
18
|
```ts
|
|
19
|
-
const updated = await ablo.weatherReports.update(
|
|
20
|
-
'report_stockholm',
|
|
21
|
-
{ status: 'ready' },
|
|
22
|
-
|
|
23
|
-
);
|
|
19
|
+
const updated = await ablo.weatherReports.update({
|
|
20
|
+
id: 'report_stockholm',
|
|
21
|
+
data: { status: 'ready' },
|
|
22
|
+
wait: 'confirmed',
|
|
23
|
+
});
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
If the call resolves, the write was accepted by the server. If it rejects, the
|
|
@@ -51,11 +51,13 @@ read:
|
|
|
51
51
|
```ts
|
|
52
52
|
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
53
53
|
|
|
54
|
-
await ablo.weatherReports.update(
|
|
55
|
-
'report_stockholm',
|
|
56
|
-
{ status: 'ready' },
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
await ablo.weatherReports.update({
|
|
55
|
+
id: 'report_stockholm',
|
|
56
|
+
data: { status: 'ready' },
|
|
57
|
+
readAt: snap.stamp,
|
|
58
|
+
onStale: 'reject',
|
|
59
|
+
wait: 'confirmed',
|
|
60
|
+
});
|
|
59
61
|
```
|
|
60
62
|
|
|
61
63
|
`onStale: 'reject'` prevents lost updates. If the target changed after the
|
|
@@ -76,17 +78,17 @@ Advanced policies exist for controlled product flows:
|
|
|
76
78
|
|
|
77
79
|
Claims are live coordination signals. They are not database locks.
|
|
78
80
|
|
|
79
|
-
`ablo.<model>.claim(id
|
|
81
|
+
`ablo.<model>.claim({ id })` serializes on contention: if another human or agent
|
|
80
82
|
already holds the row, the claim waits for them to finish, then re-reads the row
|
|
81
83
|
before handing it back, so you proceed from fresh state. Reads stay open while a
|
|
82
|
-
claim is held — `ablo.<model>.claim.state(id)` returns the current claim state
|
|
84
|
+
claim is held — `ablo.<model>.claim.state({ id })` returns the current claim state
|
|
83
85
|
(or `null`) without ever blocking. A server read can pass `ifClaimed: 'wait'` to
|
|
84
86
|
wait for the claim to clear, or `ifClaimed: 'fail'` to error out, when it should
|
|
85
87
|
not return a row while someone else is mid-edit.
|
|
86
88
|
|
|
87
89
|
A claim does not reject or block other writers; it announces work so peers
|
|
88
90
|
serialize behind it rather than racing. While you hold a claim, the matching
|
|
89
|
-
`ablo.<model>.update(id, ...)` is rejected with `AbloStaleContextError` if the row
|
|
91
|
+
`ablo.<model>.update({ id, ... })` is rejected with `AbloStaleContextError` if the row
|
|
90
92
|
changed underneath you after your claim point.
|
|
91
93
|
|
|
92
94
|
## Agent Runs
|
package/docs/index.md
CHANGED
|
@@ -13,7 +13,7 @@ changed the row first.
|
|
|
13
13
|
|
|
14
14
|
```ts
|
|
15
15
|
// The same call, whether a person, a server action, or an agent makes it.
|
|
16
|
-
await ablo.deck.update(deckId, { title: "Q3 Strategy" });
|
|
16
|
+
await ablo.deck.update({ id: deckId, data: { title: "Q3 Strategy" } });
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
@@ -42,7 +42,7 @@ Three things stay true no matter how you use Ablo:
|
|
|
42
42
|
- [Quickstart](./quickstart.md) — Make your first schema-backed write.
|
|
43
43
|
- [Schema Contract](./schema-contract.md) — One schema becomes typed model clients, React reads, agent writes, Data Source shape, and schema push.
|
|
44
44
|
- [CLI & Migrations](./cli.md) — `init` / `migrate` / `push` / `generate`, the shared Zod→Postgres type map, and structured migration errors.
|
|
45
|
-
- [Identity & Sync Groups](./identity.md) —
|
|
45
|
+
- [Identity & Sync Groups](./identity.md) — Use your own authentication; tell Ablo who's connecting and how org / team / user map to sync-group scope.
|
|
46
46
|
- [Integration Guide](./integration-guide.md) — Choose Ablo-managed state, Data Source, React, multiplayer, and agent patterns.
|
|
47
47
|
- [Guarantees](./guarantees.md) — What confirmed writes, stale checks, and claims guarantee.
|
|
48
48
|
- [Interaction Model](./interaction-model.md) — The schema, claim, update, confirmation loop.
|
|
@@ -225,7 +225,7 @@ and waits.
|
|
|
225
225
|
```ts
|
|
226
226
|
await ablo.ready();
|
|
227
227
|
|
|
228
|
-
const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
228
|
+
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
229
229
|
if (!report) throw new Error('report not found');
|
|
230
230
|
```
|
|
231
231
|
|
|
@@ -255,7 +255,7 @@ export function ReportRow({
|
|
|
255
255
|
report: { id: string; location: string; status: string };
|
|
256
256
|
}) {
|
|
257
257
|
const report = useAblo((ablo) => ablo.weatherReports.get(serverReport.id)) ?? serverReport;
|
|
258
|
-
const active = useAblo((ablo) => ablo.weatherReports.claim.state(serverReport.id));
|
|
258
|
+
const active = useAblo((ablo) => ablo.weatherReports.claim.state({ id: serverReport.id }));
|
|
259
259
|
|
|
260
260
|
return <button disabled={Boolean(active) || report.status === 'ready'}>{report.location}</button>;
|
|
261
261
|
}
|
|
@@ -272,7 +272,7 @@ const ablo = useAblo();
|
|
|
272
272
|
For simple writes:
|
|
273
273
|
|
|
274
274
|
```ts
|
|
275
|
-
await ablo.weatherReports.update('report_stockholm', { status: 'ready' },
|
|
275
|
+
await ablo.weatherReports.update({ id: 'report_stockholm', data: { status: 'ready' }, wait: 'confirmed' });
|
|
276
276
|
```
|
|
277
277
|
|
|
278
278
|
For writes based on state the user or agent already read, snapshot first and
|
|
@@ -281,15 +281,13 @@ reject stale updates:
|
|
|
281
281
|
```ts
|
|
282
282
|
const snap = ablo.snapshot({ weatherReports: 'report_stockholm' });
|
|
283
283
|
|
|
284
|
-
await ablo.weatherReports.update(
|
|
285
|
-
'report_stockholm',
|
|
286
|
-
{ status: 'ready' },
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
);
|
|
284
|
+
await ablo.weatherReports.update({
|
|
285
|
+
id: 'report_stockholm',
|
|
286
|
+
data: { status: 'ready' },
|
|
287
|
+
readAt: snap.stamp,
|
|
288
|
+
onStale: 'reject',
|
|
289
|
+
wait: 'confirmed',
|
|
290
|
+
});
|
|
293
291
|
```
|
|
294
292
|
|
|
295
293
|
`wait: 'confirmed'` resolves after the server accepts the write. Rejections roll
|
|
@@ -407,20 +405,20 @@ lock. If another writer holds the row, `claim` waits for them, re-reads the
|
|
|
407
405
|
fresh row, then hands it to you — so two writers serialize instead of clobbering.
|
|
408
406
|
|
|
409
407
|
```ts
|
|
410
|
-
const report = await ablo.weatherReports.retrieve(reportId);
|
|
408
|
+
const report = await ablo.weatherReports.retrieve({ id: reportId });
|
|
411
409
|
if (!report) return;
|
|
412
410
|
|
|
413
|
-
await ablo.weatherReports.claim(
|
|
414
|
-
reportId,
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
},
|
|
422
|
-
|
|
423
|
-
);
|
|
411
|
+
await using claim = await ablo.weatherReports.claim({
|
|
412
|
+
id: reportId,
|
|
413
|
+
wait: false,
|
|
414
|
+
action: 'forecasting',
|
|
415
|
+
});
|
|
416
|
+
const claimed = claim.data;
|
|
417
|
+
await ablo.weatherReports.update({
|
|
418
|
+
id: claimed.id,
|
|
419
|
+
data: { status: 'ready', forecast: await getForecast(claimed) },
|
|
420
|
+
wait: 'confirmed',
|
|
421
|
+
});
|
|
424
422
|
```
|
|
425
423
|
|
|
426
424
|
Use AI SDK for the model loop. Put Ablo inside the tool that persists the final
|
|
@@ -435,11 +433,13 @@ const completeReport = tool({
|
|
|
435
433
|
}),
|
|
436
434
|
execute: async ({ reportId, forecast }) => {
|
|
437
435
|
const snap = ablo.snapshot({ weatherReports: reportId });
|
|
438
|
-
return ablo.weatherReports.update(
|
|
439
|
-
reportId,
|
|
440
|
-
{ status: 'ready', forecast },
|
|
441
|
-
|
|
442
|
-
|
|
436
|
+
return ablo.weatherReports.update({
|
|
437
|
+
id: reportId,
|
|
438
|
+
data: { status: 'ready', forecast },
|
|
439
|
+
readAt: snap.stamp,
|
|
440
|
+
onStale: 'reject',
|
|
441
|
+
wait: 'confirmed',
|
|
442
|
+
});
|
|
443
443
|
},
|
|
444
444
|
});
|
|
445
445
|
```
|
|
@@ -474,7 +474,7 @@ them.
|
|
|
474
474
|
| `create(data, options?)` | Create through the model client. |
|
|
475
475
|
| `update(id, data, options?)` | Update through the model client. |
|
|
476
476
|
| `delete(id, options?)` | Delete through the model client. |
|
|
477
|
-
| `claim.state(id)` | See who is currently working on a row (synchronous). |
|
|
477
|
+
| `claim.state({ id })` | See who is currently working on a row (synchronous). |
|
|
478
478
|
| `claim(id, work, options?)` | Wait for your turn, re-read, and hold the row while `work` runs. |
|
|
479
479
|
|
|
480
480
|
Keep first integrations on the model methods above.
|
|
@@ -9,11 +9,10 @@ Here's the whole path in one block — claim a row, update it inside the claim,
|
|
|
9
9
|
let the claim release when your callback returns:
|
|
10
10
|
|
|
11
11
|
```ts
|
|
12
|
-
const report = await ablo.weatherReports.retrieve('report_stockholm');
|
|
12
|
+
const report = await ablo.weatherReports.retrieve({ id: 'report_stockholm' });
|
|
13
13
|
|
|
14
|
-
await ablo.weatherReports.claim('report_stockholm'
|
|
15
|
-
|
|
16
|
-
});
|
|
14
|
+
await using claim = await ablo.weatherReports.claim({ id: 'report_stockholm' });
|
|
15
|
+
await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready' }, wait: 'confirmed' });
|
|
17
16
|
```
|
|
18
17
|
|
|
19
18
|
Claims don't lock. If another writer holds the row, `claim` waits for them,
|
|
@@ -26,7 +25,7 @@ of clobbering.
|
|
|
26
25
|
|---|---|---|
|
|
27
26
|
| `Schema` | State | Declares typed models the app and agents can read and write. |
|
|
28
27
|
| `Model` | State | The generated `ablo.<model>` model. Use `retrieve`/`list` (async server reads), `get`/`getAll`/`getCount` (synchronous local reads), `create`, `update`, and `delete`. |
|
|
29
|
-
| `Claim` | Coordination | Who is working on a target. Taken via `ablo.<model>.claim(id
|
|
28
|
+
| `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. |
|
|
30
29
|
| `Commit` | Protocol | The durable write underneath model updates. Most users do not call it directly. |
|
|
31
30
|
| `Receipt` | Protocol | The lower-level durable result for custom runtimes. Schema writes use `wait: 'confirmed'`. |
|
|
32
31
|
|
|
@@ -50,14 +49,13 @@ expect just `Commit`; here's what the other two buy you over that minimum:
|
|
|
50
49
|
A normal schema-backed run is:
|
|
51
50
|
|
|
52
51
|
```ts
|
|
53
|
-
const report = await ablo.weatherReports.retrieve(id);
|
|
54
|
-
const active = ablo.weatherReports.claim.state(id);
|
|
55
|
-
await ablo.weatherReports.claim(id
|
|
56
|
-
|
|
57
|
-
});
|
|
52
|
+
const report = await ablo.weatherReports.retrieve({ id });
|
|
53
|
+
const active = ablo.weatherReports.claim.state({ id });
|
|
54
|
+
await using claim = await ablo.weatherReports.claim({ id });
|
|
55
|
+
await ablo.weatherReports.update({ id: claim.data.id, data: patch, wait: 'confirmed' });
|
|
58
56
|
```
|
|
59
57
|
|
|
60
|
-
`retrieve(id)` is an async server read (await it). `claim.state(id)` is a
|
|
58
|
+
`retrieve({ id })` is an async server read (await it). `claim.state({ id })` is a
|
|
61
59
|
synchronous local read of who currently holds the row — it never blocks.
|
|
62
60
|
|
|
63
61
|
## Coordination
|
|
@@ -65,24 +63,22 @@ synchronous local read of who currently holds the row — it never blocks.
|
|
|
65
63
|
> Loop view only. Full claim reference — methods, the claim-state object, the
|
|
66
64
|
> `claim.queue`, errors — is [Coordination](./coordination.md).
|
|
67
65
|
|
|
68
|
-
Claims broadcast across the org. Call `claim(id
|
|
69
|
-
|
|
70
|
-
when the
|
|
66
|
+
Claims broadcast across the org. Call `claim({ id })`, do your writes with the
|
|
67
|
+
normal `update` inside the `await using` scope, and the claim releases
|
|
68
|
+
automatically when the scope exits:
|
|
71
69
|
|
|
72
70
|
```ts
|
|
73
|
-
await ablo.weatherReports.claim(
|
|
74
|
-
'report_stockholm',
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
{ action: 'editing' },
|
|
79
|
-
);
|
|
71
|
+
await using claim = await ablo.weatherReports.claim({
|
|
72
|
+
id: 'report_stockholm',
|
|
73
|
+
action: 'editing',
|
|
74
|
+
});
|
|
75
|
+
await ablo.weatherReports.update({ id: claim.data.id, data: { status: 'ready' } }); // rejected if the row changed under the claim
|
|
80
76
|
```
|
|
81
77
|
|
|
82
|
-
`ablo.weatherReports.claim.state('report_stockholm')` reads the live claim (or
|
|
78
|
+
`ablo.weatherReports.claim.state({ id: 'report_stockholm' })` reads the live claim (or
|
|
83
79
|
`null`) without blocking. Claims don't lock: if another participant holds the
|
|
84
80
|
row, `claim` waits for them to finish, re-reads, and then hands you the fresh
|
|
85
|
-
row. The same signal is visible to every schema client through `claim.state(id)`
|
|
81
|
+
row. The same signal is visible to every schema client through `claim.state({ id })`
|
|
86
82
|
and the live claim stream.
|
|
87
83
|
|
|
88
84
|
## Conflict resolution
|