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

A colleague's move costs a kilobyte, not half a megabyte: the live event carries what changed #65

open not picked up yet opened this
opened this

Make a live change apply from the event itself, so one person's move stops making every other tab re-download the whole board.

What it is like today. Measured on main at ec382a9, locally: a board at this app's own card cap — 500 cards in 3 columns — is 525,354 bytes of HTML (10,990 bytes empty; ~1,050 bytes per card). static/board.109353762.js:403 freshBoard() re-fetches location.pathname + location.search — that whole page — and applyNow calls it for card.added, card.moved, card.edited, board.renamed, column.added, column.moved and column.renamed; only card.deleted and column.deleted apply without a fetch. So on a busy board, each move by one person costs each other open tab half a megabyte and the server one full re-render (7 ms each). Ten moves, four tabs: 21 MB and 280 ms of rendering for changes that are a few dozen bytes each. The direction's "appears for everyone within a second" holds today only because the boards we test with are small.

What done looks like

  • [ ] The BoardEvent for a card change carries the card's own fields — id, column_id, title, position — and a column change carries id, name, position; undo is unchanged and still means "re-read the board".
  • [ ] bus.publish callers pass the row they just wrote (src/routes.ts and src/mcp.ts both already hold it, or can re-read it inside the same request), so the wire never needs a second query.
  • [ ] static/board.js applies card.added, card.edited, card.moved, column.added, column.renamed and column.moved without any fetch: it builds or updates DOM nodes from the event's fields, placing a card by comparing position against its live siblings' data-position (add one to the card markup if needed).
  • [ ] Nothing from the wire is ever assigned as HTML: every title or name goes in through textContent or a property assignment, never innerHTML. A card titled <img src=x onerror=alert(1)> arriving live renders as text — assert it in a test.
  • [ ] by stays stripped from the wire exactly as it is now (src/routes.ts send()), and the frame gains no new identity field.
  • [ ] The whole-board fetch remains for exactly two cases, both documented in the script: an undo, and the reconcile after a reconnect.
  • [ ] A test in src/live-refresh.test.ts drives a card.moved frame through the script with fetch stubbed to throw, and the card still lands in the right place with the right counts; another does the same for card.added and column.renamed.
  • [ ] bun test passes, and the README's Live bullet says the event carries the change rather than pointing at the board's HTML.

Files I expect to change: src/bus.ts, src/routes.ts, src/mcp.ts, static/board.<new hash>.js (with BOARD_JS updated in src/pages/board.ts), src/live-refresh.test.ts, src/server.test.ts, README.md.

Risks. Escaping moves from the server to the client — that is the whole security weight of this change, and the reason for the explicit textContent criterion. Card titles now travel on the stream, which is fine (anyone on that stream holds the board's link and can already read every title) but must not drag by along with it. Position arithmetic on the client has to match db.moveCard's ordering (position, then id) or a card lands one slot off until the next reconnect. Waits for #48: that issue rewrites the same card markup and the same client-side card builder, and a smaller card is the right thing to build this on.

Who fits: scout to build it, reviewed by mason — the reconciliation in static/board.js is the most intricate code in the app and this touches all of it.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs