mirror of
https://gitcode.com/gh_mirrors/hy/Hydro.git
synced 2026-10-20 01:38:19 +08:00
upgrade to cordis v3 (#733)
Co-authored-by: panda <panda_dtdyy@outlook.com>
This commit is contained in:
1 parent
498cd57016
commit
a56b1650be
33 files changed
+281
-242
No files matched your search
@@ -108,7 +108,7 @@ Telegram [@undefinedmoe](https://t.me/undefinedmoe)
|
|||||||
<details>
|
<details>
|
||||||
<summary><h2>更新日志(点击展开)</h2></summary>
|
<summary><h2>更新日志(点击展开)</h2></summary>
|
||||||
|
|
||||||
### Hydro 4.10.7 / 4.49.5
|
### Hydro 4.10.7 / UI 4.49.5
|
||||||
- core: 优化比赛成绩版按照小组筛选
|
- core: 优化比赛成绩版按照小组筛选
|
||||||
- core: inject -> injectUI
|
- core: inject -> injectUI
|
||||||
- core: 修复一处内存泄漏
|
- core: 修复一处内存泄漏
|
||||||
|
|||||||
+4
-5
@@ -34,18 +34,17 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@simplewebauthn/typescript-types": "7.4.0",
|
"@simplewebauthn/typescript-types": "7.4.0",
|
||||||
"@types/autocannon": "^7.12.5",
|
"@types/autocannon": "^7.12.5",
|
||||||
"@types/chai": "^4.3.11",
|
"@types/chai": "^4.3.12",
|
||||||
"@types/cross-spawn": "^6.0.6",
|
"@types/cross-spawn": "^6.0.6",
|
||||||
"@types/node": "20.10.6",
|
"@types/node": "20.10.6",
|
||||||
"@types/semver": "^7.5.7",
|
"@types/semver": "^7.5.8",
|
||||||
"autocannon": "^7.15.0",
|
"autocannon": "^7.15.0",
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"chai": "^5.1.0",
|
"chai": "^5.1.0",
|
||||||
"chokidar": "^3.6.0",
|
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
"cross-spawn": "^7.0.3",
|
"cross-spawn": "^7.0.3",
|
||||||
"esbuild": "0.18.20",
|
"esbuild": "0.18.20",
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.57.0",
|
||||||
"eslint-import-resolver-typescript": "^3.6.1",
|
"eslint-import-resolver-typescript": "^3.6.1",
|
||||||
"eslint-import-resolver-webpack": "^0.13.8",
|
"eslint-import-resolver-webpack": "^0.13.8",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
"mongodb-memory-server": "^9.1.6",
|
"mongodb-memory-server": "^9.1.6",
|
||||||
"nyc": "^15.1.0",
|
"nyc": "^15.1.0",
|
||||||
"ora": "^8.0.1",
|
"ora": "^8.0.1",
|
||||||
"package-json": "^9.0.0",
|
"package-json": "^10.0.0",
|
||||||
"semver": "^7.6.0",
|
"semver": "^7.6.0",
|
||||||
"simple-git": "^3.22.0",
|
"simple-git": "^3.22.0",
|
||||||
"supertest": "^6.3.4",
|
"supertest": "^6.3.4",
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ export async function apply(ctx: Context) {
|
|||||||
ctx.Route('blog_create', '/blog/:uid/create', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
|
ctx.Route('blog_create', '/blog/:uid/create', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
|
||||||
ctx.Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler);
|
ctx.Route('blog_detail', '/blog/:uid/:did', BlogDetailHandler);
|
||||||
ctx.Route('blog_edit', '/blog/:uid/:did/edit', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
|
ctx.Route('blog_edit', '/blog/:uid/:did/edit', BlogEditHandler, PRIV.PRIV_USER_PROFILE);
|
||||||
ctx.inject('UserDropdown', 'blog_main', (h) => ({ icon: 'book', displayName: 'Blog', uid: h.user._id.toString() }),
|
ctx.injectUI('UserDropdown', 'blog_main', (h) => ({ icon: 'book', displayName: 'Blog', uid: h.user._id.toString() }),
|
||||||
PRIV.PRIV_USER_PROFILE);
|
PRIV.PRIV_USER_PROFILE);
|
||||||
ctx.i18n.load('zh', {
|
ctx.i18n.load('zh', {
|
||||||
"{0}'s blog": '{0} 的博客',
|
"{0}'s blog": '{0} 的博客',
|
||||||
|
|||||||
@@ -6,6 +6,6 @@
|
|||||||
"author": "undefined <i@undefined.moe>",
|
"author": "undefined <i@undefined.moe>",
|
||||||
"license": "AGPL-3.0-or-later",
|
"license": "AGPL-3.0-or-later",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@elastic/elasticsearch": "^8.12.1"
|
"@elastic/elasticsearch": "^8.12.2"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4,15 +4,15 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "react.yaml",
|
"main": "react.yaml",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@stylistic/eslint-plugin": "^1.6.2",
|
"@stylistic/eslint-plugin": "^1.6.3",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.2",
|
"@typescript-eslint/eslint-plugin": "^7.1.1",
|
||||||
"@typescript-eslint/parser": "^7.0.2",
|
"@typescript-eslint/parser": "^7.1.1",
|
||||||
"eslint-config-airbnb": "^19.0.4",
|
"eslint-config-airbnb": "^19.0.4",
|
||||||
"eslint-config-airbnb-typescript": "^17.1.0",
|
"eslint-config-airbnb-typescript": "^18.0.0",
|
||||||
"eslint-plugin-github": "^4.10.1",
|
"eslint-plugin-github": "^4.10.2",
|
||||||
"eslint-plugin-import": "2.29.0",
|
"eslint-plugin-import": "2.29.0",
|
||||||
"eslint-plugin-jsx-a11y": "^6.8.0",
|
"eslint-plugin-jsx-a11y": "^6.8.0",
|
||||||
"eslint-plugin-react": "^7.33.2",
|
"eslint-plugin-react": "^7.34.0",
|
||||||
"eslint-plugin-react-hooks": "^4.6.0",
|
"eslint-plugin-react-hooks": "^4.6.0",
|
||||||
"eslint-plugin-simple-import-sort": "^12.0.0"
|
"eslint-plugin-simple-import-sort": "^12.0.0"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -142,7 +142,7 @@ class FpsProblemImportHandler extends Handler {
|
|||||||
|
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
ctx.Route('problem_import_fps', '/problem/import/fps', FpsProblemImportHandler, PERM.PERM_CREATE_PROBLEM);
|
ctx.Route('problem_import_fps', '/problem/import/fps', FpsProblemImportHandler, PERM.PERM_CREATE_PROBLEM);
|
||||||
ctx.inject('ProblemAdd', 'problem_import_fps', { icon: 'copy', text: 'From FPS File' });
|
ctx.injectUI('ProblemAdd', 'problem_import_fps', { icon: 'copy', text: 'From FPS File' });
|
||||||
ctx.i18n.load('zh', {
|
ctx.i18n.load('zh', {
|
||||||
'From FPS File': '从 FPS 文件导入',
|
'From FPS File': '从 FPS 文件导入',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -13,7 +13,11 @@ export interface Result {
|
|||||||
display: string
|
display: string
|
||||||
}
|
}
|
||||||
|
|
||||||
class GeoIPService extends Service {
|
export default class GeoIPService extends Service {
|
||||||
|
constructor(ctx: Context) {
|
||||||
|
super(ctx, 'geoip', true);
|
||||||
|
}
|
||||||
|
|
||||||
provider = '<a href="http://www.maxmind.com" target="_blank">MaxMind</a>';
|
provider = '<a href="http://www.maxmind.com" target="_blank">MaxMind</a>';
|
||||||
lookup(ip: string, locale: string): Result {
|
lookup(ip: string, locale: string): Result {
|
||||||
const res: any = reader.get(ip);
|
const res: any = reader.get(ip);
|
||||||
@@ -30,7 +34,8 @@ class GeoIPService extends Service {
|
|||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
export function apply(ctx: Context) {
|
|
||||||
Context.service('geoip', GeoIPService);
|
export async function apply(ctx: Context) {
|
||||||
ctx.geoip = new GeoIPService(ctx, 'geoip', true);
|
ctx.provide('geoip');
|
||||||
|
ctx.geoip = new GeoIPService(ctx);
|
||||||
}
|
}
|
||||||
@@ -79,7 +79,9 @@ const session = {
|
|||||||
|
|
||||||
export async function postInit(ctx) {
|
export async function postInit(ctx) {
|
||||||
if (SystemModel.get('hydrojudge.disable')) return;
|
if (SystemModel.get('hydrojudge.disable')) return;
|
||||||
ctx.check.addChecker('Judge', (_ctx, log, warn, error) => versionCheck(warn, error));
|
ctx.inject(['check'], (c) => {
|
||||||
|
c.check.addChecker('Judge', (_ctx, log, warn, error) => versionCheck(warn, error));
|
||||||
|
});
|
||||||
await fs.ensureDir(getConfig('tmp_dir'));
|
await fs.ensureDir(getConfig('tmp_dir'));
|
||||||
const handle = async (t) => {
|
const handle = async (t) => {
|
||||||
const rdoc = await RecordModel.get(t.domainId, t.rid);
|
const rdoc = await RecordModel.get(t.domainId, t.rid);
|
||||||
|
|||||||
@@ -12,23 +12,23 @@
|
|||||||
},
|
},
|
||||||
"preferUnplugged": true,
|
"preferUnplugged": true,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@aws-sdk/client-s3": "3.515.0",
|
"@aws-sdk/client-s3": "3.525.0",
|
||||||
"@aws-sdk/lib-storage": "3.515.0",
|
"@aws-sdk/lib-storage": "3.525.1",
|
||||||
"@aws-sdk/s3-presigned-post": "3.515.0",
|
"@aws-sdk/s3-presigned-post": "3.525.0",
|
||||||
"@aws-sdk/s3-request-presigner": "3.515.0",
|
"@aws-sdk/s3-request-presigner": "3.525.0",
|
||||||
"@graphql-tools/schema": "^10.0.2",
|
"@graphql-tools/schema": "^10.0.3",
|
||||||
"@hydrooj/utils": "workspace:*",
|
"@hydrooj/utils": "workspace:*",
|
||||||
"@simplewebauthn/server": "7.4.0",
|
"@simplewebauthn/server": "7.4.0",
|
||||||
"@undefined-moe/require-resolve-hook": "^1.1.0",
|
"@undefined-moe/require-resolve-hook": "^1.1.0",
|
||||||
"adm-zip": "0.5.5",
|
"adm-zip": "0.5.5",
|
||||||
"cac": "^6.7.14",
|
"cac": "^6.7.14",
|
||||||
"cordis": "2.8.8",
|
"cordis": "3.13.3",
|
||||||
"detect-browser": "^5.3.0",
|
"detect-browser": "^5.3.0",
|
||||||
"emoji-regex": "^10.3.0",
|
"emoji-regex": "^10.3.0",
|
||||||
"emojis-list": "2.1.0",
|
"emojis-list": "2.1.0",
|
||||||
"fs-extra": "^11.2.0",
|
"fs-extra": "^11.2.0",
|
||||||
"graphql": "^16.8.1",
|
"graphql": "^16.8.1",
|
||||||
"graphql-scalars": "^1.22.4",
|
"graphql-scalars": "^1.22.5",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"koa": "^2.15.0",
|
"koa": "^2.15.0",
|
||||||
"koa-body": "^6.0.1",
|
"koa-body": "^6.0.1",
|
||||||
@@ -42,7 +42,7 @@
|
|||||||
"moment-timezone": "^0.5.45",
|
"moment-timezone": "^0.5.45",
|
||||||
"mongodb": "^5.9.2",
|
"mongodb": "^5.9.2",
|
||||||
"nanoid": "^5.0.6",
|
"nanoid": "^5.0.6",
|
||||||
"nodemailer": "^6.9.10",
|
"nodemailer": "^6.9.11",
|
||||||
"notp": "^2.0.3",
|
"notp": "^2.0.3",
|
||||||
"p-queue": "^8.0.1",
|
"p-queue": "^8.0.1",
|
||||||
"path-to-regexp": "^6.2.1",
|
"path-to-regexp": "^6.2.1",
|
||||||
@@ -69,9 +69,10 @@
|
|||||||
"@types/nodemailer": "^6.4.14",
|
"@types/nodemailer": "^6.4.14",
|
||||||
"@types/notp": "^2.0.5",
|
"@types/notp": "^2.0.5",
|
||||||
"@types/saslprep": "^1.0.3",
|
"@types/saslprep": "^1.0.3",
|
||||||
"@types/semver": "^7.5.7",
|
"@types/semver": "^7.5.8",
|
||||||
"@types/superagent": "^8.1.3",
|
"@types/superagent": "^8.1.4",
|
||||||
"@types/tar": "^6.1.11",
|
"@types/tar": "^6.1.11",
|
||||||
|
"chokidar": "^3.6.0",
|
||||||
"moment": "^2.30.1"
|
"moment": "^2.30.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -6,8 +6,7 @@ import { Loader } from './loader';
|
|||||||
import { EventMap } from './service/bus';
|
import { EventMap } from './service/bus';
|
||||||
import type { CheckService } from './service/check';
|
import type { CheckService } from './service/check';
|
||||||
|
|
||||||
export interface Events<C extends Context = Context> extends cordis.Events<C>, EventMap {
|
export interface Events<C extends Context = Context> extends cordis.Events<C>, EventMap { }
|
||||||
}
|
|
||||||
|
|
||||||
function addScript<K>(name: string, description: string, validate: Schema<K>, run: (args: K, report: any) => boolean | Promise<boolean>) {
|
function addScript<K>(name: string, description: string, validate: Schema<K>, run: (args: K, report: any) => boolean | Promise<boolean>) {
|
||||||
if (global.Hydro.script[name]) throw new Error(`duplicate script ${name} registered.`);
|
if (global.Hydro.script[name]) throw new Error(`duplicate script ${name} registered.`);
|
||||||
@@ -21,6 +20,12 @@ function provideModule<T extends keyof ModuleInterfaces>(type: T, id: string, mo
|
|||||||
return () => delete global.Hydro.module[type][id];
|
return () => delete global.Hydro.module[type][id];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 interface Context {
|
export interface Context {
|
||||||
[Context.events]: Events<Context>;
|
[Context.events]: Events<Context>;
|
||||||
loader: Loader;
|
loader: Loader;
|
||||||
@@ -28,55 +33,62 @@ export interface Context {
|
|||||||
Connection: typeof import('./service/server').Connection;
|
Connection: typeof import('./service/server').Connection;
|
||||||
withHandlerClass: import('./service/server').RouteService['withHandlerClass'];
|
withHandlerClass: import('./service/server').RouteService['withHandlerClass'];
|
||||||
check: CheckService;
|
check: CheckService;
|
||||||
setTimeout: typeof setTimeout;
|
|
||||||
setInterval: typeof setInterval;
|
|
||||||
setImmediate: typeof setImmediate;
|
setImmediate: typeof setImmediate;
|
||||||
addScript: typeof addScript;
|
addScript: typeof addScript;
|
||||||
provideModule: typeof provideModule;
|
provideModule: typeof provideModule;
|
||||||
/** @deprecated use injectUI instead */
|
|
||||||
inject: typeof inject;
|
|
||||||
injectUI: typeof inject;
|
injectUI: typeof inject;
|
||||||
api: ApiMixin;
|
|
||||||
broadcast: Context['emit'];
|
broadcast: Context['emit'];
|
||||||
geoip?: GeoIP;
|
geoip?: GeoIP;
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Context extends cordis.Context {
|
export abstract class Service<T = any, C extends Context = Context> extends cordis.Service<T, C> {
|
||||||
domain?: DomainDoc;
|
[cordis.Service.setup]() {
|
||||||
}
|
this.ctx = new Context() as C;
|
||||||
|
}
|
||||||
export namespace Context {
|
|
||||||
export interface Config extends cordis.Context.Config { }
|
|
||||||
}
|
|
||||||
|
|
||||||
export type MainScope = cordis.MainScope<Context>;
|
|
||||||
export type EffectScope = cordis.EffectScope<Context>;
|
|
||||||
export type ForkScope = cordis.ForkScope<Context>;
|
|
||||||
export type Plugin = cordis.Plugin<Context>;
|
|
||||||
export const Service = cordis.Service<Context>;
|
|
||||||
export namespace Plugin {
|
|
||||||
export type Function<T = any> = cordis.Plugin.Function<T, Context>;
|
|
||||||
export type Constructor<T = any> = cordis.Plugin.Constructor<T, Context>;
|
|
||||||
export type Object<S = any, T = any> = cordis.Plugin.Object<S, T, Context>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
|
const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
|
||||||
function method(this: cordis.Service, ...args: Parameters<F>) {
|
function method(this: cordis.Service, ...args: Parameters<F>) {
|
||||||
const res = origFunc(...args);
|
const res = origFunc(...args);
|
||||||
this.caller?.on('dispose', () => (disposeFunc ? disposeFunc(res) : res()));
|
this[Context.current]?.on('dispose', () => (disposeFunc ? disposeFunc(res) : res()));
|
||||||
};
|
};
|
||||||
|
|
||||||
export class ApiMixin extends Service {
|
export class ApiMixin extends Service {
|
||||||
static readonly methods = ['addScript', 'setInterval', 'setTimeout', 'setImmediate', 'provideModule', 'inject', 'injectUI', 'broadcast'];
|
|
||||||
addScript = T(addScript);
|
addScript = T(addScript);
|
||||||
setInterval = T(setInterval, clearInterval);
|
|
||||||
setTimeout = T(setTimeout, clearTimeout);
|
|
||||||
setImmediate = T(setImmediate, clearImmediate);
|
setImmediate = T(setImmediate, clearImmediate);
|
||||||
provideModule = T(provideModule);
|
provideModule = T(provideModule);
|
||||||
inject = T(inject);
|
|
||||||
injectUI = T(inject);
|
injectUI = T(inject);
|
||||||
broadcast = (event: keyof EventMap, ...payload) => this.ctx.emit('bus/broadcast', event, payload);
|
broadcast = (event: keyof EventMap, ...payload) => this.ctx.emit('bus/broadcast', event, payload);
|
||||||
constructor(ctx) {
|
constructor(ctx) {
|
||||||
super(ctx, 'api', true);
|
super(ctx, '$api', true);
|
||||||
|
ctx.mixin('$api', ['addScript', 'setImmediate', 'provideModule', 'injectUI', 'broadcast']);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Context.service('api', ApiMixin);
|
|
||||||
|
export class Context extends cordis.Context {
|
||||||
|
domain?: DomainDoc;
|
||||||
|
|
||||||
|
constructor(config: {} = {}) {
|
||||||
|
super(config);
|
||||||
|
this.plugin(ApiMixin);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use `ctx.root` instead */
|
||||||
|
get app() {
|
||||||
|
return this.root;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export namespace Context {
|
||||||
|
export type Associate<P extends string, C extends Context = Context> = cordis.Context.Associate<P, C>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const old = cordis.Registry.prototype.inject;
|
||||||
|
// cordis.Registry.prototype.using = old;
|
||||||
|
cordis.Registry.prototype.inject = function wrapper(...args) {
|
||||||
|
if (typeof args[0] === 'string') {
|
||||||
|
console.warn('old functionality of ctx.inject is deprecated. please use ctx.injectUI instead.');
|
||||||
|
return T(inject).call(this, ...args as any) as any;
|
||||||
|
}
|
||||||
|
return old.call(this, ...args);
|
||||||
|
};
|
||||||
@@ -107,6 +107,7 @@ export async function load(ctx: Context) {
|
|||||||
lib(pending, fail, ctx),
|
lib(pending, fail, ctx),
|
||||||
service(pending, fail, ctx),
|
service(pending, fail, ctx),
|
||||||
]);
|
]);
|
||||||
|
ctx.plugin(require('../service/worker'));
|
||||||
await builtinModel(ctx);
|
await builtinModel(ctx);
|
||||||
await model(pending, fail, ctx);
|
await model(pending, fail, ctx);
|
||||||
await setting(pending, fail, require('../model/setting'));
|
await setting(pending, fail, require('../model/setting'));
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ export async function apply(ctx: Context) {
|
|||||||
await modelSystem.runConfig();
|
await modelSystem.runConfig();
|
||||||
const storage = require('../service/storage');
|
const storage = require('../service/storage');
|
||||||
await storage.loadStorageService();
|
await storage.loadStorageService();
|
||||||
|
await require('../service/worker').apply(ctx);
|
||||||
await require('../service/server').apply(ctx);
|
await require('../service/server').apply(ctx);
|
||||||
// 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, {});
|
||||||
|
|||||||
@@ -57,20 +57,6 @@ registerResolver(
|
|||||||
'Get a contest by ID',
|
'Get a contest by ID',
|
||||||
);
|
);
|
||||||
|
|
||||||
ScheduleModel.Worker.addHandler('contest', async (doc) => {
|
|
||||||
const tdoc = await contest.get(doc.domainId, doc.tid);
|
|
||||||
if (!tdoc) return;
|
|
||||||
const tasks = [];
|
|
||||||
for (const op of doc.operation) {
|
|
||||||
if (op === 'unhide') {
|
|
||||||
for (const pid of tdoc.pids) {
|
|
||||||
tasks.push(problem.edit(doc.domainId, pid, { hidden: false }));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(tasks);
|
|
||||||
});
|
|
||||||
|
|
||||||
export class ContestListHandler extends Handler {
|
export class ContestListHandler extends Handler {
|
||||||
@param('rule', Types.Range(contest.RULES), true)
|
@param('rule', Types.Range(contest.RULES), true)
|
||||||
@param('group', Types.Name, true)
|
@param('group', Types.Name, true)
|
||||||
@@ -776,4 +762,17 @@ export async function apply(ctx: Context) {
|
|||||||
ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST);
|
ctx.Route('contest_file_download', '/contest/:tid/file/:filename', ContestFileDownloadHandler, PERM.PERM_VIEW_CONTEST);
|
||||||
ctx.Route('contest_user', '/contest/:tid/user', ContestUserHandler, PERM.PERM_VIEW_CONTEST);
|
ctx.Route('contest_user', '/contest/:tid/user', ContestUserHandler, PERM.PERM_VIEW_CONTEST);
|
||||||
ctx.Route('contest_balloon', '/contest/:tid/balloon', ContestBalloonHandler, PERM.PERM_VIEW_CONTEST);
|
ctx.Route('contest_balloon', '/contest/:tid/balloon', ContestBalloonHandler, PERM.PERM_VIEW_CONTEST);
|
||||||
|
ctx.worker.addHandler('contest', async (doc) => {
|
||||||
|
const tdoc = await contest.get(doc.domainId, doc.tid);
|
||||||
|
if (!tdoc) return;
|
||||||
|
const tasks = [];
|
||||||
|
for (const op of doc.operation) {
|
||||||
|
if (op === 'unhide') {
|
||||||
|
for (const pid of tdoc.pids) {
|
||||||
|
tasks.push(problem.edit(doc.domainId, pid, { hidden: false }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(tasks);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -300,6 +300,10 @@ export class JudgeConnectionHandler extends ConnectionHandler {
|
|||||||
this.query.lang = { $in: msg.lang };
|
this.query.lang = { $in: msg.lang };
|
||||||
this.consumer?.setQuery(this.query);
|
this.consumer?.setQuery(this.query);
|
||||||
}
|
}
|
||||||
|
if (msg.type instanceof Array && msg.type.every((i) => typeof i === 'string')) {
|
||||||
|
this.query.type = { $in: msg.type };
|
||||||
|
this.consumer?.setQuery(this.query);
|
||||||
|
}
|
||||||
} else if (msg.key === 'start') {
|
} else if (msg.key === 'start') {
|
||||||
clearTimeout(this.startTimeout);
|
clearTimeout(this.startTimeout);
|
||||||
this.consumer ||= task.consume(this.query, this.newTask.bind(this), true, this.concurrency);
|
this.consumer ||= task.consume(this.query, this.newTask.bind(this), true, this.concurrency);
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ class I18nService extends Service {
|
|||||||
load(lang: string, content: Record<string, string>) {
|
load(lang: string, content: Record<string, string>) {
|
||||||
translations[lang] ||= [];
|
translations[lang] ||= [];
|
||||||
translations[lang].unshift(content);
|
translations[lang].unshift(content);
|
||||||
this.caller?.on('dispose', () => {
|
this[Context.current]?.on('dispose', () => {
|
||||||
translations[lang] = translations[lang].filter((i) => i !== content);
|
translations[lang] = translations[lang].filter((i) => i !== content);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -35,7 +35,7 @@ class I18nService extends Service {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.service('i18n');
|
app.provide('i18n', undefined, true);
|
||||||
app.i18n = new I18nService(app);
|
app.i18n = new I18nService(app);
|
||||||
|
|
||||||
String.prototype.translate = function translate(...languages: string[]) {
|
String.prototype.translate = function translate(...languages: string[]) {
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ export function resolveConfig(plugin: any, config: any) {
|
|||||||
if (schema && plugin['schema'] !== false) config = schema(config);
|
if (schema && plugin['schema'] !== false) config = schema(config);
|
||||||
return config;
|
return config;
|
||||||
}
|
}
|
||||||
Context.service('loader');
|
|
||||||
|
|
||||||
const timeout = Symbol.for('loader.timeout');
|
const timeout = Symbol.for('loader.timeout');
|
||||||
const showLoadTime = argv.options.showLoadTime;
|
const showLoadTime = argv.options.showLoadTime;
|
||||||
@@ -43,7 +42,7 @@ export class Loader {
|
|||||||
static readonly Record = Symbol.for('loader.record');
|
static readonly Record = Symbol.for('loader.record');
|
||||||
|
|
||||||
public app: Context;
|
public app: Context;
|
||||||
public config: Context.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);
|
||||||
|
|
||||||
@@ -147,8 +146,8 @@ export function addScript(name: string, description: string) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.service('loader');
|
|
||||||
const loader = new Loader();
|
const loader = new Loader();
|
||||||
|
app.provide('loader');
|
||||||
app.loader = loader;
|
app.loader = loader;
|
||||||
loader.app = app;
|
loader.app = app;
|
||||||
app.state[Loader.Record] = Object.create(null);
|
app.state[Loader.Record] = Object.create(null);
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { Filter, ObjectId } from 'mongodb';
|
import { Filter, ObjectId } from 'mongodb';
|
||||||
import { sleep } from '@hydrooj/utils/lib/utils';
|
import { Context } from '../context';
|
||||||
import { Context, Service } from '../context';
|
|
||||||
import { Schedule } from '../interface';
|
import { Schedule } from '../interface';
|
||||||
import { Logger } from '../logger';
|
import { Logger } from '../logger';
|
||||||
import * as bus from '../service/bus';
|
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
|
import type { WorkerService } from '../service/worker';
|
||||||
import RecordModel from './record';
|
import RecordModel from './record';
|
||||||
|
|
||||||
const logger = new Logger('model/schedule');
|
const logger = new Logger('model/schedule');
|
||||||
@@ -27,73 +26,6 @@ async function getFirst(query: Filter<Schedule>) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
class Consumer {
|
|
||||||
consuming: boolean;
|
|
||||||
running?: any;
|
|
||||||
|
|
||||||
constructor(public filter: any, public func: Function, public destroyOnError = true) {
|
|
||||||
this.consuming = true;
|
|
||||||
this.consume();
|
|
||||||
bus.on('app/exit', this.destroy);
|
|
||||||
}
|
|
||||||
|
|
||||||
async consume() {
|
|
||||||
while (this.consuming) {
|
|
||||||
try {
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
const res = await getFirst(this.filter);
|
|
||||||
if (res) {
|
|
||||||
this.running = res;
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
await this.func(res);
|
|
||||||
this.running = null;
|
|
||||||
// eslint-disable-next-line no-await-in-loop
|
|
||||||
} else await sleep(1000);
|
|
||||||
} catch (err) {
|
|
||||||
logger.error(err);
|
|
||||||
if (this.destroyOnError) this.destroy();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async destroy() {
|
|
||||||
this.consuming = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
class WorkerService extends Service {
|
|
||||||
private handlers: Record<string, Function> = {};
|
|
||||||
public consumer = new Consumer(
|
|
||||||
{ type: 'schedule', subType: { $in: Object.keys(this.handlers) } },
|
|
||||||
async (doc) => {
|
|
||||||
try {
|
|
||||||
logger.debug('Worker task: %o', doc);
|
|
||||||
const start = Date.now();
|
|
||||||
await Promise.race([
|
|
||||||
this.handlers[doc.subType](doc),
|
|
||||||
sleep(1200000),
|
|
||||||
]);
|
|
||||||
const spent = Date.now() - start;
|
|
||||||
if (spent > 500) logger.warn('Slow worker task (%d ms): %o', spent, doc);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error('Worker task fail: ', e);
|
|
||||||
logger.error('%o', doc);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
public addHandler(type: string, handler: Function) {
|
|
||||||
this.handlers[type] = handler;
|
|
||||||
this.consumer.filter = { type: 'schedule', subType: { $in: Object.keys(this.handlers) } };
|
|
||||||
this.caller?.on('dispose', () => {
|
|
||||||
delete this.handlers[type];
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const Worker = new WorkerService(app, 'worker', false);
|
|
||||||
|
|
||||||
class ScheduleModel {
|
class ScheduleModel {
|
||||||
static coll = coll;
|
static coll = coll;
|
||||||
|
|
||||||
@@ -123,20 +55,14 @@ class ScheduleModel {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static getFirst = getFirst;
|
static getFirst = getFirst;
|
||||||
static Worker = Worker;
|
/** @deprecated use ctx.inject(['worker'], cb) instead */
|
||||||
}
|
static Worker: WorkerService;
|
||||||
|
|
||||||
declare module '../context' {
|
|
||||||
interface Context {
|
|
||||||
worker: WorkerService;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
Context.service('worker', WorkerService);
|
ctx.inject(['worker'], (c) => {
|
||||||
ctx.worker = Worker;
|
ScheduleModel.Worker = c.worker;
|
||||||
|
c.worker.addHandler('task.daily', async () => {
|
||||||
Worker.addHandler('task.daily', async () => {
|
|
||||||
await RecordModel.coll.deleteMany({ contest: { $in: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } });
|
await RecordModel.coll.deleteMany({ contest: { $in: [RecordModel.RECORD_PRETEST, RecordModel.RECORD_GENERATE] } });
|
||||||
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
await global.Hydro.script.rp?.run({}, new Logger('task/rp').debug);
|
||||||
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
await global.Hydro.script.problemStat?.run({}, new Logger('task/problem').debug);
|
||||||
@@ -145,6 +71,8 @@ export async function apply(ctx: Context) {
|
|||||||
}
|
}
|
||||||
await ctx.parallel('task/daily');
|
await ctx.parallel('task/daily');
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId }));
|
ctx.on('domain/delete', (domainId) => coll.deleteMany({ domainId }));
|
||||||
|
|
||||||
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/* eslint-disable max-len */
|
/* eslint-disable max-len */
|
||||||
/* eslint-disable no-await-in-loop */
|
/* eslint-disable no-await-in-loop */
|
||||||
import cordis from 'cordis';
|
import * as cordis from 'cordis';
|
||||||
import yaml from 'js-yaml';
|
import yaml from 'js-yaml';
|
||||||
import { Dictionary } from 'lodash';
|
import { Dictionary } from 'lodash';
|
||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
@@ -300,11 +300,10 @@ declare module '../context' {
|
|||||||
const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
|
const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
|
||||||
function method(this: cordis.Service, ...args: Parameters<F>) {
|
function method(this: cordis.Service, ...args: Parameters<F>) {
|
||||||
const res = origFunc(...args);
|
const res = origFunc(...args);
|
||||||
this.caller?.on('dispose', () => (disposeFunc ? disposeFunc(res) : res()));
|
this[Context.current]?.on('dispose', () => (disposeFunc ? disposeFunc(res) : res()));
|
||||||
};
|
};
|
||||||
|
|
||||||
export class SettingService extends Service {
|
export class SettingService extends Service {
|
||||||
static readonly methods = ['PreferenceSetting', 'AccountSetting', 'DomainSetting', 'DomainUserSetting', 'SystemSetting'];
|
|
||||||
PreferenceSetting = T(PreferenceSetting);
|
PreferenceSetting = T(PreferenceSetting);
|
||||||
AccountSetting = T(AccountSetting);
|
AccountSetting = T(AccountSetting);
|
||||||
DomainSetting = T(DomainSetting);
|
DomainSetting = T(DomainSetting);
|
||||||
@@ -312,16 +311,16 @@ export class SettingService extends Service {
|
|||||||
SystemSetting = T(SystemSetting);
|
SystemSetting = T(SystemSetting);
|
||||||
constructor(ctx: Context) {
|
constructor(ctx: Context) {
|
||||||
super(ctx, 'setting', true);
|
super(ctx, 'setting', true);
|
||||||
|
ctx.mixin('setting', ['PreferenceSetting', 'AccountSetting', 'DomainSetting', 'DomainUserSetting', 'SystemSetting']);
|
||||||
}
|
}
|
||||||
|
|
||||||
get(key: string) {
|
get(key: string) {
|
||||||
return (this.caller ? this.caller.domain?.config?.[key.replace(/\./g, '$')] : null) ?? global.Hydro.model.system.get(key);
|
return (this[Context.current] ? this[Context.current].domain?.config?.[key.replace(/\./g, '$')] : null) ?? global.Hydro.model.system.get(key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
Context.service('setting', SettingService);
|
ctx.plugin(SettingService);
|
||||||
ctx.setting = new SettingService(ctx);
|
|
||||||
logger.info('Ensuring settings');
|
logger.info('Ensuring settings');
|
||||||
const system = global.Hydro.model.system;
|
const system = global.Hydro.model.system;
|
||||||
for (const setting of SYSTEM_SETTINGS) {
|
for (const setting of SYSTEM_SETTINGS) {
|
||||||
|
|||||||
@@ -3,8 +3,8 @@ import { escapeRegExp } from 'lodash';
|
|||||||
import moment from 'moment-timezone';
|
import moment from 'moment-timezone';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
import type { Readable } from 'stream';
|
import type { Readable } from 'stream';
|
||||||
|
import { Context } from '../context';
|
||||||
import mime from '../lib/mime';
|
import mime from '../lib/mime';
|
||||||
import * as bus from '../service/bus';
|
|
||||||
import db from '../service/db';
|
import db from '../service/db';
|
||||||
import storage from '../service/storage';
|
import storage from '../service/storage';
|
||||||
import ScheduleModel from './schedule';
|
import ScheduleModel from './schedule';
|
||||||
@@ -135,8 +135,20 @@ async function cleanFiles() {
|
|||||||
res = await StorageModel.coll.findOneAndDelete({ autoDelete: { $lte: new Date() } });
|
res = await StorageModel.coll.findOneAndDelete({ autoDelete: { $lte: new Date() } });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ScheduleModel.Worker.addHandler('storage.prune', cleanFiles);
|
|
||||||
bus.on('ready', async () => {
|
export function apply(ctx: Context) {
|
||||||
|
ctx.inject(['worker'], (c) => {
|
||||||
|
c.worker.addHandler('storage.prune', cleanFiles);
|
||||||
|
});
|
||||||
|
ctx.on('domain/delete', async (domainId) => {
|
||||||
|
const [problemFiles, contestFiles, trainingFiles] = await Promise.all([
|
||||||
|
StorageModel.list(`problem/${domainId}`),
|
||||||
|
StorageModel.list(`contest/${domainId}`),
|
||||||
|
StorageModel.list(`training/${domainId}`),
|
||||||
|
]);
|
||||||
|
await StorageModel.del(problemFiles.concat(contestFiles).concat(trainingFiles).map((i) => i.path));
|
||||||
|
});
|
||||||
|
ctx.on('ready', async () => {
|
||||||
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||||
await db.ensureIndexes(
|
await db.ensureIndexes(
|
||||||
StorageModel.coll,
|
StorageModel.coll,
|
||||||
@@ -150,11 +162,8 @@ bus.on('ready', async () => {
|
|||||||
interval: [1, 'hour'],
|
interval: [1, 'hour'],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
bus.on('domain/delete', async (domainId) => {
|
}
|
||||||
const files = await StorageModel.list(`problem/${domainId}`);
|
|
||||||
await StorageModel.del(files.map((i) => i.path));
|
|
||||||
});
|
|
||||||
|
|
||||||
global.Hydro.model.storage = StorageModel;
|
global.Hydro.model.storage = StorageModel;
|
||||||
export default StorageModel;
|
export default StorageModel;
|
||||||
@@ -14,7 +14,7 @@ export class CheckService extends Service {
|
|||||||
c = {};
|
c = {};
|
||||||
addChecker(type: string, checkFunc: CheckItem) {
|
addChecker(type: string, checkFunc: CheckItem) {
|
||||||
this.checkers[`check${type}`] = checkFunc;
|
this.checkers[`check${type}`] = checkFunc;
|
||||||
this.caller?.on('dispose', () => {
|
this[Context.current]?.on('dispose', () => {
|
||||||
delete this.checkers[`check${type}`];
|
delete this.checkers[`check${type}`];
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -73,6 +73,6 @@ check.addChecker('Setting', async (ctx, log, warn) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
Context.service('check', CheckService);
|
ctx.provide('check', CheckService, true);
|
||||||
ctx.check = check;
|
ctx.check = check;
|
||||||
}
|
}
|
||||||
@@ -479,12 +479,12 @@ class NotFoundHandler extends Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export class RouteService extends Service {
|
export class RouteService extends Service {
|
||||||
static readonly methods = ['Route', 'Connection', 'withHandlerClass'];
|
|
||||||
private registry = {};
|
private registry = {};
|
||||||
private registrationCount = Counter();
|
private registrationCount = Counter();
|
||||||
|
|
||||||
constructor(ctx) {
|
constructor(ctx: Context) {
|
||||||
super(ctx, 'server', true);
|
super(ctx, 'server', true);
|
||||||
|
ctx.mixin('server', ['Route', 'Connection', 'withHandlerClass']);
|
||||||
}
|
}
|
||||||
|
|
||||||
private register(func: typeof Route | typeof Connection, ...args: Parameters<typeof Route>) {
|
private register(func: typeof Route | typeof Connection, ...args: Parameters<typeof Route>) {
|
||||||
@@ -498,7 +498,7 @@ export class RouteService extends Service {
|
|||||||
this.registrationCount[name]++;
|
this.registrationCount[name]++;
|
||||||
const res = func(...args);
|
const res = func(...args);
|
||||||
this.ctx.parallel(`handler/register/${name}`, HandlerClass);
|
this.ctx.parallel(`handler/register/${name}`, HandlerClass);
|
||||||
this.caller?.on('dispose', () => {
|
this[Context.current]?.on('dispose', () => {
|
||||||
this.registrationCount[name]--;
|
this.registrationCount[name]--;
|
||||||
if (!this.registrationCount[name]) delete this.registry[name];
|
if (!this.registrationCount[name]) delete this.registry[name];
|
||||||
res();
|
res();
|
||||||
@@ -508,7 +508,7 @@ export class RouteService extends Service {
|
|||||||
public withHandlerClass(name: string, callback: (HandlerClass: typeof HandlerCommon) => any) {
|
public withHandlerClass(name: string, callback: (HandlerClass: typeof HandlerCommon) => any) {
|
||||||
if (this.registry[name]) callback(this.registry[name]);
|
if (this.registry[name]) callback(this.registry[name]);
|
||||||
this.ctx.on(`handler/register/${name}`, callback);
|
this.ctx.on(`handler/register/${name}`, callback);
|
||||||
this.caller?.on('dispose', () => {
|
this[Context.current]?.on('dispose', () => {
|
||||||
this.ctx.off(`handler/register/${name}`, callback);
|
this.ctx.off(`handler/register/${name}`, callback);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -531,7 +531,7 @@ declare module '../context' {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function apply(pluginContext: Context) {
|
export async function apply(pluginContext: Context) {
|
||||||
Context.service('server', RouteService);
|
pluginContext.provide('server', undefined, true);
|
||||||
pluginContext.server = new RouteService(pluginContext);
|
pluginContext.server = new RouteService(pluginContext);
|
||||||
app.keys = system.get('session.keys') as unknown as string[];
|
app.keys = system.get('session.keys') as unknown as string[];
|
||||||
if (process.env.HYDRO_CLI) return;
|
if (process.env.HYDRO_CLI) return;
|
||||||
|
|||||||
@@ -38,10 +38,9 @@ export default class Watcher extends Service {
|
|||||||
private declined: Set<string>;
|
private declined: Set<string>;
|
||||||
private stashed = new Set<string>();
|
private stashed = new Set<string>();
|
||||||
|
|
||||||
constructor(public ctx: Context, private config: any) {
|
constructor(public ctx: Context) {
|
||||||
super(ctx, 'watcher', true);
|
super(ctx, 'watcher', true);
|
||||||
this.externals = new Set(Object.keys(require.cache));
|
this.externals = new Set(Object.keys(require.cache));
|
||||||
Context.service('watcher');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
start() {
|
start() {
|
||||||
|
|||||||
@@ -0,0 +1,90 @@
|
|||||||
|
import moment from 'moment-timezone';
|
||||||
|
import { sleep } from '@hydrooj/utils';
|
||||||
|
import { Context, Service } from '../context';
|
||||||
|
import db from './db';
|
||||||
|
|
||||||
|
declare module '../context' {
|
||||||
|
interface Context {
|
||||||
|
worker: WorkerService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class WorkerService extends Service {
|
||||||
|
private handlers: Record<string, Function> = {};
|
||||||
|
consuming = true;
|
||||||
|
running = null;
|
||||||
|
promise: Promise<any> = null;
|
||||||
|
coll = db.collection('schedule');
|
||||||
|
|
||||||
|
constructor(ctx: Context) {
|
||||||
|
super(ctx, 'worker', true);
|
||||||
|
this.consume();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getFirst() {
|
||||||
|
if (process.env.CI) return null;
|
||||||
|
const q = {
|
||||||
|
executeAfter: { $lt: new Date() },
|
||||||
|
type: 'schedule',
|
||||||
|
subType: { $in: Object.keys(this.handlers) },
|
||||||
|
};
|
||||||
|
const res = await this.coll.findOneAndDelete(q);
|
||||||
|
if (res.value) {
|
||||||
|
this.ctx.logger.debug('%o', res.value);
|
||||||
|
if (res.value.interval) {
|
||||||
|
const executeAfter = moment(res.value.executeAfter).add(...res.value.interval).toDate();
|
||||||
|
await this.coll.insertOne({ ...res.value, executeAfter });
|
||||||
|
}
|
||||||
|
return res.value;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async consume() {
|
||||||
|
while (this.consuming) {
|
||||||
|
try {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
const doc = await this.getFirst();
|
||||||
|
if (!doc) {
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
this.running = doc;
|
||||||
|
this.ctx.logger.debug('Worker task: %o', doc);
|
||||||
|
const start = Date.now();
|
||||||
|
this.promise = Promise.race([
|
||||||
|
this.handlers[doc.subType](doc).catch((e) => {
|
||||||
|
this.ctx.logger.error('Worker task fail: ', e);
|
||||||
|
this.ctx.logger.error('%o', doc);
|
||||||
|
}),
|
||||||
|
sleep(1200000),
|
||||||
|
]);
|
||||||
|
// eslint-disable-next-line no-await-in-loop
|
||||||
|
await this.promise;
|
||||||
|
const spent = Date.now() - start;
|
||||||
|
if (spent > 500) this.ctx.logger.warn('Slow worker task (%d ms): %o', spent, doc);
|
||||||
|
this.running = null;
|
||||||
|
} catch (err) {
|
||||||
|
this.ctx.logger.error(err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public addHandler(type: string, handler: Function) {
|
||||||
|
this.handlers[type] = handler;
|
||||||
|
this[Context.current]?.on('dispose', () => {
|
||||||
|
delete this.handlers[type];
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async stop() {
|
||||||
|
this.consuming = false;
|
||||||
|
await this.promise;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function apply(ctx: Context) {
|
||||||
|
ctx.provide('worker', undefined, true);
|
||||||
|
ctx.worker = new WorkerService(ctx);
|
||||||
|
}
|
||||||
@@ -135,7 +135,7 @@ class ImportHojHandler extends Handler {
|
|||||||
export const name = 'import-hoj';
|
export const name = 'import-hoj';
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
ctx.Route('problem_import_hoj', '/problem/import/hoj', ImportHojHandler, PERM.PERM_CREATE_PROBLEM);
|
ctx.Route('problem_import_hoj', '/problem/import/hoj', ImportHojHandler, PERM.PERM_CREATE_PROBLEM);
|
||||||
ctx.inject('ProblemAdd', 'problem_import_hoj', { icon: 'copy', text: 'From HOJ Export' });
|
ctx.injectUI('ProblemAdd', 'problem_import_hoj', { icon: 'copy', text: 'From HOJ Export' });
|
||||||
ctx.i18n.load('zh', {
|
ctx.i18n.load('zh', {
|
||||||
'From HOJ Export': '从 HOJ 导入',
|
'From HOJ Export': '从 HOJ 导入',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -150,7 +150,7 @@ class ImportQduojHandler extends Handler {
|
|||||||
export const name = 'import-qduoj';
|
export const name = 'import-qduoj';
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
ctx.Route('problem_import_qduoj', '/problem/import/qduoj', ImportQduojHandler, PERM.PERM_CREATE_PROBLEM);
|
ctx.Route('problem_import_qduoj', '/problem/import/qduoj', ImportQduojHandler, PERM.PERM_CREATE_PROBLEM);
|
||||||
ctx.inject('ProblemAdd', 'problem_import_qduoj', { icon: 'copy', text: 'From QDUOJ Export' });
|
ctx.injectUI('ProblemAdd', 'problem_import_qduoj', { icon: 'copy', text: 'From QDUOJ Export' });
|
||||||
ctx.i18n.load('zh', {
|
ctx.i18n.load('zh', {
|
||||||
'From QDUOJ Export': '从 QDUOJ 导入',
|
'From QDUOJ Export': '从 QDUOJ 导入',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -9,8 +9,6 @@ declare module 'hydrooj' {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Context.service('metrics');
|
|
||||||
|
|
||||||
export function createRegistry(ctx: Context) {
|
export function createRegistry(ctx: Context) {
|
||||||
const registry = new Registry();
|
const registry = new Registry();
|
||||||
|
|
||||||
@@ -77,6 +75,7 @@ export function createRegistry(ctx: Context) {
|
|||||||
|
|
||||||
collectDefaultMetrics({ register: registry });
|
collectDefaultMetrics({ register: registry });
|
||||||
|
|
||||||
|
ctx.provide('metrics');
|
||||||
ctx.metrics = registry;
|
ctx.metrics = registry;
|
||||||
return registry;
|
return registry;
|
||||||
}
|
}
|
||||||
@@ -138,7 +138,7 @@ export async function apply(ctx: Context) {
|
|||||||
ctx.on('app/started', buildUI);
|
ctx.on('app/started', buildUI);
|
||||||
const debouncedBuildUI = debounce(buildUI, 2000, { trailing: true });
|
const debouncedBuildUI = debounce(buildUI, 2000, { trailing: true });
|
||||||
const triggerHotUpdate = (path?: string) => {
|
const triggerHotUpdate = (path?: string) => {
|
||||||
if (path && !path.includes('/ui-default/') && !path.includes('/public/')) return;
|
if (path && !path.includes('/ui-default/') && !path.includes('/public/') && !path.includes('/frontend/')) return;
|
||||||
debouncedBuildUI();
|
debouncedBuildUI();
|
||||||
};
|
};
|
||||||
ctx.on('system/setting', () => triggerHotUpdate());
|
ctx.on('system/setting', () => triggerHotUpdate());
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import type * as monaco from 'monaco-editor';
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useDispatch, useSelector, useStore } from 'react-redux';
|
import { useDispatch, useSelector, useStore } from 'react-redux';
|
||||||
import Dom from 'vj/components/react/DomComponent';
|
import Dom from 'vj/components/react/DomComponent';
|
||||||
import { Context, ctx, Service } from 'vj/context';
|
import { ctx, Service } from 'vj/context';
|
||||||
import ScratchpadEditor from './ScratchpadEditorContainer';
|
import ScratchpadEditor from './ScratchpadEditorContainer';
|
||||||
import ScratchpadPretest from './ScratchpadPretestContainer';
|
import ScratchpadPretest from './ScratchpadPretestContainer';
|
||||||
import ScratchpadRecords from './ScratchpadRecordsContainer';
|
import ScratchpadRecords from './ScratchpadRecordsContainer';
|
||||||
@@ -54,7 +54,6 @@ class ScratchpadService extends Service {
|
|||||||
rerenderCallback?.();
|
rerenderCallback?.();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Context.service('scratchpad', ScratchpadService);
|
|
||||||
declare module '../../context' {
|
declare module '../../context' {
|
||||||
interface Context {
|
interface Context {
|
||||||
scratchpad: ScratchpadService;
|
scratchpad: ScratchpadService;
|
||||||
@@ -63,6 +62,7 @@ declare module '../../context' {
|
|||||||
|
|
||||||
export default function ScratchpadContainer() {
|
export default function ScratchpadContainer() {
|
||||||
const store = useStore();
|
const store = useStore();
|
||||||
|
ctx.provide('scratchpad');
|
||||||
ctx.scratchpad ||= new ScratchpadService(store);
|
ctx.scratchpad ||= new ScratchpadService(store);
|
||||||
const [, updateState] = React.useState<any>();
|
const [, updateState] = React.useState<any>();
|
||||||
const forceUpdate = React.useCallback(() => updateState({}), []);
|
const forceUpdate = React.useCallback(() => updateState({}), []);
|
||||||
|
|||||||
@@ -19,8 +19,8 @@ streamsaver.mitm = `${window.isSecureContext ? '' : 'https://hydro.ac'}/streamsa
|
|||||||
|
|
||||||
const waitForWritableStream = window.WritableStream
|
const waitForWritableStream = window.WritableStream
|
||||||
? Promise.resolve()
|
? Promise.resolve()
|
||||||
: import('web-streams-polyfill/dist/ponyfill.es6').then(({ WritableStream }) => {
|
: import('web-streams-polyfill').then(({ WritableStream }) => {
|
||||||
window.WritableStream = WritableStream;
|
window.WritableStream = WritableStream as any;
|
||||||
streamsaver.WritableStream = window.WritableStream;
|
streamsaver.WritableStream = window.WritableStream;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,38 @@
|
|||||||
import * as cordis from 'cordis';
|
import * as cordis from 'cordis';
|
||||||
import type { EventMap } from './api';
|
import type { EventMap } from './api';
|
||||||
|
|
||||||
export interface Events<C extends Context = Context> extends cordis.Events<C>, EventMap {
|
export interface Events<C extends Context = Context> extends cordis.Events<C>, EventMap { }
|
||||||
}
|
|
||||||
|
export type { Disposable, ScopeStatus, Plugin } from 'cordis';
|
||||||
|
|
||||||
export interface Context {
|
export interface Context {
|
||||||
[Context.events]: Events<Context>;
|
[Context.events]: Events<this>;
|
||||||
setTimeout: typeof setTimeout;
|
broadcast: Context['emit'];
|
||||||
setInterval: typeof setInterval;
|
|
||||||
api: ApiMixin;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Context extends cordis.Context {
|
export class Context extends cordis.Context {
|
||||||
static readonly session = Symbol('session');
|
/** @deprecated use `ctx.root` instead */
|
||||||
|
get app() {
|
||||||
|
return this.root;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated use `root.config` instead */
|
||||||
|
get options() {
|
||||||
|
return this.root.config;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export namespace Context {
|
export namespace Context {
|
||||||
export interface Config extends cordis.Context.Config { }
|
export type Associate<P extends string, C extends Context = Context> = cordis.Context.Associate<P, C>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MainScope = cordis.MainScope<Context>;
|
export type MainScope = cordis.MainScope<Context>;
|
||||||
export type EffectScope = cordis.EffectScope<Context>;
|
export type EffectScope = cordis.EffectScope<Context>;
|
||||||
export type ForkScope = cordis.ForkScope<Context>;
|
export type ForkScope = cordis.ForkScope<Context>;
|
||||||
export type Plugin = cordis.Plugin<Context>;
|
|
||||||
export const Service = cordis.Service<Context>;
|
|
||||||
export namespace Plugin {
|
|
||||||
export type Function<T = any> = cordis.Plugin.Function<T, Context>;
|
|
||||||
export type Constructor<T = any> = cordis.Plugin.Constructor<T, Context>;
|
|
||||||
export type Object<S = any, T = any> = cordis.Plugin.Object<S, T, Context>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const T = <F extends (...args: any[]) => any>(origFunc: F, disposeFunc?) =>
|
export abstract class Service<T = any, C extends Context = Context> extends cordis.Service<T, C> {
|
||||||
function method(this: cordis.Service, ...args: Parameters<F>) {
|
[cordis.Service.setup]() {
|
||||||
const res = origFunc(...args);
|
this.ctx = new Context() as C;
|
||||||
this.caller?.on('dispose', () => (disposeFunc ? disposeFunc(res) : res()));
|
|
||||||
};
|
|
||||||
export class ApiMixin extends Service {
|
|
||||||
static readonly methods = ['setInterval', 'setTimeout'];
|
|
||||||
setInterval = T(setInterval, clearInterval);
|
|
||||||
setTimeout = T(setTimeout, clearTimeout);
|
|
||||||
constructor(ctx) {
|
|
||||||
super(ctx, 'api', true);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Context.service('api', ApiMixin);
|
|
||||||
export const ctx = new Context();
|
export const ctx = new Context();
|
||||||
@@ -14,15 +14,15 @@
|
|||||||
"cli": false
|
"cli": false
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@blueprintjs/core": "^5.9.1",
|
"@blueprintjs/core": "^5.10.0",
|
||||||
"@fontsource/dm-mono": "^5.0.18",
|
"@fontsource/dm-mono": "^5.0.19",
|
||||||
"@fontsource/fira-code": "^5.0.16",
|
"@fontsource/fira-code": "^5.0.17",
|
||||||
"@fontsource/inconsolata": "^5.0.16",
|
"@fontsource/inconsolata": "^5.0.17",
|
||||||
"@fontsource/jetbrains-mono": "^5.0.18",
|
"@fontsource/jetbrains-mono": "^5.0.19",
|
||||||
"@fontsource/pt-mono": "^5.0.8",
|
"@fontsource/pt-mono": "^5.0.12",
|
||||||
"@fontsource/roboto-mono": "^5.0.16",
|
"@fontsource/roboto-mono": "^5.0.17",
|
||||||
"@fontsource/source-code-pro": "^5.0.16",
|
"@fontsource/source-code-pro": "^5.0.17",
|
||||||
"@fontsource/ubuntu-mono": "^5.0.18",
|
"@fontsource/ubuntu-mono": "^5.0.19",
|
||||||
"@hydrooj/utils": "workspace:*",
|
"@hydrooj/utils": "workspace:*",
|
||||||
"@simplewebauthn/browser": "7.4.0",
|
"@simplewebauthn/browser": "7.4.0",
|
||||||
"@svgr/webpack": "^8.1.0",
|
"@svgr/webpack": "^8.1.0",
|
||||||
@@ -34,21 +34,21 @@
|
|||||||
"@types/nunjucks": "^3.2.6",
|
"@types/nunjucks": "^3.2.6",
|
||||||
"@types/pickadate": "^3.5.35",
|
"@types/pickadate": "^3.5.35",
|
||||||
"@types/qrcode": "^1.5.5",
|
"@types/qrcode": "^1.5.5",
|
||||||
"@types/react": "^18.2.57",
|
"@types/react": "^18.2.63",
|
||||||
"@types/react-dom": "^18.2.19",
|
"@types/react-dom": "^18.2.19",
|
||||||
"@types/redux-logger": "^3.0.13",
|
"@types/redux-logger": "^3.0.13",
|
||||||
"@types/webpack-env": "^1.18.4",
|
"@types/webpack-env": "^1.18.4",
|
||||||
"@vscode/codicons": "^0.0.35",
|
"@vscode/codicons": "^0.0.35",
|
||||||
"ajv": "^8.12.0",
|
"ajv": "^8.12.0",
|
||||||
"allotment": "^1.20.0",
|
"allotment": "^1.20.0",
|
||||||
"autoprefixer": "^10.4.17",
|
"autoprefixer": "^10.4.18",
|
||||||
"browser-update": "^3.3.44",
|
"browser-update": "^3.3.51",
|
||||||
"chalk": "^5.3.0",
|
"chalk": "^5.3.0",
|
||||||
"classnames": "^2.5.1",
|
"classnames": "^2.5.1",
|
||||||
"clean-webpack-plugin": "^4.0.0",
|
"clean-webpack-plugin": "^4.0.0",
|
||||||
"clipboard": "^2.0.11",
|
"clipboard": "^2.0.11",
|
||||||
"copy-webpack-plugin": "^12.0.2",
|
"copy-webpack-plugin": "^12.0.2",
|
||||||
"cordis": "2.8.8",
|
"cordis": "3.13.3",
|
||||||
"css-loader": "^6.10.0",
|
"css-loader": "^6.10.0",
|
||||||
"diff": "^5.2.0",
|
"diff": "^5.2.0",
|
||||||
"diff-dom": "^5.1.2",
|
"diff-dom": "^5.1.2",
|
||||||
@@ -69,7 +69,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",
|
||||||
"mini-css-extract-plugin": "^2.8.0",
|
"mini-css-extract-plugin": "^2.8.1",
|
||||||
"moment": "^2.30.1",
|
"moment": "^2.30.1",
|
||||||
"monaco-editor": "0.46.0",
|
"monaco-editor": "0.46.0",
|
||||||
"monaco-editor-nls": "^3.1.0",
|
"monaco-editor-nls": "^3.1.0",
|
||||||
@@ -107,7 +107,7 @@
|
|||||||
"slideout": "^1.0.1",
|
"slideout": "^1.0.1",
|
||||||
"sticky-kit": "^1.1.3",
|
"sticky-kit": "^1.1.3",
|
||||||
"style-loader": "^3.3.4",
|
"style-loader": "^3.3.4",
|
||||||
"stylus": "^0.62.0",
|
"stylus": "^0.63.0",
|
||||||
"stylus-loader": "7.1.2",
|
"stylus-loader": "7.1.2",
|
||||||
"tether": "1.4.7",
|
"tether": "1.4.7",
|
||||||
"tether-drop": "^1.4.2",
|
"tether-drop": "^1.4.2",
|
||||||
@@ -115,9 +115,9 @@
|
|||||||
"timeago-react": "^3.0.6",
|
"timeago-react": "^3.0.6",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"ts-loader": "^9.5.1",
|
"ts-loader": "^9.5.1",
|
||||||
"vditor": "^3.9.9",
|
"vditor": "^3.10.1",
|
||||||
"vinyl-buffer": "^1.0.1",
|
"vinyl-buffer": "^1.0.1",
|
||||||
"web-streams-polyfill": "^3.3.3",
|
"web-streams-polyfill": "^4.0.0",
|
||||||
"webpack": "^5.90.3",
|
"webpack": "^5.90.3",
|
||||||
"webpack-bundle-analyzer": "^4.10.1",
|
"webpack-bundle-analyzer": "^4.10.1",
|
||||||
"webpack-dev-server": "^5.0.2",
|
"webpack-dev-server": "^5.0.2",
|
||||||
@@ -143,6 +143,6 @@
|
|||||||
"nunjucks": "^3.2.4",
|
"nunjucks": "^3.2.4",
|
||||||
"schemastery-jsonschema": "^1.1.0",
|
"schemastery-jsonschema": "^1.1.0",
|
||||||
"streamsaver": "^2.0.6",
|
"streamsaver": "^2.0.6",
|
||||||
"xss": "^1.0.14"
|
"xss": "^1.0.15"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -3,7 +3,7 @@ import React from 'react';
|
|||||||
import { createRoot } from 'react-dom/client';
|
import { createRoot } from 'react-dom/client';
|
||||||
import VjNotification from 'vj/components/notification';
|
import VjNotification from 'vj/components/notification';
|
||||||
import selectUser from 'vj/components/selectUser';
|
import selectUser from 'vj/components/selectUser';
|
||||||
import { Context, ctx, Service } from 'vj/context';
|
import { ctx, Service } from 'vj/context';
|
||||||
import { NamedPage } from 'vj/misc/Page';
|
import { NamedPage } from 'vj/misc/Page';
|
||||||
import { api, gql, loadReactRedux } from 'vj/utils';
|
import { api, gql, loadReactRedux } from 'vj/utils';
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@ class MessagePadService extends Service {
|
|||||||
super(ctx, 'messagepad', true);
|
super(ctx, 'messagepad', true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Context.service('messagepad', MessagePadService);
|
|
||||||
declare module '../context' {
|
declare module '../context' {
|
||||||
interface Context {
|
interface Context {
|
||||||
messagepad: MessagePadService;
|
messagepad: MessagePadService;
|
||||||
@@ -43,6 +43,7 @@ const page = new NamedPage('home_messages', () => {
|
|||||||
|
|
||||||
reduxStore = store;
|
reduxStore = store;
|
||||||
(window as any).store = reduxStore;
|
(window as any).store = reduxStore;
|
||||||
|
ctx.provide('messagepad');
|
||||||
ctx.messagepad = new MessagePadService(store, WebSocket);
|
ctx.messagepad = new MessagePadService(store, WebSocket);
|
||||||
|
|
||||||
const sock = new WebSocket(`${UiContext.ws_prefix}home/messages-conn`);
|
const sock = new WebSocket(`${UiContext.ws_prefix}home/messages-conn`);
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ class VJudgeService extends Service {
|
|||||||
if (account.enableOn && !account.enableOn.includes(os.hostname())) continue;
|
if (account.enableOn && !account.enableOn.includes(os.hostname())) continue;
|
||||||
this.pool[`${account.type}/${account.handle}`] = new AccountService(provider, account);
|
this.pool[`${account.type}/${account.handle}`] = new AccountService(provider, account);
|
||||||
}
|
}
|
||||||
this.caller?.on('dispose', () => {
|
this[Context.current]?.on('dispose', () => {
|
||||||
// TODO dispose session
|
// TODO dispose session
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -193,7 +193,6 @@ export { BasicFetcher } from './fetch';
|
|||||||
export { VERDICT } from './verdict';
|
export { VERDICT } from './verdict';
|
||||||
export * from './interface';
|
export * from './interface';
|
||||||
|
|
||||||
Context.service('vjudge', VJudgeService);
|
|
||||||
export const name = 'vjudge';
|
export const name = 'vjudge';
|
||||||
export async function apply(ctx: Context) {
|
export async function apply(ctx: Context) {
|
||||||
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
if (process.env.NODE_APP_INSTANCE !== '0') return;
|
||||||
@@ -205,6 +204,7 @@ export async function apply(ctx: Context) {
|
|||||||
vjudge.addProvider(k, v);
|
vjudge.addProvider(k, v);
|
||||||
}
|
}
|
||||||
// });
|
// });
|
||||||
|
ctx.provide('vjudge');
|
||||||
ctx.vjudge = vjudge;
|
ctx.vjudge = vjudge;
|
||||||
ctx.check.addChecker('Vjudge', async (_ctx, log, warn, error) => {
|
ctx.check.addChecker('Vjudge', async (_ctx, log, warn, error) => {
|
||||||
const working = [];
|
const working = [];
|
||||||
|
|||||||
Reference in new issue
Block a user