Skip to content

Conversation

@NathanFlurry
Copy link
Member

No description provided.

@vercel
Copy link

vercel bot commented Oct 22, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

3 Skipped Deployments
Project Deployment Preview Comments Updated (UTC)
rivet-cloud Ignored Ignored Oct 24, 2025 8:41pm
rivet-inspector Ignored Ignored Preview Oct 24, 2025 8:41pm
rivet-site Ignored Ignored Preview Oct 24, 2025 8:41pm

Copy link
Member Author

NathanFlurry commented Oct 22, 2025


How to use the Graphite Merge Queue

Add the label merge-queue to this PR to add it to the merge queue.

You must have a Graphite account in order to use the merge queue. Sign up using this link.

An organization admin has enabled the Graphite Merge Queue in this repository.

Please do not merge from GitHub as this will restart CI on PRs being processed by the merge queue.

This stack of pull requests is managed by Graphite. Learn more about stacking.

@claude
Copy link

claude bot commented Oct 22, 2025

PR Review: Cursors with Raw WebSockets Example

Thank you for this example! This is a great demonstration of real-time collaborative features using RivetKit. Here's my detailed review:

✅ Strengths

  1. Clean Architecture: Well-structured separation between backend (actor) and frontend (React)
  2. Good UX: Color-coded cursors, typing indicators, and multi-room support
  3. Type Safety: Proper TypeScript interfaces shared between frontend and backend
  4. Basic Test Coverage: Tests verify actor creation and initial state

🐛 Potential Bugs & Issues

Critical Issues

  1. Race Condition on Room Change (App.tsx:191)

    • When roomId changes, the useEffect cleanup closes the WebSocket, but there's no debouncing
    • If a user rapidly types in the room input, multiple connections may be created/destroyed
    • Recommendation: Add debouncing or disable room input while connecting
  2. Cursor Keying Mismatch (registry.ts:48 vs App.tsx:128)

    • Backend keys cursors by sessionId but frontend keys by userId
    • This causes cursor duplication when the same user has multiple sessions
    • Fix: Line 128 in App.tsx should be:
    [sessionId]: message.data,  // not [message.data.userId]
  3. Memory Leak Risk (registry.ts:93)

    • WebSocket event listeners are never removed
    • If a socket is somehow retained after close, listeners will leak
    • Recommendation: Store event handler references and remove them in the close handler

Moderate Issues

  1. Missing Error Handling (App.tsx:87)

    • If getOrCreate() fails, the error is only logged - no user feedback
    • Recommendation: Add error state and display connection errors to users
  2. Text Label Race Condition (registry.ts:137-144)

    • Array mutation without proper locking could cause issues under high concurrency
    • While JavaScript is single-threaded, if the actor system uses workers, this could be problematic
    • Recommendation: Verify actor state mutation semantics in RivetKit
  3. Unchecked Message Schema (registry.ts:95, App.tsx:114)

    • No runtime validation of incoming WebSocket messages
    • Malformed messages could cause crashes
    • Recommendation: Add schema validation (e.g., with Zod) or at minimum null checks

Minor Issues

  1. Hardcoded URL (App.tsx:9, 91)

    • http://localhost:6420 is hardcoded in two places
    • Recommendation: Use environment variables
  2. Console Logging in Production Code (registry.ts:68-70, App.tsx:88, 101, etc.)

    • Multiple console.log statements that should use proper logging
    • Recommendation: Use a structured logger or at minimum add log levels

🔒 Security Concerns

  1. No Input Sanitization (registry.ts:126)

    • User-provided text is stored and broadcast without sanitization
    • Could enable XSS if text is rendered as HTML anywhere
    • Current Status: Safe because React escapes by default, but should document this assumption
  2. No Rate Limiting

    • Users can spam cursor updates or text changes
    • Recommendation: Add throttling on the backend (e.g., max 60 cursor updates/second per session)
  3. No Message Size Limits

    • Text labels have no length restrictions
    • Users could send very large messages
    • Recommendation: Add validation (e.g., max 500 characters for text)
  4. Session ID Security (App.tsx:16-17)

    • SessionId and userId use Math.random() which is not cryptographically secure
    • For a demo this is fine, but should use crypto.randomUUID() for production
    • Recommendation: Add comment explaining this is demo-only

⚡ Performance Considerations

  1. Cursor Update Frequency (App.tsx:205)

    • Every mouse move sends a WebSocket message
    • This can be 100+ messages per second
    • Recommendation: Throttle cursor updates (e.g., max 30fps)
  2. Broadcast to All (registry.ts:114-121)

    • Every cursor update is broadcast to all connections including sender
    • Sender already knows their own position
    • Optimization: Skip sending to the originating session
  3. Text Update on Every Keystroke (App.tsx:232)

    • Each keystroke broadcasts to all users
    • For longer text, this is wasteful
    • Recommendation: Debounce text updates (e.g., 100ms) or only broadcast on Enter
  4. No Cursor Cleanup Timeout

    • Stale cursors from disconnected users persist until reconnection
    • Recommendation: Add timestamp checking and remove cursors older than 30 seconds

🧪 Test Coverage

Existing Tests

  • ✅ Basic actor creation
  • ✅ Initial state retrieval

Missing Test Coverage

  1. WebSocket Functionality

    • No tests for WebSocket connection, message handling, or disconnection
    • Recommendation: Add integration tests for cursor broadcasts and disconnection cleanup
  2. Text Label Management

    • No tests for text CRUD operations
    • Recommendation: Test text creation, update, and deletion
  3. Edge Cases

    • Missing sessionId handling (line 63-66 is good, but untested)
    • Malformed message handling
    • Maximum text labels per room
  4. Frontend Tests

    • No frontend tests at all
    • Recommendation: Add basic React Testing Library tests for user interactions

📝 Code Quality & Best Practices

Good Practices

  • ✅ TypeScript throughout
  • ✅ Functional React with hooks
  • ✅ Proper cleanup in useEffect
  • ✅ Shared types between frontend/backend

Improvements Needed

  1. README Path Error (README.md:19)

    • Says cd rivetkit/examples/cursors but should be examples/cursors-raw-websocket
  2. Inconsistent Logging Format

    • Some logs use spaces around =, some don't
    • Recommendation: Use structured logging with consistent format
  3. Magic Numbers

    • 1008 close code (line 64) should be a named constant
    • Canvas dimensions (40-41) are fine as constants
  4. TypeScript Any (tests/cursors.test.ts:5, 15)

    • ctx: any should be properly typed
    • Recommendation: Import proper test context type from vitest

📋 Summary & Recommendations

Priority Fixes

  1. ⚠️ Fix cursor keying bug (App.tsx:128) - Use sessionId not userId
  2. ⚠️ Add throttling to cursor updates - Prevent performance issues
  3. ⚠️ Add input validation - Max text length and message validation

Nice to Have

  1. Add debouncing to room changes
  2. Environment variable for backend URL
  3. Improve error handling and user feedback
  4. Expand test coverage

Overall Assessment

This is a well-structured example that demonstrates RivetKit's capabilities effectively. The code is clean and readable. The main issues are around production-readiness (rate limiting, validation, error handling) which are expected for an example. The cursor keying bug should be fixed as it affects functionality.

Recommendation: ✅ Approve with minor changes requested (fix cursor keying bug)

Great work on this example! 🚀

@pkg-pr-new
Copy link

pkg-pr-new bot commented Oct 22, 2025

More templates

@rivetkit/actor

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/actor@3245

@rivetkit/cloudflare-workers

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/cloudflare-workers@3245

@rivetkit/core

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/core@3245

@rivetkit/db

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/db@3245

@rivetkit/framework-base

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/framework-base@3245

@rivetkit/next-js

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/next-js@3245

@rivetkit/react

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/react@3245

rivetkit

pnpm add https://pkg.pr.new/rivet-dev/rivet/rivetkit@3245

@rivetkit/sql-loader

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/sql-loader@3245

@rivetkit/engine-runner

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner@3245

@rivetkit/engine-runner-protocol

pnpm add https://pkg.pr.new/rivet-dev/rivet/@rivetkit/engine-runner-protocol@3245

commit: 86f2ec4

@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples_cursors_with_raw_websockets_example branch from 86f2ec4 to aa8205e Compare October 23, 2025 07:34
@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples-cursors_cursors_example branch from 8766cdd to eb78d9e Compare October 23, 2025 07:34
@claude
Copy link

claude bot commented Oct 23, 2025

Code Review: Cursors with Raw WebSockets Example

Great work on this example! This is a clean demonstration of real-time collaborative features with RivetKit. Below is my detailed feedback:


Strengths

  1. Clean Architecture: Good separation between backend actor logic and frontend React components
  2. Type Safety: Proper TypeScript interfaces for CursorPosition and TextLabel
  3. User Experience: Thoughtful UX with color-coded cursors, typing indicators, and keyboard shortcuts
  4. Responsive Design: Canvas scaling implementation is well done

🐛 Bugs and Issues

Critical: Cursor Key Mismatch (registry.ts:48, App.tsx:127)

The backend uses sessionId as the cursor key, but the frontend uses userId:

Backend:

cursors[sessionId] = cursor;  // Line 48

Frontend:

[message.data.userId]: message.data  // Line 127

This causes cursors to be keyed inconsistently. The backend should use userId consistently, or clarify the distinction between session and user.

Recommendation: Use userId as the key throughout, since multiple sessions from the same user should probably share cursor state.


Bug: Cursor Removal Uses Wrong Key (registry.ts:189, 193)

When removing a cursor on disconnect, the backend sends session.cursor (which contains userId) but tries to identify it by userId in the removal message. However, the check on line 187-188 compares by sessionId:

if (id !== sessionId) {  // Line 188

This should be consistent with how cursors are keyed.


Bug: Cursor Identified Incorrectly (App.tsx:374)

const isOwnCursor = id === userId;

The id here is from Object.entries(cursors), which would be userId (from the message), but this comparison won't work correctly because the current user's cursor is keyed by their own userId in state but displayed alongside others.

This logic seems correct if cursors are properly keyed by userId, but given the session/user mismatch mentioned above, this might not work as intended.


⚠️ Security Concerns

Input Validation Missing (registry.ts:99, 126, 159)

The backend doesn't validate incoming WebSocket messages:

case "updateCursor": {
    const { userId, x, y } = message.data;  // No validation
    const cursor: CursorPosition = { userId, x, y, timestamp: Date.now() };

Recommendations:

  • Validate that x and y are numbers within reasonable bounds (e.g., 0-CANVAS_WIDTH, 0-CANVAS_HEIGHT)
  • Validate that text length doesn't exceed a reasonable limit (prevent DoS)
  • Validate that userId matches expected format
  • Consider rate limiting cursor updates per session

XSS Risk (App.tsx:337)

Text labels are rendered directly without sanitization:

{label.text}

While React's JSX escapes by default (which is good), the README says "Persistent text labels across sessions" but there's no explicit validation. Consider documenting that React handles this, or add explicit length limits.


🚀 Performance Considerations

Broadcast to All Including Sender (registry.ts:113-121)

// Broadcast to all connections (including sender)
for (const { socket } of c.vars.websockets.values()) {

Broadcasting cursor updates back to the sender is wasteful. The sender already knows their cursor position.

Recommendation: Skip the sender:

for (const [id, { socket }] of c.vars.websockets.entries()) {
    if (id !== sessionId) {
        socket.send(/* ... */);
    }
}

Inefficient Text Label Update (App.tsx:133-144)

Using findIndex and array copy on every text update:

const existingIndex = prev.findIndex((l) => l.id === message.data.id);

For large numbers of labels, this is O(n). Consider using a Map keyed by id for O(1) lookups, or at least use an object lookup.


Mouse Move Event Throttling (App.tsx:205-215)

Every mouse move sends a WebSocket message. This could flood the connection with hundreds of messages per second.

Recommendation: Throttle or debounce cursor updates to ~60fps max:

const throttledSendCursor = useCallback(
    throttle((x: number, y: number) => {
        wsRef.current?.send(JSON.stringify({ type: "updateCursor", data: { userId, x, y } }));
    }, 16), // ~60fps
    [userId]
);

📝 Code Quality

Magic Numbers (App.tsx:40-41)

const CANVAS_WIDTH = 1920;
const CANVAS_HEIGHT = 1080;

These could be configurable or at least documented why these specific values were chosen.


Inconsistent Error Handling (registry.ts:176-178, App.tsx:164-166)

Error handling uses console.error but doesn't implement any recovery or user notification strategy.

Recommendation:

  • Log structured errors with context (session ID, actor ID, message type)
  • Consider implementing exponential backoff for WebSocket reconnection on the frontend

README Inaccuracy (README.md:19)

cd rivetkit/examples/cursors

Should be:

cd rivetkit/examples/cursors-raw-websocket

🧪 Test Coverage

The tests only cover basic action calls but don't test:

  • WebSocket message handling
  • Cursor state management
  • Text label persistence
  • Connection cleanup
  • Error cases (malformed messages, missing sessionId, etc.)

Recommendation: Add tests for:

test("should handle cursor updates via WebSocket", async () => { ... });
test("should remove cursor on disconnect", async () => { ... });
test("should reject connection without sessionId", async () => { ... });
test("should validate cursor coordinates", async () => { ... });

🎯 Best Practices

Hardcoded URL (App.tsx:91)

const rivetUrl = "http://localhost:6420";

Should use environment variable or configuration:

const rivetUrl = import.meta.env.VITE_RIVET_URL || "http://localhost:6420";

Potential Memory Leak (App.tsx:76)

The resize event listener is properly cleaned up, but the WebSocket event listeners inside the connect function aren't explicitly cleaned up. While the WebSocket close will clean them up, it's better to be explicit.


📚 Documentation

  • Add JSDoc comments to the actor definition and main functions
  • Document the message protocol (types, expected fields)
  • Explain the sessionId vs userId distinction
  • Add architecture diagram showing client-actor-websocket flow

Suggestions

  1. Add a "Clear Canvas" button to remove all text labels
  2. Add user disconnect indicators (show when someone leaves)
  3. Consider adding cursor trail effects
  4. Add unit conversion helpers (screen to canvas coordinates could be extracted to a utility)
  5. Add visual feedback when WebSocket disconnects/reconnects

🎓 Summary

This is a solid example that demonstrates RivetKit's WebSocket capabilities well. The main issues to address are:

  1. Fix the cursor key mismatch between backend and frontend
  2. Add input validation on the backend
  3. Throttle cursor updates to prevent flooding
  4. Fix the README path
  5. Improve test coverage

The code is clean and well-structured overall. Once the critical bugs are fixed and validation is added, this will be an excellent example for the community!

@claude
Copy link

claude bot commented Oct 23, 2025

Code Review: Cursors with Raw WebSockets Example

Thank you for this example! It demonstrates real-time collaborative cursor tracking with RivetKit. Here's my detailed review:

🎯 Overall Assessment

This is a well-structured example that effectively demonstrates WebSocket usage with actors. The code is clean and demonstrates good practices for real-time collaborative features.


🐛 Potential Bugs & Issues

1. Backend: Missing state persistence call (registry.ts:141-144)

When updating or adding text labels, the state is modified but ctx.saveState({}) is not called. Compare with the similar example at examples/raw-websocket-handler-proxy/src/backend/registry.ts:42 which calls saveState after state mutations.

Issue: Text labels may not persist across actor restarts.

Fix: Add c.saveState({}) after state mutations:

if (existingIndex >= 0) {
    c.state.textLabels[existingIndex] = textLabel;
} else {
    c.state.textLabels.push(textLabel);
}
c.saveState({}); // Add this line

Also needed after line 162 for removeText.

2. Backend: Potential memory leak with dead WebSocket connections (registry.ts:114-121)

The broadcast loop doesn't check socket readyState before sending. Dead sockets may accumulate in the Map.

Recommendation: Check socket state before sending:

for (const { socket } of c.vars.websockets.values()) {
    if (socket.readyState === 1) { // 1 = OPEN
        socket.send(JSON.stringify({
            type: "cursorMoved",
            data: cursor,
        }));
    }
}

3. Frontend: Cursor key mismatch (App.tsx:125-128, 372-413)

In line 128, cursors are keyed by message.data.userId, but the cursor object contains its own userId. When the same user opens multiple sessions, they'll overwrite each other.

Looking at the backend (registry.ts:46-49), cursors are keyed by sessionId, not userId. This mismatch causes the frontend to incorrectly merge cursors from different sessions of the same user.

Fix: Key cursors by a unique identifier that includes the session:

case "cursorMoved": {
    // Backend should send sessionId with the cursor data
    setCursors((prev) => ({
        ...prev,
        [message.data.sessionId]: message.data, 
    }));
    break;
}

The backend needs to include sessionId in broadcast messages.

4. Frontend: Missing error boundary (App.tsx)

React component has no error boundary for WebSocket or parsing errors. Errors in message handlers could crash the component.

5. Frontend: Race condition on room change (App.tsx:81-191)

When roomId changes, the old WebSocket connection closes but there's a brief moment where both connections might be active. State updates from the old connection could interfere.

Recommendation: Add connection ID tracking or ignore messages from stale connections.


🔒 Security Concerns

1. Input validation missing

  • registry.ts:99, 126, 159: No validation of incoming message data fields (x, y, text, id, userId)
  • Text content is not sanitized or length-limited
  • Coordinates are not bounded
  • userId/sessionId can be arbitrary strings

Recommendations:

case "updateText": {
    const { id, userId, text, x, y } = message.data;
    
    // Validate inputs
    if (!id || !userId || typeof text !== 'string' || 
        typeof x !== 'number' || typeof y !== 'number') {
        return; // or send error
    }
    
    // Bounds checking
    if (x < 0 || x > 1920 || y < 0 || y > 1080) {
        return;
    }
    
    // Length limits
    if (text.length > 500) {
        return;
    }
    
    // ...rest of the logic
}

2. No rate limiting

A malicious client could flood the server with cursor updates or text changes, causing:

  • Excessive broadcasts to all clients
  • Memory exhaustion from text labels
  • CPU exhaustion from JSON parsing

Recommendation: Implement rate limiting per session or use RivetKit's built-in throttling if available.

3. XSS vulnerability in frontend (App.tsx:337-338)

Text labels are rendered directly without sanitization. While React escapes by default, the text content should still be explicitly validated.

4. No authentication

Anyone can connect and modify room state. For a demo this is acceptable, but production use would need proper auth.


🚀 Performance Considerations

1. Excessive broadcasts (registry.ts:113-121)

Every cursor movement broadcasts to all clients including the sender. For a room with N users, each movement generates N messages.

Optimization: Skip sending back to the originating session:

for (const [id, { socket }] of c.vars.websockets.entries()) {
    if (id !== sessionId && socket.readyState === 1) {
        socket.send(JSON.stringify({
            type: "cursorMoved",
            data: cursor,
        }));
    }
}

2. Mouse movement spam (App.tsx:205-215)

Every mousemove event sends a WebSocket message. This could be hundreds per second.

Recommendation: Throttle cursor updates:

const throttledSendCursor = useCallback(
    throttle((x: number, y: number) => {
        if (wsRef.current?.readyState === WebSocket.OPEN) {
            wsRef.current.send(JSON.stringify({
                type: "updateCursor",
                data: { userId, x, y },
            }));
        }
    }, 50), // Send at most every 50ms
    [userId]
);

3. Re-rendering all cursors on every update (App.tsx:372-413)

Each cursor movement causes all cursor components to re-render.

Optimization: Memoize cursor components:

const CursorComponent = React.memo(({ cursor, isOwnCursor }) => {
    // ...cursor rendering logic
});

4. No cleanup of stale cursors

Cursors remain in state forever. If a client disconnects ungracefully, their cursor stays.

Recommendation: Add timestamp-based cleanup or rely on WebSocket close events (which you already handle, but could fail).

5. Text labels grow unbounded (registry.ts:28)

No limit on number of text labels. Memory will grow indefinitely.

Recommendation: Add a limit like the chat example does (lines 44-47 in raw-websocket-handler-proxy example).


✅ Code Quality & Best Practices

Strengths:

  • Clean separation of concerns (backend/frontend)
  • Good use of TypeScript types
  • Clear variable naming
  • Proper React hooks usage
  • Well-structured WebSocket message handling

Areas for Improvement:

1. Inconsistent error handling

  • Backend logs to console (line 177)
  • Frontend logs to console (lines 165, 175, 179)
  • No error reporting to users

2. Magic numbers

  • 1008 (line 64) - use WebSocket close code constants
  • 1920, 1080 (lines 40-41) - could be configurable
  • Hard-coded colors (lines 20-28)

3. Mixed concerns in frontend

The App component handles:

  • WebSocket connection management
  • State management
  • Rendering
  • Coordinate transformation

Recommendation: Extract WebSocket logic into a custom hook:

function useRoomConnection(roomId: string, sessionId: string) {
    // WebSocket connection logic
    return { cursors, textLabels, sendCursor, sendText, connected };
}

4. Unused timestamp fields

CursorPosition.timestamp and TextLabel.timestamp are set but never used. Either use them (e.g., for stale data cleanup) or remove them.

5. Missing JSDoc comments

Public interfaces and actions should have documentation explaining their purpose and parameters.


🧪 Test Coverage

Current Coverage:

  • ✅ Actor creation
  • ✅ Initial state retrieval

Missing Tests:

  • WebSocket connection handling
  • Message broadcasting
  • State mutations (text add/update/remove)
  • Cursor tracking
  • Connection cleanup on disconnect
  • Error cases (invalid messages, missing sessionId)
  • Concurrent user scenarios
  • Edge cases (empty text, out-of-bounds coordinates)

Recommendation: Add WebSocket integration tests:

test("broadcasts cursor updates to all connected clients", async (ctx: any) => {
    const { client } = await setupTest(ctx, registry);
    const room = client.cursorRoom.getOrCreate(["test-broadcast"]);
    
    // Create multiple WebSocket connections
    // Send cursor update from one
    // Verify all others receive it
});

📝 Documentation

README Issues:

  1. Line 19: Path is incorrect - should be cursors-raw-websocket not cursors
  2. Missing prerequisites section explaining RivetKit setup
  3. No troubleshooting section
  4. Could benefit from architecture diagram showing actor/WebSocket flow

Code Comments:

  • Good high-level comments
  • Could use more inline comments for complex logic (e.g., coordinate transformation)

🎨 Minor Issues

  1. registry.ts:68-70: Inconsistent logging style (template literals vs console.log formatting)
  2. App.tsx:374: Variable isOwnCursor is computed but the check seems wrong (should compare sessionId not userId)
  3. .gitignore:2: Missing newline at end of file
  4. package.json: Dependencies using workspace:* which is correct for monorepo

✨ Recommendations Summary

Critical (Fix before merge):

  1. Add saveState() calls after state mutations
  2. Fix cursor key mismatch (sessionId vs userId)
  3. Add input validation for security

High Priority:

  1. Check WebSocket readyState before sending
  2. Add rate limiting or throttling
  3. Implement bounds checking for coordinates and text length
  4. Fix broadcast to exclude sender

Nice to Have:

  1. Extract WebSocket logic to custom hook
  2. Add comprehensive tests
  3. Implement cursor throttling
  4. Add error boundaries
  5. Memoize components for performance
  6. Add limit to text labels

🏁 Conclusion

This is a solid example that demonstrates RivetKit's WebSocket capabilities well. The main concerns are:

  1. Missing state persistence calls
  2. Security validation gaps
  3. Potential performance issues with broadcasts and rendering

With the critical fixes applied, this would be an excellent reference example. Great work overall! 🚀

@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples_cursors_with_raw_websockets_example branch from aa8205e to a27327f Compare October 23, 2025 08:22
@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples-cursors_cursors_example branch from eb78d9e to 23bc2c4 Compare October 23, 2025 08:22
@claude
Copy link

claude bot commented Oct 23, 2025

Pull Request Review: Cursors with Raw WebSockets Example

Thank you for this example! This is a well-implemented demonstration of real-time collaborative cursors using RivetKit. Here's my detailed review:

✅ Strengths

  1. Clean Architecture: Good separation between backend actor logic and frontend React code
  2. TypeScript Usage: Excellent type safety throughout with proper interfaces
  3. Good UX: The color-coded cursors, typing indicators, and room support create a polished experience
  4. Proper Cleanup: WebSocket cleanup in useEffect and event listener cleanup are handled correctly

🐛 Potential Bugs & Issues

Backend (src/backend/registry.ts)

  1. Critical: Cursor keying mismatch (Line 48, 121, 152)

    • Cursors are keyed by sessionId in the Map but by userId when broadcasting
    • Line 48: cursors[sessionId] = cursor;
    • Line 121: The client receives cursors keyed by userId: [message.data.userId]: message.data
    • Impact: Multiple sessions from same user will overwrite each other in the frontend
    • Fix: Decide on a consistent key - likely sessionId for both, since each browser tab should have its own cursor
  2. Race condition in message handling (Line 93)

    • The message event listener is not async but performs synchronous operations that could be slow
    • If messages arrive rapidly, state updates could interleave incorrectly
    • Suggestion: Consider debouncing cursor updates or using a queue
  3. Missing input validation (Lines 99, 126, 159)

    • No validation on x, y coordinates (could be NaN, Infinity, or out of bounds)
    • No validation on text length (potential for extremely large payloads)
    • No validation on userId format
    • Security concern: Malicious clients could send invalid data
    • Suggestion: Add validation:
    if (typeof x !== 'number' || typeof y !== 'number' || !isFinite(x) || !isFinite(y)) {
        return; // or close socket
    }
    if (text && text.length > 1000) { // reasonable limit
        return;
    }
  4. Potential memory leak (Line 28)

    • textLabels array grows unbounded - no cleanup mechanism
    • Consider adding a max labels per room limit or TTL

Frontend (src/frontend/App.tsx)

  1. Cursor position keying inconsistency (Line 121, 368)

    • Line 121: Cursors stored by userId: [message.data.userId]
    • Line 368: Checking if own cursor using id === userId where id is the key from cursors object
    • But cursors should be keyed by sessionId (see backend issue [SVC-2555] Set up issue templates #1)
    • Impact: "you" label won't show correctly if backend is fixed
  2. Missing WebSocket reconnection logic (Line 83)

    • If connection drops, no automatic reconnect
    • User must manually change rooms or refresh page
    • Suggestion: Add reconnection with exponential backoff
  3. Race condition on room change (Line 185)

    • useEffect depends on [roomId, sessionId]
    • If roomId changes while a connection is active, the old WebSocket closes but messages could still arrive
    • Suggestion: Add cleanup check using a ref or abort controller
  4. Stale closure in event listeners (Lines 93, 182)

    • WebSocket event listeners capture c.vars and state at creation time
    • If actor state changes externally, listeners won't see updates
    • This is likely acceptable for this use case but worth noting
  5. Missing error boundary (App.tsx)

    • No React error boundary to catch rendering errors
    • Suggestion: Add error boundary for better UX
  6. Text ID collision potential (Line 216)

    • ${userId}-${Date.now()} could collide if user clicks twice in same millisecond
    • Low probability but possible
    • Suggestion: Use crypto.randomUUID() or add a counter

Tests (tests/cursors.test.ts)

  1. Insufficient test coverage

    • Only tests actor creation and initial state
    • Missing tests for:
      • WebSocket message handling
      • Cursor updates and broadcasts
      • Text label CRUD operations
      • Edge cases (invalid data, disconnections)
      • Concurrent users
    • Suggestion: Add integration tests for WebSocket behavior
  2. Incorrect test context type (Line 5, 14)

    • Using ctx: any loses type safety
    • Suggestion: Import proper type from vitest

🚀 Performance Considerations

  1. Cursor update throttling (Line 199)

    • Every mouse move sends a WebSocket message
    • Could send 100+ messages per second
    • Suggestion: Throttle to ~30-60 updates/sec:
    const throttledUpdateCursor = useThrottle(handleMouseMove, 16); // ~60fps
  2. Broadcast optimization (Lines 114, 147, 165)

    • Currently broadcasting to all connections including sender
    • For cursor updates, sender doesn't need their own update
    • Minor optimization: Skip sender in broadcasts
  3. Re-renders on cursor updates (Line 366)

    • Every cursor move triggers re-render of all cursor elements
    • Suggestion: Memoize cursor components or use React.memo()
  4. JSON stringify/parse overhead

    • Every message is stringified/parsed
    • For high-frequency updates like cursors, consider binary protocol
    • Optional optimization for production use

🔒 Security Concerns

  1. No authentication/authorization

    • Anyone can join any room
    • No validation of sessionId or userId
    • Note: This is acceptable for an example, but document this for production use
  2. XSS potential (Line 331)

    • Text labels render user input directly
    • React escapes by default, so this is safe
    • Note: If switching to dangerouslySetInnerHTML, sanitize input
  3. CORS configuration (src/backend/server.ts:4-6)

    • Hardcoded to localhost:5173
    • Note: Document this needs updating for production
  4. No rate limiting

    • Malicious user could spam messages
    • Suggestion: Add rate limiting per session/IP

📝 Code Quality & Best Practices

  1. Console.log statements (Lines 68, 90, 95, 102, 164, 183)

    • Using console.log instead of proper logging
    • Suggestion: For production code, use structured logging (matches CLAUDE.md guidance)
  2. Magic numbers (Lines 42-43)

    • CANVAS_WIDTH = 1920 and CANVAS_HEIGHT = 1080 are hardcoded
    • Consider making configurable or document the rationale
  3. Missing JSDoc comments

    • Public interfaces and functions lack documentation
    • Helpful for other developers
  4. README path inconsistency (Line 19)

    • Says cd rivetkit/examples/cursors but folder is cursors-raw-websocket
    • Fix: Update to cursors-raw-websocket

✨ Suggestions for Enhancement

  1. Add cursor smoothing/interpolation for better visual experience
  2. Add undo/redo for text labels
  3. Add ability to drag/edit existing text
  4. Show "user is typing" indicators
  5. Add presence indicators showing who's in the room
  6. Persist state to database for true persistence across server restarts

📊 Summary

Severity Breakdown:

  • 🔴 Critical: 1 (cursor keying mismatch)
  • 🟡 Medium: 6 (validation, reconnection, memory leak, test coverage)
  • 🟢 Low: 7 (optimizations, documentation)

Recommendation: Request changes to fix the critical cursor keying issue and add input validation before merging. The other issues can be addressed in follow-up PRs or documented as known limitations for an example project.

Overall, this is a solid example that demonstrates RivetKit's capabilities well. With the suggested fixes, it will be an excellent learning resource for users.

@claude
Copy link

claude bot commented Oct 23, 2025

Code Review: Cursors Raw WebSocket Example

This PR adds a new example demonstrating real-time collaborative cursors using raw WebSocket connections instead of RivetKit's higher-level RPC actions. Overall, this is a well-structured example that demonstrates advanced WebSocket usage. Here's my detailed feedback:


✅ Strengths

  1. Clear demonstration of raw WebSocket usage - This example effectively shows how to use RivetKit's onWebSocket handler for lower-level control
  2. Good code organization - Clean separation between frontend and backend logic
  3. Proper state management - Uses both persistent state (textLabels) and ephemeral vars (websockets)
  4. Helpful documentation - README provides clear setup instructions
  5. TypeScript support - Full type safety throughout the example

🐛 Potential Issues

1. Cursor ID Mismatch Bug (registry.ts:46-48, App.tsx:118-122)

There's a critical bug in cursor tracking. The backend uses sessionId as the key:

// registry.ts:46
cursors[sessionId] = cursor;

But the frontend uses userId as the key:

// App.tsx:121
[message.data.userId]: message.data

Impact: Cursors won't be correctly tracked/removed, leading to stale cursor states.

Fix: Use consistent keys. Since the cursor removal logic (registry.ts:187-196) broadcasts cursor.userId, the backend should also use userId as the key or include sessionId in the cursor data.

2. Memory Leak in WebSocket Event Listeners (registry.ts:93-179)

Event listeners are added to WebSocket but never explicitly removed. While this may not cause issues in practice (since listeners are scoped to the connection lifetime), it's better to be explicit.

Recommendation: Store event listener cleanup functions if needed, though this is a minor concern since the WebSocket lifecycle is managed by the actor.

3. Race Condition in Initial State (registry.ts:75-82)

The initial state sent to new connections includes cursors from all existing connections, but doesn't account for connections that haven't sent a cursor position yet. A new connection gets cursor: null initially, which could lead to the initial state snapshot not matching reality.

Impact: Low - just means the initial state might be slightly stale until first cursor movement.

4. Input Validation Missing (registry.ts:99-173)

The WebSocket message handlers don't validate the incoming data structure. Missing or malformed fields could cause runtime errors.

Recommendation: Add validation for required fields:

case "updateCursor": {
    const { userId, x, y } = message.data;
    if (!userId || typeof x !== 'number' || typeof y !== 'number') {
        console.error('invalid cursor data:', message.data);
        break;
    }
    // ... rest of logic
}

⚡ Performance Considerations

1. Cursor Update Frequency (App.tsx:199-209)

Every mouse movement triggers a WebSocket send. This could generate a lot of messages with fast mouse movements.

Recommendation: Consider throttling cursor updates (e.g., max 60 updates/second):

const throttledMouseMove = useCallback(
    throttle((x: number, y: number) => {
        if (wsRef.current?.readyState === WebSocket.OPEN) {
            wsRef.current.send(JSON.stringify({ type: 'updateCursor', data: { userId, x, y }}));
        }
    }, 16), // ~60fps
    [userId]
);

2. Broadcasting to All Including Sender (registry.ts:113-121)

The cursor update broadcasts to all connections including the sender. The sender already knows their own cursor position, so this creates unnecessary network traffic.

Recommendation: Either filter out the sender or document why this is intentional (perhaps for server-authoritative position?).


🔒 Security Concerns

1. No Input Sanitization (registry.ts:126, 159)

Text content and IDs are not sanitized before being stored and broadcast. While this is an example app, it's worth noting for production use.

Recommendation: Add a comment noting that production apps should sanitize text input to prevent XSS or add basic sanitization as a best practice demonstration.

2. No Rate Limiting

A malicious client could spam cursor updates or text updates, potentially overwhelming the server or other clients.

Recommendation: For a production example, consider demonstrating basic rate limiting or add a comment about it.

3. CORS Configuration (server.ts:4-6)

The CORS origin is hardcoded to localhost. This is fine for an example, but worth documenting.


📝 Code Quality & Best Practices

1. Inconsistent Logging Style (registry.ts:68-70, 183)

The logging doesn't follow the structured logging pattern mentioned in CLAUDE.md. Per the guidelines, logs should use structured logging, not string interpolation.

Should be:

console.log('websocket connected', { sessionId, actorId: c.actorId });

2. Error Handling Could Be More Specific (registry.ts:176-178, App.tsx:158-160)

Generic catch (error) blocks don't provide much context. Consider logging more details or using structured error handling.

3. Magic Values (App.tsx:216)

The text ID generation uses a pattern ${userId}-${Date.now()} which could theoretically collide if a user creates multiple texts in the same millisecond (unlikely but possible).

Recommendation: Use crypto.randomUUID() or add a counter.

4. README Path Issue (README.md:18)

README says cd rivetkit/examples/cursors but the example is in cursors-raw-websocket.


🧪 Test Coverage

Tests Are Too Basic (cursors.test.ts)

The tests only check that actions can be called, but don't test:

  • WebSocket connection handling
  • Message broadcasting
  • State updates
  • Cleanup on disconnect
  • Multiple concurrent connections

Recommendation: Add tests for WebSocket behavior, especially the critical cursor tracking and broadcast logic.


📚 Documentation

1. Missing Architecture Explanation

The README doesn't explain why someone would use raw WebSockets instead of the RPC-based approach from the regular cursors example.

Recommendation: Add a section explaining:

  • When to use raw WebSockets vs RPC actions
  • Tradeoffs between approaches
  • Performance characteristics

2. Comparison with Regular Cursors Example

Since there's already a cursors example, it would be helpful to explain what's different about this one.


🎯 Suggested Improvements

  1. Fix the cursor ID mismatch bug (critical)
  2. Add input validation to WebSocket message handlers
  3. Fix README path
  4. Add throttling to cursor updates or document why it's not needed
  5. Improve test coverage to test WebSocket functionality
  6. Use structured logging per CLAUDE.md guidelines
  7. Add documentation comparing this approach to the RPC-based example
  8. Consider adding comments about production considerations (rate limiting, input sanitization)

Summary

This is a solid example that demonstrates raw WebSocket usage well. The main issues are:

  • Critical: Cursor ID mismatch bug needs fixing
  • Important: Add input validation and improve tests
  • Nice to have: Better documentation explaining when to use this approach

The code is generally clean and well-structured. With the bug fix and some improvements to documentation and testing, this will be an excellent addition to the examples!

@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples-cursors_cursors_example branch from 23bc2c4 to db3c3ad Compare October 23, 2025 08:37
@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples_cursors_with_raw_websockets_example branch from a27327f to 0ffdb8b Compare October 23, 2025 08:37
@claude
Copy link

claude bot commented Oct 23, 2025

Pull Request Review: Cursors with Raw WebSockets Example

Thanks for adding this comprehensive example! This demonstrates raw WebSocket usage with RivetKit actors nicely. Below is my review organized by category:

Code Quality & Best Practices

Positive Observations

  • Clean architecture: Good separation between backend (actor registry) and frontend (React app)
  • TypeScript throughout: Strong typing with shared interfaces between backend and frontend
  • Modern React patterns: Proper use of hooks, refs, and state management
  • Structured logging: Good use of console.log for debugging WebSocket connections

Suggestions

  1. Missing newline at EOF in examples/cursors-raw-websocket/.gitignore:2 - should end with a newline per POSIX standards

  2. Error handling could be more robust in src/backend/registry.ts:174:

} catch (error) {
    console.error("error handling websocket message:", error);
}

Consider logging more context (sessionId, message type, actorId) for better debugging.

  1. README inconsistency - The README references path rivetkit/examples/cursors but the actual path is examples/cursors-raw-websocket

Potential Bugs & Issues

Critical Issues

  1. Race condition in WebSocket initialization (src/frontend/App.tsx:103-182):

    • The WebSocket connection is established but messages could be sent before the connection is fully open
    • Consider queuing messages or adding a connection ready check:
    const sendMessage = (message: any) => {
        if (wsRef.current?.readyState === WebSocket.OPEN) {
            wsRef.current.send(JSON.stringify(message));
        } else {
            console.warn("WebSocket not ready, message dropped");
        }
    };
  2. Memory leak potential in src/frontend/App.tsx:197:

    • Mouse move events trigger WebSocket sends on every pixel movement
    • This could overwhelm the WebSocket connection and cause performance issues
    • Recommendation: Add throttling/debouncing:
    import { useCallback, useRef } from 'react';
    
    const throttledMouseMove = useCallback(
        throttle((x: number, y: number) => {
            if (wsRef.current?.readyState === WebSocket.OPEN) {
                wsRef.current.send(JSON.stringify({
                    type: "updateCursor",
                    data: { userId, x, y },
                }));
            }
        }, 16), // ~60fps
        [userId]
    );
  3. State mutation in src/backend/registry.ts:135-141:

    if (existingIndex >= 0) {
        c.state.textLabels[existingIndex] = textLabel;
    } else {
        c.state.textLabels.push(textLabel);
    }

    Direct mutation of state arrays could cause issues. Consider using immutable updates or clarify if RivetKit actors handle this.

Medium Priority Issues

  1. Empty text removal logic (src/frontend/App.tsx:244-255):

    • When pressing Enter with empty text, the removal message is sent, but this could happen out of order with the initial create
    • Consider checking if the text was ever created before trying to remove it
  2. Missing error boundary in React app:

    • If WebSocket errors occur, they could crash the entire app
    • Add an Error Boundary component to gracefully handle failures
  3. Unclosed WebSocket on room change (src/frontend/App.tsx:172-181):

    • When roomId changes, a new useEffect runs but the old WebSocket might not close in time
    • Could lead to multiple connections or ghost cursors

Performance Considerations

Issues

  1. Unthrottled mouse events (mentioned above in bugs) - CRITICAL for performance

    • Every mouse move sends a WebSocket message
    • This generates hundreds of messages per second during movement
    • Will cause significant network and CPU load
  2. Missing WebSocket message batching:

    • Consider batching cursor updates on the server side to reduce broadcast overhead
    • Could implement a simple buffer that flushes every 16-32ms
  3. Re-renders on every cursor update (src/frontend/App.tsx:119-122):

    • Each cursor update triggers a full state update
    • Consider using React.memo or useMemo for cursor components
  4. Scale calculation (src/frontend/App.tsx:62-75):

    • Recalculates on every window resize without debouncing
    • Add debouncing to avoid excessive re-renders during resize

Suggestions

// Add at top of file
const throttle = (fn: Function, wait: number) => {
    let lastCall = 0;
    return (...args: any[]) => {
        const now = Date.now();
        if (now - lastCall >= wait) {
            lastCall = now;
            fn(...args);
        }
    };
};

Security Concerns

Issues

  1. No input sanitization (src/backend/registry.ts:124-141):

    • User text input is accepted and broadcast without validation or sanitization
    • Could enable XSS attacks if text is rendered as HTML anywhere
    • Recommendation: Add max length validation and sanitize special characters
    const MAX_TEXT_LENGTH = 200;
    if (text.length > MAX_TEXT_LENGTH) {
        console.warn("text too long, ignoring");
        return;
    }
  2. No rate limiting:

    • Users could spam cursor updates or text placements
    • Could DoS the actor or overwhelm other clients
    • Recommendation: Implement rate limiting in the actor
  3. Session ID validation (src/backend/registry.ts:59-65):

    • Only checks if sessionId exists, not if it's valid format
    • Consider validating sessionId format (length, characters)
  4. CORS configuration (src/backend/server.ts:3-7):

    • Hardcoded to http://localhost:5173 which is fine for development
    • Add comments or environment variables for production deployment

Medium Priority

  1. No authentication/authorization:
    • Anyone can join any room and see all cursors
    • Fine for a demo, but document this limitation

Test Coverage

Current State

The test file (tests/cursors.test.ts) only has 2 basic tests:

  • Actor creation test
  • Initial state test

Missing Coverage

  1. WebSocket interaction tests:

    • Test cursor position updates
    • Test text label creation/update/deletion
    • Test cursor removal on disconnect
    • Test message broadcasting to multiple clients
  2. State management tests:

    • Test state persistence across connections
    • Test concurrent updates from multiple users
    • Test edge cases (empty text, invalid positions)
  3. Error handling tests:

    • Test invalid message formats
    • Test missing sessionId
    • Test malformed data
  4. Integration tests:

    • Test full user flow (connect -> move cursor -> place text -> disconnect)
    • Test room isolation (messages don't leak between rooms)

Recommended Test Structure

test("Cursor position updates are broadcast to all clients", async (ctx: any) => {
    // Create WebSocket connections for 2 users
    // Send cursor update from user 1
    // Verify user 2 receives the update
});

test("Text labels persist across connections", async (ctx: any) => {
    // Connect user 1, place text, disconnect
    // Connect user 2
    // Verify user 2 sees the text label
});

test("Invalid messages are handled gracefully", async (ctx: any) => {
    // Send malformed JSON
    // Send invalid message type
    // Verify actor doesn't crash
});

Additional Suggestions

  1. Documentation:

    • Add JSDoc comments to key functions, especially WebSocket handlers
    • Document the coordinate system (virtual canvas vs screen)
    • Add sequence diagrams for WebSocket message flow
  2. Configuration:

    • Extract magic numbers to constants:
      • CANVAS_WIDTH, CANVAS_HEIGHT ✓ (already done)
      • MAX_TEXT_LENGTH, CURSOR_UPDATE_THROTTLE, MAX_USERS_PER_ROOM
  3. Accessibility:

    • Canvas has cursor: none but no alternative for keyboard navigation
    • Consider adding ARIA labels and keyboard shortcuts
  4. Developer Experience:

    • Add more descriptive console.log messages with context
    • Consider adding a debug mode with verbose logging

Summary

This is a solid example that demonstrates RivetKit's WebSocket capabilities well! The main concerns are:

Must Fix (P0):

  • Throttle mouse movement events to prevent performance issues
  • Add input validation and max length for text labels

Should Fix (P1):

  • Improve error handling with more context
  • Add WebSocket message queuing/ready checks
  • Fix README path inconsistency
  • Add newline at EOF in .gitignore

Nice to Have (P2):

  • Expand test coverage significantly
  • Add rate limiting
  • Implement proper Error Boundaries
  • Add accessibility features

Overall, this is a great addition to the examples! With the performance and security fixes, it will be production-ready. Great work! 🎉

@claude
Copy link

claude bot commented Oct 23, 2025

Pull Request Review: Cursors with Raw WebSocket Example

Thank you for this example! Great demonstration of real-time collaborative features using RivetKit.

Strengths

  • Clear example structure with separate backend/frontend directories
  • Good TypeScript type definitions
  • Reasonable state management with React hooks

Critical Issues

1. Bug: Cursor Keying Mismatch (src/frontend/App.tsx:119 and src/backend/registry.ts:48)
Frontend keys cursors by userId while backend uses sessionId. This breaks multi-tab support and the isOwnCursor check.
Fix: Use consistent keys (sessionId) throughout.

2. Missing Input Validation (src/backend/registry.ts:99, 126)
No validation of coordinates or text length. Users could send extreme values causing issues.
Recommendation: Add bounds checking for coordinates and length limits for text.

3. Memory Leak: Unbounded State (src/backend/registry.ts:143-144)
Text labels never expire, causing unbounded memory growth.
Recommendation: Add max label limit or time-based expiration.

4. Missing Error Handling (src/backend/registry.ts:115, 148, 166)
socket.send() can throw but errors are not caught. One failure could crash the handler.
Fix: Wrap sends in try-catch and check readyState.

5. Performance: Excessive Updates (src/frontend/App.tsx:199-208)
Every mousemove sends a message (60-120/sec). Implement throttling to ~20 updates/sec.

6. Stale Closure Bug (src/frontend/App.tsx:83-185)
useEffect dependencies missing client and userId which are used in handlers.

7. Test Coverage (tests/cursors.test.ts)
Tests only cover basic actor creation. Missing WebSocket, message broadcasting, and CRUD tests.

8. Documentation (README.md:19)
README path is wrong. Should be: cd rivet/examples/cursors-raw-websocket

Recommendations

High Priority (fix before merge):

  1. Fix cursor keying mismatch
  2. Add input validation
  3. Add error handling for socket.send()
  4. Fix README path

Medium Priority:

  1. Implement cursor update throttling
  2. Add state growth limits
  3. Fix useEffect dependencies
  4. Add WebSocket tests

Security

No major vulnerabilities but lack of input validation could enable DoS or UI manipulation. Suitable for dev/demo but needs hardening for production.

Great work! Core functionality is solid, issues are mostly robustness and edge cases.

@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples_cursors_with_raw_websockets_example branch from 0ffdb8b to f0c5e79 Compare October 24, 2025 06:32
@claude
Copy link

claude bot commented Oct 24, 2025

Pull Request Review: Cursors with Raw WebSockets Example

Summary

This PR adds a new example demonstrating real-time collaborative cursors using raw WebSockets with RivetKit. The implementation is well-structured and demonstrates proper usage of the actor model.

Code Quality & Best Practices

Strengths

  • Good separation of concerns between backend and frontend
  • Proper TypeScript usage with well-defined interfaces
  • Clean WebSocket lifecycle management
  • Responsive design with proper canvas scaling
  • Good user experience with visual feedback

Issues & Recommendations

Bug: Cursor Key Mismatch (App.tsx:121, 368)

The cursor state is keyed by userId but compared with sessionId. This means the 'you' label will never display correctly. Use sessionId consistently as the key throughout.

Critical: Input Validation Missing (registry.ts:99-104, 126-134, 159-162)

  • No validation of incoming WebSocket message data
  • Missing sanitization of user input (text labels)
  • No limits on data sizes
    Recommendation: Add type checking and bounds validation for all incoming data.

Bug: State Mutation (registry.ts:110, 141)

Direct mutation of nested state objects. Consider if the actor framework expects immutable updates.

Performance: Unnecessary Re-broadcasts (registry.ts:113-121)

Broadcasting cursor updates to the sender wastes bandwidth. Skip the sender in broadcasts.

Missing Error Handling (App.tsx:82-184)

  • No reconnection logic for WebSocket disconnections
  • No network error handling or timeouts
  • No exponential backoff for retries

Resource Leak Potential (registry.ts:93-179)

Event listeners are added but never explicitly removed.

Missing Rate Limiting (registry.ts:98-123)

No rate limiting on cursor updates. A malicious client could overwhelm the server.

Console Logging

Use tracing.info with structured data instead of console.log per CLAUDE.md guidelines.

README Issue (README.md:19)

Path is incorrect: should be cursors-raw-websocket not cursors.

Test Coverage

Only 2 basic tests. Missing tests for WebSocket functionality, concurrent users, and edge cases.

Performance Considerations

  1. No throttling on mouse move events (recommend ~60fps max)
  2. textLabels array grows unbounded (add cleanup/limits)
  3. O(n) broadcast acceptable for small rooms

Security Concerns

  1. No authentication or authorization
  2. No rate limiting (DoS vulnerable)
  3. No input sanitization (XSS risk)
  4. CORS hardcoded to localhost (fine for example)

Summary

Must Fix:

  • Cursor key mismatch bug
  • README path correction
  • Input validation and sanitization

Should Fix:

  • Add reconnection logic
  • Implement rate limiting
  • Expand test coverage
  • Use proper logging (tracing)

Nice to Have:

  • Throttle cursor updates
  • Add cleanup for unbounded state

Overall, solid example code but needs hardening before production-ready. Great work!

@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples_cursors_with_raw_websockets_example branch from f0c5e79 to e786fc1 Compare October 24, 2025 20:40
@NathanFlurry NathanFlurry force-pushed the 10-22-feat_examples-cursors_cursors_example branch from 120bdcb to b2d5bfe Compare October 24, 2025 20:40
@claude
Copy link

claude bot commented Oct 24, 2025

PR Review: Cursors with Raw WebSockets Example

Thanks for this example! This is a great demonstration of real-time collaborative features using RivetKit. Here's my detailed review:

✅ Strengths

  1. Clean Architecture: The separation between backend (actor/registry) and frontend (React) is well done
  2. Good User Experience: Features like color-coded cursors, typing indicators, and multi-room support are nice touches
  3. TypeScript Usage: Strong typing throughout with proper interfaces
  4. Responsive Design: The canvas scaling logic handles different viewport sizes well

🐛 Potential Issues

Critical: Cursor Keying Bug (registry.ts:48, App.tsx:121, App.tsx:368)

The backend keys cursors by sessionId while the frontend keys them by userId. This creates a mismatch:

  • Backend (registry.ts:48): cursors[sessionId] = cursor;
  • Frontend (App.tsx:121): [message.data.userId]: message.data

This means if a user opens two tabs, they'll see two separate cursors instead of one unified cursor. The keying should be consistent - either both use userId OR both use sessionId.

Recommendation: Use sessionId consistently if you want each tab to have its own cursor, or userId if you want one cursor per user regardless of tabs.

Security: Input Validation Missing (registry.ts:99, 126, 159)

The WebSocket message handlers don't validate incoming data:

  • No bounds checking on x/y coordinates (could be negative or extremely large)
  • No length validation on text strings (potential memory issues)
  • No sanitization of userId (could contain malicious content)

Recommendation: Add validation like:

if (typeof x !== 'number' || x < 0 || x > MAX_X) return;
if (text.length > 500) return; // or truncate

Race Condition: WebSocket State Mutation (registry.ts:93-179)

The addEventListener callback captures the sessionId but doesn't check if the WebSocket is still connected before broadcasting. If a socket closes while a message is being processed, you could send to a closed socket.

Recommendation: Add connection state checks before sending messages.

Memory Leak: Event Listeners (registry.ts:93)

Event listeners are added to the WebSocket but never explicitly removed. While WebSocket closure typically cleans these up, it's better to be explicit.

Recommendation: Store listeners in variables and remove them on close.

⚠️ Code Quality Issues

Inconsistent Error Handling

  • registry.ts:177 catches and logs errors but doesn't close the connection
  • App.tsx:159, 173 log errors but don't update UI state
  • According to CLAUDE.md, should use structured logging: tracing::info!(?error, "error handling websocket message") (though this is TypeScript, the principle applies)

Unused Variables

  • App.tsx:368: isOwnCursor is calculated but only used for display text. Consider optimizing cursor rendering for own cursor.

Magic Numbers

  • App.tsx:42-43: Canvas dimensions (1920x1080) are hardcoded
  • App.tsx:216: Text ID format could be a constant

Recommendation: Extract to named constants at file top.

🧪 Test Coverage

The tests are minimal:

  • Only tests basic action calls, not WebSocket functionality
  • No tests for race conditions, concurrent users, or error cases
  • No integration tests for the full flow

Recommendation: Add tests for:

  • Multiple simultaneous WebSocket connections
  • Cursor removal on disconnect
  • Text label persistence
  • Invalid message handling

📝 Documentation

README Issues

  • Line 19: Path says cursors but should be cursors-raw-websocket
  • Missing information about WebSocket connection lifecycle
  • No troubleshooting section

Code Comments

Good inline comments explaining functionality, but missing:

  • JSDoc comments for exported interfaces
  • Explanation of the sessionId vs userId distinction
  • WebSocket message protocol documentation

🎯 Performance Considerations

  1. Cursor Update Frequency (App.tsx:199): Every mouse move sends a WebSocket message. Consider throttling (e.g., 60fps max) to reduce network load.

  2. Broadcast Storm (registry.ts:114): Every cursor update broadcasts to ALL connections. For many users, this could be O(n²) messages per second.

  3. State Mutations (registry.ts:141, 160): Direct array mutations could cause issues with persistence. Consider immutable updates.

🔒 Security Concerns

  1. No Rate Limiting: A malicious client could spam cursor/text updates
  2. No Authorization: Anyone with a sessionId can join any room
  3. XSS Risk: Text labels aren't sanitized before rendering (though React escapes by default)
  4. No Maximum Room Size: Unlimited users per room could cause DoS

📋 Minor Issues

  1. Missing newline: .gitignore:2 (not critical but inconsistent)
  2. Hardcoded URL: App.tsx:9 uses localhost - should be configurable
  3. Console.log: Several console.logs should use proper logging (registry.ts:68, 90, App.tsx:90, 95, 102, 164)

🎨 Style Notes

Following CLAUDE.md guidelines:

  • ✅ Lowercase log messages (registry.ts:68, 183)
  • ✅ Using structured patterns
  • ❌ Some console.logs instead of proper logging framework

💡 Suggestions for Future Enhancements

  1. Add cursor interpolation for smoother movement
  2. Implement presence heartbeat to detect stale connections
  3. Add cursor "idle" state after period of no movement
  4. Consider using binary protocol (MessagePack) instead of JSON for better performance
  5. Add user names/avatars
  6. Implement text label editing/deletion

Summary

This is a solid example that demonstrates RivetKit's capabilities well. The main concerns are:

  1. Critical: Fix the sessionId/userId keying mismatch
  2. Important: Add input validation for security
  3. Nice-to-have: Improve test coverage and add throttling

Overall: Approve with minor changes requested - fix the cursor keying issue before merging.

Great work on the example! 🚀

@claude
Copy link

claude bot commented Oct 24, 2025

PR Review: Cursors with Raw WebSockets Example

Thank you for this example! This is a clean demonstration of real-time collaborative cursors using RivetKit. Here's my detailed review:


✅ Strengths

  1. Good separation of concerns - Clean separation between backend (actor logic) and frontend (React UI)
  2. Well-structured WebSocket handling - Proper event listeners for open, message, close, and error
  3. State management - Good use of React hooks and proper separation between ephemeral (vars) and persistent (state) data
  4. User experience - Nice touches like color-coded cursors, typing indicators, and responsive canvas scaling

🐛 Potential Issues

Critical

  1. WebSocket send without error handling (registry.ts:82-90, 114-121, 147-154, 165-172, 189-195)

    • All websocket.send() calls lack error handling
    • If a socket is closed or in CLOSING state, .send() will throw an error
    • Recommendation: Wrap sends in try-catch or check socket.readyState before sending
  2. Session ID mismatch bug (registry.ts:46-48, 77-80)

    • In getRoomState, cursors are keyed by sessionId
    • In updateCursor, cursor is stored with userId in the data but broadcasted
    • In frontend (App.tsx:118-122), cursors are keyed by message.data.userId
    • Issue: The cursor lookup uses sessionId but storage uses userId, creating inconsistency
    • Recommendation: Decide on a consistent key (likely sessionId since multiple tabs from same user should have different cursors)
  3. Race condition on room change (App.tsx:83-185)

    • When roomId changes, the useEffect cleanup closes the old WebSocket, but a new connection starts immediately
    • There's no handling for when the previous connection's messages arrive after state has been reset
    • Recommendation: Add connection ID tracking or ignore messages from stale connections

Medium Priority

  1. No input validation (registry.ts:99, 126, 159)

    • Message data is destructured without validation
    • Missing fields or wrong types could cause runtime errors
    • Recommendation: Add validation for incoming message data (e.g., check typeof x === 'number')
  2. Memory leak potential (registry.ts:93-179)

    • Event listeners are added to websockets but never explicitly removed
    • While they should be GC'd when the socket is removed from the Map, it's safer to be explicit
    • Recommendation: Consider using { once: true } for close handler or explicitly remove listeners
  3. State mutation (registry.ts:141, 160)

    • Direct array index assignment and reassignment mutates state
    • While this works, it's not following immutable update patterns
    • Recommendation: Use immutable updates: c.state.textLabels = [...c.state.textLabels.slice(0, existingIndex), textLabel, ...c.state.textLabels.slice(existingIndex + 1)]
  4. Cursor key inconsistency (App.tsx:366-407)

    • Cursors are rendered using id (which is userId from message data)
    • But the check isOwnCursor = id === userId compares against the local userId
    • This should work but relies on the backend storing cursors by userId, which conflicts with the sessionId-based Map
    • Recommendation: Align backend and frontend on sessionId or userId as the primary key

Low Priority

  1. Console.log statements (registry.ts:68-70, 183; App.tsx:90, 95, 102, 159, 164, 170, 173)

    • Multiple console.log statements should use a proper logging framework
    • Recommendation: For examples this is acceptable, but note that production code should use tracing (per CLAUDE.md)
  2. Hardcoded URL (App.tsx:9; server.ts:5)

    • The Rivet URL and CORS origin are hardcoded
    • Recommendation: Use environment variables for configuration
  3. No WebSocket reconnection logic (App.tsx:163-167)

    • When WebSocket closes, there's no automatic reconnection
    • Users would need to refresh the page
    • Recommendation: Add exponential backoff reconnection logic for production use
  4. Timestamp unused (registry.ts:7, 16)

    • timestamp fields are added but never used
    • Recommendation: Either use for cleanup of stale cursors or remove

🔒 Security Concerns

  1. No message size limits - WebSocket messages aren't size-limited, could enable DoS
  2. No rate limiting - Cursor updates could be sent rapidly, overwhelming the server
  3. No authentication - Anyone can connect to any room (acceptable for examples)
  4. XSS potential (App.tsx:331, 350) - Text labels render user input directly
    • While React escapes by default, good to validate/sanitize
  5. CORS set to specific origin (server.ts:4-6) - Good! But note this needs updating for production

🚀 Performance Considerations

  1. Broadcasting to all on every cursor move (registry.ts:113-121)

    • Could be optimized with throttling or only sending position deltas
    • For example purposes this is fine
  2. Array operations (registry.ts:137-144, 160-162)

    • findIndex is O(n), could use Map for O(1) lookups
    • For small arrays this is acceptable
  3. Re-rendering on every cursor update (App.tsx:118-122)

    • Every cursor move triggers a state update and re-render
    • Consider useMemo or useCallback optimizations if performance becomes an issue
    • For examples this is fine

🧪 Test Coverage

Current coverage: Basic (2 tests)

Missing tests:

  • WebSocket connection and message handling
  • Cursor position updates
  • Text label creation/update/removal
  • Error cases (malformed messages, missing fields)
  • Concurrent user interactions
  • Room switching

Recommendation: Add integration tests that simulate WebSocket connections and verify message broadcasting


📝 Code Quality & Best Practices

  1. TypeScript usage - Good use of types throughout ✅
  2. Error handling - Inconsistent; some try-catch blocks but many operations unguarded
  3. Code organization - Clean and well-structured ✅
  4. Comments - Adequate inline comments ✅
  5. Naming conventions - Consistent and clear ✅

🎯 Recommendations Summary

Must fix before merge:

  1. Fix the sessionId/userId key inconsistency in cursor storage and lookup
  2. Add error handling for WebSocket send operations

Should fix:
3. Add input validation for WebSocket messages
4. Fix state mutation to use immutable updates
5. Add race condition handling for room changes

Nice to have:
6. Add WebSocket reconnection logic
7. Expand test coverage
8. Add rate limiting and message size limits
9. Use environment variables for configuration


Overall Assessment

This is a solid example that demonstrates RivetKit's WebSocket capabilities well. The code is clean and readable. The main concerns are around edge cases and error handling that could cause issues in real-world usage. For an example, the current implementation is good, but the sessionId/userId inconsistency should be resolved to avoid confusion.

Recommendation: ✅ Approve with requested changes (fix critical issues #1 and #2)

Great work on this example! 🎉

Base automatically changed from 10-22-feat_examples-cursors_cursors_example to main October 25, 2025 09:21
@NathanFlurry NathanFlurry merged commit 85bb318 into main Oct 25, 2025
9 of 17 checks passed
@NathanFlurry NathanFlurry deleted the 10-22-feat_examples_cursors_with_raw_websockets_example branch October 25, 2025 09:21
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