feat: add room-scoped PresenceDO with realtime reactions and seated-presence UI sync#52
Conversation
|
Warning Rate limit exceeded
Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 41 minutes and 22 seconds. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml Review profile: ASSERTIVE Plan: Pro Run ID: 📒 Files selected for processing (4)
WalkthroughIntroduces a real-time user presence synchronization system with a new Changes
Sequence Diagram(s)sequenceDiagram
participant Client1 as Client 1
participant PresenceDO as PresenceDO<br/>(Server)
participant Client2 as Client 2
Client1->>PresenceDO: WebSocket connect + token/user_id
PresenceDO->>PresenceDO: Validate auth & create session
PresenceDO->>Client1: welcome (current presence state)
Client2->>PresenceDO: WebSocket connect + token/user_id
PresenceDO->>PresenceDO: Create session for Client2
PresenceDO->>Client2: welcome (current presence state)
PresenceDO->>Client1: delta (Client2 joined)
Client1->>PresenceDO: presence message (emoji: 👍)
PresenceDO->>PresenceDO: Update Client1 presence
PresenceDO->>Client2: delta (Client1 emoji updated)
Client1->>PresenceDO: WebSocket close
PresenceDO->>PresenceDO: Remove session, delete presence
PresenceDO->>Client2: leave (Client1 left)
sequenceDiagram
participant Browser as Browser<br/>(Client)
participant PresenceWs as presenceWs<br/>(WebSocket)
participant PresenceDO as PresenceDO<br/>(Server)
participant DOM as DOM UI
Browser->>PresenceWs: Open connection
PresenceWs->>PresenceDO: Connect request
PresenceDO->>PresenceWs: welcome + presence state
PresenceWs->>Browser: Receive welcome
Browser->>DOM: Populate presenceByUser, render badges
Browser->>DOM: User clicks "👍" button
DOM->>PresenceWs: Send presence {emoji: "👍"}
PresenceWs->>PresenceDO: WebSocket message received
PresenceDO->>PresenceDO: Validate & update emoji
PresenceDO->>PresenceWs: Broadcast delta to other clients
PresenceWs->>Browser: Receive delta update
Browser->>DOM: Update presenceByUser, re-render avatars & badges
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 13
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@public/classroom_poc.html`:
- Around line 882-899: presenceBadge returns HTML-escaped content (it calls
escapeHtml) but renderOtherAvatars embeds its result into innerHTML while
updatePresenceControls assigns the self badge to myPresenceBadge.textContent;
add a brief inline comment in presenceBadge and/or next to renderOtherAvatars
explaining that presenceBadge returns already-escaped HTML and must not be
double-escaped or replaced with unescaped values when used with innerHTML
(mention presenceBadge, renderOtherAvatars, myPresenceBadge, innerHTML,
textContent, and escapeHtml) so future edits don't remove the escaping or mix
unescaped content into innerHTML.
- Around line 563-591: connectPresenceWS currently retries unconditionally every
1200ms on close which can hammer the server; modify it to reuse the main
socket's exponential backoff/cap pattern (e.g., mirror
scheduleReconnect/reconnectAttempts logic) or implement a local backoff counter
and timer (presenceReconnectAttempts, presenceReconnectTimer) that increases
delay exponentially and caps attempts, and use that timer for reconnect
scheduling instead of setTimeout(1200). Also ensure disconnectWS clears
presenceReconnectTimer and resets presenceReconnectAttempts when
intentionallyDisconnect is set so no stale timers or attempts remain.
- Around line 601-644: handlePresenceMessage currently clears presenceByUser on
a 'welcome' and fully replaces it, which clobbers optimistic local updates for
the current user; change the reconnect logic so either (A) presenceWs.onopen
re-sends the full local self state (emoji, hand_raised, display_name, x/y) to
the server to replay pending local intent, or (B) modify handlePresenceMessage
when data.type === 'welcome' to merge data.state into presenceByUser and
preserve any existing presenceByUser[myParticipantId] values (prefer local
values for keys like emoji and hand_raised) instead of overwriting them; update
references in handlePresenceMessage, presenceByUser, presenceWs.onopen, and the
button handlers that write presenceByUser to ensure consistency.
- Around line 593-599: presenceSend currently drops messages when presenceWs is
not OPEN which causes local state (presenceByUser[myParticipantId]) to drift
from the server; modify presenceSend to enqueue the serialized message (or the
raw object) into a small pending queue when presenceWs is not open, and on
presenceWs.onopen (or when a reconnect succeeds) replay and clear that queue by
sending each queued item via presenceWs.send(JSON.stringify(...)); ensure any
queueing logic references presenceSend, presenceWs, presenceByUser,
myParticipantId and the presenceWs.onopen handler so updates triggered by UI
handlers are reliably delivered after reconnect.
In `@src/worker.py`:
- Around line 1890-1902: The presence-update handling in src/worker.py currently
processes "emoji" and "hand_raised" but ignores "display_name", causing client
code (handlePresenceMessage in public/classroom_poc.html) that merges
delta.display_name to never receive mid-session name updates; add a branch
alongside the existing ones to accept an optional "display_name" when
data.get("display_name") is a string (apply the same length/sanitization policy
as emoji, e.g., truncate to a max length), compare to current["display_name"],
and if different set current["display_name"], delta["display_name"], and changed
= True so authenticated users can update their label mid-session.
- Around line 1911-1923: Rename unused handler parameters to start with
underscores to silence linter warnings and document intent: in on_webSocketClose
change parameter names from (self, ws, code, reason, wasClean) to (self, ws,
_code, _reason, _was_clean) and in on_webSocketError rename the ws parameter to
_ws (keeping other names as-is to match ClassroomDO signatures), leaving all
logic unchanged (use self._session_for_ws, self.sessions, self.presence, and
self._broadcast as before).
- Around line 1928-1937: The _send_welcome method serializes the live
self.presence dict which can produce partially-updated snapshots during
concurrent mutations and scales O(users) per join; to fix, create a stable copy
(e.g., shallow or deep copy) of self.presence inside _send_welcome before
json.dumps to avoid capturing in-flight mutations and, for scalability,
optionally transform that copy to a capped/filtered view (e.g., only include
active users or non-default fields) so the serialized "state" sent by
_send_welcome remains consistent and bounded in size.
- Around line 1837-1844: In on_webSocketMessage, avoid silently returning on
oversized frames or JSON/parsing errors: when raw length exceeds 512, record an
observability event (e.g., call capture_exception or capture_message with
context noting the frame size and ws/client id) before returning, and in the
except Exception block call capture_exception(exc) (or equivalent existing
capture_exception function used elsewhere) passing the caught exception and any
relevant context (raw snippet length, client identifier) so failures are visible
in Sentry/logs while preserving the current control flow; update references in
on_webSocketMessage to use the same capture_exception helper used elsewhere in
this file.
- Around line 1788-1798: The existing presence record for a reconnecting user
(self.presence.get(user_id) -> existing) is left with a stale display_name;
update the presence entry when a session connects so others see the new name. In
the connection path (where session_id is created and sessions[...] is set) set
existing["display_name"] = display_name when existing is not None (or assign a
new dict with display_name) and ensure self.presence[user_id] is updated
accordingly so the subsequent delta broadcast reads the refreshed name; touch
the same symbols: self.presence, existing, user_id, display_name, and
sessions/session_id.
- Around line 1831-1835: The bare except TypeError around the Response(None,
status=101, web_socket=client) call can hide real construction errors; update
the handler to re-raise unexpected TypeError values and only fall back when the
error clearly indicates the constructor rejected the web_socket keyword (e.g.,
catch TypeError as e and if "web_socket" or "unexpected keyword" appears in
str(e) then return Response(None, status=101) else raise), referencing the
Response(...) construction and the web_socket=client argument so production
failures still surface while the test shim fallback remains available.
In `@tests/test_presence_do.py`:
- Around line 75-202: Add three tests to TestPresenceDOOnFetch: (1) an
authenticated-join case that uses worker.create_token(...) with the test env's
JWT_SECRET to produce a valid token and calls do.on_fetch(...) to assert the
connection is accepted and that the token's user identity is used (ignoring
query user_id/display_name) — target functions: PresenceDO.on_fetch and
worker.create_token; (2) an empty-user rejection case where you create a token
whose payload has id="" and assert do.on_fetch(...) returns status 400 to cover
the if not user_id branch in PresenceDO.on_fetch; (3) a display-name truncation
test mirroring test_user_id_sanitised_to_64_chars that supplies a display_name
>64 chars (either via token or query depending on auth mode) and asserts the
stored presence display_name is truncated to [:64]; add these tests into
tests/test_presence_do.py under TestPresenceDOOnFetch.
- Around line 479-494: The test test_invalid_room_id_not_matched currently only
asserts that env.PRESENCE_DO.idFromName was not called; update it to also assert
the HTTP response is a 404 to make intent explicit: after calling
worker._dispatch(req, env) add an assertion like resp.status == 404 (and
optionally check resp.text or body for "API endpoint not found") so the test
verifies the request fell through to the generic API 404 rather than producing
another error.
- Around line 231-242: Rename the unused test variable bindings named "sid" to
"_sid" in the tests that call _setup_user (e.g., in
test_no_broadcast_on_no_change and the other tests referenced) so the intent is
clear and Ruff RUF059 is silenced; update each occurrence of "do, sid, ws =
await self._setup_user()" to "do, _sid, ws = await self._setup_user()" (and any
other similar tuple unpackings) without changing test logic.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f15b3dad-d452-4f6b-9bae-44de2f2df51e
📒 Files selected for processing (4)
public/classroom_poc.htmlsrc/worker.pytests/test_presence_do.pywrangler.toml
This PR introduces a new PresenceDO for realtime per-room user presence and integrates it with the classroom frontend.
What’s included
Added PresenceDO in src/worker.py:
Manages one DO instance per room via idFromName(room_id).
Tracks per-user presence state: x, y, emoji, hand_raised, display_name
Handles WebSocket lifecycle: join (welcome full state to joiner) , message (presence updates), disconnect (leave)
Broadcasts delta-only updates to connected peers.
Supports websocket hibernation restore/persist using attachments.
Dispatcher + config:
Added /api/presence/<room_id> DO dispatch.
Added PRESENCE_DO durable object binding in wrangler.toml.
Added DO migration v2 with PresenceDO.
Frontend (public/classroom_poc.html):
Added dedicated presence websocket connection.
Added UI controls for reactions: 👍 and ✋.
Added immediate local optimistic state updates for responsiveness.
Rendered reaction/hand badges on avatar circles.
Hid floating avatars for seated users to avoid duplicate identity indicators.
Rendered seated users’ presence badges directly on desk labels.
Added refresh paths to keep seat labels/presence badges in sync across local and remote updates.
Screen.Recording.2026-04-23.010509.mp4
Summary
This PR implements real-time per-room user presence synchronization through a new PresenceDO (Durable Object) and integrates it with the classroom frontend UI.
Key Changes
Backend (PresenceDO Implementation)
PresenceDOclass to manage one Durable Object instance per room, tracking user presence state including position (x, y), emoji reaction, hand-raised status, and display name/api/presence/<room_id>dispatcher route andPRESENCE_DObinding towrangler.tomlFrontend (Classroom UI)
Impact
Users can now express real-time reactions and hand-raise state across their classroom room without page refresh. The presence system provides lightweight, efficient per-user state synchronization using delta broadcasting to minimize bandwidth, and supports both authenticated and anonymous access modes.