Persist and restore¶
A machine's snapshot is part of the frame's state, so persisting a machine means saving that state and installing it again at boot. There is no machine-specific serializer.
(ns app.persist
(:require [cljs.reader]
[re-frame.core :as rf]))
;; Save: any time. The app decides what to keep.
(defn save! [frame]
(let [{app :rf.db/app runtime :rf.db/runtime} (rf/frame-state-value frame)]
(.setItem js/localStorage "app/saved-state"
(pr-str {:rf.db/app app
:rf.db/runtime (select-keys runtime [:rf.runtime/machines])}))))
;; Load: at boot, before the app's own boot events.
(defn load! [frame]
(when-let [stored (.getItem js/localStorage "app/saved-state")]
(when-let [saved (try (cljs.reader/read-string stored)
(catch :default _ nil))]
(rf/dispatch-sync [:rf/install-frame-state saved] {:frame frame}))))
frame-state-value returns the frame's app-db under :rf.db/app and its
runtime-db under :rf.db/runtime. The runtime-db subtree
:rf.runtime/machines holds every machine in the frame: singletons, spawned
children and the spawn registry.
:rf/install-frame-state replaces app-db with :rf.db/app and replaces only
the runtime-db subtrees you saved, so the route slice and anything else the
frame booted with stay as they are. Entry actions are not re-run, and spawned
children come back with their state. Each restored machine's live :after
timer is armed again for its full delay, and its :sensitive / :large
declarations are re-derived from its machine definition, so you save no
classification alongside the snapshots and a restored secret redacts exactly
as the live one did.
Only values that survive pr-str and read-string persist, which is why a
machine's :data holds no functions, atoms or host objects
(The snapshot).
Resume work after restoring¶
Load the machines artefact and register the current machine definitions before installing the saved state. A restored actor's snapshot makes it addressable, but a previously open socket or in-flight HTTP request is not recreated: restoring does not replay the entry effect that opened it.
Give resumable states an explicit event that re-enters them. The entry action
must reconstruct its input from restored :data, because the resume event
does not carry the original request:
:working
{:entry :start-work
:on {:work/resume {:target :working :reenter? true}
:work/done :complete
:work/failed :failed}}
After installing, dispatch [machine-id [:work/resume]] with
{:frame frame} when resuming that request is appropriate. Re-entry also
restarts state-bound children. For work that should not resume, migrate the
saved machine to an idle state before installing instead. Only :after
timers are automatically re-armed, each with its full delay.
What the app owns¶
- Storage and selection. Where the string goes and which subtrees it keeps
are yours.
frame-state-valuereturns actual values; trace redaction does not sanitize what thissave!writes. Select or remove fields as needed. Leave the resource runtime (:rf.runtime/resources,:rf.runtime/work-ledger,:rf.runtime/mutations) out: the install refuses it, because a resource cache is not persisted. Resources refetch. - Versioning and migration. A saved snapshot whose
:statea later deploy removed restarts from:initialon its first event, with:rf.error/machine-state-not-in-definition; a changed:meta :rf/snapshot-versiondoes the same, with:rf.error/machine-snapshot-version-mismatch. Migrating old data forward is the app's job. - Reading it back.
read-stringon stored text is your code, so guard it, asload!does.
The full contract is in the
:rf/install-frame-state reference.
Troubleshooting¶
| Symptom | Cause | Fix |
|---|---|---|
| Snapshot restored but the request or socket never starts | Restore does not replay entry effects | Send an explicit resume event that re-enters the state, or restore to idle |
Install changes nothing and reports :rf.error/handler-exception |
The payload is not a map, a partition is not a map, or it carries a resource runtime subtree | Save maps from frame-state-value, and select only :rf.runtime/machines from runtime-db |
A restored machine restarts from :initial, with :rf.error/machine-state-not-in-definition |
The saved :state names a state the deployed definition no longer has |
Migrate old snapshots before installing, or accept the restart |
A restored machine restarts from :initial, with :rf.error/machine-snapshot-version-mismatch |
The definition's :meta :rf/snapshot-version changed since the save |
Migrate old snapshots before installing, or accept the restart |