@dainprotocol/tunnel 1.1.25 → 1.1.29

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.
@@ -5,22 +5,21 @@ declare class DainTunnel extends EventEmitter {
5
5
  private tunnelUrl;
6
6
  private port;
7
7
  private reconnectAttempts;
8
- private maxReconnectAttempts;
9
- private reconnectDelay;
10
8
  private apiKey;
11
9
  private tunnelId;
12
10
  private secret;
13
11
  private webSocketClients;
14
12
  private sseClients;
15
13
  private httpAgent;
14
+ private heartbeatInterval;
16
15
  constructor(serverUrl: string, apiKey: string);
17
- /**
18
- * Sign a challenge using HMAC-SHA256
19
- * @private
20
- */
21
16
  private signChallenge;
17
+ private safeSend;
18
+ private startHeartbeat;
19
+ private stopHeartbeat;
22
20
  start(port: number): Promise<string>;
23
21
  private connect;
22
+ private cleanupAllClients;
24
23
  private requestChallenge;
25
24
  private handleMessage;
26
25
  private handleRequest;
@@ -29,7 +28,6 @@ declare class DainTunnel extends EventEmitter {
29
28
  private handleSSEConnection;
30
29
  private handleSSEClose;
31
30
  private forwardRequest;
32
- private sendMessage;
33
31
  private attemptReconnect;
34
32
  stop(): Promise<void>;
35
33
  }
@@ -9,6 +9,17 @@ const http_1 = __importDefault(require("http"));
9
9
  const events_1 = require("events");
10
10
  const crypto_1 = require("crypto");
11
11
  const auth_1 = require("@dainprotocol/service-sdk/service/auth");
12
+ const TIMEOUTS = {
13
+ HEARTBEAT: 25000,
14
+ REQUEST: 25000,
15
+ CONNECTION: 10000,
16
+ CHALLENGE: 5000,
17
+ RECONNECT_DELAY: 5000,
18
+ SHUTDOWN_GRACE: 500,
19
+ };
20
+ const RECONNECT = {
21
+ MAX_ATTEMPTS: 5,
22
+ };
12
23
  class DainTunnel extends events_1.EventEmitter {
13
24
  constructor(serverUrl, apiKey) {
14
25
  super();
@@ -17,34 +28,58 @@ class DainTunnel extends events_1.EventEmitter {
17
28
  this.tunnelUrl = null;
18
29
  this.port = null;
19
30
  this.reconnectAttempts = 0;
20
- this.maxReconnectAttempts = 5;
21
- this.reconnectDelay = 5000;
22
31
  this.webSocketClients = new Map();
23
32
  this.sseClients = new Map();
24
- // Parse API key to extract agentId and secret
33
+ this.heartbeatInterval = null;
25
34
  const parsed = (0, auth_1.parseAPIKey)(apiKey);
26
35
  if (!parsed) {
27
36
  throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
28
37
  }
29
38
  this.apiKey = apiKey;
30
- this.tunnelId = `${parsed.orgId}_${parsed.agentId}`; // orgId_agentId to prevent collisions
31
- this.secret = parsed.secret; // secret for HMAC signatures
32
- // High-frequency optimization: Create reusable HTTP agent with connection pooling
39
+ this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
40
+ this.secret = parsed.secret;
33
41
  this.httpAgent = new http_1.default.Agent({
34
42
  keepAlive: true,
35
- keepAliveMsecs: 30000, // Keep connections alive for 30s
36
- maxSockets: 50, // Allow up to 50 concurrent connections to local service
37
- maxFreeSockets: 10, // Keep up to 10 idle connections
43
+ keepAliveMsecs: 30000,
44
+ maxSockets: 100,
45
+ maxFreeSockets: 20,
38
46
  });
39
47
  }
40
- /**
41
- * Sign a challenge using HMAC-SHA256
42
- * @private
43
- */
44
48
  signChallenge(challenge) {
45
- return (0, crypto_1.createHmac)('sha256', this.secret)
46
- .update(challenge)
47
- .digest('hex');
49
+ return (0, crypto_1.createHmac)('sha256', this.secret).update(challenge).digest('hex');
50
+ }
51
+ safeSend(data) {
52
+ var _a;
53
+ try {
54
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
55
+ this.ws.send(JSON.stringify(data));
56
+ return true;
57
+ }
58
+ }
59
+ catch (_b) {
60
+ // Connection lost during send
61
+ }
62
+ return false;
63
+ }
64
+ startHeartbeat() {
65
+ this.stopHeartbeat();
66
+ this.heartbeatInterval = setInterval(() => {
67
+ var _a;
68
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
69
+ try {
70
+ this.ws.ping();
71
+ }
72
+ catch (_b) {
73
+ this.emit("error", new Error("Heartbeat ping failed"));
74
+ }
75
+ }
76
+ }, TIMEOUTS.HEARTBEAT);
77
+ }
78
+ stopHeartbeat() {
79
+ if (this.heartbeatInterval) {
80
+ clearInterval(this.heartbeatInterval);
81
+ this.heartbeatInterval = null;
82
+ }
48
83
  }
49
84
  async start(port) {
50
85
  this.port = port;
@@ -52,6 +87,23 @@ class DainTunnel extends events_1.EventEmitter {
52
87
  }
53
88
  async connect() {
54
89
  return new Promise((resolve, reject) => {
90
+ let resolved = false;
91
+ let connectionTimeoutId = null;
92
+ const finish = (value, error) => {
93
+ if (resolved)
94
+ return;
95
+ resolved = true;
96
+ if (connectionTimeoutId) {
97
+ clearTimeout(connectionTimeoutId);
98
+ connectionTimeoutId = null;
99
+ }
100
+ if (error) {
101
+ reject(error);
102
+ }
103
+ else {
104
+ resolve(value);
105
+ }
106
+ };
55
107
  try {
56
108
  this.ws = new ws_1.default(this.serverUrl);
57
109
  this.ws.on("open", async () => {
@@ -59,90 +111,113 @@ class DainTunnel extends events_1.EventEmitter {
59
111
  try {
60
112
  const challenge = await this.requestChallenge();
61
113
  const signature = this.signChallenge(challenge);
62
- this.sendMessage({
114
+ this.safeSend({
63
115
  type: "start",
64
116
  port: this.port,
65
117
  challenge,
66
118
  signature,
67
119
  tunnelId: this.tunnelId,
68
- apiKey: this.apiKey // Send API key for server validation
120
+ apiKey: this.apiKey
69
121
  });
122
+ this.startHeartbeat();
70
123
  this.emit("connected");
71
124
  }
72
125
  catch (err) {
73
- reject(err);
126
+ finish(undefined, err);
74
127
  }
75
128
  });
76
129
  this.ws.on("message", (data) => {
77
130
  try {
78
- this.handleMessage(JSON.parse(data), resolve);
131
+ this.handleMessage(JSON.parse(data), (url) => finish(url));
79
132
  }
80
133
  catch (err) {
81
- reject(err);
134
+ finish(undefined, err);
82
135
  }
83
136
  });
84
137
  this.ws.on("close", () => {
85
- // Clean up all active SSE connections
86
- for (const [, client] of this.sseClients) {
87
- try {
88
- client.destroy();
89
- }
90
- catch (e) { /* ignore */ }
91
- }
92
- this.sseClients.clear();
93
- // Clean up all WebSocket clients
94
- for (const [, client] of this.webSocketClients) {
95
- try {
96
- if (client.readyState === ws_1.default.OPEN)
97
- client.close(1001);
98
- }
99
- catch (e) { /* ignore */ }
100
- }
101
- this.webSocketClients.clear();
138
+ this.stopHeartbeat();
139
+ this.cleanupAllClients();
102
140
  if (this.tunnelUrl) {
103
141
  this.emit("disconnected");
104
142
  this.attemptReconnect();
105
143
  }
106
144
  else {
107
- reject(new Error("Connection closed before tunnel established"));
145
+ finish(undefined, new Error("Connection closed before tunnel established"));
108
146
  }
109
147
  });
110
148
  this.ws.on("error", (error) => this.emit("error", error));
111
- setTimeout(() => {
112
- if (!this.ws || this.ws.readyState !== ws_1.default.OPEN) {
113
- reject(new Error("Connection timeout"));
149
+ connectionTimeoutId = setTimeout(() => {
150
+ var _a;
151
+ if (!resolved && (!this.ws || this.ws.readyState !== ws_1.default.OPEN)) {
152
+ finish(undefined, new Error("Connection timeout"));
153
+ try {
154
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.terminate();
155
+ }
156
+ catch (_b) { }
114
157
  }
115
- }, 10000);
158
+ }, TIMEOUTS.CONNECTION);
116
159
  }
117
160
  catch (err) {
118
- reject(err);
161
+ finish(undefined, err);
119
162
  }
120
163
  });
121
164
  }
165
+ cleanupAllClients() {
166
+ for (const client of this.sseClients.values()) {
167
+ try {
168
+ client.destroy();
169
+ }
170
+ catch (_a) { }
171
+ }
172
+ this.sseClients.clear();
173
+ for (const client of this.webSocketClients.values()) {
174
+ try {
175
+ if (client.readyState === ws_1.default.OPEN)
176
+ client.close(1001);
177
+ }
178
+ catch (_b) { }
179
+ }
180
+ this.webSocketClients.clear();
181
+ }
122
182
  async requestChallenge() {
123
183
  return new Promise((resolve, reject) => {
124
184
  if (!this.ws) {
125
185
  reject(new Error("WebSocket is not connected"));
126
186
  return;
127
187
  }
128
- this.ws.send(JSON.stringify({ type: "challenge_request" }));
188
+ let resolved = false;
189
+ let timeoutId = null;
190
+ const finish = (challenge, error) => {
191
+ var _a;
192
+ if (resolved)
193
+ return;
194
+ resolved = true;
195
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.removeListener("message", challengeHandler);
196
+ if (timeoutId) {
197
+ clearTimeout(timeoutId);
198
+ timeoutId = null;
199
+ }
200
+ if (error) {
201
+ reject(error);
202
+ }
203
+ else {
204
+ resolve(challenge);
205
+ }
206
+ };
129
207
  const challengeHandler = (message) => {
130
- const data = JSON.parse(message);
131
- if (data.type === "challenge") {
132
- if (this.ws) {
133
- this.ws.removeListener("message", challengeHandler);
208
+ try {
209
+ const data = JSON.parse(message);
210
+ if (data.type === "challenge") {
211
+ finish(data.challenge);
134
212
  }
135
- resolve(data.challenge);
136
213
  }
214
+ catch (_a) { }
137
215
  };
138
216
  this.ws.on("message", challengeHandler);
139
- // Add a timeout for the challenge request
140
- setTimeout(() => {
141
- if (this.ws) {
142
- this.ws.removeListener("message", challengeHandler);
143
- }
144
- reject(new Error("Challenge request timeout"));
145
- }, 5000);
217
+ this.ws.send(JSON.stringify({ type: "challenge_request" }));
218
+ timeoutId = setTimeout(() => {
219
+ finish(undefined, new Error("Challenge request timeout"));
220
+ }, TIMEOUTS.CHALLENGE);
146
221
  });
147
222
  }
148
223
  handleMessage(message, resolve) {
@@ -172,120 +247,119 @@ class DainTunnel extends events_1.EventEmitter {
172
247
  async handleRequest(request) {
173
248
  try {
174
249
  const response = await this.forwardRequest(request);
175
- this.sendMessage(response);
250
+ this.safeSend(response);
176
251
  this.emit("request_handled", { request, response });
177
252
  }
178
253
  catch (error) {
254
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
255
+ this.safeSend({
256
+ type: "response",
257
+ requestId: request.id,
258
+ status: 502,
259
+ headers: { "content-type": "application/json" },
260
+ body: Buffer.from(JSON.stringify({
261
+ error: "Bad Gateway",
262
+ message: `Failed to forward request to local service: ${errorMessage}`
263
+ })).toString("base64")
264
+ });
179
265
  this.emit("request_error", { request, error });
180
266
  }
181
267
  }
182
268
  handleWebSocketConnection(message) {
183
- var _a;
269
+ const sendWsEvent = (event, data) => {
270
+ this.safeSend({ type: 'websocket', id: message.id, event, data });
271
+ };
184
272
  try {
185
273
  const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
186
274
  headers: message.headers
187
275
  });
188
276
  this.webSocketClients.set(message.id, client);
189
277
  client.on('message', (data) => {
190
- var _a;
191
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
192
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'message', data: data.toString('base64') }));
193
- }
278
+ sendWsEvent('message', data.toString('base64'));
194
279
  });
195
280
  client.on('close', () => {
196
- var _a;
197
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
198
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'close' }));
199
- }
281
+ sendWsEvent('close');
200
282
  this.webSocketClients.delete(message.id);
201
283
  });
202
284
  client.on('error', (error) => {
203
- var _a;
204
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
205
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
206
- }
285
+ sendWsEvent('error', error.message);
207
286
  this.webSocketClients.delete(message.id);
208
287
  });
209
288
  this.emit("websocket_connection", { id: message.id, path: message.path });
210
289
  }
211
290
  catch (error) {
212
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
213
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
214
- }
291
+ sendWsEvent('error', error.message);
215
292
  }
216
293
  }
217
294
  handleWebSocketMessage(message) {
218
295
  const client = this.webSocketClients.get(message.id);
219
296
  if (!client)
220
297
  return;
221
- if (message.event === 'message' && message.data) {
222
- const data = Buffer.from(message.data, 'base64');
223
- if (client.readyState === ws_1.default.OPEN) {
224
- client.send(data);
225
- }
226
- }
227
- else if (message.event === 'close') {
228
- if (client.readyState === ws_1.default.OPEN) {
229
- client.close();
230
- }
231
- this.webSocketClients.delete(message.id);
298
+ switch (message.event) {
299
+ case 'message':
300
+ if (message.data && client.readyState === ws_1.default.OPEN) {
301
+ client.send(Buffer.from(message.data, 'base64'));
302
+ }
303
+ break;
304
+ case 'close':
305
+ if (client.readyState === ws_1.default.OPEN) {
306
+ client.close();
307
+ }
308
+ this.webSocketClients.delete(message.id);
309
+ break;
232
310
  }
233
311
  }
234
312
  handleSSEConnection(message) {
235
- var _a;
313
+ const sendSseEvent = (event, data = '') => {
314
+ this.safeSend({ type: 'sse', id: message.id, event, data });
315
+ };
236
316
  try {
317
+ const headers = { ...message.headers };
318
+ delete headers.host;
319
+ headers['accept-encoding'] = 'identity';
237
320
  const req = http_1.default.request({
238
321
  hostname: 'localhost',
239
322
  port: this.port,
240
323
  path: message.path,
241
324
  method: message.method || 'GET',
242
- headers: message.headers,
325
+ headers,
243
326
  agent: this.httpAgent,
244
327
  }, (res) => {
245
- var _a;
246
- // Non-200 response - forward error to tunnel server
247
328
  if (res.statusCode !== 200) {
248
329
  let errorBody = '';
249
330
  res.on('data', (chunk) => { errorBody += chunk.toString(); });
250
331
  res.on('end', () => {
251
- var _a;
252
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
253
- this.ws.send(JSON.stringify({
254
- type: 'sse', id: message.id, event: 'error',
255
- data: `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`
256
- }));
257
- }
332
+ sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
258
333
  });
259
334
  return;
260
335
  }
261
- // Optimize TCP for streaming
262
336
  const socket = res.socket || res.connection;
263
337
  if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
264
338
  socket.setNoDelay(true);
265
- // Notify tunnel server that local service accepted
266
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
267
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'connected', data: '' }));
268
- }
269
- // Stream SSE events to tunnel server
339
+ sendSseEvent('connected');
270
340
  let buffer = '';
271
- // Helper to process SSE events from buffer
272
341
  const processBuffer = () => {
273
- var _a;
342
+ buffer = buffer.replace(/\r\n/g, '\n');
274
343
  while (buffer.includes('\n\n')) {
275
344
  const idx = buffer.indexOf('\n\n');
276
345
  const msgData = buffer.substring(0, idx);
277
346
  buffer = buffer.substring(idx + 2);
278
- let event = 'message', data = '';
347
+ if (!msgData.trim() || msgData.startsWith(':'))
348
+ continue;
349
+ let event = 'message';
350
+ let data = '';
279
351
  for (const line of msgData.split('\n')) {
280
- if (line.startsWith('event:'))
352
+ if (line.startsWith('event:')) {
281
353
  event = line.substring(6).trim();
282
- else if (line.startsWith('data:'))
354
+ }
355
+ else if (line.startsWith('data:')) {
283
356
  data += line.substring(5).trim() + '\n';
357
+ }
284
358
  }
285
359
  if (data.endsWith('\n'))
286
360
  data = data.slice(0, -1);
287
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
288
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event, data }));
361
+ if (data) {
362
+ sendSseEvent(event, data);
289
363
  }
290
364
  }
291
365
  };
@@ -294,34 +368,29 @@ class DainTunnel extends events_1.EventEmitter {
294
368
  processBuffer();
295
369
  });
296
370
  res.on('end', () => {
297
- var _a;
298
- // Process any remaining data in buffer (might be missing final \n\n)
299
371
  if (buffer.trim()) {
300
- // Add missing newlines to ensure processing
301
372
  buffer += '\n\n';
302
373
  processBuffer();
303
374
  }
304
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
305
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: '' }));
306
- }
375
+ sendSseEvent('close');
307
376
  });
308
377
  });
378
+ req.on('socket', (socket) => {
379
+ socket.setNoDelay(true);
380
+ socket.setTimeout(0);
381
+ });
309
382
  req.on('error', (error) => {
310
- var _a;
311
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
312
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
313
- }
383
+ sendSseEvent('error', error.message);
314
384
  });
315
- if (message.body && message.method !== 'GET')
385
+ if (message.body && message.method !== 'GET') {
316
386
  req.write(Buffer.from(message.body, 'base64'));
387
+ }
317
388
  req.end();
318
389
  this.sseClients.set(message.id, req);
319
390
  this.emit("sse_connection", { id: message.id, path: message.path });
320
391
  }
321
392
  catch (error) {
322
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
323
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
324
- }
393
+ sendSseEvent('error', error.message);
325
394
  }
326
395
  }
327
396
  handleSSEClose(message) {
@@ -333,108 +402,92 @@ class DainTunnel extends events_1.EventEmitter {
333
402
  }
334
403
  forwardRequest(request) {
335
404
  return new Promise((resolve, reject) => {
405
+ let resolved = false;
406
+ let timeoutId = null;
407
+ const finish = (response, error) => {
408
+ if (resolved)
409
+ return;
410
+ resolved = true;
411
+ if (timeoutId) {
412
+ clearTimeout(timeoutId);
413
+ timeoutId = null;
414
+ }
415
+ if (error) {
416
+ reject(error);
417
+ }
418
+ else {
419
+ resolve(response);
420
+ }
421
+ };
336
422
  const options = {
337
423
  hostname: 'localhost',
338
424
  port: this.port,
339
425
  path: request.path,
340
426
  method: request.method,
341
427
  headers: request.headers,
342
- agent: this.httpAgent, // Use connection pooling
428
+ agent: this.httpAgent,
429
+ timeout: TIMEOUTS.REQUEST,
343
430
  };
344
431
  const req = http_1.default.request(options, (res) => {
345
- let body = Buffer.from([]);
346
- res.on('data', (chunk) => {
347
- body = Buffer.concat([body, chunk]);
348
- });
432
+ const chunks = [];
433
+ res.on('data', (chunk) => chunks.push(chunk));
349
434
  res.on('end', () => {
350
435
  const headers = { ...res.headers };
351
436
  delete headers['transfer-encoding'];
352
437
  delete headers['content-length'];
353
- resolve({
438
+ finish({
354
439
  type: 'response',
355
440
  requestId: request.id,
356
441
  status: res.statusCode,
357
442
  headers,
358
- body: body.toString('base64'),
443
+ body: Buffer.concat(chunks).toString('base64'),
359
444
  });
360
445
  });
446
+ res.on('error', (err) => finish(undefined, err));
447
+ });
448
+ req.on('timeout', () => {
449
+ req.destroy();
450
+ finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
361
451
  });
362
- req.on('error', reject);
452
+ req.on('error', (err) => finish(undefined, err));
453
+ timeoutId = setTimeout(() => {
454
+ req.destroy();
455
+ finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
456
+ }, TIMEOUTS.REQUEST);
363
457
  if (request.body && request.method !== 'GET') {
364
458
  req.write(Buffer.from(request.body, 'base64'));
365
459
  }
366
460
  req.end();
367
461
  });
368
462
  }
369
- sendMessage(message) {
370
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
371
- this.ws.send(JSON.stringify(message));
372
- }
373
- }
374
463
  attemptReconnect() {
375
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
464
+ if (this.reconnectAttempts < RECONNECT.MAX_ATTEMPTS) {
376
465
  this.reconnectAttempts++;
377
466
  setTimeout(async () => {
378
467
  try {
379
468
  await this.connect();
380
469
  }
381
- catch (error) { /* ignore */ }
382
- }, this.reconnectDelay);
470
+ catch (_a) { }
471
+ }, TIMEOUTS.RECONNECT_DELAY);
383
472
  }
384
473
  else {
385
474
  this.emit("max_reconnect_attempts");
386
475
  }
387
476
  }
388
477
  async stop() {
389
- return new Promise(async (resolve) => {
478
+ this.stopHeartbeat();
479
+ this.cleanupAllClients();
480
+ if (this.ws) {
390
481
  try {
391
- // Close all WebSocket clients
392
- for (const [id, client] of this.webSocketClients.entries()) {
393
- try {
394
- if (client.readyState === ws_1.default.OPEN) {
395
- client.close();
396
- }
397
- }
398
- catch (error) {
399
- console.error(`Error closing WebSocket client ${id}:`, error);
400
- }
401
- }
402
- this.webSocketClients.clear();
403
- // Close all SSE clients
404
- for (const [id, client] of this.sseClients.entries()) {
405
- try {
406
- client.destroy();
407
- }
408
- catch (error) {
409
- console.error(`Error destroying SSE client ${id}:`, error);
410
- }
411
- }
412
- this.sseClients.clear();
413
- // Close main WebSocket connection
414
- if (this.ws) {
415
- try {
416
- if (this.ws.readyState === ws_1.default.OPEN) {
417
- this.ws.close();
418
- }
419
- }
420
- catch (error) {
421
- console.error('Error closing main WebSocket:', error);
422
- }
423
- this.ws = null;
424
- }
425
- // Destroy the HTTP agent to close pooled connections
426
- if (this.httpAgent) {
427
- this.httpAgent.destroy();
428
- }
429
- // Wait for all connections to close properly
430
- await new Promise(resolve => setTimeout(resolve, 500));
431
- resolve();
432
- }
433
- catch (error) {
434
- console.error('Error in stop method:', error);
435
- resolve(); // Resolve anyway to not block cleaning up
482
+ if (this.ws.readyState === ws_1.default.OPEN)
483
+ this.ws.close();
436
484
  }
437
- });
485
+ catch (_a) { }
486
+ this.ws = null;
487
+ }
488
+ this.tunnelUrl = null;
489
+ this.httpAgent.destroy();
490
+ await new Promise(resolve => setTimeout(resolve, TIMEOUTS.SHUTDOWN_GRACE));
438
491
  }
439
492
  }
440
493
  exports.DainTunnel = DainTunnel;