Skip to content

feat: add room-scoped PresenceDO with realtime reactions and seated-presence UI sync#52

Merged
A1L13N merged 2 commits intoalphaonelabs:mainfrom
Ananya44444:presenced
Apr 23, 2026
Merged

feat: add room-scoped PresenceDO with realtime reactions and seated-presence UI sync#52
A1L13N merged 2 commits intoalphaonelabs:mainfrom
Ananya44444:presenced

Conversation

@Ananya44444
Copy link
Copy Markdown
Contributor

@Ananya44444 Ananya44444 commented Apr 22, 2026

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)

  • Added PresenceDO class to manage one Durable Object instance per room, tracking user presence state including position (x, y), emoji reaction, hand-raised status, and display name
  • Implements WebSocket lifecycle management: sends full presence state on join, broadcasts delta-only updates on presence changes, handles disconnects with session cleanup
  • Supports WebSocket hibernation restore/persist using attachments for state preservation
  • Includes authentication via token verification with optional anonymous access via user_id/display_name query parameters
  • Added /api/presence/<room_id> dispatcher route and PRESENCE_DO binding to wrangler.toml
  • Added comprehensive unit test suite validating DO behavior including message handling, state persistence, and multi-session user support

Frontend (Classroom UI)

  • Added dedicated presence WebSocket connection separate from main classroom connection
  • Implemented presence state UI with two reaction buttons: thumbs-up (👍) and hand raise (✋) for real-time user expression
  • Reactions and hand-raise state display as badges on avatar circles
  • Seated users' floating avatars are hidden; their presence badges render on seat labels instead
  • Real-time presence state updates apply optimistically on client side and sync across all connected users
  • Refactored seat labeling to track occupants and display presence badges alongside seat information

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.

Copilot AI review requested due to automatic review settings April 22, 2026 19:48
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 22, 2026

Warning

Rate limit exceeded

@Ananya44444 has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 41 minutes and 22 seconds before requesting another review.

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 @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

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 configuration

Configuration used: Repository: alphaonelabs/coderabbit/.coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b718e8c0-2f60-47db-a7c8-7b0dae7559a3

📥 Commits

Reviewing files that changed from the base of the PR and between 09fa3e5 and 16513c9.

📒 Files selected for processing (4)
  • public/classroom_poc.html
  • src/worker.py
  • tests/conftest.py
  • tests/test_presence_do.py

Walkthrough

Introduces a real-time user presence synchronization system with a new PresenceDO Durable Object that manages and broadcasts presence state (emoji, hand-raised flag, position) across classroom participants, including client-side UI controls and a comprehensive test suite for the backend presence logic.

Changes

Cohort / File(s) Summary
Presence Backend & Configuration
src/worker.py, wrangler.toml
New PresenceDO Durable Object handles WebSocket authentication (via token or anonymous params), session management, presence state updates (x/y coordinates, emoji, hand-raised), bounds checking, and delta broadcasting to other clients. On disconnect, cleans up sessions and broadcasts leave events. Configuration adds PRESENCE_DO binding and D1 migration for new Durable Object class.
Client-side Presence UI
public/classroom_poc.html
Adds presence badge display (#myPresenceBadge), "👍" and "✋" buttons to toggle local presence state, WebSocket channel (presenceWs) for real-time synchronization, and updates avatar rendering to show presence badges and seat occupants. Character visibility now hides when seated and restores when standing. Presence state tracked per-user with immediate UI updates and message broadcasting.
Presence System Tests
tests/test_presence_do.py
Comprehensive test suite validating PresenceDO constructor and hibernation restore, WebSocket upgrade and authorization enforcement (including anonymous-presence toggles), welcome message completeness, delta broadcasting correctness, field update validation with bounds checking, session eviction on close, and dispatcher routing for /api/presence/<room_id> endpoints.

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)
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title directly and clearly summarizes the main change: adding a PresenceDO for realtime presence tracking and UI synchronization, which aligns with the primary objectives of the changeset across all modified files.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

📥 Commits

Reviewing files that changed from the base of the PR and between c132da2 and 09fa3e5.

📒 Files selected for processing (4)
  • public/classroom_poc.html
  • src/worker.py
  • tests/test_presence_do.py
  • wrangler.toml

Comment thread public/classroom_poc.html
Comment thread public/classroom_poc.html
Comment thread public/classroom_poc.html
Comment thread public/classroom_poc.html
Comment thread src/worker.py
Comment thread src/worker.py Outdated
Comment thread src/worker.py
Comment thread tests/test_presence_do.py
Comment thread tests/test_presence_do.py
Comment thread tests/test_presence_do.py
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@A1L13N A1L13N merged commit e877a6a into alphaonelabs:main Apr 23, 2026
2 checks passed
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.

3 participants