The WebSocket signaling path (server/utils/webrtcSignaling.js) handles create-transport messages without an async lock, while the REST endpoint at server/api/live/webrtc/create-transport.post.js uses acquire('create-transport-${sessionId}'). Concurrent WebSocket clients can race on calling getRouter(sessionId) and createTransport(router) before reaching updateLiveSession, potentially creating duplicate transports for the same session.
Note: updateLiveSession itself acquires a lock (session-update-${id}), so writes are serialized, but reads and transport creation happen outside any lock in the WS path. This means two concurrent WS calls can each create a separate mediasoup transport before either reaches the serialized write — both succeed, but only one transport ID survives in session state.
Acceptance criteria
Wrap the create-transport case in handleWebSocketMessage with an async lock keyed by create-transport-${sessionId} (same key as the REST endpoint to prevent cross-path races)
The entire handler body for create-transport — including getRouter, createTransport, and updateLiveSession — must be inside the locked region
Existing tests pass; add a test verifying concurrent WebSocket create-transport messages for the same session do not produce duplicate transports
## Summary
The WebSocket signaling path (`server/utils/webrtcSignaling.js`) handles `create-transport` messages without an async lock, while the REST endpoint at `server/api/live/webrtc/create-transport.post.js` uses `acquire('create-transport-${sessionId}')`. Concurrent WebSocket clients can race on calling `getRouter(sessionId)` and `createTransport(router)` before reaching `updateLiveSession`, potentially creating duplicate transports for the same session.
Note: `updateLiveSession` itself acquires a lock (`session-update-${id}`), so writes are serialized, but reads and transport creation happen outside any lock in the WS path. This means two concurrent WS calls can each create a separate mediasoup transport before either reaches the serialized write — both succeed, but only one transport ID survives in session state.
## Acceptance criteria
- [ ] Wrap the `create-transport` case in `handleWebSocketMessage` with an async lock keyed by `create-transport-${sessionId}` (same key as the REST endpoint to prevent cross-path races)
- [ ] The entire handler body for `create-transport` — including `getRouter`, `createTransport`, and `updateLiveSession` — must be inside the locked region
- [ ] Existing tests pass; add a test verifying concurrent WebSocket `create-transport` messages for the same session do not produce duplicate transports
<!-- jasper-fp:2333e3299204f265 -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Summary
The WebSocket signaling path (
server/utils/webrtcSignaling.js) handlescreate-transportmessages without an async lock, while the REST endpoint atserver/api/live/webrtc/create-transport.post.jsusesacquire('create-transport-${sessionId}'). Concurrent WebSocket clients can race on callinggetRouter(sessionId)andcreateTransport(router)before reachingupdateLiveSession, potentially creating duplicate transports for the same session.Note:
updateLiveSessionitself acquires a lock (session-update-${id}), so writes are serialized, but reads and transport creation happen outside any lock in the WS path. This means two concurrent WS calls can each create a separate mediasoup transport before either reaches the serialized write — both succeed, but only one transport ID survives in session state.Acceptance criteria
create-transportcase inhandleWebSocketMessagewith an async lock keyed bycreate-transport-${sessionId}(same key as the REST endpoint to prevent cross-path races)create-transport— includinggetRouter,createTransport, andupdateLiveSession— must be inside the locked regioncreate-transportmessages for the same session do not produce duplicate transports