@kevisual/router 0.0.27 → 0.0.29
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.d.ts +21 -4
- package/dist/router-browser.js +56 -0
- package/dist/router.d.ts +21 -4
- package/dist/router.js +56 -0
- package/package.json +2 -2
- package/src/auto/call-sock.ts +164 -0
- package/src/auto/listen/cleanup.ts +102 -0
- package/src/auto/listen/run-check.ts +51 -0
- package/src/auto/listen/server-time.ts +33 -0
- package/src/auto/listen-sock.ts +274 -0
- package/src/auto/load-ts.ts +38 -0
- package/src/auto/runtime.ts +19 -0
- package/src/auto/utils/glob.ts +83 -0
- package/src/route.ts +22 -10
- package/src/utils/listen-process.ts +50 -0
- package/src/validator/index.ts +5 -1
- package/src/validator/rule.ts +2 -3
package/dist/router-browser.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { Schema } from 'zod';
|
|
1
|
+
import { z } from 'zod';
|
|
3
2
|
import * as querystring from 'querystring';
|
|
4
3
|
import { IncomingMessage } from 'node:http';
|
|
5
4
|
import { RouteOpts as RouteOpts$1, QueryRouterServer as QueryRouterServer$1, RouteMiddleware as RouteMiddleware$1, Run as Run$1 } from '@kevisual/router';
|
|
@@ -38,7 +37,9 @@ type RuleAny = {
|
|
|
38
37
|
type: 'any';
|
|
39
38
|
} & BaseRule;
|
|
40
39
|
type Rule = RuleString | RuleNumber | RuleBoolean | RuleArray | RuleObject | RuleAny;
|
|
41
|
-
declare const createSchema: (rule: Rule) =>
|
|
40
|
+
declare const createSchema: (rule: Rule) => z.ZodType<any, any, any>;
|
|
41
|
+
|
|
42
|
+
type Schema = z.ZodType<any, any, any>;
|
|
42
43
|
|
|
43
44
|
type RouterContextT = {
|
|
44
45
|
code?: number;
|
|
@@ -359,6 +360,22 @@ declare class QueryRouter {
|
|
|
359
360
|
hasRoute(path: string, key?: string): Route<{
|
|
360
361
|
[key: string]: any;
|
|
361
362
|
}>;
|
|
363
|
+
/**
|
|
364
|
+
* 等待程序运行, 获取到message的数据,就执行
|
|
365
|
+
*
|
|
366
|
+
* emitter = process
|
|
367
|
+
* -- .exit
|
|
368
|
+
* -- .on
|
|
369
|
+
* -- .send
|
|
370
|
+
*/
|
|
371
|
+
wait(params?: {
|
|
372
|
+
path?: string;
|
|
373
|
+
key?: string;
|
|
374
|
+
payload?: any;
|
|
375
|
+
}, opts?: {
|
|
376
|
+
emitter?: any;
|
|
377
|
+
timeout?: number;
|
|
378
|
+
}): Promise<void>;
|
|
362
379
|
}
|
|
363
380
|
type QueryRouterServerOpts = {
|
|
364
381
|
handleFn?: HandleFn;
|
|
@@ -513,4 +530,4 @@ declare class QueryUtil<T extends RouteObject = RouteObject> {
|
|
|
513
530
|
declare const App: typeof QueryRouterServer;
|
|
514
531
|
|
|
515
532
|
export { App, CustomError, QueryRouter, QueryRouterServer, QueryUtil, Route, createSchema, define, parseBody, parseSearch, parseSearchValue, util };
|
|
516
|
-
export type { RouteArray, RouteContext, RouteObject, RouteOpts, Rule, Run };
|
|
533
|
+
export type { RouteArray, RouteContext, RouteObject, RouteOpts, Rule, Run, Schema };
|
package/dist/router-browser.js
CHANGED
|
@@ -4743,6 +4743,51 @@ function get(object, path, defaultValue) {
|
|
|
4743
4743
|
return result === undefined ? defaultValue : result;
|
|
4744
4744
|
}
|
|
4745
4745
|
|
|
4746
|
+
const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 * 60 * 1000 }) => {
|
|
4747
|
+
const process = emitter || globalThis.process;
|
|
4748
|
+
let isEnd = false;
|
|
4749
|
+
const timer = setTimeout(() => {
|
|
4750
|
+
if (isEnd)
|
|
4751
|
+
return;
|
|
4752
|
+
isEnd = true;
|
|
4753
|
+
process.send?.({ success: false, error: 'Timeout' });
|
|
4754
|
+
process.exit?.(1);
|
|
4755
|
+
}, timeout);
|
|
4756
|
+
// 监听来自主进程的消息
|
|
4757
|
+
const getParams = async () => {
|
|
4758
|
+
return new Promise((resolve) => {
|
|
4759
|
+
process.on('message', (msg) => {
|
|
4760
|
+
if (isEnd)
|
|
4761
|
+
return;
|
|
4762
|
+
isEnd = true;
|
|
4763
|
+
clearTimeout(timer);
|
|
4764
|
+
resolve(msg);
|
|
4765
|
+
});
|
|
4766
|
+
});
|
|
4767
|
+
};
|
|
4768
|
+
try {
|
|
4769
|
+
const { path = 'main', ...rest } = await getParams();
|
|
4770
|
+
// 执行主要逻辑
|
|
4771
|
+
const result = await app.queryRoute({ path, ...rest, ...params });
|
|
4772
|
+
// 发送结果回主进程
|
|
4773
|
+
const response = {
|
|
4774
|
+
success: true,
|
|
4775
|
+
data: result,
|
|
4776
|
+
timestamp: new Date().toISOString()
|
|
4777
|
+
};
|
|
4778
|
+
process.send?.(response, (error) => {
|
|
4779
|
+
process.exit?.(0);
|
|
4780
|
+
});
|
|
4781
|
+
}
|
|
4782
|
+
catch (error) {
|
|
4783
|
+
process.send?.({
|
|
4784
|
+
success: false,
|
|
4785
|
+
error: error.message
|
|
4786
|
+
});
|
|
4787
|
+
process.exit?.(1);
|
|
4788
|
+
}
|
|
4789
|
+
};
|
|
4790
|
+
|
|
4746
4791
|
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware'];
|
|
4747
4792
|
class Route {
|
|
4748
4793
|
/**
|
|
@@ -5304,6 +5349,17 @@ class QueryRouter {
|
|
|
5304
5349
|
hasRoute(path, key = '') {
|
|
5305
5350
|
return this.routes.find((r) => r.path === path && r.key === key);
|
|
5306
5351
|
}
|
|
5352
|
+
/**
|
|
5353
|
+
* 等待程序运行, 获取到message的数据,就执行
|
|
5354
|
+
*
|
|
5355
|
+
* emitter = process
|
|
5356
|
+
* -- .exit
|
|
5357
|
+
* -- .on
|
|
5358
|
+
* -- .send
|
|
5359
|
+
*/
|
|
5360
|
+
wait(params, opts) {
|
|
5361
|
+
return listenProcess({ app: this, params, ...opts });
|
|
5362
|
+
}
|
|
5307
5363
|
}
|
|
5308
5364
|
/**
|
|
5309
5365
|
* QueryRouterServer
|
package/dist/router.d.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { Schema } from 'zod';
|
|
1
|
+
import { z } from 'zod';
|
|
3
2
|
import http, { IncomingMessage, ServerResponse } from 'node:http';
|
|
4
3
|
import https from 'node:https';
|
|
5
4
|
import http2 from 'node:http2';
|
|
@@ -42,7 +41,9 @@ type RuleAny = {
|
|
|
42
41
|
type: 'any';
|
|
43
42
|
} & BaseRule;
|
|
44
43
|
type Rule = RuleString | RuleNumber | RuleBoolean | RuleArray | RuleObject | RuleAny;
|
|
45
|
-
declare const createSchema: (rule: Rule) =>
|
|
44
|
+
declare const createSchema: (rule: Rule) => z.ZodType<any, any, any>;
|
|
45
|
+
|
|
46
|
+
type Schema = z.ZodType<any, any, any>;
|
|
46
47
|
|
|
47
48
|
type RouterContextT = {
|
|
48
49
|
code?: number;
|
|
@@ -363,6 +364,22 @@ declare class QueryRouter {
|
|
|
363
364
|
hasRoute(path: string, key?: string): Route<{
|
|
364
365
|
[key: string]: any;
|
|
365
366
|
}>;
|
|
367
|
+
/**
|
|
368
|
+
* 等待程序运行, 获取到message的数据,就执行
|
|
369
|
+
*
|
|
370
|
+
* emitter = process
|
|
371
|
+
* -- .exit
|
|
372
|
+
* -- .on
|
|
373
|
+
* -- .send
|
|
374
|
+
*/
|
|
375
|
+
wait(params?: {
|
|
376
|
+
path?: string;
|
|
377
|
+
key?: string;
|
|
378
|
+
payload?: any;
|
|
379
|
+
}, opts?: {
|
|
380
|
+
emitter?: any;
|
|
381
|
+
timeout?: number;
|
|
382
|
+
}): Promise<void>;
|
|
366
383
|
}
|
|
367
384
|
type QueryRouterServerOpts = {
|
|
368
385
|
handleFn?: HandleFn;
|
|
@@ -728,4 +745,4 @@ declare class App<T = {}, U = AppReqRes> {
|
|
|
728
745
|
}
|
|
729
746
|
|
|
730
747
|
export { App, Connect, CustomError, QueryConnect, QueryRouter, QueryRouterServer, QueryUtil, Route, Server, createSchema, define, handleServer, util };
|
|
731
|
-
export type { RouteArray, RouteContext, RouteMiddleware, RouteObject, RouteOpts, Rule, Run };
|
|
748
|
+
export type { RouteArray, RouteContext, RouteMiddleware, RouteObject, RouteOpts, Rule, Run, Schema };
|
package/dist/router.js
CHANGED
|
@@ -4765,6 +4765,51 @@ function get(object, path, defaultValue) {
|
|
|
4765
4765
|
return result === undefined ? defaultValue : result;
|
|
4766
4766
|
}
|
|
4767
4767
|
|
|
4768
|
+
const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 * 60 * 1000 }) => {
|
|
4769
|
+
const process = emitter || globalThis.process;
|
|
4770
|
+
let isEnd = false;
|
|
4771
|
+
const timer = setTimeout(() => {
|
|
4772
|
+
if (isEnd)
|
|
4773
|
+
return;
|
|
4774
|
+
isEnd = true;
|
|
4775
|
+
process.send?.({ success: false, error: 'Timeout' });
|
|
4776
|
+
process.exit?.(1);
|
|
4777
|
+
}, timeout);
|
|
4778
|
+
// 监听来自主进程的消息
|
|
4779
|
+
const getParams = async () => {
|
|
4780
|
+
return new Promise((resolve) => {
|
|
4781
|
+
process.on('message', (msg) => {
|
|
4782
|
+
if (isEnd)
|
|
4783
|
+
return;
|
|
4784
|
+
isEnd = true;
|
|
4785
|
+
clearTimeout(timer);
|
|
4786
|
+
resolve(msg);
|
|
4787
|
+
});
|
|
4788
|
+
});
|
|
4789
|
+
};
|
|
4790
|
+
try {
|
|
4791
|
+
const { path = 'main', ...rest } = await getParams();
|
|
4792
|
+
// 执行主要逻辑
|
|
4793
|
+
const result = await app.queryRoute({ path, ...rest, ...params });
|
|
4794
|
+
// 发送结果回主进程
|
|
4795
|
+
const response = {
|
|
4796
|
+
success: true,
|
|
4797
|
+
data: result,
|
|
4798
|
+
timestamp: new Date().toISOString()
|
|
4799
|
+
};
|
|
4800
|
+
process.send?.(response, (error) => {
|
|
4801
|
+
process.exit?.(0);
|
|
4802
|
+
});
|
|
4803
|
+
}
|
|
4804
|
+
catch (error) {
|
|
4805
|
+
process.send?.({
|
|
4806
|
+
success: false,
|
|
4807
|
+
error: error.message
|
|
4808
|
+
});
|
|
4809
|
+
process.exit?.(1);
|
|
4810
|
+
}
|
|
4811
|
+
};
|
|
4812
|
+
|
|
4768
4813
|
const pickValue = ['path', 'key', 'id', 'description', 'type', 'validator', 'middleware'];
|
|
4769
4814
|
class Route {
|
|
4770
4815
|
/**
|
|
@@ -5326,6 +5371,17 @@ class QueryRouter {
|
|
|
5326
5371
|
hasRoute(path, key = '') {
|
|
5327
5372
|
return this.routes.find((r) => r.path === path && r.key === key);
|
|
5328
5373
|
}
|
|
5374
|
+
/**
|
|
5375
|
+
* 等待程序运行, 获取到message的数据,就执行
|
|
5376
|
+
*
|
|
5377
|
+
* emitter = process
|
|
5378
|
+
* -- .exit
|
|
5379
|
+
* -- .on
|
|
5380
|
+
* -- .send
|
|
5381
|
+
*/
|
|
5382
|
+
wait(params, opts) {
|
|
5383
|
+
return listenProcess({ app: this, params, ...opts });
|
|
5384
|
+
}
|
|
5329
5385
|
}
|
|
5330
5386
|
/**
|
|
5331
5387
|
* QueryRouterServer
|
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.29",
|
|
5
5
|
"description": "",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./dist/router.js",
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
"@kevisual/local-proxy": "^0.0.6",
|
|
25
25
|
"@kevisual/query": "^0.0.29",
|
|
26
26
|
"@rollup/plugin-alias": "^5.1.1",
|
|
27
|
-
"@rollup/plugin-commonjs": "
|
|
27
|
+
"@rollup/plugin-commonjs": "28.0.6",
|
|
28
28
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
29
29
|
"@rollup/plugin-typescript": "^12.1.4",
|
|
30
30
|
"@types/lodash-es": "^4.17.12",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createConnection } from 'node:net';
|
|
2
|
+
|
|
3
|
+
type QueryData = {
|
|
4
|
+
path?: string;
|
|
5
|
+
key?: string;
|
|
6
|
+
payload?: any;
|
|
7
|
+
[key: string]: any;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type CallSockOptions = {
|
|
11
|
+
socketPath?: string;
|
|
12
|
+
timeout?: number;
|
|
13
|
+
method?: 'GET' | 'POST';
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const callSock = async (data: QueryData, options: CallSockOptions = {}): Promise<any> => {
|
|
17
|
+
const { socketPath = './app.sock', timeout = 10000, method = 'POST' } = options;
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const client = createConnection(socketPath);
|
|
21
|
+
let responseData = '';
|
|
22
|
+
let timer: NodeJS.Timeout;
|
|
23
|
+
|
|
24
|
+
// 设置超时
|
|
25
|
+
if (timeout > 0) {
|
|
26
|
+
timer = setTimeout(() => {
|
|
27
|
+
client.destroy();
|
|
28
|
+
reject(new Error(`Socket call timeout after ${timeout}ms`));
|
|
29
|
+
}, timeout);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
client.on('connect', () => {
|
|
33
|
+
try {
|
|
34
|
+
let request: string;
|
|
35
|
+
|
|
36
|
+
if (method === 'GET') {
|
|
37
|
+
// GET 请求:参数放在 URL 中
|
|
38
|
+
const searchParams = new URLSearchParams();
|
|
39
|
+
Object.entries(data).forEach(([key, value]) => {
|
|
40
|
+
if (key === 'payload' && typeof value === 'object') {
|
|
41
|
+
searchParams.append(key, JSON.stringify(value));
|
|
42
|
+
} else {
|
|
43
|
+
searchParams.append(key, String(value));
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const queryString = searchParams.toString();
|
|
48
|
+
const url = queryString ? `/?${queryString}` : '/';
|
|
49
|
+
|
|
50
|
+
request = [`GET ${url} HTTP/1.1`, 'Host: localhost', 'Connection: close', '', ''].join('\r\n');
|
|
51
|
+
} else {
|
|
52
|
+
// POST 请求:数据放在 body 中
|
|
53
|
+
const body = JSON.stringify(data);
|
|
54
|
+
const contentLength = Buffer.byteLength(body, 'utf8');
|
|
55
|
+
|
|
56
|
+
request = [
|
|
57
|
+
'POST / HTTP/1.1',
|
|
58
|
+
'Host: localhost',
|
|
59
|
+
'Content-Type: application/json',
|
|
60
|
+
`Content-Length: ${contentLength}`,
|
|
61
|
+
'Connection: close',
|
|
62
|
+
'',
|
|
63
|
+
body,
|
|
64
|
+
].join('\r\n');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
client.write(request);
|
|
68
|
+
} catch (error) {
|
|
69
|
+
if (timer) clearTimeout(timer);
|
|
70
|
+
client.destroy();
|
|
71
|
+
reject(error);
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
client.on('data', (chunk) => {
|
|
76
|
+
responseData += chunk.toString();
|
|
77
|
+
|
|
78
|
+
// 检查是否收到完整的HTTP响应
|
|
79
|
+
if (responseData.includes('\r\n\r\n')) {
|
|
80
|
+
const [headerSection] = responseData.split('\r\n\r\n');
|
|
81
|
+
const contentLengthMatch = headerSection.match(/content-length:\s*(\d+)/i);
|
|
82
|
+
|
|
83
|
+
if (contentLengthMatch) {
|
|
84
|
+
const expectedLength = parseInt(contentLengthMatch[1]);
|
|
85
|
+
const bodyStart = responseData.indexOf('\r\n\r\n') + 4;
|
|
86
|
+
const currentBodyLength = Buffer.byteLength(responseData.slice(bodyStart), 'utf8');
|
|
87
|
+
|
|
88
|
+
// 如果收到了完整的响应,主动关闭连接
|
|
89
|
+
if (currentBodyLength >= expectedLength) {
|
|
90
|
+
client.end();
|
|
91
|
+
}
|
|
92
|
+
} else if (responseData.includes('\r\n0\r\n\r\n')) {
|
|
93
|
+
// 检查 chunked 编码结束标记
|
|
94
|
+
client.end();
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
client.on('end', () => {
|
|
100
|
+
if (timer) clearTimeout(timer);
|
|
101
|
+
|
|
102
|
+
try {
|
|
103
|
+
// 解析 HTTP 响应
|
|
104
|
+
const response = parseHttpResponse(responseData);
|
|
105
|
+
|
|
106
|
+
if (response.statusCode >= 400) {
|
|
107
|
+
reject(new Error(`HTTP ${response.statusCode}: ${response.body}`));
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// 尝试解析 JSON 响应
|
|
112
|
+
try {
|
|
113
|
+
const result = JSON.parse(response.body);
|
|
114
|
+
resolve(result);
|
|
115
|
+
} catch {
|
|
116
|
+
// 如果不是 JSON,直接返回文本
|
|
117
|
+
resolve(response.body);
|
|
118
|
+
}
|
|
119
|
+
} catch (error) {
|
|
120
|
+
reject(error);
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
client.on('error', (error) => {
|
|
125
|
+
if (timer) clearTimeout(timer);
|
|
126
|
+
reject(error);
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
client.on('timeout', () => {
|
|
130
|
+
if (timer) clearTimeout(timer);
|
|
131
|
+
client.destroy();
|
|
132
|
+
reject(new Error('Socket connection timeout'));
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
// 解析 HTTP 响应的辅助函数
|
|
138
|
+
function parseHttpResponse(responseData: string) {
|
|
139
|
+
const [headerSection, ...bodyParts] = responseData.split('\r\n\r\n');
|
|
140
|
+
const body = bodyParts.join('\r\n\r\n');
|
|
141
|
+
|
|
142
|
+
const lines = headerSection.split('\r\n');
|
|
143
|
+
const statusLine = lines[0];
|
|
144
|
+
const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d+)/);
|
|
145
|
+
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 200;
|
|
146
|
+
|
|
147
|
+
const headers: Record<string, string> = {};
|
|
148
|
+
for (let i = 1; i < lines.length; i++) {
|
|
149
|
+
const [key, ...valueParts] = lines[i].split(':');
|
|
150
|
+
if (key && valueParts.length > 0) {
|
|
151
|
+
headers[key.trim().toLowerCase()] = valueParts.join(':').trim();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
statusCode,
|
|
157
|
+
headers,
|
|
158
|
+
body: body || '',
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export const autoCall = (data: QueryData, options?: Omit<CallSockOptions, 'method'>) => {
|
|
163
|
+
return callSock(data, { ...options, method: 'POST' });
|
|
164
|
+
};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { getRuntime } from '../runtime.ts';
|
|
2
|
+
|
|
3
|
+
let isClean = false;
|
|
4
|
+
export const deleteFileDetached = async (path: string, pidPath: string = './app.pid') => {
|
|
5
|
+
const runtime = getRuntime();
|
|
6
|
+
if (runtime.isDeno) {
|
|
7
|
+
// Deno 实现 - 启动后不等待结果
|
|
8
|
+
const process = new Deno.Command('sh', {
|
|
9
|
+
args: ['-c', `rm -f "${path}" & rm -f "${pidPath}"`],
|
|
10
|
+
stdout: 'null',
|
|
11
|
+
stderr: 'null',
|
|
12
|
+
});
|
|
13
|
+
process.spawn(); // 不等待结果
|
|
14
|
+
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
const { spawn } = await import('node:child_process');
|
|
18
|
+
const child = spawn('sh', ['-c', `rm -f "${path}" & rm -f "${pidPath}"`], {
|
|
19
|
+
detached: true,
|
|
20
|
+
stdio: 'ignore',
|
|
21
|
+
});
|
|
22
|
+
child.unref(); // 完全分离
|
|
23
|
+
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type CleanupOptions = {
|
|
27
|
+
path: string;
|
|
28
|
+
close?: () => Promise<void>;
|
|
29
|
+
pidPath?: string;
|
|
30
|
+
};
|
|
31
|
+
export const cleanup = async ({ path, close = async () => {}, pidPath = './app.pid' }: CleanupOptions) => {
|
|
32
|
+
const runtime = getRuntime();
|
|
33
|
+
|
|
34
|
+
// 检查文件是否存在并删除
|
|
35
|
+
const cleanupFile = async () => {
|
|
36
|
+
if (isClean) return;
|
|
37
|
+
isClean = true;
|
|
38
|
+
if (runtime.isDeno) {
|
|
39
|
+
await deleteFileDetached(path, pidPath);
|
|
40
|
+
}
|
|
41
|
+
await close();
|
|
42
|
+
if (!runtime.isDeno) {
|
|
43
|
+
await deleteFileDetached(path, pidPath);
|
|
44
|
+
}
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// 根据运行时环境注册不同的退出监听器
|
|
48
|
+
if (runtime.isDeno) {
|
|
49
|
+
// Deno 环境
|
|
50
|
+
const handleSignal = () => {
|
|
51
|
+
cleanupFile();
|
|
52
|
+
Deno.exit(0);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
try {
|
|
56
|
+
Deno.addSignalListener('SIGINT', handleSignal);
|
|
57
|
+
Deno.addSignalListener('SIGTERM', handleSignal);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
console.warn('[DEBUG] Failed to add signal listeners:', error);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 对于 beforeunload 和 unload,使用异步清理
|
|
63
|
+
const handleUnload = () => {
|
|
64
|
+
cleanupFile();
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
globalThis.addEventListener('beforeunload', handleUnload);
|
|
68
|
+
globalThis.addEventListener('unload', handleUnload);
|
|
69
|
+
} else if (runtime.isNode || runtime.isBun) {
|
|
70
|
+
// Node.js 和 Bun 环境
|
|
71
|
+
import('process').then(({ default: process }) => {
|
|
72
|
+
// 信号处理使用同步清理,然后退出
|
|
73
|
+
const signalHandler = async (signal: string) => {
|
|
74
|
+
await cleanupFile();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
process.on('SIGINT', () => signalHandler('SIGINT'));
|
|
79
|
+
process.on('SIGTERM', () => signalHandler('SIGTERM'));
|
|
80
|
+
process.on('SIGUSR1', () => signalHandler('SIGUSR1'));
|
|
81
|
+
process.on('SIGUSR2', () => signalHandler('SIGUSR2'));
|
|
82
|
+
|
|
83
|
+
process.on('exit', async () => {
|
|
84
|
+
await cleanupFile();
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
process.on('uncaughtException', async (error) => {
|
|
88
|
+
console.error('Uncaught Exception:', error);
|
|
89
|
+
await cleanupFile();
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
process.on('unhandledRejection', async (reason, promise) => {
|
|
94
|
+
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
95
|
+
await cleanupFile();
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// 返回手动清理函数,以便需要时主动调用
|
|
101
|
+
return cleanupFile;
|
|
102
|
+
};
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { getRuntime } from '../runtime.ts';
|
|
2
|
+
|
|
3
|
+
export const getPid = async () => {
|
|
4
|
+
const runtime = getRuntime();
|
|
5
|
+
|
|
6
|
+
let pid = 0;
|
|
7
|
+
if (runtime.isDeno) {
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
pid = Deno.pid;
|
|
10
|
+
} else {
|
|
11
|
+
pid = process.pid;
|
|
12
|
+
}
|
|
13
|
+
return pid;
|
|
14
|
+
};
|
|
15
|
+
export const writeAppid = async (pidPath = './app.pid') => {
|
|
16
|
+
const fs = await import('node:fs');
|
|
17
|
+
const pid = await getPid();
|
|
18
|
+
fs.writeFileSync(pidPath, pid + '');
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const getPidFromFileAndStop = async () => {
|
|
22
|
+
const fs = await import('node:fs');
|
|
23
|
+
if (fs.existsSync('./app.pid')) {
|
|
24
|
+
const pid = parseInt(fs.readFileSync('./app.pid', 'utf-8'), 10);
|
|
25
|
+
if (!isNaN(pid)) {
|
|
26
|
+
if (pid === 0) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
try {
|
|
30
|
+
process.kill(pid);
|
|
31
|
+
console.log(`Stopped process with PID ${pid}`);
|
|
32
|
+
} catch (error) {
|
|
33
|
+
console.error(`Failed to stop process with PID ${pid}:`);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export const runFirstCheck = async (path: string, pidPath: string) => {
|
|
40
|
+
await getPidFromFileAndStop();
|
|
41
|
+
await writeAppid(pidPath);
|
|
42
|
+
try {
|
|
43
|
+
const fs = await import('node:fs');
|
|
44
|
+
if (fs.existsSync(path)) {
|
|
45
|
+
fs.unlinkSync(path);
|
|
46
|
+
console.log(`Socket file ${path} cleaned up during first check`);
|
|
47
|
+
}
|
|
48
|
+
} catch (error) {
|
|
49
|
+
console.error(`Failed to clean up socket file ${path} during first check:`, error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export class ServerTimer {
|
|
2
|
+
updatedAt: number;
|
|
3
|
+
timer: any;
|
|
4
|
+
timeout: number;
|
|
5
|
+
onTimeout: any;
|
|
6
|
+
interval = 10 * 1000;
|
|
7
|
+
constructor(opts?: { timeout?: number }) {
|
|
8
|
+
this.timeout = opts?.timeout || 15 * 60 * 1000;
|
|
9
|
+
this.run();
|
|
10
|
+
}
|
|
11
|
+
startTimer() {
|
|
12
|
+
const that = this;
|
|
13
|
+
if (this.timer) {
|
|
14
|
+
clearInterval(this.timer);
|
|
15
|
+
}
|
|
16
|
+
this.timer = setInterval(() => {
|
|
17
|
+
const updatedAt = Date.now();
|
|
18
|
+
const timeout = that.timeout;
|
|
19
|
+
const onTimeout = that.onTimeout;
|
|
20
|
+
const isExpired = updatedAt - that.updatedAt > timeout;
|
|
21
|
+
if (isExpired) {
|
|
22
|
+
onTimeout?.();
|
|
23
|
+
clearInterval(that.timer);
|
|
24
|
+
that.timer = null;
|
|
25
|
+
}
|
|
26
|
+
}, that.interval);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
run(): number {
|
|
30
|
+
this.updatedAt = Date.now();
|
|
31
|
+
return this.updatedAt;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
import type { IncomingMessage } from 'http';
|
|
2
|
+
import { QueryRouterServer } from '../route.ts';
|
|
3
|
+
import { getRuntime } from './runtime.ts';
|
|
4
|
+
import { runFirstCheck } from './listen/run-check.ts';
|
|
5
|
+
import { cleanup } from './listen/cleanup.ts';
|
|
6
|
+
import { ServerTimer } from './listen/server-time.ts';
|
|
7
|
+
|
|
8
|
+
type ListenSocketOptions = {
|
|
9
|
+
/**
|
|
10
|
+
* Unix socket path, defaults to './app.sock'
|
|
11
|
+
*/
|
|
12
|
+
path?: string;
|
|
13
|
+
app?: QueryRouterServer;
|
|
14
|
+
/**
|
|
15
|
+
* Unix socket path, defaults to './app.pid'
|
|
16
|
+
*/
|
|
17
|
+
pidPath?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Timeout for the server, defaults to 15 minutes.
|
|
20
|
+
* If the server is not responsive for this duration, it will be terminated
|
|
21
|
+
*/
|
|
22
|
+
timeout?: number;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const server = async (req, app: QueryRouterServer) => {
|
|
26
|
+
const runtime = getRuntime();
|
|
27
|
+
let data;
|
|
28
|
+
if (!runtime.isNode) {
|
|
29
|
+
data = await getRequestParams(req);
|
|
30
|
+
} else {
|
|
31
|
+
data = await parseBody(req);
|
|
32
|
+
}
|
|
33
|
+
// @ts-ignore
|
|
34
|
+
const serverTimer = app.serverTimer;
|
|
35
|
+
if (serverTimer) {
|
|
36
|
+
serverTimer?.run?.();
|
|
37
|
+
}
|
|
38
|
+
const result = await app.queryRoute(data as any);
|
|
39
|
+
const response = new Response(JSON.stringify(result));
|
|
40
|
+
response.headers.set('Content-Type', 'application/json');
|
|
41
|
+
return response;
|
|
42
|
+
};
|
|
43
|
+
export const closeListenSocket = () => {
|
|
44
|
+
console.log('Closing listen socket');
|
|
45
|
+
process.emit('SIGINT');
|
|
46
|
+
};
|
|
47
|
+
export const serverTimer = new ServerTimer();
|
|
48
|
+
export const listenSocket = async (options?: ListenSocketOptions) => {
|
|
49
|
+
const path = options?.path || './app.sock';
|
|
50
|
+
const pidPath = options?.pidPath || './app.pid';
|
|
51
|
+
const timeout = options?.timeout || 24 * 60 * 60 * 1000; // 24 hours
|
|
52
|
+
const runtime = getRuntime();
|
|
53
|
+
|
|
54
|
+
serverTimer.timeout = timeout;
|
|
55
|
+
serverTimer.startTimer();
|
|
56
|
+
serverTimer.onTimeout = closeListenSocket;
|
|
57
|
+
|
|
58
|
+
let app = options?.app || globalThis.context?.app;
|
|
59
|
+
if (!app) {
|
|
60
|
+
app = new QueryRouterServer();
|
|
61
|
+
}
|
|
62
|
+
app.serverTimer = serverTimer;
|
|
63
|
+
await runFirstCheck(path, pidPath);
|
|
64
|
+
let close = async () => {};
|
|
65
|
+
cleanup({ path, close });
|
|
66
|
+
if (runtime.isDeno) {
|
|
67
|
+
// 检查 Deno 版本是否支持 Unix domain socket
|
|
68
|
+
try {
|
|
69
|
+
// @ts-ignore
|
|
70
|
+
const listener = Deno.listen({
|
|
71
|
+
transport: 'unix',
|
|
72
|
+
path: path,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
// 处理连接
|
|
76
|
+
(async () => {
|
|
77
|
+
for await (const conn of listener) {
|
|
78
|
+
(async () => {
|
|
79
|
+
// @ts-ignore
|
|
80
|
+
const httpConn = Deno.serveHttp(conn);
|
|
81
|
+
for await (const requestEvent of httpConn) {
|
|
82
|
+
try {
|
|
83
|
+
const response = await server(requestEvent.request, app);
|
|
84
|
+
await requestEvent.respondWith(response);
|
|
85
|
+
} catch (error) {
|
|
86
|
+
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
}
|
|
91
|
+
})();
|
|
92
|
+
close = async () => {
|
|
93
|
+
listener.close();
|
|
94
|
+
};
|
|
95
|
+
return listener;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
// 如果 Unix socket 不支持,回退到 HTTP 服务器
|
|
98
|
+
console.warn('Unix socket not supported in this Deno environment, falling back to HTTP server');
|
|
99
|
+
|
|
100
|
+
// @ts-ignore
|
|
101
|
+
const listener = Deno.listen({ port: 0 }); // 使用随机端口
|
|
102
|
+
|
|
103
|
+
// @ts-ignore
|
|
104
|
+
console.log(`Deno server listening on port ${listener.addr.port}`);
|
|
105
|
+
|
|
106
|
+
(async () => {
|
|
107
|
+
for await (const conn of listener) {
|
|
108
|
+
(async () => {
|
|
109
|
+
// @ts-ignore
|
|
110
|
+
const httpConn = Deno.serveHttp(conn);
|
|
111
|
+
for await (const requestEvent of httpConn) {
|
|
112
|
+
try {
|
|
113
|
+
const response = await server(requestEvent.request, app);
|
|
114
|
+
await requestEvent.respondWith(response);
|
|
115
|
+
} catch (error) {
|
|
116
|
+
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
})();
|
|
120
|
+
}
|
|
121
|
+
})();
|
|
122
|
+
|
|
123
|
+
return listener;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (runtime.isBun) {
|
|
128
|
+
// @ts-ignore
|
|
129
|
+
const bunServer = Bun.serve({
|
|
130
|
+
unix: path,
|
|
131
|
+
fetch(req) {
|
|
132
|
+
return server(req, app);
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
close = async () => {
|
|
136
|
+
await bunServer.stop();
|
|
137
|
+
};
|
|
138
|
+
return bunServer;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Node.js 环境
|
|
142
|
+
const http = await import('http');
|
|
143
|
+
|
|
144
|
+
const httpServer = http.createServer(async (req, res) => {
|
|
145
|
+
try {
|
|
146
|
+
const response = await server(req, app);
|
|
147
|
+
|
|
148
|
+
// 设置响应头
|
|
149
|
+
response.headers.forEach((value, key) => {
|
|
150
|
+
res.setHeader(key, value);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// 设置状态码
|
|
154
|
+
res.statusCode = response.status;
|
|
155
|
+
|
|
156
|
+
// 读取响应体并写入
|
|
157
|
+
const body = await response.text();
|
|
158
|
+
res.end(body);
|
|
159
|
+
} catch (error) {
|
|
160
|
+
console.error('Error handling request:', error);
|
|
161
|
+
res.statusCode = 500;
|
|
162
|
+
res.end('Internal Server Error');
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
httpServer.listen(path);
|
|
167
|
+
close = async () => {
|
|
168
|
+
httpServer.close();
|
|
169
|
+
};
|
|
170
|
+
return httpServer;
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
export const getRequestParams = async (req: Request) => {
|
|
174
|
+
let urlParams: Record<string, any> = {};
|
|
175
|
+
let bodyParams: Record<string, any> = {};
|
|
176
|
+
|
|
177
|
+
// 获取URL参数
|
|
178
|
+
const url = new URL(req.url);
|
|
179
|
+
for (const [key, value] of url.searchParams.entries()) {
|
|
180
|
+
// 尝试解析JSON payload
|
|
181
|
+
if (key === 'payload') {
|
|
182
|
+
try {
|
|
183
|
+
urlParams[key] = JSON.parse(value);
|
|
184
|
+
} catch {
|
|
185
|
+
urlParams[key] = value;
|
|
186
|
+
}
|
|
187
|
+
} else {
|
|
188
|
+
urlParams[key] = value;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 获取body参数
|
|
193
|
+
if (req.method.toLowerCase() === 'post' && req.body) {
|
|
194
|
+
const contentType = req.headers.get('content-type') || '';
|
|
195
|
+
if (contentType.includes('application/json')) {
|
|
196
|
+
try {
|
|
197
|
+
bodyParams = await req.json();
|
|
198
|
+
} catch {
|
|
199
|
+
// 如果解析失败,保持空对象
|
|
200
|
+
}
|
|
201
|
+
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
202
|
+
const formData = await req.text();
|
|
203
|
+
const params = new URLSearchParams(formData);
|
|
204
|
+
for (const [key, value] of params.entries()) {
|
|
205
|
+
bodyParams[key] = value;
|
|
206
|
+
}
|
|
207
|
+
} else if (contentType.includes('multipart/form-data')) {
|
|
208
|
+
try {
|
|
209
|
+
const formData = await req.formData();
|
|
210
|
+
for (const [key, value] of formData.entries()) {
|
|
211
|
+
// @ts-ignore
|
|
212
|
+
bodyParams[key] = value instanceof File ? value : value.toString();
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
// 如果解析失败,保持空对象
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// body参数优先,合并数据
|
|
221
|
+
return {
|
|
222
|
+
...urlParams,
|
|
223
|
+
...bodyParams,
|
|
224
|
+
};
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
|
228
|
+
return new Promise<T>((resolve, reject) => {
|
|
229
|
+
const arr: any[] = [];
|
|
230
|
+
req.on('data', (chunk) => {
|
|
231
|
+
arr.push(chunk);
|
|
232
|
+
});
|
|
233
|
+
req.on('end', () => {
|
|
234
|
+
try {
|
|
235
|
+
const body = Buffer.concat(arr).toString();
|
|
236
|
+
|
|
237
|
+
// 获取 Content-Type 头信息
|
|
238
|
+
const contentType = req.headers['content-type'] || '';
|
|
239
|
+
|
|
240
|
+
// 处理 application/json
|
|
241
|
+
if (contentType.includes('application/json')) {
|
|
242
|
+
resolve(JSON.parse(body) as T);
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
// 处理 application/x-www-form-urlencoded
|
|
246
|
+
if (contentType.includes('application/x-www-form-urlencoded')) {
|
|
247
|
+
const formData = new URLSearchParams(body);
|
|
248
|
+
const result: Record<string, any> = {};
|
|
249
|
+
|
|
250
|
+
formData.forEach((value, key) => {
|
|
251
|
+
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
|
252
|
+
try {
|
|
253
|
+
result[key] = JSON.parse(value);
|
|
254
|
+
} catch {
|
|
255
|
+
result[key] = value;
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
resolve(result as T);
|
|
260
|
+
return;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// 默认尝试 JSON 解析
|
|
264
|
+
try {
|
|
265
|
+
resolve(JSON.parse(body) as T);
|
|
266
|
+
} catch {
|
|
267
|
+
resolve({} as T);
|
|
268
|
+
}
|
|
269
|
+
} catch (e) {
|
|
270
|
+
resolve({} as T);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { getRuntime } from './runtime.ts';
|
|
2
|
+
import { glob } from './utils/glob.ts';
|
|
3
|
+
type GlobOptions = {
|
|
4
|
+
cwd?: string;
|
|
5
|
+
load?: (args?: any) => Promise<any>;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}): Promise<string[]> => {
|
|
9
|
+
const runtime = getRuntime();
|
|
10
|
+
if (runtime.isNode) {
|
|
11
|
+
console.error(`Node.js is not supported`);
|
|
12
|
+
return [];
|
|
13
|
+
}
|
|
14
|
+
if (runtime.isDeno) {
|
|
15
|
+
// Deno 环境下
|
|
16
|
+
return await glob(match);
|
|
17
|
+
}
|
|
18
|
+
if (runtime.isBun) {
|
|
19
|
+
// Bun 环境下
|
|
20
|
+
// @ts-ignore
|
|
21
|
+
const { Glob } = await import('bun');
|
|
22
|
+
const path = await import('path');
|
|
23
|
+
// @ts-ignore
|
|
24
|
+
const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true });
|
|
25
|
+
const files: string[] = [];
|
|
26
|
+
for await (const file of glob.scan('.')) {
|
|
27
|
+
files.push(path.join(cwd, file));
|
|
28
|
+
}
|
|
29
|
+
// @ts-ignore
|
|
30
|
+
return Array.from(files);
|
|
31
|
+
}
|
|
32
|
+
return [];
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
export const loadTS = async (match: string = './*.ts', { cwd = process.cwd(), load }: GlobOptions = {}): Promise<any[]> => {
|
|
36
|
+
const files = await getMatchFiles(match, { cwd });
|
|
37
|
+
return Promise.all(files.map((file) => (load ? load(file) : import(file))));
|
|
38
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
type RuntimeEngine = 'node' | 'deno' | 'bun';
|
|
2
|
+
|
|
3
|
+
type Runtime = {
|
|
4
|
+
isNode?: boolean;
|
|
5
|
+
isDeno?: boolean;
|
|
6
|
+
isBun?: boolean;
|
|
7
|
+
engine: RuntimeEngine;
|
|
8
|
+
};
|
|
9
|
+
export const getRuntime = (): Runtime => {
|
|
10
|
+
// @ts-ignore
|
|
11
|
+
if (typeof Deno !== 'undefined') {
|
|
12
|
+
return { isDeno: true, engine: 'deno' };
|
|
13
|
+
}
|
|
14
|
+
// @ts-ignore
|
|
15
|
+
if (typeof Bun !== 'undefined') {
|
|
16
|
+
return { isBun: true, engine: 'bun' };
|
|
17
|
+
}
|
|
18
|
+
return { isNode: true, engine: 'node' };
|
|
19
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
type GlobOptions = {
|
|
2
|
+
cwd?: string;
|
|
3
|
+
};
|
|
4
|
+
|
|
5
|
+
export const glob = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}) => {
|
|
6
|
+
const fs = await import('node:fs');
|
|
7
|
+
const path = await import('node:path');
|
|
8
|
+
|
|
9
|
+
// 将 glob 模式转换为正则表达式
|
|
10
|
+
const globToRegex = (pattern: string): RegExp => {
|
|
11
|
+
const escaped = pattern
|
|
12
|
+
.replace(/\./g, '\\.')
|
|
13
|
+
.replace(/\*\*/g, '__DOUBLE_STAR__') // 临时替换 **
|
|
14
|
+
.replace(/\*/g, '[^/]*') // * 匹配除 / 外的任意字符
|
|
15
|
+
.replace(/__DOUBLE_STAR__/g, '.*') // ** 匹配任意字符包括 /
|
|
16
|
+
.replace(/\?/g, '[^/]'); // ? 匹配除 / 外的单个字符
|
|
17
|
+
return new RegExp(`^${escaped}$`);
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// 递归读取目录
|
|
21
|
+
const readDirRecursive = async (dir: string): Promise<string[]> => {
|
|
22
|
+
const files: string[] = [];
|
|
23
|
+
|
|
24
|
+
try {
|
|
25
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
26
|
+
|
|
27
|
+
for (const entry of entries) {
|
|
28
|
+
const fullPath = path.join(dir, entry.name);
|
|
29
|
+
|
|
30
|
+
if (entry.isFile()) {
|
|
31
|
+
files.push(fullPath);
|
|
32
|
+
} else if (entry.isDirectory()) {
|
|
33
|
+
// 递归搜索子目录
|
|
34
|
+
const subFiles = await readDirRecursive(fullPath);
|
|
35
|
+
files.push(...subFiles);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// 忽略无法访问的目录
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return files;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// 解析模式是否包含递归搜索
|
|
46
|
+
const hasRecursive = match.includes('**');
|
|
47
|
+
|
|
48
|
+
try {
|
|
49
|
+
let allFiles: string[] = [];
|
|
50
|
+
|
|
51
|
+
if (hasRecursive) {
|
|
52
|
+
// 处理递归模式
|
|
53
|
+
const basePath = match.split('**')[0];
|
|
54
|
+
const startDir = path.resolve(cwd, basePath || '.');
|
|
55
|
+
allFiles = await readDirRecursive(startDir);
|
|
56
|
+
} else {
|
|
57
|
+
// 处理非递归模式
|
|
58
|
+
const dir = path.resolve(cwd, path.dirname(match));
|
|
59
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
60
|
+
|
|
61
|
+
for (const entry of entries) {
|
|
62
|
+
if (entry.isFile()) {
|
|
63
|
+
allFiles.push(path.join(dir, entry.name));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 创建相对于 cwd 的匹配模式
|
|
69
|
+
const normalizedMatch = path.resolve(cwd, match);
|
|
70
|
+
const regex = globToRegex(normalizedMatch);
|
|
71
|
+
|
|
72
|
+
// 过滤匹配的文件
|
|
73
|
+
const matchedFiles = allFiles.filter(file => {
|
|
74
|
+
const normalizedFile = path.resolve(file);
|
|
75
|
+
return regex.test(normalizedFile);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return matchedFiles;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error(`Error in glob pattern "${match}":`, error);
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
};
|
package/src/route.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { CustomError } from './result/error.ts';
|
|
|
3
3
|
import { Schema, Rule, createSchema } from './validator/index.ts';
|
|
4
4
|
import { pick } from './utils/pick.ts';
|
|
5
5
|
import { get } from 'lodash-es';
|
|
6
|
+
import { listenProcess } from './utils/listen-process.ts';
|
|
6
7
|
|
|
7
|
-
export type RouterContextT = { code?: number;
|
|
8
|
+
export type RouterContextT = { code?: number;[key: string]: any };
|
|
8
9
|
export type RouteContext<T = { code?: number }, S = any> = {
|
|
9
10
|
// run first
|
|
10
11
|
query?: { [key: string]: any };
|
|
@@ -47,7 +48,7 @@ export type RouteContext<T = { code?: number }, S = any> = {
|
|
|
47
48
|
error?: any;
|
|
48
49
|
/** 请求 route的返回结果,包函ctx */
|
|
49
50
|
call?: (
|
|
50
|
-
message: { path: string; key?: string; payload?: any;
|
|
51
|
+
message: { path: string; key?: string; payload?: any;[key: string]: any } | { id: string; apyload?: any;[key: string]: any },
|
|
51
52
|
ctx?: RouteContext & { [key: string]: any },
|
|
52
53
|
) => Promise<any>;
|
|
53
54
|
/** 请求 route的返回结果,不包函ctx */
|
|
@@ -63,10 +64,10 @@ export type Run<T extends SimpleObject = {}> = (ctx: RouteContext<T>) => Promise
|
|
|
63
64
|
export type NextRoute = Pick<Route, 'id' | 'path' | 'key'>;
|
|
64
65
|
export type RouteMiddleware =
|
|
65
66
|
| {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
67
|
+
path: string;
|
|
68
|
+
key?: string;
|
|
69
|
+
id?: string;
|
|
70
|
+
}
|
|
70
71
|
| string;
|
|
71
72
|
export type RouteOpts = {
|
|
72
73
|
path?: string;
|
|
@@ -303,7 +304,7 @@ export class Route<U = { [key: string]: any }> {
|
|
|
303
304
|
}
|
|
304
305
|
return this;
|
|
305
306
|
}
|
|
306
|
-
addTo(router: QueryRouter | { add: (route: Route) => void;
|
|
307
|
+
addTo(router: QueryRouter | { add: (route: Route) => void;[key: string]: any }) {
|
|
307
308
|
router.add(this);
|
|
308
309
|
}
|
|
309
310
|
setData(data: any) {
|
|
@@ -608,7 +609,7 @@ export class QueryRouter {
|
|
|
608
609
|
* @description 这里的上下文是为了在handle函数中使用
|
|
609
610
|
* @param ctx
|
|
610
611
|
*/
|
|
611
|
-
|
|
612
|
+
setContext(ctx: RouteContext) {
|
|
612
613
|
this.context = ctx;
|
|
613
614
|
}
|
|
614
615
|
getList(): RouteInfo[] {
|
|
@@ -620,7 +621,7 @@ export class QueryRouter {
|
|
|
620
621
|
* 获取handle函数, 这里会去执行parse函数
|
|
621
622
|
*/
|
|
622
623
|
getHandle<T = any>(router: QueryRouter, wrapperFn?: HandleFn<T>, ctx?: RouteContext) {
|
|
623
|
-
return async (msg: { path: string; key?: string;
|
|
624
|
+
return async (msg: { path: string; key?: string;[key: string]: any }, handleContext?: RouteContext) => {
|
|
624
625
|
try {
|
|
625
626
|
const context = { ...ctx, ...handleContext };
|
|
626
627
|
const res = await router.parse(msg, context);
|
|
@@ -655,6 +656,17 @@ export class QueryRouter {
|
|
|
655
656
|
hasRoute(path: string, key: string = '') {
|
|
656
657
|
return this.routes.find((r) => r.path === path && r.key === key);
|
|
657
658
|
}
|
|
659
|
+
/**
|
|
660
|
+
* 等待程序运行, 获取到message的数据,就执行
|
|
661
|
+
*
|
|
662
|
+
* emitter = process
|
|
663
|
+
* -- .exit
|
|
664
|
+
* -- .on
|
|
665
|
+
* -- .send
|
|
666
|
+
*/
|
|
667
|
+
wait(params?: { path?: string; key?: string; payload?: any }, opts?: { emitter?: any, timeout?: number }) {
|
|
668
|
+
return listenProcess({ app: this, params, ...opts });
|
|
669
|
+
}
|
|
658
670
|
}
|
|
659
671
|
|
|
660
672
|
type QueryRouterServerOpts = {
|
|
@@ -662,7 +674,7 @@ type QueryRouterServerOpts = {
|
|
|
662
674
|
context?: RouteContext;
|
|
663
675
|
};
|
|
664
676
|
interface HandleFn<T = any> {
|
|
665
|
-
(msg: { path: string;
|
|
677
|
+
(msg: { path: string;[key: string]: any }, ctx?: any): { code: string; data?: any; message?: string;[key: string]: any };
|
|
666
678
|
(res: RouteContext<T>): any;
|
|
667
679
|
}
|
|
668
680
|
/**
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type ListenProcessOptions = {
|
|
2
|
+
app?: any; // 传入的应用实例
|
|
3
|
+
emitter?: any; // 可选的事件发射器
|
|
4
|
+
params?: any; // 可选的参数
|
|
5
|
+
timeout?: number; // 可选的超时时间 (单位: 毫秒)
|
|
6
|
+
};
|
|
7
|
+
export const listenProcess = async ({ app, emitter, params, timeout = 10 * 60 * 60 * 1000 }: ListenProcessOptions) => {
|
|
8
|
+
const process = emitter || globalThis.process;
|
|
9
|
+
let isEnd = false;
|
|
10
|
+
const timer = setTimeout(() => {
|
|
11
|
+
if (isEnd) return;
|
|
12
|
+
isEnd = true;
|
|
13
|
+
process.send?.({ success: false, error: 'Timeout' });
|
|
14
|
+
process.exit?.(1);
|
|
15
|
+
}, timeout);
|
|
16
|
+
|
|
17
|
+
// 监听来自主进程的消息
|
|
18
|
+
const getParams = async (): Promise<any> => {
|
|
19
|
+
return new Promise((resolve) => {
|
|
20
|
+
process.on('message', (msg) => {
|
|
21
|
+
if (isEnd) return;
|
|
22
|
+
isEnd = true;
|
|
23
|
+
clearTimeout(timer);
|
|
24
|
+
resolve(msg)
|
|
25
|
+
})
|
|
26
|
+
})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
const { path = 'main', ...rest } = await getParams()
|
|
31
|
+
// 执行主要逻辑
|
|
32
|
+
const result = await app.queryRoute({ path, ...rest, ...params })
|
|
33
|
+
// 发送结果回主进程
|
|
34
|
+
const response = {
|
|
35
|
+
success: true,
|
|
36
|
+
data: result,
|
|
37
|
+
timestamp: new Date().toISOString()
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
process.send?.(response, (error) => {
|
|
41
|
+
process.exit?.(0)
|
|
42
|
+
})
|
|
43
|
+
} catch (error) {
|
|
44
|
+
process.send?.({
|
|
45
|
+
success: false,
|
|
46
|
+
error: error.message
|
|
47
|
+
})
|
|
48
|
+
process.exit?.(1)
|
|
49
|
+
}
|
|
50
|
+
}
|
package/src/validator/index.ts
CHANGED
package/src/validator/rule.ts
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
import { z, ZodError
|
|
2
|
-
export { Schema };
|
|
1
|
+
import { z, ZodError } from 'zod';
|
|
3
2
|
type BaseRule = {
|
|
4
3
|
value?: any;
|
|
5
4
|
required?: boolean;
|
|
@@ -64,7 +63,7 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
|
|
|
64
63
|
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
|
65
64
|
}
|
|
66
65
|
};
|
|
67
|
-
export const createSchema = (rule: Rule):
|
|
66
|
+
export const createSchema = (rule: Rule): z.ZodType<any, any, any> => {
|
|
68
67
|
try {
|
|
69
68
|
rule.required = rule.required ?? false;
|
|
70
69
|
if (!rule.required) {
|