Skip to content

9. App-DB diff

You just clicked Refresh on the parallel-frames testbed's :above panel and the title HTTP flow rewrote a couple of slots — [:rf/machines :title/flow] walked :idle → :loading, and once the mock fetch resolves it walks again to :loaded and writes [:title :text]. The Event-detail panel's mini-diff is the right size for "one leaf changed" — what you want now is the whole delta, slice by slice, with the un-touched 90% of the tree out of the way. App-DB panel is what you escalate to.

app-db is a value. Two epochs are two values. The diff is the difference between them — and on the epoch where the title-flow machine landed :loaded, the diff is two paths and four values: the machine slot moved from :loading to :loaded, and the [:title :text] slot picked up the resolved title. Not a 200-line tree dump.

The panel is per-frame, scoped by the L1 frame picker. Switch from :above to :below and the same [:rf/machines :title/flow] slot reads from :below's app-db — a different ring buffer, a different cascade history, a different diff per epoch.

The panel renders the diff of the current epoch (the one selected in Event detail or the time-travel scrubber) — :db-before and :db-after, slice-aware.

App-DB diff for a cascade

Slice-centric, not tree dump

The panel does not render the full app-db tree on every epoch. That would be hostile to read — a 200-line tree where one leaf changed.

It renders only the slices that changed, plus any slices you've explicitly pinned. The diff is shown:

  • Inline JSON-ish (clojure pretty-printed with diff markers).
  • Side-by-side :db-before / :db-after toggle.
  • Just the delta — paths and value-pairs only.

You pick the rendering mode that fits the diff size. The first cascade after app boot is a big diff (everything went from {} to seeded); subsequent cascades are usually a single nested update.

App-DB panel — the three rendering modes side-by-side

Pinning slices

Click the pin icon next to a slice header (or use the watch path affordance) and that slice will render on every epoch — even when it didn't change. Useful for "I want to keep an eye on :auth/state while I work through a checkout flow."

Pinned slices show as unchanged rows in epochs where they didn't move. The signal is "still the same value," which is itself diagnostically useful when you expected it to change.

The underlying contract

The panel reads the epoch record's :db-before and :db-after slots, and computes the diff in-panel. The runtime doesn't pre-compute diffs — it just stores the two value references. The diff fold is cheap (Clojure values are persistent; structural equality is fast) and tool-local. Production builds DCE the whole pathway.

Read-only

The panel never writes app-db. There's no inline editor. No "save changes" button.

Two reasons:

  1. Causa is an observation tool. Writes belong to dispatch (or, for bypass cases, reset-frame-db! from a pair session). Mixing observation and mutation makes Causa hard to trust during an investigation.
  2. A typed app-db edit would race the runtime. While you're typing, dispatches keep firing. Saving a half-typed edit would clobber whatever the runtime had just written. The pair tool's pathway handles this with explicit lock-the-drain semantics; baking it into the panel would force every panel session to think about drain locks.

If you want to inject an app-db value — bug repro shipping, an experimental shape — call (rf/reset-frame-db! :frame-id new-db) from a pair session or the dev console. The runtime records a synthetic epoch and Causa renders the synthetic epoch in the scrubber so the injection is visible.

Wedge cases

Three patterns the diff handles distinctively:

  • Large blobs. Slots tagged :large? (see Guide 23b — Large blobs) render as :rf/elided placeholders in the diff. The diff still flags "this slot changed"; it doesn't expand the value. The show large toggle is per-session — useful when you actually need to look.
  • Sensitive values. Slots whose schema declares :sensitive? true render as :rf/redacted by default, with a N redacted count at the bottom of the panel. Toggle on to inspect — same opt-in as the trace panel.
  • Bigint and date types. Values that don't pretty-print well in the default Clojure printer get a type chip — [bigint], [date], [uuid] — with the underlying value reachable through expand.

When you'd open it

  • "This cascade rewrote half the tree, and I want to see exactly which half." — the framing case from the opener; the App-DB panel is what Event detail's mini-diff escalates into.
  • "I think a sub is stale, but app-db says the value's right." — pin both the slot and the sub's recompute marker; watch them tick together.
  • "I want a session-long watch on :auth/state." — pin it; the panel paints it on every epoch.