use cordis@4

This commit is contained in:
undefined committed 2025-02-19 05:28:12 +08:00
1 parent 085b6c9b4c
commit 9264f340cb
18 files changed
+284 -168

No files matched your search

+2 -2
View File
@@ -5,8 +5,8 @@
"main": "react.yaml",
"dependencies": {
"@stylistic/eslint-plugin": "^3.1.0",
"@typescript-eslint/eslint-plugin": "^8.24.0",
"@typescript-eslint/parser": "^8.24.0",
"@typescript-eslint/eslint-plugin": "^8.24.1",
"@typescript-eslint/parser": "^8.24.1",
"eslint-config-airbnb": "^19.0.4",
"eslint-plugin-de-morgan": "^1.0.1",
"eslint-plugin-github": "^5.1.8",
+1 -1
View File
@@ -7,7 +7,7 @@
"node": ">=16"
},
"dependencies": {
"esbuild": "0.24.0",
"esbuild": "0.25.0",
"source-map-support": "^0.5.21"
},
"devDependencies": {
+6 -5
View File
@@ -36,16 +36,16 @@
"@types/autocannon": "^7.12.6",
"@types/chai": "^5.0.1",
"@types/cross-spawn": "^6.0.6",
"@types/node": "22.7.4",
"@types/node": "22.13.4",
"@types/semver": "^7.5.8",
"autocannon": "^8.0.0",
"cac": "^6.7.14",
"chai": "^5.1.2",
"chai": "^5.2.0",
"cross-env": "^7.0.3",
"cross-spawn": "^7.0.6",
"esbuild": "0.24.0",
"esbuild": "0.25.0",
"eslint": "^8.57.1",
"eslint-import-resolver-typescript": "^3.7.0",
"eslint-import-resolver-typescript": "^3.8.1",
"eslint-import-resolver-webpack": "^0.13.10",
"fs-extra": "^11.3.0",
"globby": "~14.0.2",
@@ -57,7 +57,8 @@
"semver": "^7.7.1",
"simple-git": "^3.27.0",
"supertest": "^7.0.0",
"typescript": "5.7.2",
"supports-hyperlinks": "^3.2.0",
"typescript": "5.7.3",
"wtfnode": "^0.10.0"
}
}
+7 -5
View File
@@ -16,7 +16,7 @@
"@aws-sdk/lib-storage": "3.726.1",
"@aws-sdk/s3-presigned-post": "3.726.1",
"@aws-sdk/s3-request-presigner": "3.726.1",
"@graphql-tools/schema": "^10.0.16",
"@graphql-tools/schema": "^10.0.18",
"@hydrooj/common": "workspace:^",
"@hydrooj/framework": "workspace:^",
"@hydrooj/utils": "workspace:^",
@@ -25,7 +25,7 @@
"@yarnpkg/libzip": "^3.1.0",
"adm-zip": "0.5.14",
"cac": "^6.7.14",
"cordis": "3.18.1",
"cordis": "4.0.0-alpha.0",
"csv-stringify": "^6.5.2",
"emoji-regex": "^10.4.0",
"emojis-list": "2.1.0",
@@ -41,22 +41,24 @@
"moment-timezone": "^0.5.47",
"mongodb": "^5.9.2",
"mongodb-uri": "^0.9.7",
"nanoid": "^5.0.9",
"nanoid": "^5.1.0",
"nodemailer": "^6.10.0",
"notp": "^2.0.3",
"p-queue": "^8.1.0",
"sanitize-filename": "^1.6.3",
"saslprep": "^1.0.3",
"schemastery": "^3.15.0",
"schemastery": "^3.16.0",
"semver": "^7.7.1",
"shorty.js": "^1.0.1",
"superagent": "^10.1.1",
"tar": "6.2.1",
"thirty-two": "^1.0.2",
"ua-parser-js": "^2.0.1"
"ua-parser-js": "^2.0.2"
},
"devDependencies": {
"@babel/code-frame": "^7.26.2",
"@types/adm-zip": "^0.4.34",
"@types/babel__code-frame": "^7.0.6",
"@types/fs-extra": "^11.0.4",
"@types/js-yaml": "^4.0.9",
"@types/koa-static-cache": "^5.1.4",
+99 -11
View File
@@ -24,10 +24,8 @@ function provideModule<T extends keyof ModuleInterfaces>(type: T, id: string, mo
}
export type EffectScope = cordis.EffectScope<Context>;
export type ForkScope = cordis.ForkScope<Context>;
export type MainScope = cordis.MainScope<Context>;
export type { Disposable, ScopeStatus, Plugin } from 'cordis';
export { Disposable, Plugin, ScopeStatus } from 'cordis';
export interface Context extends cordis.Context, Pick<WebService, 'Route' | 'Connection' | 'withHandlerClass'> {
// @ts-ignore
@@ -40,12 +38,15 @@ export interface Context extends cordis.Context, Pick<WebService, 'Route' | 'Con
injectUI: typeof inject;
broadcast: Context['emit'];
geoip?: GeoIP;
// TODO: move to @cordisjs/plugin-timer
setTimeout(callback: () => void, delay: number): () => void
setInterval(callback: () => void, delay: number): () => void
sleep(delay: number): Promise<void>
throttle<F extends (...args: any[]) => void>(callback: F, delay: number, noTrailing?: boolean): WithDispose<F>
debounce<F extends (...args: any[]) => void>(callback: F, delay: number): WithDispose<F>
}
export abstract class Service<T = any, C extends Context = Context> extends cordis.Service<T, C> {
[cordis.Service.setup]() {
this.ctx = new Context() as C;
}
export abstract class Service extends cordis.Service<Context> {
}
const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
@@ -56,7 +57,7 @@ const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
});
};
export class ApiMixin extends Service {
export class ApiMixin extends cordis.Service {
addScript = T(addScript);
setImmediate = T(setImmediate, clearImmediate);
provideModule = T(provideModule);
@@ -65,17 +66,104 @@ export class ApiMixin extends Service {
this.ctx.emit('bus/broadcast', event, payload, process.env.TRACE_BROADCAST ? new Error().stack : null);
constructor(ctx) {
super(ctx, '$api', true);
super(ctx, '$api');
ctx.mixin('$api', ['addScript', 'setImmediate', 'provideModule', 'injectUI', 'broadcast']);
}
}
type WithDispose<T> = T & { dispose: () => void };
class TimerService extends Service {
constructor(ctx) {
super(ctx, '$timer');
ctx.mixin('$timer', ['setTimeout', 'setInterval', 'sleep', 'throttle', 'debounce']);
}
setTimeout(callback: () => void, delay: number) {
const dispose = this.ctx.effect(() => {
const timer = setTimeout(() => {
dispose();
callback();
}, delay);
return () => clearTimeout(timer);
});
return dispose;
}
setInterval(callback: () => void, delay: number) {
return this.ctx.effect(() => {
const timer = setInterval(callback, delay);
return () => clearInterval(timer);
});
}
sleep(delay: number) {
const caller = this.ctx;
return new Promise<void>((resolve, reject) => {
const dispose1 = this.setTimeout(() => {
dispose1();
dispose2(); // eslint-disable-line @typescript-eslint/no-use-before-define
resolve();
}, delay);
const dispose2 = caller.on('dispose', () => {
dispose1();
dispose2();
reject(new Error('Context has been disposed'));
});
});
}
private createWrapper(callback: (args: any[], check: () => boolean) => any, isDisposed = false) {
this.ctx.scope.assertActive();
let timer: number | NodeJS.Timeout | undefined;
const dispose = () => {
isDisposed = true;
remove(); // eslint-disable-line @typescript-eslint/no-use-before-define
clearTimeout(timer);
};
const wrapper: any = (...args: any[]) => {
clearTimeout(timer);
timer = callback(args, () => !isDisposed && this.ctx.scope.active);
};
wrapper.dispose = dispose;
const remove = this.ctx.scope.disposables.push(dispose);
return wrapper;
}
throttle<F extends (...args: any[]) => void>(callback: F, delay: number, noTrailing?: boolean): WithDispose<F> {
let lastCall = -Infinity;
const execute = (...args: any[]) => {
lastCall = Date.now();
callback(...args);
};
return this.createWrapper((args, isActive) => { // eslint-disable-line consistent-return
const now = Date.now();
const remaining = delay - (now - lastCall);
if (remaining <= 0) {
execute(...args);
} else if (isActive()) {
return setTimeout(execute, remaining, ...args);
}
}, noTrailing);
}
debounce<F extends (...args: any[]) => void>(callback: F, delay: number): WithDispose<F> {
return this.createWrapper((args, isActive) => {
if (!isActive()) return;
return setTimeout(callback, delay, ...args); // eslint-disable-line consistent-return
});
}
}
export class Context extends cordis.Context {
domain?: DomainDoc;
constructor(config: {} = {}) {
super(config);
constructor() {
super();
this.plugin(ApiMixin);
this.plugin(TimerService);
}
}
-3
View File
@@ -114,8 +114,6 @@ export async function load(ctx: Context) {
await require('../settings').loadConfig();
await require('../model/system').runConfig();
await require('../service/storage').loadStorageService();
await ctx.root.start();
await ctx.lifecycle.flush();
require('../lib/index');
await Promise.all([
lib(pending, fail, ctx),
@@ -132,6 +130,5 @@ export async function load(ctx: Context) {
ctx.loader.reloadPlugin(ctx, path.resolve(scriptDir, h), {}, `hydrooj/script/${h.split('.')[0]}`);
}
await script(pending, fail, ctx);
await ctx.lifecycle.flush();
await cli();
}
-8
View File
@@ -42,15 +42,11 @@ export async function apply(ctx: Context) {
await storage.loadStorageService();
// Make sure everything is ready and then start main entry
if (argv.options.watch) ctx.plugin(require('../service/watcher').default, {});
await ctx.root.start();
await ctx.lifecycle.flush();
await require('../service/worker').apply(ctx);
await require('../service/server').apply(ctx);
await require('../service/api').apply(ctx);
await ctx.lifecycle.flush();
require('../lib/index');
await lib(pending, fail, ctx);
await ctx.lifecycle.flush();
await setting(pending, fail, require('../model/setting'));
ctx.plugin(require('../service/monitor'));
@@ -58,7 +54,6 @@ export async function apply(ctx: Context) {
await service(pending, fail, ctx);
await builtinModel(ctx);
await model(pending, fail, ctx);
await ctx.lifecycle.flush();
const handlerDir = path.resolve(__dirname, '..', 'handler');
const handlers = await fs.readdir(handlerDir);
@@ -68,14 +63,11 @@ export async function apply(ctx: Context) {
ctx.plugin(require('../service/migration').default);
await handler(pending, fail, ctx);
await addon(pending, fail, ctx);
await ctx.lifecycle.flush();
const scriptDir = path.resolve(__dirname, '..', 'script');
for (const h of await fs.readdir(scriptDir)) {
ctx.loader.reloadPlugin(ctx, path.resolve(scriptDir, h), {}, `hydrooj/script/${h.split('.')[0]}`);
}
await ctx.lifecycle.flush();
await script(pending, fail, ctx);
await ctx.lifecycle.flush();
await ctx.parallel('app/started');
if (process.env.NODE_APP_INSTANCE === '0') {
await new Promise((resolve, reject) => {
+1 -1
View File
@@ -675,7 +675,7 @@ export class ContestScoreboardHandler extends ContestDetailBaseHandler {
class ScoreboardService extends Service {
views: Record<string, ScoreboardView<any>> = {};
constructor(ctx: Context) {
super(ctx, 'scoreboard', true);
super(ctx, 'scoreboard');
ctx.set('scoreboard', this);
}
+4 -6
View File
@@ -898,9 +898,8 @@ export class ProblemSolutionHandler extends ProblemDetailHandler {
@param('content', Types.Content)
async postEditReply(domainId: string, psid: ObjectId, psrid: ObjectId, content: string) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.parentId !== this.pdoc.docId) throw new SolutionNotFoundError(domainId, psid);
if (!(this.user.own(psrdoc)
&& this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF))) {
if (!psdoc || psdoc.parentId !== this.pdoc.docId) throw new SolutionNotFoundError(domainId, psid);
if (!this.user.own(psrdoc) || !this.user.hasPerm(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF)) {
throw new PermissionError(PERM.PERM_EDIT_PROBLEM_SOLUTION_REPLY_SELF);
}
await solution.editReply(domainId, psid, psrid, content);
@@ -911,9 +910,8 @@ export class ProblemSolutionHandler extends ProblemDetailHandler {
@param('psrid', Types.ObjectId)
async postDeleteReply(domainId: string, psid: ObjectId, psrid: ObjectId) {
const [psdoc, psrdoc] = await solution.getReply(domainId, psid, psrid);
if ((!psdoc) || psdoc.parentId !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!(this.user.own(psrdoc)
&& this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF))) {
if (!psdoc || psdoc.parentId !== this.pdoc.docId) throw new SolutionNotFoundError(psid);
if (!this.user.own(psrdoc) || !this.user.hasPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY_SELF)) {
this.checkPerm(PERM.PERM_DELETE_PROBLEM_SOLUTION_REPLY);
}
await solution.delReply(domainId, psid, psrid);
+1 -1
View File
@@ -33,5 +33,5 @@ if (!global.Hydro) {
}
global.app = new (require('./context').Context)();
process.on('exit', () => {
app.stop();
});
+46 -49
View File
@@ -14,7 +14,7 @@ import './ui';
import './lib/i18n';
import { Logger } from './logger';
import { Context } from './context';
import { Context, Service, ScopeStatus } from './context';
// eslint-disable-next-line import/no-duplicates
import { sleep, unwrapExports } from './utils';
import { PRIV } from './model/builtin';
@@ -51,71 +51,72 @@ export function resolveConfig(plugin: any, config: any) {
return config;
}
const timeout = Symbol.for('loader.timeout');
const showLoadTime = argv.options.showLoadTime;
export class Loader {
static readonly Record = Symbol.for('loader.record');
public app: Context;
export class Loader extends Service {
public state: Record<string, any> = Object.create(null);
public config: {};
public suspend = false;
public cache: Record<string, string> = Object.create(null);
// public warnings: Record<string, string> = Object.create(null);
unloadPlugin(ctx: Context, key: string) {
const fork = ctx.state[Loader.Record][key];
constructor(private app: Context) {
super(app, 'loader');
}
[Service.setup]() {
this.app.on('app/started', () => {
this.ctx.setInterval(async () => {
const pending = Object.entries(this.state).filter((v) => v[1].status === ScopeStatus.PENDING);
if (pending.length) {
logger.warn('Plugins are still pending: %s', pending.map((v) => v[0]).join(', '));
for (const [key, value] of pending) {
logger.warn('Plugin %s is still pending', key);
console.log(value);
}
}
const loading = Object.entries(this.state).filter((v) => v[1].status === ScopeStatus.LOADING);
if (loading.length) {
logger.warn('Plugins are still loading: %s', loading.map((v) => v[0]).join(', '));
for (const [key, value] of loading) {
logger.warn('Plugin %s is still loading', key);
console.log(value);
}
}
const failed = Object.entries(this.state).filter((v) => v[1].status === ScopeStatus.FAILED);
if (failed.length) {
logger.warn('Plugins failed to load: %s', failed.map((v) => v[0]).join(', '));
for (const [key, value] of failed) {
logger.warn('Plugin %s failed to load', key);
console.log(value);
}
}
}, 10000);
});
}
unloadPlugin(key: string) {
const fork = this.state[key];
if (fork) {
fork.dispose();
delete ctx.state[Loader.Record][key];
delete this.state[key];
logger.info('unload plugin %c', key);
}
}
async reloadPlugin(parent: Context, key: string, config: any, asName = '') {
let fork = parent.state[Loader.Record]?.[key];
let fork = this.state[key];
if (fork) {
logger.info('reload plugin %c', key.split('node_modules').pop());
fork.update(config);
} else {
logger.info('apply plugin %c', key.split('node_modules').pop());
let plugin = await this.resolvePlugin(key);
const plugin = await this.resolvePlugin(key);
if (!plugin) return;
resolveConfig(plugin, config);
if (asName) plugin.name = asName;
// fork = parent.plugin(plugin, this.interpolate(config));
if (plugin.apply) {
const original = plugin.apply;
const apply = (...args) => {
const start = Date.now();
const result = Promise.resolve()
.then(() => original(...args))
.catch((e) => logger.error(e));
Promise.race([
result,
new Promise((resolve) => {
setTimeout(() => resolve(timeout), 10000);
}),
]).then((t) => {
if (t === timeout) {
logger.warn('Plugin %s took too long to load', key);
}
});
if (showLoadTime && (typeof showLoadTime !== 'number' || Date.now() - start > showLoadTime)) {
logger.info('Plugin %s loaded in %dms', key, Date.now() - start);
}
return result;
};
plugin = Object.create(plugin);
Object.defineProperty(plugin, 'apply', {
writable: true,
value: apply,
enumerable: true,
});
}
fork = parent.plugin(plugin, config);
if (!fork) return;
parent.state[Loader.Record] ||= Object.create(null);
parent.state[Loader.Record][key] = fork;
this.state[key] = fork;
}
return fork;
}
@@ -135,11 +136,7 @@ export class Loader {
}
}
const loader = new Loader();
app.provide('loader');
app.loader = loader;
loader.app = app;
app.state[Loader.Record] = Object.create(null);
app.plugin(Loader);
function preload() {
for (const a of [path.resolve(__dirname, '..'), ...getAddons()]) {
+1 -1
View File
@@ -315,7 +315,7 @@ export class SettingService extends Service {
DomainUserSetting = T(DomainUserSetting);
SystemSetting = T(SystemSetting);
constructor(ctx: Context) {
super(ctx, 'setting', true);
super(ctx, 'setting');
}
get(key: string) {
+74 -50
View File
@@ -2,13 +2,52 @@
/* eslint-disable consistent-return */
/* eslint-disable import/no-dynamic-require */
/* eslint-disable @typescript-eslint/no-shadow */
import { readFileSync } from 'fs';
import { relative, resolve, sep } from 'path';
import { codeFrameColumns } from '@babel/code-frame';
import { FSWatcher, watch } from 'chokidar';
import { debounce } from 'lodash';
import { Context, MainScope, Service } from '../context';
import { BuildFailure } from 'esbuild';
import { Context, Plugin, Service } from '../context';
import { Logger } from '../logger';
import { unwrapExports } from '../utils';
declare module '../context' {
interface Events {
'hmr/reload': (reloads: Map<Plugin, { filename: string, runtime: Plugin.Runtime<Context> }>) => void;
}
}
function isBuildFailure(e: any): e is BuildFailure {
return Array.isArray(e?.errors) && e.errors.every((error: any) => error.text);
}
export function handleError(ctx: Context, e: any) {
if (!isBuildFailure(e)) {
ctx.logger.warn(e);
return;
}
for (const error of e.errors) {
if (!error.location) {
ctx.logger.warn(error.text);
continue;
}
try {
const { file, line, column } = error.location;
const source = readFileSync(file, 'utf8');
const formatted = codeFrameColumns(source, {
start: { line, column },
}, {
highlightCode: true,
message: error.text,
});
ctx.logger.warn(`File: ${file}:${line}:${column}\n${formatted}`);
} catch (e) {
ctx.logger.warn(e);
}
}
}
function loadDependencies(filename: string, ignored: Set<string>) {
const dependencies = new Set<string>();
function traverse({ filename, children }: NodeJS.Module) {
@@ -22,14 +61,6 @@ function loadDependencies(filename: string, ignored: Set<string>) {
const logger = new Logger('watch');
function coerce(val: any) {
// resolve error when stack is undefined, e.g. axios error with status code 401
const { message, stack } = val instanceof Error && val.stack ? val : new Error(val as any);
const lines = stack.split('\n');
const index = lines.findIndex((line) => line.endsWith(message));
return lines.slice(index).join('\n');
}
export default class Watcher extends Service {
private root: string;
private watcher: FSWatcher;
@@ -39,7 +70,7 @@ export default class Watcher extends Service {
private stashed = new Set<string>();
constructor(public ctx: Context) {
super(ctx, 'watcher', true);
super(ctx, 'watcher');
this.externals = new Set(Object.keys(require.cache));
}
@@ -48,7 +79,6 @@ export default class Watcher extends Service {
const roots = [this.root];
if (process.env.WATCH_ROOT) roots.push(process.env.WATCH_ROOT);
this.watcher = watch(roots, {
...this.config,
ignored: (file) => [
'node_modules', '.git', 'logs', '.cache', '.yarn', 'tsconfig.tsbuildinfo',
].some((rule) => file.startsWith(`${rule}/`) || file.endsWith(`/${rule}`) || file.includes(`/${rule}/`)),
@@ -56,7 +86,7 @@ export default class Watcher extends Service {
logger.info(`Start watching changes in ${this.root}`);
// files independent from any plugins will trigger a full reload
const triggerLocalReload = debounce(() => this.triggerLocalReload(), 1000);
const triggerLocalReload = this.ctx.debounce(() => this.triggerLocalReload(), 1000);
this.watcher.on('change', (path) => {
if (path.includes(`${sep}.`)) return;
@@ -140,14 +170,13 @@ export default class Watcher extends Service {
}
private triggerLocalReload() {
const start = Date.now();
this.analyzeChanges();
/** plugins pending classification */
const pending = new Map<string, MainScope>();
const pending = new Map<string, Plugin.Runtime<Context>>();
/** plugins that should be reloaded */
const reloads = new Map<MainScope, string>();
const reloads = new Map<Plugin, { filename: string, runtime: Plugin.Runtime<Context> }>();
// we assume that plugin entry files are "atomic"
// that is, reloading them will not cause any other reloads
@@ -173,25 +202,13 @@ export default class Watcher extends Service {
for (const dep of dependencies) this.accepted.add(dep);
// prepare for reload
let isMarked = false;
const visited = new Set<MainScope>();
const queued = [runtime];
while (queued.length) {
const runtime = queued.shift();
if (visited.has(runtime)) continue;
visited.add(runtime);
if (reloads.has(runtime)) {
isMarked = true;
break;
}
for (const state of runtime.children) {
queued.push(state.runtime);
}
}
if (!isMarked) reloads.set(runtime, filename);
reloads.set(runtime.plugin, {
filename,
runtime,
});
}
const backup: Record<string, NodeJS.Module> = {};
const backup: Record<string, NodeJS.Module> = Object.create(null);
for (const filename of this.accepted) {
backup[filename] = require.cache[filename];
delete require.cache[filename];
@@ -204,54 +221,61 @@ export default class Watcher extends Service {
}
logger.debug('Will reload the following file(s): %o', this.accepted.keys());
const attempts = {};
const attempts: Record<string, any> = {};
try {
for (const [, filename] of reloads) {
for (const [, { filename }] of reloads) {
attempts[filename] = unwrapExports(require(filename));
}
} catch (err) {
logger.warn(err);
} catch (e) {
handleError(this.ctx, e);
return rollback();
}
const reload = (plugin: any, runtime?: Plugin.Runtime) => {
if (!runtime) return;
for (const oldFiber of runtime.scopes) {
oldFiber.parent.plugin(plugin, oldFiber.config);
}
};
try {
for (const [runtime, filename] of reloads) {
for (const [plugin, { filename, runtime }] of reloads) {
const path = relative(this.root, filename);
const states = runtime.children.slice();
try {
this.ctx.registry.delete(runtime.plugin);
this.ctx.registry.delete(plugin);
} catch (err) {
logger.warn(`failed to dispose plugin at %c\n${coerce(err)}`, path);
logger.warn('failed to dispose plugin at %c', path);
logger.warn(err);
}
try {
const plugin = attempts[filename];
for (const state of states) {
state.parent.plugin(plugin, state.config);
}
reload(attempts[filename], runtime);
logger.info('reload plugin at %c', path);
} catch (err) {
logger.warn(`failed to reload plugin at %c\n${coerce(err)}`, path);
logger.warn('failed to reload plugin at %c', path);
logger.warn(err);
throw err;
}
}
} catch {
// rollback require.cache and plugin states
rollback();
for (const [runtime, filename] of reloads) {
for (const [plugin, { filename, runtime }] of reloads) {
try {
this.ctx.registry.delete(attempts[filename]);
runtime.parent.plugin(runtime.plugin, runtime.config);
reload(plugin, runtime);
} catch (err) {
logger.warn(err);
}
}
logger.warn('Rolling back changes');
return;
}
// emit reload event on success
this.ctx.emit('hmr/reload', reloads);
// reset stashed files
this.stashed = new Set();
logger.success('Reload done in %d ms', Date.now() - start);
}
}
+1 -1
View File
@@ -17,7 +17,7 @@ export class WorkerService extends Service {
coll = db.collection('schedule');
constructor(ctx: Context) {
super(ctx, 'worker', true);
super(ctx, 'worker');
this.consume();
}
@@ -46,8 +46,8 @@ const dialogAction = (id) => [
];
function bindCopyLink(id, src: string) {
const url = !(window.location.href.endsWith('file') || window.location.href.endsWith('files'))
|| window.location.href.match('homework/.*/file') || window.location.href.match('training/.*/file')
const url = !['file', 'files'].some((i) => window.location.href.endsWith(i))
|| ['homework', 'training'].some((i) => window.location.href.match(`${i}/.*/file`))
? `file://${src.substring(src.lastIndexOf('/') + 1)}` : src;
const clip = new Clipboard(`#copy-${id}`, { text: () => `${url}` });
clip.on('success', () => Notification.success(i18n(`${url.startsWith('file://') ? 'Reference' : 'Download'} link copied to clipboard!`)));
+11 -11
View File
@@ -14,8 +14,8 @@
"cli": false
},
"devDependencies": {
"@blueprintjs/core": "^5.16.4",
"@cordisjs/core": "3.18.1",
"@blueprintjs/core": "^5.16.6",
"@cordisjs/core": "4.0.0-alpha.0",
"@fontsource/dm-mono": "^5.1.1",
"@fontsource/fira-code": "^5.1.1",
"@fontsource/inconsolata": "^5.1.1",
@@ -25,7 +25,7 @@
"@fontsource/source-code-pro": "^5.1.1",
"@fontsource/ubuntu-mono": "^5.1.1",
"@hydrooj/utils": "workspace:^",
"@sentry/browser": "^8.54.0",
"@sentry/browser": "^9.1.0",
"@sentry/webpack-plugin": "^3.1.2",
"@simplewebauthn/browser": "13.1.0",
"@svgr/webpack": "^8.1.0",
@@ -45,7 +45,7 @@
"@types/webpack-env": "^1.18.8",
"@vscode/codicons": "^0.0.36",
"ajv": "^8.17.1",
"allotment": "^1.20.2",
"allotment": "^1.20.3",
"autoprefixer": "^10.4.20",
"browser-update": "^3.3.55",
"chalk": "^5.4.1",
@@ -61,7 +61,7 @@
"echarts": "^5.6.0",
"emojis-keywords": "2.0.0",
"emojis-list": "2.1.0",
"esbuild-loader": "^4.2.2",
"esbuild-loader": "^4.3.0",
"flatpickr": "^4.6.13",
"graphiql": "^3.8.3",
"idb": "^8.0.2",
@@ -70,7 +70,7 @@
"jquery.easing": "^1.4.1",
"jquery.transit": "^0.9.12",
"matchmedia-polyfill": "^0.3.2",
"md-editor-rt": "^5.2.1",
"md-editor-rt": "^5.2.3",
"mini-css-extract-plugin": "^2.9.2",
"moment": "^2.30.1",
"monaco-editor": "0.52.2",
@@ -78,7 +78,7 @@
"monaco-editor-webpack-plugin": "^7.1.0",
"monaco-themes": "^0.4.4",
"monaco-yaml": "5.1.1",
"nanoid": "^5.0.9",
"nanoid": "^5.1.0",
"normalize.css": "^8.0.1",
"nprogress": "^0.2.0",
"p-queue": "^8.1.0",
@@ -102,21 +102,21 @@
"redux-promise-middleware": "^6.2.0",
"redux-thunk": "^3.1.0",
"rupture": "^0.7.1",
"schemastery": "^3.15.0",
"schemastery": "^3.16.0",
"shorty.js": "^1.0.1",
"slideout": "^1.0.1",
"sticky-kit": "^1.1.3",
"style-loader": "^4.0.0",
"stylus": "^0.64.0",
"stylus-loader": "7.1.2",
"svgtofont": "^6.2.0",
"svgtofont": "^6.3.0",
"tether": "1.4.7",
"tether-drop": "^1.4.2",
"timeago-react": "^3.0.6",
"timeago.js": "^4.0.2",
"ts-loader": "^9.5.2",
"web-streams-polyfill": "^4.1.0",
"webpack": "^5.97.1",
"webpack": "^5.98.0",
"webpack-bundle-analyzer": "^4.10.2",
"webpack-dev-server": "^5.2.0",
"webpack-manifest-plugin": "^5.0.0",
@@ -126,7 +126,7 @@
"@hydrooj/common": "workspace:^",
"ansi_up": "^6.0.2",
"cssfilter": "^0.0.11",
"esbuild": "0.24.0",
"esbuild": "0.25.0",
"fs-extra": "^11.3.0",
"html-to-text": "^9.0.5",
"js-yaml": "^4.1.0",
+1 -1
View File
@@ -31,7 +31,7 @@ if (typeof window['WeakRef'] === 'undefined') {
return WeakRef;
}(new WeakMap()));
}
if (!(window.matchMedia('all').addListener || window.matchMedia('all').addEventListener)) {
if (!window.matchMedia('all').addListener && !window.matchMedia('all').addEventListener) {
const localMatchMedia = window.matchMedia;
const hasMediaQueries = localMatchMedia('only all').matches;
let isListening = false;
+27 -10
View File
@@ -19,6 +19,7 @@ class AccountService {
problemLists: Set<string>;
syncing = false;
listUpdated = false;
stopped = false;
working = false;
error = '';
@@ -114,6 +115,7 @@ class AccountService {
await sleep(5000);
}
page++;
if (this.stopped) return;
pids = await this.api.listProblem(page, resync, list);
}
}
@@ -153,13 +155,20 @@ class AccountService {
this.syncing = false;
}
async stop() { } // eslint-disable-line @typescript-eslint/no-empty-function
async main() {
const res = await this.login();
if (!res) return;
setInterval(() => this.login(), Time.hour);
TaskModel.consume({ type: 'remotejudge', subType: this.account.type.split('.')[0] }, this.judge.bind(this), false);
const interval = setInterval(() => this.login(), Time.hour);
const consumer = TaskModel.consume({ type: 'remotejudge', subType: this.account.type.split('.')[0] }, this.judge.bind(this), false);
this.working = true;
this.handleSync();
this.stop = async () => {
clearInterval(interval);
consumer.destroy();
this.stopped = true;
};
}
}
@@ -171,13 +180,13 @@ declare module 'hydrooj' {
class VJudgeService extends Service {
constructor(ctx: Context) {
super(ctx, 'vjudge', false);
super(ctx, 'vjudge');
}
accounts: RemoteAccount[];
private providers: Record<string, any> = {};
private pool: Record<string, AccountService> = {};
async start() {
async [Service.setup]() {
this.accounts = await coll.find().toArray();
this.ctx.setInterval(this.sync.bind(this), Time.week);
}
@@ -185,11 +194,20 @@ class VJudgeService extends Service {
addProvider(type: string, provider: BasicProvider, override = false) {
if (process.env.VJUDGE_DEBUG && !(`,${process.env.VJUDGE_DEBUG},`).includes(`,${type},`)) return;
if (!override && this.providers[type]) throw new Error(`duplicate provider ${type}`);
this.providers[type] = provider;
for (const account of this.accounts.filter((a) => a.type === type)) {
if (account.enableOn && !account.enableOn.includes(os.hostname())) continue;
this.pool[`${account.type}/${account.handle}`] = new AccountService(provider, account);
}
this.ctx.effect(() => {
this.providers[type] = provider;
const services = [];
for (const account of this.accounts.filter((a) => a.type === type)) {
if (account.enableOn && !account.enableOn.includes(os.hostname())) continue;
const service = new AccountService(provider, account);
services.push(service);
this.pool[`${account.type}/${account.handle}`] = service;
}
return () => {
for (const service of services) service.stop();
delete this.providers[type];
};
});
// FIXME: potential race condition
if (provider.Langs) this.updateLangs(type, provider.Langs);
// TODO dispose session
@@ -267,7 +285,6 @@ export async function apply(ctx: Context) {
]);
});
ctx.inject(['vjudge'], async (c) => {
await c.vjudge.start();
for (const [k, v] of Object.entries(providers)) {
if (!SystemModel.get(`vjudge.builtin-${k}-disable`)) c.vjudge.addProvider(k, v);
}