timetravel
Development-only time travel and state inspection for Lustre applications. It records application messages and model snapshots, lets you navigate through the history, and suppresses effects while you inspect an earlier state.
Pre-1.0 status
This package grew out of a debugger built for a small Lustre application. Its core history behavior is covered by unit tests, and independent example applications verify both minified and unminified browser builds.
The API may still change before 1.0 as it sees broader use. Bug reports, corrections, and suggestions are welcome.
Demo
Try the debugger in the Maybe List demo. Change the list, then open Time Travel in the lower-right corner to move through the recorded states. The public showcase is intentionally unminified so the automatic value inspector can retain Gleam constructor names.
Installation
Add the package to a Lustre project as a development dependency:
gleam add --dev timetravel
Usage
Keep your production entry point unchanged and add a separate development entry point:
import lustre
import my_app/web
import timetravel
pub fn main() -> Nil {
let app = timetravel.application(web.init, web.update, web.view)
let assert Ok(_) = lustre.start(app, "#app", Nil)
Nil
}
Start that entry point with Lustre Dev Tools:
gleam run -m lustre/dev start my_app_dev
The inspector is self-contained and injects its own prefixed CSS. It does not require Tailwind or any stylesheet configuration in the host application.
The latest 100 transitions are retained. Selecting an earlier transition restores its recorded model without replaying messages, HTTP requests, or other effects. New application messages are ignored until you return to the present.
Minified deployments
JavaScript minification renames Gleam’s generated constructor classes. If the
development entry point is deployed with minification enabled, use
application_with_formatters so timeline labels and inspected values remain
meaningful:
let formatters = timetravel.Formatters(
message_name: fn(message) {
case message {
Increment -> "Increment"
SetCount(_) -> "SetCount"
}
},
format_message: fn(message) {
case message {
Increment -> "Increment"
SetCount(count) -> "SetCount(" <> int.to_string(count) <> ")"
}
},
format_model: fn(model) { int.to_string(model.count) },
)
let app =
timetravel.application_with_formatters(
web.init,
web.update,
web.view,
formatters,
)
The ordinary application function remains convenient for unminified local
development builds.
Acknowledgements
This experiment was informed by and builds on ideas from Tardis, an earlier time-travelling debugger for Lustre by ghivert. Many thanks for showing what this kind of tooling can look like in the Gleam and Lustre ecosystem.
Development
gleam format --check src test
gleam test
./integration/run.sh
The integration command builds three small, independent Lustre applications against the local package. Together they exercise the zero-configuration API, managed effects, custom minification-safe formatters, and both unminified and minified browser bundles. Generated dependencies and build output remain inside the integration projects and are ignored by Git. The integration projects use the system installation of Bun.
License
MIT