Files
clash-verge-rev-lite/src/utils/websocket.ts

263 lines
6.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import Sockette, { type SocketteOptions } from "sockette";
/**
* A wrapper of Sockette that will automatically reconnect up to `maxError` before emitting an error event.
*/
export const createSockette = (
url: string,
opt: SocketteOptions,
maxError = 10,
) => {
let remainRetryCount = maxError;
return new Sockette(url, {
...opt,
// Sockette has a built-in reconnect when ECONNREFUSED feature
// Use maxError if opt.maxAttempts is not specified
maxAttempts: opt.maxAttempts ?? maxError,
onmessage(this: Sockette, ev) {
remainRetryCount = maxError; // reset counter
opt.onmessage?.call(this, ev);
},
onerror(this: Sockette, ev) {
remainRetryCount -= 1;
if (remainRetryCount >= 0) {
if (this instanceof Sockette) {
this.close();
this.reconnect();
}
} else {
opt.onerror?.call(this, ev);
}
},
onmaximum(this: Sockette, ev) {
opt.onmaximum?.call(this, ev);
// onmaximum will be fired when Sockette reaches built-in reconnect limit,
// We will also set remainRetryCount to 0 to prevent further reconnect.
remainRetryCount = 0;
},
});
};
/**
* 创建一个支持认证的WebSocket连接
* 使用标准的URL参数方式添加token
*
* 注意mihomo服务器对WebSocket的认证支持不佳使用URL参数方式传递token
*/
export const createAuthSockette = (
baseUrl: string,
secret: string,
opt: SocketteOptions,
maxError = 10,
) => {
// 确保baseUrl格式正确
let url = baseUrl;
if (!url.startsWith("ws://") && !url.startsWith("wss://")) {
url = `ws://${url}`;
}
// 重试控制
let reconnectAttempts = 0;
const MAX_RECONNECT = maxError;
let reconnectTimeout: any = null;
let ws: WebSocket | null = null;
// 使用URL API解析和构建URL
try {
const urlObj = new URL(url);
// 添加token参数如果有secret
if (secret) {
urlObj.searchParams.delete("token");
urlObj.searchParams.append("token", secret);
}
url = urlObj.toString();
console.log(`[WebSocket] 创建连接: ${url.replace(secret || "", "***")}`);
} catch (e) {
console.error(`[WebSocket] URL格式错误: ${url}`, e);
if (opt.onerror) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onerror(
new ErrorEvent("error", { message: `URL格式错误: ${e}` } as any),
);
}
return createDummySocket();
}
function connect() {
try {
ws = new WebSocket(url);
ws.onopen = function (event) {
console.log(
`[WebSocket] 连接成功: ${url.replace(secret || "", "***")}`,
);
reconnectAttempts = 0; // 重置重连计数
if (opt.onopen) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onopen(event);
}
};
ws.onmessage = function (event) {
if (opt.onmessage) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onmessage(event);
}
};
ws.onerror = function (event) {
console.error(
`[WebSocket] 连接错误: ${url.replace(secret || "", "***")}`,
);
// 错误处理
if (reconnectAttempts < MAX_RECONNECT) {
scheduleReconnect();
} else if (opt.onerror) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onerror(event);
}
};
ws.onclose = function (event) {
console.log(
`[WebSocket] 连接关闭: ${url.replace(secret || "", "***")}, 代码: ${event.code}`,
);
// 如果不是正常关闭(1000, 1001),尝试重连
if (
event.code !== 1000 &&
event.code !== 1001 &&
reconnectAttempts < MAX_RECONNECT
) {
scheduleReconnect();
} else {
if (opt.onclose) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onclose(event);
}
// 如果已达到最大重试次数
if (reconnectAttempts >= MAX_RECONNECT && opt.onmaximum) {
console.error(
`[WebSocket] 达到最大重试次数: ${url.replace(secret || "", "***")}`,
);
const anyOpt = opt as any;
anyOpt.onmaximum(event);
}
}
};
} catch (error) {
console.error(`[WebSocket] 创建连接失败:`, error);
if (opt.onerror) {
// 使用任意类型避免类型错误
const anyOpt = opt as any;
anyOpt.onerror(
new ErrorEvent("error", { message: `创建连接失败: ${error}` } as any),
);
}
}
}
function scheduleReconnect() {
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
}
reconnectAttempts++;
const delay = Math.min(1000 * Math.pow(1.5, reconnectAttempts), 10000); // 指数退避最大10秒
console.log(
`[WebSocket] 计划重连 (${reconnectAttempts}/${MAX_RECONNECT}) 延迟: ${delay}ms`,
);
reconnectTimeout = setTimeout(() => {
console.log(
`[WebSocket] 尝试重连 (${reconnectAttempts}/${MAX_RECONNECT})`,
);
cleanup();
connect();
}, delay);
}
function cleanup() {
if (ws) {
// 移除所有事件监听器
ws.onopen = null;
ws.onmessage = null;
ws.onerror = null;
ws.onclose = null;
// 如果连接仍然打开,关闭它
if (
ws.readyState === WebSocket.OPEN ||
ws.readyState === WebSocket.CONNECTING
) {
try {
ws.close();
} catch (e) {
console.error("[WebSocket] 关闭连接时出错:", e);
}
}
ws = null;
}
// 清除重连计时器
if (reconnectTimeout) {
clearTimeout(reconnectTimeout);
reconnectTimeout = null;
}
}
// 创建一个类似Sockette的接口对象
const socketLike = {
ws,
close: () => {
console.log(
`[WebSocket] 手动关闭连接: ${url.replace(secret || "", "***")}`,
);
cleanup();
},
reconnect: () => {
cleanup();
connect();
},
json: (data: any) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify(data));
}
},
send: (data: string) => {
if (ws && ws.readyState === WebSocket.OPEN) {
ws.send(data);
}
},
open: connect,
};
// 立即连接
connect();
return socketLike;
};
// 创建一个空的WebSocket对象
function createDummySocket() {
return {
close: () => {},
reconnect: () => {},
json: () => {},
send: () => {},
open: () => {},
};
}