Skip to content

POST /room/:code/leave for beacon teardown#12

Closed
tim4724 wants to merge 2 commits into
mainfrom
feat/leave-endpoint
Closed

POST /room/:code/leave for beacon teardown#12
tim4724 wants to merge 2 commits into
mainfrom
feat/leave-endpoint

Conversation

@tim4724
Copy link
Copy Markdown
Owner

@tim4724 tim4724 commented May 9, 2026

Summary

Add POST /room/:code/leave so clients can deliver an "I'm leaving" signal via navigator.sendBeacon from a pagehide handler.

Why: Android Chrome routinely drops the WebSocket close frame when a tab is closed (crbug 40378664, crbug 40839988, websockets/ws#1380) — the renderer is killed before the network service flushes the close handshake. The relay then only detects the dead WS via TCP-level teardown, and clients fall back to their own heartbeat (multi-second). Beacons are queued by the browser's network service and delivered independently of the renderer, so they survive the teardown that drops WS frames.

Endpoint

  • Body: form-urlencoded, clientId=<the slot's bearer secret>
  • 204 — leave processed OR idempotent no-op (room/slot not found, slot already inactive). Does not distinguish, to avoid leaking room/slot existence to scanners
  • 400 — missing or empty clientId
  • 405 — non-POST method
  • Closes the replaced WebSocket with code 4001, reason "leave"

The handler reuses existing region/instance routing (the path matches the same :code extraction as GET /room/:code), so beacons land on the right machine on Fly.

Bumps to 2.1.0.

Test plan

  • bun test — 73 pass, including new endpoint coverage
  • Deploy and verify with HexStacker-Party PR (paired client change)
  • Manual Android Chrome tab-close test against deployed relay

tim4724 and others added 2 commits May 9, 2026 23:31
Lets clients deliver an "I'm leaving" signal via navigator.sendBeacon
from a pagehide handler. Android Chrome routinely drops the WebSocket
close frame on tab close (crbug 40378664), so the renderer-independent
network-service delivery of sendBeacon is what gets the leave to the
server reliably.

Body is form-urlencoded with clientId (the slot's bearer secret).
Idempotent: missing room or wrong clientId resolves to 204 to avoid
leaking room/slot existence.

Bumps to 2.1.0.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "room cleanup on solo leave" test slept 50ms before checking room
deletion, suggesting cleanup was async — but it happens synchronously
inside the HTTP handler before the response is returned, so by the
time fetch() resolves the room is already gone. Replace with direct
post-response assertion.

Add a concurrent WS-close + HTTP-leave race test. The detach guards
in both paths (removeFromRoom checks ws.data.index, leave checks
member.ws) should make this idempotent regardless of arrival order
— the test asserts host receives exactly one peer_left.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@tim4724
Copy link
Copy Markdown
Owner Author

tim4724 commented May 10, 2026

Closing without merge.

This endpoint was designed as the receiving end for navigator.sendBeacon calls from a controller's pagehide handler — the goal being fast disconnect detection on Android Chrome tab close. Empirical testing on a real Android Chrome device (HexStacker-Party#136) showed the underlying assumption doesn't hold: on Android Chrome no client-side lifecycle event (pagehide, visibilitychange, blur, freeze, pageshow, resume) fires reliably on tab close or app-switch. The W3C and Chrome docs both confirm this is intentional/known (w3c/page-visibility#59, Chrome Page Lifecycle docs).

So the beacon never gets queued in the case we cared about — there's no caller for this endpoint that delivers a real improvement. Without that, adding the endpoint is just dead surface area.

The right fix is on this server's side instead: tighten the WebSocket idleTimeout (currently 10) so the relay closes silent connections faster regardless of client behavior. Will likely open a separate small PR for that.

@tim4724 tim4724 closed this May 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant