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

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