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