Skip to content
C.W.K.
Lesson 01 of 05 · published

Receipt Before Effect

~13 min · write-ahead, journal, atomic-write, durability

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"Persist a prepared receipt before each change. Mark applied only after the OS reports hidden." — Tinker architecture, hide invariant 3

The Order That Makes Crashes Honest

There are only two possible orders for "change the screen" and "write it down". Effect first, then write: if Tinker dies between them, an app is hidden and the disk has no record of it — after relaunch nobody owns it and nothing will ever restore it. Write first, then effect: if Tinker dies between them, the disk says "about to hide Eagle" and Eagle may or may not be hidden. The second failure is recoverable — there is a record, and it is honest about what it does not know. The first is not. So Tinker always writes first. Databases call the same idea write-ahead logging; Tinker's version is small enough to read in one sitting.

What the Journal Holds

The journal is one JSON document, journal.json, in Tinker's own data directory outside the source checkout. It has a schema version, an active set naming every owner that is currently applied (such as preset/edit), and a list of ChangeReceipts. Each receipt records its key, the capability that made it, the payload undo will need, its set of owners, its phase, and an optional error message. Nothing else — no timestamps of Watchfire readings, no UI state. A journal that only describes ownership is a journal that can be checked for consistency on every load.

Every Mutation Is a Whole-Document Write

The engine never edits the journal in place. It copies the current journal, changes the copy, and hands the whole copy to persist, which writes it through CwkKit's LocalDocumentStore. That store writes atomically — a temporary file renamed over the old one — so a crash mid-write leaves either the old document or the new one, never half of each. Only after the write succeeds does the engine adopt the copy as its in-memory journal. Memory never runs ahead of disk.

A Failed Write Disables the Engine

If a write fails — a full disk, a permissions change — persist records the failure as the engine's fault and rethrows. From then on every apply is refused with that fault as the reason. An actuator that cannot record what it is about to do must not do it. Tinker's tests include a persistence-failure case that checks exactly this: the effect never happens when the receipt could not be saved.

Code

The journal document and one receipt·swift
import CwkKitCore

public struct ChangeReceipt: Codable, Equatable, Identifiable, Sendable {
  public var id: String { key }
  public var key: String
  public var capability: String
  public var payload: JSONValue
  public var owners: Set<String>
  public var phase: Phase
  public var error: String?
  public enum Phase: String, Codable, Sendable { case prepared, applied }
}

public struct Journal: Codable, Equatable, Sendable {
  public var schemaVersion = 1
  public var active: Set<String> = []       // owners currently applied
  public var changes: [ChangeReceipt] = []
}
Copy, change, write whole, then adopt — memory never ahead of disk·swift
private func persist(_ value: Journal) throws {
  do { try persistence.write(value); journal = value }
  catch { fault = error.localizedDescription; throw error }
}

// In the apply loop, for one change:
var next = journal
next.changes.append(ChangeReceipt(key: change.key, capability: capability.id,
  payload: change.payload, owners: [owner], phase: .prepared))
try persist(next)                         // 1. receipt on disk
try await capability.apply(change.payload) // 2. effect
next = journal
let i = next.changes.firstIndex { $0.key == change.key }!
next.changes[i].phase = .applied
try persist(next)                         // 3. confirmation on disk
What journal.json looks like with the editing preset applied·json
{
  "schemaVersion": 1,
  "active": ["preset/edit"],
  "changes": [
    {
      "key": "application:com.cwk.voyager:4127:1762905600.25",
      "capability": "applications.hide",
      "payload": {"bundleID": "com.cwk.voyager", "pid": 4127,
                  "launchedAt": 784598400.25, "name": "Voyager"},
      "owners": ["preset/edit"],
      "phase": "applied"
    }
  ]
}

External links

Exercise

Draw the timeline of one hide as five points: before persist(prepared), after it, after apply, after persist(applied), after the next receipt. For a crash at each point, write what the journal says on relaunch and what Tinker will show Dad.
Hint
Crash before the first write: nothing on disk, nothing changed — clean. After the prepared write but before or during apply: a prepared receipt — uncertain, engine starts stopped. After apply but before the applied write: still prepared — uncertain, even though the hide probably happened. After the applied write: an owned change — shown under 'Changes Tinker will restore'. The third case is the price of honesty: Tinker cannot tell it apart from the second.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.