From 16f1aba47bf4034adfabe3e31ff636befb8d80ef Mon Sep 17 00:00:00 2001 From: Abdhesh Nayak Date: Sat, 23 Dec 2023 14:51:19 +0530 Subject: [PATCH] :sparkles: Added resource-watcher utility with websocket. --- lib/client/helpers/socket-context.jsx | 102 -------------- lib/client/helpers/socket-context.tsx | 194 ++++++++++++++++++++++++++ lib/configs/base-url.cjs | 2 + package.json | 1 + pnpm-lock.yaml | 70 +++++++++- src/apps/console/root.tsx | 9 +- src/apps/console/routes/test.tsx | 38 +++++ 7 files changed, 306 insertions(+), 110 deletions(-) delete mode 100644 lib/client/helpers/socket-context.jsx create mode 100644 lib/client/helpers/socket-context.tsx create mode 100644 src/apps/console/routes/test.tsx diff --git a/lib/client/helpers/socket-context.jsx b/lib/client/helpers/socket-context.jsx deleted file mode 100644 index 6a3851bf3..000000000 --- a/lib/client/helpers/socket-context.jsx +++ /dev/null @@ -1,102 +0,0 @@ -import { createContext, useContext, useEffect, useMemo } from 'react'; -import { io } from 'socket.io-client'; -import { v4 as uuid } from 'uuid'; -import { socketUrl } from '~/root/lib/base-url'; -import logger from './log'; -import { useReload } from './reloader'; - -const createSocketContext = () => { - const callbacks = {}; - const socket = io(socketUrl); - - socket.on('connect', () => { - logger.log('socket connected'); - }); - - socket.on('disconnect', () => { - logger.log('socket disconnected'); - }); - - socket.on('error', (error) => { - logger.error(error); - }); - - return { - subscribe: (topic, callback) => { - if (!callbacks[topic]) { - callbacks[topic] = {}; - // socket.emit('subscribe', topic); - socket.on(`pubsub_${topic}`, (msg = '{}') => { - // logger.log('message', topic); - const { message } = JSON.parse(msg); - logger.log('socket: ', topic); - if (callbacks[topic]) { - Object.values(callbacks[topic]).forEach((cb) => - cb({ topic, message }) - ); - } - }); - } - - const id = uuid(); - callbacks[topic][id] = callback; - return id; - }, - - unsubscribe: (topic, id) => { - delete callbacks[topic][id]; - if (Object.keys(callbacks[topic]).length === 0) { - // socket.emit('unsubscribe', topic); - socket.off(`pubsub_${topic}`).removeAllListeners(); - } - }, - }; -}; - -const SocketContext = createContext(null); - -const SocketProvider = ({ children }) => { - const socket = useMemo(() => { - // if (typeof window !== 'undefined') { - // return createSocketContext(); - // } - // - // return { - // subscribe: () => {}, - // unsubscribe: () => {}, - // }; - }, [typeof window]); - - return ( - {children} - ); -}; - -export const useSubscribe = (topics, callback, dependencies = []) => { - const t = typeof topics === 'string' ? [topics] : topics; - - const { subscribe, unsubscribe } = useContext(SocketContext); - - useEffect(() => { - const subscriptions = t.map((topic) => ({ - topic, - id: subscribe(topic, callback), - })); - return () => { - subscriptions.forEach(({ topic, id }) => unsubscribe(topic, id)); - }; - }, [...dependencies, ...t]); -}; - -export const useWatch = (...topics) => { - const reloadPage = useReload(); - useSubscribe( - topics, - () => { - reloadPage(); - }, - topics - ); -}; - -export default SocketProvider; diff --git a/lib/client/helpers/socket-context.tsx b/lib/client/helpers/socket-context.tsx new file mode 100644 index 000000000..aecf938b8 --- /dev/null +++ b/lib/client/helpers/socket-context.tsx @@ -0,0 +1,194 @@ +import { createContext, useContext, useEffect, useMemo } from 'react'; +import * as sock from 'websocket'; +import { v4 as uuid } from 'uuid'; +import { socketUrl } from '~/lib/configs/base-url.cjs'; +import { ChildrenProps } from '~/components/types'; +import logger from './log'; +import { NonNullableString } from '../../types/common'; +import { useReload } from './reloader'; + +type Ievent = 'subscribe' | 'unsubscribe' | NonNullableString; + +type IMessage = { + data: string; + event: Ievent; +}; + +const message = ({ event, data }: IMessage): string => { + return JSON.stringify({ event, data }); +}; + +const socketContextDefaultValue: { + subscribe: ( + topic: string, + callback: (arg: { topic: string; message: string }) => void + ) => string; + unsubscribe: (topic: string, id: string) => void; +} = { + subscribe: (): string => '', + unsubscribe: (): void => {}, +}; + +const createSocketContext = () => { + if (typeof window === 'undefined') { + return socketContextDefaultValue; + } + + const callbacks: { + [key: string]: { + [key: string]: (arg: { topic: string; message: string }) => void; + }; + } = {}; + + const wsclient = new Promise((res, rej) => { + try { + // eslint-disable-next-line new-cap + const w = new sock.w3cwebsocket(socketUrl, '', '', {}); + + w.onmessage = (msg) => { + try { + const m: { + topic: string; + message: string; + type: 'update' | 'error' | 'info'; + } = JSON.parse(msg.data as string); + + if (m.type === 'error') { + console.error(m.message); + return; + } + + if (m.type === 'info') { + console.log(m.message); + return; + } + + Object.values(callbacks[m.topic]).forEach((cb) => { + cb(m); + }); + } catch (err) { + console.error(err); + } + }; + + w.onopen = () => { + res(w); + }; + + w.onerror = (e) => { + rej(e); + }; + + w.onclose = () => { + // wsclient.send(newMessage({ event: 'unsubscribe', data: 'test' })); + logger.log('socket disconnected'); + }; + } catch (e) { + rej(e); + } + }); + + return { + subscribe: ( + topic: string, + callback: (arg: { topic: string; message: string }) => void + ): string => { + (async () => { + if (!callbacks[topic]) { + callbacks[topic] = {}; + + try { + const w = await wsclient; + + w.send( + message({ + event: 'subscribe', + data: topic, + }) + ); + + logger.log('subscribed to', topic); + } catch (err) { + logger.warn(err); + } + } + })(); + + const id = uuid(); + callbacks[topic][id] = callback; + return id; + }, + + unsubscribe: (topic: string, id: string) => { + (async () => { + delete callbacks[topic][id]; + + try { + const w = await wsclient; + + if (Object.keys(callbacks[topic]).length === 0) { + w.send( + message({ + event: 'unsubscribe', + data: topic, + }) + ); + } + } catch (err) { + logger.warn(err); + } + })(); + }, + }; +}; + +const SocketContext = createContext(socketContextDefaultValue); + +const SocketProvider = ({ children }: ChildrenProps) => { + const socket = useMemo(() => { + if (typeof window !== 'undefined') { + return createSocketContext(); + } + + return socketContextDefaultValue; + }, [typeof window]); + + return ( + {children} + ); +}; + +export const useSubscribe = ( + topics: string[], + callback: (arg: { topic: string; message: string }) => void, + dependencies: any[] = [] +) => { + const t = typeof topics === 'string' ? [topics] : topics; + + const { subscribe, unsubscribe } = useContext(SocketContext); + + useEffect(() => { + const subscriptions = t.map((topic) => ({ + topic, + id: subscribe(topic, callback), + })); + + return () => { + subscriptions.forEach(({ topic, id }) => unsubscribe(topic, id)); + }; + }, [...dependencies, ...t]); +}; + +export const useWatch = (...topics: string[]) => { + const reloadPage = useReload(); + useSubscribe( + topics, + () => { + console.log('hi'); + reloadPage(); + }, + topics + ); +}; + +export default SocketProvider; diff --git a/lib/configs/base-url.cjs b/lib/configs/base-url.cjs index c80fda6f8..296f99a5a 100644 --- a/lib/configs/base-url.cjs +++ b/lib/configs/base-url.cjs @@ -42,6 +42,7 @@ const baseUrls = () => { cookieDomain, baseUrl: bUrl, githubAppName: 'kloudlite-dev', + socketUrl: `wss://socket${postFix}.${bUrl}/ws`, }; }; @@ -52,6 +53,7 @@ const defaultConfig = { cookieDomain: baseUrls().cookieDomain, baseUrl: baseUrls().baseUrl, githubAppName: baseUrls().githubAppName, + socketUrl: baseUrls().socketUrl, }; module.exports = defaultConfig; diff --git a/package.json b/package.json index af083b7b2..d7b773bc8 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "react-viewport-list": "^7.1.1", "remix": "^1.19.2", "search-in-json": "^1.0.5", + "socket.io-client": "^4.7.2", "swr": "^2.2.4", "use-immer": "^0.9.0", "uuid": "^9.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a82834b3d..dfabbb9a0 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -209,6 +209,9 @@ dependencies: search-in-json: specifier: ^1.0.5 version: 1.0.67 + socket.io-client: + specifier: ^4.7.2 + version: 4.7.2 swr: specifier: ^2.2.4 version: 2.2.4(react@18.2.0) @@ -3874,6 +3877,10 @@ packages: engines: {node: '>=10'} dev: true + /@socket.io/component-emitter@3.1.0: + resolution: {integrity: sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==} + dev: false + /@swc/helpers@0.5.3: resolution: {integrity: sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==} dependencies: @@ -5192,7 +5199,6 @@ packages: optional: true dependencies: ms: 2.1.2 - dev: true /decode-named-character-reference@1.0.2: resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} @@ -5382,6 +5388,25 @@ packages: once: 1.4.0 dev: true + /engine.io-client@6.5.3: + resolution: {integrity: sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-parser: 5.2.1 + ws: 8.11.0 + xmlhttprequest-ssl: 2.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /engine.io-parser@5.2.1: + resolution: {integrity: sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==} + engines: {node: '>=10.0.0'} + dev: false + /enhanced-resolve@5.15.0: resolution: {integrity: sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==} engines: {node: '>=10.13.0'} @@ -7946,7 +7971,6 @@ packages: /ms@2.1.2: resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} - dev: true /ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} @@ -9471,6 +9495,30 @@ packages: engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} dev: true + /socket.io-client@4.7.2: + resolution: {integrity: sha512-vtA0uD4ibrYD793SOIAwlo8cj6haOeMHrGvwPxJsxH7CeIksqJ+3Zc06RvWTIFgiSqx4A3sOnTXpfAEE2Zyz6w==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + engine.io-client: 6.5.3 + socket.io-parser: 4.2.4 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + dev: false + + /socket.io-parser@4.2.4: + resolution: {integrity: sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==} + engines: {node: '>=10.0.0'} + dependencies: + '@socket.io/component-emitter': 3.1.0 + debug: 4.3.4 + transitivePeerDependencies: + - supports-color + dev: false + /socks-proxy-agent@8.0.2: resolution: {integrity: sha512-8zuqoLv1aP/66PHF5TqwJ7Czm3Yv32urJQHrVyhD7mmA6d61Zv8cIXQYPTWwmg6qlupnPvs/QKDmfa4P/qct2g==} engines: {node: '>= 14'} @@ -10499,6 +10547,19 @@ packages: optional: true dev: true + /ws@8.11.0: + resolution: {integrity: sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==} + 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 + dev: false + /xdm@2.1.0: resolution: {integrity: sha512-3LxxbxKcRogYY7cQSMy1tUuU1zKNK9YPqMT7/S0r7Cz2QpyF8O9yFySGD7caOZt+LWUOQioOIX+6ZzCoBCpcAA==} dependencies: @@ -10529,6 +10590,11 @@ packages: - supports-color dev: true + /xmlhttprequest-ssl@2.0.0: + resolution: {integrity: sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==} + engines: {node: '>=0.4.0'} + dev: false + /xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} diff --git a/src/apps/console/root.tsx b/src/apps/console/root.tsx index ff343fb7c..93548e154 100644 --- a/src/apps/console/root.tsx +++ b/src/apps/console/root.tsx @@ -1,3 +1,4 @@ +/* eslint-disable react/jsx-no-useless-fragment */ import Root, { links as baseLinks } from '~/lib/app-setup/root'; import { ChildrenProps } from '~/components/types'; import authStylesUrl from './styles/index.css'; @@ -17,12 +18,8 @@ export const links = () => { export { ErrorBoundary } from '~/lib/app-setup/root'; const Layout = ({ children }: ChildrenProps) => { - return ( - // - // eslint-disable-next-line react/jsx-no-useless-fragment - <>{children} - // - ); + // return {children}; + return <>{children}; }; const _Root = ({ ...props }) => { diff --git a/src/apps/console/routes/test.tsx b/src/apps/console/routes/test.tsx new file mode 100644 index 000000000..6c408999a --- /dev/null +++ b/src/apps/console/routes/test.tsx @@ -0,0 +1,38 @@ +import { useLoaderData } from '@remix-run/react'; +import SocketProvider, { + useSubscribe, + // useSubscribe, + useWatch, +} from '~/root/lib/client/helpers/socket-context'; + +export const loader = () => { + return { + data: Math.random(), + }; +}; + +const App = () => { + useSubscribe( + ['account:newteam.cluster'], + () => { + console.log('hi'); + }, + [] + ); + + // useWatch('account:newteam.cluster'); + // res-updates.account.acc-ruwibp-pf5jvcsew2rnl54kriv59.cluster.* + // res-updates.account.accid.project.projid.env.envid.app.* + + const { data } = useLoaderData(); + + return
{data}
; +}; + +export default () => { + return ( + + + + ); +};