feat: update package version and dependencies; add ReconnectingWebSocket for automatic reconnection

This commit is contained in:
2026-02-02 21:22:49 +08:00
parent b081a03399
commit 7f7ea79689
5 changed files with 308 additions and 51 deletions

View File

@@ -1,7 +1,7 @@
{
"$schema": "https://json.schemastore.org/package",
"name": "@kevisual/router",
"version": "0.0.66",
"version": "0.0.67",
"description": "",
"type": "module",
"main": "./dist/router.js",
@@ -24,17 +24,18 @@
"packageManager": "pnpm@10.28.2",
"devDependencies": {
"@kevisual/context": "^0.0.4",
"@kevisual/dts": "^0.0.3",
"@kevisual/js-filter": "^0.0.5",
"@kevisual/local-proxy": "^0.0.8",
"@kevisual/query": "^0.0.38",
"@kevisual/use-config": "^1.0.28",
"@opencode-ai/plugin": "^1.1.47",
"@kevisual/query": "^0.0.39",
"@kevisual/use-config": "^1.0.30",
"@opencode-ai/plugin": "^1.1.48",
"@rollup/plugin-alias": "^6.0.0",
"@rollup/plugin-commonjs": "29.0.0",
"@rollup/plugin-node-resolve": "^16.0.3",
"@rollup/plugin-typescript": "^12.3.0",
"@types/bun": "^1.3.8",
"@types/node": "^25.1.0",
"@types/node": "^25.2.0",
"@types/send": "^1.2.1",
"@types/ws": "^8.18.1",
"@types/xml2js": "^0.4.14",
@@ -52,16 +53,14 @@
"typescript": "^5.9.3",
"ws": "npm:@kevisual/ws",
"xml2js": "^0.6.2",
"zod": "^4.3.6"
"zod": "^4.3.6",
"hono": "^4.11.7"
},
"repository": {
"type": "git",
"url": "git+https://github.com/abearxiong/kevisual-router.git"
},
"dependencies": {
"@kevisual/dts": "^0.0.3",
"hono": "^4.11.7"
},
"dependencies": {},
"publishConfig": {
"access": "public"
},
@@ -88,6 +87,7 @@
"require": "./dist/router-define.js",
"types": "./dist/router-define.d.ts"
},
"./ws": "./dist/ws.js",
"./mod.ts": {
"import": "./mod.ts",
"require": "./mod.ts",
@@ -102,4 +102,4 @@
"require": "./src/modules/*"
}
}
}
}

79
pnpm-lock.yaml generated
View File

@@ -7,17 +7,13 @@ settings:
importers:
.:
dependencies:
'@kevisual/dts':
specifier: ^0.0.3
version: 0.0.3(typescript@5.9.3)
hono:
specifier: ^4.11.7
version: 4.11.7
devDependencies:
'@kevisual/context':
specifier: ^0.0.4
version: 0.0.4
'@kevisual/dts':
specifier: ^0.0.3
version: 0.0.3(typescript@5.9.3)
'@kevisual/js-filter':
specifier: ^0.0.5
version: 0.0.5
@@ -25,14 +21,14 @@ importers:
specifier: ^0.0.8
version: 0.0.8
'@kevisual/query':
specifier: ^0.0.38
version: 0.0.38
specifier: ^0.0.39
version: 0.0.39
'@kevisual/use-config':
specifier: ^1.0.28
version: 1.0.28(dotenv@17.2.3)
specifier: ^1.0.30
version: 1.0.30(dotenv@17.2.3)
'@opencode-ai/plugin':
specifier: ^1.1.47
version: 1.1.47
specifier: ^1.1.48
version: 1.1.48
'@rollup/plugin-alias':
specifier: ^6.0.0
version: 6.0.0(rollup@4.57.1)
@@ -49,8 +45,8 @@ importers:
specifier: ^1.3.8
version: 1.3.8
'@types/node':
specifier: ^25.1.0
version: 25.1.0
specifier: ^25.2.0
version: 25.2.0
'@types/send':
specifier: ^1.2.1
version: 1.2.1
@@ -66,6 +62,9 @@ importers:
fast-glob:
specifier: ^3.3.3
version: 3.3.3
hono:
specifier: ^4.11.7
version: 4.11.7
nanoid:
specifier: ^5.1.6
version: 5.1.6
@@ -86,7 +85,7 @@ importers:
version: 9.5.4(typescript@5.9.3)(webpack@5.104.1)
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@25.1.0)(typescript@5.9.3)
version: 10.9.2(@types/node@25.2.0)(typescript@5.9.3)
tslib:
specifier: ^2.8.1
version: 2.8.1
@@ -117,7 +116,7 @@ importers:
version: 1.1.1
ts-node:
specifier: ^10.9.2
version: 10.9.2(@types/node@25.1.0)(typescript@5.9.3)
version: 10.9.2(@types/node@25.2.0)(typescript@5.9.3)
typescript:
specifier: ^5.5.4
version: 5.9.3
@@ -327,11 +326,11 @@ packages:
'@kevisual/local-proxy@0.0.8':
resolution: {integrity: sha512-VX/P+6/Cc8ruqp34ag6gVX073BchUmf5VNZcTV/6MJtjrNE76G8V6TLpBE8bywLnrqyRtFLIspk4QlH8up9B5Q==}
'@kevisual/query@0.0.38':
resolution: {integrity: sha512-bfvbSodsZyMfwY+1T2SvDeOCKsT/AaIxlVe0+B1R/fNhlg2MDq2CP0L9HKiFkEm+OXrvXcYDMKPUituVUM5J6Q==}
'@kevisual/query@0.0.39':
resolution: {integrity: sha512-3UEPBIvtdykNkrby3hvrgrHdgd17Uq+Pnr4zs+JBzATkU2eKaOqtTUJqdyIEwuySCwzGTxrnlUzWP4tziDQDLQ==}
'@kevisual/use-config@1.0.28':
resolution: {integrity: sha512-ngF+LDbjxpXWrZNmnShIKF/jPpAa+ezV+DcgoZIIzHlRnIjE+rr9sLkN/B7WJbiH9C/j1tQXOILY8ujBqILrow==}
'@kevisual/use-config@1.0.30':
resolution: {integrity: sha512-kPdna0FW/X7D600aMdiZ5UTjbCo6d8d4jjauSc8RMmBwUU6WliFDSPUNKVpzm2BsDX5Nth1IXFPYMqH+wxqAmw==}
peerDependencies:
dotenv: ^17
@@ -351,11 +350,11 @@ packages:
resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==}
engines: {node: '>= 8'}
'@opencode-ai/plugin@1.1.47':
resolution: {integrity: sha512-gNMPz72altieDfLhUw3VAT1xbduKi3w3wZ57GLeS7qU9W474HdvdIiLBnt2Xq3U7Ko0/0tvK3nzCker6IIDqmQ==}
'@opencode-ai/plugin@1.1.48':
resolution: {integrity: sha512-KkaSMevXmz7tOwYDMJeWiXE5N8LmRP18qWI5Xhv3+c+FdGPL+l1hQrjSgyv3k7Co7qpCyW3kAUESBB7BzIOl2w==}
'@opencode-ai/sdk@1.1.47':
resolution: {integrity: sha512-s3PBHwk1sP6Zt/lJxIWSBWZ1TnrI1nFxSP97LCODUytouAQgbygZ1oDH7O2sGMBEuGdA8B1nNSPla0aRSN3IpA==}
'@opencode-ai/sdk@1.1.48':
resolution: {integrity: sha512-j5/79X45fUPWVD2Ffm/qvwLclDCdPeV+TYMDrm9to0p4pmzhmeKevCsyiRdLg0o0HE3AFRUnOo2rdO9NetN79A==}
'@rollup/plugin-alias@6.0.0':
resolution: {integrity: sha512-tPCzJOtS7uuVZd+xPhoy5W4vThe6KWXNmsFCNktaAh5RTqcLiSfT4huPQIXkgJ6YCOjJHvecOAzQxLFhPxKr+g==}
@@ -580,8 +579,8 @@ packages:
'@types/json-schema@7.0.15':
resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==}
'@types/node@25.1.0':
resolution: {integrity: sha512-t7frlewr6+cbx+9Ohpl0NOTKXZNV9xHRmNOvql47BFJKcEG1CxtxlPEEe+gR9uhVWM4DwhnvTF110mIL4yP9RA==}
'@types/node@25.2.0':
resolution: {integrity: sha512-DZ8VwRFUNzuqJ5khrvwMXHmvPe+zGayJhr2CDNiKB1WBE1ST8Djl00D0IC4vvNmHMdj6DlbYRIaFE7WHjlDl5w==}
'@types/resolve@1.20.2':
resolution: {integrity: sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==}
@@ -1342,11 +1341,11 @@ snapshots:
'@kevisual/local-proxy@0.0.8': {}
'@kevisual/query@0.0.38':
'@kevisual/query@0.0.39':
dependencies:
tslib: 2.8.1
'@kevisual/use-config@1.0.28(dotenv@17.2.3)':
'@kevisual/use-config@1.0.30(dotenv@17.2.3)':
dependencies:
'@kevisual/load': 0.0.6
dotenv: 17.2.3
@@ -1365,12 +1364,12 @@ snapshots:
'@nodelib/fs.scandir': 2.1.5
fastq: 1.20.1
'@opencode-ai/plugin@1.1.47':
'@opencode-ai/plugin@1.1.48':
dependencies:
'@opencode-ai/sdk': 1.1.47
'@opencode-ai/sdk': 1.1.48
zod: 4.1.8
'@opencode-ai/sdk@1.1.47': {}
'@opencode-ai/sdk@1.1.48': {}
'@rollup/plugin-alias@6.0.0(rollup@4.57.1)':
optionalDependencies:
@@ -1528,7 +1527,7 @@ snapshots:
'@types/json-schema@7.0.15': {}
'@types/node@25.1.0':
'@types/node@25.2.0':
dependencies:
undici-types: 7.16.0
@@ -1536,15 +1535,15 @@ snapshots:
'@types/send@1.2.1':
dependencies:
'@types/node': 25.1.0
'@types/node': 25.2.0
'@types/ws@8.18.1':
dependencies:
'@types/node': 25.1.0
'@types/node': 25.2.0
'@types/xml2js@0.4.14':
dependencies:
'@types/node': 25.1.0
'@types/node': 25.2.0
'@webassemblyjs/ast@1.14.1':
dependencies:
@@ -1676,7 +1675,7 @@ snapshots:
bun-types@1.3.8:
dependencies:
'@types/node': 25.1.0
'@types/node': 25.2.0
caniuse-lite@1.0.30001761: {}
@@ -1861,7 +1860,7 @@ snapshots:
jest-worker@27.5.1:
dependencies:
'@types/node': 25.1.0
'@types/node': 25.2.0
merge-stream: 2.0.0
supports-color: 8.1.1
@@ -2076,14 +2075,14 @@ snapshots:
typescript: 5.9.3
webpack: 5.104.1
ts-node@10.9.2(@types/node@25.1.0)(typescript@5.9.3):
ts-node@10.9.2(@types/node@25.2.0)(typescript@5.9.3):
dependencies:
'@cspotcode/source-map-support': 0.8.1
'@tsconfig/node10': 1.0.12
'@tsconfig/node12': 1.0.11
'@tsconfig/node14': 1.0.3
'@tsconfig/node16': 1.0.4
'@types/node': 25.1.0
'@types/node': 25.2.0
acorn: 8.15.0
acorn-walk: 8.3.4
arg: 4.1.3

View File

@@ -148,4 +148,27 @@ export default [
},
plugins: [dts()],
},
{
input: 'src/ws.ts',
output: {
file: 'dist/ws.js',
format: 'es',
},
external: ['ws'],
plugins: [
resolve({
// browser: true,
}),
commonjs(),
typescript(),
],
},
{
input: 'src/ws.ts',
output: {
file: 'dist/ws.d.ts',
format: 'es',
},
plugins: [dts()],
},
];

170
src/server/reconnect-ws.ts Normal file
View File

@@ -0,0 +1,170 @@
import WebSocket from 'ws';
export type ReconnectConfig = {
/**
* 重连配置选项, 最大重试次数,默认无限
*/
maxRetries?: number;
/**
* 重连配置选项, 重试延迟(ms)默认1000
*/
retryDelay?: number;
/**
* 重连配置选项, 最大延迟(ms)默认30000
*/
maxDelay?: number;
/**
* 重连配置选项, 退避倍数默认2
*/
backoffMultiplier?: number;
};
/**
* 一个支持自动重连的 WebSocket 客户端。
* 在连接断开时会根据配置进行重连尝试,支持指数退避。
*/
export class ReconnectingWebSocket {
private ws: WebSocket | null = null;
private url: string;
private config: Required<ReconnectConfig>;
private retryCount: number = 0;
private reconnectTimer: NodeJS.Timeout | null = null;
private isManualClose: boolean = false;
private messageHandlers: Array<(data: any) => void> = [];
private openHandlers: Array<() => void> = [];
private closeHandlers: Array<(code: number, reason: Buffer) => void> = [];
private errorHandlers: Array<(error: Error) => void> = [];
constructor(url: string, config: ReconnectConfig = {}) {
this.url = url;
this.config = {
maxRetries: config.maxRetries ?? Infinity,
retryDelay: config.retryDelay ?? 1000,
maxDelay: config.maxDelay ?? 30000,
backoffMultiplier: config.backoffMultiplier ?? 2,
};
}
log(...args: any[]): void {
console.log('[ReconnectingWebSocket]', ...args);
}
error(...args: any[]): void {
console.error('[ReconnectingWebSocket]', ...args);
}
connect(): void {
if (this.ws?.readyState === WebSocket.OPEN) {
return;
}
this.log(`正在连接到 ${this.url}...`);
this.ws = new WebSocket(this.url);
this.ws.on('open', () => {
this.log('WebSocket 连接已打开');
this.retryCount = 0;
this.openHandlers.forEach(handler => handler());
this.send({ type: 'heartbeat', timestamp: new Date().toISOString() });
});
this.ws.on('message', (data: any) => {
this.messageHandlers.forEach(handler => {
try {
const message = JSON.parse(data.toString());
handler(message);
} catch {
handler(data.toString());
}
});
});
this.ws.on('close', (code: number, reason: Buffer) => {
this.log(`WebSocket 连接已关闭: code=${code}, reason=${reason.toString()}`);
this.closeHandlers.forEach(handler => handler(code, reason));
if (!this.isManualClose) {
this.scheduleReconnect();
}
});
this.ws.on('error', (error: Error) => {
this.error('WebSocket 错误:', error.message);
this.errorHandlers.forEach(handler => handler(error));
});
}
private scheduleReconnect(): void {
if (this.reconnectTimer) {
return;
}
if (this.retryCount >= this.config.maxRetries) {
this.error(`已达到最大重试次数 (${this.config.maxRetries}),停止重连`);
return;
}
// 计算延迟(指数退避)
const delay = Math.min(
this.config.retryDelay * Math.pow(this.config.backoffMultiplier, this.retryCount),
this.config.maxDelay
);
this.retryCount++;
this.log(`将在 ${delay}ms 后进行第 ${this.retryCount} 次重连尝试...`);
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = null;
this.connect();
}, delay);
}
send(data: any): boolean {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data));
return true;
}
this.log('WebSocket 未连接,无法发送消息');
return false;
}
onMessage(handler: (data: any) => void): void {
this.messageHandlers.push(handler);
}
onOpen(handler: () => void): void {
this.openHandlers.push(handler);
}
onClose(handler: (code: number, reason: Buffer) => void): void {
this.closeHandlers.push(handler);
}
onError(handler: (error: Error) => void): void {
this.errorHandlers.push(handler);
}
close(): void {
this.isManualClose = true;
if (this.reconnectTimer) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.ws) {
this.ws.close();
this.ws = null;
}
}
getReadyState(): number {
return this.ws?.readyState ?? WebSocket.CLOSED;
}
getRetryCount(): number {
return this.retryCount;
}
}
// const ws = new ReconnectingWebSocket('ws://localhost:51516/livecode/ws?id=test-live-app', {
// maxRetries: Infinity, // 无限重试
// retryDelay: 1000, // 初始重试延迟 1 秒
// maxDelay: 30000, // 最大延迟 30 秒
// backoffMultiplier: 2, // 指数退避倍数
// });

65
src/ws.ts Normal file
View File

@@ -0,0 +1,65 @@
import { ReconnectingWebSocket, ReconnectConfig } from "./server/reconnect-ws.ts";
export * from "./server/reconnect-ws.ts";
import type { App } from "./app.ts";
export const handleCallWsApp = async (ws: ReconnectingWebSocket, app: App, message: any) => {
return handleCallApp((data: any) => {
ws.send(data);
}, app, message);
}
export const handleCallApp = async (send: (data: any) => void, app: App, message: any) => {
if (message.type === 'router' && message.id) {
const data = message?.data;
if (!message.id) {
console.error('Message id is required for router type');
return;
}
if (!data) {
send({
type: 'router',
id: message.id,
data: { code: 500, message: 'No data received' }
});
return;
}
const { tokenUser, ...rest } = data || {};
const res = await app.run(rest, {
state: { tokenUser },
appId: app.appId,
});
send({
type: 'router',
id: message.id,
data: res
});
}
}
export class Ws {
wsClient: ReconnectingWebSocket;
app: App;
showLog: boolean = true;
constructor(opts?: ReconnectConfig & {
url: string;
app: App;
showLog?: boolean;
handleMessage?: (ws: ReconnectingWebSocket, app: App, message: any) => void;
}) {
const { url, app, showLog = true, handleMessage = handleCallWsApp, ...rest } = opts;
this.wsClient = new ReconnectingWebSocket(url, rest);
this.app = app;
this.showLog = showLog;
this.wsClient.connect();
const onMessage = async (data: any) => {
return handleMessage(this.wsClient, this.app, data);
}
this.wsClient.onMessage(onMessage);
}
send(data: any): boolean {
return this.wsClient.send(data);
}
log(...args: any[]): void {
if (this.showLog)
console.log('[Ws]', ...args);
}
}