Skip to content

Commit

commit is the verb every other one is defined against. This chapter follows a single call from the caller's .await to the receipt: what is true when it resolves, how a retry avoids a second entry, and every point in the writer pipeline where it can fail. You leave understanding why a watcher can see your change before you do.

let handle = runtime.journal.as_principal(alice());
let receipt = handle
.commit(MoveSymbol {
symbol: R1,
to: Point::new(10, 45),
})
.await?;

When that future resolves Ok, three things are true: your transaction was checked against the current state, applied to it, and sealed into history at a specific position. The ? catches the fourth possibility: your own check refused, and you got back your own typed error — Error::Rejected(SchematicError::UnknownSymbol(R1)), not a framework code. Rejection is a first-class answer, and after it, nothing happened at all.

handle is an ordinary Journal clone with one thing added: the principal the writer will witness on every entry it sends. commit itself is on every handle, scoped or not.

What comes back:

pub struct Receipt {
/// Where the entry landed in the one total order.
pub position: Position,
/// The family the entry belongs to.
pub correlation: CorrelationId,
}

position is the permanent serial number of your change in the one total order — the same Position that identifies this entry forever, which is chapter 2's naming lesson now cashed in. correlation ties this entry to the family of entries it may cause (chapter 5 uses it for tracing).

Records travel the same road minus the behavior: journal.record(ExportCompleted { .. }).await? — no check, no apply, but a real position and real metadata, because facts live in the same history as changes.

Here's a failure that will happen: the commit future gets dropped — a timeout, a UI hiccup, a network client giving up — but the writer had already applied the change. The caller doesn't know. It retries. Without protection, the schematic now has the symbol moved twice, or two entries where one belongs.

The fix is a caller-supplied idempotency key:

handle
.commit_keyed(RequestKey::new("place-r1"), PlaceSymbol { .. })
.await?

If the writer has seen that RequestKey before, it doesn't re-run anything — it returns the original receipt. Retry as many times as you like; history gets exactly one entry. On a desktop this matters at the margins; in a server-shaped deployment, where commits arrive over HTTP, it's the difference between "retry-safe API" and "double-charged invoice."

Under the Hood: The Writer Pipeline, Every Failure Point Named

Section titled “Under the Hood: The Writer Pipeline, Every Failure Point Named”

Your commit call does very little locally: it packages the transaction with a oneshot reply channel (a single-use return envelope) and sends it into the bounded mpsc channel from chapter 3. Then it waits for the envelope to come back. Everything else happens inside the writer task, one request at a time:

send ─▶ dedupe ─▶ mint metadata ─▶ check ─▶ (inverse) ─▶ apply ─▶ seal ─▶ publish ─▶ reply
A B C D
  • A — the channel is full. Not an error: your .await simply waits. This is backpressure — the system's honest way of saying "the writer is busy." Memory doesn't balloon; callers slow down instead.
  • A′ — the channel is closed. The writer is gone (shutdown, or poisoned — chapter 9). Commit fails with a runtime-down error. This is the only infrastructure failure a committer sees.
  • B — dedupe. The RequestKey index is consulted. Seen before → reply with the stored receipt, pipeline over.
  • Metadata minting. The writer stamps what it witnessed: principal (from the handle's scope), correlation, causation, request, link, and at — read from the one runtime clock. You never construct Metadata; it is #[non_exhaustive] and cannot be built outside harmos — and that's a principle, not a convenience: metadata is what the runtime witnesses; the payload is what the application declares. A caller-supplied timestamp or principal would be a claim, not an observation, and history built on claims can't be trusted for undo, audit, or replay.
  • C — check(&state). Refusal replies with your typed error. State untouched, no entry, no trace — a rejected transaction never existed.
  • Inverse capture — only for undoable transactions (chapter 7) — reads the pre-state.
  • D — apply(&mut state). Infallible by contract. A panic here is the one catastrophe: poison, evacuation, death, which chapter 9 gives its own chapter. What never happens: a half-applied state becoming visible.
  • Seal. The very value that just applied is sealed into an Entry at the next Position — apply-then-seal is why a panicking transaction can never contaminate history.
  • Publish. The entry joins the tail, the applied watch channel ticks, every observer's doorbell rings (chapter 5).
  • Reply. Your envelope comes back: Ok(Receipt).

One subtlety worth writing down: publish happens before reply. A watcher can see your change a microsecond before your own commit future resolves. The receipt isn't "the moment it happened" — it's your copy of the confirmation. History doesn't wait for you to read your mail.

And ordering? Determined entirely by arrival at the channel. Two tasks committing concurrently race to the Sender; whoever's request is accepted first gets the earlier position. There is no other ordering machinery — the queue is the order.

1. A network client commits PlaceSymbol with a RequestKey, times out, retries, and gets a receipt with position: 1041. Meanwhile a watcher saw the placement happen at position 1041 twenty seconds ago. Reconstruct exactly what happened, in order.

The first attempt arrived and ran the whole pipeline: dedupe found nothing, metadata was minted, check passed, apply mutated state, the entry was sealed at 1041, and publish rang the watcher's doorbell. Then the reply envelope came back to a caller that had already timed out and dropped it.

The retry carried the same RequestKey. The writer's dedupe index recognized it, ran nothing, appended nothing, and replied with the stored original receipt for 1041.

Nothing ran twice. The twenty-second gap is just the retry delay. The watcher and the committer disagree about when only because a receipt is the caller's copy of the confirmation, not the event itself.

2. Make the case against letting a trusted client supply its own at timestamp in metadata. “Trusted” is doing work in that sentence, so the argument can't be “clients lie.”

Even a perfectly honest clock is a different clock. Client timestamps break the single witness, and the damage is structural rather than moral.

Two entries adjacent in the total order could carry timestamps that run backwards: clock skew between machines, a timezone bug, a laptop waking from sleep. at would no longer be monotone with Position, so every consumer that sorts, windows, or bucketizes by time would silently disagree with history's real order.

The runtime's clock isn't more accurate. It is one clock, read at the one place where order is already decided. Witness beats claim even when the claimant is honest.

3. Task A and task B both commit; A's future resolves with position 500, B's with 501. Your teammate concludes “A's commit was called before B's.” True, or not necessarily?

Not necessarily. Arrival at the channel decides order, and two tasks racing the Sender can arrive in either order regardless of who called commit first — task scheduling, await points, and channel capacity all interleave between the call and the send.

The only sentence the receipts license: "A's request entered the channel before B's, therefore A applied first and observers saw A's change first."

Receipts order history, not call sites.

Stokker Technologies markDesigned and built by Stokker Technologies