@abloatai/ablo 0.5.0 → 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 +22 -0
- package/README.md +242 -135
- 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/mcp.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Model Context Protocol
|
|
2
2
|
|
|
3
|
-
Ablo
|
|
4
|
-
assistant — Claude Code, Cursor, Windsurf — and your sync
|
|
3
|
+
Ablo ships an MCP server at `/api/mcp`. Connect any MCP-compatible AI
|
|
4
|
+
assistant — Claude Code, Cursor, Windsurf — and your sync models become
|
|
5
5
|
typed, callable tools.
|
|
6
6
|
|
|
7
7
|
## Install
|
|
@@ -14,38 +14,23 @@ Pick your client:
|
|
|
14
14
|
|
|
15
15
|
## How it works
|
|
16
16
|
|
|
17
|
-
Each
|
|
17
|
+
Each model you declare becomes one or more MCP tools:
|
|
18
18
|
|
|
19
|
-
|
|
|
19
|
+
| Model method | MCP tool name | What it does |
|
|
20
20
|
|---|---|---|
|
|
21
|
-
| `retrieve` | `<
|
|
22
|
-
| `list` | `<
|
|
23
|
-
| `update` | `<
|
|
24
|
-
| `<model>.
|
|
21
|
+
| `retrieve` | `<model>.retrieve` | Returns the row + a stamp. |
|
|
22
|
+
| `list` | `<model>.list` | Cursor-paginated discovery. |
|
|
23
|
+
| `update` | `<model>.update` | Write, requires the prior stamp. |
|
|
24
|
+
| `<model>.claim` | `claim.create` | Claim a row before writing, then release when held work finishes. |
|
|
25
25
|
|
|
26
26
|
The assistant gets typed JSON schemas, real argument types, and typed
|
|
27
27
|
rejections when it writes stale state. No invention, no hallucinated IDs.
|
|
28
28
|
|
|
29
29
|
## Auth
|
|
30
30
|
|
|
31
|
-
The MCP transport
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```ts
|
|
35
|
-
const capability = await ablo.capabilities.create({
|
|
36
|
-
participantKind: 'agent',
|
|
37
|
-
participantId: 'agent:claude-code',
|
|
38
|
-
// Strings derive from the schema's `identityRoles` templates
|
|
39
|
-
// (see integration-guide.md §1).
|
|
40
|
-
syncGroups: ['org:acme'],
|
|
41
|
-
operations: ['tasks.retrieve', 'tasks.update'],
|
|
42
|
-
label: 'claude-code dev session',
|
|
43
|
-
lease: '8h',
|
|
44
|
-
});
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
Pass `capability.token` into the MCP client's auth header configuration.
|
|
48
|
-
See your client's setup guide for the exact mechanism.
|
|
31
|
+
The MCP transport uses a scoped bearer token issued by your server. Pass that
|
|
32
|
+
token into the MCP client's auth header configuration. See your client's setup
|
|
33
|
+
guide for the exact mechanism.
|
|
49
34
|
|
|
50
35
|
## Limits
|
|
51
36
|
|
package/docs/quickstart.md
CHANGED
|
@@ -21,7 +21,7 @@ export ABLO_API_KEY=sk_test_...
|
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
`ABLO_API_KEY` is for trusted server runtimes. Browser apps should use the React
|
|
24
|
-
provider with a scoped
|
|
24
|
+
provider with a scoped session route, not a bundled API key.
|
|
25
25
|
|
|
26
26
|
## 3. Declare a Schema
|
|
27
27
|
|
|
@@ -44,7 +44,7 @@ export const ablo = Ablo({
|
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
Customer apps should always pass `schema`. Treat it like Prisma's schema file:
|
|
47
|
-
it is the source of truth for typed model
|
|
47
|
+
it is the source of truth for typed model clients, realtime subscriptions,
|
|
48
48
|
agent writes, and Data Source requests.
|
|
49
49
|
|
|
50
50
|
## 4. Create and Update
|
|
@@ -81,71 +81,65 @@ ABLO_API_KEY=sk_test_... npx tsx quickstart.ts
|
|
|
81
81
|
## 6. AI Activity on Existing State
|
|
82
82
|
|
|
83
83
|
When AI or background work will touch an existing row for more than a quick
|
|
84
|
-
write, coordinate through `ablo.<model>.
|
|
85
|
-
|
|
86
|
-
|
|
84
|
+
write, coordinate through the flat model verbs: `ablo.<model>.claim(id, ...)` to
|
|
85
|
+
claim the row (returns the row), `ablo.<model>.claimState(id)` to read who's working
|
|
86
|
+
on it (synchronous; never blocks), and the normal `ablo.<model>.update(id, ...)`
|
|
87
|
+
to write. Normal reads still work while the claim is held; server reads can opt
|
|
88
|
+
into `ifClaimed: 'wait'` or `ifClaimed: 'fail'` when they should not read through
|
|
89
|
+
active work. The callback form releases the claim when the callback returns or
|
|
90
|
+
throws.
|
|
87
91
|
|
|
88
92
|
```ts
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
await report.
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
forecast: weather.summary,
|
|
105
|
-
});
|
|
93
|
+
// Claim the row so other participants serialize behind us while we work.
|
|
94
|
+
await ablo.weatherReports.claim(
|
|
95
|
+
'weather_stockholm',
|
|
96
|
+
async (report) => {
|
|
97
|
+
// Your existing weather tool or agent call. While this runs, other clients
|
|
98
|
+
// see that weather_stockholm is being checked.
|
|
99
|
+
const weather = await weatherAgent.getWeather(report.location);
|
|
100
|
+
|
|
101
|
+
await ablo.weatherReports.update(report.id, {
|
|
102
|
+
status: 'ready',
|
|
103
|
+
forecast: weather.summary,
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
{ action: 'checking_weather', ttl: '2m' },
|
|
107
|
+
);
|
|
106
108
|
```
|
|
107
109
|
|
|
108
|
-
Ablo does not fetch the weather.
|
|
109
|
-
|
|
110
|
-
|
|
110
|
+
Ablo does not fetch the weather. The claim is **advisory**: if another
|
|
111
|
+
participant already holds the row, `claim` waits for them to finish and re-reads
|
|
112
|
+
before handing back the row. While you hold the claim, `update(id, ...)` is
|
|
113
|
+
stale-guarded and rejects with `AbloStaleContextError` if the row changed under
|
|
114
|
+
you. The claim releases automatically once the callback returns or throws.
|
|
111
115
|
|
|
112
|
-
## 7. Multiplayer and
|
|
116
|
+
## 7. Multiplayer and Claimed Work
|
|
113
117
|
|
|
114
118
|
There is no separate multiplayer mode. Use the same schema client for human UI,
|
|
115
119
|
server actions, and agents; Ablo fans out confirmed writes and keeps active
|
|
116
|
-
|
|
120
|
+
claims visible on the same model row.
|
|
117
121
|
|
|
118
|
-
|
|
119
|
-
schema clients,
|
|
122
|
+
`claimState(id)` tells you when another human or agent is active on the same row.
|
|
123
|
+
For schema clients, `claim(id, work)` waits fairly, re-reads, and then lets you
|
|
124
|
+
write through the model.
|
|
120
125
|
|
|
121
126
|
```ts
|
|
122
|
-
const
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
});
|
|
126
|
-
|
|
127
|
-
if (busy.length > 0) {
|
|
128
|
-
await ablo.intents.waitFor(
|
|
129
|
-
{ resource: 'weatherReports', id: 'weather_stockholm' },
|
|
130
|
-
{ timeout: 30_000 },
|
|
131
|
-
);
|
|
127
|
+
const active = ablo.weatherReports.claimState('weather_stockholm');
|
|
128
|
+
if (active) {
|
|
129
|
+
console.log(`${active.heldBy} is ${active.action}`);
|
|
132
130
|
}
|
|
133
131
|
|
|
134
|
-
await ablo.weatherReports.
|
|
132
|
+
await ablo.weatherReports.claim('weather_stockholm', async (report) => {
|
|
133
|
+
await ablo.weatherReports.update(report.id, { status: 'ready' });
|
|
134
|
+
});
|
|
135
135
|
```
|
|
136
136
|
|
|
137
|
-
`
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
- `return` returns immediately with active intents.
|
|
141
|
-
- `wait` waits for the intent stream to clear.
|
|
142
|
-
- `fail` throws `AbloBusyError` with the active intents attached.
|
|
137
|
+
Use `{ wait: false }` on `claim` when work should be skipped instead of queued
|
|
138
|
+
behind an active holder.
|
|
143
139
|
|
|
144
140
|
## 8. Next Steps
|
|
145
141
|
|
|
146
|
-
Keep using the schema client for app and agent writes.
|
|
147
|
-
schema-less agent wrapper only when a worker intentionally cannot import the
|
|
148
|
-
app schema.
|
|
142
|
+
Keep using the schema client for app and agent writes.
|
|
149
143
|
|
|
150
144
|
- [Integration Guide](./integration-guide.md) explains the full app, React, Data Source, multiplayer, and agent path.
|
|
151
145
|
- [Guarantees](./guarantees.md) explains what confirmed writes and stale checks mean.
|
package/docs/react.md
CHANGED
|
@@ -14,21 +14,71 @@ The React bindings ship with the main package — no extra install.
|
|
|
14
14
|
import { useAblo } from '@abloatai/ablo/react';
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
##
|
|
17
|
+
## AbloProvider
|
|
18
|
+
|
|
19
|
+
Mount it once near the root of your tree. It owns the connection, the local
|
|
20
|
+
pool, and the engine lifecycle; everything below it reads with `useAblo`.
|
|
21
|
+
|
|
22
|
+
```tsx
|
|
23
|
+
'use client';
|
|
24
|
+
|
|
25
|
+
import { AbloProvider } from '@abloatai/ablo/react';
|
|
26
|
+
import { schema } from '@/ablo.schema';
|
|
27
|
+
|
|
28
|
+
export function Providers({
|
|
29
|
+
children,
|
|
30
|
+
user, // resolved server-side from YOUR auth
|
|
31
|
+
}: {
|
|
32
|
+
children: React.ReactNode;
|
|
33
|
+
user: { id: string; teamIds: string[] };
|
|
34
|
+
}) {
|
|
35
|
+
return (
|
|
36
|
+
<AbloProvider
|
|
37
|
+
schema={schema}
|
|
38
|
+
userId={user.id}
|
|
39
|
+
teamIds={user.teamIds}
|
|
40
|
+
fallback={<AppSkeleton />}
|
|
41
|
+
>
|
|
42
|
+
{children}
|
|
43
|
+
</AbloProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
`schema` is the only required prop. The rest are situational:
|
|
49
|
+
|
|
50
|
+
| Prop | Default | Purpose |
|
|
51
|
+
| ------------------- | ---------------------- | --------------------------------------------------------------------------------------------------------- |
|
|
52
|
+
| `schema` | — | **Required.** From `defineSchema()`. Determines the typed hook surface. |
|
|
53
|
+
| `userId` | resolved from auth | App participant id for app-owned fields and your `identityRoles.extract`. Not the security boundary. |
|
|
54
|
+
| `teamIds` | resolved from auth | Team ids expanded into team sync groups via `identityRoles`. |
|
|
55
|
+
| `syncGroups` | full allowed set | **Narrows** the subscription to a subset of what auth allows (e.g. `['deck:abc123']`). Never widens it. |
|
|
56
|
+
| `url` | hosted endpoint | WebSocket URL of the sync server (`wss://…`). Hosted apps omit it. |
|
|
57
|
+
| `apiKey` | session/cookie | Bootstrap auth. Browser apps **omit this** — the key stays server-side. See Identity below. |
|
|
58
|
+
| `fallback` | neutral spinner | Rendered during the *first* bootstrap only. Pass a branded skeleton, `null`, or `'passthrough'`. |
|
|
59
|
+
| `bootstrapMode` | `'full'` | `'full'` pulls the org's baseline before ready; `'none'` skips the baseline and processes live deltas only.|
|
|
60
|
+
| `persistence` | `'volatile'` | `'indexeddb'` opts into an offline queue + reload-surviving cache. |
|
|
61
|
+
| `onSessionExpired` | — | Fired after the engine has already purged on a rejected session — use for redirect-to-sign-in. |
|
|
62
|
+
| `onError` | — | Engine / WebSocket / `postBootstrap` errors. Wire to Sentry / Datadog. |
|
|
63
|
+
|
|
64
|
+
Where `userId` / `teamIds` / `syncGroups` come from, and why the API key never
|
|
65
|
+
reaches the browser, is the whole of
|
|
66
|
+
[Identity & Sync Groups](./identity.md) — read that if it isn't obvious how org
|
|
67
|
+
/ team / user map to what a participant can see.
|
|
68
|
+
|
|
69
|
+
## useAblo — model client
|
|
18
70
|
|
|
19
71
|
```tsx
|
|
20
72
|
'use client';
|
|
21
73
|
|
|
22
74
|
import { useAblo } from '@abloatai/ablo/react';
|
|
23
75
|
|
|
24
|
-
export function
|
|
25
|
-
const
|
|
26
|
-
const
|
|
27
|
-
|
|
28
|
-
) ?? [];
|
|
29
|
-
const busy = intents.length > 0;
|
|
76
|
+
export function ReportView({ report: serverReport }: { report: { id: string; location: string } }) {
|
|
77
|
+
const report = useAblo((ablo) => ablo.weatherReports.retrieve(serverReport.id)) ?? serverReport;
|
|
78
|
+
const active = useAblo((ablo) => ablo.weatherReports.claimState(serverReport.id));
|
|
79
|
+
const claimed = Boolean(active);
|
|
30
80
|
|
|
31
|
-
return <article>{
|
|
81
|
+
return <article>{report.location}</article>;
|
|
32
82
|
}
|
|
33
83
|
```
|
|
34
84
|
|
|
@@ -37,9 +87,9 @@ The hook:
|
|
|
37
87
|
1. Reads through the same `ablo.<model>` methods as the rest of the SDK.
|
|
38
88
|
2. Tracks the model fields read by the selector and re-renders when confirmed
|
|
39
89
|
deltas arrive.
|
|
40
|
-
3. Lets Server Component data stay outside the hook: use `??
|
|
90
|
+
3. Lets Server Component data stay outside the hook: use `?? serverReport` when a
|
|
41
91
|
parent already loaded the row.
|
|
42
|
-
4. Works for coordination state too, such as `ablo.
|
|
92
|
+
4. Works for coordination state too, such as `ablo.weatherReports.claimState(id)`.
|
|
43
93
|
|
|
44
94
|
Use the zero-argument form only when you need the full client for callbacks,
|
|
45
95
|
effects, or writes:
|
|
@@ -50,15 +100,15 @@ const abloClient = useAblo();
|
|
|
50
100
|
|
|
51
101
|
Prefer selector reads like `useAblo((ablo) => ablo.<model>.retrieve(id))`.
|
|
52
102
|
String model names are kept on older hooks for compatibility, but first examples
|
|
53
|
-
should use the same model-
|
|
103
|
+
should use the same model-client shape as the rest of the SDK.
|
|
54
104
|
|
|
55
|
-
For collections, keep the selector on the model
|
|
105
|
+
For collections, keep the selector on the model client too:
|
|
56
106
|
|
|
57
107
|
```tsx
|
|
58
|
-
const
|
|
59
|
-
ablo.
|
|
108
|
+
const reports = useAblo((ablo) =>
|
|
109
|
+
ablo.weatherReports.list({
|
|
60
110
|
where: { projectId },
|
|
61
|
-
filter: (
|
|
111
|
+
filter: (report) => report.status !== 'ready',
|
|
62
112
|
scope: 'live',
|
|
63
113
|
}),
|
|
64
114
|
);
|
|
@@ -67,7 +117,7 @@ const tasks = useAblo((ablo) =>
|
|
|
67
117
|
## Server Load
|
|
68
118
|
|
|
69
119
|
```tsx
|
|
70
|
-
const [
|
|
120
|
+
const [report] = await ablo.weatherReports.load({ where: { id } });
|
|
71
121
|
```
|
|
72
122
|
|
|
73
123
|
Use `load` in Server Components when the row may not be in the local pool yet.
|
|
@@ -79,8 +129,8 @@ For Server Actions and route handlers, call the SDK directly:
|
|
|
79
129
|
```ts
|
|
80
130
|
import { ablo } from '@/lib/ablo';
|
|
81
131
|
|
|
82
|
-
const snap = ablo.snapshot({
|
|
83
|
-
await ablo.
|
|
132
|
+
const snap = ablo.snapshot({ weatherReports: id });
|
|
133
|
+
await ablo.weatherReports.update(id, patch, {
|
|
84
134
|
readAt: snap.stamp,
|
|
85
135
|
onStale: 'reject',
|
|
86
136
|
wait: 'confirmed',
|
|
@@ -88,17 +138,17 @@ await ablo.tasks.update(id, patch, {
|
|
|
88
138
|
```
|
|
89
139
|
|
|
90
140
|
For client event handlers, get the provider-owned client and call the same
|
|
91
|
-
model
|
|
141
|
+
model client:
|
|
92
142
|
|
|
93
143
|
```tsx
|
|
94
144
|
const ablo = useAblo();
|
|
95
145
|
|
|
96
|
-
async function
|
|
146
|
+
async function markReady() {
|
|
97
147
|
if (!ablo) return;
|
|
98
|
-
const snap = ablo.snapshot({
|
|
99
|
-
await ablo.
|
|
148
|
+
const snap = ablo.snapshot({ weatherReports: id });
|
|
149
|
+
await ablo.weatherReports.update(
|
|
100
150
|
id,
|
|
101
|
-
{ status: '
|
|
151
|
+
{ status: 'ready' },
|
|
102
152
|
{ readAt: snap.stamp, onStale: 'reject', wait: 'confirmed' },
|
|
103
153
|
);
|
|
104
154
|
}
|
package/docs/roadmap.md
CHANGED
|
@@ -4,8 +4,7 @@ What is shipped, what is next, and what we will not build.
|
|
|
4
4
|
|
|
5
5
|
## Shipped
|
|
6
6
|
|
|
7
|
-
- **
|
|
8
|
-
- **Capability tokens** — Biscuit-signed, scoped, attenuable, hot-revocable.
|
|
7
|
+
- **Models, claims, commits** — the core API.
|
|
9
8
|
- **Audit log** — hash-chained per principal, with `delegationChainRoot`.
|
|
10
9
|
- **MCP transport** — HTTP server at `/api/mcp`.
|
|
11
10
|
- **TypeScript SDK** — `@abloatai/ablo`, with React bindings.
|
|
@@ -13,13 +12,12 @@ What is shipped, what is next, and what we will not build.
|
|
|
13
12
|
|
|
14
13
|
## In flight
|
|
15
14
|
|
|
16
|
-
- **Real-time presence** — see who else is viewing/editing a
|
|
15
|
+
- **Real-time presence** — see who else is viewing/editing a model.
|
|
17
16
|
- **Cross-instance fan-out via Redis** — pub/sub deltas at scale.
|
|
18
|
-
- **Hot capability revocation UI** — in the dashboard, today via API only.
|
|
19
17
|
|
|
20
18
|
## On deck
|
|
21
19
|
|
|
22
|
-
- **Schema migrations** — declarative
|
|
20
|
+
- **Schema migrations** — declarative model schema changes.
|
|
23
21
|
- **Field-level subscriptions** — subscribe to one path, not the whole row.
|
|
24
22
|
- **Bulk import/export** — CSV/JSON round-trip with chain verification.
|
|
25
23
|
|
|
@@ -31,11 +29,11 @@ What is shipped, what is next, and what we will not build.
|
|
|
31
29
|
|
|
32
30
|
## We will not build
|
|
33
31
|
|
|
34
|
-
- **A general-purpose Postgres wrapper** — Ablo
|
|
32
|
+
- **A general-purpose Postgres wrapper** — Ablo is for state with
|
|
35
33
|
concurrency semantics, not for storing every table.
|
|
36
34
|
- **Server-side compute** — no triggers, no stored procedures. Compute
|
|
37
35
|
belongs in your application code.
|
|
38
|
-
- **A document database UI** — your data lives in Ablo
|
|
36
|
+
- **A document database UI** — your data lives in Ablo; the UI is your
|
|
39
37
|
product, not ours.
|
|
40
38
|
|
|
41
39
|
## How priorities shift
|
package/llms.txt
CHANGED
|
@@ -11,34 +11,26 @@ import Ablo from '@abloatai/ablo';
|
|
|
11
11
|
import { defineSchema, model, z } from '@abloatai/ablo/schema';
|
|
12
12
|
|
|
13
13
|
const schema = defineSchema({
|
|
14
|
-
|
|
14
|
+
weatherReports: model({
|
|
15
15
|
id: z.string(),
|
|
16
|
-
|
|
17
|
-
status: z.enum(['
|
|
18
|
-
|
|
16
|
+
location: z.string(),
|
|
17
|
+
status: z.enum(['pending', 'ready']),
|
|
18
|
+
forecast: z.string().optional(),
|
|
19
19
|
}),
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
23
23
|
|
|
24
|
-
const [
|
|
25
|
-
if (!
|
|
26
|
-
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
'task_123',
|
|
35
|
-
{ status: 'done', summary: await summarize(task) },
|
|
36
|
-
{
|
|
37
|
-
readAt: snap.stamp,
|
|
38
|
-
onStale: 'reject',
|
|
39
|
-
wait: 'confirmed',
|
|
40
|
-
},
|
|
41
|
-
);
|
|
24
|
+
const [report] = await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
25
|
+
if (!report) throw new Error('Row not found');
|
|
26
|
+
|
|
27
|
+
const updated = await ablo.weatherReports.claim('report_stockholm', async (report) => {
|
|
28
|
+
return ablo.weatherReports.update(
|
|
29
|
+
report.id,
|
|
30
|
+
{ status: 'ready', forecast: await getForecast(report) },
|
|
31
|
+
{ wait: 'confirmed' },
|
|
32
|
+
);
|
|
33
|
+
});
|
|
42
34
|
```
|
|
43
35
|
|
|
44
36
|
That is the normal app path: declare models in a schema, then use `ablo.<model>.load(...)`, `ablo.<model>.retrieve(...)`, `ablo.<model>.create(...)`, `ablo.<model>.update(...)`, and `ablo.<model>.delete(...)`.
|
|
@@ -54,7 +46,7 @@ defaults to `'live'`, with `'archived'` and `'all'` for lifecycle-aware reads.
|
|
|
54
46
|
Advanced schema-less agents exist for workers that cannot import the app schema,
|
|
55
47
|
but do not teach that path first.
|
|
56
48
|
|
|
57
|
-
React reads should use selector `useAblo`: `useAblo((ablo) => ablo.
|
|
49
|
+
React reads should use selector `useAblo`: `useAblo((ablo) => ablo.weatherReports.retrieve(id))`.
|
|
58
50
|
Use zero-argument `useAblo()` only when a component needs the client for an
|
|
59
51
|
event handler or effect. Treat `useQuery`, `useOne`, `useReader`, and
|
|
60
52
|
`useMutate` as compatibility hooks for older string-keyed integrations, not the
|
|
@@ -64,36 +56,39 @@ first integration path.
|
|
|
64
56
|
|
|
65
57
|
Multiplayer is not a separate mode. When human UI, server actions, and agents use
|
|
66
58
|
the same schema client and write through `ablo.<model>`, Ablo coordinates the
|
|
67
|
-
shared
|
|
68
|
-
|
|
59
|
+
shared model stream: confirmed deltas fan out to subscribers, active claims are
|
|
60
|
+
visible through `claimState(id)`, and stale writes can be rejected with `readAt`.
|
|
69
61
|
|
|
70
62
|
If an app writes directly to its own database outside Ablo, that write bypasses
|
|
71
63
|
coordination until the app reports it through Data Source events.
|
|
72
64
|
|
|
73
65
|
## Nouns
|
|
74
66
|
|
|
75
|
-
- `Model
|
|
76
|
-
- `
|
|
67
|
+
- `Model client` is the typed `ablo.<model>` object generated from schema.
|
|
68
|
+
- `Claim` holds a model row while slow work runs; `claimState(id)` observes it.
|
|
77
69
|
- `Commit` is the durable protocol write behind `ablo.<model>.update(...)`.
|
|
78
70
|
- `Receipt` confirms the commit.
|
|
79
|
-
- `Capability` scopes what an agent may do. `agent.run(...)` handles the common case.
|
|
80
|
-
- `Task` is one agent run, with audit and cost attribution.
|
|
81
71
|
|
|
82
|
-
##
|
|
72
|
+
## Claimed Behavior
|
|
83
73
|
|
|
84
|
-
Reads never silently block.
|
|
74
|
+
Reads never silently block. Schema reads stay open while a row is claimed.
|
|
75
|
+
Server reads through `ablo.model(name)` can pass `ifClaimed: 'return'` to
|
|
76
|
+
receive active claims, `ifClaimed: 'fail'` to throw `AbloClaimedError`, or
|
|
77
|
+
`ifClaimed: 'wait'` to wait until the active claim clears.
|
|
85
78
|
|
|
86
|
-
Schema clients wait from the realtime
|
|
79
|
+
Schema clients wait from the realtime claim stream. Schema-less HTTP callers must provide an explicit `claimedPollInterval` when using `ifClaimed: 'wait'`; Ablo does not hide a hard-coded polling loop.
|
|
87
80
|
|
|
88
|
-
Use `
|
|
81
|
+
Use `claimedTimeout` only as a maximum wait, not as the coordination mechanism.
|
|
89
82
|
|
|
90
83
|
## Guarantees
|
|
91
84
|
|
|
92
85
|
`wait: 'confirmed'` means the server accepted the write. Schema model writes are optimistic by default; server rejection rolls back local state. Use `snapshot(...)` plus `readAt` and `onStale: 'reject'` to prevent lost updates.
|
|
93
86
|
|
|
94
|
-
|
|
87
|
+
Claims coordinate writers; they do not block readers. Most users should stay on
|
|
88
|
+
schema-backed reads/writes and `claim(...)`; do not teach manual protocol
|
|
89
|
+
bookkeeping in the happy path.
|
|
95
90
|
|
|
96
|
-
All SDK errors extend `AbloError`. Important classes: `
|
|
91
|
+
All SDK errors extend `AbloError`. Important classes: `AbloClaimedError`, `AbloStaleContextError`, `AbloAuthenticationError`, `AbloPermissionError`, `AbloRateLimitError`, `AbloIdempotencyError`, `AbloConnectionError`, `AbloValidationError`, and `AbloServerError`.
|
|
97
92
|
|
|
98
93
|
## Schema Scope
|
|
99
94
|
|
|
@@ -114,7 +109,7 @@ import { dataSource } from '@abloatai/ablo';
|
|
|
114
109
|
## Sandboxes
|
|
115
110
|
|
|
116
111
|
Public `/sandbox` is a deterministic visual demo. It should teach shared state,
|
|
117
|
-
|
|
112
|
+
claims, stale-write rejection, receipts, and deltas, but it does not use a real
|
|
118
113
|
API key. It also exposes a Claude Code / Codex handoff prompt. Prefer that shape
|
|
119
114
|
when an agent is asked to "make Ablo work" in an existing app.
|
|
120
115
|
|
|
@@ -124,16 +119,16 @@ sandbox like Stripe test mode: it has an isolated sync group prefix and mints
|
|
|
124
119
|
Resetting a sandbox creates a clean future stream without touching live data.
|
|
125
120
|
Use `sk_live_*` only for production.
|
|
126
121
|
|
|
127
|
-
For coding agents, the sandbox success path is: pick one shared
|
|
122
|
+
For coding agents, the sandbox success path is: pick one shared model,
|
|
128
123
|
declare schema, create the Ablo client, replace one direct mutation with a typed
|
|
129
124
|
`ablo.<model>.update(...)`, use selector `useAblo` for live reads, and add a
|
|
130
|
-
two-writer stale/
|
|
125
|
+
two-writer stale/claim smoke test.
|
|
131
126
|
|
|
132
127
|
## Public Surface
|
|
133
128
|
|
|
134
129
|
Import from these public paths only:
|
|
135
130
|
|
|
136
|
-
- `@abloatai/ablo` — `Ablo`, errors, typed model
|
|
131
|
+
- `@abloatai/ablo` — `Ablo`, errors, typed model clients, claims, `dataSource`, and advanced protocol models.
|
|
137
132
|
- `@abloatai/ablo/schema` — schema DSL.
|
|
138
133
|
- `@abloatai/ablo/react` — React provider and hooks.
|
|
139
134
|
- `@abloatai/ablo/testing` — test harnesses and mocks.
|
package/package.json
CHANGED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import type { Schema, InferModel, InferCreate } from '../schema/schema.js';
|
|
2
|
-
import type { ResolveSchema } from '../types/global.js';
|
|
3
|
-
import type { SyncStoreContract } from './context.js';
|
|
4
|
-
type GlobalMutateKey = ResolveSchema extends {
|
|
5
|
-
models: infer M;
|
|
6
|
-
} ? keyof M & string : string;
|
|
7
|
-
type GlobalMutateActions<K extends string> = ResolveSchema extends Schema ? K extends keyof ResolveSchema['models'] & string ? MutateActions<ResolveSchema, K> : MutateActions<Schema, string> : MutateActions<Schema, string>;
|
|
8
|
-
/**
|
|
9
|
-
* Compatibility mutation hook. Returns CRUD methods for a single model type.
|
|
10
|
-
*
|
|
11
|
-
* Prefer `useAblo()` and call `ablo.<model>.create/update/delete` inside
|
|
12
|
-
* callbacks for new integrations. This hook remains for older string-keyed code.
|
|
13
|
-
*
|
|
14
|
-
* @example
|
|
15
|
-
* import { schema } from '@ablo/schema';
|
|
16
|
-
* import { useMutate } from '@abloatai/ablo/react';
|
|
17
|
-
*
|
|
18
|
-
* const tasks = useMutate(schema, 'tasks');
|
|
19
|
-
*
|
|
20
|
-
* // Create — fields are type-checked against the schema's Zod shape
|
|
21
|
-
* await tasks.create({ title: 'Fix bug', status: 'todo', projectId });
|
|
22
|
-
*
|
|
23
|
-
* // Update — id + partial changes, no need to hold a model instance
|
|
24
|
-
* await tasks.update({ id: task.id, status: 'done', completedAt: new Date() });
|
|
25
|
-
*
|
|
26
|
-
* // Delete / archive / unarchive — by id
|
|
27
|
-
* await tasks.delete(task.id);
|
|
28
|
-
* await tasks.archive(task.id);
|
|
29
|
-
*
|
|
30
|
-
* Mirrors the Zero pattern: `zero.mutate.task.update({ id, status: 'done' })`.
|
|
31
|
-
*/
|
|
32
|
-
/**
|
|
33
|
-
* `create` / `update` / `delete` are overloaded: pass one row or an
|
|
34
|
-
* array. Drizzle and Prisma use the same shape (`db.insert(table).values(rowOrRows)`).
|
|
35
|
-
* Avoids the `*Many` suffix while keeping the semantics: every entry in
|
|
36
|
-
* an array call lands in the same synchronous tick (Promise.all under
|
|
37
|
-
* the hood), so the microtask coalescer in `TransactionQueue` collapses
|
|
38
|
-
* N pushes into one wire commit with one `batchIndex` — structurally
|
|
39
|
-
* identical to Zero's mutator-boundary commit.
|
|
40
|
-
*/
|
|
41
|
-
type UpdatePatch<S extends Schema, K extends keyof S['models'] & string> = {
|
|
42
|
-
id: string;
|
|
43
|
-
} & Partial<InferModel<S, K>>;
|
|
44
|
-
export interface MutateActions<S extends Schema, K extends keyof S['models'] & string> {
|
|
45
|
-
/**
|
|
46
|
-
* Create one entity, or an array of entities in a single tick. ID,
|
|
47
|
-
* createdAt, updatedAt, organizationId default automatically per row.
|
|
48
|
-
*/
|
|
49
|
-
create(data: InferCreate<S, K>): Promise<InferModel<S, K>>;
|
|
50
|
-
create(data: InferCreate<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
51
|
-
/**
|
|
52
|
-
* Update one row, or an array of rows in a single tick. Each patch is
|
|
53
|
-
* `{ id, ...changes }` — missing ids throw. Schema-generated models
|
|
54
|
-
* are MobX-observable, so direct assignment fires reactivity.
|
|
55
|
-
*/
|
|
56
|
-
update(patch: UpdatePatch<S, K>): Promise<InferModel<S, K>>;
|
|
57
|
-
update(patches: UpdatePatch<S, K>[]): Promise<InferModel<S, K>[]>;
|
|
58
|
-
/**
|
|
59
|
-
* Delete one row by id, or an array of ids in a single tick. Missing
|
|
60
|
-
* ids are silently ignored.
|
|
61
|
-
*/
|
|
62
|
-
delete(id: string): Promise<void>;
|
|
63
|
-
delete(ids: string[]): Promise<void>;
|
|
64
|
-
/** Soft-archive by ID. */
|
|
65
|
-
archive: (id: string) => Promise<void>;
|
|
66
|
-
/** Restore an archived entity by ID. */
|
|
67
|
-
unarchive: (id: string) => Promise<void>;
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Pure factory — testable without React. The hook just wraps this in
|
|
71
|
-
* useMemo with the React context.
|
|
72
|
-
*/
|
|
73
|
-
export declare function createMutateActions<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K, store: SyncStoreContract, organizationId: string): MutateActions<S, K>;
|
|
74
|
-
/** @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`. */
|
|
75
|
-
export declare function useMutate<S extends Schema, K extends keyof S['models'] & string>(schema: S, modelKey: K): MutateActions<S, K>;
|
|
76
|
-
/** Typed CRUD via the `AbloSync` global augmentation. The schema is
|
|
77
|
-
* resolved from the `SyncProvider`'s context — consumer doesn't pass it
|
|
78
|
-
* at the call site.
|
|
79
|
-
*
|
|
80
|
-
* @deprecated Prefer `useAblo()` plus `ablo.<model>.create/update/delete`.
|
|
81
|
-
*/
|
|
82
|
-
export declare function useMutate<K extends GlobalMutateKey>(modelKey: K): GlobalMutateActions<K>;
|
|
83
|
-
export {};
|