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
Check · Review · Land · Live done — Landed and live in production.
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
Sign in to join this conversation.
Pushed.
bun testis green (38 tests), and a live boot showed a note written by a second process arriving on an open/eventsstream 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
eventmigration (schema version 3) backsemit(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 /eventsreplays a client'sLast-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 readsid > <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 whosedatawill not parse is skipped, never thrown.Two things to look at.
src/tty.tsis untouched:useDb(db)insrc/server.tsnames 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.spawnSyncrunningaddNoteover the same data dir) rather than a secondDatabasehandle 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'semitdoes.src/live.test.tsopens most streams by callingevents(request)directly with anAbortController, because Bun keeps a cancelledfetch'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 oncreated_at(the trim scans at most a thousand rows) and did not touchstatic/live.js, which already reconnects withsince.To try it in the preview: open the page in a browser, open the terminal with the exact command in
FORGE_APP_SSH, pressaand type a note — it appears in the browser without a reload.🤖 Generated with Claude Code
Closes #9