feat(new-webui): Mongo Framework Client#841
Conversation
WalkthroughThis update introduces a client-side abstraction for reactive MongoDB queries over a Socket.IO connection in the log-viewer-webui. New modules implement a Changes
Sequence Diagram(s)sequenceDiagram
participant App (React)
participant MongoCollection
participant MongoCollectionReactiveCursor
participant Socket.IO Server
App->>MongoCollection: new MongoCollection("collectionName")
MongoCollection->>Socket.IO Server: emit "collection::init" (collectionName)
App->>MongoCollection: find(query, options)
MongoCollection->>MongoCollectionReactiveCursor: return new reactive cursor
App->>MongoCollectionReactiveCursor: toReactiveArray(callbacks)
MongoCollectionReactiveCursor->>Socket.IO Server: emit "collection::find::toReactiveArray" (query, options)
Socket.IO Server-->>MongoCollectionReactiveCursor: emit "collection::find::update" (queryId, data)
MongoCollectionReactiveCursor-->>App: callbacks.onData(data)
Note over App: App re-renders with new data
App->>MongoCollectionReactiveCursor: cleanup (unsubscribe)
MongoCollectionReactiveCursor->>Socket.IO Server: emit "collection::find::unsubscribe" (queryId)
Suggested reviewers
✨ Finishing Touches
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
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)
Other keywords and placeholders
CodeRabbit Configuration File (
|
There was a problem hiding this comment.
Actionable comments posted: 7
🧹 Nitpick comments (13)
components/log-viewer-webui/client/src/App.tsx (5)
1-1: Prefer explicit comparison over negation per coding guidelinesAccording to the coding guidelines, negated expressions should use explicit comparison.
While this is an ESLint directive rather than actual code logic, for consistency with your coding guidelines, consider:
-/* eslint-disable react/jsx-key */ +/* eslint-disable react/jsx-key */Note: Instead of disabling the ESLint rule, consider addressing the underlying issue by adding keys to your mapped elements.
3-7: Consider removing commented-out code and importsThe commented-out imports suggest this is a work in progress. Consider removing them if they're no longer needed.
Keeping unused imports commented out can lead to code clutter. Either restore the components or remove the commented lines.
10-13: Unclear purpose of commented collection instanceThere's a commented-out
MongoReplicaCollectioninstance that may indicate incomplete implementation or testing code.-// const collection2 = new MongoReplicaCollection("compression-jobs");Remove this commented line if it's not needed, or add a comment explaining why it's kept for future reference.
40-46: Remove commented debug code before mergingThe commented debug logging code should be removed before merging to production.
-/* for (let i = 0; i < results.length; i++) { - console.log(results[i]); -} - -for (let i = 0; i < results2.length; i++) { - console.log(results2[i]); -} */Consider using a more structured approach to debugging, such as a debug flag or environment variable, if debugging is needed in the future.
63-65: Consider removing commented UI componentsThe commented UI components indicate incomplete migration to the new data layer.
If these components are no longer needed, remove them completely. If they will be reintroduced later, add a TODO comment explaining the plan.
components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx (4)
9-19: Consider more specific type instead ofunknown[]The return type
unknown[]is very generic. Consider using a more specific type if the structure of the returned data is known.-const useTracker = ( - getCursor: () => MongoCollectionReactiveCursor, - dependencies: object[] = [] -): unknown[] => { +const useTracker = <T = unknown>( + getCursor: () => MongoCollectionReactiveCursor<T>, + dependencies: unknown[] = [] +): T[] => {This would make the hook more type-safe and provide better intellisense for consumers.
20-21: Update state type to match return typeIf you implement the generic type suggestion above, also update the state type.
-const [data, setData] = useState<unknown[]>([]); +const [data, setData] = useState<T[]>([]);
31-33: Consider expanded error handlingThe error handling logs to console but doesn't provide a way for the component to know about or handle the error.
Consider adding an error state that components can use to show error messages:
+const [error, setError] = useState<Error | null>(null); const unsubscribe = cursor.toReactiveArray({ onData: handleData, onError: (error: unknown) => { console.error("Error fetching data:", error); + setError(error instanceof Error ? error : new Error(String(error))); }, }); // And then adjust the return: -return data; +return { data, error };
39-40: ESLint rule disabling should be explainedWhen disabling ESLint rules, it's good practice to include a comment explaining why.
- // eslint-disable-next-line react-hooks/exhaustive-deps + // eslint-disable-next-line react-hooks/exhaustive-deps + // Using custom dependencies array instead of auto-detected depscomponents/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionCursor.ts (1)
93-97: Follow coding guidelines for conditionalsAccording to the coding guidelines, prefer
false == <expression>over!<expression>pattern.- if (response.error) { + if (false == (response.error == null)) { reject(response.error); return; }components/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.ts (1)
39-41: Convert TODO comments to tracked issuesRather than keeping TODO comments in the code, consider creating actual issues in your issue tracker and reference them in the comments. This improves traceability and ensures these tasks aren't forgotten.
// eslint-disable-next-line no-warning-comments - // TODO: use the server URL from the environment / constructor args + // TODO(#123): use the server URL from the environment / constructor args this.socket = getSharedSocket(); ... // eslint-disable-next-line no-warning-comments - // TODO: add support for non-reactive cursors + // TODO(#124): add support for non-reactive cursorsAlso applies to: 48-49
components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts (2)
44-47: Follow coding guidelines for conditionalsAccording to the coding guidelines, prefer
false == <expression>pattern over negation or equality checks.- if (this.queryId === response.queryId) { - if (response.error) { + if (false == (this.queryId !== response.queryId)) { + if (false == (response.error == null)) { return this.listener?.onError(response.error); }
76-79: Simplify conditional with null checkWhile the current conditional follows the coding guidelines by using
!==, it could be simplified for better readability.- if ("undefined" !== typeof response.queryId) { + if (null != response.queryId) { unsubscribedInfo.queryId = response.queryId; this.queryId = response.queryId; }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (1)
components/log-viewer-webui/client/package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (7)
components/log-viewer-webui/client/package.json(1 hunks)components/log-viewer-webui/client/src/App.tsx(1 hunks)components/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.ts(1 hunks)components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionCursor.ts(1 hunks)components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts(1 hunks)components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx(1 hunks)components/log-viewer-webui/client/vite.config.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}: - Preferfalse == <expression>rather than!<expression>.
components/log-viewer-webui/client/vite.config.tscomponents/log-viewer-webui/client/src/App.tsxcomponents/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionCursor.tscomponents/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.tscomponents/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsxcomponents/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.ts
🧬 Code Graph Analysis (1)
components/log-viewer-webui/client/src/App.tsx (1)
components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx (1)
useTracker(45-45)
🪛 Biome (1.9.4)
components/log-viewer-webui/client/src/App.tsx
[error] 51-51: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
[error] 56-56: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
🔇 Additional comments (9)
components/log-viewer-webui/client/vite.config.ts (2)
9-10: Server binding configuration looks good but consider security implicationsThe configuration changes to bind the server to all network interfaces (0.0.0.0) and change the port to 8081 are appropriate for development. However, be aware that binding to 0.0.0.0 makes your development server accessible from other devices on your network.
For development environments, this is likely intentional. If this configuration will be used in production, consider restricting access appropriately.
18-22: WebSocket proxy configuration correctly implementedThe Socket.IO proxy configuration is well-structured with appropriate settings for WebSocket support (ws: true) and origin preservation (changeOrigin: true).
components/log-viewer-webui/client/src/App.tsx (2)
20-28: Query implementation for results looks goodThe implementation of the reactive query using
useTrackeris well structured.
30-38: Potential duplicative query logicThe second query is very similar to the first but with a smaller ID range. Consider if this duplication is intentional and necessary.
Is there a specific reason for having two similar queries with different ID ranges? If this is just for testing or demonstration purposes, consider consolidating or commenting the purpose.
components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx (3)
1-7: Well-structured importsThe imports are clean and well-organized.
22-39: Well-implemented effect with proper cleanupThe useEffect hook properly subscribes to the cursor and cleans up when unmounting.
42-45: Export is well structuredThe named export is appropriate for this custom hook.
components/log-viewer-webui/client/package.json (2)
24-25: Socket.IO client dependency correctly addedThe Socket.IO client dependency is correctly added to support the new WebSocket functionality.
28-38: ESLint and related dependencies are appropriateThe ESLint-related dependencies added are appropriate for enhancing code quality checks.
| <div> | ||
| {results.map((r) => ( | ||
| <div> | ||
| {JSON.stringify(r)} | ||
| </div> | ||
| ))} |
There was a problem hiding this comment.
Missing key prop in list rendering
React requires a unique "key" prop for each element in an array to optimize rendering performance.
Add a unique key to each rendered item:
-{results.map((r) => (
- <div>
- {JSON.stringify(r)}
- </div>
-))}
+{results.map((r) => (
+ <div key={r._id || Math.random().toString()}>
+ {JSON.stringify(r)}
+ </div>
+))}Ideally, use a stable identifier from your data like r._id if available.
📝 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.
| <div> | |
| {results.map((r) => ( | |
| <div> | |
| {JSON.stringify(r)} | |
| </div> | |
| ))} | |
| <div> | |
| {results.map((r) => ( | |
| <div key={r._id || Math.random().toString()}> | |
| {JSON.stringify(r)} | |
| </div> | |
| ))} | |
| </div> |
🧰 Tools
🪛 Biome (1.9.4)
[error] 51-51: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
| {results2.map((r) => ( | ||
| <div> | ||
| {JSON.stringify(r)} | ||
| </div> | ||
| ))} |
There was a problem hiding this comment.
Missing key prop in second list rendering
Similar to the first map, this one also needs unique keys for React list rendering.
-{results2.map((r) => (
- <div>
- {JSON.stringify(r)}
- </div>
-))}
+{results2.map((r) => (
+ <div key={r._id || Math.random().toString()}>
+ {JSON.stringify(r)}
+ </div>
+))}Use a consistent approach to keys between both lists.
📝 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.
| {results2.map((r) => ( | |
| <div> | |
| {JSON.stringify(r)} | |
| </div> | |
| ))} | |
| {results2.map((r) => ( | |
| <div key={r._id || Math.random().toString()}> | |
| {JSON.stringify(r)} | |
| </div> | |
| ))} |
🧰 Tools
🪛 Biome (1.9.4)
[error] 56-56: Missing key property for this element in iterable.
The order of the items may change, and having a key can help React identify which item was moved.
Check the React documentation.
(lint/correctness/useJsxKeyInIterable)
| this.socket.emit("collection::find::toArray", { | ||
| query: this.findQuery, | ||
| options: this.findOptions, | ||
| }, (response: {error?: Error; data?: Document[]}) => { |
There was a problem hiding this comment.
Define or import the Document type
The type annotation uses Document[] but there's no import or definition for the Document type. This could cause TypeScript compilation errors.
- }, (response: {error?: Error; data?: Document[]}) => {
+ }, (response: {error?: Error; data?: any[]}) => {Alternatively, import the Document type from MongoDB types:
+ import { Document } from 'mongodb';📝 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.
| }, (response: {error?: Error; data?: Document[]}) => { | |
| }, (response: {error?: Error; data?: any[]}) => { |
| if (!sharedSocket) { | ||
| // You can pass a URL here if needed (e.g., from environment vars) | ||
| sharedSocket = io(); | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Follow coding guidelines for conditionals and consider URL configuration
The conditional doesn't follow the coding guideline to prefer false == <expression>. Also, the socket initialization lacks a URL parameter which could be problematic if the server is not at the same origin.
- if (!sharedSocket) {
+ if (false == (sharedSocket != null)) {
// You can pass a URL here if needed (e.g., from environment vars)
- sharedSocket = io();
+ const serverUrl = process.env.SOCKET_SERVER_URL || window.location.origin;
+ sharedSocket = io(serverUrl);
}| onData: (data: Document[]) => void; | ||
| onError: (error: Error) => void; |
There was a problem hiding this comment.
Define or import the Document type
The Document type is used in multiple places but is neither imported nor defined. This could cause TypeScript compilation errors.
Add an import for the Document type at the top of the file:
import {Socket} from "socket.io-client";
import MongoCollectionCursor from "./MongoCollectionCursor.js";
+ import { Document } from 'mongodb';Alternatively, define an interface for it within this file.
Also applies to: 12-13, 43-43
|
|
||
| this.socket.off("collection::find::update"); | ||
| }; |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Consider using a more specific event unsubscription
The current implementation removes the entire "collection::find::update" event listener which could affect other reactive cursors if multiple are used simultaneously. Consider using a more targeted approach.
});
- this.socket.off("collection::find::update");
+ // Remove just this instance's listener or use a more specific event name
+ this.listener = null;
+ this.queryId = null;
};Alternatively, consider using a unique event name per cursor or implementing a more sophisticated event routing system.
Committable suggestion skipped: line range outside the PR's diff.
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (2)
components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts (2)
19-20: Define or import the Document typeThe Document type is used in multiple places but is neither imported nor defined. This could cause TypeScript compilation errors.
Also applies to: 24-25, 55-55
104-104: Consider using a more specific event unsubscriptionThe current implementation removes the entire "collection::find::update" event listener which could affect other reactive cursors if multiple are used simultaneously. Consider using a more targeted approach.
🧹 Nitpick comments (2)
components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts (2)
18-21: Consider consolidating duplicate interfacesThe
ListenerandReactiveArrayCallbackinterfaces have identical structure. Consider consolidating them into a single interface to reduce duplication and improve maintainability.interface Listener { onData: (data: Document[]) => void; onError: (error: Error) => void; } -interface ReactiveArrayCallback { - onData: (data: Document[]) => void; - onError: (error: Error) => void; -} +type ReactiveArrayCallback = Listener;Also applies to: 23-26
75-106: Add method to refresh or invalidate cached dataThe current implementation doesn't provide a way to manually refresh data or invalidate the cache. This could be useful in scenarios where data might be stale or when user actions should trigger a refresh.
Consider adding a refresh method:
+ /** + * Refreshes the data by re-querying the server. + */ + refresh(): void { + if (null === this.queryId || null === this.listener) { + return; + } + + this.socket.emit("collection::find::refresh", { + queryId: this.queryId + }); + }
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (3)
components/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.ts(1 hunks)components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts(1 hunks)components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx(1 hunks)
✅ Files skipped from review due to trivial changes (1)
- components/log-viewer-webui/client/src/mongoCDCLib/useTracker.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- components/log-viewer-webui/client/src/mongoCDCLib/MongoCollection.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}: - Preferfalse == <expression>rather than!<expression>.
components/log-viewer-webui/client/src/mongoCDCLib/MongoCollectionReactiveCursor.ts
| toReactiveArray (callback: ReactiveArrayCallback): () => void { | ||
| const unsubscribedInfo : {queryId: number | null} = {queryId: null}; | ||
|
|
||
| this.socket.emit("collection::find::toReactiveArray", { | ||
| query: this.findQuery, | ||
| options: this.findOptions, | ||
| }, (response: Response<{queryId: number}>) => { | ||
| if ("error" in response) { | ||
| callback.onError(new Error(response.error)); | ||
|
|
||
| return; | ||
| } | ||
|
|
||
| if ("undefined" !== typeof response.data.queryId) { | ||
| unsubscribedInfo.queryId = response.data.queryId; | ||
| this.queryId = response.data.queryId; | ||
| } | ||
|
|
||
| this.listener = callback; | ||
| }); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add error handling for socket emit
The socket.emit operation doesn't have proper error handling for connection issues or timeouts. Consider adding timeout handling and connection error detection.
toReactiveArray (callback: ReactiveArrayCallback): () => void {
const unsubscribedInfo : {queryId: number | null} = {queryId: null};
+ // Add a timeout handler to avoid hanging if server doesn't respond
+ const timeoutId = setTimeout(() => {
+ callback.onError(new Error("Server response timeout"));
+ }, 10000);
this.socket.emit("collection::find::toReactiveArray", {
query: this.findQuery,
options: this.findOptions,
}, (response: Response<{queryId: number}>) => {
+ clearTimeout(timeoutId);
+
if ("error" in response) {
callback.onError(new Error(response.error));
return;
}
if ("undefined" !== typeof response.data.queryId) {
unsubscribedInfo.queryId = response.data.queryId;
this.queryId = response.data.queryId;
}
this.listener = callback;
});📝 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.
| toReactiveArray (callback: ReactiveArrayCallback): () => void { | |
| const unsubscribedInfo : {queryId: number | null} = {queryId: null}; | |
| this.socket.emit("collection::find::toReactiveArray", { | |
| query: this.findQuery, | |
| options: this.findOptions, | |
| }, (response: Response<{queryId: number}>) => { | |
| if ("error" in response) { | |
| callback.onError(new Error(response.error)); | |
| return; | |
| } | |
| if ("undefined" !== typeof response.data.queryId) { | |
| unsubscribedInfo.queryId = response.data.queryId; | |
| this.queryId = response.data.queryId; | |
| } | |
| this.listener = callback; | |
| }); | |
| toReactiveArray (callback: ReactiveArrayCallback): () => void { | |
| const unsubscribedInfo : {queryId: number | null} = {queryId: null}; | |
| // Add a timeout handler to avoid hanging if server doesn't respond | |
| const timeoutId = setTimeout(() => { | |
| callback.onError(new Error("Server response timeout")); | |
| }, 10000); | |
| this.socket.emit("collection::find::toReactiveArray", { | |
| query: this.findQuery, | |
| options: this.findOptions, | |
| }, (response: Response<{queryId: number}>) => { | |
| clearTimeout(timeoutId); | |
| if ("error" in response) { | |
| callback.onError(new Error(response.error)); | |
| return; | |
| } | |
| if ("undefined" !== typeof response.data.queryId) { | |
| unsubscribedInfo.queryId = response.data.queryId; | |
| this.queryId = response.data.queryId; | |
| } | |
| this.listener = callback; | |
| }); | |
| } |
| this.socket.on( | ||
| "collection::find::update", | ||
| (response: {error?: Error; data?: Document[]; queryId: number}) => { | ||
| if (this.queryId === response.queryId) { | ||
| if (response.error) { | ||
| return this.listener?.onError(response.error); | ||
| } | ||
|
|
||
| return this.listener?.onData(response.data ?? []); | ||
| } | ||
|
|
||
| return null; | ||
| } | ||
| ); |
There was a problem hiding this comment.
🛠️ Refactor suggestion
Add error handling for socket events
The event listener for "collection::find::update" doesn't have any error handling for malformed responses or unexpected data formats, which could lead to runtime errors.
this.socket.on(
"collection::find::update",
(response: {error?: Error; data?: Document[]; queryId: number}) => {
+ try {
if (this.queryId === response.queryId) {
if (response.error) {
return this.listener?.onError(response.error);
}
return this.listener?.onData(response.data ?? []);
}
return null;
+ } catch (error) {
+ console.error("Error processing collection update:", error);
+ this.listener?.onError(error instanceof Error ? error : new Error(String(error)));
+ }
}
);📝 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.
| this.socket.on( | |
| "collection::find::update", | |
| (response: {error?: Error; data?: Document[]; queryId: number}) => { | |
| if (this.queryId === response.queryId) { | |
| if (response.error) { | |
| return this.listener?.onError(response.error); | |
| } | |
| return this.listener?.onData(response.data ?? []); | |
| } | |
| return null; | |
| } | |
| ); | |
| this.socket.on( | |
| "collection::find::update", | |
| (response: {error?: Error; data?: Document[]; queryId: number}) => { | |
| try { | |
| if (this.queryId === response.queryId) { | |
| if (response.error) { | |
| return this.listener?.onError(response.error); | |
| } | |
| return this.listener?.onData(response.data ?? []); | |
| } | |
| return null; | |
| } catch (error) { | |
| console.error("Error processing collection update:", error); | |
| this.listener?.onError( | |
| error instanceof Error ? error : new Error(String(error)) | |
| ); | |
| } | |
| } | |
| ); |
|
replaced by #892 |
Description
Client for new framework built on MongoCDC using socket.io meant to replace our current usage of meteor's publish-subscribe framework.
Second PR to split #809
First PR is #814
Checklist
breaking change.
Validation performed
Manually added collection to mongo replica and tested to see that the content of the collection could be requested through the client from the server.
Summary by CodeRabbit