Skip to content

Concurrency and idempotency

Every command accepts an idempotencyKey. A repeat with the same key returns the first result without doing anything again (EC-16):

// A client that retries a timed-out POST sends the same key; the request is created once.
const input = {
flow: "expense",
requesterId: "u_req",
subject: { type: "expense", id: "EXP-1" },
payload: { amount: 50 },
idempotencyKey: "form-7f3a",
};
const first = await rungs.submit(input);
const retry = await rungs.submit(input);
console.log(retry.id === first.id); // true
// The same key for a different command is refused, so a bug cannot hide behind it.
const misuse = await rungs
.submit({ ...input, payload: { amount: 60 } })
.catch((error: unknown) => error instanceof InvalidCommandError);

Runs in CI:examples/docs-snippets/src/guides/concurrency.ts

Generate the key when you render the form or start the client request, not when you handle it.

Every request has a version that each change increments. Send the version the user saw as expectedVersion; if the request has moved on, the command is refused with ConflictError (EC-15):

// Both approvers opened the request at the same version. The first decision wins;
// the second gets ConflictError instead of deciding something that already changed.
const seen = first.version;
await rungs.approve({ requestId: first.id, actorId: "u_a", expectedVersion: seen });
try {
await rungs.reject({ requestId: first.id, actorId: "u_b", expectedVersion: seen });
} catch (error) {
if (error instanceof ConflictError) {
console.log("Someone decided first; showing the latest state.");
}
}

Runs in CI:examples/docs-snippets/src/guides/concurrency.ts

Rungs never retries a refused command for you: the person may no longer be the one who should decide. Show them the latest state and let them act again.