Model the State
Before any harmos API, one decision is yours alone: what your application's state is. This chapter draws that line — what belongs in the one value replay rebuilds, what looks like state and is not, and where the rest goes instead. You leave with a struct you could have written without this library, and four rules that keep replay honest.
What Harmos Does With Your State
Section titled “What Harmos Does With Your State”Harmos is a runtime for applications whose state matters. Your application's entire working state lives in memory as one plain Rust value, and it changes in exactly one way: by committing transactions, which are recorded — in order, forever — in a journal. Because every change is recorded, harmos can rebuild your state at any time by replaying that record. Undo, crash recovery, audit history, and live observation all fall out of this one arrangement. But it starts with a decision that is entirely yours: what is your state?
The Example: A Minischematic Editor
Section titled “The Example: A Minischematic Editor”Throughout this guide we build a small schematic editor: electronic symbols
(a resistor, an op-amp) placed on a canvas, their pins connected by nets
(named electrical connections like VCC). Here is its state:
pub struct Schematic { pub symbols: BTreeMap<SymbolId, Symbol>, pub nets: BTreeMap<NetId, Net>,}
pub struct Symbol { pub reference: String, // "R1", "U3" pub position: Point, pub rotation: Rotation,}
pub struct Net { pub name: NetName, // "VCC", "GND" pub pins: BTreeSet<PinRef>, // which symbol pins this net connects}That's it. No database schema, no ORM annotations, no base class to inherit. If you can write the struct, you have modeled the state.
One field is worth a footnote you can ignore for now. A net's name is a small
type of its own rather than a String, because the editor learns about buses
later and "DATA[3]" becomes bus DATA, member 3. That is
chapter 10's subject; until then, read NetName as "the name,
in whatever shape the current version of the editor understands names."
The Four Rules of Journal-Friendly State
Section titled “The Four Rules of Journal-Friendly State”Each rule exists because of what harmos must be able to do with your state. Understand the reason and you'll never need to memorize the rule.
1. Plain data only — no live resources. No file handles, sockets, thread pools, or GPU buffers inside state. Reason: harmos rebuilds state by replaying recorded changes, and no replay can conjure an open socket back into existence. State must also serialize, for snapshots (chapter 8), and a file handle has no meaningful bytes. If your app talks to a device or a file, the handle lives in your own application code beside the runtime, and state holds only the plain facts about it.
2. Deterministic behavior — prefer ordered containers. The replay
guarantee is: same changes, in the same order, produce the same state — every
time, on every machine. BTreeMap and BTreeSet iterate in a fixed order;
HashMap does not. A HashMap is not automatically wrong — but the moment any
change-applying logic depends on iteration order ("pick the first free
reference"), replay diverges silently, and a diverged replay is corrupted
recovery. Ordered containers make determinism structural instead of
something you must remember under deadline. They also make snapshots
byte-stable, which a future tamper-evidence feature will thank you for.
3. Ids, not references. Net.pins holds PinRef values — ids — not
pointers or Rcs into symbols. Reason: transactions must name the things
they change ("move symbol R1"), and names must survive serialization; a
pointer names a memory address that means nothing after restart. Cross-entity
relationships in state are always id-to-id, resolved through lookups.
4. Truth only — derived data stays out. "Total component count" and "net connectivity graph, indexed for hit-testing" are computable from state. Storing them in state means maintaining them in every change, forever, on pain of subtle divergence. Keep state minimal; derived values live in projections that recompute from changes (chapter 5). One honest exception: a cache may live in state if it is updated exclusively by the same recorded changes — but make that a deliberate, documented choice, not an accident.
What Deliberately Stays Out
Section titled “What Deliberately Stays Out”Three things that look like state but aren't, and where they go instead:
- Per-user view state — the current selection, hover highlight, scroll position. This is what one user is looking at, not what the schematic is. It stays in your UI layer. (An application may choose to promote selection into state — say, to make it collaborative or undoable — but that's a decision, not a default.)
- High-rate data — a live simulation waveform at 10 kHz. The journal records meaningful changes; it is not a firehose. Dense data flows through Streams, the separate facility chapter 6 runs from a live window into a recorded artifact.
- Anything you can recompute — rule 4.
Common Mistakes
Section titled “Common Mistakes”Almost every mis-modeled state struct comes from the same reflex: it is data, so it goes in state. Selection, viewport, a map of per-user settings — each one is data, and none of them belongs. State is not where data goes. State is what the document is.
The second reflex is subtler, and it is the one that catches careful people: it has to survive a restart, so it has to go in the journal. It doesn't. The journal is where truth lives, not where durability lives. Durable data has three homes, and only one of them is the journal:
- The journal — durable shared truth. It prints on the drawing, it ships to the fab, it belongs in the document's history.
- Derived — computed on demand, stored nowhere. Losing it costs a recomputation, not a fact.
- An ordinary file your application writes — durable but personal. Viewport, preferences, one user's private highlight colors.
KiCad separates exactly these: .kicad_prl holds project-local view state
beside the .kicad_sch truth, and stays out of version control. Reach for the
third home whenever the answer to "would the document on disk be different?"
is no.
One more thing to be suspicious of: reaching for a transaction because you want atomicity. Atomicity is a benefit transactions provide, not a reason to use them. Don't buy insurance for a risk you don't have.
Under the Hood: Where Your State Actually Lives
Section titled “Under the Hood: Where Your State Actually Lives”When the runtime starts (chapter 3), your
Schematic moves into a structure called TrackedState — the journal's single
shared reality:
struct TrackedState<A: Application> { state: RwLock<At<A::State>>, // your Schematic, paired with a Position // ... recent history and progress markers, introduced in later chapters}
pub struct At<T> { pub value: T, // the Schematic itself pub position: Position, // how many changes deep in history this value is}Two things to notice now. First, your state is stored paired with its
position in history — so every read can tell you not just what the state is
but when it is. Second, notably absent: harmos never requires Clone on
your state. Changes are applied in place by a single writer; nothing ever
copies your multi-megabyte schematic to modify it. Big state is a first-class
citizen.
Test Your Knowledge
Section titled “Test Your Knowledge”
1. A colleague adds open_file: std::fs::File to Schematic so saving is faster. Explain why this breaks harmos — there are two independent reasons.
open_file: std::fs::File to Schematic so saving is faster. Explain why this breaks harmos — there are two independent reasons.Two separate doors close on it. First, replay rebuilds state purely from
recorded facts, and no fact can reopen a file descriptor. Second, state
must serialize for snapshots, and a File has no meaningful bytes —
writing fd 7 into a snapshot restores a number that points at nothing.
Replay can't recreate a resource; a snapshot can't capture one. Either one alone is fatal.
Note what is not the reason: non-determinism. That fd numbers differ from run to run is a symptom, not the cause.
2. The editor needs “currently selected symbols” for copy and paste. In or out of Schematic? Defend the answer — and name the circumstance that flips it.
Schematic? Defend the answer — and name the circumstance that flips it.Out, by default. The test is one question: does the schematic on disk change when the selection changes? No. So selection is not part of what the document is, and it stays in the UI layer.
"But then we'd have transactions to select things" is circular — the
placement forces the transactions, not the reverse. Promotion costs three
concrete things: journal noise (every click becomes a permanent entry),
undo pollution (Ctrl+Z starts undoing glances instead of edits), and a
wrong multi-user shape (selected: BTreeSet<_> gives everyone one shared
selection; you would need a per-principal map plus cleanup for the ghost
selections of departed users).
The flip: selection must be shared or replayed as an application feature. Even then, live presence belongs on Streams. Only a selection that must survive restart and appear in history earns a field in state.
3. Rule 2 said HashMap is “not automatically wrong.” Write the one-sentence test that decides whether a HashMap in state is safe.
HashMap is “not automatically wrong.” Write the one-sentence test that decides whether a HashMap in state is safe.A HashMap in state is safe if no check, apply, or snapshot encoding
ever iterates it where order matters.
Contents are always deterministic — the same keys and values are present either way. Traversal order is the only danger.
4. Product asks you to restore the viewport when a file is reopened. In Schematic or out — and does “must survive restart” flip the answer?
Schematic or out — and does “must survive restart” flip the answer?Out, and no, it does not flip it. "Must survive restart" is a durability
requirement, and durability has three homes; only one of them is the
journal. The viewport is durable-but-personal: it belongs in an ordinary
file your application writes beside the document, the way KiCad keeps
.kicad_prl beside .kicad_sch.
Putting it in state would make every pan and zoom a permanent journal entry: undoable, shared with collaborators, and printed in the audit trail of a document it does not change.
5. Sort into the three homes — journal truth, derived, or personal sidecar: (a) each user's net-highlight color, (b) the title-block text on sheet 1, (c) the current list of ERC violations.
(a) Personal sidecar. It changes nothing about the document; two users can disagree about it forever without either being wrong.
(b) Journal truth. It prints on the drawing.
(c) Derived. Delete the list, re-run the electrical rule check, and it reappears identically.
One nuance on (c): the day application wants ERC witnessed — "this file
passed ERC before release" — that historical claim becomes a record
(chapter 2), an ErcPassed fact the editor
does not have today, while the current list stays derived. Same data,
different question.