diff --git a/package.json b/package.json index 81e7805bf..81df75625 100644 --- a/package.json +++ b/package.json @@ -28,15 +28,15 @@ "license": "AGPL-3.0-only", "devDependencies": { "@types/cross-spawn": "^6.0.2", - "@types/node": "^16.11.21", + "@types/node": "^16.11.22", "@types/semver": "^7.3.9", - "@typescript-eslint/eslint-plugin": "5.10.0", - "@typescript-eslint/parser": "5.10.0", + "@typescript-eslint/eslint-plugin": "5.10.2", + "@typescript-eslint/parser": "5.10.2", "cac": "^6.7.12", "cross-env": "^7.0.3", "cross-spawn": "^7.0.3", "esbuild": "0.14.3", - "eslint": "^8.7.0", + "eslint": "^8.8.0", "eslint-config-airbnb-typescript": "12.3.1", "eslint-import-resolver-typescript": "2.5.0", "eslint-plugin-import": "2.25.4", diff --git a/packages/ui-default/build/config/webpack.js b/packages/ui-default/build/config/webpack.js index 8746d5222..a3fe4cec8 100644 --- a/packages/ui-default/build/config/webpack.js +++ b/packages/ui-default/build/config/webpack.js @@ -199,6 +199,7 @@ export default function (env = {}) { { from: root('static') }, { from: root(`${dirname(require.resolve('vditor/package.json'))}`), to: 'vditor/' }, { from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' }, + { from: root('.build/sharedworker.js'), to: 'sharedworker.js' }, ], }), new webpack.DefinePlugin({ diff --git a/packages/ui-default/build/main.js b/packages/ui-default/build/main.js index 51172d68c..62a123d28 100644 --- a/packages/ui-default/build/main.js +++ b/packages/ui-default/build/main.js @@ -2,6 +2,7 @@ import cac from 'cac'; import fs from 'fs-extra'; import webpack from 'webpack'; +import esbuild from 'esbuild'; import WebpackDevServer from 'webpack-dev-server'; import gulp from 'gulp'; import log from 'fancy-log'; @@ -13,9 +14,18 @@ import webpackConfig from './config/webpack'; const argv = cac().parse(); -function runWebpack({ +async function runWebpack({ watch, production, measure, dev, }) { + const result = esbuild.build({ + bundle: true, + format: 'iife', + entryPoints: [root('sharedworker.ts')], + outfile: root('.build/sharedworker.js'), + watch: dev || watch, + minify: production, + }); + if (!dev && !watch) await result; const compiler = webpack(webpackConfig({ watch, production, measure })); if (dev) { const server = new WebpackDevServer(compiler, { @@ -23,9 +33,9 @@ function runWebpack({ hot: true, disableHostCheck: true, stats: 'none', - index: '', + index: root('public'), proxy: { - context: () => true, + context: (path) => !path.includes('sharedworker.js'), target: 'http://localhost:2333', ws: true, }, diff --git a/packages/ui-default/components/message/index.page.js b/packages/ui-default/components/message/index.page.js index 25e05b3f3..337f97487 100644 --- a/packages/ui-default/components/message/index.page.js +++ b/packages/ui-default/components/message/index.page.js @@ -26,7 +26,7 @@ const messagePage = new AutoloadPage('messagePage', (pagename) => { const { default: SockJs } = await import('../socket'); - const sock = new SockJs('/home/messages-conn'); + const sock = new SockJs('/home/messages-conn', true, true); sock.onmessage = (message) => { const msg = JSON.parse(message.data); console.log('Received message', msg); diff --git a/packages/ui-default/components/socket/index.js b/packages/ui-default/components/socket/index.js index e7066699c..f752ee326 100644 --- a/packages/ui-default/components/socket/index.js +++ b/packages/ui-default/components/socket/index.js @@ -20,11 +20,13 @@ function remove() { } export default class Sock { - constructor(url, showNotification = true) { + constructor(url, showNotification = true, useShared = false) { this.url = url; + this.shared = false; this.isreconnect = false; this.retryCount = 0; - this.init(); + if (useShared && window.SharedWorker) this.initShared(); + else this.init(); this.showNotification = showNotification; } @@ -60,12 +62,25 @@ export default class Sock { }; } + initShared() { + this.shared = true; + const worker = new SharedWorker('/sharedworker.js', { name: 'Hydro Shared Connections Worker' }); + worker.port.start(); + const path = `${new URL(this.url, window.location.href).href.replace('http', 'ws')}/websocket`; + worker.port.postMessage({ type: 'sharedConn', path, cookie: document.cookie }); + worker.port.onmessage = (e) => { + if (e.data.type === 'message') this.onmessage({ data: e.data.payload }); + }; + this.worker = worker; + } + send(data) { - this.sock.send(data); + if (this.shared) this.worker.port.postMessage({ type: 'message', path: this.url, payload: data }); + else this.sock.send(data); } close() { this.closed = true; - this.sock.close(); + if (!this.shared) this.sock.close(); } } diff --git a/packages/ui-default/jsconfig.json b/packages/ui-default/jsconfig.json index d7c8c7925..8280895e7 100644 --- a/packages/ui-default/jsconfig.json +++ b/packages/ui-default/jsconfig.json @@ -8,6 +8,9 @@ "allowSyntheticDefaultImports": true, "target": "ES2020", "baseUrl": ".", + "lib": [ + "ES2020" + ], "moduleResolution": "node", "paths": { "vj/*": [ diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 2ed4f35bc..290cfecfc 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -1,6 +1,6 @@ { "name": "@hydrooj/ui-default", - "version": "4.30.11", + "version": "4.31.0", "author": "undefined ", "license": "AGPL-3.0", "main": "hydro.js", @@ -18,6 +18,7 @@ "@types/katex": "^0.11.1", "@types/qrcode": "^1.4.2", "@types/redux-logger": "^3.0.9", + "@types/sharedworker": "^0.0.64", "@types/sockjs-client": "^1.5.1", "@undefined-moe/monaco-yaml": "^2.5.1", "ansi_up": "^5.1.0", @@ -74,6 +75,7 @@ "react-dom": "^17.0.2", "react-redux": "^7.2.6", "react-split-pane": "^0.1.92", + "reconnecting-websocket": "^4.4.0", "redux": "^4.1.2", "redux-logger": "^3.0.6", "redux-promise-middleware": "^6.1.2", diff --git a/packages/ui-default/sharedworker.ts b/packages/ui-default/sharedworker.ts new file mode 100644 index 000000000..5f025e4a3 --- /dev/null +++ b/packages/ui-default/sharedworker.ts @@ -0,0 +1,58 @@ +import ReconnectingWebsocket from 'reconnecting-websocket'; + +console.log('SharedWorker init'); + +const sharedConnections: Record = {}; +interface RequestInitSharedConnPayload { + type: 'sharedConn'; + cookie: any; + path: string; +} +interface RequestSendMessagePayload { + type: 'message'; + path: string; + payload: string; +} +type RequestPayload = RequestInitSharedConnPayload | RequestSendMessagePayload; + +function broadcastMsg(ports: MessagePort[], message: any) { + for (const p of ports) p.postMessage(message); +} +function initSharedConn(path: string, port: MessagePort, cookie: any) { + if (!sharedConnections[path]) { + console.log('Init connection for', path); + const sock = new ReconnectingWebsocket(path); + const ports: MessagePort[] = []; + sharedConnections[path] = [sock, ports]; + + sock.onopen = () => sock.send(cookie); + sock.onmessage = (message) => { + if (process.env.NODE_ENV !== 'production') console.log('SharedWorker.port.onmessage: ', message); + const payload = JSON.parse(message.data); + if (payload.event === 'auth') { + if (['PermissionError', 'PrivilegeError'].includes(payload.error)) { + broadcastMsg(ports, { type: 'close', path, error: payload.error }); + } else { + console.log('Connected to', path); + broadcastMsg(ports, { type: 'open', path }); + } + } else broadcastMsg(ports, { type: 'message', path, payload: message.data }); + }; + sock.onerror = () => broadcastMsg(ports, { type: 'error', path }); + sock.onclose = (ev) => broadcastMsg(ports, { type: 'close', path, error: ev.reason }); + } + + sharedConnections[path][1].push(port); +} + +// eslint-disable-next-line no-undef +onconnect = function (e) { + const port = e.ports[0]; + + port.addEventListener('message', (msg: { data: RequestPayload }) => { + if (msg.data.type === 'sharedConn') initSharedConn(msg.data.path, port, msg.data.cookie); + if (msg.data.type === 'message') sharedConnections[msg.data.path]?.[0].send(msg.data.payload); + }); + + port.start(); +};