ui: use broadcast channel to handle message update

This commit is contained in:
undefined committed 2022-05-04 19:57:49 +08:00
1 parent 27056c5b69
commit 87b980f87d
7 files changed
+133 -139

No files matched your search

@@ -206,7 +206,6 @@ export default function (env = {}) {
{ from: root('static') },
{ from: root(`${dirname(require.resolve('vditor/package.json'))}/dist`), to: 'vditor/dist' },
{ from: `${dirname(require.resolve('monaco-themes/package.json'))}/themes`, to: 'monaco/themes/' },
{ from: root('.build/sharedworker.js'), to: 'sharedworker.js' },
],
}),
new webpack.DefinePlugin({
+1 -11
View File
@@ -3,7 +3,6 @@
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';
@@ -18,15 +17,6 @@ const argv = cac().parse();
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, {
@@ -36,7 +26,7 @@ async function runWebpack({
stats: 'none',
index: root('public'),
proxy: {
context: (path) => !path.includes('sharedworker.js'),
context: (path) => !path.includes('sockjs-node'),
target: 'http://localhost:2333',
ws: true,
},
@@ -1,67 +0,0 @@
import { AutoloadPage } from 'vj/misc/Page';
import { InfoDialog } from 'vj/components/dialog';
import VjNotification from 'vj/components/notification/index';
import { FLAG_ALERT } from 'vj/constant/message';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
const messagePage = new AutoloadPage('messagePage', (pagename) => {
if (pagename === 'home_messages' || !UserContext._id) return;
if (UserContext.unreadMsg) {
new VjNotification({
type: 'info',
message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`),
duration: 5000,
action: () => window.open('/home/messages', '_blank'),
}).show();
}
const onmessage = (message) => {
const msg = JSON.parse(message);
console.log('Received message', msg);
if (msg.mdoc.flag & FLAG_ALERT) {
// Is alert
return new InfoDialog({
cancelByClickingBack: false,
$body: tpl`
<div class="typo">
${{ templateRaw: true, html: i18n('Message from {0}:', `<div data-user>${msg.mdoc.from}</div>`) }}
<p>${i18n(msg.mdoc.content)}</p>
</div>`,
}).open();
}
if (msg.udoc._id === 1 && msg.mdoc.flag & 4) {
// System notification
return new VjNotification({
message: i18n('You received a system message, click here to view.'),
duration: 15000,
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
}).show();
}
// Is message
return new VjNotification({
title: msg.udoc.uname,
avatar: msg.udoc.avatarUrl,
message: msg.mdoc.content,
duration: 15000,
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
}).show();
};
const path = new URL('/home/messages-conn', window.location.href).toString();
const worker = new SharedWorker('/sharedworker.js', { name: 'Hydro Messages Shared Worker' });
worker.port.start();
worker.port.postMessage({ type: 'init', path, cookie: document.cookie });
worker.port.onmessage = (e) => {
if (e.data.type !== 'message') return;
if (document.hidden) {
worker.port.postMessage({ type: 'fail' });
} else {
worker.port.postMessage({ type: 'success' });
onmessage(e.data.payload);
}
};
});
export default messagePage;
@@ -0,0 +1,129 @@
import { AutoloadPage } from 'vj/misc/Page';
import { InfoDialog } from 'vj/components/dialog';
import VjNotification from 'vj/components/notification/index';
import ReconnectingWebsocket from 'reconnecting-websocket';
import { FLAG_ALERT } from 'vj/constant/message';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
import { nanoid } from 'nanoid';
import sleep from 'vj/utils/delay';
function asMaster() {
console.log('init as master');
const channel = new BroadcastChannel('hydro-messages');
const interval = setInterval(() => channel.postMessage({ type: 'master' }), 5000);
const url = new URL('/home/messages-conn', window.location.href.replace('http', 'ws'));
url.searchParams.append('sid', document.cookie);
const sock = new ReconnectingWebsocket(url.toString());
function destory() {
sock.close();
channel.close();
clearInterval(interval);
}
const pending = {};
sock.onopen = () => console.log('Connected');
sock.onerror = console.error;
sock.onclose = (...args) => console.log('Closed', ...args);
sock.onmessage = async (message) => {
if (process.env.NODE_ENV !== 'production') console.log('onmessage: ', message);
const payload = JSON.parse(message.data);
if (['PermissionError', 'PrivilegeError'].includes(payload.error)) {
destory();
return;
}
const id = nanoid();
channel.postMessage({ type: 'message', id, payload });
const success = await new Promise<boolean>((resolve) => {
pending[id] = resolve;
setTimeout(() => {
delete pending[id];
resolve(false);
}, 1000);
});
if (!success && window.Notification?.permission === 'granted') {
// eslint-disable-next-line no-new
new window.Notification(
payload.udoc.uname || 'Hydro Notification',
{
icon: payload.udoc.avatarUrl || '/android-chrome-192x192.png',
body: payload.mdoc.content,
},
);
}
};
channel.onmessage = (msg) => {
if (msg.data.type === 'message-push') pending[msg.data.id]?.(true);
};
}
const onmessage = (msg) => {
console.log('Received message', msg);
if (msg.mdoc.flag & FLAG_ALERT) {
// Is alert
return new InfoDialog({
cancelByClickingBack: false,
$body: tpl`
<div class="typo">
${{ templateRaw: true, html: i18n('Message from {0}:', `<div data-user>${msg.mdoc.from}</div>`) }}
<p>${i18n(msg.mdoc.content)}</p>
</div>`,
}).open();
}
// Is message
return new VjNotification({
...(msg.udoc._id === 1 && msg.mdoc.flag & 4)
? { message: i18n('You received a system message, click here to view.') }
: {
title: msg.udoc.uname,
avatar: msg.udoc.avatarUrl,
message: msg.mdoc.content,
},
duration: 15000,
action: () => window.open(`/home/messages?uid=${msg.udoc._id}`, '_blank'),
}).show();
};
async function asSlave() {
console.log('message load as slave');
const channel = new BroadcastChannel('hydro-messages');
const selfId = nanoid();
setInterval(() => channel.postMessage({ type: 'node', id: selfId }), 5000);
let alive = false;
let nodes = new Set<string>([selfId]);
channel.onmessage = (msg) => {
if (msg.data.target && msg.data.target !== selfId) return;
if (msg.data.type === 'master') alive = true;
if (msg.data.type === 'node') nodes.add(msg.data.id);
if (msg.data.type === 'message') {
if (!document.hidden) {
channel.postMessage({ type: 'message-push', id: msg.data.id });
onmessage(msg.data.payload);
}
}
};
while (true) { // eslint-disable-line no-constant-condition
alive = false;
nodes = new Set<string>([selfId]);
await sleep(7000);
if (!alive && Array.from(nodes).sort()[0] === selfId) asMaster();
}
}
const messagePage = new AutoloadPage('messagePage', (pagename) => {
if (pagename === 'home_messages' || !UserContext._id) return;
if (UserContext.unreadMsg) {
new VjNotification({
type: 'info',
message: i18n(`You have ${UserContext.unreadMsg > 1 ? 'new messages' : 'a new message'}.`),
duration: 5000,
action: () => window.open('/home/messages', '_blank'),
}).show();
}
if (!window.BroadcastChannel) {
console.error('BoardcastChannel not supported');
return;
}
asSlave();
});
export default messagePage;
@@ -19,11 +19,8 @@ export default class Sock {
this.sock.onmessage = (message) => {
if (process.env.NODE_ENV !== 'production') console.log('Sock.onmessage: ', message);
const msg = JSON.parse(message.data);
if (msg.event === 'auth') {
if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') {
this.close();
}
} else this.onmessage?.(message);
if (msg.error === 'PermissionError' || msg.error === 'PrivilegeError') this.close();
else this.onmessage?.(message);
};
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.35.0",
"version": "4.35.1",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
-54
View File
@@ -1,54 +0,0 @@
import ReconnectingWebsocket from 'reconnecting-websocket';
console.log('SharedWorker init');
const ports: MessagePort[] = [];
let conn: ReconnectingWebsocket;
function initSharedConn(path: string, sid: string) {
if (conn) return;
console.log('Init connection for', path);
const url = new URL(path);
url.protocol = url.protocol.replace('http', 'ws');
url.searchParams.append('sid', sid);
const sock = new ReconnectingWebsocket(url.toString());
conn = sock;
sock.onopen = () => console.log('Connected');
sock.onerror = console.error;
sock.onclose = (...args) => console.log('Closed', ...args);
sock.onmessage = async (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)) {
conn?.close();
conn = null;
}
return;
}
for (const port of ports) {
port.postMessage({ type: 'message', payload: message.data });
const success = await new Promise((resolve) => {
const handle = (msg) => {
if (['success', 'fail'].includes(msg.data.type)) {
port.removeEventListener('message', handle);
resolve(msg.data.type === 'success');
}
};
port.addEventListener('message', handle);
});
if (success) return;
}
console.log('Failed to push notification');
// TODO try system notification
};
}
// @ts-ignore
onconnect = function (e) { // eslint-disable-line no-undef
const port: MessagePort = e.ports[0];
port.addEventListener('message', (msg) => {
initSharedConn(msg.data.path, msg.data.cookie);
});
port.start();
ports.push(port);
};