Skip to content

feat: SSE heartbeat and stale-client cleanup strategy (Closes #17) - #114

Open
laurentketterle-hub wants to merge 4 commits into
Flamki:masterfrom
laurentketterle-hub:feat/sse-heartbeat-1786475897
Open

feat: SSE heartbeat and stale-client cleanup strategy (Closes #17)#114
laurentketterle-hub wants to merge 4 commits into
Flamki:masterfrom
laurentketterle-hub:feat/sse-heartbeat-1786475897

Conversation

@laurentketterle-hub

@laurentketterle-hub laurentketterle-hub commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds SSE heartbeat and stale-client cleanup strategy per #17.

Changes

New: src/sse-heartbeat.js

  • Periodic heartbeat events every 30s with client count
  • Safe broadcast with try/catch — broken connections are detected and evicted immediately
  • Stale client detection — clients inactive for >90s (3 heartbeat cycles) are evicted
  • Client tracker — tracks write errors, evicts after 3 consecutive failures
  • Max clients — caps at 100 connections to prevent resource exhaustion
  • Stats exportgetStats() for monitoring

New: tests/sse-heartbeat.test.js

  • Unit tests for client tracker, safe broadcast, and heartbeat lifecycle

Integration

In server.js, wrap the existing SSE setup:

import { setupHeartbeat } from './sse-heartbeat.js'

const { safeBroadcast, stopHeartbeat } = setupHeartbeat(sseClients, broadcast, {
  intervalMs: 30000    // heartbeat every 30s
  // staleTimeoutMs defaults to 90000 (3 cycles)
})

// Then use safeBroadcast instead of broadcast for SSE events

Acceptance Criteria

  • Long-running sessions do not accumulate stale clients
  • Dashboard reconnects cleanly after network blips
  • Memory behavior remains stable during extended runs
  • Broadcast writes are guarded with error handling

Closes #17


Signed-off-by: laurentketterle-hub laurentketterle-hub@users.noreply.github.com

Summary by CodeRabbit

  • New Features

    • Added reliable real-time connection monitoring with periodic heartbeat events.
    • Added automatic removal of stale or unreachable connections.
    • Added safeguards to limit active connections and prevent failed broadcasts.
    • Added connection lifecycle controls and statistics for monitoring.
  • Tests

    • Added coverage for connection limits, heartbeat behavior, broadcasting, error handling, and stale connection cleanup.

- Periodic heartbeat events every 30s
- Safe broadcast with try/catch error handling
- Stale client eviction after 90s inactivity
- Client tracking with write-error thresholds
- Integration hooks for server.js

Closes Flamki#17

Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
@coderabbitai

coderabbitai Bot commented Aug 11, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e6443769-044d-4543-b9d4-1e17fd2184bd

📥 Commits

Reviewing files that changed from the base of the PR and between 8586a97 and a2a7dc0.

📒 Files selected for processing (2)
  • src/sse-heartbeat.js
  • tests/sse-heartbeat.test.js
🚧 Files skipped from review as they are similar to previous changes (2)
  • tests/sse-heartbeat.test.js
  • src/sse-heartbeat.js

📝 Walkthrough

Walkthrough

Changes

The PR adds bounded SSE client tracking, guarded event broadcasting, write-error eviction, stale-client cleanup, periodic heartbeats, lifecycle controls, logging, and connection statistics. Tests cover tracking, broadcasting, cleanup, and heartbeat control.

SSE heartbeat lifecycle

Layer / File(s) Summary
Client tracking and connection statistics
src/sse-heartbeat.js, tests/sse-heartbeat.test.js
createClientTracker() registers up to 100 clients, tracks write results, removes clients, detects stale clients, and reports statistics. Tests cover registration, capacity, removal, and error-counter behavior.
Safe event broadcasting
src/sse-heartbeat.js, tests/sse-heartbeat.test.js
safeBroadcast() serializes events, handles write failures, and removes failing clients. Tests verify event delivery and failed-client removal.
Heartbeat scheduling and stale cleanup
src/sse-heartbeat.js, tests/sse-heartbeat.test.js
setupHeartbeat() schedules heartbeat events, removes stale clients, supports timer control, and exposes statistics. Tests verify configured lifecycle behavior and shutdown.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant SSEClient
  participant setupHeartbeat
  participant createClientTracker
  participant safeBroadcast
  participant clients

  SSEClient->>createClientTracker: register connection
  createClientTracker->>clients: store client metadata
  setupHeartbeat->>safeBroadcast: broadcast heartbeat
  safeBroadcast->>SSEClient: write serialized event
  SSEClient-->>safeBroadcast: success or write error
  safeBroadcast->>createClientTracker: update errors or remove client
  setupHeartbeat->>createClientTracker: remove stale clients
  setupHeartbeat-->>setupHeartbeat: expose statistics and stop control
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes implement heartbeat events, guarded broadcasts, stale-client eviction, client limits, tracking, and tests for issue #17.
Out of Scope Changes check ✅ Passed The new heartbeat module and related tests are directly related to issue #17 and the stated pull request objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Title check ✅ Passed The title clearly identifies the SSE heartbeat and stale-client cleanup strategy, which matches the primary changes.
Description check ✅ Passed The description clearly states the purpose, changes, integration guidance, acceptance criteria, and tests, but it omits the template's Validation and Checklist sections.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
🧪 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (1)
tests/sse-heartbeat.test.js (1)

102-123: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Test the observable heartbeat lifecycle.

This test does not let the interval run. It does not verify a heartbeat event, clientCount, stale cleanup, or that stopHeartbeat() prevents later writes.

Add timer-controlled tests for one emitted heartbeat and no heartbeat after shutdown.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/sse-heartbeat.test.js` around lines 102 - 123, Expand the “starts and
stops heartbeat” coverage around setupHeartbeat to use controlled timers,
advance time until one heartbeat event is emitted, and assert the observable
event and clientCount behavior. Then call stopHeartbeat(), advance the timers
again, and verify no further heartbeat or client writes occur while retaining
the existing getStats assertions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/sse-heartbeat.js`:
- Around line 3-141: Run the configured Prettier formatter and commit the
resulting formatting changes in src/sse-heartbeat.js (lines 3-141) and
tests/sse-heartbeat.test.js (lines 13-121); no behavioral changes are required.
- Around line 171-175: Update the heartbeat and stale-client handling around
safeBroadcast, clientTracker.getStaleClients, and the response lifecycle so
heartbeat writes do not refresh client staleness. Track activity separately
using only actual client-originated signals, and remove dead responses from
tracking on both close and error events.
- Around line 122-125: Update the write-error handling around
clientTracker.markError so any thrown res.write() immediately marks the response
for eviction by adding it to deadClients on the first failure. Remove or bypass
the markError-based delay unless writeErrors is explicitly retained for separate
metrics or a non-fatal policy.
- Around line 159-161: Update the SSE request-close cleanup associated with
client registration so closing a response also removes it from clientTracker,
either by exposing and invoking removeClient(res) from the server close handler
or by wrapping the existing cleanup. Add a test covering tracker removal when
the request closes, while preserving the existing sseClients cleanup behavior.
- Around line 154-156: Update src/sse-heartbeat.js lines 154-156 in the
sseClients.push override to honor clientTracker.addClient(res): reject and close
responses at the 100-client limit before sending the initial connection event,
and only append accepted responses. Add coverage in tests/sse-heartbeat.test.js
lines 22-29 by creating 100 clients, verifying client 101 is rejected, and
asserting the response array remains capped at 100 entries.

---

Nitpick comments:
In `@tests/sse-heartbeat.test.js`:
- Around line 102-123: Expand the “starts and stops heartbeat” coverage around
setupHeartbeat to use controlled timers, advance time until one heartbeat event
is emitted, and assert the observable event and clientCount behavior. Then call
stopHeartbeat(), advance the timers again, and verify no further heartbeat or
client writes occur while retaining the existing getStats assertions.
🪄 Autofix

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: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 4727c532-f547-4aa3-a341-fa94cec1b9e6

📥 Commits

Reviewing files that changed from the base of the PR and between 6eb1d2b and 8586a97.

📒 Files selected for processing (2)
  • src/sse-heartbeat.js
  • tests/sse-heartbeat.test.js

Comment thread src/sse-heartbeat.js Outdated
Comment thread src/sse-heartbeat.js
Comment on lines +122 to +125
const shouldRemove = clientTracker ? clientTracker.markError(res) : true
if (shouldRemove) {
deadClients.push(res)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Evict a response after the first thrown write.

At Line 122, the first two thrown writes do not add the response to deadClients. The test at tests/sse-heartbeat.test.js:93-97 therefore leaves badRes in clients.

A thrown res.write() indicates a broken connection. Remove that response immediately. Keep writeErrors only if it supports a separate metric or a non-fatal failure policy.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sse-heartbeat.js` around lines 122 - 125, Update the write-error handling
around clientTracker.markError so any thrown res.write() immediately marks the
response for eviction by adding it to deadClients on the first failure. Remove
or bypass the markError-based delay unless writeErrors is explicitly retained
for separate metrics or a non-fatal policy.

Comment thread src/sse-heartbeat.js
Comment on lines +154 to +156
sseClients.push = function (res) {
clientTracker.addClient(res)
return originalPush(res)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Enforce and test the 100-client limit. addClient() can reject a response, but Line 155 ignores that result and Line 156 still appends it. The connection limit is therefore bypassed and excess responses are not tracked.

  • src/sse-heartbeat.js#L154-L156: do not append a rejected response. Reject and close the SSE request before sending its initial connection event.
  • tests/sse-heartbeat.test.js#L22-L29: add 100 clients, assert that client 101 is rejected, and assert that the response array does not exceed 100 entries.
📍 Affects 2 files
  • src/sse-heartbeat.js#L154-L156 (this comment)
  • tests/sse-heartbeat.test.js#L22-L29
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sse-heartbeat.js` around lines 154 - 156, Update src/sse-heartbeat.js
lines 154-156 in the sseClients.push override to honor
clientTracker.addClient(res): reject and close responses at the 100-client limit
before sending the initial connection event, and only append accepted responses.
Add coverage in tests/sse-heartbeat.test.js lines 22-29 by creating 100 clients,
verifying client 101 is rejected, and asserting the response array remains
capped at 100 entries.

Comment thread src/sse-heartbeat.js
Comment on lines +159 to +161
// Wrap the existing cleanup on close to also remove from tracker
// Note: server.js already has req.on('close', ...) but we also track

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Remove tracker entries when the request closes.

src/server.js:122-139 removes a closed response from sseClients only. Its Map entry remains in clientTracker. Later stale cleanup skips that entry because sseClients.indexOf(res) is -1.

Expose tracker removal during registration, or update the server close handler to call removeClient(res). Also add this lifecycle case to the tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sse-heartbeat.js` around lines 159 - 161, Update the SSE request-close
cleanup associated with client registration so closing a response also removes
it from clientTracker, either by exposing and invoking removeClient(res) from
the server close handler or by wrapping the existing cleanup. Add a test
covering tracker removal when the request closes, while preserving the existing
sseClients cleanup behavior.

Comment thread src/sse-heartbeat.js
Comment on lines +171 to +175
safeBroadcast(sseClients, clientTracker, heartbeatEvent)

// 2. Evict stale clients
const staleClients = clientTracker.getStaleClients(staleTimeoutMs)
for (const { res, entry } of staleClients) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | 🏗️ Heavy lift

Do not use heartbeat writes as stale-client activity.

The heartbeat at Line 171 updates lastWrite for every successful response before Line 174 checks staleness. A response that accepts writes can never become stale under this rule.

Track a separate lifecycle signal. For dead peers, remove responses on close and error. If client activity is required, update a separate timestamp only from an actual client-originated signal.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/sse-heartbeat.js` around lines 171 - 175, Update the heartbeat and
stale-client handling around safeBroadcast, clientTracker.getStaleClients, and
the response lifecycle so heartbeat writes do not refresh client staleness.
Track activity separately using only actual client-originated signals, and
remove dead responses from tracking on both close and error events.

Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
Signed-off-by: laurentketterle-hub <laurentketterle-hub@users.noreply.github.com>
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.

Add SSE heartbeat and stale-client cleanup strategy

1 participant