Skip to content

fix: prevent stale presence showing users as online after server restart#21

Merged
BuckyMcYolo merged 1 commit intomainfrom
dev
Mar 21, 2026
Merged

fix: prevent stale presence showing users as online after server restart#21
BuckyMcYolo merged 1 commit intomainfrom
dev

Conversation

@BuckyMcYolo
Copy link
Copy Markdown
Owner

@BuckyMcYolo BuckyMcYolo commented Mar 21, 2026

Adds heartbeat TTL to per-user socket sets and periodic reconciliation to clean up stale presence entries when servers crash without running disconnect handlers. Also wires up onboarding invite code/link joining and updates roadmap.

Pull Request Summary

This PR addresses stale presence entries that remain when realtime servers crash without running disconnect handlers. The solution implements a heartbeat TTL mechanism combined with periodic reconciliation.

Key Changes

Presence Service (apps/realtime/src/services/presence.ts)

  • Added PRESENCE_HEARTBEAT_TTL constant (60 seconds)
  • Extended Redis Lua script for markUserConnected to set TTL on per-user socket sets
  • Added refreshPresenceHeartbeat() to refresh TTL via Redis EXPIRE command
  • Added reconcilePresence() to periodically detect and remove stale presence entries by checking if socket-set keys exist

Realtime Server (apps/realtime/src/index.ts)

  • On successful connection, immediately calls refreshPresenceHeartbeat()
  • Starts a 30-second interval timer to continuously refresh the heartbeat TTL while socket is connected
  • Clears the interval on socket disconnect via socket.once("disconnect")
  • Bootstrap process starts a 30-second reconciliation timer to call reconcilePresence() and log results

Onboarding Integration (apps/web/src/components/onboarding/onboarding-dialog.tsx & apps/web/src/routes/_authenticated.tsx)

  • Added parseInviteCode() function to extract codes from both raw codes (alphanumeric) and full invite URLs with optional path/query/fragment
  • Updated handleJoin to parse input and navigate to /invite/$code with the extracted code
  • Updated UI copy and labels to clarify users can paste either invite links or codes
  • Added route guard to prevent onboarding dialog from showing on /invite/* routes even when onboarding is incomplete

Project Roadmap

  • Marked "Shareable invite links" as completed
  • Marked "Typing indicators" and "Pinned messages panel" as completed

Implementation Quality

Strengths:

  • Solves a real operational problem (stale presence after server crashes)
  • Clean separation of concerns with presence logic isolated in a service
  • Proper race condition handling via isCurrentSocketAlive check during initialization
  • Sound technical approach combining TTL expiry with active reconciliation
  • Good error logging context in initialization and disconnect paths
  • Invite code parsing robustly handles both URLs and raw codes

Concerns:

  • No tests: New functions (refreshPresenceHeartbeat, reconcilePresence) lack any unit or integration tests
  • Reconciliation scalability: O(n) Redis calls (one exists() per online user) on every reconciliation cycle could be problematic with thousands of concurrent users
  • Silent error handling: Heartbeat failures are silently swallowed with .catch(() => {}), potentially masking Redis connectivity issues
  • No monitoring: Only console logging for reconciliation; no metrics exposed for heartbeat failures or stale entry counts
  • Documentation gaps: New service functions lack comprehensive JSDoc comments beyond basic comments in code

Risk Assessment

The implementation is functionally correct and won't break existing code. The approach is sound, though it trades some operational overhead (additional Redis calls every 30s) for operational safety (preventing stale presence). At scale (thousands of concurrent users), the reconciliation loop could become a bottleneck. The lack of tests is a notable gap but doesn't block basic functionality.

Confidence Score: 3/5

Mostly solid with some minor issues. The core logic is sound and solves the intended problem. However, the absence of tests, potential scalability concerns with the reconciliation loop, and silent error handling in the heartbeat mechanism prevent a higher score. The implementation follows existing patterns and includes appropriate error logging, but should have unit/integration tests and monitoring instrumentation before production deployment at scale.

Adds heartbeat TTL to per-user socket sets and periodic reconciliation
to clean up stale presence entries when servers crash without running
disconnect handlers. Also wires up onboarding invite code/link joining
and updates roadmap.
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 21, 2026

Caution

Review failed

Pull request was closed or merged during review

📝 Walkthrough

Walkthrough

Presence heartbeat management system introduced with periodic TTL refresh and reconciliation logic. Invite code parsing improved to accept both URLs and raw codes. Onboarding dialog gated to exclude invite routes. ROADMAP tasks marked complete.

Changes

Cohort / File(s) Summary
Roadmap Updates
ROADMAP.md
Marked Social Features (shareable invite links) and Polish phase tasks (typing indicators, pinned messages) as completed.
Presence Heartbeat System
apps/realtime/src/index.ts, apps/realtime/src/services/presence.ts
Added presence heartbeat management with 30s refresh interval tied to socket lifecycle. Introduced PRESENCE_HEARTBEAT_TTL (60s), new refreshPresenceHeartbeat() and reconcilePresence() functions to manage user presence TTL and clean stale entries from Redis.
Onboarding Invite Flow
apps/web/src/components/onboarding/onboarding-dialog.tsx, apps/web/src/routes/_authenticated.tsx
Enhanced invite code input to parse both raw codes and invite URLs. Updated onboarding dialog visibility logic to suppress display on /invite/* routes.

Sequence Diagram

sequenceDiagram
    participant Client as Client Socket
    participant Server as Realtime Server
    participant Redis as Redis
    
    Client->>Server: Socket Connection
    Server->>Redis: markUserConnected(userId, socketId, TTL)
    Redis->>Redis: ADD socket to user set<br/>EXPIRE user set (60s)
    
    Server->>Server: Start 30s heartbeat interval
    
    loop Every 30s (while socket connected)
        Server->>Redis: refreshPresenceHeartbeat(userId)
        Redis->>Redis: EXPIRE user socket set (60s)
    end
    
    Client->>Server: Disconnect Event
    Server->>Server: Clear heartbeat interval
    
    Note over Server,Redis: Separate reconciliation loop
    Server->>Server: Every 30s: reconcilePresence()
    Server->>Redis: Get all online user IDs
    Redis-->>Server: User ID list
    loop For each user ID
        Server->>Redis: Check if socket set exists
        Redis-->>Server: Key exists? true/false
        alt Key missing (stale)
            Server->>Redis: Remove user from online set
        end
    end
    Server->>Server: Log reconciled count
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

  • BuckyMcYolo/townhall#7: Modifies the same onboarding files (onboarding-dialog.tsx and _authenticated.tsx) to add foundational onboarding dialog and routing wiring that this PR builds upon with invite-specific logic.

Poem

🐰 Heartbeats tick in Redis streams,
Thirty seconds, dreams within dreams!
Invites parsed, routes gate the flow,
Presence synced, watch stale friends go.
Tasks completed, roadmap's bright! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title accurately reflects the primary objective of preventing stale presence entries after server restart, which is the core purpose of the heartbeat and reconciliation logic additions.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch dev

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.

Tip

Migrating from UI to YAML configuration.

Use the @coderabbitai configuration command in a PR comment to get a dump of all your UI settings in YAML format. You can then edit this YAML file and upload it to the root of your repository to configure CodeRabbit programmatically.

@BuckyMcYolo BuckyMcYolo merged commit d85d061 into main Mar 21, 2026
1 check was pending
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.

1 participant