mstf-kit
Version:
一个现代化的 JavaScript/TypeScript 工具库,提供了丰富的常用工具函数
851 lines (850 loc) • 25.7 kB
JavaScript
var WebSocketState = /* @__PURE__ */ ((WebSocketState2) => {
WebSocketState2["CONNECTING"] = "connecting";
WebSocketState2["OPEN"] = "open";
WebSocketState2["CLOSING"] = "closing";
WebSocketState2["CLOSED"] = "closed";
WebSocketState2["RECONNECTING"] = "reconnecting";
return WebSocketState2;
})(WebSocketState || {});
var MessagePriority = /* @__PURE__ */ ((MessagePriority2) => {
MessagePriority2["HIGH"] = "high";
MessagePriority2["NORMAL"] = "normal";
MessagePriority2["LOW"] = "low";
return MessagePriority2;
})(MessagePriority || {});
class WebSocketClient {
/**
* 创建WebSocket客户端实例
* @param options WebSocket配置选项
*/
constructor(options) {
this.socket = null;
this.state = "closed";
this.messageQueue = [];
this.pendingMessages = /* @__PURE__ */ new Map();
this.reconnectAttempts = 0;
this.reconnectTimer = null;
this.heartbeatTimer = null;
this.heartbeatTimeoutTimer = null;
this.batchTimer = null;
this.batchedMessages = [];
this.eventListeners = /* @__PURE__ */ new Map();
this.connectionStartTime = 0;
this.lastMessageTime = 0;
this.metrics = {
messagesSent: 0,
messagesReceived: 0,
bytesReceived: 0,
bytesSent: 0,
errors: 0,
reconnects: 0
};
this.defaultHeartbeatConfig = {
enabled: true,
interval: 3e4,
message: { type: "ping" },
timeout: 5e3
};
this.defaultReconnectConfig = {
enabled: true,
maxRetries: -1,
// 无限重试
initialDelay: 1e3,
maxDelay: 3e4,
factor: 1.5,
jitter: 0.5
};
this.defaultMessageConfig = {
timeout: 1e4,
maxQueueSize: 100,
autoJsonStringify: true,
autoJsonParse: true,
batchThreshold: 0,
// 默认不开启批处理
batchTimeWindow: 50
};
this.options = options;
this.heartbeatConfig = { ...this.defaultHeartbeatConfig, ...options.heartbeat };
this.reconnectConfig = { ...this.defaultReconnectConfig, ...options.reconnect };
this.messageConfig = { ...this.defaultMessageConfig, ...options.message };
["open", "close", "error", "message", "reconnect", "reconnect_failed", "reconnecting", "heartbeat"].forEach((event) => {
this.eventListeners.set(event, /* @__PURE__ */ new Set());
});
}
/**
* 连接到WebSocket服务器
* @returns 连接Promise
*/
connect() {
if (this.socket && (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING)) {
return Promise.resolve(this);
}
this.setState(
"connecting"
/* CONNECTING */
);
this.connectionStartTime = Date.now();
return new Promise((resolve, reject) => {
try {
this.socket = new WebSocket(this.options.url, this.options.protocols);
this.socket.binaryType = "arraybuffer";
const connectionTimeout = setTimeout(() => {
if (this.state === "connecting") {
const error = new Error(`连接超时: ${this.options.connectionTimeout}ms`);
this.log("error", error.message);
this.cleanupSocket();
reject(error);
this.handleReconnect();
}
}, this.options.connectionTimeout || 1e4);
this.socket.onopen = async (event) => {
clearTimeout(connectionTimeout);
this.setState(
"open"
/* OPEN */
);
this.reconnectAttempts = 0;
this.log("info", `连接成功,耗时: ${Date.now() - this.connectionStartTime}ms`);
if (this.options.authHandler) {
try {
await this.options.authHandler(this);
this.log("info", "认证成功");
} catch (authError) {
this.log("error", "认证失败", authError);
this.close(4e3, "认证失败");
reject(new Error("认证失败"));
return;
}
}
this.startHeartbeat();
this.processPendingMessages();
this.emit("open", event);
resolve(this);
};
this.socket.onmessage = (event) => {
this.lastMessageTime = Date.now();
let data = event.data;
this.metrics.messagesReceived++;
if (typeof data === "string") {
this.metrics.bytesReceived += data.length;
} else if (data instanceof ArrayBuffer) {
this.metrics.bytesReceived += data.byteLength;
} else if (data instanceof Blob) {
this.metrics.bytesReceived += data.size;
}
if (this.messageConfig.autoJsonParse && typeof data === "string") {
try {
data = JSON.parse(data);
} catch (e) {
this.log("warn", "无法解析JSON消息", e);
}
}
if (data && typeof data === "object" && (data.type === "pong" || data.type === "ping" || data.type === "heartbeat")) {
this.handleHeartbeatResponse(data);
return;
}
if (data && typeof data === "object" && data.id && this.pendingMessages.has(data.id)) {
const pending = this.pendingMessages.get(data.id);
clearTimeout(pending.timeout);
this.pendingMessages.delete(data.id);
pending.resolve(data);
this.log("debug", `收到消息响应: ${data.id}`);
} else {
if (this.options.messageHandler) {
this.options.messageHandler(data, this);
}
this.emit("message", data);
}
};
this.socket.onclose = (event) => {
clearTimeout(connectionTimeout);
const wasConnected = this.state === "open";
this.setState(
"closed"
/* CLOSED */
);
this.log("info", `连接关闭: [${event.code}] ${event.reason || "未提供原因"}`);
this.stopHeartbeat();
if (wasConnected && event.code !== 1e3 && event.code !== 1001) {
this.handleReconnect();
}
this.emit("close", event);
if (this.state === "connecting") {
reject(new Error(`连接失败: [${event.code}] ${event.reason || "未提供原因"}`));
}
};
this.socket.onerror = (event) => {
this.metrics.errors++;
this.log("error", "连接错误", event);
this.emit("error", event);
if (this.state === "connecting") {
clearTimeout(connectionTimeout);
reject(new Error("连接错误"));
}
};
} catch (error) {
this.log("error", "创建WebSocket实例失败", error);
this.setState(
"closed"
/* CLOSED */
);
reject(error);
}
});
}
/**
* 关闭WebSocket连接
* @param code 关闭代码
* @param reason 关闭原因
*/
close(code, reason) {
if (!this.socket || this.state === "closed") {
return;
}
this.log("info", `主动关闭连接: [${code || 1e3}] ${reason || ""}`);
this.stopHeartbeat();
this.stopReconnect();
this.rejectAllPendingMessages(new Error("连接关闭"));
try {
this.setState(
"closing"
/* CLOSING */
);
this.socket.close(code || 1e3, reason);
} catch (e) {
this.log("error", "关闭连接错误", e);
}
}
/**
* 发送消息
* @param data 要发送的数据
* @param options 发送选项
* @returns 如果expectResponse为true,则返回响应数据的Promise
*/
send(data, options = {}) {
const defaultOptions = {
id: this.generateId(),
expectResponse: false,
timeout: this.messageConfig.timeout,
retries: 0,
retryDelay: 1e3,
priority: "normal",
cacheIfOffline: true,
forceText: false
};
const sendOptions = { ...defaultOptions, ...options };
return new Promise((resolve, reject) => {
const message = {
id: sendOptions.id,
data,
options: sendOptions,
resolve,
reject,
timestamp: Date.now(),
retryCount: 0
};
if (this.state !== "open") {
if (sendOptions.cacheIfOffline) {
this.queueMessage(message);
this.log("debug", `消息已加入队列 (ID: ${message.id})`);
return;
} else {
return reject(new Error("未连接到服务器"));
}
}
if (this.messageConfig.batchThreshold > 0 && !sendOptions.expectResponse) {
this.addToBatch(message);
return;
}
this.sendMessage(message);
});
}
/**
* 订阅WebSocket事件
* @param event 事件名称
* @param listener 监听器函数
* @returns this 实例,用于链式调用
*/
on(event, listener) {
if (!this.eventListeners.has(event)) {
this.eventListeners.set(event, /* @__PURE__ */ new Set());
}
this.eventListeners.get(event).add(listener);
return this;
}
/**
* 取消订阅WebSocket事件
* @param event 事件名称
* @param listener 监听器函数
* @returns this 实例,用于链式调用
*/
off(event, listener) {
if (this.eventListeners.has(event)) {
this.eventListeners.get(event).delete(listener);
}
return this;
}
/**
* 获取当前WebSocket状态
* @returns 当前状态
*/
getState() {
return this.state;
}
/**
* 获取延迟时间 (从最后一次心跳响应计算)
* @returns 延迟时间 (ms)
*/
getLatency() {
return this.lastMessageTime > 0 ? Date.now() - this.lastMessageTime : -1;
}
/**
* 获取连接统计信息
* @returns 统计指标
*/
getMetrics() {
return { ...this.metrics };
}
/**
* 清除消息队列
*/
clearQueue() {
this.messageQueue.forEach((message) => {
message.reject(new Error("消息队列已清除"));
});
this.messageQueue = [];
}
/**
* 检查连接是否活跃
* @returns 是否为活跃连接
*/
isConnected() {
return this.state === "open";
}
/**
* 手动触发重连
* @returns 重连Promise
*/
reconnect() {
if (this.state === "open" || this.state === "connecting") {
this.cleanupSocket();
}
this.reconnectAttempts = 0;
return this.handleReconnect(true);
}
// ===== 私有辅助方法 =====
/**
* 触发事件
* @param event 事件名称
* @param data 事件数据
*/
emit(event, data) {
if (this.eventListeners.has(event)) {
this.eventListeners.get(event).forEach((listener) => {
try {
listener(data);
} catch (e) {
this.log("error", `事件监听器错误 (${event})`, e);
}
});
}
}
/**
* 设置WebSocket状态
* @param state 新状态
*/
setState(state) {
this.log("debug", `状态变更: ${this.state} -> ${state}`);
this.state = state;
}
/**
* 生成唯一消息ID
* @returns 唯一ID
*/
generateId() {
return Date.now().toString(36) + Math.random().toString(36).substr(2, 5);
}
/**
* 发送实际的WebSocket消息
* @param message 消息对象
*/
sendMessage(message) {
var _a;
if (!this.socket || this.state !== "open") {
if (message.options.cacheIfOffline) {
this.queueMessage(message);
} else {
message.reject(new Error("未连接到服务器"));
}
return;
}
try {
let sendData = message.data;
let isBinary = message.options.binary === true;
if (this.messageConfig.autoJsonStringify && typeof sendData !== "string" && !(sendData instanceof ArrayBuffer) && !(sendData instanceof Blob)) {
if (message.options.expectResponse && typeof sendData === "object") {
sendData = { ...sendData, id: message.id };
}
sendData = JSON.stringify(sendData);
isBinary = false;
}
this.socket.send(sendData);
this.metrics.messagesSent++;
if (typeof sendData === "string") {
this.metrics.bytesSent += sendData.length;
} else if (sendData instanceof ArrayBuffer) {
this.metrics.bytesSent += sendData.byteLength;
} else if (sendData instanceof Blob) {
this.metrics.bytesSent += sendData.size;
}
this.log("debug", `已发送消息 (ID: ${message.id})`);
if (message.options.expectResponse) {
this.pendingMessages.set(message.id, message);
message.timeout = setTimeout(() => {
this.pendingMessages.delete(message.id);
if (message.retryCount < (message.options.retries || 0)) {
message.retryCount++;
this.log("warn", `消息超时,正在重试 (${message.retryCount}/${message.options.retries}) ID: ${message.id}`);
setTimeout(() => {
this.sendMessage(message);
}, message.options.retryDelay);
} else {
message.reject(new Error(`消息超时 (ID: ${message.id})`));
}
}, message.options.timeout);
} else {
message.resolve(void 0);
}
} catch (error) {
this.log("error", "发送消息失败", error);
if (message.options.cacheIfOffline && (((_a = this.socket) == null ? void 0 : _a.readyState) !== WebSocket.OPEN || error instanceof Error && error.message.includes("INVALID_STATE_ERR"))) {
this.queueMessage(message);
} else {
message.reject(error);
}
if (this.state === "open") {
this.handleConnectionFailure();
}
}
}
/**
* 将消息添加到队列
* @param message 消息对象
*/
queueMessage(message) {
if (this.messageQueue.length >= this.messageConfig.maxQueueSize) {
if (message.options.priority === "high") {
const lowPriorityIndex = this.messageQueue.findIndex(
(m) => m.options.priority === "low"
/* LOW */
);
if (lowPriorityIndex >= 0) {
const removed = this.messageQueue.splice(lowPriorityIndex, 1)[0];
removed.reject(new Error("由于队列已满,低优先级消息被丢弃"));
} else {
message.reject(new Error("消息队列已满"));
return;
}
} else {
message.reject(new Error("消息队列已满"));
return;
}
}
if (message.options.priority === "high") {
this.messageQueue.unshift(message);
} else if (message.options.priority === "low") {
this.messageQueue.push(message);
} else {
const lastNormalIndex = [...this.messageQueue].reverse().findIndex(
(m) => m.options.priority !== "low"
/* LOW */
);
if (lastNormalIndex >= 0) {
this.messageQueue.splice(this.messageQueue.length - lastNormalIndex, 0, message);
} else {
this.messageQueue.push(message);
}
}
}
/**
* 处理队列中的消息
*/
processPendingMessages() {
if (this.messageQueue.length === 0 || this.state !== "open") {
return;
}
this.log("info", `处理排队消息: ${this.messageQueue.length}条`);
const pendingMessages = [...this.messageQueue];
this.messageQueue = [];
pendingMessages.forEach((message) => {
this.sendMessage(message);
});
}
/**
* 将消息添加到批处理
* @param message 消息对象
*/
addToBatch(message) {
this.batchedMessages.push(message.data);
if (this.batchedMessages.length >= this.messageConfig.batchThreshold || this.batchedMessages.length === 1 && this.batchTimer === null) {
this.flushBatchMessages();
}
message.resolve(void 0);
}
/**
* 发送批处理消息
*/
flushBatchMessages() {
if (this.batchedMessages.length === 0) {
return;
}
if (this.batchTimer !== null) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
if (this.state === "open") {
const batchData = {
type: "batch",
messages: [...this.batchedMessages],
count: this.batchedMessages.length
};
try {
const data = this.messageConfig.autoJsonStringify || typeof batchData !== "string" ? JSON.stringify(batchData) : batchData;
this.socket.send(data);
this.metrics.messagesSent++;
if (typeof data === "string") {
this.metrics.bytesSent += data.length;
}
this.log("debug", `发送批量消息: ${batchData.count}条`);
} catch (error) {
this.log("error", "发送批量消息失败", error);
if (this.state === "open") {
this.handleConnectionFailure();
}
}
} else {
this.batchedMessages.forEach((data) => {
this.queueMessage({
id: this.generateId(),
data,
options: {
cacheIfOffline: true,
priority: "normal"
/* NORMAL */
},
resolve: () => {
},
reject: () => {
},
timestamp: Date.now(),
retryCount: 0
});
});
}
this.batchedMessages = [];
this.batchTimer = setTimeout(() => {
this.batchTimer = null;
this.flushBatchMessages();
}, this.messageConfig.batchTimeWindow);
}
/**
* 启动心跳
*/
startHeartbeat() {
if (!this.heartbeatConfig.enabled || this.heartbeatTimer !== null) {
return;
}
this.log("debug", `启动心跳: ${this.heartbeatConfig.interval}ms`);
this.heartbeatTimer = setInterval(() => {
this.sendHeartbeat();
}, this.heartbeatConfig.interval);
}
/**
* 发送心跳
*/
sendHeartbeat() {
if (this.state !== "open") {
return;
}
this.log("debug", "发送心跳");
try {
const data = this.messageConfig.autoJsonStringify ? JSON.stringify(this.heartbeatConfig.message) : this.heartbeatConfig.message;
this.socket.send(data);
this.emit("heartbeat", "sent");
this.heartbeatTimeoutTimer = setTimeout(() => {
this.log("warn", "心跳超时");
this.emit("heartbeat", "timeout");
this.handleConnectionFailure();
}, this.heartbeatConfig.timeout);
} catch (error) {
this.log("error", "发送心跳失败", error);
this.handleConnectionFailure();
}
}
/**
* 处理心跳响应
* @param data 响应数据
*/
handleHeartbeatResponse(data) {
if (this.heartbeatTimeoutTimer !== null) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
this.log("debug", "收到心跳响应");
this.emit("heartbeat", "received");
}
/**
* 停止心跳
*/
stopHeartbeat() {
if (this.heartbeatTimer !== null) {
clearInterval(this.heartbeatTimer);
this.heartbeatTimer = null;
}
if (this.heartbeatTimeoutTimer !== null) {
clearTimeout(this.heartbeatTimeoutTimer);
this.heartbeatTimeoutTimer = null;
}
}
/**
* 处理连接失败
*/
handleConnectionFailure() {
if (this.state !== "open") {
return;
}
this.log("warn", "检测到连接问题");
this.cleanupSocket();
this.handleReconnect();
}
/**
* 处理重连逻辑
* @param immediate 是否立即重连
* @returns 重连Promise
*/
handleReconnect(immediate = false) {
if (!this.reconnectConfig.enabled) {
this.log("info", "自动重连已禁用");
return Promise.reject(new Error("自动重连已禁用"));
}
if (this.reconnectTimer !== null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
if (this.reconnectConfig.maxRetries !== -1 && this.reconnectAttempts >= this.reconnectConfig.maxRetries) {
this.log("error", `重连失败: 已超过最大重试次数 (${this.reconnectConfig.maxRetries})`);
this.emit("reconnect_failed", this.reconnectAttempts);
return Promise.reject(new Error(`已超过最大重试次数 (${this.reconnectConfig.maxRetries})`));
}
let delay = immediate ? 0 : this.calculateReconnectDelay();
this.setState(
"reconnecting"
/* RECONNECTING */
);
this.reconnectAttempts++;
this.log("info", `尝试重连: 第${this.reconnectAttempts}次 (延迟: ${delay}ms)`);
this.emit("reconnecting", this.reconnectAttempts);
return new Promise((resolve, reject) => {
this.reconnectTimer = setTimeout(async () => {
this.reconnectTimer = null;
try {
await this.connect();
this.metrics.reconnects++;
this.emit("reconnect", this.reconnectAttempts);
resolve(this);
} catch (error) {
this.log("error", `重连失败: ${error}`);
this.handleReconnect().then(resolve).catch(reject);
}
}, delay);
});
}
/**
* 计算重连延迟时间
* @returns 延迟时间 (ms)
*/
calculateReconnectDelay() {
let delay = this.reconnectConfig.initialDelay * Math.pow(this.reconnectConfig.factor, this.reconnectAttempts);
const jitter = this.reconnectConfig.jitter;
if (jitter > 0) {
delay = delay * (1 + jitter * (Math.random() * 2 - 1));
}
return Math.min(delay, this.reconnectConfig.maxDelay);
}
/**
* 清理Socket资源
*/
cleanupSocket() {
if (!this.socket) {
return;
}
this.log("debug", "清理Socket资源");
this.stopHeartbeat();
this.socket.onopen = null;
this.socket.onclose = null;
this.socket.onerror = null;
this.socket.onmessage = null;
try {
if (this.socket.readyState === WebSocket.OPEN || this.socket.readyState === WebSocket.CONNECTING) {
this.socket.close();
}
} catch (e) {
this.log("error", "关闭socket错误", e);
}
this.socket = null;
}
/**
* 停止重连尝试
*/
stopReconnect() {
if (this.reconnectTimer !== null) {
clearTimeout(this.reconnectTimer);
this.reconnectTimer = null;
}
}
/**
* 拒绝所有等待的消息
* @param reason 拒绝原因
*/
rejectAllPendingMessages(reason) {
this.log("debug", `拒绝 ${this.pendingMessages.size} 个等待响应的消息`);
this.pendingMessages.forEach((message) => {
if (message.timeout) {
clearTimeout(message.timeout);
}
message.reject(reason);
});
this.pendingMessages.clear();
}
/**
* 记录日志
* @param level 日志级别
* @param message 日志消息
* @param error 错误对象(可选)
*/
log(level, message, error) {
if (!this.options.debug && level === "debug") {
return;
}
const timestamp = (/* @__PURE__ */ new Date()).toISOString();
const logMessage = `[${timestamp}] [${level.toUpperCase()}] ${message}`;
switch (level) {
case "debug":
console.debug(logMessage);
break;
case "info":
console.info(logMessage);
break;
case "warn":
console.warn(logMessage);
break;
case "error":
console.error(logMessage, error || "");
break;
}
}
/**
* 获取连接统计信息
* @returns 统计信息对象
*/
getConnectionStats() {
const uptime = this.connectionStartTime > 0 ? Date.now() - this.connectionStartTime : 0;
return {
uptime,
messagesSent: this.metrics.messagesSent,
messagesReceived: this.metrics.messagesReceived,
bytesSent: this.metrics.bytesSent,
bytesReceived: this.metrics.bytesReceived,
errors: this.metrics.errors,
reconnects: this.metrics.reconnects,
latency: this.getLatency(),
queueSize: this.messageQueue.length,
pendingResponses: this.pendingMessages.size
};
}
/**
* 重置统计信息
*/
resetStats() {
this.metrics = {
messagesSent: 0,
messagesReceived: 0,
bytesReceived: 0,
bytesSent: 0,
errors: 0,
reconnects: 0
};
this.connectionStartTime = Date.now();
}
/**
* 设置心跳配置
* @param config 心跳配置
*/
setHeartbeatConfig(config) {
this.heartbeatConfig = { ...this.heartbeatConfig, ...config };
if (this.state === "open") {
this.stopHeartbeat();
this.startHeartbeat();
}
}
/**
* 设置重连配置
* @param config 重连配置
*/
setReconnectConfig(config) {
this.reconnectConfig = { ...this.reconnectConfig, ...config };
}
/**
* 设置消息配置
* @param config 消息配置
*/
setMessageConfig(config) {
this.messageConfig = { ...this.messageConfig, ...config };
if (this.batchTimer !== null) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
if (this.messageConfig.batchThreshold > 0 && this.batchedMessages.length > 0) {
this.flushBatchMessages();
}
}
/**
* 获取当前配置
* @returns 当前配置对象
*/
getConfig() {
return {
heartbeat: { ...this.heartbeatConfig },
reconnect: { ...this.reconnectConfig },
message: { ...this.messageConfig }
};
}
/**
* 销毁WebSocket客户端
*/
destroy() {
this.log("info", "销毁WebSocket客户端");
this.stopHeartbeat();
this.stopReconnect();
if (this.batchTimer !== null) {
clearTimeout(this.batchTimer);
this.batchTimer = null;
}
this.clearQueue();
this.rejectAllPendingMessages(new Error("客户端已销毁"));
this.close();
this.eventListeners.clear();
this.state = "closed";
this.socket = null;
this.batchedMessages = [];
this.reconnectAttempts = 0;
this.connectionStartTime = 0;
this.lastMessageTime = 0;
this.resetStats();
}
}
export {
MessagePriority,
WebSocketClient,
WebSocketState
};