Skip to content
Prose v0.3.3

Durability Store

For the conceptual overview and patterns, see the durability guide.

Pluggable persistence layer for flow checkpoints. Adapters may live in separate packages.

interface DurabilityStore {
load(runId: string): Promise<FlowCheckpoint | null>;
save(checkpoint: FlowCheckpoint): Promise<void>;
delete(runId: string): Promise<void>;
}
MethodSemantics
load(runId)Return the checkpoint for runId, or null if none exists. Must not throw on unknown ids.
save(checkpoint)Persist the checkpoint, overwriting any existing entry for the same runId. Must be atomic — partial writes defeat checkpointing.
delete(runId)Remove the checkpoint. No-op for an unknown runId — must not throw.

Implementations are responsible for serialization. JSON is the typical choice; whatever you pick must round-trip Date objects (createdAt, updatedAt) and support undefined for breakValue slots that are absent.

The shape persisted to the store. Adapters store and return this verbatim.

interface FlowCheckpoint {
flowName: string;
runId: string;
input: unknown;
state: unknown;
completedSteps: string[];
status: 'running' | 'completed' | 'failed';
breakValue?: unknown;
failedStep?: { name: string; error: string };
createdAt: Date;
updatedAt: Date;
}
FieldDescription
flowNameThe flow’s name (from createFlow('name')). Useful for diagnostics; not used to route resume.
runIdThe DurabilityOptions.runId of this run.
inputThe argument the flow was first started with. Used verbatim on resume — the caller’s second input argument is ignored.
stateAccumulated state at the moment of the last successful step.
completedStepsNames of finished or condition-skipped steps, in execution order.
status'running' (in progress or crashed), 'completed' (saved result is final), 'failed' (errored after exhausting retries).
breakValueOnly present when the flow short-circuited via breakIf. The field’s presence distinguishes “broke” from “completed normally” — the value itself may be undefined.
failedStepOnly present when status === 'failed'. The step that errored, plus its error message.
createdAtWhen the run first started. Preserved across resumes.
updatedAtWhen the checkpoint was last written.

The shape passed via FlowExecutionOptions.durability.

interface DurabilityOptions {
store: DurabilityStore;
runId: string;
}
FieldDescription
storeThe store to read and write checkpoints through.
runIdStable identifier for this run. Same runId across execute() calls → resume or replay. New runId → fresh run. Typically derived from a business identifier.

A reference DurabilityStore that holds checkpoints in a Map.

import { MemoryDurabilityStore } from '@celom/prose';
const store = new MemoryDurabilityStore();

Test helpers (not part of the DurabilityStore interface):

MethodPurpose
size()Number of stored runs.
snapshot(runId)Synchronous read of a single checkpoint (returns a clone).
clear()Drop all stored checkpoints.

MemoryDurabilityStore clones on both load() and save() so the executor cannot mutate stored state. Adapters that serialize to bytes get this property for free.

Conformance tests for adapter authors live at packages/prose/src/lib/__tests__/store-conformance.ts. The file is not re-exported from the package entry point and is not included in the published dist/. Copy or vendor it into your adapter package — or, for adapters in this monorepo, import it directly from source.

// Once copied or vendored, run the suite in your adapter's spec file:
import { storeConformanceSuite } from './store-conformance.js';
import { MyStore } from './my-store.js';
storeConformanceSuite('MyStore', () => new MyStore());

The suite covers the public DurabilityStore contract — round-tripping checkpoints, overwriting on second save(), preserving the failedStep / breakValue fields, no-op delete() for unknown ids, and isolation between runIds. Adapter-specific invariants (clone-on-read for in-memory adapters, transaction semantics for SQL adapters) belong in your adapter’s own spec.

Implementation notes:

  • Atomic writes. A reader observing a partial checkpoint defeats the entire point of durability. Wrap the write in a transaction, or write to a temp row and swap.
  • load() returns null, not throws. Unknown runIds are normal — they mean “fresh run.”
  • delete() of an unknown id is a no-op. No error.
  • Clone on read for non-serializing stores. If your store keeps the checkpoint in memory (or shares an object reference with the caller), clone on load() and save() to prevent accidental mutation.