4.7 KiB
Issue 006: Spectator Role Handshake Retries
Summary
The spectator route sometimes attempts subscribeAll before the server has accepted the
socket as a spectator. In the 8x throttled profile, the console repeatedly showed:
Failed to enter spectator mode Error: Spectator role required
This did not reproduce in the later 6x runtime audit, so it is intermittent. It is still worth fixing because it can create startup retries, extra session/auth events, console noise, and delayed media setup on slow devices.
Severity
Medium-low.
This is not the main steady-state CPU eater. Treat it as a correctness/startup reliability issue that can make performance worse under throttling.
Upstream Likelihood
Low.
This is specific to /spectate startup and server role/subscription ordering. The /
performance backlog probably will not fix it.
Affected Files
webui/src/hooks/useSpectatorMode.jswebui/src/context/SocketContext.jsxwebui/src/context/SessionContext.jsxserver/src/services/authService/index.jsserver/src/services/roverManager/socketHandlers.js
Evidence
Artifact:
perf/results/2026-06-11T03-14-34-168Z/report.json
That 8x CPU-throttled profile captured repeated console errors:
Failed to enter spectator mode Error: Spectator role required
Fresh 6x runtime audit:
consoleErrors: []
auth:role events: 4
So the race is not constant, but it exists under some timing conditions.
Current Code Path
useSpectatorMode does this:
webui/src/hooks/useSpectatorMode.js:18
if (session?.role !== 'spectator') {
await setRole('spectator');
}
webui/src/hooks/useSpectatorMode.js:21
await subscribeAll();
The effect reruns when these change:
webui/src/hooks/useSpectatorMode.js:36
[connected, session?.mode, session?.role, setRole, subscribeAll]
The server rejects subscribeAll unless the socket role is already spectator:
server/src/services/roverManager/socketHandlers.js:161
if (socket.data?.role !== 'spectator') {
cb({ error: 'Spectator role required' });
}
The auth service can initialize a socket as spectator only if the socket handshake query requests it:
server/src/services/authService/index.js:40
const requestedRole = socket.handshake?.query?.role;
server/src/services/authService/index.js:41
const initialRole = requestedRole === 'spectator' ? 'spectator' : 'user';
If the spectate page connects as a normal user and then switches role after connect, slow timing can expose ordering issues.
Why This Matters
The retry itself is not a huge CPU cost. The damage is indirect:
- extra auth/session events
- repeated
subscribeAllattempts - delayed rover room joins
- delayed video/snapshot setup
- noisy console errors during profiling
- worse startup behavior on weak devices
Fix Strategies
Option A: Connect /spectate With role=spectator
Make the spectator route establish its socket with a spectator role query from the start:
io(..., { query: { role: 'spectator' } })
This uses the existing server path in authService.
Option B: Wait For Confirmed Role Before subscribeAll
After calling setRole('spectator'), wait until auth:role or session state confirms
role === 'spectator', then call subscribeAll.
Avoid calling subscribeAll in the same effect tick if the role update has not propagated.
Option C: Add A Server-Side Atomic Spectator Enter Event
Create one event:
session:enterSpectator
Server behavior:
- set socket role to spectator
- join all visible rover rooms
- return one ack
This removes client-side ordering risk.
Option D: Debounce Or Guard Retries
If subscribeAll fails with Spectator role required, do not retry in a tight loop. Wait
for an explicit role event or connection change.
Recommended Path
The cleanest fix is Option A plus Option B:
- Make the
/spectatesocket connect asrole=spectatorwhen possible. - Keep
useSpectatorModeas a fallback, but callsubscribeAllonly after confirmed spectator role.
If socket creation is shared in a way that makes route-specific query parameters awkward, use the atomic server event instead.
Validation
Run the throttled profile several times:
CPU_THROTTLE=8 VIEWPORT=390x844 MOBILE=1 \
node perf/live-cpu-profile.mjs https://rover.otter.land/spectate perf/results
Expected improvements:
- No
Failed to enter spectator modeconsole errors. - Fewer
auth:roleevents during startup. - More consistent startup timing.
Risks
- Be careful not to accidentally make ordinary
/users spectators. - If using route-specific socket query parameters, verify reconnects preserve the correct role.
- In lockdown mode, spectator setup must still fail cleanly.