@myassis/gateway 1.0.85 → 1.0.87

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.
@@ -0,0 +1,541 @@
1
+ "use strict";
2
+ /**
3
+ * 中继客户端(Gateway 侧)
4
+ *
5
+ * 维持一条到 Server 的出站 WSS 长连接,把 Server 转发来的帧还原成对本机的
6
+ * 真实 HTTP / WebSocket 请求,再把结果按帧回传。
7
+ *
8
+ * 为什么是「出站」:网关跑在用户机器上,绝大多数处于 NAT 之后,没有公网入口,
9
+ * 只有由网关主动连出才能建立双向通道;这同时避免了要求用户做端口映射。
10
+ *
11
+ * 三条不可妥协的原则:
12
+ * 1. 不缓冲:响应头与每个数据块都立刻发帧,保证 SSE 首字延迟;
13
+ * 2. 有流控:发送窗口耗尽即暂停读取本机响应,避免大响应把网关内存打爆;
14
+ * 3. 默认关闭:未显式开启中继时完全不建连,不产生任何外联行为。
15
+ */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
19
+ Object.defineProperty(exports, "__esModule", { value: true });
20
+ exports.relayClient = void 0;
21
+ const crypto_1 = __importDefault(require("crypto"));
22
+ const shared_1 = require("@myassis/shared");
23
+ const protocol_js_1 = require("./protocol.js");
24
+ const LoopbackForwarder_js_1 = require("./LoopbackForwarder.js");
25
+ const relayConfig_js_1 = require("./relayConfig.js");
26
+ const logger = (0, shared_1.getLogger)('relay/Client');
27
+ /** 重连退避:1s 起,指数增长至 30s 上限 */
28
+ const RECONNECT_BASE_MS = 1000;
29
+ const RECONNECT_MAX_MS = 30000;
30
+ class RelayClient {
31
+ ws = null;
32
+ state = 'idle';
33
+ localPort = 0;
34
+ failureCount = 0;
35
+ lastError;
36
+ connectedAt;
37
+ httpStreams = new Map();
38
+ wsStreams = new Map();
39
+ pingTimer = null;
40
+ reconnectTimer = null;
41
+ lastPongAt = 0;
42
+ /** 主动停止后不再重连(SIGTERM / 用户关闭开关) */
43
+ stopped = false;
44
+ /** Server 在 HELLO_ACK 中下发的参数,未收到前用协议默认值 */
45
+ initialWindowSize = protocol_js_1.INITIAL_WINDOW_SIZE;
46
+ maxConcurrentStreams = protocol_js_1.MAX_CONCURRENT_STREAMS;
47
+ /**
48
+ * 启动中继。
49
+ * 必须在 HTTP Server 监听成功后调用——回环转发需要真实端口,
50
+ * 而端口存在自动顺延(3001 被占用则 3002),配置值不可信。
51
+ */
52
+ start(port) {
53
+ this.localPort = port;
54
+ this.stopped = false;
55
+ if (!(0, relayConfig_js_1.isRelayEnabled)()) {
56
+ logger.info('中继未开启,跳过隧道建连');
57
+ return;
58
+ }
59
+ const credential = (0, relayConfig_js_1.getRelayCredential)();
60
+ if (!credential) {
61
+ logger.info('中继已开启但尚未配对,等待用户在客户端完成配对');
62
+ return;
63
+ }
64
+ this.connect();
65
+ }
66
+ /** 用户开启中继:立即尝试建连(无需重启网关) */
67
+ enable() {
68
+ if (this.state === 'connecting' || this.state === 'online')
69
+ return;
70
+ if (!this.localPort) {
71
+ logger.warn('HTTP 服务尚未就绪,中继将在服务启动后自动建连');
72
+ return;
73
+ }
74
+ this.stopped = false;
75
+ this.failureCount = 0;
76
+ this.connect();
77
+ }
78
+ /** 用户关闭中继或进程退出:优雅关闭所有流与隧道 */
79
+ stop(reason = '中继已停止') {
80
+ this.stopped = true;
81
+ this.clearTimers();
82
+ // 先中止所有在途流,让远端客户端立刻收到明确错误而不是等超时
83
+ for (const [streamId, stream] of this.httpStreams) {
84
+ stream.request.abort(protocol_js_1.AbortReason.TUNNEL_CLOSING, reason);
85
+ this.sendAbort(streamId, protocol_js_1.AbortReason.TUNNEL_CLOSING, reason);
86
+ }
87
+ this.httpStreams.clear();
88
+ for (const stream of this.wsStreams.values()) {
89
+ stream.socket.close(1001, reason);
90
+ }
91
+ this.wsStreams.clear();
92
+ const ws = this.ws;
93
+ this.ws = null;
94
+ if (ws) {
95
+ try {
96
+ ws.close(1000, reason);
97
+ }
98
+ catch {
99
+ ws.terminate();
100
+ }
101
+ }
102
+ this.setState('idle');
103
+ }
104
+ /** 当前状态,供 /health 与 relay 路由暴露 */
105
+ getStatus() {
106
+ const credential = (0, relayConfig_js_1.getRelayCredential)();
107
+ return {
108
+ state: this.state,
109
+ connected: this.state === 'online',
110
+ gatewayId: credential?.gatewayId,
111
+ activeStreams: this.httpStreams.size + this.wsStreams.size,
112
+ failureCount: this.failureCount,
113
+ lastError: this.lastError,
114
+ connectedAt: this.connectedAt,
115
+ };
116
+ }
117
+ // ─── 连接管理 ────────────────────────────────────────
118
+ connect() {
119
+ if (this.stopped)
120
+ return;
121
+ const credential = (0, relayConfig_js_1.getRelayCredential)();
122
+ const tunnelUrl = (0, relayConfig_js_1.getTunnelUrl)();
123
+ if (!credential || !tunnelUrl) {
124
+ logger.warn('缺少配对凭据或中继入口地址,放弃建连');
125
+ return;
126
+ }
127
+ this.setState('connecting');
128
+ // 延迟 require:未启用中继的网关不加载 ws 客户端
129
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
130
+ const { WebSocket } = require('ws');
131
+ // 隧道鉴权用 HMAC 而非直接送 secret:即使日志或中间设备记录了请求头,
132
+ // 也拿不到长期凭据;timestamp + nonce 用于服务端防重放。
133
+ const timestamp = Date.now();
134
+ const nonce = crypto_1.default.randomBytes(16).toString('hex');
135
+ const signature = crypto_1.default
136
+ .createHmac('sha256', credential.secret)
137
+ .update(`${credential.gatewayId}.${timestamp}.${nonce}`)
138
+ .digest('hex');
139
+ logger.info(`正在连接中继隧道: ${tunnelUrl}`);
140
+ const ws = new WebSocket(tunnelUrl, {
141
+ headers: {
142
+ 'x-gateway-id': credential.gatewayId,
143
+ 'x-relay-timestamp': String(timestamp),
144
+ 'x-relay-nonce': nonce,
145
+ 'x-relay-signature': signature,
146
+ 'x-relay-protocol': String(protocol_js_1.RELAY_PROTOCOL_VERSION),
147
+ },
148
+ handshakeTimeout: 15000,
149
+ // 隧道上跑的是已压缩/流式内容,再开 permessage-deflate 只增加延迟与内存
150
+ perMessageDeflate: false,
151
+ });
152
+ this.ws = ws;
153
+ ws.on('open', () => this.handleOpen(credential.gatewayId));
154
+ ws.on('message', (data, isBinary) => {
155
+ if (!isBinary) {
156
+ // 帧协议是二进制的;收到文本帧说明对端实现有误,直接判协议错误
157
+ this.failTunnel('隧道收到非二进制消息');
158
+ return;
159
+ }
160
+ this.handleFrame(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
161
+ });
162
+ ws.on('close', (code, reason) => {
163
+ this.handleClose(`隧道关闭 code=${code} ${reason?.toString() || ''}`);
164
+ });
165
+ ws.on('error', (err) => {
166
+ this.lastError = err.message;
167
+ logger.warn(`隧道错误: ${err.message}`);
168
+ });
169
+ ws.on('pong', () => {
170
+ this.lastPongAt = Date.now();
171
+ });
172
+ }
173
+ handleOpen(gatewayId) {
174
+ logger.info('隧道已建立,发送 HELLO');
175
+ this.lastPongAt = Date.now();
176
+ const hello = {
177
+ protocolVersion: protocol_js_1.RELAY_PROTOCOL_VERSION,
178
+ gatewayId,
179
+ gatewayVersion: getGatewayVersion(),
180
+ platform: process.platform,
181
+ features: ['direct@1', `relay@${protocol_js_1.RELAY_PROTOCOL_VERSION}`],
182
+ timestamp: Date.now(),
183
+ nonce: crypto_1.default.randomBytes(8).toString('hex'),
184
+ };
185
+ this.sendFrame((0, protocol_js_1.encodeJsonFrame)(protocol_js_1.FrameType.HELLO, protocol_js_1.CONTROL_STREAM_ID, hello));
186
+ // 自检:确认回环端口上的进程就是自己。
187
+ // 同机跑着多个网关时(端口顺延 3001→3002),若这里取错端口,请求会被
188
+ // 转进另一个进程 —— 隧道在线、/health 正常,但业务接口全 404,极难定位。
189
+ // 只告警不阻断:自检本身可能因时序(HTTP 尚未完全就绪)误判,
190
+ // 阻断会把一个诊断辅助变成新的故障源。
191
+ void (0, LoopbackForwarder_js_1.verifyLoopbackIdentity)(this.localPort, gatewayId).then((problem) => {
192
+ if (problem) {
193
+ logger.error(`中继回环自检未通过:${problem}。中继请求可能被转发到错误的进程。`);
194
+ }
195
+ });
196
+ }
197
+ handleClose(message) {
198
+ if (this.ws === null && this.stopped)
199
+ return;
200
+ this.ws = null;
201
+ this.clearTimers();
202
+ this.connectedAt = undefined;
203
+ // 隧道断开时在途流全部无效:本机请求必须销毁,否则会继续跑完并浪费资源
204
+ for (const stream of this.httpStreams.values()) {
205
+ stream.request.abort(protocol_js_1.AbortReason.TUNNEL_CLOSING, message);
206
+ }
207
+ this.httpStreams.clear();
208
+ for (const stream of this.wsStreams.values()) {
209
+ stream.socket.close(1001, message);
210
+ }
211
+ this.wsStreams.clear();
212
+ if (this.stopped) {
213
+ this.setState('idle');
214
+ return;
215
+ }
216
+ this.failureCount += 1;
217
+ this.lastError = message;
218
+ logger.warn(`${message},准备重连(第 ${this.failureCount} 次)`);
219
+ this.scheduleReconnect();
220
+ }
221
+ failTunnel(message) {
222
+ this.lastError = message;
223
+ logger.error(`隧道协议错误: ${message}`);
224
+ const ws = this.ws;
225
+ this.ws = null;
226
+ try {
227
+ ws?.close(1002, 'protocol error');
228
+ }
229
+ catch {
230
+ ws?.terminate();
231
+ }
232
+ this.handleClose(message);
233
+ }
234
+ scheduleReconnect() {
235
+ this.setState('backoff');
236
+ // 指数退避 + 抖动:大量网关同时掉线时避免在同一刻齐涌回来把 Server 打满
237
+ const exponential = Math.min(RECONNECT_BASE_MS * 2 ** (this.failureCount - 1), RECONNECT_MAX_MS);
238
+ const delay = Math.round(exponential * (0.5 + Math.random() * 0.5));
239
+ this.reconnectTimer = setTimeout(() => {
240
+ this.reconnectTimer = null;
241
+ if (this.stopped)
242
+ return;
243
+ if (!(0, relayConfig_js_1.isRelayEnabled)() || !(0, relayConfig_js_1.getRelayCredential)()) {
244
+ logger.info('中继已被关闭或解绑,停止重连');
245
+ this.setState('idle');
246
+ return;
247
+ }
248
+ this.connect();
249
+ }, delay);
250
+ logger.info(`将在 ${delay}ms 后重连`);
251
+ }
252
+ startHeartbeat() {
253
+ this.pingTimer = setInterval(() => {
254
+ const ws = this.ws;
255
+ if (!ws)
256
+ return;
257
+ // 超时判定基于最后一次 PONG:只发不收说明链路已是「半开」状态,
258
+ // TCP 层面可能仍然「正常」,必须靠应用层心跳发现。
259
+ if (Date.now() - this.lastPongAt > protocol_js_1.PING_TIMEOUT_MS) {
260
+ this.failTunnel(`心跳超时(${protocol_js_1.PING_TIMEOUT_MS}ms 未收到 PONG)`);
261
+ return;
262
+ }
263
+ this.sendFrame((0, protocol_js_1.encodeFrame)(protocol_js_1.FrameType.PING, protocol_js_1.CONTROL_STREAM_ID));
264
+ }, protocol_js_1.PING_INTERVAL_MS);
265
+ }
266
+ clearTimers() {
267
+ if (this.pingTimer) {
268
+ clearInterval(this.pingTimer);
269
+ this.pingTimer = null;
270
+ }
271
+ if (this.reconnectTimer) {
272
+ clearTimeout(this.reconnectTimer);
273
+ this.reconnectTimer = null;
274
+ }
275
+ }
276
+ setState(state) {
277
+ this.state = state;
278
+ }
279
+ // ─── 帧分发 ──────────────────────────────────────────
280
+ handleFrame(data) {
281
+ let frame;
282
+ try {
283
+ frame = (0, protocol_js_1.decodeFrame)(data);
284
+ }
285
+ catch (err) {
286
+ // 解析失败无法定位 streamId,只能按连接级错误处理
287
+ this.failTunnel(err instanceof Error ? err.message : '帧解析失败');
288
+ return;
289
+ }
290
+ try {
291
+ switch (frame.type) {
292
+ case protocol_js_1.FrameType.HELLO_ACK:
293
+ this.onHelloAck(frame.payload);
294
+ break;
295
+ case protocol_js_1.FrameType.PING:
296
+ this.sendFrame((0, protocol_js_1.encodeFrame)(protocol_js_1.FrameType.PONG, protocol_js_1.CONTROL_STREAM_ID));
297
+ break;
298
+ case protocol_js_1.FrameType.PONG:
299
+ this.lastPongAt = Date.now();
300
+ break;
301
+ case protocol_js_1.FrameType.REQ_HEAD:
302
+ this.onReqHead(frame.streamId, frame.payload);
303
+ break;
304
+ case protocol_js_1.FrameType.REQ_DATA:
305
+ this.onReqData(frame.streamId, frame.payload);
306
+ break;
307
+ case protocol_js_1.FrameType.REQ_END:
308
+ this.onReqEnd(frame.streamId);
309
+ break;
310
+ case protocol_js_1.FrameType.STREAM_ABORT:
311
+ this.onStreamAbort(frame.streamId, frame.payload);
312
+ break;
313
+ case protocol_js_1.FrameType.WINDOW_UPDATE:
314
+ this.onWindowUpdate(frame.streamId, frame.payload);
315
+ break;
316
+ case protocol_js_1.FrameType.WS_OPEN:
317
+ this.onWsOpen(frame.streamId, frame.payload);
318
+ break;
319
+ case protocol_js_1.FrameType.WS_DATA:
320
+ this.onWsData(frame.streamId, frame.payload);
321
+ break;
322
+ case protocol_js_1.FrameType.WS_CLOSE:
323
+ this.onWsClose(frame.streamId, frame.payload);
324
+ break;
325
+ default:
326
+ // RESP_* 是网关自己发出的方向,收到说明对端实现有误
327
+ logger.warn(`忽略非预期帧: ${(0, protocol_js_1.frameTypeName)(frame.type)}`);
328
+ }
329
+ }
330
+ catch (err) {
331
+ const message = err instanceof Error ? err.message : String(err);
332
+ if (err instanceof protocol_js_1.RelayProtocolError && frame.streamId !== protocol_js_1.CONTROL_STREAM_ID) {
333
+ // 单流协议错误只中止该流,不牵连整条隧道上的其他会话
334
+ this.abortStream(frame.streamId, err.reason, message);
335
+ return;
336
+ }
337
+ this.failTunnel(message);
338
+ }
339
+ }
340
+ onHelloAck(payload) {
341
+ const ack = (0, protocol_js_1.decodeJsonPayload)(payload);
342
+ const check = (0, protocol_js_1.checkProtocolVersion)(ack.protocolVersion);
343
+ if (!check.compatible) {
344
+ // 版本不兼容属于确定性失败,重连也不会好转,直接停止并记录原因供用户查看
345
+ this.stopped = true;
346
+ this.failTunnel(check.reason || '协议版本不兼容');
347
+ return;
348
+ }
349
+ if (ack.initialWindowSize > 0)
350
+ this.initialWindowSize = ack.initialWindowSize;
351
+ if (ack.maxConcurrentStreams > 0)
352
+ this.maxConcurrentStreams = ack.maxConcurrentStreams;
353
+ this.failureCount = 0;
354
+ this.lastError = undefined;
355
+ this.connectedAt = Date.now();
356
+ this.setState('online');
357
+ this.startHeartbeat();
358
+ logger.info(`中继隧道已就绪 tunnelId=${ack.tunnelId} window=${this.initialWindowSize}`);
359
+ }
360
+ // ─── HTTP 流 ─────────────────────────────────────────
361
+ onReqHead(streamId, payload) {
362
+ if (this.httpStreams.has(streamId) || this.wsStreams.has(streamId)) {
363
+ throw new protocol_js_1.RelayProtocolError(`streamId ${streamId} 重复使用`);
364
+ }
365
+ if (this.httpStreams.size + this.wsStreams.size >= this.maxConcurrentStreams) {
366
+ this.abortStream(streamId, protocol_js_1.AbortReason.QUOTA_EXCEEDED, '超过最大并发流数');
367
+ return;
368
+ }
369
+ const head = (0, protocol_js_1.decodeJsonPayload)(payload);
370
+ const sendWindow = new protocol_js_1.SendWindow(this.initialWindowSize);
371
+ const recvWindow = new protocol_js_1.ReceiveWindow(this.initialWindowSize);
372
+ const request = new LoopbackForwarder_js_1.LoopbackRequest(this.localPort, head, {
373
+ onHead: (status, headers) => {
374
+ this.sendFrame((0, protocol_js_1.encodeJsonFrame)(protocol_js_1.FrameType.RESP_HEAD, streamId, { status, headers }));
375
+ },
376
+ onData: (chunk) => {
377
+ const stream = this.httpStreams.get(streamId);
378
+ if (!stream)
379
+ return;
380
+ // 信用不足时先暂停本机响应,等 WINDOW_UPDATE 再继续。
381
+ // 这一步是防止「本机产出快于隧道上传」导致内存无界增长的关键。
382
+ if (!stream.sendWindow.canSend(chunk.byteLength)) {
383
+ stream.request.pauseResponse();
384
+ }
385
+ stream.sendWindow.consume(chunk.byteLength);
386
+ this.sendFrame((0, protocol_js_1.encodeFrame)(protocol_js_1.FrameType.RESP_DATA, streamId, chunk));
387
+ },
388
+ onEnd: () => {
389
+ this.sendFrame((0, protocol_js_1.encodeFrame)(protocol_js_1.FrameType.RESP_END, streamId));
390
+ this.httpStreams.delete(streamId);
391
+ },
392
+ onError: (reason, message) => {
393
+ this.sendAbort(streamId, reason, message);
394
+ this.httpStreams.delete(streamId);
395
+ },
396
+ });
397
+ this.httpStreams.set(streamId, { request, sendWindow, recvWindow });
398
+ }
399
+ onReqData(streamId, payload) {
400
+ const stream = this.httpStreams.get(streamId);
401
+ // 流已结束仍收到数据是正常竞态(对端尚未收到 RESP_END),静默丢弃
402
+ if (!stream)
403
+ return;
404
+ stream.recvWindow.receive(payload.byteLength);
405
+ stream.request.writeBody(payload);
406
+ // 数据已交给本机,立刻归还信用;达到半窗阈值才实际发帧
407
+ const delta = stream.recvWindow.consume(payload.byteLength);
408
+ if (delta > 0) {
409
+ this.sendFrame((0, protocol_js_1.encodeJsonFrame)(protocol_js_1.FrameType.WINDOW_UPDATE, streamId, { delta }));
410
+ }
411
+ }
412
+ onReqEnd(streamId) {
413
+ this.httpStreams.get(streamId)?.request.endBody();
414
+ }
415
+ onWindowUpdate(streamId, payload) {
416
+ const { delta } = (0, protocol_js_1.decodeJsonPayload)(payload);
417
+ const http = this.httpStreams.get(streamId);
418
+ if (http) {
419
+ http.sendWindow.increase(delta);
420
+ http.request.resumeResponse();
421
+ return;
422
+ }
423
+ this.wsStreams.get(streamId)?.sendWindow.increase(delta);
424
+ }
425
+ onStreamAbort(streamId, payload) {
426
+ const { reason, message } = (0, protocol_js_1.decodeJsonPayload)(payload);
427
+ const text = message || `对端中止 reason=${reason}`;
428
+ const http = this.httpStreams.get(streamId);
429
+ if (http) {
430
+ http.request.abort(reason, text);
431
+ this.httpStreams.delete(streamId);
432
+ return;
433
+ }
434
+ const ws = this.wsStreams.get(streamId);
435
+ if (ws) {
436
+ ws.socket.close(1001, text);
437
+ this.wsStreams.delete(streamId);
438
+ }
439
+ }
440
+ /** 中止某条流:同时清理本地资源并通知对端 */
441
+ abortStream(streamId, reason, message) {
442
+ const http = this.httpStreams.get(streamId);
443
+ if (http) {
444
+ http.request.abort(reason, message);
445
+ this.httpStreams.delete(streamId);
446
+ }
447
+ const ws = this.wsStreams.get(streamId);
448
+ if (ws) {
449
+ ws.socket.close(1011, message);
450
+ this.wsStreams.delete(streamId);
451
+ }
452
+ this.sendAbort(streamId, reason, message);
453
+ }
454
+ // ─── WebSocket 流 ────────────────────────────────────
455
+ onWsOpen(streamId, payload) {
456
+ if (this.wsStreams.has(streamId) || this.httpStreams.has(streamId)) {
457
+ throw new protocol_js_1.RelayProtocolError(`streamId ${streamId} 重复使用`);
458
+ }
459
+ if (this.httpStreams.size + this.wsStreams.size >= this.maxConcurrentStreams) {
460
+ this.abortStream(streamId, protocol_js_1.AbortReason.QUOTA_EXCEEDED, '超过最大并发流数');
461
+ return;
462
+ }
463
+ const open = (0, protocol_js_1.decodeJsonPayload)(payload);
464
+ const sendWindow = new protocol_js_1.SendWindow(this.initialWindowSize);
465
+ const recvWindow = new protocol_js_1.ReceiveWindow(this.initialWindowSize);
466
+ const socket = new LoopbackForwarder_js_1.LoopbackWebSocket(this.localPort, open.path, open.headers, {
467
+ onOpen: () => {
468
+ // 回环连上后无需额外通知:远端 WS 在 Server 侧已经 accept,
469
+ // 这里若再发帧反而要引入新的帧类型,收益不大。
470
+ logger.debug(`中继 WS 已连通 stream=${streamId}`);
471
+ },
472
+ onMessage: (data, binary) => {
473
+ const stream = this.wsStreams.get(streamId);
474
+ if (!stream)
475
+ return;
476
+ if (!stream.sendWindow.canSend(data.byteLength)) {
477
+ // WS 消息无法暂停(ws 库无逐条背压),超窗时中止该流比静默丢消息安全:
478
+ // 丢消息会让前端状态与网关不一致,而中止会触发前端重连并补拉。
479
+ this.abortStream(streamId, protocol_js_1.AbortReason.QUOTA_EXCEEDED, 'WS 发送窗口耗尽');
480
+ return;
481
+ }
482
+ stream.sendWindow.consume(data.byteLength);
483
+ for (const frame of (0, protocol_js_1.encodeWsDataFrames)(streamId, data, binary)) {
484
+ this.sendFrame(frame);
485
+ }
486
+ },
487
+ onClose: (code, reason) => {
488
+ this.sendFrame((0, protocol_js_1.encodeJsonFrame)(protocol_js_1.FrameType.WS_CLOSE, streamId, { code, reason }));
489
+ this.wsStreams.delete(streamId);
490
+ },
491
+ });
492
+ this.wsStreams.set(streamId, { socket, sendWindow, recvWindow });
493
+ }
494
+ onWsData(streamId, payload) {
495
+ const stream = this.wsStreams.get(streamId);
496
+ if (!stream)
497
+ return;
498
+ const chunk = (0, protocol_js_1.decodeWsData)(payload);
499
+ stream.recvWindow.receive(chunk.data.byteLength);
500
+ stream.socket.send(chunk.data, chunk.binary);
501
+ const delta = stream.recvWindow.consume(chunk.data.byteLength);
502
+ if (delta > 0) {
503
+ this.sendFrame((0, protocol_js_1.encodeJsonFrame)(protocol_js_1.FrameType.WINDOW_UPDATE, streamId, { delta }));
504
+ }
505
+ }
506
+ onWsClose(streamId, payload) {
507
+ const stream = this.wsStreams.get(streamId);
508
+ if (!stream)
509
+ return;
510
+ const { code, reason } = (0, protocol_js_1.decodeJsonPayload)(payload);
511
+ stream.socket.close(code, reason || '');
512
+ this.wsStreams.delete(streamId);
513
+ }
514
+ // ─── 发送 ────────────────────────────────────────────
515
+ sendFrame(frame) {
516
+ const ws = this.ws;
517
+ if (!ws || ws.readyState !== 1 /* OPEN */)
518
+ return;
519
+ ws.send(frame, { binary: true });
520
+ }
521
+ sendAbort(streamId, reason, message) {
522
+ this.sendFrame((0, protocol_js_1.encodeJsonFrame)(protocol_js_1.FrameType.STREAM_ABORT, streamId, { reason, message }));
523
+ }
524
+ }
525
+ /** 读取网关自身版本,失败时不影响建连 */
526
+ function getGatewayVersion() {
527
+ try {
528
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
529
+ const { readFileSync } = require('fs');
530
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
531
+ const { resolve } = require('path');
532
+ const pkg = JSON.parse(readFileSync(resolve(__dirname, '../../../package.json'), 'utf-8'));
533
+ return pkg.version || 'unknown';
534
+ }
535
+ catch {
536
+ return 'unknown';
537
+ }
538
+ }
539
+ /** 单例:整个进程只维持一条隧道 */
540
+ exports.relayClient = new RelayClient();
541
+ exports.default = exports.relayClient;