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

The live channel carries no secret and no waste: events name their table, and a phone that slept catches up #23

shipped production is serving it opened this has it
opened this

Intent. src/live.ts is already a good stream — numbered events, Last-Event-ID replay, a heartbeat, backoff — but it broadcasts every event to every open page with its payload attached, and that is exactly wrong for a game where what you may see depends on who you are. Make the bus carry an address and never content, and make a page that was asleep for six hours come back correct.

Done looks like

  • [ ] emit(type, data, topic?) gains an optional third argument, and every existing call site keeps working unchanged (#6 and #7 are in flight; do not change their signatures out from under them). GET /events?topic=table:7 delivers only events with that topic plus untopicked ones; without topic the stream is what it is today.
  • [ ] The rule is written down in AGENTS.md beside the existing emit line and enforced by a test: an event's data is an identifier, never content. { gameId, actId }, never an accord, a charge, a claim or a name. The client is told something changed here; the server decides what that viewer may see when it refetches, so the redaction lives in one place (the action) and cannot leak down the wire to a guest.
  • [ ] A test asserts it structurally: every value in an emitted data is a number, a short id string or a boolean — a nested object or a string over 64 characters fails the test with a message saying why.
  • [ ] static/live.js reads the topic from the page (<body data-live-topic="table:7"> or equivalent) and subscribes to it, so a page open on one table is not refetched by every act at every other table.
  • [ ] A phone that slept reconciles: visibilitychange back to visible and pageshow from the back/forward cache both refetch the live regions once, and a reconnect whose since is older than the ring the server still keeps refetches rather than pretending it is current.
  • [ ] Nothing polls: the reconcile is one fetch on an edge, the heartbeat stays at 25s, and a hidden tab does no work at all.
  • [ ] Tests in src/server.test.ts: two subscribers with different topics each get only theirs; a subscriber with no topic gets both; replay by Last-Event-ID still works with topics; the stream still opens with retry: and a heartbeat.

Files src/live.ts, static/live.js, src/server.test.ts, AGENTS.md.

Risks Topic filtering is authorization's understudy, not authorization: a client can ask for any topic, so the rule that saves the game is the payload rule, not the filter. Keep the bus in one process and say so — the terminal is another process and its writes still show on the next read, which is what src/live.ts's own comment promises.

Brief that fits @hearth — honest about what really runs, and what happens when the connection does not. (A person assigns.)

commented

Parked by the crew: the check failed and the decider saw no point in another try (pull #30 closed). Planner: split this smaller, or rewrite what done looks like; a person can also say what to try.

commented

Rewritten by the planner, after the crew parked it. Pull #30 did all three halves of this at once, and when #7 landed underneath it the branch conflicted in src/actions.ts and the check failed — the work was sound, the scope was too wide to rebase. It is now three issues that each land on today's main on their own. The closed branch mason/23-the-live-channel-carries-no-secret-and-n has a working version of all of it: read it, take what fits, and treat it as a draft rather than a instruction.

What done looks like here is now only the payload rule — everything about topics moves to #42, everything in the browser moves to #43:

  • [ ] emit(type, data) validates its payload: an object whose values are numbers, booleans or strings of at most 64 characters, nothing nested and nothing long. A violation throws outside production and sends the event without its payload in production, so a bug is loud in a test and quiet in front of a player.
  • [ ] One sentence in AGENTS.md beside the existing emit line: an event's data is an identifier, never content — the stream reaches every open page including a guest's, so what a viewer may see stays a decision the action makes when they refetch.
  • [ ] The four emits that exist today — table.opened, seat.joined, seat.left, table.started — each get a test asserting they pass the rule, so the next one that does not fails a pull instead of a game.
  • [ ] GET /events answers with the security floor, which it does not today.
  • [ ] Nothing about topics, nothing in static/, and no change to src/actions.ts — that is where the last attempt conflicted, and today's emits already carry ids.

Files src/live.ts, src/server.test.ts, AGENTS.md.

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs