Skip to content

feat(new-webui): Add client library for real-time MongoDB updates. - #892

Merged
davemarco merged 15 commits into
y-scope:mainfrom
davemarco:client
May 14, 2025
Merged

feat(new-webui): Add client library for real-time MongoDB updates.#892
davemarco merged 15 commits into
y-scope:mainfrom
davemarco:client

Conversation

@davemarco

@davemarco davemarco commented May 9, 2025

Copy link
Copy Markdown
Contributor

Description

This PR is a refactor of #841
PR adds a custom hook to front end which connects to new fastify mongoDB query service #880.

The custom hook (useCursor) should requery, when any of the dependencies have changed. It is meant to be a drop-in replacement for meteor useTracker.

There are some minor server changes as well. Server changes are to prevent, rare, but potential bugs.

Checklist

  • The PR satisfies the contribution guidelines.
  • This is a breaking change and that has been indicated in the PR title, OR this isn't a
    breaking change.
  • Necessary docs have been updated, OR no docs need to be updated.

Validation performed

Replaced app with a new testing component (code below). New component can modify queries with buttons,
testing how it would actually work in new webui (say a different query by user). Subscription and unsubscription worked as expected.

import {useState} from "react";

import MongoCollectionSocket from "./api/socket/MongoCollectionSocket";
import {useCursor} from "./api/socket/useCursor";


/**
 * Test1
 *
 * @param root0
 * @param root0.collectionName
 * @return
 */
const Results1 = ({collectionName = "compression-jobs"}: {collectionName?: string}) => {
    const singleResult = useCursor(
        () => new MongoCollectionSocket(collectionName).find({}, {}),
        [collectionName]
    );

    return (
        <div>
            {singleResult.map((r, index) => (
                <div key={index}>
                    {JSON.stringify(r)}
                </div>
            ))}
        </div>
    );
};

/**
 * Test2
 *
 * @param root0
 * @param root0.collectionName
 * @param root0.limit
 * @return
 */
const Results2 = ({collectionName = "stats", limit = 5}: {collectionName?: string; limit?: number}) => {
    const singleResult = useCursor(
        () => new MongoCollectionSocket(collectionName).find({}, {limit}),
        [collectionName,
            limit]
    );

    return (
        <div>
            {singleResult.map((r, index) => (
                <div key={index}>
                    {JSON.stringify(r)}
                </div>
            ))}
        </div>
    );
};

/**
 *
 */
const App = () => {
    const [show, setShow] = useState(true);
    const [collection1, setCollection1] = useState("compression-jobs");
    const [limit, setLimit] = useState(5);

    const toggleCollection = () => {
        setCollection1("compression-jobs" === collection1 ?
            "stats" :
            "compression-jobs");
    };

    const toggleLimit = () => {
        setLimit(5 === limit ?
            1 :
            5);
    };

    return (
        <div>
            <div>
                <button
                    onClick={() => {
                        setShow(!show);
                    }}
                >
                    Toggle
                </button>
                <button onClick={toggleCollection}>
                    Switch Collection:
                    {" "}
                    {collection1}
                </button>
                {show && <Results1 collectionName={collection1}/>}
            </div>

            <p>------------------------</p>

            <div>
                <button
                    onClick={() => {
                        setShow(!show);
                    }}
                >
                    Toggle
                </button>
                <button onClick={toggleLimit}>
                    Set Limit:
                    {" "}
                    {limit}
                </button>
                {show && <Results2
                    collectionName={"stats"}
                    limit={limit}/>}
            </div>

            <p>------------------------</p>
        </div>
    );
};

export default App;

Summary by CodeRabbit

  • New Features
    • Introduced real-time MongoDB collection and cursor support over sockets for live data updates.
    • Added a React hook to subscribe to real-time MongoDB data streams.
    • Added a shared singleton socket connection for efficient WebSocket communication.
  • Configuration
    • Enhanced TypeScript and Vite configurations for improved path aliasing and WebSocket proxy support.
  • Bug Fixes
    • Improved error handling and streamlined event handling for MongoDB socket events.
  • Chores
    • Updated dependencies to support new socket and path alias features.

@davemarco
davemarco requested a review from a team as a code owner May 9, 2025 17:57
@coderabbitai

coderabbitai Bot commented May 9, 2025

Copy link
Copy Markdown
Contributor
## Walkthrough

This change introduces a real-time client-server data subscription system using Socket.IO for MongoDB collections. It adds client-side socket and cursor classes, a React hook for live data, updates TypeScript and Vite configurations for path aliasing, and modifies server-side event handling to streamline collection initialization and subscription logic.

## Changes

| File(s)                                                                                          | Change Summary                                                                                                                                                                                                                  |
|-------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| `client/package.json`                                                                            | Added `socket.io-client` and `vite-tsconfig-paths` as dependencies.                                                                                                                                                            |
| `client/src/api/socket/MongoCollectionSocket.ts`, `client/src/api/socket/MongoCursorSocket.ts`  | Introduced `MongoCollectionSocket` for socket-based collection access and `MongoCursorSocket` for real-time cursor-like data subscriptions.                                                                                   |
| `client/src/api/socket/useCursor.tsx`                                                           | Added a React hook `useCursor` for reactive data arrays from `MongoCursorSocket`.                                                                                                                                              |
| `client/src/api/socket/SocketSingleton.ts`                                                     | Added singleton pattern to manage a shared Socket.IO client connection.                                                                                                                                                        |
| `client/tsconfig/tsconfig.app.json`, `client/vite.config.ts`                                    | Updated TypeScript and Vite configs for path aliasing, WebSocket proxying, and file system serving from parent directory.                                                                                                     |
| `common/index.ts`                                                                               | Removed `"collection::init"` event type, updated `"collection::find::subscribe"` event to include `collectionName`, and changed export statements to `export type`.                                                          |
| `server/src/plugins/MongoSocketIoServer/index.ts`                                               | Removed collection init event and listener, added method to get or create watcher collections, updated subscription listener to require collection name and validate collection existence, improving error handling and logging. |

## Sequence Diagram(s)

```mermaid
sequenceDiagram
    participant ReactComponent
    participant useCursor
    participant MongoCursorSocket
    participant MongoCollectionSocket
    participant SocketIOClient
    participant SocketIOServer
    participant MongoDB

    ReactComponent->>useCursor: Call useCursor(query, deps)
    useCursor->>MongoCollectionSocket: query() → MongoCursorSocket
    useCursor->>MongoCursorSocket: subscribe(onDataUpdate)
    MongoCursorSocket->>SocketIOClient: Emit "collection::find::subscribe"
    SocketIOClient->>SocketIOServer: "collection::find::subscribe" (collectionName, query, options)
    SocketIOServer->>MongoDB: Check collection existence
    alt Collection exists
        SocketIOServer->>SocketIOClient: Ack with initial data, queryId
        SocketIOServer-->>SocketIOClient: Push "collection::find::update" on data change
        SocketIOClient->>MongoCursorSocket: Receive update
        MongoCursorSocket->>useCursor: onDataUpdate(data)
        useCursor->>ReactComponent: setState(data)
    else Collection missing
        SocketIOServer->>SocketIOClient: Ack with error
        MongoCursorSocket->>useCursor: Throw error
    end
    ReactComponent-->>useCursor: Unmount
    useCursor->>MongoCursorSocket: unsubscribe()
    MongoCursorSocket->>SocketIOClient: Emit "collection::find::unsubscribe"
    SocketIOClient->>SocketIOServer: "collection::find::unsubscribe"

Possibly related PRs

Suggested reviewers

  • junhaoliao
  • haiqi96

<!-- walkthrough_end -->

<!-- announcements_start -->

> [!TIP]
> <details>
> <summary>⚡️ Faster reviews with caching</summary>
> 
> - CodeRabbit now supports caching for code and dependencies, helping speed up reviews. This means quicker feedback, reduced wait times, and a smoother review experience overall. Cached data is encrypted and stored securely. This feature will be automatically enabled for all accounts on May 16th. To opt out, configure `Review - Disable Cache` at either the organization or repository level. If you prefer to disable all data retention across your organization, simply turn off the `Data Retention` setting under your Organization Settings.
> 
> Enjoy the performance boost—your workflow just got faster.
> 
> </details>

<!-- announcements_end -->
<!-- internal state start -->


<!-- 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 -->

<!-- internal state end -->
<!-- finishing_touch_checkbox_start -->

<details open="true">
<summary>✨ Finishing Touches</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> 📝 Generate Docstrings

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

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.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>🪧 Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=y-scope/clp&utm_content=892):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
  - `I pushed a fix in commit <commit_id>, please review it.`
  - `Explain this complex logic.`
  - `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
  - `@coderabbitai explain this code block.`
  -	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
  - `@coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.`
  - `@coderabbitai read src/utils.ts and explain its main purpose.`
  - `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
  - `@coderabbitai help me debug CodeRabbit configuration file.`

### Support

Need help? Create a ticket on our [support page](https://www.coderabbit.ai/contact-us/support) for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (Invoked using PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai generate docstrings` to [generate docstrings](https://docs.coderabbit.ai/finishing-touches/docstrings) for this PR.
- `@coderabbitai generate sequence diagram` to generate a sequence diagram of the changes in this PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://docs.coderabbit.ai) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](http://discord.gg/coderabbit) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 10

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5da5b05 and f5dd06c.

⛔ Files ignored due to path filters (1)
  • components/log-viewer-webui/client/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (8)
  • components/log-viewer-webui/client/package.json (1 hunks)
  • components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (1 hunks)
  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1 hunks)
  • components/log-viewer-webui/client/src/api/socket/useCursor.tsx (1 hunks)
  • components/log-viewer-webui/client/tsconfig/tsconfig.app.json (2 hunks)
  • components/log-viewer-webui/client/vite.config.ts (2 hunks)
  • components/log-viewer-webui/common/index.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}`: - Prefer `false == ` rather than `!`.

**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}: - Prefer false == <expression> rather than !<expression>.

  • components/log-viewer-webui/client/vite.config.ts
  • components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts
  • components/log-viewer-webui/common/index.ts
  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts
  • components/log-viewer-webui/client/src/api/socket/useCursor.tsx
🧬 Code Graph Analysis (2)
components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (2)
components/log-viewer-webui/common/index.ts (2)
  • ServerToClientEvents (80-80)
  • ClientToServerEvents (76-76)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1)
  • MongoCursorSocket (107-107)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1)
components/log-viewer-webui/common/index.ts (4)
  • ServerToClientEvents (80-80)
  • ClientToServerEvents (76-76)
  • QueryId (82-82)
  • Response (79-79)
🪛 Biome (1.9.4)
components/log-viewer-webui/client/tsconfig/tsconfig.app.json

[error] 21-21: Expected a property but instead found '// Map imports from "@common/*" to the shared folder'.

Expected a property here.

(parse)

🔇 Additional comments (9)
components/log-viewer-webui/client/package.json (1)

26-27: Dependencies added correctly for socket integration

The addition of socket.io-client and vite-tsconfig-paths dependencies properly supports the real-time MongoDB query service integration described in the PR objectives. The socket.io-client enables websocket communication while vite-tsconfig-paths supports the TypeScript path aliasing needed for the common directory imports.

components/log-viewer-webui/client/tsconfig/tsconfig.app.json (1)

4-5: Common directory inclusion looks good

Adding the common directory to the TypeScript compilation scope is appropriate for accessing shared types and interfaces between client and server components.

components/log-viewer-webui/client/vite.config.ts (5)

3-3: Import for tsconfig paths plugin added correctly

The import of the vite-tsconfig-paths plugin aligns with the dependency added in package.json and is necessary for TypeScript path resolution in Vite.


9-10: Plugin configuration looks good

The tsconfig paths plugin is properly configured in the Vite plugins array, enabling the path aliases defined in tsconfig.app.json.


13-13: Server configured to bind on all network interfaces

Setting host: "0.0.0.0" allows the development server to accept connections from any network interface, which is useful for testing across devices but may have security implications in certain environments.


22-26: WebSocket proxy configured correctly for Socket.IO

The WebSocket proxy configuration is correctly set up to forward Socket.IO connections to the backend server, with appropriate options for WebSocket support.


28-31: File system configuration allows access to common directory

The configuration to serve files from one level up in the directory structure is necessary for accessing the common directory, which contains shared types for the client and server.

components/log-viewer-webui/common/index.ts (2)

32-39: Simplified collection init event signature

The "collection::init" event no longer requires a callback parameter, aligning with server-side changes that simplify the collection initialization process. This is a clean simplification that reduces complexity.


75-83: Type-only exports improve type safety

Changing to type-only exports with export type is a good practice that prevents type information from being included in the compiled JavaScript bundle, potentially reducing bundle size and improving type safety.

Comment thread components/log-viewer-webui/client/tsconfig/tsconfig.app.json
Comment thread components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts Outdated
Comment on lines +25 to +43
constructor (collectionName: string) {
// eslint-disable-next-line no-warning-comments
// TODO: Add support for user provided domain name (i.e. io("https://server-domain.com")).
// Implementation could involve parsing server .env file and moving server .env to a
// common folder.

// eslint-disable-next-line no-warning-comments
// TODO: Current implementation creates a new socket connection for each collection. This
// could be problematic if the number of collections is large since browsers limit the
// number of web sockets per domain. An simple change is to use Socket.IO namespace
// feature, and having Socket.IO multiplex the socket per collection namespace. Another
// more involved option is to implement a shared socket.
// Namespace reference: https://socket.io/docs/v4/namespaces/
this.socket = io();
this.socket.emit("collection::init", {
collectionName: collectionName,
});
console.log(`MongoDB collection:${collectionName} initialized.`);
}

@coderabbitai coderabbitai Bot May 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Socket lifecycle is never closed – risk of resource leaks

MongoCollectionSocket opens a fresh socket per instance (io()), but there is no public disconnect()/close() method or automatic teardown logic (beforeunload, React unmount, etc.).
In long-running UIs that mount/unmount many components (or hot-reload during dev), the browser can easily exceed the ~6 WebSocket per-origin cap and silently drop newer ones.

Diff sketch:

+  /**
+   * Cleanly tears down the socket connection.
+   * Call this when the owning component unmounts.
+   */
+  public disconnect(): void {
+      if (this.socket.connected) {
+          this.socket.disconnect();
+      }
+  }

Expose this method (or share a singleton socket—see next comment) and invoke it from the consumer (e.g., inside useEffect cleanup that creates the collection object).


🏁 Script executed:

#!/bin/bash
# Inspect MongoCollectionSocket.ts and search for disconnect/close usage

FILE="components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts"
echo "=== Contents of $FILE ==="
sed -n '1,200p' "$FILE"

echo
echo "=== Searching for 'disconnect' across the repo ==="
rg "disconnect" -n .

echo
echo "=== Searching for 'close' across the repo ==="
rg "close" -n .

Length of output: 66430


Add socket teardown to prevent connection leaks

The MongoCollectionSocket constructor creates a new io() connection but never closes it, which can exhaust the browser’s per-origin WebSocket limit in long-running or hot-reloaded UIs.

Please implement and expose a cleanup method and invoke it from the owning component’s unmount logic:

• File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts
– After the existing methods, add:

/**
 * Cleanly disconnects the socket.
 * Call this when the owning component unmounts.
 */
public disconnect(): void {
  if (this.socket.connected) {
    this.socket.disconnect();
  }
}

• Consumer (e.g. in a React component):

const collectionSocket = new MongoCollectionSocket("myCollection");
useEffect(() => {
  return () => {
    collectionSocket.disconnect();
  };
}, []);

This ensures sockets are closed when no longer needed.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

When the class is garbage collected, will it automatically disconnect?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

I need to research this question about Socket.IO garbage collection behavior to give you an accurate answer.


🌐 Web query:

Does Socket.IO automatically disconnect when the socket object is garbage collected in JavaScript?

💡 Result:

Does Socket.IO Automatically Disconnect When the Socket Object Is Garbage Collected in JavaScript?

Summary:
No, Socket.IO does not automatically disconnect a socket when its JavaScript object is garbage collected. Explicit disconnection is required to properly close the network connection.


Detailed Explanation

1. Socket.IO Connection Lifecycle

  • Socket.IO maintains a live connection between the client and server, managed internally by its own event system and timers[1][4].
  • To close a connection intentionally, you must explicitly call socket.disconnect() on the client-side or server-side[5][8].

2. Garbage Collection in JavaScript

  • In JavaScript, objects are garbage collected when there are no more references to them, but this does not guarantee any cleanup of side effects or external resources (like network sockets).
  • The garbage collector only frees memory; it does not invoke any disconnection logic provided by Socket.IO.

3. Implicit vs. Explicit Disconnection

  • Explicit Disconnection:

    • Use socket.disconnect() to close the connection cleanly and emit the disconnect event[5][8].
    • Once disconnected, the socket will not try to reconnect (unless you explicitly call connect again)[5].
  • Garbage Collection Alone:

    • If the socket object is simply dereferenced and left for garbage collection, the underlying connection may persist until the OS or browser forcibly closes idle or orphaned connections (which could take time and may not be reliable)[1][3][4].
    • Socket.IO does not register a finalizer or destructor with the JavaScript garbage collector to disconnect itself automatically.

4. Memory and Resource Management

  • Relying on garbage collection to manage network connections is not safe, as network resources (like open TCP connections) can remain open, potentially causing memory leaks or exhausting server resources[9].
  • Proper practice is always to call .disconnect() when you are done using a socket.

5. Analogies and Related Technologies

  • Other networking libraries (e.g., in .NET or certain C++ environments) do provide explicit .close() or .dispose() methods, and recommend their use to ensure proper resource cleanup[6].
  • In JavaScript, such deterministic cleanup must be manually invoked.

Conclusion

Socket.IO does not automatically disconnect sockets when their JavaScript object is garbage collected.
To ensure connections are closed and resources are cleaned up, always explicitly call socket.disconnect() when you are finished with a socket object[5][8]. Garbage collection alone will not handle network disconnection.

Citations:


@davemarco No, Socket.IO connections are not automatically disconnected when the object is garbage collected. JavaScript's garbage collector only frees memory; it doesn't handle cleanup of external resources like network connections.

The socket connection will remain open even after the class instance is garbage collected, which can lead to:

  1. Exhaustion of the browser's WebSocket connection limit (typically ~6 per origin)
  2. Unnecessary server resource consumption
  3. Potential memory leaks

You should implement the suggested disconnect() method and ensure it's called when the connection is no longer needed (typically in component unmount logic):

/**
 * Cleanly disconnects the socket.
 * Call this when the owning component unmounts.
 */
public disconnect(): void {
  if (this.socket.connected) {
    this.socket.disconnect();
  }
}

This is standard practice for managing connection lifecycle in networking libraries.


✏️ Learnings added
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:25-43
Timestamp: 2025-05-09T18:06:33.030Z
Learning: Socket.IO connections in JavaScript require explicit disconnection by calling socket.disconnect() and are not automatically cleaned up when the object is garbage collected.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

hmm i imagine this will be difficult to implement in the current useCusor hook. Will the connection automatically disconnect after a certain amount of no activity?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@davemarco Socket.IO does have a built-in ping/pong mechanism, but it doesn't automatically disconnect inactive connections by default. Here's how it works:

  1. Socket.IO has a "ping timeout" mechanism, but this is primarily to detect connection failures, not application inactivity.
  2. By default, pings are sent every 25 seconds, and if no pong is received within 20 seconds after a ping, the connection is considered broken.
  3. These timeouts detect network issues, not application inactivity - a silent but connected client will still maintain its connection.

Regarding implementation in useCursor:

You could modify the useCursor hook to handle the disconnection in its cleanup function:

// In useCursor.tsx
useEffect(() => {
  // ... existing subscription logic ...
  
  return () => {
    // Cleanup function when hook unmounts
    if (cursor) {
      cursor.unsubscribe();
      // If you had access to the MongoCollectionSocket instance:
      // collectionSocket.disconnect();
    }
  };
}, [dependencies]);

The challenge is getting access to the underlying MongoCollectionSocket instance from the cursor. You might need to refactor to either:

  1. Expose the socket through the cursor
  2. Use a singleton pattern or context for socket management
  3. Pass socket lifecycle responsibility to the component using the hook

(●__●)


🌐 Web query:

Does Socket.IO automatically disconnect inactive connections after a timeout period?

💡 Result:

Socket.IO and Automatic Disconnection on Inactivity

Summary:
Yes, Socket.IO does automatically disconnect inactive connections after a timeout period using a built-in heartbeat mechanism. This mechanism relies on periodic "ping" and "pong" messages to detect inactive or lost connections.


How the Timeout Works

  • Heartbeat Mechanism:
    The server sends a PING packet to each connected client at regular intervals defined by the pingInterval. The client must reply with a PONG within a time window defined by pingTimeout. If the server does not receive a PONG in time, it considers the connection closed due to inactivity and disconnects the client[4][7][9].

  • Default Values:

    • pingInterval: 25,000 ms (25 seconds)
    • pingTimeout: 20,000 ms (20 seconds)

    These can be adjusted in the server configuration[9].

  • Disconnection Reason:
    When a connection is disconnected due to inactivity, the reason is logged as "ping timeout"[4][7][9].

  • Both Client and Server:

    • If the client does not receive a ping from the server within pingInterval + pingTimeout, it also considers the connection lost and disconnects itself[4][7][9].

Example Configuration (Node.js):

const io = new Server({
  pingInterval: 25000,
  pingTimeout: 20000,
});

You can increase pingTimeout and pingInterval to allow for longer periods of inactivity before disconnecting, but setting these too high may delay the detection of truly lost connections[2][7][9].


Common Scenarios and Limitations

  • Browser/OS Power Saving:
    If a browser tab is minimized or a mobile app is backgrounded, timers may be throttled or network connections paused, leading to unexpected disconnects even if the user expects to remain connected[2][1][7].

  • Proxies, Load Balancers, and Network Devices:
    In some environments, proxies or load balancers may close inactive WebSocket connections after their own timeout period, potentially overriding Socket.IO's settings[2][8][10].


Key Takeaway

  • Socket.IO will automatically disconnect connections that are inactive beyond the configured timeout period, primarily as a safety and resource-saving feature, but this is highly configurable[4][7][9].
  • Unexpected disconnects can also occur due to factors outside of Socket.IO, such as browser power-saving, proxies, or network interruptions[2][8][10].

References to the Mechanism in Official Documentation:

“At a given interval (the pingInterval value sent in the handshake) the server sends a PING packet and the client has a few seconds (the pingTimeout value) to send a PONG packet back. If the server does not receive a PONG packet back, it will consider that the connection is closed.”[4]

“The server sends a ping packet every pingInterval ms, and if the client does not answer with a pong within pingTimeout ms, the server considers that the connection is closed.”[7]


Conclusion:
Socket.IO does automatically disconnect clients after a period of inactivity, as determined by its ping/pong heartbeat mechanism and the values of pingInterval and pingTimeout[4][7][9]. These settings can be customized to suit your application's needs.

Citations:


@davemarco Yes, Socket.IO has a built-in heartbeat mechanism, but it's designed to detect broken connections, not application inactivity:

  1. By default, the server sends a PING every 25 seconds
  2. Clients must respond with a PONG within 20 seconds
  3. If no PONG is received, the connection is closed with "ping timeout" reason

This mechanism only detects network failures. If your connection is healthy but idle, it will remain open indefinitely.

For implementing disconnection in useCursor, you have several options:

  1. Refactor to expose the socket: Modify your MongoCursorSocket to provide access to the underlying MongoCollectionSocket

    // In MongoCursorSocket.ts
    getCollection(): MongoCollectionSocket {
      return this._collection; // You would need to store this reference
    }
  2. Use a connection manager: Create a singleton that tracks all socket connections

  3. React Context provider: Manage connections at a higher level through React Context

The simplest approach would be to pass the collection instance to the cursor and expose a getter:

// When creating the cursor
const cursor = collectionSocket.find(query, options);
useEffect(() => {
  // ...subscription logic
  return () => {
    cursor.unsubscribe();
    cursor.getCollection().disconnect();
  }
}, []);

(x__x)


✏️ Learnings added
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:25-43
Timestamp: 2025-05-09T18:21:05.824Z
Learning: Socket.IO has a built-in ping/pong mechanism that detects connection failures, but it doesn't automatically disconnect truly inactive but connected clients. By default, it sends pings every 25 seconds and expects pongs within 20 seconds, but this is for connection health monitoring, not application inactivity detection.

Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:25-43
Timestamp: 2025-05-09T18:21:05.824Z
Learning: Socket.IO has a built-in heartbeat mechanism that uses PING/PONG messages (default: 25s interval, 20s timeout) to detect broken connections, but this only detects network failures, not application inactivity. If a connection is healthy but idle, it will remain open indefinitely unless manually disconnected.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

No this looks terrible. will namespace help?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧩 Analysis chain

@davemarco Yes, namespaces would help significantly! They're actually the perfect solution for this problem.

Socket.IO namespaces allow multiple logical connections to share a single physical WebSocket connection. This directly addresses your TODO comment about browser connection limits.

Here's how you could implement it:

// SocketManager.ts - Singleton to manage the socket connection
export class SocketManager {
  private static instance: SocketManager;
  private socket: Socket;
  private collectionSockets: Map<string, Socket> = new Map();
  
  private constructor() {
    this.socket = io();
  }
  
  public static getInstance(): SocketManager {
    if (!SocketManager.instance) {
      SocketManager.instance = new SocketManager();
    }
    return SocketManager.instance;
  }
  
  public getCollectionSocket(collectionName: string): Socket {
    if (!this.collectionSockets.has(collectionName)) {
      // Create a namespace for this collection
      const collectionSocket = this.socket.io(`/${collectionName}`);
      this.collectionSockets.set(collectionName, collectionSocket);
    }
    return this.collectionSockets.get(collectionName)!;
  }
  
  public disconnect(): void {
    if (this.socket.connected) {
      this.socket.disconnect();
      this.collectionSockets.clear();
    }
  }
}

// Modified MongoCollectionSocket.ts
class MongoCollectionSocket {
  private socket: Socket<ServerToClientEvents, ClientToServerEvents>;

  constructor(collectionName: string) {
    // Get the namespaced socket from the singleton manager
    this.socket = SocketManager.getInstance().getCollectionSocket(collectionName);
    this.socket.emit("collection::init", { collectionName });
    console.log(`MongoDB collection:${collectionName} initialized.`);
  }
  
  // Rest of your class...
}

Benefits:

  1. Single physical connection for all collections
  2. Centralized connection management
  3. App-level disconnect on shutdown rather than component-level
  4. No risk of hitting browser connection limits

This avoids the "terrible" approach of having to manage disconnection in every component that uses a collection.


🌐 Web query:

Do Socket.IO namespaces solve the connection disconnection management problem?

💡 Result:

Socket.IO namespaces help organize and separate different communication channels within a single Socket.IO connection. They provide a way to split the connection into multiple logical endpoints, each with its own event handlers and logic. However, namespaces alone do not fully solve connection and disconnection management problems.

Here's a more detailed explanation:

  1. Purpose of Namespaces: Namespaces allow you to segment the connection space, which can be useful for separating concerns within your application (e.g., chat, notifications, gaming). Each namespace can have its own event listeners and middleware.

  2. Connection Management: When a client connects, it connects to a specific namespace. Socket.IO manages the connection lifecycle for each namespace separately. This means you can handle events like connect, disconnect, and reconnections within each namespace independently.

  3. Disconnection Management: Namespaces can help you handle disconnections more granularly because each namespace triggers its own disconnect event. However, namespaces don't inherently solve challenges like detecting real connection drops, handling reconnections, or managing resources based on connection state. You still need to implement proper event handlers and logic to manage these cases.

  4. Limitations: If a client disconnects from the underlying transport (e.g., network issues), all namespaces for that client are affected since they share the underlying connection. So, namespaces don't isolate the physical connection—they just provide logical segmentation over it.

Summary:

  • Namespaces improve logical separation and event management per context.
  • They provide a framework to handle connection/disconnection events per namespace.
  • They do not independently solve all connection and disconnection management problems.
  • You still need to write application-level code for robust connection handling, reconnection strategies, and resource cleanup.

If you're looking to manage connection and disconnection issues comprehensively, consider combining namespaces with other Socket.IO features like rooms, middleware, and custom reconnection logic.


@davemarco Yes, Socket.IO namespaces would solve the primary issue mentioned in your TODO comment - they allow multiple logical connections to share a single physical WebSocket connection.

From the TODO in your code:

"An simple change is to use Socket.IO namespace feature, and having Socket.IO multiplex the socket per collection namespace."

Here's how namespaces help:

  1. Single physical connection: Instead of one connection per collection, all collections use the same underlying connection
  2. Avoid browser limits: Browsers typically limit connections to 6-8 per domain, namespaces bypass this
  3. Simpler architecture: Centralized connection management instead of per-component disconnection

Basic implementation approach:

// Create a singleton socket manager
export class SocketManager {
  private static instance: SocketManager;
  private socket: Socket;
  
  private constructor() {
    this.socket = io(); // Single connection
  }
  
  public static getInstance(): SocketManager {
    if (!SocketManager.instance) {
      SocketManager.instance = new SocketManager();
    }
    return SocketManager.instance;
  }
  
  public getCollectionNamespace(collectionName: string): Socket {
    return this.socket.io(`/${collectionName}`);
  }
  
  // Single disconnect point for application shutdown
  public disconnect(): void {
    this.socket.disconnect();
  }
}

This addresses both the connection limit issue and simplifies disconnection management by centralizing it.

Note that server-side changes would also be needed to properly handle these namespaces.


🧠 Learnings used
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.788Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I moved to shared socket which should fix the issue

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@davemarco Great! Using a shared socket singleton is the perfect solution for this issue. This implementation:

  1. Prevents creating multiple unnecessary connections
  2. Avoids hitting browser connection limits
  3. Centralizes connection management
  4. Eliminates the need for per-collection disconnection logic

The getSharedSocket() approach is ideal since it gives you all the benefits of connection pooling without the complexity of implementing namespaces. Nice work addressing this!

(^◡^)


🧠 Learnings used
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.788Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.

Comment thread components/log-viewer-webui/client/src/api/socket/useCursor.tsx
Comment thread components/log-viewer-webui/client/src/api/socket/useCursor.tsx
Comment thread components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts Outdated
Comment on lines +49 to +79
async subscribe (onDataUpdate: (data: object[]) => void): Promise<void> {
console.log("Attepting to subscribe to query:", this.#query);

this.#updateListener = (respArgs: {queryId: number; data: object[]}) => {
// Server sends updates for multiple queryIDs using the same event name.
if (this.#queryId === respArgs.queryId) {
onDataUpdate(respArgs.data);
}
};

this.#socket.on("collection::find::update", this.#updateListener);

const response: Response<{queryId: number; initialDocuments: object[]}> =
await this.#socket.emitWithAck(
"collection::find::subscribe",
{
query: this.#query,
options: this.#options,
}
);

if ("error" in response) {
throw new Error(`Subscription failed: ${response.error}`);
}

// Set the initial documents received from the server.
onDataUpdate(response.data.initialDocuments);

this.#queryId = response.data.queryId;
console.log(`Subscribed to queryID:${this.#queryId}.`);
}

@coderabbitai coderabbitai Bot May 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Multiple subscribe() calls overwrite listener – guard against misuse

Nothing stops a consumer from calling subscribe twice on the same cursor; the second call overwrites #updateListener without deregistering the first, causing duplicate updates and memory bloat.

Add an early check:

+        if (this.#queryId !== null) {
+            throw new Error("Cursor already subscribed");
+        }

and/or expose a subscribed flag.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async subscribe (onDataUpdate: (data: object[]) => void): Promise<void> {
console.log("Attepting to subscribe to query:", this.#query);
this.#updateListener = (respArgs: {queryId: number; data: object[]}) => {
// Server sends updates for multiple queryIDs using the same event name.
if (this.#queryId === respArgs.queryId) {
onDataUpdate(respArgs.data);
}
};
this.#socket.on("collection::find::update", this.#updateListener);
const response: Response<{queryId: number; initialDocuments: object[]}> =
await this.#socket.emitWithAck(
"collection::find::subscribe",
{
query: this.#query,
options: this.#options,
}
);
if ("error" in response) {
throw new Error(`Subscription failed: ${response.error}`);
}
// Set the initial documents received from the server.
onDataUpdate(response.data.initialDocuments);
this.#queryId = response.data.queryId;
console.log(`Subscribed to queryID:${this.#queryId}.`);
}
async subscribe(onDataUpdate: (data: object[]) => void): Promise<void> {
// Guard against multiple subscriptions on the same cursor
+ if (this.#queryId !== null) {
+ throw new Error("Cursor already subscribed");
+ }
console.log("Attepting to subscribe to query:", this.#query);
this.#updateListener = (respArgs: {queryId: number; data: object[]}) => {
// Server sends updates for multiple queryIDs using the same event name.
if (this.#queryId === respArgs.queryId) {
onDataUpdate(respArgs.data);
}
};
this.#socket.on("collection::find::update", this.#updateListener);
const response: Response<{queryId: number; initialDocuments: object[]}> =
await this.#socket.emitWithAck(
"collection::find::subscribe",
{
query: this.#query,
options: this.#options,
}
);
if ("error" in response) {
throw new Error(`Subscription failed: ${response.error}`);
}
// Set the initial documents received from the server.
onDataUpdate(response.data.initialDocuments);
this.#queryId = response.data.queryId;
console.log(`Subscribed to queryID:${this.#queryId}.`);
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is probably overkill since u would never call subscribe twice

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@davemarco I understand your perspective. If the application code ensures subscribe won't be called twice on the same cursor instance, then the check may indeed be unnecessary.

That said, defensive programming can help prevent subtle bugs if the component is ever reused or the calling code changes. Without the check, calling subscribe twice would leave a dangling event listener that could cause memory leaks.

But I'll defer to your judgment on this one since you know the usage patterns better!

Comment on lines +257 to +261
this.#fastify.log.info(
`Socket:${socket.id} requested query:${JSON.stringify(query)} ` +
`with options:${JSON.stringify(options)} to collection:${collectionName}`
);

@coderabbitai coderabbitai Bot May 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Log string building uses concatenation – template literal is enough

You already start with a template literal; concatenation is unnecessary and less readable.

-        this.#fastify.log.info(
-            `Socket:${socket.id} requested query:${JSON.stringify(query)} ` +
-            `with options:${JSON.stringify(options)} to collection:${collectionName}`
-        );
+        this.#fastify.log.info(
+            `Socket:${socket.id} requested query:${JSON.stringify(query)} ` +
+            `with options:${JSON.stringify(options)} to collection:${collectionName}`
+        );

(If you keep multi-line, drop the +.)

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

what did u change

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@davemarco The bot was suggesting to remove the + concatenation operator between the template literals. The diff visualization didn't show this change correctly, but the comment note "(If you keep multi-line, drop the +.)" clarifies the intention.

The recommendation is to change:

this.#fastify.log.info(
    `Socket:${socket.id} requested query:${JSON.stringify(query)} ` +
    `with options:${JSON.stringify(options)} to collection:${collectionName}`
);

To a single multi-line template literal:

this.#fastify.log.info(
    `Socket:${socket.id} requested query:${JSON.stringify(query)} 
    with options:${JSON.stringify(options)} to collection:${collectionName}`
);

This avoids unnecessary string concatenation since template literals already support multi-line strings.

Comment thread components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (3)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (3)

49-50: Typo: "Attepting" → "Attempting"

-        console.log("Attepting to subscribe to query:", this.#query);
+        console.log("Attempting to subscribe to query:", this.#query);

92-95: Unsubscribe emits without awaiting server ACK

If the server rejects the request (e.g., invalid queryId), the client silently assumes success and clears local state, causing an irrecoverable desync.

Consider using emitWithAck and reverting local cleanup on failure:

-    unsubscribe (): void {
+    async unsubscribe (): Promise<void> {
         if (null === this.#queryId) {
             console.error("Attempted to unsubscribe, but no active subscription exists.");
 
             return;
         }
 
-        this.#socket.emit("collection::find::unsubscribe", {
-            queryId: this.#queryId,
-        });
+        const response = await this.#socket.emitWithAck("collection::find::unsubscribe", {
+            queryId: this.#queryId,
+        });
+
+        if ("error" in response) {
+            console.error(`Unsubscribe failed: ${response.error}`);
+            return;
+        }

49-73: 🛠️ Refactor suggestion

Add protection against multiple subscribe calls on same cursor

Nothing prevents a consumer from calling subscribe twice on the same cursor; the second call overwrites #updateListener without deregistering the first, causing duplicate updates and memory bloat.

Add an early check at the beginning of the subscribe method:

    async subscribe (onDataUpdate: (data: object[]) => void): Promise<void> {
+        if (this.#queryId !== null) {
+            throw new Error("Cursor already subscribed");
+        }
        console.log("Attepting to subscribe to query:", this.#query);
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f5dd06c and ee3ffb7.

📒 Files selected for processing (1)
  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}`: - Prefer `false == ` rather than `!`.

**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}: - Prefer false == <expression> rather than !<expression>.

  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.788Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
🧬 Code Graph Analysis (1)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1)
components/log-viewer-webui/common/index.ts (4)
  • ServerToClientEvents (80-80)
  • ClientToServerEvents (76-76)
  • QueryId (82-82)
  • Response (79-79)
🔇 Additional comments (1)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1)

1-108:

Details

❌ Incorrect review comment

Consider implementing support for Socket.IO namespaces

Based on the retrieved learning from davemarco, there's a plan to implement Socket.IO namespaces for connection pooling. The MongoCursorSocket class is a good place to prepare for this future enhancement.

This is worth tracking as a TODO for a future update, as implementing piecemeal changes to the connection architecture could lead to coordination issues between client and server.


🌐 Web query:

What are Socket.IO namespaces and how are they used for connection pooling?

💡 Result:

Socket.IO namespaces are a way to create separate communication channels on the same physical connection between a client and server. Each namespace is identified by a unique name (typically a string starting with a slash, e.g., /chat, /news). They allow you to split the logic of your application over a single shared connection, effectively enabling multiple independent chat rooms or services to coexist.

What are Socket.IO Namespaces?

  • Separation of concerns: Namespaces allow you to divide your app into different endpoints, each handling its own events and logic.
  • Single connection: Even though there are multiple namespaces, the client typically maintains a single underlying WebSocket connection, with multiplexed channels for each namespace.
  • Event segregation: Each namespace has its own event handlers both on the client and server sides.

How Socket.IO Namespaces Work

When a client connects to a Socket.IO server, it connects to the default namespace / by default. To connect to a different namespace, the client explicitly specifies it, for example:

const socket = io('/chat');

On the server side, you define namespace-specific event handling:

const chatNamespace = io.of('/chat');

chatNamespace.on('connection', (socket) => {
  console.log('User connected to chat namespace');
  socket.on('message', (msg) => {
    chatNamespace.emit('message', msg);
  });
});

Connection Pooling with Namespaces

  • Multiplexing connections: Namespaces allow multiple independent logical connections over a single physical WebSocket connection. This avoids the overhead of establishing multiple connections.
  • Efficient resource use: Instead of each service or channel requiring its own TCP connection, namespaces pool these onto one underlying connection, reducing latency and resource use.
  • Example use cases: You might have /chat, /news, /notifications namespaces, all using the same underlying connection but logically separated.

Summary

Socket.IO namespaces provide a way to architect your real-time application with isolated communication channels over a single WebSocket or long-polling connection. This technique pools multiple logical connections efficiently into a single physical connection, thereby optimizing resource usage and simplifying client-server interaction.

If you'd like, I can provide a code example demonstrating how to implement namespaces for connection pooling in a Socket.IO app!


Ignore Socket.IO namespace support suggestion in MongoCursorSocket
This class already accepts a pre-configured Socket instance, so it’s namespace-agnostic by design. Namespace creation and connection pooling should be handled at the socket-initialization layer rather than within this cursor wrapper.

Likely an incorrect or invalid review comment.

Comment on lines +28 to +32
/**
* @param socket
* @param query
* @param options
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Enhance JSDoc documentation for constructor parameters

The constructor JSDoc has @PARAM tags but doesn't provide descriptions of what each parameter represents or their expected format. Adding more detailed parameter descriptions would improve code maintainability and developer understanding.

 /**
  * @param socket
  * @param query
  * @param options
  */
+/**
+ * @param socket - Socket.IO client instance for server communication
+ * @param query - MongoDB query object defining the filter criteria
+ * @param options - MongoDB query options (like sort, limit, etc.)
+ */

Committable suggestion skipped: line range outside the PR's diff.

Comment on lines +98 to +100
this.#socket.off("collection::find::update", this.#updateListener);
this.#updateListener = null;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Consider adding a guard around unsubscribe listener removal

The current logic checks for this.#updateListener before removing it, which is good defensive programming. However, you might want to add more context in an error log if the listener is unexpectedly missing:

        if (this.#updateListener) {
            this.#socket.off("collection::find::update", this.#updateListener);
            this.#updateListener = null;
+        } else {
+            console.warn(`No update listener found for queryID:${this.#queryId} during unsubscribe.`);
        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
this.#socket.off("collection::find::update", this.#updateListener);
this.#updateListener = null;
}
// --- components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts ---
if (this.#updateListener) {
this.#socket.off("collection::find::update", this.#updateListener);
this.#updateListener = null;
} else {
console.warn(
`No update listener found for queryID:${this.#queryId} during unsubscribe.`
);
}
// ------------------------------------------------------------------------

Comment on lines +12 to +106
/**
* A cursor-like object receiving MongoDB documents over a socket connection.
*/
class MongoCursorSocket {
#socket: Socket<ServerToClientEvents, ClientToServerEvents>;

#query: object;

#options: object;

#queryId: Nullable<QueryId> = null;


// Listener for data updates from the server.
#updateListener: Nullable<(respArgs: {queryId: number; data: object[]}) => void> = null;

/**
* @param socket
* @param query
* @param options
*/
constructor (
socket: Socket<ServerToClientEvents, ClientToServerEvents>,
query: object,
options: object
) {
this.#socket = socket;
this.#query = query;
this.#options = options;
}

/**
* Subscribes to query watcher for real-time updates.
*
* @param onDataUpdate Handler which sets data updates from the server in react ui component.
* @throws {Error} if subscription fails.
*/
async subscribe (onDataUpdate: (data: object[]) => void): Promise<void> {
console.log("Attepting to subscribe to query:", this.#query);

this.#updateListener = (respArgs: {queryId: number; data: object[]}) => {
// Server sends updates for multiple queryIDs using the same event name.
if (this.#queryId === respArgs.queryId) {
onDataUpdate(respArgs.data);
}
};

this.#socket.on("collection::find::update", this.#updateListener);

const response: Response<{queryId: number; initialDocuments: object[]}> =
await this.#socket.emitWithAck(
"collection::find::subscribe",
{
query: this.#query,
options: this.#options,
}
);

if ("error" in response) {
this.#socket.off("collection::find::update", this.#updateListener);
throw new Error(`Subscription failed: ${response.error}`);
}

// Set the initial documents received from the server.
onDataUpdate(response.data.initialDocuments);

this.#queryId = response.data.queryId;
console.log(`Subscribed to queryID:${this.#queryId}.`);
}

/**
* Unsubscribe from the query.
*/
unsubscribe (): void {
if (null === this.#queryId) {
console.error("Attempted to unsubscribe, but no active subscription exists.");

return;
}


this.#socket.emit("collection::find::unsubscribe", {
queryId: this.#queryId,
});

if (this.#updateListener) {
this.#socket.off("collection::find::update", this.#updateListener);
this.#updateListener = null;
}

console.log(`Unsubscribed to queryID:${this.#queryId}.`);

this.#queryId = null;
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Implement a isSubscribed() method for state checking

To help consumers check subscription state without accessing private fields, consider adding a simple method:

+    /**
+     * Returns whether this cursor is currently subscribed.
+     */
+    isSubscribed(): boolean {
+        return this.#queryId !== null;
+    }

This would allow consumers to safely check subscription state before calling subscribe/unsubscribe.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* A cursor-like object receiving MongoDB documents over a socket connection.
*/
class MongoCursorSocket {
#socket: Socket<ServerToClientEvents, ClientToServerEvents>;
#query: object;
#options: object;
#queryId: Nullable<QueryId> = null;
// Listener for data updates from the server.
#updateListener: Nullable<(respArgs: {queryId: number; data: object[]}) => void> = null;
/**
* @param socket
* @param query
* @param options
*/
constructor (
socket: Socket<ServerToClientEvents, ClientToServerEvents>,
query: object,
options: object
) {
this.#socket = socket;
this.#query = query;
this.#options = options;
}
/**
* Subscribes to query watcher for real-time updates.
*
* @param onDataUpdate Handler which sets data updates from the server in react ui component.
* @throws {Error} if subscription fails.
*/
async subscribe (onDataUpdate: (data: object[]) => void): Promise<void> {
console.log("Attepting to subscribe to query:", this.#query);
this.#updateListener = (respArgs: {queryId: number; data: object[]}) => {
// Server sends updates for multiple queryIDs using the same event name.
if (this.#queryId === respArgs.queryId) {
onDataUpdate(respArgs.data);
}
};
this.#socket.on("collection::find::update", this.#updateListener);
const response: Response<{queryId: number; initialDocuments: object[]}> =
await this.#socket.emitWithAck(
"collection::find::subscribe",
{
query: this.#query,
options: this.#options,
}
);
if ("error" in response) {
this.#socket.off("collection::find::update", this.#updateListener);
throw new Error(`Subscription failed: ${response.error}`);
}
// Set the initial documents received from the server.
onDataUpdate(response.data.initialDocuments);
this.#queryId = response.data.queryId;
console.log(`Subscribed to queryID:${this.#queryId}.`);
}
/**
* Unsubscribe from the query.
*/
unsubscribe (): void {
if (null === this.#queryId) {
console.error("Attempted to unsubscribe, but no active subscription exists.");
return;
}
this.#socket.emit("collection::find::unsubscribe", {
queryId: this.#queryId,
});
if (this.#updateListener) {
this.#socket.off("collection::find::update", this.#updateListener);
this.#updateListener = null;
}
console.log(`Unsubscribed to queryID:${this.#queryId}.`);
this.#queryId = null;
}
}
/**
* A cursor-like object receiving MongoDB documents over a socket connection.
*/
class MongoCursorSocket {
#socket: Socket<ServerToClientEvents, ClientToServerEvents>;
#query: object;
#options: object;
#queryId: Nullable<QueryId> = null;
// Listener for data updates from the server.
#updateListener: Nullable<(respArgs: {queryId: number; data: object[]}) => void> = null;
/**
* @param socket
* @param query
* @param options
*/
constructor (
socket: Socket<ServerToClientEvents, ClientToServerEvents>,
query: object,
options: object
) {
this.#socket = socket;
this.#query = query;
this.#options = options;
}
/**
* Subscribes to query watcher for real-time updates.
*
* @param onDataUpdate Handler which sets data updates from the server in react ui component.
* @throws {Error} if subscription fails.
*/
async subscribe (onDataUpdate: (data: object[]) => void): Promise<void> {
console.log("Attepting to subscribe to query:", this.#query);
this.#updateListener = (respArgs: {queryId: number; data: object[]}) => {
// Server sends updates for multiple queryIDs using the same event name.
if (this.#queryId === respArgs.queryId) {
onDataUpdate(respArgs.data);
}
};
this.#socket.on("collection::find::update", this.#updateListener);
const response: Response<{queryId: number; initialDocuments: object[]}> =
await this.#socket.emitWithAck(
"collection::find::subscribe",
{
query: this.#query,
options: this.#options,
}
);
if ("error" in response) {
this.#socket.off("collection::find::update", this.#updateListener);
throw new Error(`Subscription failed: ${response.error}`);
}
// Set the initial documents received from the server.
onDataUpdate(response.data.initialDocuments);
this.#queryId = response.data.queryId;
console.log(`Subscribed to queryID:${this.#queryId}.`);
}
/**
* Unsubscribe from the query.
*/
unsubscribe (): void {
if (null === this.#queryId) {
console.error("Attempted to unsubscribe, but no active subscription exists.");
return;
}
this.#socket.emit("collection::find::unsubscribe", {
queryId: this.#queryId,
});
if (this.#updateListener) {
this.#socket.off("collection::find::update", this.#updateListener);
this.#updateListener = null;
}
console.log(`Unsubscribed to queryID:${this.#queryId}.`);
this.#queryId = null;
}
/**
* Returns whether this cursor is currently subscribed.
*/
isSubscribed(): boolean {
return this.#queryId !== null;
}
}

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

♻️ Duplicate comments (4)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (2)

50-58: Guard against double-subscription to prevent dangling listeners.

Creating a new #updateListener without first checking this.#queryId enables this cursor to be subscribed twice, leaving the original listener orphaned and leaking memory. A one-liner protects against that:

+        if (this.#queryId !== null) {
+            throw new Error("Cursor already subscribed; call unsubscribe() first.");
+        }

106-106: Expose an isSubscribed() helper for consumer ergonomics.

A trivial predicate improves discoverability and avoids consumers poking private state:

     }
+
+    /**
+     * Returns `true` while the cursor is actively subscribed.
+     */
+    isSubscribed(): boolean {
+        return this.#queryId !== null;
+    }
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (2)

257-260: Avoid string concatenation inside template literal.

Since a template literal is already used, the + operators are redundant and hinder readability.

-        this.#fastify.log.info(
-            `Socket:${socket.id} requested query:${JSON.stringify(query)} ` +
-            `with options:${JSON.stringify(options)} to collection:${collectionName}`
-        );
+        this.#fastify.log.info(
+            `Socket:${socket.id} requested query:${JSON.stringify(query)} ` +
+            `with options:${JSON.stringify(options)} to collection:${collectionName}`
+        );

168-170: 🧹 Nitpick (assertive)

Spelling: “aysnc/await” → “async/await”.

-        // init and subscription events should be serialised by Socket.IO.
-        // ...
-        // With no aysnc/await, a race is unlikely since the
+        // init and subscription events should be serialised by Socket.IO.
+        // ...
+        // With no async/await, a race is unlikely since the
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ee3ffb7 and 39e44cf.

📒 Files selected for processing (2)
  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}`: - Prefer `false == ` rather than `!`.

**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}: - Prefer false == <expression> rather than !<expression>.

  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (ubuntu-latest)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: lint-check (macos-latest)

Comment on lines 164 to 171
/* eslint-disable no-warning-comments */
// TODO: The init socket event could race with the subscription event (i.e. the
// subscription event could run concurrently with the init event) leading to errors due
// to an uninitialized collection. Consider removing this event entirely and using the
// subscription event to initialize the collection. If this event remains, do not run any
// async/await code in this function. With no aysnc/await, a race is unlikely since the
// init and subscription events should be serialised by Socket.IO.
socket.data = {...socket.data, collectionName};

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Function is declared async but must stay synchronous to avoid race; drop async.

The comment explicitly warns against async/await, yet the listener is still marked async, introducing an unnecessary micro-task delay and potential race condition. Removing the async keyword prevents accidental awaits from being added later.

-    async #collectionInitListener (
+    #collectionInitListener (
         socket: MongoCustomSocket,
         requestArgs: {collectionName: string},
-    ): Promise<void> {
+    ): void {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/* eslint-disable no-warning-comments */
// TODO: The init socket event could race with the subscription event (i.e. the
// subscription event could run concurrently with the init event) leading to errors due
// to an uninitialized collection. Consider removing this event entirely and using the
// subscription event to initialize the collection. If this event remains, do not run any
// async/await code in this function. With no aysnc/await, a race is unlikely since the
// init and subscription events should be serialised by Socket.IO.
socket.data = {...socket.data, collectionName};
#collectionInitListener (
socket: MongoCustomSocket,
requestArgs: {collectionName: string},
): void {
/* eslint-disable no-warning-comments */
// TODO: The init socket event could race with the subscription event (i.e. the
// subscription event could run concurrently with the init event) leading to errors due
// to an uninitialized collection. Consider removing this event entirely and using the
// subscription event to initialize the collection. If this event remains, do not run any
// async/await code in this function. With no aysnc/await, a race is unlikely since the
// init and subscription events should be serialised by Socket.IO.
socket.data = {...socket.data, collectionName};
}

@davemarco

Copy link
Copy Markdown
Contributor Author

@junhaoliao coderabbit brought up something interesting here . Basically the problem is that every time the query changes, say user makes a new search query. The collection will change opening up a new socket. This will potentially make us run out of sockets much faster. Moreover, there is no simple way to disconnect the collection in the hook

    const singleResult = useCursor(
        () => new MongoCollectionSocket(collectionName).find({}, {limit}),
        [collectionName,
            limit]
    );

I propose going to the socket singleton approach discussed earlier where there is only one socket.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🔭 Outside diff range comments (2)
components/log-viewer-webui/common/index.ts (2)

32-46: 🧹 Nitpick (assertive)

Consider discriminating Response<T> for safer narrowing

Right now Response<T> is a union of Err | Success<T> with overlapping keys, so consumers must use "error" in res checks all over the codebase.
A discriminated union with a literal tag (status: "ok" | "err") improves readability and compiler exhaustiveness checks.

type Response<T> =
  | { status: "ok"; data: T }
  | { status: "err"; error: string; queryId?: QueryId };

This would simplify handlers:

if (response.status === "err") {  }

51-58: 🧹 Nitpick (assertive)

Event-per-query TODO — add severity or tracking reference

The comment hints at changing "collection::find::update" to a query-specific channel. Given that the client already filters by queryId, this is mostly a performance/memory optimisation. Consider turning the TODO into a GitHub issue with context & proposed approach; inline TODOs tend to be forgotten.

♻️ Duplicate comments (1)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1)

55-55: JSON.stringify can throw on circular structures

This was raised previously and the code remains unchanged. The risk is low in typical queries, but a defensive wrapper prevents an unhandled exception that breaks subscription flow.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 39e44cf and e3c3ce7.

📒 Files selected for processing (4)
  • components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (1 hunks)
  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1 hunks)
  • components/log-viewer-webui/common/index.ts (3 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}`: - Prefer `false == ` rather than `!`.

**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}: - Prefer false == <expression> rather than !<expression>.

  • components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts
  • components/log-viewer-webui/common/index.ts
  • components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.788Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (3)
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.788Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:25-43
Timestamp: 2025-05-09T18:06:33.030Z
Learning: Socket.IO connections in JavaScript require explicit disconnection by calling socket.disconnect() and are not automatically cleaned up when the object is garbage collected.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:25-43
Timestamp: 2025-05-09T18:21:05.824Z
Learning: Socket.IO has a built-in ping/pong mechanism that detects connection failures, but it doesn't automatically disconnect truly inactive but connected clients. By default, it sends pings every 25 seconds and expects pongs within 20 seconds, but this is for connection health monitoring, not application inactivity detection.
🧬 Code Graph Analysis (1)
components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (3)
components/log-viewer-webui/common/index.ts (2)
  • ServerToClientEvents (79-79)
  • ClientToServerEvents (75-75)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (2)
  • collectionName (143-146)
  • collectionName (198-210)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1)
  • MongoCursorSocket (114-114)
🔇 Additional comments (1)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (1)

192-210: 🧹 Nitpick (assertive)

#getOrCreateWatcherCollection can race under heavy concurrency

If two sockets subscribe to the same brand-new collection at nearly the same time, both could evaluate undefined and create two MongoWatcherCollection instances before one is cached. Although rare, it wastes memory and may duplicate change-stream watchers.

Guard with a Map double-check or use Map.prototype.get’s return as authoritative after set:

if (false === this.#collections.has(collectionName)) {
    this.#collections.set(
        collectionName,
        new MongoWatcherCollection(collectionName, this.#mongoDb)
    );
}
return this.#collections.get(collectionName)!;

Likely an incorrect or invalid review comment.

Comment on lines +27 to +31
constructor (collectionName: string) {
this.socket = getSharedSocket();
this.collectionName = collectionName;
console.log(`MongoDB collection:${collectionName} initialized.`);
}

@coderabbitai coderabbitai Bot May 9, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Expose a deterministic teardown path for the collection-level socket

MongoCollectionSocket keeps only a reference returned by getSharedSocket().
Call-sites currently have no way to disconnect or remove listeners when the component tree unmounts (e.g., during hot-reload). Even though the underlying connection is shared, it is still good practice to provide a dispose() / disconnect() that simply delegates to the shared singleton. This allows React cleanup hooks (or unit tests) to release resources explicitly and avoids lingering listeners that keep collections in memory.

Example:

class MongoCollectionSocket {
   …
+  /**
+   * Releases any listeners added by this instance.
+   * No-op when the shared socket is already closed.
+   */
+  public disconnect(): void {
+      if (this.socket.connected) {
+          // Only detach collection-specific listeners here
+          // (do not close the underlying shared socket).
+          this.socket.removeAllListeners("collection::find::update");
+      }
+  }
}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
constructor (collectionName: string) {
this.socket = getSharedSocket();
this.collectionName = collectionName;
console.log(`MongoDB collection:${collectionName} initialized.`);
}
// components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts
export class MongoCollectionSocket {
constructor (collectionName: string) {
this.socket = getSharedSocket();
this.collectionName = collectionName;
console.log(`MongoDB collection:${collectionName} initialized.`);
}
/**
* Releases any listeners added by this instance.
* No-op when the shared socket is already closed.
*/
public disconnect(): void {
if (this.socket.connected) {
// Only detach collection-specific listeners here
// (do not close the underlying shared socket).
this.socket.removeAllListeners("collection::find::update");
}
}
// …other methods…
}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This is only one socket. So the only way to get another socket should disconnect the previous socket. I think it should be okay

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@davemarco I understand your point about using a socket singleton pattern. Since the socket is shared and managed centrally, and since the unsubscription is handled in the useCursor hook, you're right that an explicit disconnect in the MongoCollectionSocket class may not be strictly necessary for the current implementation.

Given that the class isn't directly attaching listeners (those appear to be in the MongoCursorSocket class instead), your current approach should work fine as long as the hook's cleanup is thorough.


✏️ Learnings added
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:27-31
Timestamp: 2025-05-09T19:15:26.158Z
Learning: For the MongoDB real-time updates implementation in components/log-viewer-webui/client/src/api/socket, a socket singleton pattern is used where a single shared socket connection is maintained rather than creating multiple connections. The socket lifecycle is managed centrally, with unsubscription handling in the useCursor React hook's cleanup function.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Comment on lines +41 to +48
find (query: object, options: object) {
return new MongoCursorSocket(
this.socket,
this.collectionName,
query,
options,
);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Strengthen typing & return signature of find()

query and options are declared as plain object, which forfeits type-safety and IDE autocompletion.
Consider parameterising the class (or at least the method) with generics, or constrain them to Record<string, unknown>.

In addition, the method relies on type inference for its return value. Being explicit helps consumers and documentation tools:

- find (query: object, options: object) {
-     return new MongoCursorSocket(
+ find<Q extends Record<string, unknown>, O extends Record<string, unknown>>(
+     query: Q,
+     options: O
+ ): MongoCursorSocket {
+     return new MongoCursorSocket(
          this.socket,
          this.collectionName,
          query,
          options,
      );
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
find (query: object, options: object) {
return new MongoCursorSocket(
this.socket,
this.collectionName,
query,
options,
);
}
find<Q extends Record<string, unknown>, O extends Record<string, unknown>>(
query: Q,
options: O
): MongoCursorSocket {
return new MongoCursorSocket(
this.socket,
this.collectionName,
query,
options,
);
}

Comment on lines +20 to +23
#query: object;

#options: object;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Loose object types reduce safety

Both #query and #options are typed as bare object. This accepts primitives such as new Date() or arrays and blocks introspection in IDEs. Prefer Record<string, unknown> or generics as suggested for MongoCollectionSocket.

Comment on lines +235 to +246
const hasCollection = await this.#hasCollection(collectionName);
if (false === hasCollection) {
this.#fastify.log.error(`Collection ${collectionName} does not exist in MongoDB`);
callback({
error: `Collection ${collectionName} does not exist in MongoDB on server`,
});

return;
}

const watcherCollection = this.#getOrCreateWatcherCollection(collectionName);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Per-subscription listCollections() impacts latency

#hasCollection() performs an extra round-trip to MongoDB for every subscribe request. Under heavy churn (e.g., dashboards with polling queries) this becomes a hotspot.

Since you already cache MongoWatcherCollection instances, you can cheaply infer existence:

if (!this.#collections.has(collectionName) &&
    false === await this.#hasCollection(collectionName)) {
   
}

Alternatively memoise the positive/negative result with a TTL.

@davemarco

Copy link
Copy Markdown
Contributor Author

@junhaoliao - okay I made the socket singleton change, to fix the runaway socket problem. Note the singleton was an issue before since the collection name was not part of the hash. But now it is, so should not be an issue.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e3c3ce7 and 11a1288.

📒 Files selected for processing (2)
  • components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts (1 hunks)
  • components/log-viewer-webui/common/index.ts (3 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}`: - Prefer `false == ` rather than `!`.

**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}: - Prefer false == <expression> rather than !<expression>.

  • components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts
  • components/log-viewer-webui/common/index.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.788Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
🪛 GitHub Actions: clp-lint
components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts

[error] 1-1: Imports must be broken into multiple lines if there are more than 1 elements (import-newlines/enforce)


[error] 1-1: Expected 2 empty lines after import statement not followed by another import (import/newline-after-import)


[error] 1-1: Expected a line break after this opening brace (@stylistic/object-curly-newline)


[error] 1-1: There should be no space after '{' (@stylistic/object-curly-spacing)


[error] 1-1: There should be no space before '}' (@stylistic/object-curly-spacing)


[error] 1-1: Expected a line break before this closing brace (@stylistic/object-curly-newline)


[warning] 6-6: Expected a function expression (func-style)


[warning] 6-6: Missing JSDoc comment (jsdoc/require-jsdoc)


[error] 6-6: Missing space before function parentheses (@stylistic/space-before-function-paren)


[warning] 14-14: Expected blank line before this statement (@stylistic/padding-line-between-statements)


[error] 15-15: Newline required at end of file but not found (@stylistic/eol-last)

🔇 Additional comments (4)
components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts (2)

7-7: ⚠️ Potential issue

Use explicit comparison instead of negation operator

According to the coding guidelines, prefer false == <expression> rather than !<expression>.

-    if (!sharedSocket) {
+    if (null == sharedSocket) {

Likely an incorrect or invalid review comment.


1-1: ⚠️ Potential issue

Fix import statement to follow linting guidelines

The import statement needs to be broken into multiple lines since there are multiple elements being imported. Also ensure proper spacing around the curly braces.

-import { io, Socket } from "socket.io-client";
+import {
+    io,
+    Socket
+} from "socket.io-client";

Likely an incorrect or invalid review comment.

🧰 Tools
🪛 GitHub Actions: clp-lint

[error] 1-1: Imports must be broken into multiple lines if there are more than 1 elements (import-newlines/enforce)


[error] 1-1: Expected 2 empty lines after import statement not followed by another import (import/newline-after-import)


[error] 1-1: Expected a line break after this opening brace (@stylistic/object-curly-newline)


[error] 1-1: There should be no space after '{' (@stylistic/object-curly-spacing)


[error] 1-1: There should be no space before '}' (@stylistic/object-curly-spacing)


[error] 1-1: Expected a line break before this closing brace (@stylistic/object-curly-newline)

components/log-viewer-webui/common/index.ts (2)

36-36: LGTM: Explicit collectionName parameter added

Adding collectionName explicitly to the request arguments is a good improvement for type safety and clarity.


74-82: LGTM: Type-only exports improve modularity

Changing to type-only exports is a good practice that better separates types from runtime code, supporting improved type-only imports consistent with your TypeScript configuration updates.

Comment thread components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts Outdated
Comment on lines +1 to +3
import { io, Socket } from "socket.io-client";

let sharedSocket: Socket | null = null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue

Add proper line spacing after imports

According to the linting errors, there should be 2 empty lines after the import statement.

 import { io, Socket } from "socket.io-client";

+
 let sharedSocket: Socket | null = null;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import { io, Socket } from "socket.io-client";
let sharedSocket: Socket | null = null;
import { io, Socket } from "socket.io-client";
let sharedSocket: Socket | null = null;
🧰 Tools
🪛 GitHub Actions: clp-lint

[error] 1-1: Imports must be broken into multiple lines if there are more than 1 elements (import-newlines/enforce)


[error] 1-1: Expected 2 empty lines after import statement not followed by another import (import/newline-after-import)


[error] 1-1: Expected a line break after this opening brace (@stylistic/object-curly-newline)


[error] 1-1: There should be no space after '{' (@stylistic/object-curly-spacing)


[error] 1-1: There should be no space before '}' (@stylistic/object-curly-spacing)


[error] 1-1: Expected a line break before this closing brace (@stylistic/object-curly-newline)

Comment thread components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts Outdated
Comment on lines +52 to 55
// eslint-disable-next-line no-warning-comments
// TODO: Consider replacing this with `collection::find::update${number}`, which will
// limit callbacks being triggered in the client to their respective query IDs.
"collection::find::update": (respArgs: {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Consider prioritizing the TODO for query-specific updates

The TODO comment suggests a valuable optimization that would reduce unnecessary client-side processing by limiting callback triggers to specific query IDs. This aligns with the PR objective of preventing potential bugs with the query service.

Based on the PR objectives and the retrieved learning about coordinated client-server changes, consider prioritizing this TODO item in your implementation roadmap. This would help address the concern mentioned in the PR comments about managing socket connections efficiently.

Comment thread components/log-viewer-webui/client/vite.config.ts Outdated
return collections.some((collection) => collection.name === collectionName);
}

/**

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

fyi i removed this since it could race with subscribe event. Anyways it is probably simpler either way without it.

@davemarco davemarco changed the title feat(new-webui): Adds UI component to display real-time updates from Fastify MongoDB query service. feat(new-webui): Adds custom cursor to receive real-time updates from Fastify MongoDB query service. May 9, 2025
@davemarco davemarco changed the title feat(new-webui): Adds custom cursor to receive real-time updates from Fastify MongoDB query service. feat(new-webui): Adds custom hook to receive real-time updates from Fastify MongoDB query service. May 9, 2025
Comment thread components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts Outdated
"useDefineForClassFields": true,
"baseUrl": "../",
"paths": {
"@common/*": ["../common/*"], // Map imports from "@common/*" to the shared folder

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@hoophalab
as a note, we will revisit this as we implement #822

Comment thread components/log-viewer-webui/client/vite.config.ts Outdated
Comment thread components/log-viewer-webui/client/vite.config.ts Outdated
ws: true,
},
},
fs: {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@hoophalab
as a note, we will revisit this as we implement #822

this.#updateListener = null;
}

console.log(`Unsubscribed to queryID:${this.#queryId}.`);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
console.log(`Unsubscribed to queryID:${this.#queryId}.`);
console.log(`Unsubscribed to queryID=${this.#queryId}.`);

@davemarco davemarco May 12, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I'm using the other syntax in a lot of places already. Not that I disagree with this syntax, but im using the : in many other places

@junhaoliao junhaoliao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

lgtm

i proposed some stylish changes

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8e3c034 and 32289d1.

⛔ Files ignored due to path filters (1)
  • components/log-viewer-webui/client/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (1)
  • components/log-viewer-webui/client/package.json (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📓 Common learnings
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:27-31
Timestamp: 2025-05-09T19:15:26.180Z
Learning: For the MongoDB real-time updates implementation in components/log-viewer-webui/client/src/api/socket, a socket singleton pattern is used where a single shared socket connection is maintained rather than creating multiple connections. The socket lifecycle is managed centrally, with unsubscription handling in the useCursor React hook's cleanup function.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.814Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
🔇 Additional comments (1)
components/log-viewer-webui/client/package.json (1)

27-27: Verify socket.io-client version compatibility with server.
Ensure the client version ^4.8.1 aligns with your Socket.IO server version to avoid runtime mismatches. Also confirm that TypeScript definitions are available—if not, consider adding @types/socket.io-client.

"react-dom": "^19.0.0",
"react-router": "^7.4.1",
"socket.io-client": "^4.8.1",
"vite-tsconfig-paths": "^5.1.4",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Relocate vite-tsconfig-paths to devDependencies.
The vite-tsconfig-paths plugin is only needed at build time and for local development. Moving it to devDependencies will reduce production install size and clarify its usage scope.

@davemarco
davemarco requested a review from junhaoliao May 12, 2025 21:57

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (2)
components/log-viewer-webui/client/package.json (1)

29-29: Relocate vite-tsconfig-paths to devDependencies.
As discussed, this plugin is only needed during build and development. Moving it to devDependencies will reduce production install size and clarify its scope.
Suggest:

 "dependencies": {
-  "vite-tsconfig-paths": "^5.1.4",
 },
 "devDependencies": {
+  "vite-tsconfig-paths": "^5.1.4",
 }
components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts (1)

27-27: 🛠️ Refactor suggestion

Add error and disconnect handlers for socket resilience.
Register handlers immediately after initializing the socket to improve observability and retry logic:

 sharedSocket = io();
+sharedSocket.on("connect_error", (err) => {
+  console.error("Socket connection error:", err);
+});
+sharedSocket.on("disconnect", (reason) => {
+  console.warn("Socket disconnected:", reason);
+});

This will help surface connection issues and guide reconnection strategies.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 32289d1 and 1e35e7f.

⛔ Files ignored due to path filters (1)
  • components/log-viewer-webui/client/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • components/log-viewer-webui/client/package.json (1 hunks)
  • components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts (1 hunks)
  • components/log-viewer-webui/client/vite.config.ts (2 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}`: - Prefer `false == ` rather than `!`.

**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}: - Prefer false == <expression> rather than !<expression>.

  • components/log-viewer-webui/client/vite.config.ts
  • components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts
🧠 Learnings (1)
📓 Common learnings
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:27-31
Timestamp: 2025-05-09T19:15:26.180Z
Learning: For the MongoDB real-time updates implementation in components/log-viewer-webui/client/src/api/socket, a socket singleton pattern is used where a single shared socket connection is maintained rather than creating multiple connections. The socket lifecycle is managed centrally, with unsubscription handling in the useCursor React hook's cleanup function.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.814Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
🧬 Code Graph Analysis (1)
components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts (1)
components/log-viewer-webui/common/index.ts (2)
  • ServerToClientEvents (79-79)
  • ClientToServerEvents (75-75)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint-check (macos-latest)
  • GitHub Check: lint-check (ubuntu-latest)
🔇 Additional comments (5)
components/log-viewer-webui/client/package.json (1)

28-28: Add socket.io-client dependency for real-time updates.
The socket.io-client version ^4.8.1 aligns with the server setup and is required for the singleton and cursor modules.

components/log-viewer-webui/client/vite.config.ts (3)

3-3: Import vite-tsconfig-paths plugin for alias resolution.
Including tsconfigPaths() enables TypeScript path alias support in Vite as configured in tsconfig.app.json.


9-12: Apply tsconfigPaths() in plugin list.
Ensures that your @common/* imports are correctly resolved at build and runtime.


23-27: Configure WebSocket proxy for Socket.IO.
The /socket.io/ proxy with ws: true is essential for supporting real-time communication between client and server. Looks correct.

components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts (1)

1-4: Verify path alias and extension in import.
The import from @common/index.js uses a .js extension. Confirm that your bundler and TS configuration correctly resolve this alias to the TypeScript source (.ts), or adjust the alias mapping to omit the extension.

Comment on lines +29 to 32
fs: {
// allow serving files from one level up (common folder)
allow: [".."],
},

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Review fs.allow setting for static serving.
Allowing the parent directory ("..") can unintentionally expose files outside the client bundle. Consider restricting this to only the necessary common subfolder or adding explicit path filters to avoid serving sensitive files.

Comment on lines +22 to +23
if (!sharedSocket) {
// eslint-disable-next-line no-warning-comments

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Prefer explicit null check over negation.
Instead of if (!sharedSocket), use an explicit null comparison for clarity and to avoid negating a potentially complex expression:

-if (!sharedSocket) {
+if (sharedSocket === null) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if (!sharedSocket) {
// eslint-disable-next-line no-warning-comments
if (sharedSocket === null) {
// eslint-disable-next-line no-warning-comments

* query the collection.
*/
class MongoCollectionSocket {
collectionName: string;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

sorry for missing this in the last batch of comments

does it need to be public? if not, let's change it to private

@junhaoliao junhaoliao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

For the PR title, how about:

feat(new-webui): Add client library for real-time MongoDB updates.

@davemarco davemarco changed the title feat(new-webui): Adds custom hook to receive real-time updates from Fastify MongoDB query service. feat(new-webui): Add client library for real-time MongoDB updates. May 13, 2025
@davemarco
davemarco requested a review from junhaoliao May 13, 2025 19:44

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (1)

39-46: 🧹 Nitpick (assertive)

Improve typing on find method
The find signature currently uses untyped object parameters and relies on inferred return types. For better type safety and IDE support, you could:

  1. Parameterise query and options with generics (e.g. Q extends Record<string, unknown>, O extends Record<string, unknown>).
  2. Explicitly declare the return type (e.g. : MongoCursorSocket<Q, O> if the cursor class is generic).

Example:

-find (query: object, options: object) {
+find<Q extends Record<string, unknown>, O extends Record<string, unknown>>(
+  query: Q,
+  options: O
+): MongoCursorSocket<Q, O> {
📜 Review details

Configuration used: CodeRabbit UI
Review profile: ASSERTIVE
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1e35e7f and 80e47c5.

📒 Files selected for processing (1)
  • components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (1 hunks)
🧰 Additional context used
📓 Path-based instructions (1)
`**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}`: - Prefer `false == ` rather than `!`.

**/*.{cpp,hpp,java,js,jsx,tpp,ts,tsx}: - Prefer false == <expression> rather than !<expression>.

  • components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:27-31
Timestamp: 2025-05-09T19:15:26.180Z
Learning: For the MongoDB real-time updates implementation in components/log-viewer-webui/client/src/api/socket, a socket singleton pattern is used where a single shared socket connection is maintained rather than creating multiple connections. The socket lifecycle is managed centrally, with unsubscription handling in the useCursor React hook's cleanup function.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.814Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (4)
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:32-38
Timestamp: 2025-05-09T18:05:42.814Z
Learning: When implementing Socket.IO namespaces or connection pooling in a client-server architecture (like the MongoDB collection socket system in the log-viewer-webui), coordinated changes are needed on both client and server sides, making it appropriate to track as a TODO rather than implement piecemeal.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:25-43
Timestamp: 2025-05-09T18:06:33.042Z
Learning: Socket.IO connections in JavaScript require explicit disconnection by calling socket.disconnect() and are not automatically cleaned up when the object is garbage collected.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:25-43
Timestamp: 2025-05-09T18:21:05.843Z
Learning: Socket.IO has a built-in ping/pong mechanism that detects connection failures, but it doesn't automatically disconnect truly inactive but connected clients. By default, it sends pings every 25 seconds and expects pongs within 20 seconds, but this is for connection health monitoring, not application inactivity detection.
Learnt from: davemarco
PR: y-scope/clp#892
File: components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts:27-31
Timestamp: 2025-05-09T19:15:26.180Z
Learning: For the MongoDB real-time updates implementation in components/log-viewer-webui/client/src/api/socket, a socket singleton pattern is used where a single shared socket connection is maintained rather than creating multiple connections. The socket lifecycle is managed centrally, with unsubscription handling in the useCursor React hook's cleanup function.
🧬 Code Graph Analysis (1)
components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (4)
components/log-viewer-webui/common/index.ts (2)
  • ServerToClientEvents (79-79)
  • ClientToServerEvents (75-75)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (2)
  • collectionName (143-146)
  • collectionName (198-210)
components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts (1)
  • getSharedSocket (33-33)
components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts (1)
  • MongoCursorSocket (114-114)
⏰ Context from checks skipped due to timeout of 90000ms (1)
  • GitHub Check: lint-check (macos-latest)
🔇 Additional comments (1)
components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts (1)

16-18: Verify ES private-field support
You’re using the #private field syntax in TypeScript. Ensure that your tsconfig.json target (or build toolchain) supports ES2022 private class fields; otherwise, this may result in a compilation error.

Comment on lines +25 to +29
constructor (collectionName: string) {
this.#socket = getSharedSocket();
this.#collectionName = collectionName;
console.log(`MongoDB collection:${collectionName} initialized.`);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Replace console.log with proper logger
Using console.log for production code can make debugging and log management harder. Consider using a structured logging utility or debug namespace (e.g. debug, winston, or a custom logger) to control log levels and output channels.

ClientToServerEvents,
ServerToClientEvents,
} from "@common/index.js";
import {Socket} from "socket.io-client";

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Prefer type-only import for Socket
Consider using a type-only import for the Socket interface to avoid pulling in the module at runtime if it’s only used for type annotations:

-import {Socket} from "socket.io-client";
+import type {Socket} from "socket.io-client";
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
import {Socket} from "socket.io-client";
-import {Socket} from "socket.io-client";
+import type {Socket} from "socket.io-client";

Comment on lines +20 to +24
/**
* Initalizes socket connection to a MongoDB collection on the server.
*
* @param collectionName
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick (assertive)

Fix typo and clarify JSDoc in constructor
There's a typo in the JSDoc (InitalizesInitializes) and the @param lacks a description. Consider updating to:

- * Initalizes socket connection to a MongoDB collection on the server.
+ * Initializes the shared socket connection for a MongoDB collection on the server.
+ *
+ * @param collectionName - The name of the MongoDB collection to subscribe to.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Initalizes socket connection to a MongoDB collection on the server.
*
* @param collectionName
*/
/**
* Initializes the shared socket connection for a MongoDB collection on the server.
*
* @param collectionName - The name of the MongoDB collection to subscribe to.
*/

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.

2 participants