diff --git a/components/log-viewer-webui/client/package-lock.json b/components/log-viewer-webui/client/package-lock.json index d80e7f03b1..b1408ded38 100644 --- a/components/log-viewer-webui/client/package-lock.json +++ b/components/log-viewer-webui/client/package-lock.json @@ -20,6 +20,8 @@ "react-dom": "^19.0.0", "react-router": "^7.4.1", "react-syntax-highlighter": "^15.6.1", + "socket.io-client": "^4.8.1", + "vite-tsconfig-paths": "^5.1.4", "zustand": "^5.0.3" }, "devDependencies": { @@ -2165,6 +2167,12 @@ "integrity": "sha512-gu+tdy9WZIRulrR4CAcGXZAAixwakKszkUXudMJ4EhtNflBEify5Pm5vnVEVqdmMkxnT4tcdfJps5XYqaNeF9Q==", "license": "MIT" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@stylistic/eslint-plugin": { "version": "2.13.0", "resolved": "https://registry.npmjs.org/@stylistic/eslint-plugin/-/eslint-plugin-2.13.0.tgz", @@ -2241,7 +2249,7 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/@types/hast": { @@ -3749,6 +3757,45 @@ "dev": true, "license": "ISC" }, + "node_modules/engine.io-client": { + "version": "6.6.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.3.tgz", + "integrity": "sha512-T0iLjnyNWahNyv/lcjS2y4oE358tVS/SYQNxYXGAJ9/GLgH4VCvOQ/mhTjqU88mLZCQgiG8RIegFHYCdVC+j5w==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/enhanced-resolve": { "version": "5.18.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz", @@ -3959,7 +4006,7 @@ "version": "0.25.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.1.tgz", "integrity": "sha512-BGO5LtrGC7vxnqucAe/rmvKdJllfGaYWdyABvyMoXQlfYMb2bbRuReWR5tEGE//4LcNJj9XrkovTqNYRFZHAMQ==", - "dev": true, + "devOptional": true, "hasInstallScript": true, "license": "MIT", "bin": { @@ -5002,6 +5049,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "license": "MIT" + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -6054,7 +6107,7 @@ "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "dev": true, + "devOptional": true, "funding": [ { "type": "github", @@ -6419,7 +6472,7 @@ "version": "8.5.3", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", - "dev": true, + "devOptional": true, "funding": [ { "type": "opencollective", @@ -7370,7 +7423,7 @@ "version": "4.34.8", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.8.tgz", "integrity": "sha512-489gTVMzAYdiZHFVA/ig/iYFllCcWFHMvUHI1rpFmkoUtRlQxqh6/yiNqnYibjMZ2b/+FUQwldG+aLsEt6bglQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "@types/estree": "1.0.6" @@ -7688,6 +7741,68 @@ "license": "ISC", "peer": true }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.5.7", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", @@ -7701,7 +7816,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "dev": true, + "devOptional": true, "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -8009,6 +8124,26 @@ "typescript": ">=4.8.4" } }, + "node_modules/tsconfck": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.5.tgz", + "integrity": "sha512-CLDfGgUp7XPswWnezWwsCRxNmgQjhYq3VXHM0/XIRxhVrKw0M1if9agzryh1QS3nxjCROvV+xWxoJO1YctzzWg==", + "license": "MIT", + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^18 || >=20" + }, + "peerDependencies": { + "typescript": "^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.15.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", @@ -8151,7 +8286,7 @@ "version": "5.6.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -8371,7 +8506,7 @@ "version": "6.2.6", "resolved": "https://registry.npmjs.org/vite/-/vite-6.2.6.tgz", "integrity": "sha512-9xpjNl3kR4rVDZgPNdTL0/c6ao4km69a/2ihNQbcANz8RuCOK3hQBmLSJf3bRKVQjVMda+YvizNE8AwvogcPbw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "esbuild": "^0.25.0", @@ -8439,6 +8574,25 @@ } } }, + "node_modules/vite-tsconfig-paths": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-5.1.4.tgz", + "integrity": "sha512-cYj0LRuLV2c2sMqhqhGpaO3LretdtMn/BVX4cPLanIZuwwrkVl+lK84E/miEXkCHWXuq65rhNN4rXsBcOB3S4w==", + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^3.0.3" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -8559,6 +8713,35 @@ "node": ">=0.10.0" } }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/components/log-viewer-webui/client/package.json b/components/log-viewer-webui/client/package.json index ea62e92066..2f2488cb69 100644 --- a/components/log-viewer-webui/client/package.json +++ b/components/log-viewer-webui/client/package.json @@ -25,6 +25,8 @@ "react-dom": "^19.0.0", "react-router": "^7.4.1", "react-syntax-highlighter": "^15.6.1", + "socket.io-client": "^4.8.1", + "vite-tsconfig-paths": "^5.1.4", "zustand": "^5.0.3" }, "devDependencies": { diff --git a/components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts b/components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts new file mode 100644 index 0000000000..ef95a9fb2a --- /dev/null +++ b/components/log-viewer-webui/client/src/api/socket/MongoCollectionSocket.ts @@ -0,0 +1,50 @@ +import { + ClientToServerEvents, + ServerToClientEvents, +} from "@common/index.js"; +import {Socket} from "socket.io-client"; + +import {MongoCursorSocket} from "./MongoCursorSocket.js"; +import {getSharedSocket} from "./SocketSingleton.js"; + + +/** + * Socket connection to a MongoDB collection residing on a server. Class provides methods to + * query the collection. + */ +class MongoCollectionSocket { + #collectionName: string; + + #socket: Socket; + + /** + * Initalizes socket connection to a MongoDB collection on the server. + * + * @param collectionName + */ + constructor (collectionName: string) { + this.#socket = getSharedSocket(); + this.#collectionName = collectionName; + console.log(`MongoDB collection:${collectionName} initialized.`); + } + + /** + * Selects documents in collection and returns a cursor-like object. + * + * @param query + * @param options + * @return a `MongoCursorSocket`. + */ + + find (query: object, options: object) { + return new MongoCursorSocket( + this.#socket, + this.#collectionName, + query, + options, + ); + } +} + + +export default MongoCollectionSocket; diff --git a/components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts b/components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts new file mode 100644 index 0000000000..d869d83745 --- /dev/null +++ b/components/log-viewer-webui/client/src/api/socket/MongoCursorSocket.ts @@ -0,0 +1,114 @@ +import { + ClientToServerEvents, + QueryId, + Response, + ServerToClientEvents, +} from "@common/index.js"; +import {Socket} from "socket.io-client"; + +import {Nullable} from "../../typings/common"; + + +/** + * A cursor-like object receiving MongoDB documents over a socket connection. + */ +class MongoCursorSocket { + #socket: Socket; + + #collectionName: string; + + #query: object; + + #options: object; + + #queryId: Nullable = null; + + + // Listener for data updates from the server. + #updateListener: Nullable<(respArgs: {queryId: number; data: object[]}) => void> = null; + + /** + * @param socket + * @param collectionName + * @param query + * @param options + */ + constructor ( + socket: Socket, + collectionName: string, + query: object, + options: object + ) { + this.#socket = socket; + this.#collectionName = collectionName; + this.#query = query; + this.#options = options; + } + + /** + * Subscribes to query watcher for real-time updates. + * + * @param onDataUpdate Handler which sets data updates from the server in react ui component. + * @throws {Error} if subscription fails. + */ + async subscribe (onDataUpdate: (data: object[]) => void): Promise { + console.log("Attempting to subscribe to query:", JSON.stringify(this.#query)); + + this.#updateListener = (respArgs: {queryId: number; data: object[]}) => { + // Server sends updates for multiple queryIDs using the same event name. + if (this.#queryId === respArgs.queryId) { + onDataUpdate(respArgs.data); + } + }; + + this.#socket.on("collection::find::update", this.#updateListener); + + const response: Response<{queryId: number; initialDocuments: object[]}> = + await this.#socket.emitWithAck( + "collection::find::subscribe", + { + collectionName: this.#collectionName, + query: this.#query, + options: this.#options, + } + ); + + if ("error" in response) { + this.#socket.off("collection::find::update", this.#updateListener); + throw new Error(`Subscription failed: ${response.error}`); + } + + // Set the initial documents received from the server. + onDataUpdate(response.data.initialDocuments); + + this.#queryId = response.data.queryId; + console.log(`Subscribed to queryID:${this.#queryId}.`); + } + + /** + * Unsubscribe from the query. + */ + unsubscribe (): void { + if (null === this.#queryId) { + console.error("Attempted to unsubscribe, but no active subscription exists."); + + return; + } + + + this.#socket.emit("collection::find::unsubscribe", { + queryId: this.#queryId, + }); + + if (this.#updateListener) { + this.#socket.off("collection::find::update", this.#updateListener); + this.#updateListener = null; + } + + console.log(`Unsubscribed to queryID:${this.#queryId}.`); + + this.#queryId = null; + } +} + +export {MongoCursorSocket}; diff --git a/components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts b/components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts new file mode 100644 index 0000000000..77aa07be60 --- /dev/null +++ b/components/log-viewer-webui/client/src/api/socket/SocketSingleton.ts @@ -0,0 +1,33 @@ +import { + ClientToServerEvents, + ServerToClientEvents, +} from "@common/index.js"; +import { + io, + Socket, +} from "socket.io-client"; + +import {Nullable} from "../../typings/common"; + + +let sharedSocket: Nullable> = null; + +/** + * Returns the shared Socket.io instance for the application. Creates a new connection if one + * doesn't exist yet. + * + * @return The shared Socket.io instance. + */ +const getSharedSocket = (): Socket => { + if (!sharedSocket) { + // eslint-disable-next-line no-warning-comments + // TODO: Add support for user provided domain name (i.e. io("https://server-domain.com")). + // Implementation could involve parsing server .env file and moving server .env to a + // common folder. + sharedSocket = io(); + } + + return sharedSocket; +}; + +export {getSharedSocket}; diff --git a/components/log-viewer-webui/client/src/api/socket/useCursor.tsx b/components/log-viewer-webui/client/src/api/socket/useCursor.tsx new file mode 100644 index 0000000000..4242072831 --- /dev/null +++ b/components/log-viewer-webui/client/src/api/socket/useCursor.tsx @@ -0,0 +1,68 @@ +import { + DependencyList, + useEffect, + useState, +} from "react"; + +import {MongoCursorSocket} from "./MongoCursorSocket.js"; + + +/** + * Custom hook which returns a real-time reactive array of documents from a `MongoCursorSocket`. + * + * @param query Function which returns a `MongoCursorSocket` instance. + * @param dependencies Array of dependencies for the query. + * @return Reactive array. + */ +const useCursor = ( + query: () => MongoCursorSocket, + dependencies: DependencyList = [] +): object[] => { + const [data, setData] = useState([]); + + + useEffect(() => { + const cursor = query(); + + // Flag to ignore updates after unmounting. + let ignore = false; + console.log("Subscribing to cursor"); + + // Handler to set data updates from the server. + const onDataUpdate = (dataUpdate: object[]) => { + if (false === ignore) { + setData(dataUpdate); + } + }; + + const subscribed = cursor.subscribe(onDataUpdate); + + subscribed.catch((error: unknown) => { + console.error("Error during subscription:", error); + }); + + return () => { + ignore = true; + + // For a shortly lived cursor (ex. strict mode), the subscription may have not yet + // recieved the queryID from the server, making it impossible to unsubscribe + // immediately (there is no queryID). The subscribed promise allows unsubcription + // to happen when the subscription actually completes. + subscribed + .then(() => { + // Unsubscribe will not run if the subscription failed since the promise was + // rejected. + console.log("Unsubscribing from cursor"); + cursor.unsubscribe(); + }) + .catch((error: unknown) => { + console.error("Error during unsubscription:", error); + }); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, dependencies); + + return data; +}; + +export {useCursor}; diff --git a/components/log-viewer-webui/client/tsconfig/tsconfig.app.json b/components/log-viewer-webui/client/tsconfig/tsconfig.app.json index 059a6f365a..3f2fa4631b 100644 --- a/components/log-viewer-webui/client/tsconfig/tsconfig.app.json +++ b/components/log-viewer-webui/client/tsconfig/tsconfig.app.json @@ -1,7 +1,8 @@ { "extends": "./tsconfig.base.json", "include": [ - "../src" + "../src", + "../common/**/*" ], "compilerOptions": { @@ -15,6 +16,10 @@ "target": "ES2022", "tsBuildInfoFile": "../node_modules/.tmp/tsconfig.app.tsbuildinfo", "useDefineForClassFields": true, + "baseUrl": "../", + "paths": { + "@common/*": ["../common/*"], // Map imports from "@common/*" to the shared folder + }, /* Bundler mode */ "isolatedModules": true, diff --git a/components/log-viewer-webui/client/vite.config.ts b/components/log-viewer-webui/client/vite.config.ts index 9b2428dc87..f3e6c5d260 100644 --- a/components/log-viewer-webui/client/vite.config.ts +++ b/components/log-viewer-webui/client/vite.config.ts @@ -1,11 +1,15 @@ import react from "@vitejs/plugin-react"; import {defineConfig} from "vite"; +import tsconfigPaths from "vite-tsconfig-paths"; // https://vite.dev/config/ export default defineConfig({ base: "./", - plugins: [react()], + plugins: [ + react(), + tsconfigPaths(), + ], publicDir: "public", server: { port: 8080, @@ -16,6 +20,15 @@ export default defineConfig({ target: "http://localhost:3000/", changeOrigin: true, }, + "/socket.io/": { + target: "ws://localhost:3000/", + changeOrigin: true, + ws: true, + }, + }, + fs: { + // allow serving files from one level up (common folder) + allow: [".."], }, }, }); diff --git a/components/log-viewer-webui/common/index.ts b/components/log-viewer-webui/common/index.ts index 0983fdb6e2..73d4923469 100644 --- a/components/log-viewer-webui/common/index.ts +++ b/components/log-viewer-webui/common/index.ts @@ -31,14 +31,9 @@ type Response = Err | Success; */ type ClientToServerEvents = { "disconnect": () => void; - "collection::init": ( - requestArgs: { - collectionName: string; - }, - callback: (res: Response) => void - ) => void; "collection::find::subscribe": ( requestArgs: { + collectionName: string; query: object; options: object; }, @@ -54,6 +49,9 @@ type ClientToServerEvents = { * Events that the server can emit to the client. */ interface ServerToClientEvents { + // eslint-disable-next-line no-warning-comments + // TODO: Consider replacing this with `collection::find::update${number}`, which will + // limit callbacks being triggered in the client to their respective query IDs. "collection::find::update": (respArgs: { queryId: QueryId; data: object[]; @@ -73,7 +71,7 @@ interface SocketData { collectionName?: string; } -export { +export type { ClientToServerEvents, Err, InterServerEvents, diff --git a/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts index 5d9a3bc259..87c3d7d52c 100644 --- a/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts +++ b/components/log-viewer-webui/server/src/plugins/MongoSocketIoServer/index.ts @@ -99,7 +99,6 @@ class MongoSocketIoServer { this.#io.on("connection", (socket) => { this.#fastify.log.info(`New socket connected with ID:${socket.id}`); socket.on("disconnect", this.#disconnectListener.bind(this, socket)); - socket.on("collection::init", this.#collectionInitListener.bind(this, socket)); socket.on( "collection::find::subscribe", this.#collectionFindSubscribeListener.bind(this, socket) @@ -146,38 +145,6 @@ class MongoSocketIoServer { return collections.some((collection) => collection.name === collectionName); } - /** - * Listener for initializing a connection to a collection. - * - * @param socket - * @param requestArgs - * @param requestArgs.collectionName - * @param callback - */ - async #collectionInitListener ( - socket: MongoCustomSocket, - requestArgs: {collectionName: string}, - callback:(res: Response) => void - ): Promise { - const {collectionName} = requestArgs; - this.#fastify.log.info( - `Socket:${socket.id} requested init of collection:${collectionName}` - ); - - const hasCollection = await this.#hasCollection(collectionName); - if (false === hasCollection) { - this.#fastify.log.error(`Collection ${collectionName} does not exist in MongoDB`); - callback({ - error: `Collection ${collectionName} does not exist in MongoDB`, - }); - - return; - } - - socket.data = {...socket.data, collectionName}; - } - - /** * Adds the query ID to the connection's subscribed query IDs. * @@ -222,6 +189,26 @@ class MongoSocketIoServer { return queryId; } + /** + * Gets an existing watcher collection or creates a new one if it doesn't exist. + * + * @param collectionName + * @return The watcher collection instance. + */ + #getOrCreateWatcherCollection ( + collectionName: string + ) + : MongoWatcherCollection { + let watcherCollection = this.#collections.get(collectionName); + if ("undefined" === typeof watcherCollection) { + watcherCollection = new MongoWatcherCollection(collectionName, this.#mongoDb); + this.#fastify.log.info(`Created MongoDb collection:${collectionName}.`); + this.#collections.set(collectionName, watcherCollection); + } + + return watcherCollection; + } + /** * Listener for subscribing to a find query. The client will receive updates whenever * the query results change. @@ -230,33 +217,33 @@ class MongoSocketIoServer { * @param requestArgs * @param requestArgs.query * @param requestArgs.options + * @param requestArgs.collectionName * @param callback */ async #collectionFindSubscribeListener ( socket: MongoCustomSocket, - requestArgs: {query: object; options: object}, + requestArgs: {collectionName: string; query: object; options: object}, callback: (res: Response<{queryId: number; initialDocuments: object[]}>) => void ): Promise { - const {query, options} = requestArgs; - const {collectionName} = socket.data; - - if ("undefined" === typeof collectionName) { - this.#fastify.log.error(`Collection name:${collectionName} is undefined`); - - return; - } + const {collectionName, query, options} = requestArgs; this.#fastify.log.info( `Socket:${socket.id} requested query:${JSON.stringify(query)} ` + `with options:${JSON.stringify(options)} to collection:${collectionName}` ); - let watcherCollection = this.#collections.get(collectionName); - if ("undefined" === typeof watcherCollection) { - watcherCollection = new MongoWatcherCollection(collectionName, this.#mongoDb); - this.#collections.set(collectionName, watcherCollection); + const hasCollection = await this.#hasCollection(collectionName); + if (false === hasCollection) { + this.#fastify.log.error(`Collection ${collectionName} does not exist in MongoDB`); + callback({ + error: `Collection ${collectionName} does not exist in MongoDB on server`, + }); + + return; } + const watcherCollection = this.#getOrCreateWatcherCollection(collectionName); + const queryParameters: QueryParameters = {collectionName, query, options}; const queryId = this.#getQueryId(queryParameters);