- Shipped
- September 9, 2026 at 1:21 AM UTC
- Author
- Kamo
- Commit
- 4b2316a
Both ends of a table's life allowed the same thing: a table with nobody at it. OPENING ONE COST NOTHING AND SEATED NOBODY. A host could put a table in front of the whole organization without taking a seat at it, so the first colleague to accept the invitation arrived alone at a game whose creator was not playing — and waited for a second player who had never intended to be one. The host now takes seat zero and pays the buy-in in the same transaction. The charge runs LAST, after every other write, because it is the only step that leaves the service and a rollback cannot un-debit a wallet; if it is refused, the whole transaction goes with it and there is no table left behind advertising a seat nobody bought. Invitees are still asked rather than billed — being in a chat where somebody started a game is an invitation to play, never a commitment of your money. AND A TABLE EVERYBODY LEFT STAYED OPEN FOR EVER. It kept its row in every colleague's lobby, offering a seat at a game that had finished. `closeIfEmpty` runs on the two paths a seat can actually go away — standing up, and the delayed stand-up a player who asked to leave mid-hand gets when the hand settles — and a closed table drops out of the lobby, which reads only OPEN tables. There is no third path: a busted player keeps their seat and is offered a rebuy. Closing publishes CLOSED, which is the point of it. A player who stands up walks themselves back to the lobby; a SPECTATOR has no seat to leave and would otherwise sit at a felt that will never send another frame. When the table empties as a hand settles, that one frame carries both — the result and the status — rather than showing somebody a finished hand and then separately telling them the table it was on no longer exists. The lobby also answers with the reader's balance now, as a RECORD rather than a map: opening a table costs money, so the button can name its price and refuse in advance instead of sending somebody through a server error to find out — and `Map.of` throws on a null value, which would have made the one reply whose normal answer can be absent ("the wallet did not answer") the one that 500s. Null and zero stay different answers: null disables nothing, because telling somebody they are broke on the strength of a failed network hop is worse than letting them try.