Skip to content
AbloAblo Docs
Esc
navigateopen⌘Jpreview
On this page

Concurrency Convention

What Ablo checks when a guarded write depends on earlier state.

Ablo never infers whether a write depends on earlier state. You decide, in two places. The model’s conflict setting in the schema says what each kind of participant does when it hits a conflict, and it is the policy for that model. A per-write onStale states the disposition for one write. Ablo enforces what you declared and nothing else.

Unguarded writes

A plain write has no stale premise:

await ablo.records.update({ id, data: { status: 'done' } });

If no active claim conflicts with it, the write is last-write-wins. That is a choice rather than a fallback: use it for independent assignments where the latest value should win. When a model’s writes are never independent, say so once in its conflict setting instead of at every call site.

Guarded writes

Pass the exact returned rows when a write is based on values previously read:

const record = await ablo.records.get({ id });
const policy = await ablo.policies.get({ id: policyId });
if (!record || !policy) throw new Error('required input is missing');

await ablo.records.update({
  id: record.id,
  data: { status: 'done' },
  reads: [record, policy],
});

Ablo privately resolves each exact object to its model, id, and read watermark, then compares those premises with current state when the write is accepted. Clones, fabrications, and rows returned by another client are rejected locally.

Disposition If the premise is stale
reject Reject the write with AbloStaleContextError.
notify Keep the current row, return a StaleNotification, and let the caller reconcile.
overwrite Apply the new value without enforcing the stale premise.

notify is useful when an agent or human can merge the new information. reject is useful when the caller should restart from fresh state. Use overwrite only when the newer assignment should unconditionally win.

Functional updates

For a pure read-modify-write calculation, use the functional update form:

await ablo.counters.update(counterId, (current) => ({
  value: current.value + 1,
}));

It performs the read, guarded write, and bounded reconciliation loop for you. See Coordination.

Claims

A claim protects a target across a longer interval. By default, other participants cannot write the claimed target, while contenders that claim it wait their turn. Reads remain open. A model’s explicit conflict policy can choose a different disposition for a participant kind.

Claims and stale guards protect different things:

  • A claim excludes other participants while it is held.
  • A stale guard proves that the state a write depended on has not changed.
  • A write made under a claim is still rejected if its own claimed snapshot has become stale.

See Coordination for the API.

Cross-row and batch premises

Model writes and lower-level commits can declare rows they read even when the write targets somewhere else. This protects decisions such as “update the record only if the deal I inspected has not changed.” A stale batch premise applies to the whole batch so atomicity is preserved.

Use the high-level model methods unless you are building a custom runtime. When you do use batch premises, declare only the rows or groups that materially influenced the decision; overly broad premises create unnecessary contention.

Notifications

A StaleNotification identifies the stale premise and provides the current state needed to reconcile. The original write has not been applied.

A typical loop is:

  1. Inspect the current value in the notification.
  2. Recompute the intended change.
  3. Submit a new guarded write with a fresh premise.

Give this loop a retry budget. Continuous contention should surface to the caller rather than retry forever.

Boundaries

Concurrency control does not replace:

  • database constraints and transactions for application invariants;
  • authorization for deciding who may read or write;
  • idempotency for safely replaying the same request;
  • claims for exclusivity across slow, side-effecting work.

The rule is simple: the model’s conflict setting is the policy, and each write declares what it read. Plain writes are last-write-wins because declaring nothing is itself a decision, so make it deliberately.

Was this page helpful?