Skip to content

feat(new-webui): Mongo Framework Server - #814

Closed
AVMatthews wants to merge 26 commits into
y-scope:mainfrom
AVMatthews:mongo-framework-server
Closed

feat(new-webui): Mongo Framework Server#814
AVMatthews wants to merge 26 commits into
y-scope:mainfrom
AVMatthews:mongo-framework-server

Conversation

@AVMatthews

@AVMatthews AVMatthews commented Apr 10, 2025

Copy link
Copy Markdown
Contributor

Description

Server for new framework built on MongoCDC using socket.io meant to replace our current usage of meteor's publish-subscribe framework.

First PR to split #809

TODO: A few listing errors left to solve

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

Manually added collection to mongo replica and tested to see that the contest of the collection could be requested through the client from the server.

Summary by CodeRabbit

  • New Features

    • Enabled real-time reactive MongoDB query subscriptions with live updates via WebSocket.
    • Added user interface support for subscribing and unsubscribing to MongoDB queries.
    • Integrated WebSocket communication for efficient, event-driven backend data updates.
  • Chores

    • Added new dependencies to support WebSocket and real-time functionality.

@AVMatthews
AVMatthews requested a review from a team as a code owner April 10, 2025 20:08
@coderabbitai

coderabbitai Bot commented Apr 10, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

This change introduces a new real-time, reactive MongoDB query subscription system to the log viewer web UI server. The update adds Socket.IO as a dependency and integrates a new Fastify plugin that allows clients to subscribe to MongoDB queries via WebSockets. The system manages query watchers, leverages MongoDB change streams, and emits updates to subscribed clients when query results change. Supporting utilities and TypeScript typings are included for query hashing, MongoDB connection, and event typing. The changes are modular, with new files for the main plugin, watcher management, utilities, and type definitions.

Changes

File(s) Change Summary
components/log-viewer-webui/server/package.json Added socket.io as a new dependency (^4.8.1) to enable WebSocket-based real-time communication.
components/log-viewer-webui/server/src/app.ts Registered the new MongoSocketIoServer Fastify plugin (except in test environments) with configuration from settings, enabling MongoDB and Socket.IO integration.
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts Introduced MongoWatcherCollection class to manage MongoDB change streams, handle client query subscriptions, emit real-time updates via Socket.IO, and clean up watchers when no subscribers remain.
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts Added the MongoSocketIoServer class and Fastify plugin, implementing event-driven management of reactive MongoDB query subscriptions over Socket.IO, including subscription tracking, event handling, and integration with Fastify and MongoDB.
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts Added TypeScript typings for event signatures, response types, socket data, query identifiers, database options, watcher structure, and constants used in the MongoSocketIoServer plugin.
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/utils.ts Added utility functions for converting queries for change streams, generating and parsing query hashes, and initializing MongoDB clients with connection options.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant SocketIoServer
    participant MongoSocketIoServer
    participant MongoWatcherCollection
    participant MongoDB

    Client->>SocketIoServer: Connect (WebSocket)
    SocketIoServer->>MongoSocketIoServer: "collection::init" (collectionName)
    MongoSocketIoServer->>MongoDB: Check collection existence
    MongoDB-->>MongoSocketIoServer: Collection exists/does not exist
    MongoSocketIoServer-->>SocketIoServer: Response (success/error)

    Client->>SocketIoServer: "collection::find::toReactiveArray" (query, options)
    SocketIoServer->>MongoSocketIoServer: Handle subscription
    MongoSocketIoServer->>MongoWatcherCollection: getWatcher(query, queryId, socket)
    MongoWatcherCollection->>MongoDB: Create or reuse ChangeStream
    MongoWatcherCollection-->>MongoSocketIoServer: Watcher ready
    MongoSocketIoServer-->>SocketIoServer: Response (queryId)

    MongoWatcherCollection->>MongoDB: Listen for changes (ChangeStream)
    MongoDB-->>MongoWatcherCollection: Change event
    MongoWatcherCollection->>SocketIoServer: Emit "collection::find::update" (queryId, data) to subscribed clients

    Client->>SocketIoServer: "collection::find::unsubscribe" (queryId)
    SocketIoServer->>MongoSocketIoServer: Handle unsubscription
    MongoSocketIoServer->>MongoWatcherCollection: unsubscribeFromWatcher(queryId, connectionId)
    MongoWatcherCollection-->>MongoSocketIoServer: Unsubscribed/cleaned up

    Client->>SocketIoServer: Disconnect
    SocketIoServer->>MongoSocketIoServer: Handle disconnect
    MongoSocketIoServer->>MongoWatcherCollection: Cleanup all subscriptions for client
Loading
✨ Finishing Touches
  • 📝 Generate Docstrings

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Generate unit testing code for this file.
    • 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 generate unit testing code for this file.
    • @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 generate unit testing code.
    • @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.

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 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 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 for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@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

🧹 Nitpick comments (9)
components/log-viewer-webui/server/src/app.ts (1)

60-65: Consider conditional loading for test environments and ensure fail-safe error handling.

Here you register the MongoReplicaServerPlugin unconditionally. If the test environment does not require a MongoDB connection or real-time communication, you could skip loading the plugin to speed up tests and avoid potential connection overhead. Also, make sure you handle any registration errors gracefully if the plugin’s underlying MongoDB connection fails.

components/log-viewer-webui/server/src/plugins/MongoReplicaServerPlugin.ts (4)

50-62: Include host and port in the error message for better debugging.

When a MongoDB connection error occurs, the thrown error message does not mention the host or port. Including them can ease troubleshooting in multi-environment setups.

-    throw new Error("MongoDB connection error", {cause: e});
+    throw new Error(`MongoDB connection error (host: ${host}, port: ${port})`, {cause: e});

64-81: Add access control in collection initialization if required.

You rely on the user’s payload to identify the desired collection. If there is sensitive data, consider verifying user permissions before allowing clients to initialize certain collections.


83-96: Use consistent comparison style for improved clarity.

Line 90 uses the exclamation operator (!collection.isReferenced()). Per your coding guidelines, prefer false == collection.isReferenced() for boolean checks in TypeScript files.

-    if (!collection.isReferenced()) {
+    if (false == collection.isReferenced()) {

98-116: Consider result size handling for large queries.

As written, .toArray() on large datasets could impact performance. You may benefit from pagination, limiting the returned documents, or streaming results. Let me know if you would like assistance implementing that.

components/log-viewer-webui/server/src/plugins/MongoReplicaServerCollection.ts (4)

8-16: Improve documentation for getQueryHash

The function documentation contains a TODO comment and is missing proper parameter and return descriptions.

Update the JSDoc to better document the purpose, parameters, and return value:

 /**
- * // eslint-disable-next-line no-warning-comments
- * TODO: Improve this? Think about security (other queries should not be able to kick others
- *  offline; maybe add a ref count then), performance, and collision chances.
- *
- * @param query
- * @param options
- * @return
+ * Generates a hash string from the query and options objects to uniquely identify a MongoDB query.
+ * Used for tracking change stream watchers associated with specific queries.
+ * 
+ * @param query - The MongoDB query object
+ * @param options - The MongoDB query options object
+ * @return A string hash representing the combined query and options
  */

55-57: Add validation and type safety to the find method

The find method lacks input validation and type annotations for return value.

-find (query: object, options: object) {
-    return this.collection.find(query, options);
+/**
+ * Find documents in the collection that match the query
+ * 
+ * @param query - The MongoDB query object
+ * @param options - The MongoDB query options object
+ * @return The MongoDB cursor for the query results
+ */
+find (query: object, options: object = {}) {
+    // Validate inputs
+    if (query === null || typeof query !== 'object') {
+        throw new Error('Query must be a valid object');
+    }
+    return this.collection.find(query, options);
 }

62-62: Follow coding conventions for type checking

The code uses string comparison with "undefined" instead of directly checking if the variable is undefined.

-    if ("undefined" === typeof watcher) {
+    if (watcher === undefined) {

70-82: Consider adding a cleanup method for resource management

The class has methods to add, remove, and check references, but lacks a comprehensive cleanup method to release all resources.

Add a cleanup method to close all watchers and reset the collection state:

+/**
+ * Closes all active watchers and resets the collection state
+ */
+async cleanup() {
+    // Close all watchers
+    const closePromises = Array.from(this.watchers.entries()).map(async ([queryHash, watcher]) => {
+        try {
+            await watcher.close();
+        } catch (err) {
+            console.error(`Error closing watcher for queryHash ${queryHash}:`, err);
+        }
+    });
+    
+    await Promise.all(closePromises);
+    
+    // Clear all watchers
+    this.watchers.clear();
+    
+    // Reset count
+    this.count = 0;
+}
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 1e647d4 and e30e019.

⛔ Files ignored due to path filters (1)
  • components/log-viewer-webui/server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (4)
  • components/log-viewer-webui/server/package.json (1 hunks)
  • components/log-viewer-webui/server/src/app.ts (2 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServerCollection.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServerPlugin.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/server/src/app.ts
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServerPlugin.ts
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServerCollection.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: lint-check (macos-latest)
🔇 Additional comments (6)
components/log-viewer-webui/server/package.json (1)

33-33: Confirm compatibility of newly added dependency.

The addition of "socket.io": "^4.8.1" is consistent with introducing WebSocket-based communication. However, if your environment or other dependencies have special version requirements, please verify that this version does not introduce any compatibility issues or vulnerabilities.

components/log-viewer-webui/server/src/app.ts (1)

11-11: Good integration of the MongoReplicaServerPlugin.

Importing the plugin here seems appropriate. No immediate issues are observed.

components/log-viewer-webui/server/src/plugins/MongoReplicaServerPlugin.ts (1)

198-220: Well-structured plugin decoration.

Decorating Fastify with the MongoReplicaServer instance is straightforward and keeps your code modular. No immediate issues are found here.

components/log-viewer-webui/server/src/plugins/MongoReplicaServerCollection.ts (3)

26-30: LGTM: Constructor initializes properties appropriately

The constructor correctly initializes the count, collection, and watchers map.


51-53: LGTM: Boolean check follows coding guidelines

The isReferenced method uses 0 < this.count which aligns with the coding guideline to prefer false == <expression> rather than !<expression>.


73-81: LGTM: Good error handling in removeWatcher method

The method properly handles both the case when a watcher exists and when it doesn't, including error handling for the close operation.

Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServerPlugin.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServerPlugin.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServerCollection.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServerCollection.ts Outdated
* @param options
* @return
*/
const getQueryHash = (query: object, options: object): string => JSON.stringify({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.

🛠️ Refactor suggestion

Improve hash generation to prevent collisions

The current implementation of getQueryHash uses JSON.stringify which doesn't guarantee consistent ordering of object keys. This could lead to different hashes for semantically identical queries with properties in different orders.

Consider using a more robust hashing approach that:

  1. Ensures consistent ordering of keys
  2. Handles potential circular references
-const getQueryHash = (query: object, options: object): string => JSON.stringify({query, options});
+const getQueryHash = (query: object, options: object): string => {
+    // Sort keys for consistent hash generation
+    const sortObjectKeys = (obj: Record<string, any>): Record<string, any> => {
+        return Object.keys(obj).sort().reduce((result, key) => {
+            const value = obj[key];
+            result[key] = value && typeof value === 'object' && !Array.isArray(value) 
+                ? sortObjectKeys(value) 
+                : value;
+            return result;
+        }, {} as Record<string, any>);
+    };
+    
+    return JSON.stringify({
+        query: sortObjectKeys(query as Record<string, any>),
+        options: sortObjectKeys(options as Record<string, any>)
+    });
+};
📝 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
const getQueryHash = (query: object, options: object): string => JSON.stringify({query, options});
const getQueryHash = (query: object, options: object): string => {
// Sort keys for consistent hash generation
const sortObjectKeys = (obj: Record<string, any>): Record<string, any> => {
return Object.keys(obj).sort().reduce((result, key) => {
const value = obj[key];
result[key] = value && typeof value === 'object' && !Array.isArray(value)
? sortObjectKeys(value)
: value;
return result;
}, {} as Record<string, any>);
};
return JSON.stringify({
query: sortObjectKeys(query as Record<string, any>),
options: sortObjectKeys(options as Record<string, any>)
});
};

@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

🧹 Nitpick comments (8)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (6)

28-33: Ensure potential environment variable support for MongoDB connection details.

While hard-coding or passing host/port directly works, you may want to consider environment variables or a configuration file for more flexibility (especially in production). This would reduce the need to recompile or redeploy for different environments.


35-49: Add fallback or validation for port string conversions.

In case the provided port is not a valid string or does not parse correctly, consider adding validation or a fallback to avoid runtime errors.


51-63: Consider improving MongoClient error handling or retries.

Currently, if the client connection fails even temporarily, the error is thrown, stopping the server from functioning. Adding retry logic or graceful fallback could improve reliability.


65-82: Validate collectionName payload to enhance security.

Clients can supply arbitrary strings under collectionName, which might lead to unintended or potentially malicious queries. Consider validating or sanitizing the collection name before usage.

Would you like help drafting a validation approach (e.g., whitelisting, regex-based checks) for collection names?


91-91: Adhere to coding guideline “Prefer false == over !”.

You are using a “not” operator (!collection.isReferenced()). Per the guidelines, it should be false == collection.isReferenced().

- if (!collection.isReferenced()) {
+ if (false == collection.isReferenced()) {

166-196: Configure Socket.IO for production security (e.g., CORS).

Using a default socket server might expose your application to cross-domain requests. Consider specifying CORS or authorization checks, especially if the server is publicly accessible.

components/log-viewer-webui/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts (2)

17-18: Strengthen query-hash collision avoidance.

Relying on a simple JSON stringification for the query hash might lead to collisions if two distinct queries produce equivalent JSON strings. If collisions are rare but possible, consider an additional salt or hashing algorithm like SHA-1 for precision.


59-68: Validate watchers for large or complex queries.

Change streams can be expensive if the queries are very large or frequent. Consider limiting watchers to ensure the server is not overwhelmed.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e30e019 and e64a375.

⛔ Files ignored due to path filters (1)
  • components/log-viewer-webui/server/package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (3)
  • components/log-viewer-webui/server/src/app.ts (2 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/log-viewer-webui/server/src/app.ts
🧰 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/server/src/plugins/MongoReplicaServer/index.ts
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: build (macos-latest)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: lint-check (macos-latest)
🔇 Additional comments (3)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (1)

208-221: Plugin initialization looks good.

The overall structure for registering the MongoReplicaServer with Fastify aligns well with plugin best practices.

components/log-viewer-webui/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts (2)

26-30: Constructor sets up watchers and reference counter cleanly.

No immediate concerns; the structure is straightforward and aligns with intended design.


70-82:

Details

❓ Verification inconclusive

Confirm watcher closure on final collection removal.

If the containing collection is garbage collected, watchers might remain open. Before removing the collection from memory, consider closing all watchers to avoid memory leaks or open streams.


🏁 Script executed:

#!/bin/bash
# Explore references to `this.collections.delete(collectionName)` and confirm watchers are closed.
rg -A 5 $'this.collections.delete(collectionName)'

Length of output: 51


Subject: Verify that watchers are explicitly closed when a collection is finally removed

The current search for references to this.collections.delete(collectionName) produced no output, so we cannot confirm automatically that watchers are closed during final collection deletion. Please manually verify that all watchers linked to a collection are fully closed before the collection is garbage collected. If needed, consider adding explicit code to close any open watchers prior to or during the collection removal process.

  • File: components/log-viewer-webui/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts (Lines 70-82)
  • Action: Manually inspect the collection deletion flow to ensure that removeWatcher is invoked as needed, and check for any cases where watchers might remain open after the collection is removed.

Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/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: 0

♻️ Duplicate comments (2)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (2)

84-90: [Duplicate from previous review] Close watchers properly on disconnect if no queries remain.

Close watchers upon disconnect if no queries remain. Although the disconnect listener decrements the reference count, it does not explicitly close watchers. If the user never unsubscribes, watchers may remain active until isReferenced() returns false. Consider automatically clearing watchers for that socket if the reference count drops to zero.

Also applies to: 92-97


143-144: [Duplicate from previous review] Debounce the "change" handler.

Repeated change events for large or rapidly updating collections can degrade performance. The inline comment references a “FIXME” regarding debouncing. Consider implementing a debounce strategy to avoid overwhelming downstream consumers with frequent updates.

🧹 Nitpick comments (4)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (4)

70-79: Use a more conventional undefined check.

In TypeScript, it is more typical to check if (collection === undefined) or simply if (!collection). Using the string-literal 'undefined' === typeof collection is unusual and may reduce readability. Consider switching to a standard approach.


91-91: Adhere to coding guidelines: prefer false == expression rather than !expression.

The current condition uses the logical NOT operator. According to the project's guidelines, consider using false == <expression> for consistency:

- if (!collection.isReferenced()) {
+ if (false == collection.isReferenced()) {

170-199: Optional improvement for event registration logic.

Enumerating each event-listener pair in an array is effective but can become unwieldy as the application grows. Consider extracting this mapping into a typed configuration object or using a public method to attach these listeners, improving maintainability and making it easier to expand or modify event handling in future updates.


203-225: Consider adding automated tests for plug-in functionality.

This plugin provides complex real-time interactions with MongoDB. A dedicated test suite would help ensure stability and spot regressions early. Tests for events, watcher handling, and error scenarios (e.g., failed connections) are recommended for robust coverage.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between e64a375 and cc24dd2.

📒 Files selected for processing (1)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.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/server/src/plugins/MongoReplicaServer/index.ts
⏰ Context from checks skipped due to timeout of 90000ms (3)
  • GitHub Check: lint-check (macos-latest)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: build (macos-latest)

@davemarco

Copy link
Copy Markdown
Contributor

I am still looking through but just for linting to start

  • we should have comments on top of the new classes roughly explaining what they do
  • Also comments for the internal functions

For some of the linting failures, It looks like they are related to missing types for socket io. They have some docs here that may help for adding missing types - https://socket.io/docs/v4/typescript/

@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: 9

♻️ Duplicate comments (2)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (2)

126-139: 🛠️ Refactor suggestion

Close watchers upon disconnect if no queries remain.

The disconnect listener decrements the reference count but does not explicitly close watchers. If the user never unsubscribes from queries, watchers may remain open until isReferenced() returns false. Consider automatically clearing all watchers for that socket if the ref count drops to zero.

 #getCollectionDisconnectListener (socket: Socket): EventListener<"disconnect"> {
     return () => {
         this.#fastify.log.info(`Socket disconnected: ${socket.id}`);
         const {collectionName} = socket.data as {collectionName: string};
         const collection = this.#collections.get(collectionName);
         if ("undefined" !== typeof collection) {
+            // Clean up any remaining watchers for this socket
+            collection.removeAllWatchersForSocket(socket.id);
             collection.refRemove();
             if (!collection.isReferenced()) {
                 this.#fastify.log.info(`Collection ${collectionName} removed`);
                 this.#collections.delete(collectionName);
             }
         }
     };
 }

Note: This requires adding a new method removeAllWatchersForSocket to the MongoReplicaServerCollection class.


194-200: 🛠️ Refactor suggestion

Consider debouncing the "change" handler.

The inline comment notes the need for debouncing. Repeated "change" events for large or rapidly updating collections could degrade performance.

Implement debouncing to prevent excessive updates:

-            // eslint-disable-next-line @typescript-eslint/no-misused-promises
-            watcher.on("change", async () => {
-                // eslint-disable-next-line no-warning-comments
-                // FIXME: this should be debounced
-                socket.emit("collection::find::update", {
-                    data: await collection.find(query, options).toArray(),
-                });
-            });
+            // Create debounced update function
+            let updateTimeout: NodeJS.Timeout | null = null;
+            const debouncedUpdate = async () => {
+                if (updateTimeout) {
+                    clearTimeout(updateTimeout);
+                }
+                updateTimeout = setTimeout(async () => {
+                    try {
+                        const data = await collection.find(query, options).toArray();
+                        socket.emit("collection::find::update", { data });
+                    } catch (error) {
+                        this.#fastify.log.error(`Error in change update: ${error}`);
+                        socket.emit("error", { message: "Failed to update data" });
+                    }
+                }, 100); // 100ms debounce time
+            };
+            
+            // eslint-disable-next-line @typescript-eslint/no-misused-promises
+            watcher.on("change", debouncedUpdate);
🧹 Nitpick comments (2)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (2)

1-47: Add class-level JSDoc documentation to improve maintainability and readability.

The imports and interface definitions are well-structured, but the file lacks a comprehensive class description at the top level. Adding JSDoc documentation would help future developers understand the purpose and architecture of this server implementation.

+/**
+ * MongoReplicaServer plugin for Fastify
+ * 
+ * This module implements a Socket.IO based server that provides real-time 
+ * data synchronization with MongoDB collections. It allows clients to 
+ * subscribe to collection changes and receive updates when documents change.
+ *
+ * The plugin is part of a new framework built on MongoCDC to replace
+ * the existing usage of Meteor's publish-subscribe framework.
+ */
 import {FastifyInstance} from "fastify";
 import fastifyPlugin from "fastify-plugin";

49-62: Add JSDoc comments to explain the class purpose and constructor.

This class lacks documentation to explain its purpose and how it should be used. Adding JSDoc comments would improve maintainability.

+/**
+ * Manages connections to MongoDB replica sets and handles real-time data
+ * synchronization through Socket.IO.
+ *
+ * This class maintains a collection of MongoDB collections and provides
+ * methods for querying and subscribing to changes in these collections.
+ */
 class MongoReplicaServer {
     #fastify: FastifyInstance;
 
     #collections: Map<string, MongoReplicaServerCollection>;
 
     #mongoDb: Db;
 
+    /**
+     * Creates a new MongoReplicaServer instance.
+     *
+     * @param fastify - The Fastify instance to attach to
+     * @param mongoDb - The MongoDB database instance
+     */
     constructor ({fastify, mongoDb}: {fastify: FastifyInstance; mongoDb: Db}) {
         this.#fastify = fastify;
         this.#collections = new Map();
         this.#mongoDb = mongoDb;
         this.#initializeSocketServer(fastify.server);
     }
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 8626745 and 738f4a4.

📒 Files selected for processing (1)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.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/server/src/plugins/MongoReplicaServer/index.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: lint-check (macos-latest)

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

@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.

as discussed offline, let's remove all references to the name "replica". e.g., MongoReplicaServer -> FastifyMongoServer (i added "Fastify" in my proposal here because the name "MongoServer" alone can be ambiguous)

@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

♻️ Duplicate comments (8)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (8)

100-112: ⚠️ Potential issue

Add cleanup when MongoDB connection fails.

The error handling doesn't properly clean up resources if the connection fails. The MongoClient should be closed in the catch block to prevent resource leaks.

 static async initializeMongoClient (
     {database, host, port}: {database: string; host: string; port: string}
 ): Promise<Db> {
     const mongoUri = `mongodb://${host}:${port}`;
     const mongoClient = new MongoClient(mongoUri);
     try {
         await mongoClient.connect();

         return mongoClient.db(database);
     } catch (e) {
+        await mongoClient.close().catch(closeError => {
+            console.error("Failed to close MongoDB connection after error", closeError);
+        });
         throw new Error("MongoDB connection error", {cause: e});
     }
 }

137-150: 🛠️ Refactor suggestion

Close watchers upon disconnect if no queries remain.

The disconnect listener decrements the reference count but does not explicitly close watchers. If the user never unsubscribes from queries, watchers may remain open until isReferenced() returns false.

 #getCollectionDisconnectListener (socket: CustomSocket) {
     return () => {
         this.#fastify.log.info(`Socket disconnected: ${socket.id}`);
         const {collectionName} = socket.data as {collectionName: string};
         const collection = this.#collections.get(collectionName);
         if ("undefined" !== typeof collection) {
+            // Clean up any remaining watchers for this socket
+            collection.clearWatchersForSocket(socket.id);
             collection.refRemove();
             if (!collection.isReferenced()) {
                 this.#fastify.log.info(`Collection ${collectionName} removed`);
                 this.#collections.delete(collectionName);
             }
         }
     };
 }

Note: This requires implementing a clearWatchersForSocket method in the MongoReplicaServerCollection class to track watchers by socket ID.


187-194: 🛠️ Refactor suggestion

Implement debouncing for the "change" handler.

The inline comment indicates the need for debouncing. Without it, repeated "change" events for large or rapidly updating collections will degrade performance and potentially flood clients with updates.

-            // eslint-disable-next-line @typescript-eslint/no-misused-promises
-            watcher.on("change", async () => {
-                // eslint-disable-next-line no-warning-comments
-                // FIXME: this should be debounced
-                socket.emit("collection::find::update", {
-                    data: await collection.find(query, options).toArray(),
-                });
-            });
+            // Implement debounced change handler
+            let debounceTimer: NodeJS.Timeout | null = null;
+            // eslint-disable-next-line @typescript-eslint/no-misused-promises
+            watcher.on("change", async () => {
+                if (debounceTimer) {
+                    clearTimeout(debounceTimer);
+                }
+                
+                debounceTimer = setTimeout(async () => {
+                    try {
+                        const data = await collection.find(query, options).toArray();
+                        socket.emit("collection::find::update", { data });
+                    } catch (error) {
+                        this.#fastify.log.error(`Error fetching updated data: ${error}`);
+                    }
+                    debounceTimer = null;
+                }, 300); // 300ms debounce time - adjust as needed
+            });

195-198: 🛠️ Refactor suggestion

Add error handling for initial data fetch.

The initial data fetch lacks error handling, which could lead to unhandled exceptions if the query fails.

-            socket.emit("collection::find::update", {
-                data: await collection.find(query, options).toArray(),
-            });
+            try {
+                const initialData = await collection.find(query, options).toArray();
+                socket.emit("collection::find::update", {
+                    data: initialData,
+                });
+            } catch (error) {
+                this.#fastify.log.error(`Error in initial data fetch: ${error}`);
+                socket.emit("error", { message: "Failed to fetch initial data" });
+            }

202-215: 🛠️ Refactor suggestion

Improve unsubscribe handling with validation and feedback.

The unsubscribe handler lacks validation and client feedback, which could make debugging difficult.

 #getCollectionFindUnsubscribeListener (socket: CustomSocket)
     : ClientToServerEvents["collection::find::unsubscribe"] {
     return ({queryHash}) => {
+        // Validate queryHash
+        if (false == queryHash || typeof queryHash !== 'string') {
+            this.#fastify.log.error(`Invalid queryHash: ${queryHash}`);
+            socket.emit('error', { message: 'Invalid queryHash' });
+            return;
+        }
+
         const {collectionName} = socket.data as {collectionName: string};
         this.#fastify.log.info(`Collection name ${collectionName} requested unsubscription`);
         const collection = this.#collections.get(collectionName);

         if ("undefined" === typeof collection) {
+            socket.emit('error', { message: 'Collection not initialized' });
             return;
         }

-        collection.removeWatcher(queryHash);
+        try {
+            const removed = collection.removeWatcher(queryHash);
+            if (removed) {
+                socket.emit('collection::find::unsubscribed', { queryHash });
+            } else {
+                socket.emit('error', { message: 'Watcher not found' });
+            }
+        } catch (error) {
+            this.#fastify.log.error(`Error removing watcher: ${error}`);
+            socket.emit('error', { message: 'Failed to unsubscribe' });
+        }
     };
 }

227-237: 🛠️ Refactor suggestion

Add error handling and cleanup for the plugin.

The plugin lacks error handling for server creation and cleanup logic for when the plugin is unregistered, which could lead to resource leaks.

 const MongoReplicaServerPlugin = async (
     app: FastifyInstance,
     options: {host: string; port: number; database: string}
 ) => {
-    await MongoReplicaServer.create({
-        fastify: app,
-        host: options.host,
-        port: options.port.toString(),
-        database: options.database,
-    });
+    let server;
+    try {
+        server = await MongoReplicaServer.create({
+            fastify: app,
+            host: options.host,
+            port: options.port.toString(),
+            database: options.database,
+        });
+        
+        // Add cleanup logic when Fastify closes
+        app.addHook('onClose', async () => {
+            app.log.info('Closing MongoDB connections...');
+            // Add method to MongoReplicaServer to close MongoDB connections
+            // await server.close();
+        });
+    } catch (error) {
+        app.log.error(`Failed to create MongoDB replica server: ${error}`);
+        throw error;
+    }
 };

84-98: 🛠️ Refactor suggestion

Correct type inconsistency for port parameter.

The port parameter is defined as a string in the create method, but as a number in the plugin function (line 229). This type inconsistency could lead to type errors.

 static async create ({
     fastify,
     database,
     host,
     port,
 }: {
     fastify: FastifyInstance;
     database: string;
     host: string;
-    port: string;
+    port: number;
 }): Promise<MongoReplicaServer> {
-    const mongoDb = await MongoReplicaServer.initializeMongoClient({database, host, port});
+    const mongoDb = await MongoReplicaServer.initializeMongoClient({database, host, port: port.toString()});

     return new MongoReplicaServer({fastify, mongoDb});
 }

152-168: 🛠️ Refactor suggestion

Add validation for collection name.

The collection initialization logic lacks validation for the collection name, which could lead to security issues or unexpected behavior.

 #getCollectionInitListener (socket: CustomSocket): ClientToServerEvents["collection::init"] {
     return ({collectionName}) => {
+        // Validate collection name
+        if (false == collectionName || typeof collectionName !== 'string' || !collectionName.trim()) {
+            this.#fastify.log.error(`Invalid collection name requested: ${collectionName}`);
+            socket.emit('error', { message: 'Invalid collection name' });
+            return;
+        }
+
         this.#fastify.log.info(`Collection name ${collectionName} requested`);

         let collection = this.#collections.get(collectionName);
         if ("undefined" === typeof collection) {
             collection = new MongoReplicaServerCollection(
                 this.#mongoDb,
                 collectionName
             );
             this.#collections.set(collectionName, collection);
         }
         collection.refAdd();

         socket.data.collectionName = collectionName;
     };
 }
🧹 Nitpick comments (2)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (2)

1-69: Consider adding ServerError type for consistent error handling.

The error handling across the codebase is inconsistent. Creating a standardized error type would improve code consistency and client error handling.

+/**
+ * Standardized server error type
+ */
+interface ServerError {
+  message: string;
+  code?: string;
+  details?: unknown;
+}
+
 type ClientToServerEvents = {
     "disconnect": (reqArgs: never) => void;
     "collection::init": (reqArgs: {
         collectionName: string;
     }) => void;
     "collection::find::toArray": (
         reqArgs: {
             query: object;
             options: object;
         },
         callback: (respArgs: {
             data: Document[];
         } | {
-            error: string;
+            error: ServerError;
         }) => void
     ) => Promise<void>;
     // Update other error types similarly...

This would allow for more detailed error information to be passed to clients, including error codes, and would make error handling more consistent across the codebase.


218-226: Improve JSDoc comments for the plugin.

The JSDoc comments for MongoReplicaServerPlugin are minimal and could be improved with more detailed descriptions of parameters and the plugin's purpose.

 /**
- * MongoDB replica set plugin for Fastify.
+ * MongoDB replica set plugin for Fastify. This plugin establishes a connection to a MongoDB
+ * replica set and sets up Socket.IO handlers for real-time collection queries and subscriptions.
+ * It replaces the existing Meteor publish-subscribe framework with a more efficient solution
+ * based on MongoDB Change Data Capture.
  *
- *
- * @param app
- * @param options
- * @param options.database
- * @param options.host
- * @param options.port
+ * @param app - The Fastify instance to register the plugin with
+ * @param options - Configuration options for the MongoDB connection
+ * @param options.database - The name of the MongoDB database to connect to
+ * @param options.host - The hostname of the MongoDB server
+ * @param options.port - The port number of the MongoDB server
+ * @returns {Promise<void>} - A promise that resolves when the plugin is registered
  */
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 738f4a4 and d9e409e.

📒 Files selected for processing (1)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.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/server/src/plugins/MongoReplicaServer/index.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: lint-check (macos-latest)

Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/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: 0

♻️ Duplicate comments (1)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (1)

146-167: Missing "collection::find::toArray" event registration.

The client-to-server events define a "collection::find::toArray" event, but it is never registered in the #initializeSocketServer method. This prevents clients from calling this functionality.

🧹 Nitpick comments (3)
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts (1)

183-183: Use 'false == condition' instead of '!condition' to comply with coding guidelines.

Replace:

if (!collection.isReferenced()) {
    ...
}

with:

-if (!collection.isReferenced()) {
+if (false == collection.isReferenced()) {
components/log-viewer-webui/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts (2)

8-11: Address the security TODO comment.

The in-file TODO hints at potential security/enforcement improvements (e.g. ensuring watchers cannot be undesirably terminated by other clients). Consider access controls or user-bound watchers to mitigate abuse and collisions.

Would you like help drafting a secure design that integrates user-specific watchers, preventing unauthorized unsubscriptions?


130-137: Use the Fastify logger for consistent logging.

The code uses console.error and console.warn for watcher closure and missing watcher warnings. For consistency and improved observability, replace them with this.#fastify.log.error or this.#fastify.log.warn.

- console.error(`Error closing watcher for queryHash ${queryHash}:`, err);
+ this.#fastify.log.error(`Error closing watcher for queryHash ${queryHash}: ${err}`);

- console.warn(`No watcher found for queryHash ${queryHash}`);
+ this.#fastify.log.warn(`No watcher found for queryHash ${queryHash}`);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d9e409e and c7c49ba.

📒 Files selected for processing (2)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.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/server/src/plugins/MongoReplicaServer/MongoReplicaServerCollection.ts
  • components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: lint-check (macos-latest)

@junhaoliao
junhaoliao requested a review from davemarco April 13, 2025 00:53

@davemarco davemarco 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.

I did my first pass of the code.

Two larger comments.

  1. It looks like each client can only subscribe to a single collection at once. Is that intended? It may make sense to add support to subscribe to multiple collections. Let me know
  2. Right now it looks like the client needs to disconnect to unsubscribe from a collection. Maybe it makes senses to add a "collection::unsubscribe" command, so the client can remove the collection itself?.

I will more at style in the next review.

Edit per discussion - The client is set up to open multiple sockets. As a result, it can support multiple collections. We can look into moving to a single socket at a later date.

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.

We should probably register await fastify.register(fastifySocketIO);, and use their existing plugin, instead of setting up socket.io with fastify.server

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 now we will do our own registration, since https://www.npmjs.com/package/fastify-socket.io does not support Fastify v5 or above.

Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
Comment thread components/log-viewer-webui/server/src/plugins/MongoReplicaServer/index.ts Outdated
@davemarco

davemarco commented Apr 14, 2025

Copy link
Copy Markdown
Contributor

Here is an option for the client side using only one socket, but maintaining something close to the current interface. Not neccesary for now, but potential later optimization.

(1a) shared socket
The main benefit is that the browser limits websockets to domain to 255 per this - https://news.ycombinator.com/item?id=30314281

https://dev.to/bilelsalemdev/understanding-the-singleton-pattern-in-typescript-4kep#:~:text=Singleton%20Pattern%20with%20Socket.IO,ensure%20consistent%20and%20efficient%20communication.

We could have a new function on the client

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

let sharedSocket: Socket | null = null;

export function getSharedSocket(): Socket {
    if (!sharedSocket) {
        // You can pass a URL here if needed (e.g., from environment vars)
        sharedSocket = io();
    }
    return sharedSocket;
}

then when creating new collection on the client, it uses the shared socket across all components

class MongoReplicaCollection {
    private socket: Socket;

    constructor(collectionName: string) {
        this.socket = getSharedSocket();

        this.socket.emit("collection::init", {
            collectionName: collectionName,
        });
    }

Then for the cursor for the component could listen on something like this
this.socket.on("collection::find::update${hash})",

where the hash is the collection, query, options.

1b) Even if we dont do shared socket, it may still sense to still use a different event name for reactive arrays from the same client from the same collection. Something like this.socket.on("collection::find::update${hash})", where the hash is the query, option. It looks like if the client has multiple queries to the same collection, the notification will collide on the same event since they have the same name? Another option (1c) is maybe to force only one cursor per collection

(2) Rooms.
Also another potential improvement is to consider rooms each query hash. Then we don't need to rerun the query for every client that subscribes.

@junhaoliao

Copy link
Copy Markdown
Member

Here is an option for the client side using only one socket, but maintaining something close to the current interface. Not neccesary for now, but potential later optimization.

(1a) shared socket The main benefit is that the browser limits websockets to domain to 255 per this - https://news.ycombinator.com/item?id=30314281

https://dev.to/bilelsalemdev/understanding-the-singleton-pattern-in-typescript-4kep#:~:text=Singleton%20Pattern%20with%20Socket.IO,ensure%20consistent%20and%20efficient%20communication.

We could have a new function on the client

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

let sharedSocket: Socket | null = null;

export function getSharedSocket(): Socket {
    if (!sharedSocket) {
        // You can pass a URL here if needed (e.g., from environment vars)
        sharedSocket = io();
    }
    return sharedSocket;
}

then when creating new collection on the client, it uses the shared socket across all components

class MongoReplicaCollection {
    private socket: Socket;

    constructor(collectionName: string) {
        this.socket = getSharedSocket();

        this.socket.emit("collection::init", {
            collectionName: collectionName,
        });
    }

Then for the cursor for the component could listen on something like this this.socket.on("collection::find::update${hash})",

where the hash is the collection, query, options.

1a) Even if we dont do shared socket, it may still sense to still use a different event name for reactive arrays from the same client from the same collection. Something like this.socket.on("collection::find::update${hash})", where the hash is the query, option. It looks like if the client has multiple queries to the same collection, the notification will collide on the same event since they have the same name? Another option (1c) is maybe to force only one cursor per collection

(2) Rooms. Also another potential improvement is to consider rooms each query hash. Then we don't need to rerun the query for every client that subscribes.

(1a) we should do shared socket on the client side given it's been fairly clear how it can be achived
(1b) assigning different event names might cause challenge working with TS. instead, I agree we can include the query hash / identifier in the server->client update payload. it's likely required if we're to share the same socket on the client side; and even if not, it will be good for some sanity check
(2) i agree we should use rooms now, given how we will be able to share the same watcher across requests associated with an identical query. like in our latest discussion, we proposed that we want to wrap the watcher instance with a list of subscribers and only free the watcher if there is no subscriber left.

@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

🧹 Nitpick comments (7)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.ts (1)

20-21: URI should include the database for correct auth / read preferences

const mongoUri = \mongodb://${host}:${port}`;omits the database name, which forces the driver to issue a secondisMastercall when selectingdb(database). Including /${database}` in the URI simplifies the handshake and respects per‑DB authentication rules.

components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (3)

238-243: Style: follow project guideline “false == ” instead of negation

Coding‑guidelines specify avoiding the ! operator. Replace:

if (!exists) {

with:

if (false == exists) {

This pops up in several places (!exists, !collection, etc.). A quick search‑replace will keep lint happy.


284-300: Race‑prone manual key generation

#getQueryId walks Map entries and computes Math.max(...) + 1. Under concurrent awaits two subscribers could grab the same id. An auto‑incrementing field is simpler and atomic:

private nextQueryId = 0;

private #getQueryId(...) {
    for ()
    return existing;

    return this.nextQueryId++;
}

178-199: Sequential await inside for‑loops slows disconnection

#collectionDisconnectListener awaits inside nested for loops. If a client was subscribed to many queries the disconnect stalls. Gather the Promises and await Promise.all(...) instead to clean up in parallel.

components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts (3)

108-116: Duplicate subscriber IDs possible – favour a Set

watcher.subscribers.push(connectionId) can insert the same id multiple times if a client re‑subscribes. Replace the array with Set<string> to guarantee uniqueness and O(1) removals.


140-142: Prefer Fastify logger over console.error

Direct console calls bypass Fastify’s logging levels/serialisers. Inject a logger or pass it in the constructor so errors integrate with the server’s log pipeline.


69-74: Reference counter underflow guard is silent

A warning is printed but the counter can still go negative if refRemove() is mis‑used multiple times. Consider throwing or at least keeping this.count = 0 to avoid negative numbers that later break isReferenced().

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between c7c49ba and fdf6286.

📒 Files selected for processing (4)
  • components/log-viewer-webui/server/src/app.ts (2 hunks)
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/log-viewer-webui/server/src/app.ts
🧰 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/server/src/plugins/FastifyMongoServer/utils.ts
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts
🧬 Code Graph Analysis (1)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (1)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.ts (1)
  • initializeMongoClient (31-31)
⏰ Context from checks skipped due to timeout of 90000ms (2)
  • GitHub Check: lint-check (ubuntu-latest)
  • GitHub Check: lint-check (macos-latest)

Comment on lines +15 to +29
const initializeMongoClient = async function (
database: string,
host: string,
port: string
): Promise<Db> {
const mongoUri = `mongodb://${host}:${port}`;
const mongoClient = new MongoClient(mongoUri);
try {
await mongoClient.connect();

return mongoClient.db(database);
} catch (e) {
throw new Error("MongoDB connection error", {cause: e});
}
};

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 returning a cleanup handle to avoid leaked Mongo connections

initializeMongoClient opens a long‑lived MongoClient but never exposes a way for callers (or Fastify’s onClose) to close it. In production this can leave idle sockets hanging around after hot‑reloads or test runs, eventually exhausting the MongoDB connection pool.

-const initializeMongoClient = async function (
+const initializeMongoClient = async function (
     database: string,
     host: string,
     port: string
-): Promise<Db> {
+): Promise<{db: Db; close: () => Promise<void>}> {-    await mongoClient.connect();
-    return mongoClient.db(database);
+    await mongoClient.connect();
+    return {
+        db: mongoClient.db(database),
+        close: () => mongoClient.close(),
+    };
 }

Fastify plugins can then register:

fastify.addHook("onClose", async () => {
    await mongoHandle.close();
});

Comment on lines +149 to +156
#initializeSocketServer (httpServer: HttpServer) {
const io = new Server<
ClientToServerEvents,
ServerToClientEvents,
InterServerEvents,
SocketData
>(httpServer);

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

Socket.IO initialisation lacks CORS and path hardening

The server is created with default settings:

const io = new Server(httpServer);

By default this will:

  1. Accept connections from any origin.
  2. Attach to the root path (/socket.io) which may collide with other middleware.

Please restrict origins and explicitly set the path, e.g.:

-const io = new Server(httpServer);
+const io = new Server(httpServer, {
+    path: "/ws",
+    cors: {
+        origin: ["https://your-ui.example.com"],
+        methods: ["GET", "POST"],
+    },
+});

This blocks drive‑by websocket abuse and aligns with security best‑practices.

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

ghost 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.

I added some more comments. I am still looking through, and will add more later.

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

ghost 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: 9

♻️ Duplicate comments (3)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.ts (1)

58-72: Consider returning a cleanup handle to avoid leaked Mongo connections

initializeMongoClient opens a long-lived MongoClient but never exposes a way for callers (or Fastify's onClose) to close it. In production this can leave idle sockets hanging around after hot-reloads or test runs, eventually exhausting the MongoDB connection pool.

 const initializeMongoClient = async function (
     database: string,
     host: string,
     port: string
-): Promise<Db> {
+): Promise<{db: Db; close: () => Promise<void>}> {
     const mongoUri = `mongodb://${host}:${port}`;
     const mongoClient = new MongoClient(mongoUri);
     try {
         await mongoClient.connect();

-        return mongoClient.db(database);
+        return {
+            db: mongoClient.db(database),
+            close: () => mongoClient.close(),
+        };
     } catch (e) {
         throw new Error("MongoDB connection error", {cause: e});
     }
 };

Fastify plugins can then register:

fastify.addHook("onClose", async () => {
    await mongoHandle.close();
});
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (2)

116-124: Socket.IO initialisation lacks CORS and path hardening

The server is created with default settings, which could expose your application to security risks.

 this.#io = new Server<
     ClientToServerEvents,
     ServerToClientEvents,
     SocketData
->(fastify.server);
+>(fastify.server, {
+    path: "/ws",
+    cors: {
+        origin: ["https://your-ui.example.com"],
+        methods: ["GET", "POST"],
+    },
+});

This blocks drive-by websocket abuse and aligns with security best practices.


342-378: N listeners per watcher ⇒ N² events

Each new subscriber attaches its own change listener to the same ChangeStream. When many clients share a query, multiple listeners fire per change, which can impact performance.

Move the listener creation into MongoServerCollection.getWatcher when the watcher is first created, and inside it broadcast to the room derived from queryHash. Only one listener per watcher will keep event density O(1).

🧹 Nitpick comments (6)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (4)

221-222: Use positive condition for better readability

Prefer positive conditions over negated ones for better readability.

-if (false === collections?.includes(collectionName)) {
+if (!collections?.includes(collectionName)) {
     collections.push(collectionName);
 }

292-293: Use positive condition for better readability

Prefer positive conditions over negated ones for better readability.

-if (false === queries?.includes(queryId)) {
+if (!queries?.includes(queryId)) {
     queries.push(queryId);
 }

298-300: Use positive condition for better readability

Prefer positive conditions over negated ones for better readability. This pattern appears throughout the code.

-if (false === this.#queryIdToCollectionNameMap.has(queryId)) {
+if (!this.#queryIdToCollectionNameMap.has(queryId)) {
     this.#queryIdToCollectionNameMap.set(queryId, collectionName);
 }

403-404: Use positive condition for better readability

This follows the same pattern. Consider updating all similar instances in the file for consistency.

-if (false === queryIds.includes(queryId)) {
+if (!queryIds.includes(queryId)) {
     return;
 }
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts (2)

119-149: Improve throttled update mechanism

The current implementation uses a mix of direct emission and timeout-based throttling which could be simplified. Also, the error is logged to console instead of using a proper logger.

Here's a more concise implementation using a throttling mechanism:

const emitUpdate = async () => {
-    const currentTime = Date.now();
-
-    if (updateTimeout <= currentTime - lastEmitTime) {
-        lastEmitTime = currentTime;
-        this.io.to(`${queryId}`).emit("collection::find::update", {
-            queryId: queryId,
-            data: await this.collection.find(query, options).toArray(),
-        });
-
-        return;
-    }
-
     if (!pendingUpdate) {
         pendingUpdate = true;
-        // eslint-disable-next-line @typescript-eslint/no-misused-promises
-        setTimeout(async () => {
+        const timeToNextUpdate = Math.max(0, (lastEmitTime + updateTimeout) - Date.now());
+        setTimeout(() => {
+            void (async () => {
+                lastEmitTime = Date.now();
+                try {
+                    const data = await this.collection.find(query, options).toArray();
+                    this.io.to(`${queryId}`).emit("collection::find::update", {
+                        queryId: queryId,
+                        data: data,
+                    });
+                } catch (error) {
+                    // Use proper logger instead of console
+                    console.error("Error fetching data for update:", error);
+                } finally {
+                    pendingUpdate = false;
+                }
+            })();
+        }, timeToNextUpdate);
+    }
+};

173-177: Apply consistent style for conditional checks

To maintain consistency with the code style used elsewhere in the project:

-if (1 < watcher.subscribers.length) {
+if (watcher.subscribers.length > 1) {
     // Remove the connectionId from the subscribers list
     watcher.subscribers = watcher.subscribers.filter((id) => id !== connectionId);

     removed = false;
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between fdf6286 and 440e459.

📒 Files selected for processing (3)
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.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/server/src/plugins/FastifyMongoServer/index.ts
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.ts
  • components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts
🧬 Code Graph Analysis (2)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.ts (1)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (4)
  • collectionName (207-210)
  • collectionName (289-301)
  • collectionName (312-330)
  • collectionName (396-425)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts (2)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts (4)
  • collectionName (207-210)
  • collectionName (289-301)
  • collectionName (312-330)
  • collectionName (396-425)
components/log-viewer-webui/server/src/plugins/FastifyMongoServer/utils.ts (1)
  • convertFindToChangeStreamQuery (74-74)

Comment on lines +31 to +41
* TODO: Improve this? Think about security (other queries should not be able to kick others
* offline; maybe add a ref count then), performance, and collision chances.
*
* Generates a unique hash for a given query and options.
* This hash is used to identify and manage change streams for specific queries.
*
* @param collectionName
* @param query The query object to be hashed.
* @param options The options object to be hashed.
* @return A string representing the unique hash for the query and options.
*/

ghost Apr 26, 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

TODO comment needs to be addressed

The TODO comment indicates that the query hash generation needs improvement for security, performance, and collision avoidance.

Consider implementing a more robust solution for generating query hashes:

-// eslint-disable-next-line no-warning-comments
-TODO: Improve this? Think about security (other queries should not be able to kick others
- offline; maybe add a ref count then), performance, and collision chances.
-
 /**
  * Generates a unique hash for a given query and options.
  * This hash is used to identify and manage change streams for specific queries.
  *
  * @param collectionName
  * @param query The query object to be hashed.
  * @param options The options object to be hashed.
  * @return A string representing the unique hash for the query and options.
  */
const getQueryHash = function (
    collectionName: string,
    query: object,
    options: object
): string {
-    return JSON.stringify({collectionName, query, options});
+    // Use a deterministic JSON stringifier to handle key order consistency
+    const stringifyStable = (obj: object): string => {
+        const allKeys: string[] = [];
+        const seen = new WeakMap();
+        
+        // Collect all keys
+        JSON.stringify(obj, (key, value) => {
+            if (typeof value === 'object' && value !== null) {
+                if (seen.has(value)) {
+                    return '[Circular]';
+                }
+                seen.set(value, true);
+                allKeys.push(...Object.keys(value));
+            }
+            return value;
+        });
+        
+        // Create a stable version with sorted keys
+        return JSON.stringify(obj, allKeys.sort());
+    };
+    
+    return stringifyStable({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
* TODO: Improve this? Think about security (other queries should not be able to kick others
* offline; maybe add a ref count then), performance, and collision chances.
*
* Generates a unique hash for a given query and options.
* This hash is used to identify and manage change streams for specific queries.
*
* @param collectionName
* @param query The query object to be hashed.
* @param options The options object to be hashed.
* @return A string representing the unique hash for the query and options.
*/
/**
* Generates a unique hash for a given query and options.
*
* @param collectionName
* @param query The query object to be hashed.
* @param options The options object to be hashed.
* @return A string representing the unique hash for the query and options.
*/
const getQueryHash = function (
collectionName: string,
query: object,
options: object
): string {
// Use a deterministic JSON stringifier to handle key order consistency
const stringifyStable = (obj: object): string => {
const allKeys: string[] = [];
const seen = new WeakMap();
// Collect all keys
JSON.stringify(obj, (key, value) => {
if (typeof value === 'object' && value !== null) {
if (seen.has(value)) {
return '[Circular]';
}
seen.set(value, true);
allKeys.push(...Object.keys(value));
}
return value;
});
// Create a stable version with sorted keys
return JSON.stringify(obj, allKeys.sort());
};
return stringifyStable({ collectionName, query, options });
};

Comment on lines +271 to +274
this.#fastify.log
.error(`Error checking collection existence:
${collectionName} - ${(error as Error).error}`);
callback({

ghost Apr 26, 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

Fix error handling and string formatting

The error message has template literal syntax issues causing incorrect formatting and potentially exposing internal error information to clients.

 this.#fastify.log
-    .error(`Error checking collection existence: 
-        ${collectionName} - ${(error as Error).error}`);
+    .error(`Error checking collection existence: ${collectionName}`, error);
 callback({
     collectionName: collectionName,
     error: "An error occurred while checking the collection.",
 });
📝 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.#fastify.log
.error(`Error checking collection existence:
${collectionName} - ${(error as Error).error}`);
callback({
this.#fastify.log
.error(`Error checking collection existence: ${collectionName}`, error);
callback({
collectionName: collectionName,
error: "An error occurred while checking the collection.",
});

Comment on lines +119 to +126
this.#io = new Server<
ClientToServerEvents,
ServerToClientEvents,
SocketData
>(fastify.server);

this.#startSocketListeners();
}

ghost Apr 26, 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

Enhance collection management with strong error handling

Make sure you retain a reference to the MongoDB client to enable proper cleanup when Fastify is shutting down. You'll need to update the static create method and MongoDB client initialization.

 static async create (
     fastify: FastifyInstance,
     database: string,
     host: string,
     port: number
 ): Promise<MongoSocketIoServer> {
-    const mongoDb = await initializeMongoClient(
+    const mongoClient = await initializeMongoClient(
         database,
         host,
         port.toString()
     );
 
-    return new MongoSocketIoServer(fastify, mongoDb);
+    const instance = new MongoSocketIoServer(fastify, mongoClient.db);
+    
+    // Register cleanup hook to close MongoDB connection when Fastify shuts down
+    fastify.addHook("onClose", async () => {
+        await mongoClient.close();
+    });
+    
+    return instance;
 }
📝 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.#io = new Server<
ClientToServerEvents,
ServerToClientEvents,
SocketData
>(fastify.server);
this.#startSocketListeners();
}
static async create (
fastify: FastifyInstance,
database: string,
host: string,
port: number
): Promise<MongoSocketIoServer> {
- const mongoDb = await initializeMongoClient(
+ const mongoClient = await initializeMongoClient(
database,
host,
port.toString()
);
- return new MongoSocketIoServer(fastify, mongoDb);
+ const instance = new MongoSocketIoServer(fastify, mongoClient.db);
+
+ // Register cleanup hook to close MongoDB connection when Fastify shuts down
+ fastify.addHook("onClose", async () => {
+ await mongoClient.close();
+ });
+
+ return instance;
}

Comment on lines +181 to +182
console.error(`Error closing watcher for queryId ${queryId}:`, err);
});

ghost Apr 26, 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

Use proper logging instead of console.error

Replace console logging with the provided logging mechanism for better consistency and control over log levels.

-console.error(`Error closing watcher for queryId ${queryId}:`, err);
+// Pass io server's logger or inject a logger dependency in constructor
+this.logger.error(`Error closing watcher for queryId ${queryId}:`, err);

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

Comment on lines +186 to +187
console.warn(`No watcher found for queryId ${queryId}`);
}

ghost Apr 26, 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

Use proper logging instead of console.warn

Replace console logging with the provided logging mechanism for better consistency and control over log levels.

-console.warn(`No watcher found for queryId ${queryId}`);
+// Pass io server's logger or inject a logger dependency in constructor
+this.logger.warn(`No watcher found for queryId ${queryId}`);

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

Comment on lines +67 to +68
console.warn("Attempted to remove reference when count is already 0");
}

ghost Apr 26, 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

Use proper logging instead of console.warn

Replace console logging with the provided logging mechanism for better consistency and control over log levels.

-console.warn("Attempted to remove reference when count is already 0");
+// Pass io server's logger or inject a logger dependency in constructor
+this.logger.warn("Attempted to remove reference when count is already 0");

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

Comment on lines +143 to +144
console.error("Error fetching data for final update:", error);
} finally {

ghost Apr 26, 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

Use proper logging instead of console.error

Replace console logging with the provided logging mechanism for better consistency and control over log levels.

-console.error("Error fetching data for final update:", error);
+// Pass io server's logger or inject a logger dependency in constructor
+this.logger.error("Error fetching data for final update:", error);

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

Comment on lines +151 to +152
// eslint-disable-next-line @typescript-eslint/no-misused-promises
watcher.changeStream.on("change", emitUpdate);

ghost Apr 26, 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

Improve change stream listener efficiency

The current implementation adds a unique change listener for each socket connection. For many subscribers to the same query, this results in multiple redundant executions of the same query.

Refactor the watcher management to ensure only one change listener per change stream, regardless of the number of subscribers:

-// eslint-disable-next-line @typescript-eslint/no-misused-promises
-watcher.changeStream.on("change", emitUpdate);
+// Only add the change listener once when watcher is created
+if (watcher.subscribers.length === 1) {
+    // eslint-disable-next-line @typescript-eslint/no-misused-promises
+    watcher.changeStream.on("change", emitUpdate);
+}
📝 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-next-line @typescript-eslint/no-misused-promises
watcher.changeStream.on("change", emitUpdate);
// Only add the change listener once when watcher is created
if (watcher.subscribers.length === 1) {
// eslint-disable-next-line @typescript-eslint/no-misused-promises
watcher.changeStream.on("change", emitUpdate);
}

Comment on lines +28 to +51
class MongoServerCollection {
// Reference count for active subscriptions
private count: number;

// MongoDB collection instance
private collection: Collection;

private io: Server;

// Map of active change streams keyed by queryId
private watchers: Map<number, Watcher> = new Map();

/**
* Creates an instance of MongoReplicaServerCollection.
*
* @param collectionName The name of the collection to manage.
* @param io The Socket.IO server instance.
* @param mongoDb The MongoDB database instance.
*/
constructor (collectionName: string, io: Server, mongoDb: Db) {
this.count = 0;
this.collection = mongoDb.collection(collectionName);
this.io = io;
}

ghost Apr 26, 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

Add proper logging support

The class needs a logger for consistent log handling rather than using console statements. Consider adding a logger dependency to the constructor.

 class MongoServerCollection {
     // Reference count for active subscriptions
     private count: number;
 
     // MongoDB collection instance
     private collection: Collection;
 
     private io: Server;
+    
+    // Logger for consistent logging
+    private logger: any; // Use appropriate logger interface type here
 
     // Map of active change streams keyed by queryId
     private watchers: Map<number, Watcher> = new Map();
 
     /**
      * Creates an instance of MongoReplicaServerCollection.
      *
      * @param collectionName The name of the collection to manage.
      * @param io The Socket.IO server instance.
      * @param mongoDb The MongoDB database instance.
+     * @param logger The logger instance.
      */
-    constructor (collectionName: string, io: Server, mongoDb: Db) {
+    constructor (collectionName: string, io: Server, mongoDb: Db, logger: any) {
         this.count = 0;
         this.collection = mongoDb.collection(collectionName);
         this.io = io;
+        this.logger = logger;
     }

Then, update the call site in FastifyMongoServer/index.ts to pass the Fastify logger:

// In index.ts
collection = new MongoServerCollection(collectionName, this.#io, this.#mongoDb);
// becomes:
collection = new MongoServerCollection(collectionName, this.#io, this.#mongoDb, this.#fastify.log);
📝 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
class MongoServerCollection {
// Reference count for active subscriptions
private count: number;
// MongoDB collection instance
private collection: Collection;
private io: Server;
// Map of active change streams keyed by queryId
private watchers: Map<number, Watcher> = new Map();
/**
* Creates an instance of MongoReplicaServerCollection.
*
* @param collectionName The name of the collection to manage.
* @param io The Socket.IO server instance.
* @param mongoDb The MongoDB database instance.
*/
constructor (collectionName: string, io: Server, mongoDb: Db) {
this.count = 0;
this.collection = mongoDb.collection(collectionName);
this.io = io;
}
// File: components/log-viewer-webui/server/src/plugins/FastifyMongoServer/MongoServerCollection.ts
class MongoServerCollection {
// Reference count for active subscriptions
private count: number;
// MongoDB collection instance
private collection: Collection;
private io: Server;
// Logger for consistent logging
private logger: any; // Use appropriate logger interface type here
// Map of active change streams keyed by queryId
private watchers: Map<number, Watcher> = new Map();
/**
* Creates an instance of MongoReplicaServerCollection.
*
* @param collectionName The name of the collection to manage.
* @param io The Socket.IO server instance.
* @param mongoDb The MongoDB database instance.
* @param logger The logger instance.
*/
constructor(
collectionName: string,
io: Server,
mongoDb: Db,
logger: any
) {
this.count = 0;
this.collection = mongoDb.collection(collectionName);
this.io = io;
this.logger = logger;
}
// ...
}
Suggested change
class MongoServerCollection {
// Reference count for active subscriptions
private count: number;
// MongoDB collection instance
private collection: Collection;
private io: Server;
// Map of active change streams keyed by queryId
private watchers: Map<number, Watcher> = new Map();
/**
* Creates an instance of MongoReplicaServerCollection.
*
* @param collectionName The name of the collection to manage.
* @param io The Socket.IO server instance.
* @param mongoDb The MongoDB database instance.
*/
constructor (collectionName: string, io: Server, mongoDb: Db) {
this.count = 0;
this.collection = mongoDb.collection(collectionName);
this.io = io;
}
// File: components/log-viewer-webui/server/src/plugins/FastifyMongoServer/index.ts
- collection = new MongoServerCollection(collectionName, this.#io, this.#mongoDb);
+ collection = new MongoServerCollection(
+ collectionName,
+ this.#io,
+ this.#mongoDb,
+ this.#fastify.log
+ );

@davemarco

ghost commented Apr 29, 2025

Copy link
Copy Markdown
Contributor

I added a stylistic refactor here. and some other minor logic changes - #865

@AVMatthews
AVMatthews requested a review from junhaoliao April 30, 2025 16:53

ghost 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

🧹 Nitpick comments (9)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/utils.ts (1)

67-76: Mongo URI construction ignores auth, SSL & options

initializeMongoClient hard-codes mongodb://host:port which breaks for:

• replica sets / SRV records (mongodb+srv://…)
• authentication credentials
• TLS requirements

Accept a full URI or extend DbOptions:

interface DbOptions {
  uri?: string;       // takes precedence
  host: string;
  port: number;
  username?: string;
  password?: string;
  tls?: boolean;
  database: string;
}

Then build the URI accordingly, or simply new MongoClient(options.uri ?? ...).

components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts (2)

161-171: Negation style deviates from code-base guideline

else if (!emitTimeout) violates the “prefer false == <expr> over !<expr>” rule.
Same pattern occurs in other files – worth running a linter autofix.

-            } else if (!emitTimeout) {
+            } else if (false == emitTimeout) {

80-88: Return value description & implementation out of sync

The JSDoc says the method returns “True if connection is last subscriber”, yet the early-exit path when the watcher is missing also returns false.
Clarify documentation or distinguish between “watcher not found” and “still has subscribers” with separate return codes / an enum.

components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (2)

238-245: Negation shorthand violates code-style guideline

if (!collectionName) should follow the documented guideline:

-        if (!collectionName) {
-            this.#fastify.log.error("Collection name is undefined");
-            return;
-        }
+        if (false == collectionName) {
+            this.#fastify.log.error("Collection name is undefined");
+            return;
+        }

204-221: #getQueryId performance & collision risk

Iterating Map.entries() each time scales O(N).
A reverse map (hash → id) or simply using the hash string as the key avoids linear scans and removes the max-key calculation.

-        const queryHash = getQueryHash(queryParams);
-        for (const [queryId, hash] of this.#queryIdtoQueryHashMap.entries()) {
-            if (hash === queryHash) {
-                return queryId;
-            }
-        }
-        let queryId = 0;
-        ...
-        this.#queryIdtoQueryHashMap.set(queryId, queryHash);
-        return queryId;
+        const hash = getQueryHash(queryParams);
+        let id = this.#queryIdtoQueryHashMap.get(hash);
+        if ("undefined" !== typeof id) {
+            return id;
+        }
+        id = this.#queryIdtoQueryHashMap.size;
+        this.#queryIdtoQueryHashMap.set(hash, id);
+        return id;
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts (4)

33-58: Add JSDoc for each client-to-server event
The top-level comment is helpful, but adding small JSDoc blocks per event (e.g., for "disconnect", "collection::init", etc.) will improve discoverability and make it clearer what each callback’s arguments and expected behavior are.


41-46: Simplify Response generic for find-toArray callback
Using Response<{data: Document[]}> leads to payloads shaped like { data: { data: Document[] } }. Instead, consider:

"collection::find::toArray": (
  args: { query: Filter<Document>; options: FindOptions },
  callback: (res: Response<Document[]>) => void
) => void;

so that Success<Document[]> yields { data: Document[] }.


80-82: Replace empty interface with a type alias
An empty interface is equivalent to {}. You can simplify this section by removing the ESLint disable and using:

type InterServerEvents = {};

This aligns with Biome’s recommendation and reduces boilerplate.

🧰 Tools
🪛 Biome (1.9.4)

[error] 80-82: An empty interface is equivalent to {}.

Safe fix: Use a type alias instead.

(lint/suspicious/noEmptyInterface)


128-128: Consider making the client update timeout configurable
The hardcoded CLIENT_UPDATE_TIMEOUT_MS = 500 may need tuning in different environments. Exposing this as a plugin option or via an environment variable will let operators adjust throttling without code changes.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 440e459 and 24d2bdc.

📒 Files selected for processing (5)
  • components/log-viewer-webui/server/src/app.ts (2 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/utils.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/log-viewer-webui/server/src/app.ts
🧰 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/server/src/plugins/MongoSocketIoServer/utils.ts
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts
🪛 Biome (1.9.4)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts

[error] 80-82: An empty interface is equivalent to {}.

Safe fix: Use a type alias instead.

(lint/suspicious/noEmptyInterface)

🔇 Additional comments (2)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/typings.ts (2)

1-6: Well-scoped imports
Imports from mongodb are grouped together and the Socket import from socket.io is clearly separated. This makes it easy to see third-party dependencies at a glance.


7-7: Verify Socket.IO type definitions are installed
You’re importing Socket from "socket.io". Ensure that the corresponding TypeScript definitions (@types/socket.io or the built-in types in your Socket.IO version) are present in devDependencies, otherwise the build may fail with missing type declarations.

Comment on lines +41 to +45
const getQueryHash = function (
queryParams: QueryParameters,
): string {
return JSON.stringify(queryParams);
};

ghost Apr 30, 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

JSON.stringify is order-sensitive → equal queries may receive different hashes

Using JSON.stringify directly means {a:1, b:2} and {b:2, a:1} hash differently even though they are semantically identical.
A stable, canonical serialiser (e.g. canonical-json, fast-json-stable-stringify, or a custom “alphabetise keys” helper) will prevent duplicate watchers and wasted sockets.

-import {type QueryParameters} from "./typings.js";
+import {type QueryParameters} from "./typings.js";
+import stringify from "fast-json-stable-stringify";   // ← tiny, zero-dep

...
-    return JSON.stringify(queryParams);
+    return stringify(queryParams);
📝 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
const getQueryHash = function (
queryParams: QueryParameters,
): string {
return JSON.stringify(queryParams);
};
import {type QueryParameters} from "./typings.js";
import stringify from "fast-json-stable-stringify"; // ← tiny, zero-dep
const getQueryHash = function (
queryParams: QueryParameters,
): string {
return stringify(queryParams);
};

Comment on lines +24 to +33
const convertQueryToChangeStreamFormat = (query: Filter<Document>): Filter<Document> => {
const changeStreamQuery: Filter<Document> = {};
for (const key in query) {
if (Object.hasOwn(query, key)) {
changeStreamQuery[`fullDocument.${key}`] = query[key] as unknown;
}
}

return changeStreamQuery;
};

ghost Apr 30, 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

Edge cases for nested & operator-based queries are not handled

convertQueryToChangeStreamFormat works only for simple top-level key/value pairs.
Queries that contain operators like $and, $or, or nested documents ({ "meta.type": "foo" }) will not be rewritten correctly, which means the change-stream will miss updates for those queries.

Consider a recursive approach that walks the query tree and rewrites only leaf field paths, or document this limitation explicitly so that callers do the conversion beforehand.

Comment on lines +153 to +175
async #collectionInitListener (
socket: MongoCustomSocket,
requestArgs: {collectionName: string},
callback:(res: Response<void>) => void
): Promise<void> {
const {collectionName} = requestArgs;
this.#fastify.log.info(
`Socket ${socket.id} requested init of collection ${collectionName}`
);

const exists = await this.#collectionExists(collectionName);
if (false === exists) {
this.#fastify.log.error(`Collection ${collectionName} does not exist in MongoDB`);
callback({
error: `Collection ${collectionName} does not exist in MongoDB`,
});

return;
}


socket.data.collectionName = collectionName;
}

ghost Apr 30, 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

Success path never invokes callback → client hangs

#collectionInitListener calls the callback only on the error branch, leaving the promise unresolved for happy-path initialisation.
Clients waiting on the acknowledgement will time out.

@@
-        socket.data.collectionName = collectionName;
+        socket.data.collectionName = collectionName;
+        callback({});   // acknowledge success
📝 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 #collectionInitListener (
socket: MongoCustomSocket,
requestArgs: {collectionName: string},
callback:(res: Response<void>) => void
): Promise<void> {
const {collectionName} = requestArgs;
this.#fastify.log.info(
`Socket ${socket.id} requested init of collection ${collectionName}`
);
const exists = await this.#collectionExists(collectionName);
if (false === exists) {
this.#fastify.log.error(`Collection ${collectionName} does not exist in MongoDB`);
callback({
error: `Collection ${collectionName} does not exist in MongoDB`,
});
return;
}
socket.data.collectionName = collectionName;
}
async #collectionInitListener (
socket: MongoCustomSocket,
requestArgs: {collectionName: string},
callback: (res: Response<void>) => void
): Promise<void> {
const {collectionName} = requestArgs;
this.#fastify.log.info(
`Socket ${socket.id} requested init of collection ${collectionName}`
);
const exists = await this.#collectionExists(collectionName);
if (false === exists) {
this.#fastify.log.error(`Collection ${collectionName} does not exist in MongoDB`);
callback({
error: `Collection ${collectionName} does not exist in MongoDB`,
});
return;
}
socket.data.collectionName = collectionName;
callback({}); // acknowledge success
}

ghost 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 (1)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (1)

153-178: Success path never invokes callback → client hangs

The #collectionInitListener calls the callback only on the error branch, leaving the promise unresolved for happy-path initialization. Clients waiting on the acknowledgement will time out.

-        socket.data.collectionName = collectionName;
+        socket.data.collectionName = collectionName;
+        callback({});   // acknowledge success
🧹 Nitpick comments (2)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (2)

243-247: Use explicit comparison instead of negation operator

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

-        if (!collectionName) {
+        if (false === collectionName || undefined === collectionName) {

214-221: Simplify query ID generation

The query ID generation logic could be simplified by using a class-level counter that increments for each new query.

-        let queryId = 0;
-        if (0 === this.#queryIdtoQueryHashMap.size) {
-            this.#queryIdtoQueryHashMap.set(queryId, queryHash);
-        } else {
-            const maxKey = Math.max(...Array.from(this.#queryIdtoQueryHashMap.keys()));
-            queryId = maxKey + 1;
-            this.#queryIdtoQueryHashMap.set(queryId, queryHash);
-        }
+        // Use a class property to track the next query ID
+        if (!this.nextQueryId) {
+            this.nextQueryId = 0;
+        }
+        const queryId = this.nextQueryId++;
+        this.#queryIdtoQueryHashMap.set(queryId, queryHash);
📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 24d2bdc and d56bd8a.

📒 Files selected for processing (1)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.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/server/src/plugins/MongoSocketIoServer/index.ts
🔇 Additional comments (1)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (1)

39-44: Consider client-side socket sharing per PR comments

Based on PR comments, consider implementing socket sharing on the client side using a singleton pattern rather than creating separate socket connections for each collection. This would align with browser limits on WebSocket connections per domain.

You could enhance the design by:

  1. Using a shared socket on the client side
  2. Including query hash in the event payload instead of using different event names
  3. Leveraging socket.io rooms for efficient resource sharing

Comment on lines +264 to +266
const queryId = this.#getQueryId(queryParameters);
await collection.getWatcher(queryParameters, queryId, socket);
callback({data: {queryId}});

ghost Apr 30, 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

Add error handling for getWatcher

The call to collection.getWatcher() might fail, but there's no error handling here. Add a try/catch block to gracefully handle potential errors and provide helpful feedback to the client.

-        const queryId = this.#getQueryId(queryParameters);
-        await collection.getWatcher(queryParameters, queryId, socket);
-        callback({data: {queryId}});
+        const queryId = this.#getQueryId(queryParameters);
+        try {
+            await collection.getWatcher(queryParameters, queryId, socket);
+            callback({data: {queryId}});
+        } catch (error) {
+            this.#fastify.log.error(`Error getting watcher: ${error}`);
+            callback({
+                error: "Failed to subscribe to collection updates",
+            });
+            return;
+        }
📝 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
const queryId = this.#getQueryId(queryParameters);
await collection.getWatcher(queryParameters, queryId, socket);
callback({data: {queryId}});
const queryId = this.#getQueryId(queryParameters);
try {
await collection.getWatcher(queryParameters, queryId, socket);
callback({ data: { queryId } });
} catch (error) {
this.#fastify.log.error(`Error getting watcher: ${error}`);
callback({
error: "Failed to subscribe to collection updates",
});
return;
}

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

ghost 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

♻️ Duplicate comments (2)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (2)

153-178: Success path never invokes callback → client hangs

#collectionInitListener calls the callback only on the error branch, leaving the promise unresolved for happy-path initialization. Clients waiting on the acknowledgement will time out.

 if (socket.disconnected) {
     return;
 }
 socket.data.collectionName = collectionName;
+callback({}); // acknowledge success

264-266: Add error handling for getWatcher

The call to collection.getWatcher() might fail, but there's no error handling here. Add a try/catch block to gracefully handle potential errors and provide helpful feedback to the client.

        const queryId = this.#getQueryId(queryParameters);
-       await collection.getWatcher(queryParameters, queryId, socket);
-       callback({data: {queryId}});
+       try {
+           await collection.getWatcher(queryParameters, queryId, socket);
+           callback({data: {queryId}});
+       } catch (error) {
+           this.#fastify.log.error(`Error getting watcher: ${error}`);
+           callback({
+               error: "Failed to subscribe to collection updates",
+           });
+           return;
+       }
🧹 Nitpick comments (6)
components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (6)

1-3: TODO comments should be addressed before merging

The file starts with TODOs that should be addressed. Moving listeners to separate files would improve code organization and maintainability.

Consider creating separate files for the different socket event listeners to reduce the complexity of this class, as indicated by your TODO comment.


243-247: Use the coding standard format for boolean expressions

According to the project guidelines, boolean negations should use the format false == expression rather than !expression.

-        if (!collectionName) {
+        if (false == collectionName) {

327-328: Combine boolean conditions for better readability

The combination of "undefined" === typeof subscribedQueryIds and false === subscribedQueryIds.includes(queryId) can be simplified.

-       if ("undefined" === typeof subscribedQueryIds ||
-           false === subscribedQueryIds.includes(queryId)
-       ) {
+       if (!subscribedQueryIds || false === subscribedQueryIds.includes(queryId)) {

189-193: Consider using optional chaining operator for cleaner code

The check for undefined followed by setting a value can be simplified using modern JavaScript/TypeScript features.

-       const subscribedQueryIds = this.#subscribedQueryIdsMap.get(socketId);
-       if ("undefined" === typeof subscribedQueryIds) {
-           this.#subscribedQueryIdsMap.set(socketId, [queryId]);
-
-           return;
-       }
+       const subscribedQueryIds = this.#subscribedQueryIdsMap.get(socketId) || [];
+       if (false === subscribedQueryIds.includes(queryId)) {
+           subscribedQueryIds.push(queryId);
+       }
+       this.#subscribedQueryIdsMap.set(socketId, subscribedQueryIds);

214-224: Simplify query ID generation logic

The current implementation has conditional logic that can be simplified. Using Math.max() with a default value would make this more concise.

-       let queryId = 0;
-       if (0 === this.#queryIdtoQueryHashMap.size) {
-           this.#queryIdtoQueryHashMap.set(queryId, queryHash);
-       } else {
-           const maxKey = Math.max(...Array.from(this.#queryIdtoQueryHashMap.keys()));
-           queryId = maxKey + 1;
-           this.#queryIdtoQueryHashMap.set(queryId, queryHash);
-       }
+       const queryId = this.#queryIdtoQueryHashMap.size === 0 ? 0 : 
+           Math.max(...Array.from(this.#queryIdtoQueryHashMap.keys())) + 1;
+       this.#queryIdtoQueryHashMap.set(queryId, queryHash);

337-340: Use inline filter for more concise code

The current implementation sets a filtered array back to the map in multiple steps. This can be done more concisely.

-       this.#subscribedQueryIdsMap.set(
-           socket.id,
-           subscribedQueryIds.filter((id) => id !== queryId)
-       );
+       const filteredIds = subscribedQueryIds.filter((id) => id !== queryId);
+       if (filteredIds.length > 0) {
+           this.#subscribedQueryIdsMap.set(socket.id, filteredIds);
+       } else {
+           this.#subscribedQueryIdsMap.delete(socket.id);
+       }

This also handles cleanup if no subscriptions remain for this socket.

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between d56bd8a and a0377cf.

📒 Files selected for processing (2)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts (1 hunks)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/MongoWatcherCollection.ts
🧰 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/server/src/plugins/MongoSocketIoServer/index.ts

Comment on lines +38 to +44

/**
* Manages client interactions with MongoDB.
*
* TODO: In current implementation, all queries in a collection are sent using the same event. A
* potential improvement would be to use different event names per query.
*/

ghost Apr 30, 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

Apply room-based optimization mentioned in PR comments

The PR comments suggest using socket.io rooms to optimize watcher sharing across multiple clients. Consider implementing this improvement as mentioned in the discussion.

The current TODO comment addresses only part of the optimization. Consider implementing socket.io rooms keyed by query hashes, as discussed in the PR comments. This would allow sharing a single watcher instance across multiple clients subscribing to the same query, improving resource usage.

ghost May 2, 2025

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 now we will do our own registration, since https://www.npmjs.com/package/fastify-socket.io does not support Fastify v5 or above.

Comment on lines +1 to +4
import {
Collection,
Db,
} from "mongodb";

ghost May 2, 2025

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.

Can we write

Suggested change
import {
Collection,
Db,
} from "mongodb";
import type {
Collection,
Db,
} from "mongodb";

* Provides watchers for MongoDB queries to a specific collection.
*/
class MongoWatcherCollection {
private collection: Collection;

ghost May 2, 2025

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.

Can we use the #collection sytax instead of the private keyword?

class MongoWatcherCollection {
private collection: Collection;

private io: Server;

ghost May 2, 2025

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.

ditto: let's use the # syntax if there's no technical restriction.

private io: Server;

// Active watchers
private queryIdtoWatcherMap: Map<QueryId, Watcher> = new Map();

ghost May 2, 2025

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.

ditto

Collection,
Db,
} from "mongodb";
import {Server} from "socket.io";

ghost May 2, 2025

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.

Similarly, if we're only using the Server as a type:

Suggested change
import {Server} from "socket.io";
import type {Server} from "socket.io";

* @param io
* @param mongoDb
*/
constructor (collectionName: string, io: Server, mongoDb: Db) {

ghost May 2, 2025

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.

This is really nit-picking but I find it counter intuitive to list the arguments io and mongoDb after collectionName.

When we look at the individual arguments: * `collectionName` is a Mongo collection identifier. * `io` is a Socket.io server. * `mongoDb` is a Mongo Database instance.

Accepting collectionName and connectionOptions would have made sense. That said, for the sake of simplicity, can we use a single object for the constructor arguments? e.g.,

constructor ({collectionName, io, mongoDb}: {collectionName: string, io: Server, mongoDb: Db})

Please see the below comment for interface update suggestions.

* @param queryId
* @param socket
*/
async getWatcher (

ghost May 2, 2025

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.

This is an unusual design choice in the interface. The getWatcher method is mixing two very different responsibilities:

  • Creating/retrieving a watcher (resource management)
  • Subscribing a specific socket and sending initial data (communication)

This violates the single responsibility principle and creates an awkward interface that tightly couples distinct operations.

Can we implement those interfaces instead:

interface WatcherOptions<T> {
  onUpdate: (data: T[]) => void;
  onError: (error: Error) => void;
  debounceMillis: number;
}

class MongoWatcherCollection {
    constructor(mongoDb: Db, collectionName: string) {}

    async createOrGetWatcher<T>(queryId: QueryId, options: WatcherOptions<T>) {
        ...
        return watcher;
    }

    addSubscriber(queryId: QueryId, socketId: string) {}

    removeSubscriber (queryId: number, socketId: string): boolean {}

    // add this, or maybe we can simply extend MongoWatcherCollection from mongo.Collection
    find(...) {
        this.#collection.find(...);
    }
}

ghost May 5, 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.

I took inspiration from this in the new PR. My implementation is not exactly the same. But uses the same idea

@junhaoliao

ghost commented May 2, 2025

Copy link
Copy Markdown
Member

It seems there're some conflicts with main as well. Please attempt merging from main and resolve any conflicts.

@junhaoliao
junhaoliao requested a review from davemarco May 2, 2025 07:29
@junhaoliao

ghost commented May 2, 2025

Copy link
Copy Markdown
Member

For the PR title, how about:

feat(new-webui): Add MongoSocketIoServer Fasity plugin for initial data and realtime updates.

@davemarco

ghost commented May 5, 2025

Copy link
Copy Markdown
Contributor

This PR was moved to #880 and can be closed

@davemarco davemarco closed this May 8, 2025
@davemarco

ghost commented May 8, 2025

Copy link
Copy Markdown
Contributor

replaced by #880

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants