@kevisual/router 0.0.41 → 0.0.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/router-browser.js +66 -48
- package/dist/router-sign.js +3 -1
- package/dist/router-simple.js +50 -30
- package/dist/router.d.ts +142 -79
- package/dist/router.js +5057 -4872
- package/package.json +5 -2
- package/src/app.ts +21 -16
- package/src/index.ts +2 -2
- package/src/router-simple.ts +1 -0
- package/src/server/handle-server.ts +1 -1
- package/src/server/index.ts +2 -1
- package/src/server/parse-body.ts +44 -33
- package/src/server/server-base.ts +265 -0
- package/src/server/server-bun.ts +174 -0
- package/src/server/server-type.ts +70 -0
- package/src/server/server.ts +25 -177
- package/src/server/ws-server.ts +35 -103
- package/src/test/listen-ip.ts +2 -2
- package/src/utils/is-engine.ts +5 -1
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://json.schemastore.org/package",
|
|
3
3
|
"name": "@kevisual/router",
|
|
4
|
-
"version": "0.0.
|
|
4
|
+
"version": "0.0.42",
|
|
5
5
|
"description": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/router.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"keywords": [],
|
|
21
21
|
"author": "abearxiong",
|
|
22
22
|
"license": "MIT",
|
|
23
|
+
"packageManager": "pnpm@10.26.0",
|
|
23
24
|
"devDependencies": {
|
|
24
25
|
"@kevisual/local-proxy": "^0.0.8",
|
|
25
26
|
"@kevisual/query": "^0.0.32",
|
|
@@ -27,6 +28,7 @@
|
|
|
27
28
|
"@rollup/plugin-commonjs": "29.0.0",
|
|
28
29
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
29
30
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
31
|
+
"@types/bun": "^1.3.5",
|
|
30
32
|
"@types/node": "^25.0.3",
|
|
31
33
|
"@types/send": "^1.2.1",
|
|
32
34
|
"@types/ws": "^8.18.1",
|
|
@@ -38,6 +40,7 @@
|
|
|
38
40
|
"ts-loader": "^9.5.4",
|
|
39
41
|
"ts-node": "^10.9.2",
|
|
40
42
|
"tslib": "^2.8.1",
|
|
43
|
+
"tsx": "^4.21.0",
|
|
41
44
|
"typescript": "^5.9.3",
|
|
42
45
|
"ws": "npm:@kevisual/ws",
|
|
43
46
|
"xml2js": "^0.6.2",
|
|
@@ -96,4 +99,4 @@
|
|
|
96
99
|
"require": "./src/*"
|
|
97
100
|
}
|
|
98
101
|
}
|
|
99
|
-
}
|
|
102
|
+
}
|
package/src/app.ts
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { QueryRouter, Route, RouteContext, RouteOpts } from './route.ts';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
2
|
+
import { ServerNode, ServerNodeOpts } from './server/server.ts';
|
|
3
|
+
import { HandleCtx } from './server/server-base.ts';
|
|
4
|
+
import { ServerType } from './server/server-type.ts';
|
|
4
5
|
import { CustomError } from './result/error.ts';
|
|
5
6
|
import { handleServer } from './server/handle-server.ts';
|
|
6
7
|
import { IncomingMessage, ServerResponse } from 'http';
|
|
8
|
+
import { isBun } from './utils/is-engine.ts';
|
|
9
|
+
import { BunServer } from './server/server-bun.ts';
|
|
7
10
|
|
|
8
11
|
type RouterHandle = (msg: { path: string;[key: string]: any }) => { code: string; data?: any; message?: string;[key: string]: any };
|
|
9
12
|
type AppOptions<T = {}> = {
|
|
10
13
|
router?: QueryRouter;
|
|
11
|
-
server?:
|
|
14
|
+
server?: ServerType;
|
|
12
15
|
/** handle msg 关联 */
|
|
13
16
|
routerHandle?: RouterHandle;
|
|
14
17
|
routerContext?: RouteContext<T>;
|
|
15
|
-
serverOptions?:
|
|
16
|
-
io?: boolean;
|
|
17
|
-
ioOpts?: { routerHandle?: RouterHandle; routerContext?: RouteContext<T>; path?: string };
|
|
18
|
+
serverOptions?: ServerNodeOpts;
|
|
18
19
|
};
|
|
19
20
|
|
|
20
21
|
export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T> };
|
|
@@ -25,18 +26,22 @@ export type AppRouteContext<T = {}> = HandleCtx & RouteContext<T> & { app: App<T
|
|
|
25
26
|
*/
|
|
26
27
|
export class App<U = {}> {
|
|
27
28
|
router: QueryRouter;
|
|
28
|
-
server:
|
|
29
|
-
io: WsServer;
|
|
29
|
+
server: ServerType;
|
|
30
30
|
constructor(opts?: AppOptions<U>) {
|
|
31
31
|
const router = opts?.router || new QueryRouter();
|
|
32
|
-
|
|
32
|
+
let server = opts?.server;
|
|
33
|
+
if (!server) {
|
|
34
|
+
const serverOptions = opts?.serverOptions || {};
|
|
35
|
+
if (!isBun) {
|
|
36
|
+
server = new ServerNode(serverOptions)
|
|
37
|
+
} else {
|
|
38
|
+
server = new BunServer(serverOptions)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
33
41
|
server.setHandle(router.getHandle(router, opts?.routerHandle, opts?.routerContext));
|
|
34
42
|
router.setContext({ needSerialize: true, ...opts?.routerContext });
|
|
35
43
|
this.router = router;
|
|
36
44
|
this.server = server;
|
|
37
|
-
if (opts?.io) {
|
|
38
|
-
this.io = new WsServer(server, opts?.ioOpts);
|
|
39
|
-
}
|
|
40
45
|
}
|
|
41
46
|
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
|
42
47
|
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
|
@@ -49,9 +54,6 @@ export class App<U = {}> {
|
|
|
49
54
|
listen(...args: any[]) {
|
|
50
55
|
// @ts-ignore
|
|
51
56
|
this.server.listen(...args);
|
|
52
|
-
if (this.io) {
|
|
53
|
-
this.io.listen();
|
|
54
|
-
}
|
|
55
57
|
}
|
|
56
58
|
use(path: string, fn: (ctx: any) => any, opts?: RouteOpts) {
|
|
57
59
|
const route = new Route(path, '', opts);
|
|
@@ -130,7 +132,10 @@ export class App<U = {}> {
|
|
|
130
132
|
if (!this.server) {
|
|
131
133
|
throw new Error('Server is not initialized');
|
|
132
134
|
}
|
|
133
|
-
this.server.on(
|
|
135
|
+
this.server.on({
|
|
136
|
+
id: 'app-request-listener',
|
|
137
|
+
fun: fn
|
|
138
|
+
});
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
|
package/src/index.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type { RouteContext, RouteOpts, RouteMiddleware } from './route.ts';
|
|
|
5
5
|
|
|
6
6
|
export type { Run } from './route.ts';
|
|
7
7
|
|
|
8
|
-
export {
|
|
8
|
+
export { ServerNode, handleServer } from './server/index.ts';
|
|
9
9
|
/**
|
|
10
10
|
* 自定义错误
|
|
11
11
|
*/
|
|
@@ -13,7 +13,7 @@ export { CustomError } from './result/error.ts';
|
|
|
13
13
|
|
|
14
14
|
export { createSchema } from './validator/index.ts';
|
|
15
15
|
|
|
16
|
-
export type { Rule, Schema,
|
|
16
|
+
export type { Rule, Schema, } from './validator/index.ts';
|
|
17
17
|
|
|
18
18
|
export { App } from './app.ts';
|
|
19
19
|
|
package/src/router-simple.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
2
|
import { parseBody } from './parse-body.ts';
|
|
3
3
|
import url from 'node:url';
|
|
4
|
-
import { createHandleCtx } from './server.ts';
|
|
4
|
+
import { createHandleCtx } from './server-base.ts';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* get params and body
|
package/src/server/index.ts
CHANGED
package/src/server/parse-body.ts
CHANGED
|
@@ -1,7 +1,49 @@
|
|
|
1
1
|
import type { IncomingMessage } from 'node:http';
|
|
2
2
|
import url from 'node:url';
|
|
3
|
-
|
|
3
|
+
import { isBun } from '../utils/is-engine.ts';
|
|
4
4
|
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
|
5
|
+
const resolveBody = (body: string) => {
|
|
6
|
+
// 获取 Content-Type 头信息
|
|
7
|
+
const contentType = req.headers['content-type'] || '';
|
|
8
|
+
const resolve = (data: T) => {
|
|
9
|
+
return data;
|
|
10
|
+
}
|
|
11
|
+
// 处理 application/json
|
|
12
|
+
if (contentType.includes('application/json')) {
|
|
13
|
+
return resolve(JSON.parse(body) as T);
|
|
14
|
+
}
|
|
15
|
+
// 处理 application/x-www-form-urlencoded
|
|
16
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
17
|
+
const formData = new URLSearchParams(body);
|
|
18
|
+
const result: Record<string, any> = {};
|
|
19
|
+
|
|
20
|
+
formData.forEach((value, key) => {
|
|
21
|
+
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
|
22
|
+
try {
|
|
23
|
+
result[key] = JSON.parse(value);
|
|
24
|
+
} catch {
|
|
25
|
+
result[key] = value;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
return resolve(result as T);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 默认尝试 JSON 解析
|
|
33
|
+
try {
|
|
34
|
+
return resolve(JSON.parse(body) as T);
|
|
35
|
+
} catch {
|
|
36
|
+
return resolve({} as T);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
if (isBun) {
|
|
40
|
+
// @ts-ignore
|
|
41
|
+
const body = req.body;
|
|
42
|
+
if (body) {
|
|
43
|
+
return resolveBody(body)
|
|
44
|
+
}
|
|
45
|
+
return {} as T;
|
|
46
|
+
}
|
|
5
47
|
return new Promise<T>((resolve, reject) => {
|
|
6
48
|
const arr: any[] = [];
|
|
7
49
|
req.on('data', (chunk) => {
|
|
@@ -10,39 +52,8 @@ export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) =
|
|
|
10
52
|
req.on('end', () => {
|
|
11
53
|
try {
|
|
12
54
|
const body = Buffer.concat(arr).toString();
|
|
55
|
+
resolve(resolveBody(body));
|
|
13
56
|
|
|
14
|
-
// 获取 Content-Type 头信息
|
|
15
|
-
const contentType = req.headers['content-type'] || '';
|
|
16
|
-
|
|
17
|
-
// 处理 application/json
|
|
18
|
-
if (contentType.includes('application/json')) {
|
|
19
|
-
resolve(JSON.parse(body) as T);
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
// 处理 application/x-www-form-urlencoded
|
|
23
|
-
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
24
|
-
const formData = new URLSearchParams(body);
|
|
25
|
-
const result: Record<string, any> = {};
|
|
26
|
-
|
|
27
|
-
formData.forEach((value, key) => {
|
|
28
|
-
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
|
29
|
-
try {
|
|
30
|
-
result[key] = JSON.parse(value);
|
|
31
|
-
} catch {
|
|
32
|
-
result[key] = value;
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
resolve(result as T);
|
|
37
|
-
return;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
// 默认尝试 JSON 解析
|
|
41
|
-
try {
|
|
42
|
-
resolve(JSON.parse(body) as T);
|
|
43
|
-
} catch {
|
|
44
|
-
resolve({} as T);
|
|
45
|
-
}
|
|
46
57
|
} catch (e) {
|
|
47
58
|
resolve({} as T);
|
|
48
59
|
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { handleServer } from './handle-server.ts';
|
|
3
|
+
import * as cookie from './cookie.ts';
|
|
4
|
+
import { ServerType, Listener, OnListener, ServerOpts } from './server-type.ts';
|
|
5
|
+
import { parseIfJson } from '../utils/parse.ts';
|
|
6
|
+
|
|
7
|
+
type CookieFn = (name: string, value: string, options?: cookie.SerializeOptions, end?: boolean) => void;
|
|
8
|
+
|
|
9
|
+
export type HandleCtx = {
|
|
10
|
+
req: IncomingMessage & { cookies: Record<string, string> };
|
|
11
|
+
res: ServerResponse & {
|
|
12
|
+
/**
|
|
13
|
+
* cookie 函数, end 参数用于设置是否立即设置到响应头,设置了后面的cookie再设置会覆盖前面的
|
|
14
|
+
*/
|
|
15
|
+
cookie: CookieFn; //
|
|
16
|
+
};
|
|
17
|
+
};
|
|
18
|
+
// 实现函数
|
|
19
|
+
export function createHandleCtx(req: IncomingMessage, res: ServerResponse): HandleCtx {
|
|
20
|
+
// 用于存储所有的 Set-Cookie 字符串
|
|
21
|
+
const cookies: string[] = [];
|
|
22
|
+
let handReq = req as HandleCtx['req'];
|
|
23
|
+
let handRes = res as HandleCtx['res'];
|
|
24
|
+
// 扩展 res.cookie 方法
|
|
25
|
+
const cookieFn: CookieFn = (name, value, options = {}, end = true) => {
|
|
26
|
+
// 序列化新的 Cookie
|
|
27
|
+
const serializedCookie = cookie.serialize(name, value, options);
|
|
28
|
+
cookies.push(serializedCookie); // 将新的 Cookie 添加到数组
|
|
29
|
+
if (end) {
|
|
30
|
+
// 如果设置了 end 参数,则立即设置到响应头
|
|
31
|
+
res.setHeader('Set-Cookie', cookies);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// 解析请求中的现有 Cookie
|
|
35
|
+
const parsedCookies = cookie.parse(req.headers.cookie || '');
|
|
36
|
+
handReq.cookies = parsedCookies;
|
|
37
|
+
handRes.cookie = cookieFn;
|
|
38
|
+
// 返回扩展的上下文
|
|
39
|
+
return {
|
|
40
|
+
req: handReq,
|
|
41
|
+
res: handRes,
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export type Cors = {
|
|
45
|
+
/**
|
|
46
|
+
* @default '*''
|
|
47
|
+
*/
|
|
48
|
+
origin?: string | undefined;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
export const resultError = (error: string, code = 500) => {
|
|
52
|
+
const r = {
|
|
53
|
+
code: code,
|
|
54
|
+
message: error,
|
|
55
|
+
};
|
|
56
|
+
return JSON.stringify(r);
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
export class ServerBase implements ServerType {
|
|
60
|
+
path = '/api/router';
|
|
61
|
+
_server: any;
|
|
62
|
+
handle: ServerOpts['handle'];
|
|
63
|
+
_callback: any;
|
|
64
|
+
cors: Cors;
|
|
65
|
+
listeners: Listener[] = [];
|
|
66
|
+
constructor(opts?: ServerOpts) {
|
|
67
|
+
this.path = opts?.path || '/api/router';
|
|
68
|
+
this.handle = opts?.handle;
|
|
69
|
+
this.cors = opts?.cors;
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): void;
|
|
73
|
+
listen(port: number, hostname?: string, listeningListener?: () => void): void;
|
|
74
|
+
listen(port: number, backlog?: number, listeningListener?: () => void): void;
|
|
75
|
+
listen(port: number, listeningListener?: () => void): void;
|
|
76
|
+
listen(path: string, backlog?: number, listeningListener?: () => void): void;
|
|
77
|
+
listen(path: string, listeningListener?: () => void): void;
|
|
78
|
+
listen(handle: any, backlog?: number, listeningListener?: () => void): void;
|
|
79
|
+
listen(handle: any, listeningListener?: () => void): void;
|
|
80
|
+
listen(...args: any[]) {
|
|
81
|
+
this.customListen(...args);
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* child class can custom listen method
|
|
85
|
+
* @param args
|
|
86
|
+
*/
|
|
87
|
+
customListen(...args: any[]) {
|
|
88
|
+
console.error('Please use createServer to create server instance');
|
|
89
|
+
}
|
|
90
|
+
get handleServer() {
|
|
91
|
+
return this._callback;
|
|
92
|
+
}
|
|
93
|
+
set handleServer(fn: any) {
|
|
94
|
+
this._callback = fn;
|
|
95
|
+
}
|
|
96
|
+
get callback() {
|
|
97
|
+
return this._callback || this.createCallback();
|
|
98
|
+
}
|
|
99
|
+
get server() {
|
|
100
|
+
return this._server;
|
|
101
|
+
}
|
|
102
|
+
setHandle(handle?: any) {
|
|
103
|
+
this.handle = handle;
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* get callback
|
|
107
|
+
* @returns
|
|
108
|
+
*/
|
|
109
|
+
createCallback() {
|
|
110
|
+
const path = this.path;
|
|
111
|
+
const handle = this.handle;
|
|
112
|
+
const cors = this.cors;
|
|
113
|
+
const that = this;
|
|
114
|
+
const _callback = async (req: IncomingMessage, res: ServerResponse) => {
|
|
115
|
+
// only handle /api/router
|
|
116
|
+
if (req.url === '/favicon.ico') {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
const listeners = that.listeners || [];
|
|
120
|
+
for (const item of listeners) {
|
|
121
|
+
const fun = item.fun;
|
|
122
|
+
if (typeof fun === 'function' && !item.io) {
|
|
123
|
+
await fun(req, res);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
if (res.headersSent) {
|
|
127
|
+
// 程序已经在其他地方响应了
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
if (!req.url.startsWith(path)) {
|
|
131
|
+
// 判断不是当前路径的请求,交给其他监听处理
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
if (cors) {
|
|
135
|
+
res.setHeader('Access-Control-Allow-Origin', cors?.origin || '*'); // 允许所有域名的请求访问,可以根据需要设置具体的域名
|
|
136
|
+
res.setHeader('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept, Authorization');
|
|
137
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST');
|
|
138
|
+
if (req.method === 'OPTIONS') {
|
|
139
|
+
res.end();
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const url = req.url;
|
|
144
|
+
if (!url.startsWith(path)) {
|
|
145
|
+
res.end(resultError(`not path:[${path}]`));
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const messages = await handleServer(req, res);
|
|
149
|
+
if (!handle) {
|
|
150
|
+
res.end(resultError('no handle'));
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
try {
|
|
154
|
+
const end = await handle(messages as any, { req, res });
|
|
155
|
+
if (res.writableEnded) {
|
|
156
|
+
// 如果响应已经结束,则不进行任何操作
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
160
|
+
if (typeof end === 'string') {
|
|
161
|
+
res.end(end);
|
|
162
|
+
} else {
|
|
163
|
+
res.end(JSON.stringify(end));
|
|
164
|
+
}
|
|
165
|
+
} catch (e) {
|
|
166
|
+
console.error(e);
|
|
167
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
168
|
+
if (e.code && typeof e.code === 'number') {
|
|
169
|
+
res.end(resultError(e.message || `Router Server error`, e.code));
|
|
170
|
+
} else {
|
|
171
|
+
res.end(resultError('Router Server error'));
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
};
|
|
175
|
+
this._callback = _callback;
|
|
176
|
+
return _callback;
|
|
177
|
+
}
|
|
178
|
+
on(listener: OnListener) {
|
|
179
|
+
this.listeners = [];
|
|
180
|
+
if (typeof listener === 'function') {
|
|
181
|
+
this.listeners.push({ fun: listener });
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
if (Array.isArray(listener)) {
|
|
185
|
+
for (const item of listener) {
|
|
186
|
+
if (typeof item === 'function') {
|
|
187
|
+
this.listeners.push({ fun: item });
|
|
188
|
+
} else {
|
|
189
|
+
this.listeners.push(item);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
} else {
|
|
193
|
+
this.listeners.push(listener);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
async onWebSocket({ ws, message, pathname, token, id }) {
|
|
197
|
+
const listener = this.listeners.find((item) => item.path === pathname && item.io);
|
|
198
|
+
const data: any = parseIfJson(message);
|
|
199
|
+
|
|
200
|
+
if (listener) {
|
|
201
|
+
const end = (data: any) => {
|
|
202
|
+
ws.send(JSON.stringify(data));
|
|
203
|
+
}
|
|
204
|
+
listener.fun({
|
|
205
|
+
data,
|
|
206
|
+
token,
|
|
207
|
+
id,
|
|
208
|
+
ws,
|
|
209
|
+
}, { end });
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (typeof data === 'string') {
|
|
214
|
+
const cleanMessage = data.trim().replace(/^["']|["']$/g, '');
|
|
215
|
+
if (cleanMessage === 'close') {
|
|
216
|
+
ws.close();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (cleanMessage === 'ping') {
|
|
220
|
+
ws.send('pong');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const { type, data: typeData, ...rest } = data;
|
|
226
|
+
if (!type) {
|
|
227
|
+
ws.send(JSON.stringify({ code: 500, message: 'type is required' }));
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const res = {
|
|
232
|
+
type,
|
|
233
|
+
data: {} as any,
|
|
234
|
+
...rest,
|
|
235
|
+
};
|
|
236
|
+
const end = (data: any, all?: Record<string, any>) => {
|
|
237
|
+
const result = {
|
|
238
|
+
...res,
|
|
239
|
+
data,
|
|
240
|
+
...all,
|
|
241
|
+
};
|
|
242
|
+
ws.send(JSON.stringify(result));
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
// 调用 handle 处理消息
|
|
247
|
+
if (type === 'router' && this.handle) {
|
|
248
|
+
try {
|
|
249
|
+
const result = await this.handle(typeData as any);
|
|
250
|
+
end(result);
|
|
251
|
+
} catch (e: any) {
|
|
252
|
+
if (e.code && typeof e.code === 'number') {
|
|
253
|
+
end({
|
|
254
|
+
code: e.code,
|
|
255
|
+
message: e.message,
|
|
256
|
+
});
|
|
257
|
+
} else {
|
|
258
|
+
end({ code: 500, message: 'Router Server error' });
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
} else {
|
|
262
|
+
end({ code: 500, message: `${type} server is error` });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @title Bun Server Implementation
|
|
3
|
+
* @description Bun 服务器实现,提供基于 Bun.serve 的 HTTP 和 WebSocket 功能
|
|
4
|
+
* @tags bun, server, websocket, http
|
|
5
|
+
* @createdAt 2025-12-20
|
|
6
|
+
*/
|
|
7
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
8
|
+
import { ServerType, type ServerOpts, type Cors, Listener } from './server-type.ts';
|
|
9
|
+
import { handleServer } from './handle-server.ts';
|
|
10
|
+
import { ServerBase } from './server-base.ts';
|
|
11
|
+
import { parseIfJson } from '../utils/parse.ts';
|
|
12
|
+
|
|
13
|
+
const resultError = (error: string, code = 500) => {
|
|
14
|
+
const r = {
|
|
15
|
+
code: code,
|
|
16
|
+
message: error,
|
|
17
|
+
};
|
|
18
|
+
return JSON.stringify(r);
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export class BunServer extends ServerBase implements ServerType {
|
|
22
|
+
declare _server: any;
|
|
23
|
+
declare _callback: any;
|
|
24
|
+
declare cors: Cors;
|
|
25
|
+
constructor(opts?: ServerOpts) {
|
|
26
|
+
super(opts);
|
|
27
|
+
}
|
|
28
|
+
customListen(...args: any[]): void {
|
|
29
|
+
this.listenWithBun(...args);
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Bun 运行时的 listen 实现
|
|
33
|
+
*/
|
|
34
|
+
private listenWithBun(...args: any[]) {
|
|
35
|
+
// @ts-ignore - Bun 全局 API
|
|
36
|
+
if (typeof Bun === 'undefined' || !Bun.serve) {
|
|
37
|
+
throw new Error('Bun runtime not detected');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let port: number = 3000;
|
|
41
|
+
let hostname: string = 'localhost';
|
|
42
|
+
let callback: (() => void) | undefined;
|
|
43
|
+
|
|
44
|
+
// 解析参数
|
|
45
|
+
if (typeof args[0] === 'number') {
|
|
46
|
+
port = args[0];
|
|
47
|
+
if (typeof args[1] === 'string') {
|
|
48
|
+
hostname = args[1];
|
|
49
|
+
callback = args[2] || args[3];
|
|
50
|
+
} else if (typeof args[1] === 'function') {
|
|
51
|
+
callback = args[1];
|
|
52
|
+
} else {
|
|
53
|
+
callback = args[2];
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const requestCallback = this.createCallback();
|
|
58
|
+
const wsPath = this.path;
|
|
59
|
+
// @ts-ignore
|
|
60
|
+
this._server = Bun.serve({
|
|
61
|
+
port,
|
|
62
|
+
hostname,
|
|
63
|
+
idleTimeout: 0, // 4 minutes idle timeout (max 255 seconds)
|
|
64
|
+
fetch: async (request: Request, server: any) => {
|
|
65
|
+
const host = request.headers.get('host') || 'localhost';
|
|
66
|
+
const url = new URL(request.url, `http://${host}`);
|
|
67
|
+
// 处理 WebSocket 升级请求
|
|
68
|
+
if (request.headers.get('upgrade') === 'websocket') {
|
|
69
|
+
const listenPath = this.listeners.map((item) => item.path).filter((item) => item);
|
|
70
|
+
if (listenPath.includes(url.pathname) || url.pathname === wsPath) {
|
|
71
|
+
const token = url.searchParams.get('token') || '';
|
|
72
|
+
const id = url.searchParams.get('id') || '';
|
|
73
|
+
const upgraded = server.upgrade(request, {
|
|
74
|
+
data: { url: url, pathname: url.pathname, token, id },
|
|
75
|
+
});
|
|
76
|
+
if (upgraded) {
|
|
77
|
+
return undefined; // WebSocket 连接成功
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return new Response('WebSocket upgrade failed', { status: 400 });
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 将 Bun 的 Request 转换为 Node.js 风格的 req/res
|
|
84
|
+
return new Promise((resolve) => {
|
|
85
|
+
const req: any = {
|
|
86
|
+
url: url.pathname + url.search,
|
|
87
|
+
method: request.method,
|
|
88
|
+
headers: Object.fromEntries(request.headers.entries()),
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const res: any = {
|
|
92
|
+
statusCode: 200,
|
|
93
|
+
headersSent: false,
|
|
94
|
+
writableEnded: false,
|
|
95
|
+
_headers: {} as Record<string, string | string[]>,
|
|
96
|
+
writeHead(statusCode: number, headers: Record<string, string | string[]>) {
|
|
97
|
+
this.statusCode = statusCode;
|
|
98
|
+
for (const key in headers) {
|
|
99
|
+
this._headers[key] = headers[key];
|
|
100
|
+
}
|
|
101
|
+
this.headersSent = true;
|
|
102
|
+
},
|
|
103
|
+
setHeader(name: string, value: string | string[]) {
|
|
104
|
+
this._headers[name] = value;
|
|
105
|
+
},
|
|
106
|
+
cookie(name: string, value: string, options?: any) {
|
|
107
|
+
let cookieString = `${name}=${value}`;
|
|
108
|
+
if (options) {
|
|
109
|
+
if (options.maxAge) {
|
|
110
|
+
cookieString += `; Max-Age=${options.maxAge}`;
|
|
111
|
+
}
|
|
112
|
+
if (options.domain) {
|
|
113
|
+
cookieString += `; Domain=${options.domain}`;
|
|
114
|
+
}
|
|
115
|
+
if (options.path) {
|
|
116
|
+
cookieString += `; Path=${options.path}`;
|
|
117
|
+
}
|
|
118
|
+
if (options.expires) {
|
|
119
|
+
cookieString += `; Expires=${options.expires.toUTCString()}`;
|
|
120
|
+
}
|
|
121
|
+
if (options.httpOnly) {
|
|
122
|
+
cookieString += `; HttpOnly`;
|
|
123
|
+
}
|
|
124
|
+
if (options.secure) {
|
|
125
|
+
cookieString += `; Secure`;
|
|
126
|
+
}
|
|
127
|
+
if (options.sameSite) {
|
|
128
|
+
cookieString += `; SameSite=${options.sameSite}`;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
this.setHeader('Set-Cookie', cookieString);
|
|
132
|
+
},
|
|
133
|
+
end(data?: string) {
|
|
134
|
+
this.writableEnded = true;
|
|
135
|
+
resolve(
|
|
136
|
+
new Response(data, {
|
|
137
|
+
status: this.statusCode,
|
|
138
|
+
headers: this._headers as any,
|
|
139
|
+
})
|
|
140
|
+
);
|
|
141
|
+
},
|
|
142
|
+
};
|
|
143
|
+
// 处理请求体
|
|
144
|
+
if (request.method !== 'GET' && request.method !== 'HEAD') {
|
|
145
|
+
request.text().then((body) => {
|
|
146
|
+
(req as any).body = body;
|
|
147
|
+
requestCallback(req, res);
|
|
148
|
+
});
|
|
149
|
+
} else {
|
|
150
|
+
requestCallback(req, res);
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
},
|
|
154
|
+
websocket: {
|
|
155
|
+
open: (ws: any) => {
|
|
156
|
+
ws.send('connected');
|
|
157
|
+
},
|
|
158
|
+
message: async (ws: any, message: string | Buffer) => {
|
|
159
|
+
const pathname = ws.data.pathname || '';
|
|
160
|
+
const token = ws.data.token || '';
|
|
161
|
+
const id = ws.data.id || '';
|
|
162
|
+
await this.onWebSocket({ ws, message, pathname, token, id });
|
|
163
|
+
},
|
|
164
|
+
close: (ws: any) => {
|
|
165
|
+
// WebSocket 连接关闭
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (callback) {
|
|
171
|
+
callback();
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|