diff --git a/package.json b/package.json index 046bc8b..ccd4a68 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package", "name": "@kevisual/router", - "version": "0.0.20", + "version": "0.0.21-beta", "description": "", "type": "module", "main": "./dist/router.js", diff --git a/src/router-simple.ts b/src/router-simple.ts index 8eeeef0..cd1e5ad 100644 --- a/src/router-simple.ts +++ b/src/router-simple.ts @@ -1,8 +1,12 @@ import { pathToRegexp, Key } from 'path-to-regexp'; -import type { IncomingMessage, ServerResponse } from 'node:http'; +import type { IncomingMessage, ServerResponse, Server } from 'node:http'; import { parseBody, parseSearch, parseSearchValue } from './server/parse-body.ts'; +import { ListenOptions } from 'node:net'; type Req = IncomingMessage & { params?: Record }; +type SimpleObject = { + [key: string]: any; +}; interface Route { method: string; regexp: RegExp; @@ -28,7 +32,6 @@ export class SimpleRouter { use(method: string, route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise | void>) { const handlers = Array.isArray(fns) ? fns.flat() : []; const pattern = pathToRegexp(route); - this.routes.push({ method: method.toLowerCase(), regexp: pattern.regexp, keys: pattern.keys, handlers }); return this; } @@ -38,11 +41,35 @@ export class SimpleRouter { post(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise | void>) { return this.use('post', route, ...fns); } + sse(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise | void>) { + return this.use('sse', route, ...fns); + } all(route: string, ...fns: Array<(req: Req, res: ServerResponse) => Promise | void>) { this.use('post', route, ...fns); this.use('get', route, ...fns); + this.use('sse', route, ...fns); return this; } + getJson(v: string | number | boolean | SimpleObject) { + if (typeof v === 'object') { + return v; + } + try { + return JSON.parse(v as string); + } catch (e) { + return {}; + } + } + isSse(req: Req) { + const { headers } = req; + if (headers['accept'] && headers['accept'].includes('text/event-stream')) { + return true; + } + if (headers['content-type'] && headers['content-type'].includes('text/event-stream')) { + return true; + } + return false; + } /** * 解析 req 和 res 请求 * @param req @@ -51,10 +78,12 @@ export class SimpleRouter { */ parse(req: Req, res: ServerResponse) { const { pathname } = new URL(req.url, 'http://localhost'); - const method = req.method.toLowerCase(); + let method = req.method.toLowerCase(); if (this.exclude.includes(pathname)) { return 'is_exclude'; } + const isSse = this.isSse(req); + if (isSse) method = 'sse'; const route = this.routes.find((route) => { const matchResult = route.regexp.exec(pathname); if (matchResult && route.method === method) { @@ -74,4 +103,166 @@ export class SimpleRouter { return 'not_found'; } + /** + * 创建一个新的 HttpChain 实例 + * @param req + * @param res + * @returns + */ + chain(req?: Req, res?: ServerResponse) { + const chain = new HttpChain({ req, res, simpleRouter: this }); + return chain; + } + static Chain(opts?: HttpChainOpts) { + return new HttpChain(opts); + } +} + +type HttpChainOpts = { + req?: Req; + res?: ServerResponse; + simpleRouter?: SimpleRouter; +}; +export class HttpChain { + req: Req; + res: ServerResponse; + simpleRouter: SimpleRouter; + server: Server; + hasSetHeader: boolean = false; + isSseSet: boolean = false; + constructor(opts?: HttpChainOpts) { + this.req = opts?.req; + this.res = opts?.res; + this.simpleRouter = opts?.simpleRouter; + } + setReq(req: Req) { + this.req = req; + return this; + } + setRes(res: ServerResponse) { + this.res = res; + return this; + } + setRouter(router: SimpleRouter) { + this.simpleRouter = router; + return this; + } + setServer(server: Server) { + this.server = server; + return this; + } + /** + * 兼容 express 的一点功能 + * @param status + * @returns + */ + status(status: number) { + if (!this.res) return this; + if (this.hasSetHeader) { + return this; + } + this.hasSetHeader = true; + this.res.writeHead(status); + return this; + } + writeHead(status: number) { + if (!this.res) return this; + if (this.hasSetHeader) { + return this; + } + this.hasSetHeader = true; + this.res.writeHead(status); + return this; + } + json(data: SimpleObject) { + if (!this.res) return this; + this.res.end(JSON.stringify(data)); + return this; + } + /** + * 兼容 express 的一点功能 + * @param data + * @returns + */ + end(data: SimpleObject | string) { + if (!this.res) return this; + if (typeof data === 'object') { + this.res.end(JSON.stringify(data)); + } else if (typeof data === 'string') { + this.res.end(data); + } else { + this.res.end('nothing'); + } + return this; + } + + listen(opts: ListenOptions, callback?: () => void) { + this.server.listen(opts, callback); + return this; + } + parse() { + if (!this.server || !this.simpleRouter) { + throw new Error('Server and SimpleRouter must be set before calling parse'); + } + const that = this; + const listener = (req: Req, res: ServerResponse) => { + try { + that.simpleRouter.parse(req, res); + } catch (error) { + console.error('Error parsing request:', error); + if (!res.headersSent) { + res.writeHead(500); + res.end(JSON.stringify({ code: 500, message: 'Internal Server Error' })); + } + } + }; + this.server.on('request', listener); + return () => { + that.server.removeListener('request', listener); + }; + } + getString(value: string | SimpleObject) { + if (typeof value === 'string') { + return value; + } + return JSON.stringify(value); + } + sse(value: string | SimpleObject) { + const res = this.res; + const req = this.req; + if (!res || !req) return; + const data = this.getString(value); + if (this.isSseSet) { + res.write(`data: ${data}\n\n`); + return this; + } + const headersMap = new Map([ + ['Content-Type', 'text/event-stream'], + ['Cache-Control', 'no-cache'], + ['Connection', 'keep-alive'], + ]); + this.isSseSet = true; + let intervalId: NodeJS.Timeout; + if (!this.hasSetHeader) { + this.hasSetHeader = true; + res.setHeaders(headersMap); + // 每隔 2 秒发送一个空行,保持连接 + setInterval(() => { + res.write('\n'); // 发送一个空行,保持连接 + }, 3000); + // 客户端断开连接时清理 + req.on('close', () => { + clearInterval(intervalId); + res.end(); + }); + } + this.res.write(`data: ${data}\n\n`); + return this; + } + close() { + if (this.req?.destroy) { + this.req.destroy(); + } + return this; + } } diff --git a/src/validator/rule.ts b/src/validator/rule.ts index f490902..db5c9e5 100644 --- a/src/validator/rule.ts +++ b/src/validator/rule.ts @@ -8,8 +8,8 @@ type BaseRule = { type RuleString = { type: 'string'; - minLength?: number; - maxLength?: number; + min?: number; + max?: number; regex?: string; } & BaseRule; @@ -26,8 +26,6 @@ type RuleBoolean = { type RuleArray = { type: 'array'; items: Rule; - minItems?: number; - maxItems?: number; } & BaseRule; type RuleObject = { @@ -45,8 +43,8 @@ export const schemaFormRule = (rule: Rule): z.ZodType => { switch (rule.type) { case 'string': let stringSchema = z.string(); - if (rule.minLength) stringSchema = stringSchema.min(rule.minLength, `String must be at least ${rule.minLength} characters long.`); - if (rule.maxLength) stringSchema = stringSchema.max(rule.maxLength, `String must not exceed ${rule.maxLength} characters.`); + if (rule.min) stringSchema = stringSchema.min(rule.min, `String must be at least ${rule.min} characters long.`); + if (rule.max) stringSchema = stringSchema.max(rule.max, `String must not exceed ${rule.max} characters.`); if (rule.regex) stringSchema = stringSchema.regex(new RegExp(rule.regex), 'Invalid format'); return stringSchema; case 'number':