This project is a Todo application built in Elm with a custom, app-agnostic Time Travel Debugger.
It demonstrates a powerful architectural idea:
An app can remain pure and simple, while additional behavior (like time travel) is layered on top as reusable wrappers.
The app is split into two main parts:
This contains all of the domain logic:
Model— the state of the appMsg— all possible user actionsupdate : Msg -> Model -> Model— how state changesview : Model -> Html Msg— how the UI is rendered
👉 This part has no knowledge of time travel and represents a standard Elm-style application using a pure update function.
This module wraps an application and adds:
- History tracking
- Prev / Next navigation
- Debug UI
- Export / import timeline replay
- Runtime debugger visibility toggle
It introduces:
TimeTravel msg model— a wrapper around your model that stores timeline and debugger stateMsg msg— an internal message type that wraps your app messages (AppMsg msg) and adds debugger controls likePrev,Next, and import/export actions
Each time the app updates:
- The current model, message, and resulting model are saved as a "frame"
- The new model becomes the "present"
- Previous transitions are stored in
past - Future transitions are stored in
future
So the state becomes:
type alias Timeline msg model =
{ past : List (Frame msg model)
, present : model
, future : List (Frame msg model)
}
type alias Frame msg model =
{ msg : msg
, prev : model
, next : model
}
This allows you to:
- Go backward (Prev)
- Go forward (Next)
- Inspect every state transition
The TimeTravel module does not replace the application.
Instead, it wraps it.
App
↓
TimeTravel.withTimeTravel
↓
Enhanced App
This is similar to the Decorator pattern.
The magic happens here:
withTimeTravel :
AppConfig msg model
-> Program Flags (TimeTravel msg model) (Msg msg)Developers provide the application configuration:
{ init = initModel
, update = update
, view = view
, msgToDebug = msgToDebugInfo
, modelToString = modelToPrettyString
, decodeMsg = decodeMsg
}It returns a fully working Elm program with time travel enabled.
The debugger visibility is controlled via flags passed from index.html at initialization time, allowing it to be enabled or disabled without changing Elm code.
<div id="todo-app"></div>
<script src="main.js"></script>
<script>
Elm.Main.init({
node: document.getElementById("todo-app"),
flags: { visibleByDefault: true }
})
</script>The debugger is initially controlled via flags passed from index.html, but can also be toggled at runtime using a checkbox in the debugger UI.
Main.elm only defines:
- state
- behavior
- UI
No debugging concerns leak into it.
This module can be used with any Elm app by providing:
- update
- view
- model serializer (for readable debugging output)
This pattern allows developers to add more "wrappers":
- withLogger
- withPersistence
- withAnalytics
Each one enhances the app without changing it.
The wrapped application must use a pure update function:
update : Msg -> Model -> Model
Side effects (Cmd) are not currently supported by the TimeTravel module.
This keeps the debugger simple and predictable, but means features like focus management or HTTP requests would require extending the wrapper.
- Checkbox → Toggle complete
- Click task text → Start editing a task
- Enter → Save changes
- Escape → Cancel editing
- Save / Cancel buttons provide explicit control
- Shift + Click → Toggle important
- Step through history (Prev / Next)
- See each message (Msg) that caused a transition
- Inspect model changes with inline diffing and grouped previous-state lines
- View previous and next states
- Export timeline as JSON
- Import timeline and replay state transitions
- Press Enter inside the import textarea to trigger import
- Show inline import status feedback
- Toggle debugger visibility from the UI
This project demonstrates a key idea:
Architecture can be composed just like functions.
An application is not tightly coupled to its runtime behavior.
Instead, behavior is layered on top in a clean, functional way.
- Highlight changed fields more precisely
- Persist history or app state to localStorage
- Explore additional reusable wrappers around the core app
This approach keeps Elm code:
- Simple
- Predictable
- Extensible
And opens the door to building reusable architectural tools.
This project now supports exporting and importing timelines, turning the debugger into a deterministic replay engine.
Clicking "Export Timeline" generates JSON like:
[
{ "index": 0, "type": "ToggledStatus", "payload": { "id": 2 } },
{ "index": 1, "type": "SetFilter", "payload": { "filter": "All" } }
]This format is:
- Human-readable
- Easy to copy/paste
- Uses a
typefield to identify the message (ElmMsgconstructor) - Stores all message data in a structured
payload - Sufficient to reconstruct application behavior deterministically
You can paste JSON back into the app and click "Import Timeline", or press Enter while focused in the import textarea.
The system will:
- Decode JSON
- Reconstruct real
Msgvalues fromtype+payload - Replay them from the initial model
Instead of storing snapshots, the app rebuilds state using the update function:
List.foldl
(\msg (prevModel, frames) ->
let
nextModel =
update msg prevModel
in
( nextModel
, { msg = msg, prev = prevModel, next = nextModel } :: frames
)
)
( initModel, [] )
messagesThis ensures:
- Accurate reproduction of behavior
- No hidden state
- Deterministic debugging
Each event is decoded using its type and optional payload.
- Stateless messages (like
NoOp) can decode without a payload - Stateful messages decode their data from structured JSON
Replay remains correct as long as all state-changing messages are decoded successfully.
This turns the debugger into a powerful tool:
- Reproduce bugs from real sessions
- Share timelines between developers
- Verify behavior deterministically
This is conceptually similar to Redux DevTools, but implemented in pure Elm.