recorver code
This commit is contained in:
20
auto.ts
Normal file
20
auto.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { loadTS, getMatchFiles } from './src/auto/load-ts.ts';
|
||||||
|
import { listenSocket } from './src/auto/listen-sock.ts';
|
||||||
|
import { Route, QueryRouter, QueryRouterServer } from './src/route.ts';
|
||||||
|
|
||||||
|
export { Route, QueryRouter, QueryRouterServer };
|
||||||
|
|
||||||
|
export const App = QueryRouterServer;
|
||||||
|
|
||||||
|
export { createSchema } from './src/validator/index.ts';
|
||||||
|
export type { Rule } from './src/validator/rule.ts';
|
||||||
|
export type { Schema } from 'zod';
|
||||||
|
export type { RouteContext, RouteOpts } from './src/route.ts';
|
||||||
|
|
||||||
|
export type { Run } from './src/route.ts';
|
||||||
|
|
||||||
|
export { CustomError } from './src/result/error.ts';
|
||||||
|
|
||||||
|
export { listenSocket, loadTS, getMatchFiles };
|
||||||
|
|
||||||
|
export { autoCall } from './src/auto/call-sock.ts';
|
164
src/auto/call-sock.ts
Normal file
164
src/auto/call-sock.ts
Normal file
@@ -0,0 +1,164 @@
|
|||||||
|
import { createConnection } from 'node:net';
|
||||||
|
|
||||||
|
type QueryData = {
|
||||||
|
path?: string;
|
||||||
|
key?: string;
|
||||||
|
payload?: any;
|
||||||
|
[key: string]: any;
|
||||||
|
};
|
||||||
|
|
||||||
|
type CallSockOptions = {
|
||||||
|
socketPath?: string;
|
||||||
|
timeout?: number;
|
||||||
|
method?: 'GET' | 'POST';
|
||||||
|
};
|
||||||
|
|
||||||
|
export const callSock = async (data: QueryData, options: CallSockOptions = {}): Promise<any> => {
|
||||||
|
const { socketPath = './app.sock', timeout = 10000, method = 'POST' } = options;
|
||||||
|
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const client = createConnection(socketPath);
|
||||||
|
let responseData = '';
|
||||||
|
let timer: NodeJS.Timeout;
|
||||||
|
|
||||||
|
// 设置超时
|
||||||
|
if (timeout > 0) {
|
||||||
|
timer = setTimeout(() => {
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error(`Socket call timeout after ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
client.on('connect', () => {
|
||||||
|
try {
|
||||||
|
let request: string;
|
||||||
|
|
||||||
|
if (method === 'GET') {
|
||||||
|
// GET 请求:参数放在 URL 中
|
||||||
|
const searchParams = new URLSearchParams();
|
||||||
|
Object.entries(data).forEach(([key, value]) => {
|
||||||
|
if (key === 'payload' && typeof value === 'object') {
|
||||||
|
searchParams.append(key, JSON.stringify(value));
|
||||||
|
} else {
|
||||||
|
searchParams.append(key, String(value));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = searchParams.toString();
|
||||||
|
const url = queryString ? `/?${queryString}` : '/';
|
||||||
|
|
||||||
|
request = [`GET ${url} HTTP/1.1`, 'Host: localhost', 'Connection: close', '', ''].join('\r\n');
|
||||||
|
} else {
|
||||||
|
// POST 请求:数据放在 body 中
|
||||||
|
const body = JSON.stringify(data);
|
||||||
|
const contentLength = Buffer.byteLength(body, 'utf8');
|
||||||
|
|
||||||
|
request = [
|
||||||
|
'POST / HTTP/1.1',
|
||||||
|
'Host: localhost',
|
||||||
|
'Content-Type: application/json',
|
||||||
|
`Content-Length: ${contentLength}`,
|
||||||
|
'Connection: close',
|
||||||
|
'',
|
||||||
|
body,
|
||||||
|
].join('\r\n');
|
||||||
|
}
|
||||||
|
|
||||||
|
client.write(request);
|
||||||
|
} catch (error) {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
client.destroy();
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('data', (chunk) => {
|
||||||
|
responseData += chunk.toString();
|
||||||
|
|
||||||
|
// 检查是否收到完整的HTTP响应
|
||||||
|
if (responseData.includes('\r\n\r\n')) {
|
||||||
|
const [headerSection] = responseData.split('\r\n\r\n');
|
||||||
|
const contentLengthMatch = headerSection.match(/content-length:\s*(\d+)/i);
|
||||||
|
|
||||||
|
if (contentLengthMatch) {
|
||||||
|
const expectedLength = parseInt(contentLengthMatch[1]);
|
||||||
|
const bodyStart = responseData.indexOf('\r\n\r\n') + 4;
|
||||||
|
const currentBodyLength = Buffer.byteLength(responseData.slice(bodyStart), 'utf8');
|
||||||
|
|
||||||
|
// 如果收到了完整的响应,主动关闭连接
|
||||||
|
if (currentBodyLength >= expectedLength) {
|
||||||
|
client.end();
|
||||||
|
}
|
||||||
|
} else if (responseData.includes('\r\n0\r\n\r\n')) {
|
||||||
|
// 检查 chunked 编码结束标记
|
||||||
|
client.end();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('end', () => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 解析 HTTP 响应
|
||||||
|
const response = parseHttpResponse(responseData);
|
||||||
|
|
||||||
|
if (response.statusCode >= 400) {
|
||||||
|
reject(new Error(`HTTP ${response.statusCode}: ${response.body}`));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 尝试解析 JSON 响应
|
||||||
|
try {
|
||||||
|
const result = JSON.parse(response.body);
|
||||||
|
resolve(result);
|
||||||
|
} catch {
|
||||||
|
// 如果不是 JSON,直接返回文本
|
||||||
|
resolve(response.body);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
reject(error);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('error', (error) => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
reject(error);
|
||||||
|
});
|
||||||
|
|
||||||
|
client.on('timeout', () => {
|
||||||
|
if (timer) clearTimeout(timer);
|
||||||
|
client.destroy();
|
||||||
|
reject(new Error('Socket connection timeout'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析 HTTP 响应的辅助函数
|
||||||
|
function parseHttpResponse(responseData: string) {
|
||||||
|
const [headerSection, ...bodyParts] = responseData.split('\r\n\r\n');
|
||||||
|
const body = bodyParts.join('\r\n\r\n');
|
||||||
|
|
||||||
|
const lines = headerSection.split('\r\n');
|
||||||
|
const statusLine = lines[0];
|
||||||
|
const statusMatch = statusLine.match(/HTTP\/\d\.\d (\d+)/);
|
||||||
|
const statusCode = statusMatch ? parseInt(statusMatch[1]) : 200;
|
||||||
|
|
||||||
|
const headers: Record<string, string> = {};
|
||||||
|
for (let i = 1; i < lines.length; i++) {
|
||||||
|
const [key, ...valueParts] = lines[i].split(':');
|
||||||
|
if (key && valueParts.length > 0) {
|
||||||
|
headers[key.trim().toLowerCase()] = valueParts.join(':').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
statusCode,
|
||||||
|
headers,
|
||||||
|
body: body || '',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const autoCall = (data: QueryData, options?: Omit<CallSockOptions, 'method'>) => {
|
||||||
|
return callSock(data, { ...options, method: 'POST' });
|
||||||
|
};
|
274
src/auto/listen-sock.ts
Normal file
274
src/auto/listen-sock.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import type { IncomingMessage } from 'http';
|
||||||
|
import { QueryRouterServer } from '../route.ts';
|
||||||
|
import { getRuntime } from './runtime.ts';
|
||||||
|
import { runFirstCheck } from './listen/run-check.ts';
|
||||||
|
import { cleanup } from './listen/cleanup.ts';
|
||||||
|
import { ServerTimer } from './listen/server-time.ts';
|
||||||
|
|
||||||
|
type ListenSocketOptions = {
|
||||||
|
/**
|
||||||
|
* Unix socket path, defaults to './app.sock'
|
||||||
|
*/
|
||||||
|
path?: string;
|
||||||
|
app?: QueryRouterServer;
|
||||||
|
/**
|
||||||
|
* Unix socket path, defaults to './app.pid'
|
||||||
|
*/
|
||||||
|
pidPath?: string;
|
||||||
|
/**
|
||||||
|
* Timeout for the server, defaults to 15 minutes.
|
||||||
|
* If the server is not responsive for this duration, it will be terminated
|
||||||
|
*/
|
||||||
|
timeout?: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = async (req, app: QueryRouterServer) => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
let data;
|
||||||
|
if (!runtime.isNode) {
|
||||||
|
data = await getRequestParams(req);
|
||||||
|
} else {
|
||||||
|
data = await parseBody(req);
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
const serverTimer = app.serverTimer;
|
||||||
|
if (serverTimer) {
|
||||||
|
serverTimer?.run?.();
|
||||||
|
}
|
||||||
|
const result = await app.queryRoute(data as any);
|
||||||
|
const response = new Response(JSON.stringify(result));
|
||||||
|
response.headers.set('Content-Type', 'application/json');
|
||||||
|
return response;
|
||||||
|
};
|
||||||
|
export const closeListenSocket = () => {
|
||||||
|
console.log('Closing listen socket');
|
||||||
|
process.emit('SIGINT');
|
||||||
|
};
|
||||||
|
export const serverTimer = new ServerTimer();
|
||||||
|
export const listenSocket = async (options?: ListenSocketOptions) => {
|
||||||
|
const path = options?.path || './app.sock';
|
||||||
|
const pidPath = options?.pidPath || './app.pid';
|
||||||
|
const timeout = options?.timeout || 24 * 60 * 60 * 1000; // 24 hours
|
||||||
|
const runtime = getRuntime();
|
||||||
|
|
||||||
|
serverTimer.timeout = timeout;
|
||||||
|
serverTimer.startTimer();
|
||||||
|
serverTimer.onTimeout = closeListenSocket;
|
||||||
|
|
||||||
|
let app = options?.app || globalThis.context?.app;
|
||||||
|
if (!app) {
|
||||||
|
app = new QueryRouterServer();
|
||||||
|
}
|
||||||
|
app.serverTimer = serverTimer;
|
||||||
|
await runFirstCheck(path, pidPath);
|
||||||
|
let close = async () => {};
|
||||||
|
cleanup({ path, close });
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// 检查 Deno 版本是否支持 Unix domain socket
|
||||||
|
try {
|
||||||
|
// @ts-ignore
|
||||||
|
const listener = Deno.listen({
|
||||||
|
transport: 'unix',
|
||||||
|
path: path,
|
||||||
|
});
|
||||||
|
|
||||||
|
// 处理连接
|
||||||
|
(async () => {
|
||||||
|
for await (const conn of listener) {
|
||||||
|
(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const httpConn = Deno.serveHttp(conn);
|
||||||
|
for await (const requestEvent of httpConn) {
|
||||||
|
try {
|
||||||
|
const response = await server(requestEvent.request, app);
|
||||||
|
await requestEvent.respondWith(response);
|
||||||
|
} catch (error) {
|
||||||
|
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
close = async () => {
|
||||||
|
listener.close();
|
||||||
|
};
|
||||||
|
return listener;
|
||||||
|
} catch (error) {
|
||||||
|
// 如果 Unix socket 不支持,回退到 HTTP 服务器
|
||||||
|
console.warn('Unix socket not supported in this Deno environment, falling back to HTTP server');
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
const listener = Deno.listen({ port: 0 }); // 使用随机端口
|
||||||
|
|
||||||
|
// @ts-ignore
|
||||||
|
console.log(`Deno server listening on port ${listener.addr.port}`);
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
for await (const conn of listener) {
|
||||||
|
(async () => {
|
||||||
|
// @ts-ignore
|
||||||
|
const httpConn = Deno.serveHttp(conn);
|
||||||
|
for await (const requestEvent of httpConn) {
|
||||||
|
try {
|
||||||
|
const response = await server(requestEvent.request, app);
|
||||||
|
await requestEvent.respondWith(response);
|
||||||
|
} catch (error) {
|
||||||
|
await requestEvent.respondWith(new Response('Internal Server Error', { status: 500 }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
return listener;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (runtime.isBun) {
|
||||||
|
// @ts-ignore
|
||||||
|
const bunServer = Bun.serve({
|
||||||
|
unix: path,
|
||||||
|
fetch(req) {
|
||||||
|
return server(req, app);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
close = async () => {
|
||||||
|
await bunServer.stop();
|
||||||
|
};
|
||||||
|
return bunServer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Node.js 环境
|
||||||
|
const http = await import('http');
|
||||||
|
|
||||||
|
const httpServer = http.createServer(async (req, res) => {
|
||||||
|
try {
|
||||||
|
const response = await server(req, app);
|
||||||
|
|
||||||
|
// 设置响应头
|
||||||
|
response.headers.forEach((value, key) => {
|
||||||
|
res.setHeader(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 设置状态码
|
||||||
|
res.statusCode = response.status;
|
||||||
|
|
||||||
|
// 读取响应体并写入
|
||||||
|
const body = await response.text();
|
||||||
|
res.end(body);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error handling request:', error);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end('Internal Server Error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
httpServer.listen(path);
|
||||||
|
close = async () => {
|
||||||
|
httpServer.close();
|
||||||
|
};
|
||||||
|
return httpServer;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getRequestParams = async (req: Request) => {
|
||||||
|
let urlParams: Record<string, any> = {};
|
||||||
|
let bodyParams: Record<string, any> = {};
|
||||||
|
|
||||||
|
// 获取URL参数
|
||||||
|
const url = new URL(req.url);
|
||||||
|
for (const [key, value] of url.searchParams.entries()) {
|
||||||
|
// 尝试解析JSON payload
|
||||||
|
if (key === 'payload') {
|
||||||
|
try {
|
||||||
|
urlParams[key] = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
urlParams[key] = value;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
urlParams[key] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取body参数
|
||||||
|
if (req.method.toLowerCase() === 'post' && req.body) {
|
||||||
|
const contentType = req.headers.get('content-type') || '';
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
try {
|
||||||
|
bodyParams = await req.json();
|
||||||
|
} catch {
|
||||||
|
// 如果解析失败,保持空对象
|
||||||
|
}
|
||||||
|
} else if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||||
|
const formData = await req.text();
|
||||||
|
const params = new URLSearchParams(formData);
|
||||||
|
for (const [key, value] of params.entries()) {
|
||||||
|
bodyParams[key] = value;
|
||||||
|
}
|
||||||
|
} else if (contentType.includes('multipart/form-data')) {
|
||||||
|
try {
|
||||||
|
const formData = await req.formData();
|
||||||
|
for (const [key, value] of formData.entries()) {
|
||||||
|
// @ts-ignore
|
||||||
|
bodyParams[key] = value instanceof File ? value : value.toString();
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// 如果解析失败,保持空对象
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// body参数优先,合并数据
|
||||||
|
return {
|
||||||
|
...urlParams,
|
||||||
|
...bodyParams,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseBody = async <T = Record<string, any>>(req: IncomingMessage) => {
|
||||||
|
return new Promise<T>((resolve, reject) => {
|
||||||
|
const arr: any[] = [];
|
||||||
|
req.on('data', (chunk) => {
|
||||||
|
arr.push(chunk);
|
||||||
|
});
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const body = Buffer.concat(arr).toString();
|
||||||
|
|
||||||
|
// 获取 Content-Type 头信息
|
||||||
|
const contentType = req.headers['content-type'] || '';
|
||||||
|
|
||||||
|
// 处理 application/json
|
||||||
|
if (contentType.includes('application/json')) {
|
||||||
|
resolve(JSON.parse(body) as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 处理 application/x-www-form-urlencoded
|
||||||
|
if (contentType.includes('application/x-www-form-urlencoded')) {
|
||||||
|
const formData = new URLSearchParams(body);
|
||||||
|
const result: Record<string, any> = {};
|
||||||
|
|
||||||
|
formData.forEach((value, key) => {
|
||||||
|
// 尝试将值解析为 JSON,如果失败则保留原始字符串
|
||||||
|
try {
|
||||||
|
result[key] = JSON.parse(value);
|
||||||
|
} catch {
|
||||||
|
result[key] = value;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
resolve(result as T);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认尝试 JSON 解析
|
||||||
|
try {
|
||||||
|
resolve(JSON.parse(body) as T);
|
||||||
|
} catch {
|
||||||
|
resolve({} as T);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
resolve({} as T);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
102
src/auto/listen/cleanup.ts
Normal file
102
src/auto/listen/cleanup.ts
Normal file
@@ -0,0 +1,102 @@
|
|||||||
|
import { getRuntime } from '../runtime.ts';
|
||||||
|
|
||||||
|
let isClean = false;
|
||||||
|
export const deleteFileDetached = async (path: string, pidPath: string = './app.pid') => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// Deno 实现 - 启动后不等待结果
|
||||||
|
const process = new Deno.Command('sh', {
|
||||||
|
args: ['-c', `rm -f "${path}" & rm -f "${pidPath}"`],
|
||||||
|
stdout: 'null',
|
||||||
|
stderr: 'null',
|
||||||
|
});
|
||||||
|
process.spawn(); // 不等待结果
|
||||||
|
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { spawn } = await import('node:child_process');
|
||||||
|
const child = spawn('sh', ['-c', `rm -f "${path}" & rm -f "${pidPath}"`], {
|
||||||
|
detached: true,
|
||||||
|
stdio: 'ignore',
|
||||||
|
});
|
||||||
|
child.unref(); // 完全分离
|
||||||
|
console.log(`[DEBUG] Fire-and-forget delete initiated for ${path}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
type CleanupOptions = {
|
||||||
|
path: string;
|
||||||
|
close?: () => Promise<void>;
|
||||||
|
pidPath?: string;
|
||||||
|
};
|
||||||
|
export const cleanup = async ({ path, close = async () => {}, pidPath = './app.pid' }: CleanupOptions) => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
|
||||||
|
// 检查文件是否存在并删除
|
||||||
|
const cleanupFile = async () => {
|
||||||
|
if (isClean) return;
|
||||||
|
isClean = true;
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
await deleteFileDetached(path, pidPath);
|
||||||
|
}
|
||||||
|
await close();
|
||||||
|
if (!runtime.isDeno) {
|
||||||
|
await deleteFileDetached(path, pidPath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 根据运行时环境注册不同的退出监听器
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// Deno 环境
|
||||||
|
const handleSignal = () => {
|
||||||
|
cleanupFile();
|
||||||
|
Deno.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
Deno.addSignalListener('SIGINT', handleSignal);
|
||||||
|
Deno.addSignalListener('SIGTERM', handleSignal);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn('[DEBUG] Failed to add signal listeners:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 对于 beforeunload 和 unload,使用异步清理
|
||||||
|
const handleUnload = () => {
|
||||||
|
cleanupFile();
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.addEventListener('beforeunload', handleUnload);
|
||||||
|
globalThis.addEventListener('unload', handleUnload);
|
||||||
|
} else if (runtime.isNode || runtime.isBun) {
|
||||||
|
// Node.js 和 Bun 环境
|
||||||
|
import('process').then(({ default: process }) => {
|
||||||
|
// 信号处理使用同步清理,然后退出
|
||||||
|
const signalHandler = async (signal: string) => {
|
||||||
|
await cleanupFile();
|
||||||
|
process.exit(0);
|
||||||
|
};
|
||||||
|
|
||||||
|
process.on('SIGINT', () => signalHandler('SIGINT'));
|
||||||
|
process.on('SIGTERM', () => signalHandler('SIGTERM'));
|
||||||
|
process.on('SIGUSR1', () => signalHandler('SIGUSR1'));
|
||||||
|
process.on('SIGUSR2', () => signalHandler('SIGUSR2'));
|
||||||
|
|
||||||
|
process.on('exit', async () => {
|
||||||
|
await cleanupFile();
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('uncaughtException', async (error) => {
|
||||||
|
console.error('Uncaught Exception:', error);
|
||||||
|
await cleanupFile();
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
process.on('unhandledRejection', async (reason, promise) => {
|
||||||
|
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||||||
|
await cleanupFile();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 返回手动清理函数,以便需要时主动调用
|
||||||
|
return cleanupFile;
|
||||||
|
};
|
51
src/auto/listen/run-check.ts
Normal file
51
src/auto/listen/run-check.ts
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
import { getRuntime } from '../runtime.ts';
|
||||||
|
|
||||||
|
export const getPid = async () => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
|
||||||
|
let pid = 0;
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// @ts-ignore
|
||||||
|
pid = Deno.pid;
|
||||||
|
} else {
|
||||||
|
pid = process.pid;
|
||||||
|
}
|
||||||
|
return pid;
|
||||||
|
};
|
||||||
|
export const writeAppid = async (pidPath = './app.pid') => {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
const pid = await getPid();
|
||||||
|
fs.writeFileSync(pidPath, pid + '');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPidFromFileAndStop = async () => {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
if (fs.existsSync('./app.pid')) {
|
||||||
|
const pid = parseInt(fs.readFileSync('./app.pid', 'utf-8'), 10);
|
||||||
|
if (!isNaN(pid)) {
|
||||||
|
if (pid === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
process.kill(pid);
|
||||||
|
console.log(`Stopped process with PID ${pid}`);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to stop process with PID ${pid}:`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const runFirstCheck = async (path: string, pidPath: string) => {
|
||||||
|
await getPidFromFileAndStop();
|
||||||
|
await writeAppid(pidPath);
|
||||||
|
try {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
if (fs.existsSync(path)) {
|
||||||
|
fs.unlinkSync(path);
|
||||||
|
console.log(`Socket file ${path} cleaned up during first check`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Failed to clean up socket file ${path} during first check:`, error);
|
||||||
|
}
|
||||||
|
};
|
33
src/auto/listen/server-time.ts
Normal file
33
src/auto/listen/server-time.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
export class ServerTimer {
|
||||||
|
updatedAt: number;
|
||||||
|
timer: any;
|
||||||
|
timeout: number;
|
||||||
|
onTimeout: any;
|
||||||
|
interval = 10 * 1000;
|
||||||
|
constructor(opts?: { timeout?: number }) {
|
||||||
|
this.timeout = opts?.timeout || 15 * 60 * 1000;
|
||||||
|
this.run();
|
||||||
|
}
|
||||||
|
startTimer() {
|
||||||
|
const that = this;
|
||||||
|
if (this.timer) {
|
||||||
|
clearInterval(this.timer);
|
||||||
|
}
|
||||||
|
this.timer = setInterval(() => {
|
||||||
|
const updatedAt = Date.now();
|
||||||
|
const timeout = that.timeout;
|
||||||
|
const onTimeout = that.onTimeout;
|
||||||
|
const isExpired = updatedAt - that.updatedAt > timeout;
|
||||||
|
if (isExpired) {
|
||||||
|
onTimeout?.();
|
||||||
|
clearInterval(that.timer);
|
||||||
|
that.timer = null;
|
||||||
|
}
|
||||||
|
}, that.interval);
|
||||||
|
}
|
||||||
|
|
||||||
|
run(): number {
|
||||||
|
this.updatedAt = Date.now();
|
||||||
|
return this.updatedAt;
|
||||||
|
}
|
||||||
|
}
|
38
src/auto/load-ts.ts
Normal file
38
src/auto/load-ts.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { getRuntime } from './runtime.ts';
|
||||||
|
import { glob } from './utils/glob.ts';
|
||||||
|
type GlobOptions = {
|
||||||
|
cwd?: string;
|
||||||
|
load?: (args?: any) => Promise<any>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getMatchFiles = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}): Promise<string[]> => {
|
||||||
|
const runtime = getRuntime();
|
||||||
|
if (runtime.isNode) {
|
||||||
|
console.error(`Node.js is not supported`);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
if (runtime.isDeno) {
|
||||||
|
// Deno 环境下
|
||||||
|
return await glob(match);
|
||||||
|
}
|
||||||
|
if (runtime.isBun) {
|
||||||
|
// Bun 环境下
|
||||||
|
// @ts-ignore
|
||||||
|
const { Glob } = await import('bun');
|
||||||
|
const path = await import('path');
|
||||||
|
// @ts-ignore
|
||||||
|
const glob = new Glob(match, { cwd, absolute: true, onlyFiles: true });
|
||||||
|
const files: string[] = [];
|
||||||
|
for await (const file of glob.scan('.')) {
|
||||||
|
files.push(path.join(cwd, file));
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
return Array.from(files);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const loadTS = async (match: string = './*.ts', { cwd = process.cwd(), load }: GlobOptions = {}): Promise<any[]> => {
|
||||||
|
const files = await getMatchFiles(match, { cwd });
|
||||||
|
return Promise.all(files.map((file) => (load ? load(file) : import(file))));
|
||||||
|
};
|
19
src/auto/runtime.ts
Normal file
19
src/auto/runtime.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
type RuntimeEngine = 'node' | 'deno' | 'bun';
|
||||||
|
|
||||||
|
type Runtime = {
|
||||||
|
isNode?: boolean;
|
||||||
|
isDeno?: boolean;
|
||||||
|
isBun?: boolean;
|
||||||
|
engine: RuntimeEngine;
|
||||||
|
};
|
||||||
|
export const getRuntime = (): Runtime => {
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof Deno !== 'undefined') {
|
||||||
|
return { isDeno: true, engine: 'deno' };
|
||||||
|
}
|
||||||
|
// @ts-ignore
|
||||||
|
if (typeof Bun !== 'undefined') {
|
||||||
|
return { isBun: true, engine: 'bun' };
|
||||||
|
}
|
||||||
|
return { isNode: true, engine: 'node' };
|
||||||
|
};
|
83
src/auto/utils/glob.ts
Normal file
83
src/auto/utils/glob.ts
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
type GlobOptions = {
|
||||||
|
cwd?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const glob = async (match: string = './*.ts', { cwd = process.cwd() }: GlobOptions = {}) => {
|
||||||
|
const fs = await import('node:fs');
|
||||||
|
const path = await import('node:path');
|
||||||
|
|
||||||
|
// 将 glob 模式转换为正则表达式
|
||||||
|
const globToRegex = (pattern: string): RegExp => {
|
||||||
|
const escaped = pattern
|
||||||
|
.replace(/\./g, '\\.')
|
||||||
|
.replace(/\*\*/g, '__DOUBLE_STAR__') // 临时替换 **
|
||||||
|
.replace(/\*/g, '[^/]*') // * 匹配除 / 外的任意字符
|
||||||
|
.replace(/__DOUBLE_STAR__/g, '.*') // ** 匹配任意字符包括 /
|
||||||
|
.replace(/\?/g, '[^/]'); // ? 匹配除 / 外的单个字符
|
||||||
|
return new RegExp(`^${escaped}$`);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 递归读取目录
|
||||||
|
const readDirRecursive = async (dir: string): Promise<string[]> => {
|
||||||
|
const files: string[] = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isFile()) {
|
||||||
|
files.push(fullPath);
|
||||||
|
} else if (entry.isDirectory()) {
|
||||||
|
// 递归搜索子目录
|
||||||
|
const subFiles = await readDirRecursive(fullPath);
|
||||||
|
files.push(...subFiles);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 忽略无法访问的目录
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 解析模式是否包含递归搜索
|
||||||
|
const hasRecursive = match.includes('**');
|
||||||
|
|
||||||
|
try {
|
||||||
|
let allFiles: string[] = [];
|
||||||
|
|
||||||
|
if (hasRecursive) {
|
||||||
|
// 处理递归模式
|
||||||
|
const basePath = match.split('**')[0];
|
||||||
|
const startDir = path.resolve(cwd, basePath || '.');
|
||||||
|
allFiles = await readDirRecursive(startDir);
|
||||||
|
} else {
|
||||||
|
// 处理非递归模式
|
||||||
|
const dir = path.resolve(cwd, path.dirname(match));
|
||||||
|
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
if (entry.isFile()) {
|
||||||
|
allFiles.push(path.join(dir, entry.name));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 创建相对于 cwd 的匹配模式
|
||||||
|
const normalizedMatch = path.resolve(cwd, match);
|
||||||
|
const regex = globToRegex(normalizedMatch);
|
||||||
|
|
||||||
|
// 过滤匹配的文件
|
||||||
|
const matchedFiles = allFiles.filter(file => {
|
||||||
|
const normalizedFile = path.resolve(file);
|
||||||
|
return regex.test(normalizedFile);
|
||||||
|
});
|
||||||
|
|
||||||
|
return matchedFiles;
|
||||||
|
} catch (error) {
|
||||||
|
console.error(`Error in glob pattern "${match}":`, error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
};
|
@@ -1,2 +1,6 @@
|
|||||||
export type { Rule, Schema } from './rule.ts';
|
import { z } from 'zod';
|
||||||
export { schemaFormRule, createSchema, createSchemaList } from './rule.ts';
|
export { schemaFormRule, createSchema, createSchemaList } from './rule.ts';
|
||||||
|
|
||||||
|
export type { Rule } from './rule.ts';
|
||||||
|
|
||||||
|
export type Schema = z.ZodType<any, any, any>;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
import { z, ZodError, Schema } from 'zod';
|
import { z, ZodError } from 'zod';
|
||||||
export { Schema };
|
|
||||||
type BaseRule = {
|
type BaseRule = {
|
||||||
value?: any;
|
value?: any;
|
||||||
required?: boolean;
|
required?: boolean;
|
||||||
@@ -64,7 +63,7 @@ export const schemaFormRule = (rule: Rule): z.ZodType<any, any, any> => {
|
|||||||
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
throw new Error(`Unknown rule type: ${(rule as any)?.type}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
export const createSchema = (rule: Rule): Schema => {
|
export const createSchema = (rule: Rule): z.ZodType<any, any, any> => {
|
||||||
try {
|
try {
|
||||||
rule.required = rule.required ?? false;
|
rule.required = rule.required ?? false;
|
||||||
if (!rule.required) {
|
if (!rule.required) {
|
||||||
|
Reference in New Issue
Block a user