Persistence
The persistence plugin writes a serializable state slice whenever transitions settle, context
changes, or status changes. saveOn narrows which of those write, and debounceMs collapses a
burst of them into a single write.
Install and use
Section titled “Install and use”import { createPersistencePlugin } from "@rxova/journey-core/plugins";
const machine = createLinearJourney(definition, { plugins: [ createPersistencePlugin({ storage: localStorage, key: "checkout", clearOnTerminate: true }) ]});storage must implement getItem, setItem, and removeItem. setItem may return a promise.
Debounced writes
Section titled “Debounced writes”createPersistencePlugin({ storage: localStorage, key: "checkout-draft", debounceMs: 300, saveOn: ["context", "transition"]});Omit debounceMs (or pass 0) and each observation writes immediately. Pass a window and the
plugin waits that long after the last observation, so typing into a field produces one write rather
than one per keystroke. flushPersisted() cancels the wait and writes now.
This used to be a second plugin called autosave. It was this plugin with a timer — the same serializer, the same adapter contract, the same key — so it is a parameter now.
An immediate write that throws propagates, exactly as it always did, and the runtime’s listener
isolation reports it. A debounced write happens on a timer with no caller left to throw to, so its
failures land in getPersistenceState().error and are reported through onListenerError.
Disposing the machine cancels a pending debounce. It does not flush automatically.
The persist creation option
Section titled “The persist creation option”For the common case, every factory accepts persist as sugar over the plugin:
const machine = createLinearJourney(definition, { persist: { key: "checkout" }});persist expands into the persistence plugin, prepended to plugins. storage is optional here
and defaults to globalThis.localStorage; creation throws when neither a storage value nor
localStorage is available. Combining persist with an explicitly registered persistence plugin
fails at creation as a duplicate plugin name. Use the explicit plugin form when you need
clearOnTerminate or an injected clock.
Unlike the explicit plugin, persist also restores: a valid record found at creation
seeds the machine so the first start() resumes where the record left off.
Persisted shape
Section titled “Persisted shape”type JourneyPersistedState = { status: JourneyStatus; context: unknown; timeline: readonly string[]; currentIndex: number; savedAt: number;};The plugin serializes this value with JSON.stringify. Keep persisted context serializable.
API and snapshot
Section titled “API and snapshot”const api = machine.plugins.persistence;
api.inspectPersistedState(); // last value written by this machineapi.readPersisted(); // re-read and parse storageapi.clearPersisted(); // cancel any pending write and remove the entryawait api.flushPersisted(); // cancel the debounce and write now
machine.getSnapshot().plugins.persistence;// {// status: "idle" | "pending" | "saving" | "saved" | "error",// lastSavedAt: number | null,// error: unknown | null// }Malformed or structurally invalid storage values return null. status only passes through
"pending" when debounceMs is set — an immediate write has no window in which to be pending. Use
the namespaced snapshot value in selectors when the UI displays save status.
Restore behavior
Section titled “Restore behavior”The creation-time persist option restores. At creation, the factory reads the stored record; when
it is restorable, the record seeds context, timeline, and pointer, and the first start() re-enters
the persisted current step instead of the first/initial one:
const machine = createLinearJourney(definition, { persist: { key: "checkout" }});// creation starts the machine, resuming at the persisted step when a valid record existedA record is restorable when its status is running or paused, its currentIndex points inside
its timeline, and every timeline step is declared by the current definition. Anything else —
terminal-status records, definition drift, malformed or foreign payloads, a throwing storage read —
is ignored and the journey starts fresh. Restore is best-effort by design and never throws.
Details of a restored start:
- the initial entry runs as a normal
stepEnterwithfrom: nullanddirection: "jump"; - visit counts are reconstructed from the restored timeline, so the re-entered step reports
isFirstTimeVisit: false; - an explicit
startAtoption wins over the persisted record; restart()always begins a fresh run — the seed applies only to the firststart().
Registering createPersistencePlugin explicitly in plugins stays save-only: plugins are
observe-only and cannot seed the runtime. Use the persist option when you want restore.
Options
Section titled “Options”| Option | Meaning |
|---|---|
storage | Required localStorage-compatible adapter. |
key | Required storage key. |
debounceMs | Wait this long before writing; omitted or 0 writes immediately. |
saveOn | Any of context, transition, status; defaults to all three. |
clearOnTerminate | Remove the entry on termination; defaults to false. |
now | Injectable clock, mainly for tests. |