Opening a table buys you into it, and the last one out closes it

FeatureMediaService
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.

All changes

Like what you see shipping?

Every one of these updates lands in your workspace automatically. Start free and watch it grow week after week.

Start Free ForeverView Pricing