anvilsign in

collin/mahjong · f857279a

The tunnel keeps its name: mahjong-table, every run

Collin Richards · 2026-08-21 17:01 UTC · f857279a2dbb8b7527af80d127b27e1d3e991ce1 · parent a0adfbc3 · browse files

modifiedREADME.md+6 −0
⋯ 228 unchanged lines
229229 or `rsgrok` is not on the PATH (`RSGROK_BIN` points elsewhere), links fall
230230 back to the page's own address and the tunnel is retried on later joins.
231231
232+The tunnel always asks for the same subdomain — `mahjong-table`, or whatever
233+`TUNNEL_NAME` says — so restarts land back on the URL people already have
234+instead of burning a fresh name each run. A name someone else already holds
235+kills that first attempt before it prints a URL; the retry then goes out
236+nameless and takes whatever it is given.
237+
232238 ## On a phone
233239
234240 The table assumes four people sitting around a screen lying flat, and about
⋯ 287 unchanged lines
modifiedserver/rooms.ts+15 −1
⋯ 73 unchanged lines
7474 * moment the server knows its port, read the https URL off the tunnel-up
7575 * line, and hand it to every client on join. A tunnel that dies — network
7676 * gone, rsgrok missing — is retried on later joins, at most once per window.
77+ *
78+ * We always ask for the same subdomain (`TUNNEL_NAME`, default `mahjong-table`), so
79+ * a restart lands back on the URL people already have and we spend one name
80+ * rather than a fresh one per run. If that name is taken — a second copy of
81+ * the server, a tunnel the last run left behind — the first attempt dies
82+ * without ever printing a URL, and the retry goes out nameless.
7783 */
7884 const TUNNEL_RETRY = 10_000;
7985 /** How long a join will wait on a tunnel still shaking hands. */
8086 const TUNNEL_WAIT = 3_000;
87+/** The subdomain we ask for: {name}.vibe.richardscollin.com. */
88+const TUNNEL_NAME = process.env.TUNNEL_NAME ?? 'mahjong-table';
8189
8290 export function attachRooms(server: UpgradeServer, path = '/ws'): void {
8391 const rooms = new Map<string, RoomRec>();
⋯ 7 unchanged lines
9199 let tunnel: ChildProcess | null = null;
92100 let tunnelUrl = '';
93101 let tunnelDiedAt = 0;
102+ /** Cleared once a named attempt dies URL-less: the name is someone else's. */
103+ let tunnelName = TUNNEL_NAME;
94104 let waiters: Array<() => void> = [];
95105 const wake = () => {
96106 for (const w of waiters) w();
⋯ 4 unchanged lines
101111 if (process.env.PUBLIC_URL || tunnel || Date.now() - tunnelDiedAt < TUNNEL_RETRY) return;
102112 const port = ownPort();
103113 if (port === null) return;
114+ const named = tunnelName;
104115 const child = spawn(
105116 process.env.RSGROK_BIN ?? 'rsgrok',
106117 // No :4040 inspection API — another agent may already own that port.
107- ['http', `127.0.0.1:${port}`, '--web-addr', 'false'],
118+ ['http', `127.0.0.1:${port}`, '--web-addr', 'false', ...(named ? ['-n', named] : [])],
108119 { stdio: ['ignore', 'pipe', 'inherit'] },
109120 );
110121 tunnel = child;
⋯ 10 unchanged lines
121132 });
122133 const gone = () => {
123134 if (tunnel === child) {
135+ // Died before it ever said a URL, and we had asked for a name: read
136+ // that as the name being spoken for, and go nameless from here.
137+ if (!tunnelUrl && named) tunnelName = '';
124138 tunnel = null;
125139 tunnelUrl = '';
126140 tunnelDiedAt = Date.now();
⋯ 195 unchanged lines