Author: Michael Lynch <git@mtlynch.io>
Fix session takeover signaling and slim the reconnect plumbing
The replaced-tab flow never worked over the wire: stopping a channel
with a non-shutdown reason reaches phoenix.js as a bare "phx_error"
with an empty payload, so the client classified takeovers as
"reconnecting" and auto-rejoined, letting two tabs on one session
steal it from each other forever. The server now pushes an explicit
"session_replaced" event before stopping with {:shutdown, :replaced},
and the client reacts to that event alone. Covered by a channel test,
a network unit test, and a two-tab e2e test.
Robustness and performance:
- Suspended sessions are pruned after a retention period
(session_retention_ms, default one hour); previously state.sessions
grew by one entry per page load forever.
- A connections index maps connection_ref -> session_id, replacing the
per-message linear scan over all sessions (including 20 Hz
report_state casts and per-tick delivery).
- Session tokens now expire after 30 days instead of never.
- Token verification moved from socket connect to channel join so a
stale token (rotated secret, expired) surfaces as a join error the
client can see; app.ts then discards the stored token and reloads
once instead of wedging in a silent reconnect loop.
Simplifications:
- Removed world_instance_id/revision from every payload and event;
nothing consumed them. Removed the unused onRejoin/onStateChange
handler hooks.
- Inventory counts now flow only through inventory_changed totals;
pickup events carry ids for sprite removal but no counts, and the
event is only sent when totals actually changed.
- World events deliver synchronously instead of bouncing through a
send-to-self hop, restoring event-before-snapshot ordering.
- One fenced-push helper in Network drops stale replies for all call
sites, replacing four hand-rolled connectionCycle guards.
- disconnect_grace_ms has one default (State.disconnect_grace_ms/0);
the config entry and struct default duplicating it are gone.
- Texture creators are idempotent, so scene restarts call them
unconditionally instead of guarding at every call site.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Total
5m04s
| Time to Start | Worker time | Duration | Time to finish | Idle | |
|---|---|---|---|---|---|
| Config | 4s | 14s | 14s | 18s | 4s |
| Eval | 17s | 29s | 29s | 47s | 0s |
| Build | 45s | 8m59s | 4m24s | 5m09s | 3s |
| Suite | 4s | 9m43s | 5m04s | 5m09s | 8s |