recorver code

This commit is contained in:
2025-10-14 19:15:18 +08:00
parent 19c4cc2e06
commit cd96b53f6e
11 changed files with 791 additions and 4 deletions

20
auto.ts Normal file
View 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
View 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
View 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
View 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;
};

View 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);
}
};

View 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
View 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
View 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
View 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 [];
}
};

View File

@@ -1,2 +1,6 @@
export type { Rule, Schema } from './rule.ts';
import { z } from 'zod';
export { schemaFormRule, createSchema, createSchemaList } from './rule.ts';
export type { Rule } from './rule.ts';
export type Schema = z.ZodType<any, any, any>;

View File

@@ -1,5 +1,4 @@
import { z, ZodError, Schema } from 'zod';
export { Schema };
import { z, ZodError } from 'zod';
type BaseRule = {
value?: any;
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}`);
}
};
export const createSchema = (rule: Rule): Schema => {
export const createSchema = (rule: Rule): z.ZodType<any, any, any> => {
try {
rule.required = rule.required ?? false;
if (!rule.required) {