@abloatai/ablo 0.58.0 → 0.59.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.
@@ -2,6 +2,20 @@
2
2
 
3
3
  > Choose plain writes, functional updates, stale guards, or claims without losing concurrent work.
4
4
 
5
+ This page owns row coordination: acquire ownership, proceed from the granted
6
+ state, and release on every exit path.
7
+
8
+ ## Choose the owner first
9
+
10
+ | Where the result lands | Start here |
11
+ |---|---|
12
+ | Existing application path: API, GraphQL operation, Postgres transaction, filesystem write, or Git branch merge | [Coordinate existing work](./coordinate-existing-work.md) |
13
+ | Ablo model row, written through the Ablo client | Continue on this page |
14
+
15
+ If Ablo only decides who may start, use the first row and stop here. The claim
16
+ examples below own Ablo-row writes; they are not the starter for wrapping an
17
+ existing operation.
18
+
5
19
  Ablo gives you several concurrency tools because not every write has the same
6
20
  meaning. Choose the narrowest one that matches the operation.
7
21
 
@@ -125,19 +139,66 @@ const forecast = await generateForecast(claim.data.location);
125
139
  await ablo.reports.update({
126
140
  id: claim.data.id,
127
141
  data: { forecast, status: 'ready' },
142
+ claim,
128
143
  });
129
144
  ```
130
145
 
131
146
  If another participant already holds the target, `claim` waits its turn and
132
- then resolves with a fresh row in `claim.data`. Ordinary reads remain open. By
133
- default, a write from a participant that does not hold the active claim is
134
- rejected.
147
+ then resolves with a fresh row in `claim.data`. Ordinary reads remain open. Pass
148
+ the handle as `claim` on the write so Ablo can verify that you still own the row
149
+ and that it has not changed since the claim was granted.
135
150
 
136
151
  Bind claims with `await using` whenever possible. The claim then releases when
137
152
  the scope exits, including when the external call or write throws. For runtimes
138
153
  without explicit resource management, use `try/finally` and
139
154
  `await claim.release()`.
140
155
 
156
+ ### Handle an expired claim
157
+
158
+ When heartbeat is unset, the lease ends at its TTL. A delayed write that passes
159
+ the expired handle rejects with `AbloClaimedError` and code `claim_lost`. Do not
160
+ apply the prepared result elsewhere; clean up best-effort, then restart from a
161
+ new claim and its fresh `claim.data`.
162
+
163
+ ```ts
164
+ import { AbloClaimedError } from '@abloatai/ablo';
165
+
166
+ const claim = await ablo.tasks.claim({ id: taskId, ttl: '2s' });
167
+ try {
168
+ await new Promise((resolve) => setTimeout(resolve, 2300));
169
+ await ablo.tasks.update({
170
+ id: claim.data.id,
171
+ data: { status: 'done' },
172
+ claim,
173
+ });
174
+ } catch (error) {
175
+ if (error instanceof AbloClaimedError && error.code === 'claim_lost') {
176
+ console.log(error.code);
177
+ } else {
178
+ throw error;
179
+ }
180
+ } finally {
181
+ try { await claim.release(); } catch { /* already expired */ }
182
+ }
183
+ ```
184
+
185
+ ### Keep a claim alive
186
+
187
+ Set `ttl` to how quickly another worker should recover if this one stops. If the
188
+ work can take longer, set `heartbeat: true` so Ablo renews the claim:
189
+
190
+ ```ts
191
+ const claim = await ablo.records.claim({
192
+ id: recordId,
193
+ ttl: '30s',
194
+ heartbeat: true,
195
+ });
196
+ ```
197
+
198
+ Leave `heartbeat` out when the claim should expire after the TTL; do not pass
199
+ `false`. If a write returns `claim_lost`, discard that result, claim the row
200
+ again, and restart from the new `claim.data`.
201
+
141
202
  ### One identity per participant
142
203
 
143
204
  Explicit claims coordinate authenticated participants. Two clients using the
@@ -241,100 +302,15 @@ The main methods are:
241
302
  | Method | Purpose |
242
303
  |---|---|
243
304
  | `claim({ id, ...options })` | Read and claim an existing model row; the handle includes fresh row data. |
244
- | `claim(id, options)` | Claim an identifier in a registered model namespace without reading a row. |
245
305
  | `claim.state({ id })` | Read the current holder without blocking. |
246
306
  | `claim.queue({ id })` | Read the current wait order. |
247
307
  | `claim.release({ id })` | Release early when you do not hold a handle. |
248
308
  | `join({ scope })` | Observe presence for a broader scope. |
249
309
 
250
- Choose the overload deliberately:
251
-
252
- | Form | Evidence requirement | Typical use |
253
- |---|---|---|
254
- | `model.claim({ id })` | The row exists and the caller may read it. | Coordinate work on a synchronized row while using `handle.data`. |
255
- | `model.claim(id, options)` | The model namespace is registered; no row is read. | Select one participant before calling an existing authoritative service. |
256
-
257
- The identifier-only form is row-free, not schema-free. It does not authorize a
258
- worker or test fixture to push an unrelated model into an inherited production
259
- schema. Register the namespace through the application's normal schema process,
260
- or select an already registered namespace whose ownership matches the operation.
261
-
262
- ## Coordinate an existing database operation
263
-
264
- Use this pattern when an application already has a service that protects a
265
- transition with a Postgres row lock or advisory lock, but slow preparation such
266
- as OCR, a model call, or another tool currently happens while that database
267
- lock is held.
268
-
269
- Keep the ownership boundary explicit:
270
-
271
- | Owner | Responsibility |
272
- |---|---|
273
- | Ablo claim | Select one participating worker before expensive work begins. |
274
- | Application service | Authoritative re-read, transition policy, database lock, idempotency, and commit. |
275
- | Postgres | Canonical row, constraints, and final integrity boundary. |
276
-
277
- The operation runs in this order:
278
-
279
- ```text
280
- claim identifier
281
- -> prepare expensive result once
282
- -> application service re-reads and commits under its database lock
283
- -> release claim in finally
284
- ```
285
-
286
- Model the service seam as two operations rather than moving database policy
287
- into a resolver, worker, or agent tool:
288
-
289
- ```ts
290
- interface ExistingOperationService<Input, Result, Row> {
291
- run(
292
- input: Input,
293
- prepare: () => Promise<Result>,
294
- ): Promise<Row>;
295
-
296
- commitPrepared(
297
- input: Input,
298
- prepared: Result,
299
- ): Promise<Row>;
300
- }
301
- ```
302
-
303
- The existing rollout path calls `run` and preserves current behavior. The
304
- coordinated path wins the claim, prepares once, then calls `commitPrepared`.
305
- Both methods stay under the same application-service owner and enforce the same
306
- authorization and transition rules.
307
-
308
- When the transition permits it, implement `commitPrepared` as one SQL statement
309
- that acquires a transaction-level advisory lock, re-reads the row, validates
310
- its current state, and updates it. The statement's implicit transaction
311
- releases the advisory lock automatically. This can remove several sequential
312
- client/database round trips without replacing the existing database lock.
313
-
314
- Do not make any of these substitutions:
315
-
316
- - Do not assume a remote Ablo request joins a local Postgres transaction.
317
- - Do not remove database constraints or locks during the coordination rollout.
318
- - Do not prepare expensive work speculatively before the claim resolves.
319
- - Do not assume direct SQL writers obey an Ablo claim. A claim coordinates only
320
- callers routed through the participating operation.
321
- - Do not use a claim as durable workflow state. A lease expires; workflow
322
- progress must survive independently.
323
-
324
- Measure the old and coordinated paths with the same inputs. Record cold and
325
- warm claim acquire/release latency, database round-trip latency, database-lock
326
- duration, end-to-end latency, duplicate work under contention, and recovery
327
- after worker exit. Keep a per-operation switch to the old path until the new
328
- path preserves behavior and improves the selected race at production
329
- percentiles.
330
-
331
- For a runnable GraphQL.js implementation and PostgreSQL race/crash proof, see
332
- [GraphQL.js over an existing backend](./approaches/graphql/graphql-js.md).
333
- For a domain-neutral hosted lease proof, see
334
- [Verify hosted coordination separately](./examples/coordination-conformance.md).
335
- For the same operation boundary applied to source-versioned document
336
- processing, see
337
- [Process an existing document once](./examples/existing-document-pipeline.md).
310
+ This page owns row-backed claims: `model.claim({ id })` reads and claims an Ablo
311
+ model row, and the handle carries fresh data. Identifier-only claims before an
312
+ existing authoritative service have a different persistence boundary; use the
313
+ [coordinate-existing-work guide](./coordinate-existing-work.md) for that form.
338
314
 
339
315
  ## Choosing correctly
340
316
 
@@ -229,6 +229,24 @@ ABLO_API_KEY=sk_… npx ablo webhooks list # endpoints + delivery health
229
229
  place to look when a mirror falls behind. [Webhooks](./webhooks.md) covers the
230
230
  handler, the Standard Webhooks signature, and rolling a secret.
231
231
 
232
+ ## Multi-stage schema changes
233
+
234
+ `ablo plan` is the read-only front door for source, active artifact, and
235
+ PostgreSQL together. For a live rename, type transition, or required-field
236
+ change, commit a deployment manifest and pass the same file to plan and push.
237
+ Its gates are `expand`, `dual_write`, `backfill`, `verify`, `switch`, and
238
+ `contract`; each names an owner, resource, dependencies, status, and action.
239
+
240
+ ```bash
241
+ npx ablo plan --manifest ablo/deployment.json
242
+ npx ablo push --manifest ablo/deployment.json --yes
243
+ ```
244
+
245
+ A live manifest cannot combine expand and contract for the same resource.
246
+ Contract belongs in a later manifest with its own recorded `approval`. A
247
+ backfill being finished is not contract evidence; verification and client-drain
248
+ gates must also be satisfied.
249
+
232
250
  ## What to watch once it is live
233
251
 
234
252
  - **`ablo logs`:** commit activity as it happens, scoped by the key. A
@@ -263,7 +281,7 @@ and what each promises.
263
281
  `ablo connect check` all green.
264
282
  2. A secret `sk_` in the deploy environment as `ABLO_API_KEY` — never in a
265
283
  browser bundle.
266
- 3. `ablo push --yes` in the pipeline, ahead of the code that needs the new models.
284
+ 3. `ablo plan` reviewed, followed by fingerprint-gated `ablo push --yes`.
267
285
  4. `ablo status --json` gating the deploy on an empty `blockers` array.
268
286
  5. Browser clients on a root-bound `pk_` or an `authEndpoint`, not a secret key.
269
287
  6. Webhook endpoints registered at their deployed URLs, with the signing secret
@@ -1,9 +1,9 @@
1
- # Process an existing document once
1
+ # Build an evidence-backed document pipeline
2
2
 
3
3
  > This is an advanced evidence-backed state reference, not the default Ablo
4
4
  > integration tutorial. If you are coordinating an operation that already owns
5
5
  > its Postgres write, start with the
6
- > [Agent Integration Decision Guide](../agent-integration-decision-guide.md).
6
+ > [coordinate existing work guide](../coordinate-existing-work.md).
7
7
 
8
8
  > Coordinate expensive processing over an application-owned document without taking ownership of uploads or storage.
9
9
 
package/docs/faq.md ADDED
@@ -0,0 +1,75 @@
1
+ # FAQ
2
+
3
+ > Short answers to the implementation choices developers encounter most often.
4
+
5
+ ## Does Ablo replace PostgreSQL?
6
+
7
+ No. PostgreSQL remains the durable source of truth. Keep its schema, constraints,
8
+ transactions, row-level security, and short write locks. Ablo coordinates work
9
+ around that state and can route guarded writes into it.
10
+
11
+ ## Does Ablo replace Redis locks?
12
+
13
+ It can replace an application-owned reservation layer, but it does not require a
14
+ rewrite. Ablo standardizes resource identity, participant identity, leases,
15
+ waiting, recovery, stale-work rejection, and visibility. Teams can begin by
16
+ coordinating an existing operation while its final transaction remains in the
17
+ application.
18
+
19
+ ## Is Ablo only for agents?
20
+
21
+ No. Agents, workers, application services, and people can coordinate over the
22
+ same resources. The package-root client is suited to stateless server work; the
23
+ [React client](./react.md) adds live state and presence for human interfaces.
24
+
25
+ ## Which client should I import?
26
+
27
+ Use the default export from `@abloatai/ablo` for agents, workers, route handlers,
28
+ and server operations. It uses ordinary request/response HTTP. Use
29
+ `@abloatai/ablo/react` when a live interface needs local synchronized state,
30
+ subscriptions, or presence.
31
+
32
+ ## What is the difference between get and read?
33
+
34
+ `get({ id })` observes the current row. `read({ id })` captures the exact row
35
+ version as evidence for a later guarded write. Pass that returned row in the
36
+ mutation's `reads` array when the decision must be rejected if its premise
37
+ changed.
38
+
39
+ ## When should I claim a resource?
40
+
41
+ Claim before slow or expensive work when another participant should not perform
42
+ conflicting work on the same business resource. Do not add a claim to every
43
+ write: a short, independent update can use its normal database and mutation
44
+ semantics.
45
+
46
+ ## Is a claim a database lock?
47
+
48
+ No. A claim is a participant-scoped lease held across work that may outlive one
49
+ database transaction. It expires after heartbeat loss. PostgreSQL locks still
50
+ protect the short authoritative transaction.
51
+
52
+ ## What if code writes directly to PostgreSQL?
53
+
54
+ Ablo observes the resulting change through the configured data source, but the
55
+ writer bypasses Ablo claims and request ordering. Keep database constraints for
56
+ rules that must apply to every writer.
57
+
58
+ ## Are retries exactly once?
59
+
60
+ Ablo idempotency deduplicates the same Ablo request within its retention window.
61
+ It does not make external side effects exactly once. Use the external provider's
62
+ idempotency mechanism or an application-owned effect record.
63
+
64
+ ## Does Ablo run long workflows?
65
+
66
+ No. Temporal, Inngest, queues, and application workers still own scheduling,
67
+ retries, and durable workflow progress. Ablo coordinates the shared state those
68
+ executions read and change.
69
+
70
+ ## Do I need to understand fencing first?
71
+
72
+ No. Start from the public behavior: a claim expires, another participant can
73
+ take over, and an obsolete owner cannot use an old claim to commit through Ablo.
74
+ The implementation mechanism is documented for operators and advanced
75
+ integrations, not required for basic SDK use.
@@ -97,8 +97,9 @@ claim queues fairly behind the holder).
97
97
  By default, a held claim rejects writes from other participants to the claimed
98
98
  target. Contenders that call `claim` wait their turn; ordinary reads remain
99
99
  open. While you hold a claim, the matching
100
- `ablo.<model>.update({ id, ... })` is rejected with `AbloStaleContextError` if
101
- the row changed underneath you after your claim point.
100
+ `ablo.<model>.update({ id, ..., claim })` proves ownership at write time and is
101
+ rejected with `AbloStaleContextError` if the row changed underneath you after
102
+ your claim point. Do not omit `claim` from a row-backed claimed write.
102
103
 
103
104
  ## Agent Runs
104
105
 
@@ -2,6 +2,9 @@
2
2
 
3
3
  > Make a retried write safe: the same key never applies the same change twice.
4
4
 
5
+ This page owns idempotency for updates and retries, including changed intent
6
+ and the `idempotency_conflict` failure code.
7
+
5
8
  An agent retries. A socket drops mid-commit, a worker restarts, a queue redelivers — and the write
6
9
  you already sent arrives again. An idempotency key is how Ablo tells a retry from a new intention.
7
10
 
@@ -0,0 +1,61 @@
1
+ # Choose the Ablo operation
2
+
3
+ > Find one implementation route from the work you are doing, copy its smallest recipe, and open guarantee details only when you need recovery behavior.
4
+
5
+ Do not read the documentation front to back. Start with the operation your
6
+ application already has, then use one row below.
7
+
8
+ | What you are implementing | Start here | Add only when |
9
+ |---|---|---|
10
+ | Get one row by id or list rows with the installed client | [Basic usage](./basic-usage.md) | Use `read`, not `get`, only when a later Ablo write depends on that exact row. |
11
+ | Configure a stateless worker's identity, permissions, or lifecycle | [Agents](./agents.md) | Keep the worker on HTTP; live human interfaces use React. |
12
+ | Wrap an existing API, service, Postgres transaction, filesystem write, or Git merge | [Coordinate existing work](./coordinate-existing-work.md) | Keep the final write in its existing owner. |
13
+ | Expose an existing named operation through GraphQL.js | [GraphQL.js](./approaches/graphql/graphql-js.md) | Keep the resolver dependent on that operation, not directly on Ablo. |
14
+ | Hold an Ablo model row while slow work runs, then write it through Ablo | [Coordination](./coordination.md) | Pass the returned claim handle to the write. |
15
+ | Reject a write when an earlier decision input changed | [Concurrency convention](./concurrency-convention.md) | Pass the exact object returned by `read` through `reads`. |
16
+ | Apply several Ablo writes all-or-none | [Atomic commits](./api.md#atomic-commits) | Put every operation and every captured premise in one `commits.create`. |
17
+ | Make a retried Ablo write safe | [Idempotency](./idempotency.md) | Derive one key from the business event and reuse it only for the identical request. |
18
+ | Send email, charge money, call a provider, or write a file | Keep that effect in the application | Use the provider's key or an application outbox; an Ablo key covers only the Ablo mutation. |
19
+ | Add a live human interface | [React](./react.md) | Humans use the WebSocket/live plane; stateless workers stay on HTTP. |
20
+
21
+ ## The four choices agents most often confuse
22
+
23
+ ```ts
24
+ // Observe one current row. No later stale check.
25
+ const task = await ablo.tasks.get({ id });
26
+
27
+ // Declare a premise for one later Ablo write.
28
+ const premise = await ablo.tasks.read({ id });
29
+ if (!premise) throw new Error('task not found');
30
+ await ablo.tasks.update({ id, data, reads: [premise] });
31
+
32
+ // Hold an Ablo row across slow work. The final write goes through Ablo.
33
+ await using claim = await ablo.tasks.claim({ id });
34
+ await ablo.tasks.update({ id, data, claim });
35
+
36
+ // Coordinate row-free work whose final write stays in the application.
37
+ await using lease = await ablo.taskRuns.claim(id, {
38
+ contention: { mode: 'skip' },
39
+ });
40
+ if (lease) await existingTaskService.complete(id);
41
+ ```
42
+
43
+ Use only one of those shapes unless the operation genuinely has both a claimed
44
+ target and separate captured premises. Claims answer who may work; `reads`
45
+ answer whether evidence is still current; `commits.create` answers whether
46
+ several Ablo writes land together; the existing database transaction still
47
+ owns atomicity for application-owned writes.
48
+
49
+ ## Before writing code
50
+
51
+ Answer these five questions:
52
+
53
+ 1. Which existing operation am I preserving?
54
+ 2. Does the final write belong to Ablo or to the application?
55
+ 3. Is the coordination identity a model row or only a stable business id?
56
+ 4. Which exact rows influenced the decision?
57
+ 5. Which writes must land together?
58
+
59
+ If an answer is unknown, preserve the existing operation and database boundary.
60
+ Do not introduce claims, captured reads, or atomic commits merely because they
61
+ exist.
@@ -0,0 +1,20 @@
1
+ # Ablo implementation index
2
+
3
+ > Route an implementation task to the smallest relevant Ablo guide before reading details.
4
+
5
+ Start with [Introduction](./index.md), then [Choose the Ablo operation](./implement.md). Its one routing table
6
+ separates ordinary reads, existing application writes, claims, captured reads,
7
+ atomic commits, retries, GraphQL, and external effects. Do not scan every page.
8
+
9
+ | Your task | Read first |
10
+ |---|---|
11
+ | Choose among nearby SDK and ownership patterns | [Choose the Ablo operation](./implement.md) |
12
+ | Add Ablo to existing work without replacing its API, transaction, filesystem write, or Git merge | [Coordinate existing work](./coordinate-existing-work.md) |
13
+ | Install Ablo and create a typed client | [Installation](./installation.md) |
14
+ | Read, write, and coordinate shared state | [Basic usage](./basic-usage.md) |
15
+ | Choose between a plain read, guarded read, claim, or atomic commit | [Concurrency convention](./concurrency-convention.md) |
16
+ | Look up an exact method, option, or error type | [API reference](./api.md) |
17
+ | Connect Ablo to an existing Postgres database | [Integration guide](./integration-guide.md) |
18
+
19
+ Follow links from that page only when its routing rule applies. Examples prove a
20
+ specific integration; they are not required reading for a first implementation.