@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
|
@@ -1,26 +1,26 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Per-model
|
|
2
|
+
* Per-model client factory.
|
|
3
3
|
*
|
|
4
|
-
* Mirrors Anthropic SDK's
|
|
5
|
-
*
|
|
4
|
+
* Mirrors Anthropic SDK's per-endpoint module pattern: each model client
|
|
5
|
+
* has its own file, and the root client just instantiates
|
|
6
6
|
* one per model. Extracted from `Ablo.ts` so the proxy logic is
|
|
7
7
|
* testable in isolation and the constructor doesn't carry it.
|
|
8
8
|
*
|
|
9
9
|
* Each schema model gets one `ModelOperations<T, CreateInput>` —
|
|
10
10
|
* exposes `retrieve`, `list`, `count`, `create`, `update`, `delete`,
|
|
11
|
-
* `
|
|
12
|
-
* factory returns a plain object; the client assembles the
|
|
11
|
+
* `claim`, `claimState`, `queue`, `release`, `subscribe`, and `load`.
|
|
12
|
+
* The factory returns a plain object; the client assembles the
|
|
13
13
|
* `ablo.<model>` lookup table from these.
|
|
14
14
|
*/
|
|
15
15
|
import { autorun } from 'mobx';
|
|
16
|
-
import {
|
|
16
|
+
import { AbloClaimedError, AbloValidationError } from '../errors.js';
|
|
17
17
|
import { Model, modelAsRow } from '../Model.js';
|
|
18
18
|
import { ModelScope } from '../types/index.js';
|
|
19
|
-
const
|
|
20
|
-
export function
|
|
21
|
-
if (typeof
|
|
19
|
+
const modelClientMeta = new WeakMap();
|
|
20
|
+
export function getModelClientMeta(modelClient) {
|
|
21
|
+
if (typeof modelClient !== 'object' || modelClient === null)
|
|
22
22
|
return undefined;
|
|
23
|
-
return
|
|
23
|
+
return modelClientMeta.get(modelClient);
|
|
24
24
|
}
|
|
25
25
|
export function createModelProxy(schemaKey, registeredModelName, objectPool, syncClient, registry, hydration, collaboration) {
|
|
26
26
|
const ModelClass = registry.getModelByName(registeredModelName);
|
|
@@ -42,6 +42,93 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
42
42
|
await syncClient.syncNow();
|
|
43
43
|
await syncClient.waitForConfirmation(model.getModelName(), model.id);
|
|
44
44
|
};
|
|
45
|
+
// Claims this proxy currently holds, keyed by entity id. Lets the flat
|
|
46
|
+
// `release(id)` and `update(id)` find the lease + snapshot a `claim(id)`
|
|
47
|
+
// took — no per-call handle. Released on dispose, explicit release, or TTL.
|
|
48
|
+
const activeClaims = new Map();
|
|
49
|
+
const releaseClaim = async (id) => {
|
|
50
|
+
const held = activeClaims.get(id);
|
|
51
|
+
if (!held)
|
|
52
|
+
return;
|
|
53
|
+
activeClaims.delete(id);
|
|
54
|
+
await held.lease.release();
|
|
55
|
+
};
|
|
56
|
+
const takeClaim = async (id, options) => {
|
|
57
|
+
if (!collaboration) {
|
|
58
|
+
throw new AbloValidationError(`Model "${schemaKey}" cannot claim a row without collaboration wiring.`, { code: 'model_claim_not_configured' });
|
|
59
|
+
}
|
|
60
|
+
// Is someone ELSE already on this target? Read the local coordination
|
|
61
|
+
// snapshot up front — it decides whether we'll need to re-read after the
|
|
62
|
+
// claim (a free / already-mine target can't have changed under us).
|
|
63
|
+
const held = collaboration.observe({ model: schemaKey, id });
|
|
64
|
+
const contended = !!held && held.heldBy !== collaboration.selfParticipantId;
|
|
65
|
+
const failFast = options?.wait === false;
|
|
66
|
+
// Fail-fast (`wait: false`): if another participant already holds it,
|
|
67
|
+
// reject now instead of queuing. Best-effort at the client (a racing
|
|
68
|
+
// claim not yet synced into our snapshot slips through here) — the
|
|
69
|
+
// commit-time intent guard is the authoritative backstop that rejects
|
|
70
|
+
// the loser's first write. For work-distribution dedup that's exactly
|
|
71
|
+
// right: don't wait (that would double-process), skip.
|
|
72
|
+
if (failFast && contended) {
|
|
73
|
+
throw new AbloClaimedError(`${registeredModelName}/${id} is held by ${held?.heldBy ?? 'another participant'}.`, { code: 'entity_claimed' });
|
|
74
|
+
}
|
|
75
|
+
// Ensure the row exists locally before claiming.
|
|
76
|
+
let model = objectPool.get(id);
|
|
77
|
+
if (!model) {
|
|
78
|
+
await load({ where: { id } });
|
|
79
|
+
model = objectPool.get(id);
|
|
80
|
+
}
|
|
81
|
+
if (!model) {
|
|
82
|
+
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
83
|
+
}
|
|
84
|
+
// Acquire the lease. Default (`wait` !== false) goes through the server's
|
|
85
|
+
// fair FIFO queue — `queue: true` resolves only once the lease is genuinely
|
|
86
|
+
// ours, blocking behind any current holder, with no TOCTOU gap (the server
|
|
87
|
+
// orders contenders). Fail-fast skips the queue: we already rejected an
|
|
88
|
+
// observed conflict above, so this just records our lease.
|
|
89
|
+
const lease = await collaboration.createIntent({
|
|
90
|
+
target: {
|
|
91
|
+
model: schemaKey,
|
|
92
|
+
id,
|
|
93
|
+
...(options?.field ? { field: options.field } : {}),
|
|
94
|
+
},
|
|
95
|
+
action: options?.action ?? 'editing',
|
|
96
|
+
ttl: options?.ttl,
|
|
97
|
+
queue: !failFast,
|
|
98
|
+
maxQueueDepth: options?.maxQueueDepth,
|
|
99
|
+
});
|
|
100
|
+
// Only when we actually waited behind another holder can the row have
|
|
101
|
+
// changed underneath us — re-read so the claimed snapshot reflects what
|
|
102
|
+
// they committed before releasing.
|
|
103
|
+
if (contended && !failFast) {
|
|
104
|
+
await load({ where: { id } });
|
|
105
|
+
model = objectPool.get(id) ?? model;
|
|
106
|
+
}
|
|
107
|
+
const snapshot = collaboration.createSnapshot(schemaKey, id);
|
|
108
|
+
activeClaims.set(id, { lease, snapshot });
|
|
109
|
+
const row = modelAsRow(model);
|
|
110
|
+
// `await using` calls this on scope exit; releases the claim.
|
|
111
|
+
Object.defineProperty(row, Symbol.asyncDispose, {
|
|
112
|
+
value: () => releaseClaim(id),
|
|
113
|
+
enumerable: false,
|
|
114
|
+
configurable: true,
|
|
115
|
+
});
|
|
116
|
+
return row;
|
|
117
|
+
};
|
|
118
|
+
function claim(id, a, b) {
|
|
119
|
+
if (typeof a === 'function') {
|
|
120
|
+
return (async () => {
|
|
121
|
+
const row = await takeClaim(id, b);
|
|
122
|
+
try {
|
|
123
|
+
return await a(row);
|
|
124
|
+
}
|
|
125
|
+
finally {
|
|
126
|
+
await releaseClaim(id);
|
|
127
|
+
}
|
|
128
|
+
})();
|
|
129
|
+
}
|
|
130
|
+
return takeClaim(id, a);
|
|
131
|
+
}
|
|
45
132
|
const operations = {
|
|
46
133
|
retrieve(id) {
|
|
47
134
|
return objectPool.get(id);
|
|
@@ -99,9 +186,21 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
99
186
|
const model = objectPool.get(id);
|
|
100
187
|
if (!model)
|
|
101
188
|
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
189
|
+
// If we hold a claim on this row, guard the write with its snapshot
|
|
190
|
+
// watermark + lease so it's stale-rejected and attributed to the claim.
|
|
191
|
+
const claimed = activeClaims.get(id);
|
|
192
|
+
const effective = claimed
|
|
193
|
+
? {
|
|
194
|
+
wait: 'confirmed',
|
|
195
|
+
readAt: claimed.snapshot.stamp,
|
|
196
|
+
onStale: 'reject',
|
|
197
|
+
intent: claimed.lease,
|
|
198
|
+
...options,
|
|
199
|
+
}
|
|
200
|
+
: options;
|
|
102
201
|
model.updateFromData(data);
|
|
103
|
-
syncClient.update(model,
|
|
104
|
-
await waitForMutation(model,
|
|
202
|
+
syncClient.update(model, effective);
|
|
203
|
+
await waitForMutation(model, effective);
|
|
105
204
|
return modelAsRow(model);
|
|
106
205
|
},
|
|
107
206
|
async delete(id, options) {
|
|
@@ -111,122 +210,20 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
111
210
|
syncClient.delete(model, options);
|
|
112
211
|
await waitForMutation(model, options);
|
|
113
212
|
},
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
const cancel = () => {
|
|
123
|
-
if (released)
|
|
124
|
-
return;
|
|
125
|
-
released = true;
|
|
126
|
-
if (snapshot)
|
|
127
|
-
snapshot.signal.removeEventListener('abort', cancel);
|
|
128
|
-
acquired?.revoke();
|
|
129
|
-
};
|
|
130
|
-
// Public `finish()` — give the claim back after committing. Calls the
|
|
131
|
-
// lower-level lease handle's `release()` (a different API; leave it).
|
|
132
|
-
const finish = async () => {
|
|
133
|
-
if (released)
|
|
134
|
-
return;
|
|
135
|
-
released = true;
|
|
136
|
-
if (snapshot)
|
|
137
|
-
snapshot.signal.removeEventListener('abort', cancel);
|
|
138
|
-
await acquired?.release();
|
|
139
|
-
};
|
|
140
|
-
const whenFree = async (options) => {
|
|
141
|
-
if (!collaboration)
|
|
142
|
-
return;
|
|
143
|
-
await collaboration.waitFor(target, options);
|
|
144
|
-
};
|
|
145
|
-
const claim = async (options) => {
|
|
146
|
-
if (!collaboration) {
|
|
147
|
-
throw new AbloValidationError(`Model "${schemaKey}" cannot claim an intent without collaboration wiring.`, { code: 'model_intent_not_configured' });
|
|
148
|
-
}
|
|
149
|
-
if (acquired)
|
|
150
|
-
return;
|
|
151
|
-
acquireWait = options?.wait;
|
|
152
|
-
// Load the row so update() has a snapshot to guard against.
|
|
153
|
-
let model = objectPool.get(id);
|
|
154
|
-
if (!model) {
|
|
155
|
-
await load({ where: { id } });
|
|
156
|
-
model = objectPool.get(id);
|
|
157
|
-
}
|
|
158
|
-
if (!model) {
|
|
159
|
-
throw new AbloValidationError(`Entity not found: ${registeredModelName}/${id}`, { code: 'entity_not_found' });
|
|
160
|
-
}
|
|
161
|
-
const snap = collaboration.createSnapshot(schemaKey, id);
|
|
162
|
-
snap.signal.addEventListener('abort', cancel, { once: true });
|
|
163
|
-
snapshot = snap;
|
|
164
|
-
released = false;
|
|
165
|
-
acquired = await collaboration.createIntent({
|
|
166
|
-
target: {
|
|
167
|
-
resource: schemaKey,
|
|
168
|
-
id,
|
|
169
|
-
...(options?.field ? { field: options.field } : {}),
|
|
170
|
-
},
|
|
171
|
-
action: options?.action ?? 'editing',
|
|
172
|
-
ttl: options?.ttl,
|
|
173
|
-
});
|
|
174
|
-
};
|
|
175
|
-
const claimOrWait = async (options) => {
|
|
176
|
-
if (!collaboration) {
|
|
177
|
-
throw new AbloValidationError(`Model "${schemaKey}" cannot claim an intent without collaboration wiring.`, { code: 'model_intent_not_configured' });
|
|
178
|
-
}
|
|
179
|
-
const held = collaboration.observe(target);
|
|
180
|
-
// A foreign holder: wait for them to finish, then re-read before
|
|
181
|
-
// claiming. Our own claim (or a free target) goes straight to claim.
|
|
182
|
-
if (held && held.heldBy !== collaboration.selfParticipantId) {
|
|
183
|
-
await whenFree();
|
|
184
|
-
await load({ where: { id } });
|
|
185
|
-
}
|
|
186
|
-
await claim(options);
|
|
187
|
-
};
|
|
188
|
-
const handle = {
|
|
189
|
-
id,
|
|
190
|
-
get current() {
|
|
191
|
-
return collaboration?.observe(target) ?? null;
|
|
192
|
-
},
|
|
193
|
-
get status() {
|
|
194
|
-
return collaboration?.observe(target)?.status ?? 'idle';
|
|
195
|
-
},
|
|
196
|
-
claim,
|
|
197
|
-
claimOrWait,
|
|
198
|
-
async update(data, updateOptions) {
|
|
199
|
-
if (!acquired || !snapshot) {
|
|
200
|
-
throw new AbloValidationError(`Call claim() before update() on ablo.${schemaKey}.intent(${id}).`, { code: 'intent_not_acquired' });
|
|
201
|
-
}
|
|
202
|
-
if (snapshot.signal.aborted) {
|
|
203
|
-
throw new AbloStaleContextError(`Intent context is stale for ${schemaKey}/${id}. Re-read the row and retry.`, {
|
|
204
|
-
code: 'edit_context_stale',
|
|
205
|
-
readAt: snapshot.stamp,
|
|
206
|
-
cause: snapshot.signal.reason,
|
|
207
|
-
});
|
|
208
|
-
}
|
|
209
|
-
try {
|
|
210
|
-
return await operations.update(id, data, {
|
|
211
|
-
wait: acquireWait ?? 'confirmed',
|
|
212
|
-
readAt: snapshot.stamp,
|
|
213
|
-
onStale: 'reject',
|
|
214
|
-
...updateOptions,
|
|
215
|
-
intent: acquired,
|
|
216
|
-
});
|
|
217
|
-
}
|
|
218
|
-
finally {
|
|
219
|
-
await finish();
|
|
220
|
-
}
|
|
221
|
-
},
|
|
222
|
-
finish,
|
|
223
|
-
whenFree,
|
|
224
|
-
cancel,
|
|
225
|
-
[Symbol.asyncDispose]: finish,
|
|
213
|
+
claim,
|
|
214
|
+
claimState(id) {
|
|
215
|
+
return collaboration?.observe({ model: schemaKey, id }) ?? null;
|
|
216
|
+
},
|
|
217
|
+
queue(id) {
|
|
218
|
+
return {
|
|
219
|
+
object: 'list',
|
|
220
|
+
data: collaboration?.queue({ model: schemaKey, id }) ?? [],
|
|
226
221
|
};
|
|
227
|
-
return handle;
|
|
228
222
|
},
|
|
229
|
-
|
|
223
|
+
release(id) {
|
|
224
|
+
return releaseClaim(id);
|
|
225
|
+
},
|
|
226
|
+
onChange(callback, options) {
|
|
230
227
|
return autorun(() => {
|
|
231
228
|
const entities = this.list(options);
|
|
232
229
|
callback(entities);
|
|
@@ -234,7 +231,7 @@ export function createModelProxy(schemaKey, registeredModelName, objectPool, syn
|
|
|
234
231
|
},
|
|
235
232
|
load,
|
|
236
233
|
};
|
|
237
|
-
|
|
234
|
+
modelClientMeta.set(operations, {
|
|
238
235
|
key: schemaKey,
|
|
239
236
|
typename: registeredModelName,
|
|
240
237
|
});
|
package/dist/client/index.d.ts
CHANGED
|
@@ -15,22 +15,21 @@
|
|
|
15
15
|
* apiKey: process.env.ABLO_API_KEY,
|
|
16
16
|
* });
|
|
17
17
|
*
|
|
18
|
-
* const
|
|
19
|
-
* await ablo.
|
|
18
|
+
* const reports = ablo.weatherReports.list({ where: { status: 'pending' } });
|
|
19
|
+
* await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
|
-
* For headless agents (workers, bots), pass
|
|
23
|
-
*
|
|
22
|
+
* For headless agents (workers, bots), pass the same schema and an API key
|
|
23
|
+
* scoped for that server runtime:
|
|
24
24
|
*
|
|
25
25
|
* ```ts
|
|
26
26
|
* const bot = Ablo({
|
|
27
27
|
* schema,
|
|
28
28
|
* apiKey: process.env.ABLO_API_KEY,
|
|
29
|
-
* kind: 'agent',
|
|
30
29
|
* });
|
|
31
30
|
* ```
|
|
32
31
|
*/
|
|
33
|
-
export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type
|
|
32
|
+
export { Ablo, computeFKDepthPriority, type AbloOptions, type InternalAbloOptions, type ClaimedOptions, type IfClaimedPolicy, type IntentWaitOptions, type ModelCountOptions, type ModelListOptions, type ModelListScope, type ModelLoadOptions, type ModelOperations, type ModelReadOptions, } from './Ablo.js';
|
|
34
33
|
export type { AbloPersistence } from './persistence.js';
|
|
35
|
-
export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions,
|
|
34
|
+
export type { AbloApi, AbloApiClientOptions, AbloApiIntents, Agent, AgentIntentInput, AgentIntentOptions, AgentOptions, AgentModelClient, AgentModelReadOptions, AgentModelMutationOptions, AgentRunContext, AgentRunDone, AgentRunFailed, AgentRunCancelled, AgentRunOptions, AgentRunResult, AgentRunStatus, Capability, CapabilityCreateOptions, CapabilityParticipantKind, CapabilityRecord, CapabilityResource, CapabilityRevocation, CapabilityScope, Task, TaskCloseOptions, TaskCloseResult, TaskCreateOptions, TaskResource, } from './ApiClient.js';
|
|
36
35
|
export type { EngineParticipant, JoinedParticipant, ParticipantJoinOptions, ParticipantManager, ParticipantScope, ParticipantStatus, ScopedIntents, ScopedPresence, } from '../sync/participants.js';
|
package/dist/client/index.js
CHANGED
|
@@ -15,18 +15,17 @@
|
|
|
15
15
|
* apiKey: process.env.ABLO_API_KEY,
|
|
16
16
|
* });
|
|
17
17
|
*
|
|
18
|
-
* const
|
|
19
|
-
* await ablo.
|
|
18
|
+
* const reports = ablo.weatherReports.list({ where: { status: 'pending' } });
|
|
19
|
+
* await ablo.weatherReports.create({ location: 'Stockholm', status: 'pending' });
|
|
20
20
|
* ```
|
|
21
21
|
*
|
|
22
|
-
* For headless agents (workers, bots), pass
|
|
23
|
-
*
|
|
22
|
+
* For headless agents (workers, bots), pass the same schema and an API key
|
|
23
|
+
* scoped for that server runtime:
|
|
24
24
|
*
|
|
25
25
|
* ```ts
|
|
26
26
|
* const bot = Ablo({
|
|
27
27
|
* schema,
|
|
28
28
|
* apiKey: process.env.ABLO_API_KEY,
|
|
29
|
-
* kind: 'agent',
|
|
30
29
|
* });
|
|
31
30
|
* ```
|
|
32
31
|
*/
|
|
@@ -17,9 +17,9 @@ export function validateAbloOptions(input) {
|
|
|
17
17
|
return new Error('Ablo: `url` is required. Pass the sync server URL, e.g. ' +
|
|
18
18
|
`Ablo({ baseURL: 'wss://sync.ablo.dev', schema, user })`);
|
|
19
19
|
}
|
|
20
|
-
// Schema is optional for the
|
|
21
|
-
// Ablo({ apiKey }).
|
|
22
|
-
// Passing a schema only enables typed model sugar (`ablo.
|
|
20
|
+
// Schema is optional for the model-first API:
|
|
21
|
+
// Ablo({ apiKey }).model('clauses').retrieve(...)
|
|
22
|
+
// Passing a schema only enables typed model sugar (`ablo.weatherReports.update(...)`).
|
|
23
23
|
if (!configuredApiKey &&
|
|
24
24
|
!configuredAuthToken &&
|
|
25
25
|
!options.capabilityToken &&
|
package/dist/core/index.d.ts
CHANGED
|
@@ -28,6 +28,8 @@ export { ObjectStore } from '../stores/ObjectStore.js';
|
|
|
28
28
|
export { NetworkMonitor } from '../NetworkMonitor.js';
|
|
29
29
|
export { SyncWebSocket, type SyncDelta, type VersionVector, type BootstrapHint, type SyncGroupChangePayload, type BootstrapDataEvent, type PresenceUpdateEvent, type SyncWebSocketOptions, } from '../sync/SyncWebSocket.js';
|
|
30
30
|
export { BootstrapHelper, type BootstrapData, type BootstrapOptions, type BootstrapFetchResult } from '../sync/BootstrapHelper.js';
|
|
31
|
+
export { createIntentStream, type AttachableIntentStream, type IntentStreamConfig, } from '../sync/createIntentStream.js';
|
|
32
|
+
export { awaitIntentGrant, type GrantTransport, } from '../sync/awaitIntentGrant.js';
|
|
31
33
|
export { OfflineTransactionStore, offlineTxStore, Priority } from '../sync/OfflineTransactionStore.js';
|
|
32
34
|
export { PropertyType, LoadStrategy, MutationOperationType } from '../types/index.js';
|
|
33
35
|
export type { PropertyMetadata, ReferenceMetadata, ModelMetadata, SyncAction, DeltaPacket, BootstrapMetadata, DatabaseMetadata, } from '../types/index.js';
|
package/dist/core/index.js
CHANGED
|
@@ -49,6 +49,13 @@ export { NetworkMonitor } from '../NetworkMonitor.js';
|
|
|
49
49
|
// Sync layer
|
|
50
50
|
export { SyncWebSocket, } from '../sync/SyncWebSocket.js';
|
|
51
51
|
export { BootstrapHelper } from '../sync/BootstrapHelper.js';
|
|
52
|
+
// Intent coordination primitives (the lower-level pieces behind the
|
|
53
|
+
// consumer-facing `ablo.<model>.claim`). The stream factory builds the
|
|
54
|
+
// announce/await machinery on a SyncWebSocket; `awaitIntentGrant` is the
|
|
55
|
+
// fair-queue grant coordinator. Exposed on /core for framework-level
|
|
56
|
+
// orchestration and e2e harnesses — NOT on the consumer `.` root.
|
|
57
|
+
export { createIntentStream, } from '../sync/createIntentStream.js';
|
|
58
|
+
export { awaitIntentGrant, } from '../sync/awaitIntentGrant.js';
|
|
52
59
|
// Offline transaction queue — moved out of the main barrel in the headless
|
|
53
60
|
// audit cleanup (see docs/headless-audit.md §4.1 Task 23). The class
|
|
54
61
|
// touches indexedDB + crypto.subtle and therefore cannot live on the main
|
package/dist/errors.d.ts
CHANGED
|
@@ -111,21 +111,21 @@ export declare class AbloStaleContextError extends AbloError {
|
|
|
111
111
|
});
|
|
112
112
|
}
|
|
113
113
|
/**
|
|
114
|
-
* The target entity currently
|
|
115
|
-
*
|
|
114
|
+
* The target entity is currently claimed by another participant and the caller
|
|
115
|
+
* asked the SDK not to read/write through that claim.
|
|
116
116
|
*
|
|
117
|
-
* Use `
|
|
118
|
-
* `
|
|
117
|
+
* Use `ifClaimed: 'wait'` to wait for the claim to clear, or
|
|
118
|
+
* `ifClaimed: 'return'` to inspect active claims yourself.
|
|
119
119
|
*/
|
|
120
|
-
export declare class
|
|
121
|
-
readonly type: "
|
|
122
|
-
readonly
|
|
120
|
+
export declare class AbloClaimedError extends AbloError {
|
|
121
|
+
readonly type: "AbloClaimedError";
|
|
122
|
+
readonly claims?: ReadonlyArray<unknown>;
|
|
123
123
|
constructor(message: string, options?: {
|
|
124
124
|
code?: string;
|
|
125
125
|
httpStatus?: number;
|
|
126
126
|
requestId?: string;
|
|
127
127
|
cause?: unknown;
|
|
128
|
-
|
|
128
|
+
claims?: ReadonlyArray<unknown>;
|
|
129
129
|
});
|
|
130
130
|
}
|
|
131
131
|
/**
|
package/dist/errors.js
CHANGED
|
@@ -109,19 +109,19 @@ export class AbloStaleContextError extends AbloError {
|
|
|
109
109
|
}
|
|
110
110
|
}
|
|
111
111
|
/**
|
|
112
|
-
* The target entity currently
|
|
113
|
-
*
|
|
112
|
+
* The target entity is currently claimed by another participant and the caller
|
|
113
|
+
* asked the SDK not to read/write through that claim.
|
|
114
114
|
*
|
|
115
|
-
* Use `
|
|
116
|
-
* `
|
|
115
|
+
* Use `ifClaimed: 'wait'` to wait for the claim to clear, or
|
|
116
|
+
* `ifClaimed: 'return'` to inspect active claims yourself.
|
|
117
117
|
*/
|
|
118
|
-
export class
|
|
119
|
-
type = '
|
|
120
|
-
|
|
118
|
+
export class AbloClaimedError extends AbloError {
|
|
119
|
+
type = 'AbloClaimedError';
|
|
120
|
+
claims;
|
|
121
121
|
constructor(message, options) {
|
|
122
122
|
super(message, options);
|
|
123
|
-
if (options?.
|
|
124
|
-
this.
|
|
123
|
+
if (options?.claims !== undefined)
|
|
124
|
+
this.claims = options.claims;
|
|
125
125
|
}
|
|
126
126
|
}
|
|
127
127
|
/**
|
|
@@ -224,7 +224,8 @@ export function translateHttpError(status, body, requestId) {
|
|
|
224
224
|
flatError ??
|
|
225
225
|
(typeof body === 'string' ? body : `HTTP ${status}`);
|
|
226
226
|
const requiredCapability = nested?.requiredCapability ?? parsed.requiredCapability;
|
|
227
|
-
const
|
|
227
|
+
const publicCode = code === 'intent_conflict' ? 'claim_conflict' : code;
|
|
228
|
+
const baseOpts = { code: publicCode, httpStatus: status, requestId };
|
|
228
229
|
if (status === 401)
|
|
229
230
|
return new AbloAuthenticationError(message, baseOpts);
|
|
230
231
|
if (status === 403 || code === 'capability_scope_denied' || code === 'capability_invalid') {
|
|
@@ -233,6 +234,13 @@ export function translateHttpError(status, body, requestId) {
|
|
|
233
234
|
}
|
|
234
235
|
return new AbloPermissionError(message, baseOpts);
|
|
235
236
|
}
|
|
237
|
+
// Claim enforcement also rides 409 (a commit blocked by a foreign claim).
|
|
238
|
+
// Discriminate on the code BEFORE the generic idempotency mapping so a
|
|
239
|
+
// claim rejection surfaces as AbloClaimedError, not AbloIdempotencyError —
|
|
240
|
+
// same typed error the WebSocket commit path yields for these codes.
|
|
241
|
+
if (code === 'intent_conflict' || code === 'claim_conflict' || code === 'entity_claimed') {
|
|
242
|
+
return new AbloClaimedError(message, baseOpts);
|
|
243
|
+
}
|
|
236
244
|
if (status === 409)
|
|
237
245
|
return new AbloIdempotencyError(message, baseOpts);
|
|
238
246
|
if (status === 422 || status === 400)
|
package/dist/index.d.ts
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* import Ablo from '@abloatai/ablo';
|
|
6
6
|
*
|
|
7
7
|
* const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
8
|
-
* await ablo.
|
|
9
|
-
* await ablo.
|
|
8
|
+
* await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
9
|
+
* await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
|
|
10
10
|
*
|
|
11
11
|
* type Entry = Ablo.Peer;
|
|
12
12
|
* ```
|
|
13
13
|
*
|
|
14
|
-
* `Ablo({ schema, apiKey })` gives typed model
|
|
15
|
-
* gives the
|
|
16
|
-
*
|
|
14
|
+
* `Ablo({ schema, apiKey })` gives typed model clients. `Ablo({ apiKey })`
|
|
15
|
+
* gives the HTTP model/commit client for agents, MCP routes, and custom
|
|
16
|
+
* runtimes.
|
|
17
17
|
*
|
|
18
|
-
* Stripe / Anthropic / OpenAI all do this: one import,
|
|
18
|
+
* Stripe / Anthropic / OpenAI all do this: one import, model clients
|
|
19
19
|
* reached via dot-access on the engine, types via namespace dots.
|
|
20
20
|
*
|
|
21
21
|
* Public subpaths:
|
|
@@ -42,14 +42,15 @@
|
|
|
42
42
|
* If you don't recognize one, you don't need it — the default path covers you.
|
|
43
43
|
*/
|
|
44
44
|
export { Ablo } from './client/Ablo.js';
|
|
45
|
-
export type { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions,
|
|
45
|
+
export type { AbloOptions, ModelCountOptions, ModelListOptions, ModelListScope, ModelLoadOptions, ClaimOptions, ClaimedRow, ModelOperations, } from './client/Ablo.js';
|
|
46
46
|
export type { AbloPersistence } from './client/persistence.js';
|
|
47
47
|
export { session, agent } from './principal.js';
|
|
48
48
|
import { Ablo } from './client/Ablo.js';
|
|
49
49
|
export default Ablo;
|
|
50
50
|
export { dataSource, abloSource, signAbloSourceRequest, verifyAbloSourceRequest, } from './source/index.js';
|
|
51
51
|
export { defaultPolicy } from './policy/index.js';
|
|
52
|
-
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError,
|
|
52
|
+
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, } from './errors.js';
|
|
53
53
|
export type { CommitReceipt, RequiredCapability } from './errors.js';
|
|
54
|
+
export type { Register, DefaultSyncShape } from './types/global.js';
|
|
54
55
|
export { defineMutators } from './mutators/defineMutators.js';
|
|
55
56
|
export { createTransaction, type Transaction } from './mutators/Transaction.js';
|
package/dist/index.js
CHANGED
|
@@ -5,17 +5,17 @@
|
|
|
5
5
|
* import Ablo from '@abloatai/ablo';
|
|
6
6
|
*
|
|
7
7
|
* const ablo = Ablo({ schema, apiKey: process.env.ABLO_API_KEY });
|
|
8
|
-
* await ablo.
|
|
9
|
-
* await ablo.
|
|
8
|
+
* await ablo.weatherReports.load({ where: { id: 'report_stockholm' } });
|
|
9
|
+
* await ablo.weatherReports.update('report_stockholm', { status: 'ready' });
|
|
10
10
|
*
|
|
11
11
|
* type Entry = Ablo.Peer;
|
|
12
12
|
* ```
|
|
13
13
|
*
|
|
14
|
-
* `Ablo({ schema, apiKey })` gives typed model
|
|
15
|
-
* gives the
|
|
16
|
-
*
|
|
14
|
+
* `Ablo({ schema, apiKey })` gives typed model clients. `Ablo({ apiKey })`
|
|
15
|
+
* gives the HTTP model/commit client for agents, MCP routes, and custom
|
|
16
|
+
* runtimes.
|
|
17
17
|
*
|
|
18
|
-
* Stripe / Anthropic / OpenAI all do this: one import,
|
|
18
|
+
* Stripe / Anthropic / OpenAI all do this: one import, model clients
|
|
19
19
|
* reached via dot-access on the engine, types via namespace dots.
|
|
20
20
|
*
|
|
21
21
|
* Public subpaths:
|
|
@@ -78,11 +78,7 @@ export { defaultPolicy } from './policy/index.js';
|
|
|
78
78
|
// Typed error hierarchy — Stripe-style. One import gets every class
|
|
79
79
|
// consumers need to discriminate failures (`e instanceof AbloX` or
|
|
80
80
|
// `e.type === 'AbloX'`) plus the HTTP-response translator.
|
|
81
|
-
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError,
|
|
82
|
-
// Typed-global augmentation point. Consumers declare their Schema/Presence/
|
|
83
|
-
// Intents/UserMeta once in a `.d.ts` via `declare global { interface AbloSync
|
|
84
|
-
// { ... } }`. Resolver types live under the `Ablo` namespace —
|
|
85
|
-
// `Ablo.ResolveSchema`, `Ablo.ResolvePresence`, etc. — pure type-level.
|
|
81
|
+
export { SyncSessionError, AbloError, AbloAuthenticationError, AbloPermissionError, AbloRateLimitError, AbloIdempotencyError, AbloConnectionError, AbloValidationError, AbloServerError, AbloStaleContextError, AbloClaimedError, CapabilityError, translateHttpError, } from './errors.js';
|
|
86
82
|
// Advanced — most apps never import this. Custom (Zero-style) mutators:
|
|
87
83
|
// `ablo.<model>.create/update/delete` already covers normal writes. Reach
|
|
88
84
|
// for `defineMutators` only when you need a named, multi-step mutation with
|
|
@@ -151,18 +151,11 @@ export interface CommitResult {
|
|
|
151
151
|
* When omitted, the SDK auto-generates a UUIDv4 per mutation so every
|
|
152
152
|
* call is retry-safe by default. Opt out with `{ idempotencyKey: null }`
|
|
153
153
|
* if you genuinely want retry-unsafe writes (rare).
|
|
154
|
-
* - `timeout` — abort the request if it takes longer than this many ms.
|
|
155
|
-
* No default (uses underlying transport's timeout).
|
|
156
|
-
* - `maxNetworkRetries` — retry with exponential backoff on 5xx / 429 /
|
|
157
|
-
* network errors. The same `idempotencyKey` is reused across retries
|
|
158
|
-
* so the server dedupes correctly. Default: 0.
|
|
159
154
|
* - `label` — human-readable audit tag. Flows to `mutation_log.label`
|
|
160
155
|
* server-side for operator debugging ("nightly cleanup", "user click").
|
|
161
156
|
*/
|
|
162
157
|
export interface MutationOptions {
|
|
163
158
|
idempotencyKey?: string | null;
|
|
164
|
-
timeout?: number;
|
|
165
|
-
maxNetworkRetries?: number;
|
|
166
159
|
label?: string;
|
|
167
160
|
wait?: 'queued' | 'confirmed';
|
|
168
161
|
readAt?: number | null;
|
|
@@ -205,9 +198,8 @@ export interface MutationOperation {
|
|
|
205
198
|
/**
|
|
206
199
|
* Per-op idempotency + audit metadata. `idempotencyKey` doubles as
|
|
207
200
|
* the `mutation_log.client_tx_id` cache key; `label` is persisted to
|
|
208
|
-
* `mutation_log.label` for debugging.
|
|
209
|
-
*
|
|
210
|
-
* NOT sent over the wire.
|
|
201
|
+
* `mutation_log.label` for debugging. These are the only `MutationOptions`
|
|
202
|
+
* fields carried over the wire.
|
|
211
203
|
*/
|
|
212
204
|
options?: Pick<MutationOptions, 'idempotencyKey' | 'label'>;
|
|
213
205
|
}
|
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
*/
|
|
22
22
|
import type { Schema } from '../schema/schema.js';
|
|
23
23
|
import type { SyncStoreContract } from '../react/context.js';
|
|
24
|
-
import { type MutateActions } from '
|
|
25
|
-
import { type ReaderActions, type ReaderFindOptions } from '
|
|
24
|
+
import { type MutateActions } from './mutateActions.js';
|
|
25
|
+
import { type ReaderActions, type ReaderFindOptions } from './readerActions.js';
|
|
26
26
|
/**
|
|
27
27
|
* The full transaction surface. `tx.mutations.<key>.*` for writes,
|
|
28
28
|
* `tx.read.<key>.*` for imperative reads. Re-exports the base read options
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
* microtask coalescer in `TransactionQueue` collapses N pushes into one
|
|
20
20
|
* wire commit. Same shape Zero uses: no `insertMany`, just an array map.
|
|
21
21
|
*/
|
|
22
|
-
import { createMutateActions } from '
|
|
23
|
-
import { createReaderActions } from '
|
|
22
|
+
import { createMutateActions } from './mutateActions.js';
|
|
23
|
+
import { createReaderActions } from './readerActions.js';
|
|
24
24
|
import { AbloValidationError } from '../errors.js';
|
|
25
25
|
/**
|
|
26
26
|
* Build a Transaction for a single mutator invocation. The returned object
|