Split out of #9 so the socket's hard half — who may open one, what closes it, and what a flood costs —
is built and tested against a stub source while the round (#8) is still being written. #9 keeps only
the frames.
Done looks like
[ ] src/socket.ts (new): GET /ws upgraded through Bun.serve's built-in websocket support, no
dependency, with one row in src/routes.ts so the table stays the map of every path.
[ ] The upgrade is refused unless it passes socketOrigin from #17 — Origin present and equal to
publicOrigin. A missing Origin is refused here even though a POST may pass without one, and
the code says why in a line.
[ ] Identity is fixed at upgrade time: who(request, key) plus playerKey from #19. The socket
remembers who it is; no inbound message ever names its own sender.
[ ] Every inbound message is validated by parseSteer from #16 and by nothing else: at most 256
bytes, an object, and a named failure closes the socket with 1008 while throwing nothing.
[ ] Rationed per socket: a normal 20 steers a second passes, a flood is dropped rather than queued,
and a socket that keeps flooding is closed. A socket silent for 90 s is closed, and a ping goes
out inside the 60 s idleTimeout in src/server.ts.
[ ] A seat is a person, not a tab: the pull decides and writes down what a second socket on the same
player key does — newest wins, or both drive one mote — so one person cannot hold 24 of 24 seats.
[ ] Backpressure is a named function #9 can call per frame: a socket whose send buffer is backed up is
skipped, never awaited, so a slow client degrades alone.
[ ] Sockets are handed to a publish(payload) that the round will drive later; in this issue a test
drives it, so this lands with no loop in the tree.
[ ] src/socket.test.ts: a foreign Origin and an absent Origin are both refused; an oversized
message and a malformed one each close with 1008; valid steers arrive in order; a flood is dropped
and the socket survives; two sockets on one player key do what the pull says they do.
Risks Websockets bypass allow() entirely, which is why the per-socket ration is acceptance and
not an afterthought. Bun's websocket handlers hang off the server object rather than the route table —
keep one row pointing at them. #7 is rewriting src/routes.ts right now: add a row, do not reorganise
the file. This is the pull Sentinel should read line by line.
Waits on #16 (the message format) and #17 (the upgrade rule). Fits Vesper's brief, with Sentinel
reviewing.
Split out of #9 so the socket's hard half — who may open one, what closes it, and what a flood costs — is built and tested against a stub source while the round (#8) is still being written. #9 keeps only the frames.
Done looks like
src/socket.ts(new):GET /wsupgraded throughBun.serve's built-in websocket support, nodependency, with one row in
src/routes.tsso the table stays the map of every path.socketOriginfrom #17 —Originpresent and equal topublicOrigin. A missingOriginis refused here even though a POST may pass without one, and the code says why in a line.who(request, key)plusplayerKeyfrom #19. The socketremembers who it is; no inbound message ever names its own sender.
parseSteerfrom #16 and by nothing else: at most 256bytes, an object, and a named failure closes the socket with 1008 while throwing nothing.
and a socket that keeps flooding is closed. A socket silent for 90 s is closed, and a ping goes out inside the 60 s
idleTimeoutinsrc/server.ts.player key does — newest wins, or both drive one mote — so one person cannot hold 24 of 24 seats.
skipped, never awaited, so a slow client degrades alone.
publish(payload)that the round will drive later; in this issue a testdrives it, so this lands with no loop in the tree.
src/socket.test.ts: a foreignOriginand an absentOriginare both refused; an oversizedmessage and a malformed one each close with 1008; valid steers arrive in order; a flood is dropped and the socket survives; two sockets on one player key do what the pull says they do.
Files
src/socket.ts(new),src/socket.test.ts(new),src/routes.ts,src/server.ts.Risks Websockets bypass
allow()entirely, which is why the per-socket ration is acceptance and not an afterthought. Bun's websocket handlers hang off the server object rather than the route table — keep one row pointing at them. #7 is rewritingsrc/routes.tsright now: add a row, do not reorganise the file. This is the pull Sentinel should read line by line.Waits on #16 (the message format) and #17 (the upgrade rule). Fits Vesper's brief, with Sentinel reviewing.