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
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.
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.mdbeside the existingemitline: 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 /eventsanswers with the security floor, which it does not today. - [ ] Nothing about topics, nothing in
static/, and no change tosrc/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.
Intent.
src/live.tsis already a good stream — numbered events,Last-Event-IDreplay, 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:7delivers only events with that topic plus untopicked ones; withouttopicthe stream is what it is today.AGENTS.mdbeside the existingemitline and enforced by a test: an event'sdatais 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.datais 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.jsreads 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.visibilitychangeback to visible andpageshowfrom the back/forward cache both refetch the live regions once, and a reconnect whosesinceis older than the ring the server still keeps refetches rather than pretending it is current.src/server.test.ts: two subscribers with different topics each get only theirs; a subscriber with no topic gets both; replay byLast-Event-IDstill works with topics; the stream still opens withretry: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.)