mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-17 09:36:08 +08:00
use cordis@4
This commit is contained in:
18 files changed
+284
-168
No files matched your search
@@ -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",
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"node": ">=16"
|
||||
},
|
||||
"dependencies": {
|
||||
"esbuild": "0.24.0",
|
||||
"esbuild": "0.25.0",
|
||||
"source-map-support": "^0.5.21"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
+6
-5
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -33,5 +33,5 @@ if (!global.Hydro) {
|
||||
}
|
||||
global.app = new (require('./context').Context)();
|
||||
process.on('exit', () => {
|
||||
app.stop();
|
||||
|
||||
});
|
||||
@@ -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()]) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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!`)));
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user