core, ui, judge: rewrite http & ws layer (#352)

This commit is contained in:
undefined authored and GitHub committed 2022-05-03 20:22:12 +08:00
1 parent 3d2384f31a
commit 5af082eac8
37 files changed
+868 -889

No files matched your search

@@ -6,60 +6,62 @@ import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
const messagePage = new AutoloadPage('messagePage', (pagename) => {
if (pagename === 'home_messages') return;
let visible = true;
document.addEventListener('visibilitychange', () => {
const state = document.visibilityState;
if (state === 'hidden' && visible) visible = false;
if (state === 'visible' && !visible) visible = true;
});
if (pagename === 'home_messages' || !UserContext._id) return;
async function init() {
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 { default: SockJs } = await import('../socket');
const sock = new SockJs('/home/messages-conn', true, true);
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
console.log('Received message', msg);
if (msg.mdoc.flag & FLAG_ALERT) {
// Is alert
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();
} else if (msg.udoc._id === 1 && msg.mdoc.flag & 4) {
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();
} else {
// Is message
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();
}
};
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 (UserContext._id !== 0) init();
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;
@@ -35,7 +35,6 @@ function handlerSwitchAccount(ev) {
</div>
`,
onDispatch(action) {
console.log(action, userSelector.value());
if (action === 'ok' && userSelector.value() === null) {
userSelector.focus();
return false;
+17 -67
View File
@@ -1,88 +1,38 @@
import SockJS from 'sockjs-client';
import $ from 'jquery';
import Notification from 'vj/components/notification/index';
import i18n from 'vj/utils/i18n';
import tpl from 'vj/utils/tpl';
const notification = tpl`<div style="position: fixed; left: 0px; bottom: -100px; width: 100%; \
background: rgb(255, 241, 184); z-index: 2147483647; color: rgb(97, 52, 0); \
text-align: center; font-size: 18px; font-family: Consolas, Menlo, Courier, monospace; \
padding: 8px 0px; box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 8px;" id="disconnect_notification" \
>${i18n('Disconnected from the server, trying to reconnect...')}</div>`;
const $el = $(notification);
$el.appendTo('body');
$el.hide();
function create() {
$el.show();
$el.animate({ bottom: 0 }, 100);
}
function remove() {
$el.animate({ bottom: -100 }, 100, () => $el.remove());
}
import ReconnectingWebSocket from 'reconnecting-websocket';
export default class Sock {
constructor(url, showNotification = true, useShared = false) {
this.url = url;
this.shared = false;
this.isreconnect = false;
this.retryCount = 0;
if (useShared && window.SharedWorker) this.initShared();
else this.init();
this.showNotification = showNotification;
}
onauth() {
remove();
this.retryCount = 0;
if (this.onopen) this.onopen(this.sock);
}
init() {
this.sock = new SockJS(this.url);
// SockJS wouldn't send cookie. hack.
this.sock.onopen = () => this.send(document.cookie);
constructor(url) {
const i = new URL(url, window.location.href);
i.protocol = i.protocol.replace('http', 'ws');
this.url = i.toString();
this.closed = false;
this.sock = new ReconnectingWebSocket(this.url);
this.sock.onopen = () => {
console.log('Connected');
this.onopen?.(this.sock);
};
this.sock.onclose = ({ code, reason }) => {
this.retryCount++;
console.warn('Connection closed, ', code, reason);
if (code >= 4000) this.closed = true;
if (!this.closed) {
this.isreconnect = setTimeout(this.init.bind(this), 3000);
}
if (this.showNotification && this.retryCount > 3) create();
if (this.onclose) this.onclose(code, reason);
this.onclose?.(code, reason);
};
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') {
if (this.showNotification) Notification.info(i18n('Connect fail: Permission denied.'));
this.closed = true;
} else this.onauth();
} else if (this.onmessage) this.onmessage(message);
this.close();
}
} else this.onmessage?.(message);
};
}
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) {
if (this.shared) this.worker.port.postMessage({ type: 'message', path: this.url, payload: data });
else this.sock.send(data);
this.sock.send(data);
}
close() {
this.closed = true;
if (!this.shared) this.sock.close();
this.sock?.close?.();
}
}
+1 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@hydrooj/ui-default",
"version": "4.34.40",
"version": "4.35.0",
"author": "undefined <i@undefined.moe>",
"license": "AGPL-3.0",
"main": "hydro.js",
@@ -26,7 +26,6 @@
"@types/katex": "^0.14.0",
"@types/qrcode": "^1.4.2",
"@types/redux-logger": "^3.0.9",
"@types/sockjs-client": "^1.5.1",
"@undefined-moe/monaco-yaml": "^2.5.1",
"ansi_up": "^5.1.0",
"autoprefixer": "^9.8.8",
@@ -89,7 +88,6 @@
"redux-thunk": "^2.4.1",
"rupture": "^0.7.1",
"slideout": "^1.0.1",
"sockjs-client": "^1.6.0",
"speed-measure-webpack-plugin": "^1.5.0",
"sticky-kit": "^1.1.3",
"style-loader": "^2.0.0",
@@ -23,7 +23,7 @@ const page = new NamedPage('home_messages', () => {
}
async function mountComponent() {
const { default: SockJs } = await import('../components/socket');
const { default: WebSocket } = await import('../components/socket');
const { default: MessagePadApp } = await import('../components/messagepad');
const { default: MessagePadReducer } = await import('../components/messagepad/reducers');
const {
@@ -32,7 +32,7 @@ const page = new NamedPage('home_messages', () => {
reduxStore = store;
const sock = new SockJs('/home/messages-conn');
const sock = new WebSocket('/home/messages-conn');
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
store.dispatch({
@@ -1,9 +1,9 @@
import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('manage_dashboard', async () => {
const { default: SockJs } = await import('../components/socket');
const { default: WebSocket } = await import('../components/socket');
const sock = new SockJs('/manage/check-conn');
const sock = new WebSocket('/manage/check-conn');
sock.onopen = () => {
$('<blockquote class="blue"><p>Connection opened.</p></blockquote>').appendTo('#messages');
};
@@ -129,14 +129,14 @@ const page = new NamedPage(['problem_detail', 'contest_detail_problem', 'homewor
if (reactLoaded) return;
$('.loader-container').show();
const { default: SockJs } = await import('../components/socket');
const { default: WebSocket } = await import('../components/socket');
const { default: ScratchpadApp } = await import('../components/scratchpad');
const { default: ScratchpadReducer } = await import('../components/scratchpad/reducers');
const {
React, render, unmountComponentAtNode, Provider, store,
} = await loadReactRedux(ScratchpadReducer);
const sock = new SockJs(UiContext.pretestConnUrl);
const sock = new WebSocket(UiContext.pretestConnUrl);
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
store.dispatch({
@@ -2,12 +2,12 @@ import { NamedPage } from 'vj/misc/Page';
const page = new NamedPage('record_detail', async () => {
if (!UiContext.socketUrl) return;
const [{ default: SockJs }, { DiffDOM }] = await Promise.all([
const [{ default: WebSocket }, { DiffDOM }] = await Promise.all([
import('../components/socket'),
import('diff-dom'),
]);
const sock = new SockJs(UiContext.socketUrl);
const sock = new WebSocket(UiContext.socketUrl);
const dd = new DiffDOM();
sock.onmessage = (message) => {
const msg = JSON.parse(message.data);
@@ -5,12 +5,12 @@ import tpl from 'vj/utils/tpl';
import getAvailableLangs from 'vj/utils/availableLangs';
const page = new NamedPage('record_main', async () => {
const [{ default: SockJs }, { DiffDOM }] = await Promise.all([
const [{ default: WebSocket }, { DiffDOM }] = await Promise.all([
import('../components/socket'),
import('diff-dom'),
]);
const sock = new SockJs(UiContext.socketUrl);
const sock = new WebSocket(UiContext.socketUrl);
const dd = new DiffDOM();
let firstLoad = true;
+42 -51
View File
@@ -1,63 +1,54 @@
import ReconnectingWebsocket from 'reconnecting-websocket';
console.log('SharedWorker init');
const ports: MessagePort[] = [];
let conn: ReconnectingWebsocket;
const sharedConnections: Record<string, [ReconnectingWebsocket, MessagePort[]]> = {};
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 closeSharedConn(path: string) {
sharedConnections[path][0].close();
delete sharedConnections[path];
}
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 });
closeSharedConn(path);
} 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);
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: { 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.addEventListener('message', (msg) => {
initSharedConn(msg.data.path, msg.data.cookie);
});
port.start();
ports.push(port);
};
@@ -2,11 +2,11 @@
{%- if not invalid -%}
{% set _linkArgs = { pid:pdoc.pid|default(pdoc.docId) } %}
{% if tdoc %}{{ set(_linkArgs, 'query', {tid:tdoc.docId}) }}{% endif %}
{% if pdoc.domainId !== handler.domainId %}{{ set(_linkArgs, 'domainId', pdoc.domainId) }}{% endif %}
{% if pdoc.domainId !== handler.args.domainId %}{{ set(_linkArgs, 'domainId', pdoc.domainId) }}{% endif %}
<a href="{{ url('problem_detail', _linkArgs) }}"{% if small %} data-tooltip="{{ pdoc.title }}"{% endif %}>
{%- endif -%}
{%- if pdoc.domainId !== handler.domainId -%}<b>{{ pdoc.domainId }}#</b>{%- endif -%}
<b>{{ pdoc.pid or 'P'+pdoc.docId if pdoc.domainId === handler.domainId else pdoc.docId }}</b>
{%- if pdoc.domainId !== handler.args.domainId -%}<b>{{ pdoc.domainId }}#</b>{%- endif -%}
<b>{{ pdoc.pid or 'P'+pdoc.docId if pdoc.domainId === handler.args.domainId else pdoc.docId }}</b>
{%- if not small -%}&nbsp;&nbsp;{{ pdoc.title }}{%- endif -%}
{%- if not invalid -%}
</a>
@@ -49,7 +49,7 @@
<td class="col--pid display-mode-hide">{{ pdoc.docId }}</td>
{% if handler.user.hasPriv(PRIV.PRIV_USER_PROFILE) %}
{% if psdoc['rid'] %}
{{ record.render_status_td(psdoc, rid_key='rid', class="edit-mode-hide", allDomain=pdoc.domainId!==handler.domainId) }}
{{ record.render_status_td(psdoc, rid_key='rid', class="edit-mode-hide", allDomain=pdoc.domainId!==handler.args.domainId) }}
{% else %}
<td class="col--status record-status--border edit-mode-hide"></td>
{% endif %}
@@ -13,14 +13,14 @@
{{ set(UiContext, 'postSubmitUrl', url('problem_submit', pid=pdoc.docId, query={tid:tdoc.docId})) }}
{{ set(UiContext, 'getSubmissionsUrl', url('record_main', query={fullStatus:'true', pid:pdoc.docId, tid:tdoc.docId})) }}
{{ set(UiContext, 'getRecordDetailUrl', url('record_detail', rid='{rid}', query={tid:tdoc.docId})) }}
{{ set(UiContext, 'pretestConnUrl', "/record-conn?pretest=1&uidOrName=" + handler.user._id + "&pid=" + pdoc.docId + "&domainId=" + handler.domainId + "&tid=" + tdoc.docId) }}
{{ set(UiContext, 'pretestConnUrl', "/record-conn?pretest=1&uidOrName=" + handler.user._id + "&pid=" + pdoc.docId + "&domainId=" + handler.args.domainId + "&tid=" + tdoc.docId) }}
{{ set(UiContext, 'tdoc', tdoc) }}
{% else %}
{{ set(UiContext, 'canViewRecord', true) }}
{{ set(UiContext, 'postSubmitUrl', url('problem_submit', pid=pdoc.docId)) }}
{{ set(UiContext, 'getSubmissionsUrl', url('record_main', query={fullStatus:'true', pid:pdoc.docId})) }}
{{ set(UiContext, 'getRecordDetailUrl', url('record_detail', rid='{rid}')) }}
{{ set(UiContext, 'pretestConnUrl', "/record-conn?pretest=1&uidOrName=" + handler.user._id + "&pid=" + pdoc.docId + "&domainId=" + handler.domainId) }}
{{ set(UiContext, 'pretestConnUrl', "/record-conn?pretest=1&uidOrName=" + handler.user._id + "&pid=" + pdoc.docId + "&domainId=" + handler.args.domainId) }}
{% endif %}
</script>
<div class="row">
@@ -2,7 +2,7 @@
{% block content %}
{{ set(
UiContext, 'socketUrl',
"/record-conn?domainId=" + handler.domainId
"/record-conn?domainId=" + handler.args.domainId
+ (("&tid=" + filterTid) if filterTid else "")
+ (("&uidOrName=" + filterUidOrName) if filterUidOrName else "")
+ (("&pid=" + filterPid) if filterPid else "")