Serialization

A running machine is not just “which state am I in” — it is the whole stack of active states from the root down to the current leaf, plus any data those states recorded while active. StateWalker can snapshot all of it to a plain object and rebuild it later, so a process can be paused, persisted to disk or a database, and resumed exactly where it left off.

Dump and restore

dump() walks the active-state stack root→leaf and returns a plain object. restore() takes that object and rebuilds the machine — recreating each state (which fires onStateCreate, so your handlers re-attach) and replaying whatever each state saved.

import { FsmProcess } from "@statewalker/fsm";

const process = new FsmProcess(config);
// … register handlers, dispatch some events …

const snapshot = await process.dump();
// snapshot is JSON-serializable: persist it anywhere
localStorage.setItem("machine", JSON.stringify(snapshot));

// Later, in a fresh process:
const revived = new FsmProcess(config);
// re-register the SAME onStateCreate handlers, then:
await revived.restore(JSON.parse(localStorage.getItem("machine")));
// `revived` now rests on the same leaf, ready to dispatch again

Restore into a fresh, not-yet-started process, and register the same onStateCreate handlers before restoring — restore fires them for each recreated state so the machine comes back fully wired.

The dump shape

type FsmProcessDump = {
  status: number;              // internal traversal status
  event?: string;              // the last event
  stack: { key: string; data: Record<string, unknown> }[];  // root → leaf
};

The stack is the chain of active states. Each entry has the state’s key and a data bag — empty unless that state’s dump hooks filled it.

Saving per-state data

By default a dump records only which states are active. If a state carries data that must survive — a retry counter, a form draft, a timestamp — record it with the state’s dump hook and read it back with restore:

process.onStateCreate((state) => {
  if (state.key !== "Retrying") return;

  let attempts = 0;
  state.onEnter(() => { attempts++; });

  // Contribute to this state's snapshot:
  state.dump((_state, d) => { d.attempts = attempts; });

  // Rehydrate from this state's snapshot:
  state.restore((_state, d) => { attempts = (d.attempts as number) ?? 0; });
});

The data object passed to dump is the same bag that appears in the corresponding stack entry, and the same object handed back to restore. Keep it JSON-serializable if you plan to persist across processes.

Both dump() and restore() accept extra ...args that are forwarded to every hook, so you can pass a serializer, a store, or a context object through to your per-state logic.

Through the runner

If you use startProcess, the same capability is on the returned handle — handle.dump() and handle.restore(dump) — so you can persist and resume without holding the underlying FsmProcess.