diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 000000000..74e2839b3 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +*.js text eol=lf +*.jsx text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.json text eol=lf +*.yaml text eol=lf +*.yml text eol=lf +*.md text eol=lf +*.css text eol=lf +*.scss text eol=lf +*.less text eol=lf +*.html text eol=lf diff --git a/framework/framework/interface.ts b/framework/framework/interface.ts index d054d84a9..7796bc443 100644 --- a/framework/framework/interface.ts +++ b/framework/framework/interface.ts @@ -1,17 +1,39 @@ -import type { ConnectionHandler, Handler } from './server'; -import type { Context as CordisContext } from '@cordisjs/core'; +import type { ConnectionHandler, Handler, NotFoundHandler } from './server'; -type HandlerEvents = - Record<`handler/${HookType}/${string}`, (thisArg: Handler & Record) => VoidReturn> - & Record<`handler/${HookType}`, (thisArg: Handler) => VoidReturn> - & Record<`handler/register/${string}`, (HandlerClass: new (...args: any[]) => Handler) => VoidReturn> - & Record<`connection/${'create' | 'active' | 'close'}`, (thisArg: ConnectionHandler) => VoidReturn>; +export interface KnownHandlers { + 'NotFound': NotFoundHandler; +} + +type MapHandlerEvents> = + Record<`handler/${HookType}/${N}`, (thisArg: H) => VoidReturn> + & Record<`handler/${HookWithMethod}/${N}/${Methods}`, (thisArg: H) => VoidReturn>; + +type KnownHandlerEvents = { + [key in keyof KnownHandlers]: MapHandlerEvents +}[keyof KnownHandlers]; + +type HandlerEvents = + Record<`handler/${HookType}`, (thisArg: Handler) => VoidReturn> + & Record<`connection/${'create' | 'active' | 'close'}`, (thisArg: any) => VoidReturn>; export type VoidReturn = Promise | any; export type HookType = 'before-prepare' | 'before' | 'before-operation' | 'after' | 'finish'; +export type Methods = 'get' | 'post' | 'put' | 'delete' | 'patch'; +export type HookWithMethod = Exclude; -export interface ServerEvents extends HandlerEvents { - 'handler/create': (thisArg: Handler, type: 'ws' | 'http') => VoidReturn +export interface ServerEvents extends KnownHandlerEvents, HandlerEvents { + 'handler/create': (thisArg: Handler | ConnectionHandler, type: 'ws' | 'http') => VoidReturn + 'handler/create/http': (thisArg: Handler) => VoidReturn + 'handler/create/ws': (thisArg: ConnectionHandler) => VoidReturn 'handler/init': (thisArg: Handler) => VoidReturn - 'handler/error': (thisArg: Handler, e: Error) => VoidReturn + 'handler/error': (thisArg: Handler | ConnectionHandler, e: Error) => VoidReturn + [k: `handler/${HookType}/${string}`]: (thisArg: any) => VoidReturn + [k: `handler/${HookWithMethod}/${string}/${Methods}`]: (thisArg: any) => VoidReturn + [k: `handler/register/${string}`]: (HandlerClass: new (...args: any[]) => any) => VoidReturn + [k: `handler/error/${string}`]: (thisArg: Handler, e: Error) => VoidReturn +} + +declare module '@cordisjs/core' { + interface Events extends ServerEvents { + } } diff --git a/framework/framework/serializer.ts b/framework/framework/serializer.ts index 1510250ef..affa84a5c 100644 --- a/framework/framework/serializer.ts +++ b/framework/framework/serializer.ts @@ -1,7 +1,6 @@ import type { HandlerCommon } from './server'; -import type { Context } from '@cordisjs/core'; -export default function serializer(ignoreSerializeFunction = false, h?: HandlerCommon) { +export default function serializer(ignoreSerializeFunction = false, h?: HandlerCommon) { return (k: string, v: any) => { if (k.startsWith('_') && k !== '_id') return undefined; if (typeof v === 'bigint') return `BigInt::${v.toString()}`; diff --git a/framework/framework/server.ts b/framework/framework/server.ts index ee15b6f5f..7b8181f3d 100644 --- a/framework/framework/server.ts +++ b/framework/framework/server.ts @@ -125,8 +125,8 @@ export interface UserModel { _id: number; } -export interface HandlerCommon { } -export class HandlerCommon { +export interface HandlerCommon { } // eslint-disable-line @typescript-eslint/no-unused-vars +export class HandlerCommon { static [kHandler]: string | boolean = true; session: Record; args: Record; @@ -143,7 +143,7 @@ export class HandlerCommon { this.request = context.HydroContext.request; this.response = context.HydroContext.response; this.UiContext = context.HydroContext.UiContext; - this.ctx = ctx.extend({}); + this.ctx = (ctx as any).extend({}); } checkPerm(..._: bigint[]) { @@ -184,7 +184,8 @@ export class HandlerCommon { } renderHTML(templateName: string, args: Record) { - const renderers = Object.values(this.ctx.server.renderers).filter((r) => r.accept.includes(templateName) || r.asFallback); + const renderers = Object.values((this.ctx as any).server.renderers as Record) + .filter((r) => r.accept.includes(templateName) || r.asFallback); const topPrio = renderers.sort((a, b) => b.priority - a.priority)[0]; const engine = topPrio?.render || (() => JSON.stringify(args, serializer(false, this))); return engine(templateName, args, { @@ -196,7 +197,7 @@ export class HandlerCommon { } } -export class Handler extends HandlerCommon { +export class Handler extends HandlerCommon { loginMethods: any; noCheckPermView = false; notUsage = false; @@ -242,7 +243,7 @@ export class Handler extends HandlerCommon { } } -export class ConnectionHandler extends HandlerCommon { +export class ConnectionHandler extends HandlerCommon { conn: WebSocket; compression: Shorty; counter = 0; @@ -279,7 +280,7 @@ export class ConnectionHandler extends HandlerCommon } } -class NotFoundHandler extends Handler { +export class NotFoundHandler extends Handler { prepare() { throw new NotFoundError(this.request.path); } all() { } } @@ -477,7 +478,9 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); ? `_${ctx.request.body.operation}`.replace(/_([a-z])/gm, (s) => s[1].toUpperCase()) : ''; - await this.ctx.parallel('handler/create', h, 'http'); + // FIXME: should pass type check + await (this.ctx.parallel as any)('handler/create', h, 'http'); + await (this.ctx.parallel as any)('handler/create/http', h); if (checker) checker.call(h); if (method === 'post') { @@ -513,8 +516,8 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); const step = steps[current]; let control; if (step.startsWith('log/')) h.args[step.slice(4)] = Date.now(); - // eslint-disable-next-line no-await-in-loop - else if (step.startsWith('handler/')) control = await this.ctx.serial(step as any, h); + // @ts-ignore + else if (step.startsWith('handler/')) control = await this.ctx.serial(step, h); // eslint-disable-line no-await-in-loop // eslint-disable-next-line no-await-in-loop else if (typeof h[step] === 'function') control = await h[step](args); if (control) { @@ -528,8 +531,9 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); } } catch (e) { try { - await this.ctx.serial(`handler/error/${name}`, h, e); - await this.ctx.serial('handler/error', h, e); + // FIXME: should pass type check + await (this.ctx.serial as any)(`handler/error/${name}`, h, e); + await (this.ctx.serial as any)('handler/error', h, e); await h.onerror(e); } catch (err) { logger.error(err); @@ -543,7 +547,8 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); private async handleWS(ctx: KoaContext, HandlerClass, checker, conn, layer?) { const { args } = ctx.HydroContext; const h = new HandlerClass(ctx, this.ctx); - await this.ctx.parallel('connection/create', h); + // FIXME: should pass type check + await (this.ctx.parallel as any)('connection/create', h); const stream = new PassThrough(); if (!conn) { // By HTTP @@ -571,7 +576,9 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); h.conn = conn; const disposables = []; try { - await this.ctx.parallel('handler/create', h, 'ws'); + // FIXME: should pass type check + await (this.ctx.parallel as any)('handler/create', h, 'ws'); + await (this.ctx.parallel as any)('handler/create/ws', h); checker.call(h); if (args.shorty) h.resetCompression(); if (h._prepare) await h._prepare(args); @@ -583,7 +590,8 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); const clean = () => { if (closed) return; closed = true; - this.ctx.emit('connection/close', h); + // FIXME: should pass type check + (this.ctx.emit as any)('connection/close', h); if (layer) layer.clients.delete(conn); if (interval) clearInterval(interval); for (const d of disposables) d(); @@ -621,7 +629,8 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); } }; } else ctx.body = stream; - await this.ctx.parallel('connection/active', h); + // FIXME: should pass type check + await (this.ctx.parallel as any)('connection/active', h as any); if (layer) { if (conn.readyState === conn.OPEN) { conn.on('close', clean); @@ -643,11 +652,11 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); this.registry[name] = HandlerClass; this.registrationCount[name]++; - const Checker = (permPrivChecker) => { + const Checker = (args) => { let perm: bigint; let priv: number; let checker = () => { }; - for (const item of permPrivChecker) { + for (const item of args) { if (typeof item === 'object') { if (typeof item.call !== 'undefined') { checker = item; @@ -692,7 +701,8 @@ ${c.response.status} ${endTime - startTime}ms ${c.response.length}`); name: T, callback: (HandlerClass: T extends `${string}ConnectionHandler` ? typeof ConnectionHandler : typeof Handler) => any, ) { if (this.registry[name]) callback(this.registry[name]); - this.ctx.on(`handler/register/${name}`, callback); + // FIXME: should pass type check + this.ctx.on(`handler/register/${name}`, callback as any); } // FIXME: should be typeof Handler instead of any diff --git a/package.json b/package.json index ff581c3b7..748bcdce8 100644 --- a/package.json +++ b/package.json @@ -20,8 +20,8 @@ "build:ui:production:webpack": "cross-env NODE_OPTIONS=--max_old_space_size=8192 node packages/ui-default/build --production", "test": "node -r @hydrooj/register packages/common/tests/subtask.spec.ts && node test/entry.js", "benchmark": "cross-env BENCHMARK=true node test/entry.js", - "lint": "eslint packages plugins modules --ext .ts,.tsx,.jsx --fix", - "lint:ci": "eslint packages plugins modules --ext .ts,.tsx,.jsx --max-warnings=0", + "lint": "eslint packages plugins modules framework --ext .ts,.tsx,.jsx --fix", + "lint:ci": "eslint packages plugins modules framework --ext .ts,.tsx,.jsx --max-warnings=0", "lint:ui": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx . --fix", "lint:ui:ci": "yarn workspace @hydrooj/ui-default lint --ext .js,.ts,.jsx,.tsx . --max-warnings=0", "debug": "node --trace-warnings --async-stack-traces --trace-deprecation packages/hydrooj/bin/hydrooj --debug --template", diff --git a/packages/hydrooj/package.json b/packages/hydrooj/package.json index cc0d44242..e8e558755 100644 --- a/packages/hydrooj/package.json +++ b/packages/hydrooj/package.json @@ -16,8 +16,8 @@ "@aws-sdk/lib-storage": "3.726.1", "@aws-sdk/s3-presigned-post": "3.726.1", "@aws-sdk/s3-request-presigner": "3.726.1", - "@cordisjs/core": "4.0.0-alpha.0", - "@cordisjs/logger": "1.0.0-alpha.0", + "@cordisjs/core": "4.0.0-alpha.1", + "@cordisjs/logger": "1.0.0-alpha.1", "@cordisjs/plugin-timer": "1.0.0-alpha.0", "@graphql-tools/schema": "^10.0.18", "@hydrooj/common": "workspace:^", diff --git a/packages/hydrooj/src/context.ts b/packages/hydrooj/src/context.ts index f1f6e2610..29f77a202 100644 --- a/packages/hydrooj/src/context.ts +++ b/packages/hydrooj/src/context.ts @@ -2,16 +2,15 @@ import * as cordis from '@cordisjs/core'; import { Logger } from '@cordisjs/logger'; import { TimerService } from '@cordisjs/plugin-timer'; import Schema from 'schemastery'; -import type { ServerEvents, WebService } from '@hydrooj/framework'; import type { DomainDoc, GeoIP, ModuleInterfaces } from './interface'; import { inject } from './lib/ui'; import { Loader } from './loader'; import type { EventMap } from './service/bus'; import type { CheckService } from './service/check'; import type { } from './service/migration'; -import type { ConnectionHandler, Handler } from './service/server'; -export interface Events extends cordis.Events, EventMap { } +// TODO: this is an broken declaration +export { EventMap as Events }; function addScript(name: string, description: string, validate: Schema, run: (args: K, report: any) => boolean | Promise) { if (global.Hydro.script[name]) throw new Error(`duplicate script ${name} registered.`); @@ -30,8 +29,7 @@ export type EffectScope = cordis.EffectScope; export { Disposable, Plugin, ScopeStatus } from '@cordisjs/core'; export interface Context extends cordis.Context { - // @ts-ignore - [Context.events]: Events & ServerEvents; + [Context.events]: EventMap & cordis.Events; loader: Loader; check: CheckService; setImmediate: typeof setImmediate; @@ -53,7 +51,7 @@ const T = any>(origFunc: F, disposeFunc?) => }); }; -export class ApiMixin extends cordis.Service { +export class ApiMixin extends cordis.Service { addScript = T(addScript); setImmediate = T(setImmediate, clearImmediate); provideModule = T(provideModule); diff --git a/packages/hydrooj/src/service/bus.ts b/packages/hydrooj/src/service/bus.ts index 5cf0fc157..22c0576dc 100644 --- a/packages/hydrooj/src/service/bus.ts +++ b/packages/hydrooj/src/service/bus.ts @@ -2,11 +2,9 @@ import { BSON, Db, Filter, ObjectId, OnlyFieldsOfType, } from 'mongodb'; -import type { Handler, ServerEvents } from '@hydrooj/framework'; +import type { Handler } from '@hydrooj/framework'; import pm2 from '@hydrooj/utils/lib/locate-pm2'; import { Context } from '../context'; -import type { ProblemSolutionHandler } from '../handler/problem'; -import type { UserRegisterHandler } from '../handler/user'; import type { BaseUserDict, ContestBalloonDoc, DiscussionDoc, DomainDoc, FileInfo, MessageDoc, ProblemDict, ProblemDoc, RecordDoc, @@ -16,14 +14,8 @@ import type { DocType } from '../model/document'; export type Disposable = () => void; export type VoidReturn = Promise | any; -type HookType = 'before-prepare' | 'before' | 'before-operation' | 'after' | 'finish'; -type MapHandlerEvents> = Record<`handler/${HookType}/${N}`, (thisArg: H) => VoidReturn>; -type KnownHandlerEvents = - MapHandlerEvents<'UserRegister', UserRegisterHandler> - & MapHandlerEvents<'ProblemSolution', ProblemSolutionHandler>; -/* eslint-disable @typescript-eslint/naming-convention */ -export interface EventMap extends KnownHandlerEvents { +export interface EventMap { 'app/listen': () => void 'app/started': () => void 'app/ready': () => VoidReturn @@ -38,7 +30,7 @@ export interface EventMap extends KnownHandlerEvents { 'database/config': () => VoidReturn 'system/setting': (args: Record) => VoidReturn - 'bus/broadcast': (event: keyof EventMap | keyof ServerEvents, payload: any, trace?: string) => VoidReturn + 'bus/broadcast': (event: keyof EventMap, payload: any, trace?: string) => VoidReturn 'monitor/update': (type: 'server' | 'judge', $set: any) => VoidReturn 'monitor/collect': (info: any) => VoidReturn 'api/update': () => void; @@ -101,11 +93,6 @@ export interface EventMap extends KnownHandlerEvents { 'record/change': (rdoc: RecordDoc, $set?: any, $push?: any, body?: any) => void 'record/judge': (rdoc: RecordDoc, updated: boolean, pdoc?: ProblemDoc) => VoidReturn } -/* eslint-enable @typescript-eslint/naming-convention */ - -declare module '@cordisjs/core' { - interface Events extends EventMap { } -} export function apply(ctx: Context) { try { diff --git a/packages/hydrooj/src/service/hmr.ts b/packages/hydrooj/src/service/hmr.ts index 7e693dffe..1ecb0549c 100644 --- a/packages/hydrooj/src/service/hmr.ts +++ b/packages/hydrooj/src/service/hmr.ts @@ -148,7 +148,7 @@ export default class HMR extends Service { this.analyzeChanges(); /** plugins pending classification */ - const pending = new Map>(); + const pending = new Map(); /** plugins that should be reloaded */ const reloads = new Map }>(); @@ -161,15 +161,16 @@ export default class HMR extends Service { if (typeof plugin !== 'object' || !plugin || !('apply' in plugin)) continue; const runtime = this.ctx.registry.get(plugin); if (!runtime || this.declined.has(filename)) continue; - pending.set(filename, runtime); + pending.set(filename, plugin); if (!plugin || !('sideEffect' in plugin) || !plugin['sideEffect']) this.declined.add(filename); } - for (const [filename, runtime] of pending) { + for (const [filename, plugin] of pending) { // check if it is a dependent of the changed file this.declined.delete(filename); const dependencies = [...loadDependencies(filename, this.declined)]; - if (!runtime.plugin['sideEffect']) this.declined.add(filename); + const runtime = this.ctx.registry.get(plugin); + if (!plugin['sideEffect']) this.declined.add(filename); // we only detect reloads at plugin level // a plugin will be reloaded if any of its dependencies are accepted @@ -177,7 +178,7 @@ export default class HMR extends Service { for (const dep of dependencies) this.accepted.add(dep); // prepare for reload - reloads.set(runtime.plugin, { + reloads.set(plugin, { filename, runtime, }); diff --git a/packages/hydrooj/src/service/server.ts b/packages/hydrooj/src/service/server.ts index c3b8e0466..6c01a9882 100644 --- a/packages/hydrooj/src/service/server.ts +++ b/packages/hydrooj/src/service/server.ts @@ -28,7 +28,7 @@ const ignoredLimit = `,${argv.options.ignoredLimit},`; const logger = new Logger('server'); declare module '@hydrooj/framework' { - export interface HandlerCommon { + export interface HandlerCommon { // eslint-disable-line @typescript-eslint/no-unused-vars domain: DomainDoc; paginate(cursor: FindCursor, page: number, key: string): Promise<[docs: T[], numPages: number, count: number]>; @@ -277,6 +277,8 @@ export async function apply(ctx: Context) { h.ctx = h.ctx.extend({ domain: h.domain, }); + }); + on('handler/create/http', async (h) => { if (!argv.options.benchmark && !h.notUsage) await h.limitRate('global', 5, 100); h.loginMethods = Object.keys(global.Hydro.module.oauth) .map((key) => ({ @@ -287,6 +289,9 @@ export async function apply(ctx: Context) { if (!h.noCheckPermView && !h.user.hasPriv(PRIV.PRIV_VIEW_ALL_DOMAIN)) h.checkPerm(PERM.PERM_VIEW); if (h.context.pendingError) throw h.context.pendingError; }); + on('handler/create/ws', async (h) => { + if (h.context.pendingError) throw h.context.pendingError; + }); on('app/listen', () => { server.listen(); diff --git a/packages/onsite-toolkit/index.ts b/packages/onsite-toolkit/index.ts index 44e298887..e0d09f653 100644 --- a/packages/onsite-toolkit/index.ts +++ b/packages/onsite-toolkit/index.ts @@ -5,6 +5,7 @@ import { ForbiddenError, fs, ObjectId, parseTimeMS, PERM, ProblemConfig, ProblemModel, STATUS, STATUS_SHORT_TEXTS, STATUS_TEXTS, Time, UserModel, } from 'hydrooj'; +import type { } from 'hydrooj/src/handler/contest'; import { ResolverInput } from './interface'; interface IpLoginInfo { diff --git a/packages/scoreboard-xcpcio/index.ts b/packages/scoreboard-xcpcio/index.ts index a999af122..28db0f17b 100644 --- a/packages/scoreboard-xcpcio/index.ts +++ b/packages/scoreboard-xcpcio/index.ts @@ -2,6 +2,7 @@ import path from 'path'; import { ContestModel, Context, fs, Handler, ObjectId, PERM, Schema, STATUS, Types, UserModel, } from 'hydrooj'; +import type { } from 'hydrooj/src/handler/contest'; const file = fs.readFileSync(path.join(__dirname, 'public/assets/board.html'), 'utf8'); const indexJs = file.match(/index-([A-Za-z0-9_-]+)\.js"/)?.[1]; diff --git a/packages/ui-default/package.json b/packages/ui-default/package.json index 759b4f0af..571376621 100644 --- a/packages/ui-default/package.json +++ b/packages/ui-default/package.json @@ -15,7 +15,7 @@ }, "devDependencies": { "@blueprintjs/core": "^5.16.6", - "@cordisjs/core": "4.0.0-alpha.0", + "@cordisjs/core": "4.0.0-alpha.1", "@fontsource/dm-mono": "^5.1.1", "@fontsource/fira-code": "^5.1.1", "@fontsource/inconsolata": "^5.1.1",