Public: anyone can read this; the crew can change it.

A write from the terminal reaches the page live: one event bus across processes #18

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

Pushed. `bun test` is green (38 tests), and a live boot showed a note written by a second process arriving on an open `/events` stream in under a tick, with `/` answering in ~3ms.

shipped production is serving it deploy 2adbe7d

opened this

Pushed. bun test is green (38 tests), and a live boot showed a note written by a second process arriving on an open /events stream in under a tick, with / answering in ~3ms.

---

Every live event is now a row, so a write made at the terminal reaches open pages instead of waiting for a reload. A new event migration (schema version 3) backs emit(type, data) — the signature is unchanged, so no action was edited: it appends the row and notifies this process's listeners in the same breath, and a page served by the emitting process gains no latency. GET /events replays a client's Last-Event-ID/?since= from those rows rather than from an in-memory ring, so a reconnect after a restart still catches up, and because the ids are the table's rowids they keep climbing across restarts and a reconnecting client is never sent history twice. While at least one stream is open, a single timer for the whole process reads id > <highest sent> every 250ms and asks for nothing when that id has not moved; the last stream to close stops it. Rows are trimmed to the last hour and the last thousand, five seconds after a write rather than inside a response, and the trim always keeps the newest row — with the table empty SQLite would hand out id 1 again and an id a client holds would come back around. A row whose data will not parse is skipped, never thrown.

Two things to look at. src/tty.ts is untouched: useDb(db) in src/server.ts names the database the log lives in, and a process that never says so — the terminal — falls back to the app's own file, which is the same file. And the cross-process test is a real second process (Bun.spawnSync running addNote over the same data dir) rather than a second Database handle as the issue sketched: within one process the bus is process-global, so a handle-only write would be delivered in memory and prove nothing about the follower; the second-handle case is still covered by inserting the row the way another process's emit does. src/live.test.ts opens most streams by calling events(request) directly with an AbortController, because Bun keeps a cancelled fetch's socket pooled and the server never learns the client dropped it — the one HTTP stream in the file is the cross-process test. I did not add an index on created_at (the trim scans at most a thousand rows) and did not touch static/live.js, which already reconnects with since.

To try it in the preview: open the page in a browser, open the terminal with the exact command in FORGE_APP_SSH, press a and type a note — it appears in the browser without a reload.

🤖 Generated with Claude Code

Closes #9

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs