Files
router/src/server/server-base.ts
2025-12-21 19:29:34 +08:00

285 lines
8.5 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import type { IncomingMessage, ServerResponse } from 'node:http';
import { handleServer } from './handle-server.ts';
import * as cookie from './cookie.ts';
import { ServerType, Listener, OnListener, ServerOpts, OnWebSocketOptions, OnWebSocketFn, WebSocketListenerFun, ListenerFun, HttpListenerFun, WS } from './server-type.ts';
import { parseIfJson } from '../utils/parse.ts';
import { EventEmitter } from 'events';
type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void;
export type HandleCtx = {
req: IncomingMessage & { cookies: Record<string, string> };
res: ServerResponse & {
/**
* cookie 函数, end 参数用于设置是否立即设置到响应头设置了后面的cookie再设置会覆盖前面的
*/
cookie: CookieFn; //
};
};
// 实现函数
export function createHandleCtx(req: IncomingMessage, res: ServerResponse): HandleCtx {
// 用于存储所有的 Set-Cookie 字符串
const cookies: string[] = [];
let handReq = req as HandleCtx['req'];
let handRes = res as HandleCtx['res'];
// 扩展 res.cookie 方法
const cookieFn: CookieFn = (name, value, options = {}, end = true) => {
// 序列化新的 Cookie
const serializedCookie = cookie.serialize(name, value, options);
cookies.push(serializedCookie); // 将新的 Cookie 添加到数组
if (end) {
// 如果设置了 end 参数,则立即设置到响应头
res.setHeader('Set-Cookie', cookies);
}
};
// 解析请求中的现有 Cookie
const parsedCookies = cookie.parse(req.headers.cookie || '');
handReq.cookies = parsedCookies;
handRes.cookie = cookieFn;
// 返回扩展的上下文
return {
req: handReq,
res: handRes,
};
}
export type Cors = {
/**
* @default '*''
*/
origin?: string | undefined;
};
export const resultError = (error: string, code = 500) => {
const r = {
code: code,
message: error,
};
return JSON.stringify(r);
};
export class ServerBase implements ServerType {
path = '/api/router';
_server: any;
handle: ServerOpts['handle'];
_callback: any;
cors: Cors;
listeners: Listener[] = [];
emitter = new EventEmitter();
showConnected = true;
constructor(opts?: ServerOpts) {
this.path = opts?.path || '/api/router';
this.handle = opts?.handle;
this.cors = opts?.cors;
this.showConnected = opts?.showConnected !== false;
}
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
listen(port: number, hostname?: string, listeningListener?: () => void): void;
listen(port: number, backlog?: number, listeningListener?: () => void): void;
listen(port: number, listeningListener?: () => void): void;
listen(path: string, backlog?: number, listeningListener?: () => void): void;
listen(path: string, listeningListener?: () => void): void;
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
listen(handle: any, listeningListener?: () => void): void;
listen(...args: any[]) {
this.customListen(...args);
}
/**
* child class can custom listen method
* @param args
*/
customListen(...args: any[]) {
console.error('Please use createServer to create server instance');
}
get handleServer() {
return this._callback;
}
set handleServer(fn: any) {
this._callback = fn;
}
get callback() {
return this._callback || this.createCallback();
}
get server() {
return this._server;
}
setHandle(handle?: any) {
this.handle = handle;
}
/**
* get callback
* @returns
*/
createCallback() {
const path = this.path;
const handle = this.handle;
const cors = this.cors;
const that = this;
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
// only handle /api/router
if (req.url === '/favicon.ico') {
return;
}
const listeners = that.listeners || [];
for (const item of listeners) {
const func = item.func as any;
if (typeof func === 'function' && !item.io) {
await func(req, res);
}
}
if (res.headersSent) {
// 程序已经在其他地方响应了
return;
}
if (!req.url.startsWith(path)) {
// 判断不是当前路径的请求,交给其他监听处理
return;
}
if (cors) {
res.setHeader('Access-Control-Allow-Origin', cors?.origin || '*'); // 允许所有域名的请求访问,可以根据需要设置具体的域名
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
if (req.method === 'OPTIONS') {
res.end();
return;
}
}
const url = req.url;
if (!url.startsWith(path)) {
res.end(resultError(`not path:[${path}]`));
return;
}
const messages = await handleServer(req, res);
if (!handle) {
res.end(resultError('no handle'));
return;
}
try {
const end = await handle(messages as any, { req, res });
if (res.writableEnded) {
// 如果响应已经结束,则不进行任何操作
return;
}
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (typeof end === 'string') {
res.end(end);
} else {
res.end(JSON.stringify(end));
}
} catch (e) {
console.error(e);
res.setHeader('Content-Type', 'application/json; charset=utf-8');
if (e.code && typeof e.code === 'number') {
res.end(resultError(e.message || `Router Server error`, e.code));
} else {
res.end(resultError('Router Server error'));
}
}
};
this._callback = _callback;
return _callback;
}
on(listener: OnListener) {
this.listeners = [];
const randomId = Math.random().toString(36).substring(2, 15);
if (typeof listener === 'function') {
this.listeners.push({ func: listener, id: 'all-' + randomId });
return;
}
if (Array.isArray(listener)) {
for (const item of listener) {
if (typeof item === 'function') {
this.listeners.push({ func: item, id: 'all-' + randomId });
} else {
this.listeners.push(item);
}
}
} else {
this.listeners.push(listener);
}
}
async onWebSocket({ ws, message, pathname, token, id }: OnWebSocketOptions) {
const listener = this.listeners.find((item) => item.path === pathname && item.io);
const data: any = parseIfJson(message);
if (listener) {
const end = (data: any) => {
ws.send(JSON.stringify(data));
}
(listener.func as WebSocketListenerFun)({
emitter: this.emitter,
data,
token,
id,
ws,
}, { end });
return;
}
if (typeof data === 'string') {
const cleanMessage = data.trim().replace(/^["']|["']$/g, '');
if (cleanMessage === 'close') {
ws.close();
return;
}
if (cleanMessage === 'ping') {
ws.send('pong');
return;
}
}
const { type, data: typeData, ...rest } = data;
if (!type) {
ws.send(JSON.stringify({ code: 500, message: 'type is required' }));
return;
}
const res = {
type,
data: {} as any,
...rest,
};
const end = (data: any, all?: Record<string, any>) => {
const result = {
...res,
data,
...all,
};
ws.send(JSON.stringify(result));
};
// 调用 handle 处理消息
if (type === 'router' && this.handle) {
try {
const result = await this.handle(typeData as any);
end(result);
} catch (e: any) {
if (e.code && typeof e.code === 'number') {
end({
code: e.code,
message: e.message,
});
} else {
end({ code: 500, message: 'Router Server error' });
}
}
} else {
end({ code: 500, message: `${type} server is error` });
}
}
async onWsClose(ws: WS) {
const id = ws?.data?.id || '';
if (id) {
this.emitter.emit('close--' + id, { type: 'close', ws, id });
setTimeout(() => {
// 关闭后 5 秒清理监听器, 避免内存泄漏, 理论上原本的自己就应该被清理掉了,这里是保险起见
this.emitter.removeAllListeners('close--' + id);
this.emitter.removeAllListeners(id);
}, 5000);
}
}
async sendConnected(ws: WS) {
if (this.showConnected)
ws.send(JSON.stringify({ type: 'connected' }));
}
}