@tunnelbox/core 0.1.9 → 0.1.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +28 -28
- package/README.zh-CN.md +28 -28
- package/dist/index.js +68 -3
- package/locales/de-DE.json +166 -165
- package/locales/en-US.json +166 -165
- package/locales/es-ES.json +166 -165
- package/locales/fr-FR.json +166 -165
- package/locales/ja-JP.json +166 -165
- package/locales/ko-KR.json +166 -165
- package/locales/zh-CN.json +166 -165
- package/locales/zh-TW.json +166 -165
- package/package.json +1 -1
- package/src/messages.ts +171 -171
- package/src/qr.ts +108 -108
- package/src/relay.ts +67 -2
package/src/relay.ts
CHANGED
|
@@ -13,6 +13,11 @@ export interface RelayClientHandlers {
|
|
|
13
13
|
|
|
14
14
|
const BASE_DELAY = 1000;
|
|
15
15
|
const MAX_DELAY = 15000;
|
|
16
|
+
/** 保活心跳周期(ms):连接存活期间定时发 ping,让 relay 回 pong 刷新收帧时间戳。 */
|
|
17
|
+
const KEEPALIVE_INTERVAL_MS = 15000;
|
|
18
|
+
/** 静默断流判定窗口(ms):超过该时长收不到任何帧(含 pong)→ 判定连接已死并主动重连,
|
|
19
|
+
* 避免半开连接(NAT/换网静默丢包)只能等系统 TCP 超时(可达数分钟)。 */
|
|
20
|
+
const KEEPALIVE_TIMEOUT_MS = 25000;
|
|
16
21
|
|
|
17
22
|
export class RelayClient {
|
|
18
23
|
private ws: WebSocket | null = null;
|
|
@@ -23,6 +28,11 @@ export class RelayClient {
|
|
|
23
28
|
private retry = 0;
|
|
24
29
|
private stopped = false;
|
|
25
30
|
private connected = false;
|
|
31
|
+
private reconnectScheduled = false;
|
|
32
|
+
/** 保活心跳定时器(onopen 启动 / onclose 停止) */
|
|
33
|
+
private keepaliveTimer: ReturnType<typeof setInterval> | null = null;
|
|
34
|
+
/** 最近一次收到服务端帧的时间(含心跳回执 pong),用于静默断流检测 */
|
|
35
|
+
private lastInbound = 0;
|
|
26
36
|
|
|
27
37
|
get isConnected(): boolean {
|
|
28
38
|
return this.connected;
|
|
@@ -34,6 +44,8 @@ export class RelayClient {
|
|
|
34
44
|
this.handlers = handlers;
|
|
35
45
|
this.stopped = false;
|
|
36
46
|
this.retry = 0;
|
|
47
|
+
this.reconnectScheduled = false;
|
|
48
|
+
this.stopKeepalive();
|
|
37
49
|
this.open();
|
|
38
50
|
}
|
|
39
51
|
|
|
@@ -51,6 +63,8 @@ export class RelayClient {
|
|
|
51
63
|
|
|
52
64
|
close(): void {
|
|
53
65
|
this.stopped = true;
|
|
66
|
+
this.reconnectScheduled = false;
|
|
67
|
+
this.stopKeepalive();
|
|
54
68
|
if (this.timer) clearTimeout(this.timer);
|
|
55
69
|
this.timer = null;
|
|
56
70
|
if (this.ws) {
|
|
@@ -74,11 +88,14 @@ export class RelayClient {
|
|
|
74
88
|
ws.onopen = () => {
|
|
75
89
|
this.connected = true;
|
|
76
90
|
this.retry = 0;
|
|
91
|
+
this.startKeepalive();
|
|
77
92
|
this.handlers?.onOpen();
|
|
78
93
|
};
|
|
79
94
|
ws.onmessage = (ev) => {
|
|
95
|
+
this.lastInbound = Date.now(); // 任何服务端帧都视为连接存活(含心跳回执 pong)
|
|
80
96
|
try {
|
|
81
97
|
const msg = JSON.parse(String(ev.data)) as Envelope;
|
|
98
|
+
if (msg.type === "pong") return; // 心跳回执:吞掉,不回传处理器
|
|
82
99
|
this.handlers?.onMessage(msg);
|
|
83
100
|
} catch {
|
|
84
101
|
/* 忽略非 JSON 帧 */
|
|
@@ -88,6 +105,7 @@ export class RelayClient {
|
|
|
88
105
|
ws.onclose = () => {
|
|
89
106
|
this.connected = false;
|
|
90
107
|
this.ws = null;
|
|
108
|
+
this.stopKeepalive();
|
|
91
109
|
this.handlers?.onClose();
|
|
92
110
|
this.scheduleReconnect();
|
|
93
111
|
};
|
|
@@ -98,9 +116,56 @@ export class RelayClient {
|
|
|
98
116
|
}
|
|
99
117
|
|
|
100
118
|
private scheduleReconnect(): void {
|
|
101
|
-
if (this.stopped) return;
|
|
119
|
+
if (this.stopped || this.reconnectScheduled) return;
|
|
120
|
+
this.reconnectScheduled = true;
|
|
102
121
|
const delay = Math.min(BASE_DELAY * 2 ** this.retry, MAX_DELAY);
|
|
103
122
|
this.retry++;
|
|
104
|
-
this.timer = setTimeout(() =>
|
|
123
|
+
this.timer = setTimeout(() => {
|
|
124
|
+
this.reconnectScheduled = false;
|
|
125
|
+
this.open();
|
|
126
|
+
}, delay);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** 启动保活心跳:每周期发 ping,长时间收不到任何帧则判定断流主动重连。 */
|
|
130
|
+
private startKeepalive() {
|
|
131
|
+
this.stopKeepalive();
|
|
132
|
+
this.lastInbound = Date.now();
|
|
133
|
+
this.keepaliveTimer = setInterval(() => {
|
|
134
|
+
if (!this.connected || !this.ws) return;
|
|
135
|
+
const now = Date.now();
|
|
136
|
+
if (now - this.lastInbound > KEEPALIVE_TIMEOUT_MS) {
|
|
137
|
+
this.forceReconnect();
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
try {
|
|
141
|
+
this.ws.send(JSON.stringify({ type: "ping", payload: {}, ts: now }));
|
|
142
|
+
} catch {
|
|
143
|
+
/* 发送失败:交由下一轮断流判定触发重连 */
|
|
144
|
+
}
|
|
145
|
+
}, KEEPALIVE_INTERVAL_MS);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private stopKeepalive() {
|
|
149
|
+
if (this.keepaliveTimer !== null) {
|
|
150
|
+
clearInterval(this.keepaliveTimer);
|
|
151
|
+
this.keepaliveTimer = null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/** 判定连接已死:关闭当前 socket 并调度重连(沿用指数退避,onclose 去重)。 */
|
|
156
|
+
private forceReconnect() {
|
|
157
|
+
if (this.stopped) return;
|
|
158
|
+
this.connected = false;
|
|
159
|
+
this.stopKeepalive();
|
|
160
|
+
const w = this.ws;
|
|
161
|
+
this.ws = null;
|
|
162
|
+
if (w) {
|
|
163
|
+
try {
|
|
164
|
+
w.close();
|
|
165
|
+
} catch {
|
|
166
|
+
/* ignore */
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
this.scheduleReconnect();
|
|
105
170
|
}
|
|
106
171
|
}
|