@dainprotocol/tunnel 1.1.26 → 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.
@@ -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,63 +28,53 @@ 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
- // Heartbeat interval for client-side liveness
25
33
  this.heartbeatInterval = null;
26
- this.HEARTBEAT_INTERVAL = 25000; // 25 seconds (less than server's 30s)
27
- // Client-side timeout (less than server's 30s to respond before server times out)
28
- this.REQUEST_TIMEOUT = 25000;
29
- // Parse API key to extract agentId and secret
30
34
  const parsed = (0, auth_1.parseAPIKey)(apiKey);
31
35
  if (!parsed) {
32
36
  throw new Error('Invalid API key format. Expected: sk_agent_{agentId}_{orgId}_{secret}');
33
37
  }
34
38
  this.apiKey = apiKey;
35
- this.tunnelId = `${parsed.orgId}_${parsed.agentId}`; // orgId_agentId to prevent collisions
36
- this.secret = parsed.secret; // secret for HMAC signatures
37
- // High-frequency optimization: Create reusable HTTP agent with connection pooling
38
- // Aligned with server's MAX_CONCURRENT_REQUESTS_PER_TUNNEL = 100
39
+ this.tunnelId = `${parsed.orgId}_${parsed.agentId}`;
40
+ this.secret = parsed.secret;
39
41
  this.httpAgent = new http_1.default.Agent({
40
42
  keepAlive: true,
41
- keepAliveMsecs: 30000, // Keep connections alive for 30s
42
- maxSockets: 100, // Match server's concurrent request limit
43
- maxFreeSockets: 20, // Keep more idle connections for burst traffic
43
+ keepAliveMsecs: 30000,
44
+ maxSockets: 100,
45
+ maxFreeSockets: 20,
44
46
  });
45
47
  }
46
- /**
47
- * Sign a challenge using HMAC-SHA256
48
- * @private
49
- */
50
48
  signChallenge(challenge) {
51
- return (0, crypto_1.createHmac)('sha256', this.secret)
52
- .update(challenge)
53
- .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;
54
63
  }
55
- /**
56
- * Start client-side heartbeat to detect connection issues early
57
- * @private
58
- */
59
64
  startHeartbeat() {
60
- this.stopHeartbeat(); // Clear any existing heartbeat
65
+ this.stopHeartbeat();
61
66
  this.heartbeatInterval = setInterval(() => {
62
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
67
+ var _a;
68
+ if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
63
69
  try {
64
70
  this.ws.ping();
65
71
  }
66
- catch (error) {
67
- // Ping failed, connection might be dead
72
+ catch (_b) {
68
73
  this.emit("error", new Error("Heartbeat ping failed"));
69
74
  }
70
75
  }
71
- }, this.HEARTBEAT_INTERVAL);
76
+ }, TIMEOUTS.HEARTBEAT);
72
77
  }
73
- /**
74
- * Stop the client-side heartbeat
75
- * @private
76
- */
77
78
  stopHeartbeat() {
78
79
  if (this.heartbeatInterval) {
79
80
  clearInterval(this.heartbeatInterval);
@@ -88,25 +89,20 @@ class DainTunnel extends events_1.EventEmitter {
88
89
  return new Promise((resolve, reject) => {
89
90
  let resolved = false;
90
91
  let connectionTimeoutId = null;
91
- const cleanup = () => {
92
+ const finish = (value, error) => {
93
+ if (resolved)
94
+ return;
95
+ resolved = true;
92
96
  if (connectionTimeoutId) {
93
97
  clearTimeout(connectionTimeoutId);
94
98
  connectionTimeoutId = null;
95
99
  }
96
- };
97
- const safeResolve = (value) => {
98
- if (!resolved) {
99
- resolved = true;
100
- cleanup();
101
- resolve(value);
102
- }
103
- };
104
- const safeReject = (error) => {
105
- if (!resolved) {
106
- resolved = true;
107
- cleanup();
100
+ if (error) {
108
101
  reject(error);
109
102
  }
103
+ else {
104
+ resolve(value);
105
+ }
110
106
  };
111
107
  try {
112
108
  this.ws = new ws_1.default(this.serverUrl);
@@ -115,78 +111,74 @@ class DainTunnel extends events_1.EventEmitter {
115
111
  try {
116
112
  const challenge = await this.requestChallenge();
117
113
  const signature = this.signChallenge(challenge);
118
- this.sendMessage({
114
+ this.safeSend({
119
115
  type: "start",
120
116
  port: this.port,
121
117
  challenge,
122
118
  signature,
123
119
  tunnelId: this.tunnelId,
124
- apiKey: this.apiKey // Send API key for server validation
120
+ apiKey: this.apiKey
125
121
  });
126
- // Start heartbeat after successful authentication
127
122
  this.startHeartbeat();
128
123
  this.emit("connected");
129
124
  }
130
125
  catch (err) {
131
- safeReject(err);
126
+ finish(undefined, err);
132
127
  }
133
128
  });
134
129
  this.ws.on("message", (data) => {
135
130
  try {
136
- this.handleMessage(JSON.parse(data), safeResolve);
131
+ this.handleMessage(JSON.parse(data), (url) => finish(url));
137
132
  }
138
133
  catch (err) {
139
- safeReject(err);
134
+ finish(undefined, err);
140
135
  }
141
136
  });
142
137
  this.ws.on("close", () => {
143
- // Stop heartbeat on disconnect
144
138
  this.stopHeartbeat();
145
- // Clean up all active SSE connections
146
- for (const [, client] of this.sseClients) {
147
- try {
148
- client.destroy();
149
- }
150
- catch (e) { /* ignore */ }
151
- }
152
- this.sseClients.clear();
153
- // Clean up all WebSocket clients
154
- for (const [, client] of this.webSocketClients) {
155
- try {
156
- if (client.readyState === ws_1.default.OPEN)
157
- client.close(1001);
158
- }
159
- catch (e) { /* ignore */ }
160
- }
161
- this.webSocketClients.clear();
139
+ this.cleanupAllClients();
162
140
  if (this.tunnelUrl) {
163
141
  this.emit("disconnected");
164
142
  this.attemptReconnect();
165
143
  }
166
144
  else {
167
- safeReject(new Error("Connection closed before tunnel established"));
145
+ finish(undefined, new Error("Connection closed before tunnel established"));
168
146
  }
169
147
  });
170
148
  this.ws.on("error", (error) => this.emit("error", error));
171
- // Connection timeout with proper cleanup
172
149
  connectionTimeoutId = setTimeout(() => {
150
+ var _a;
173
151
  if (!resolved && (!this.ws || this.ws.readyState !== ws_1.default.OPEN)) {
174
- safeReject(new Error("Connection timeout"));
175
- // Close the WebSocket if it's still trying to connect
176
- if (this.ws) {
177
- try {
178
- this.ws.terminate();
179
- }
180
- catch (e) { /* ignore */ }
152
+ finish(undefined, new Error("Connection timeout"));
153
+ try {
154
+ (_a = this.ws) === null || _a === void 0 ? void 0 : _a.terminate();
181
155
  }
156
+ catch (_b) { }
182
157
  }
183
- }, 10000);
158
+ }, TIMEOUTS.CONNECTION);
184
159
  }
185
160
  catch (err) {
186
- safeReject(err);
161
+ finish(undefined, err);
187
162
  }
188
163
  });
189
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
+ }
190
182
  async requestChallenge() {
191
183
  return new Promise((resolve, reject) => {
192
184
  if (!this.ws) {
@@ -195,38 +187,37 @@ class DainTunnel extends events_1.EventEmitter {
195
187
  }
196
188
  let resolved = false;
197
189
  let timeoutId = null;
198
- const cleanup = () => {
199
- if (this.ws) {
200
- this.ws.removeListener("message", challengeHandler);
201
- }
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);
202
196
  if (timeoutId) {
203
197
  clearTimeout(timeoutId);
204
198
  timeoutId = null;
205
199
  }
200
+ if (error) {
201
+ reject(error);
202
+ }
203
+ else {
204
+ resolve(challenge);
205
+ }
206
206
  };
207
207
  const challengeHandler = (message) => {
208
208
  try {
209
209
  const data = JSON.parse(message);
210
- if (data.type === "challenge" && !resolved) {
211
- resolved = true;
212
- cleanup();
213
- resolve(data.challenge);
210
+ if (data.type === "challenge") {
211
+ finish(data.challenge);
214
212
  }
215
213
  }
216
- catch (e) {
217
- // Ignore parse errors for non-challenge messages
218
- }
214
+ catch (_a) { }
219
215
  };
220
216
  this.ws.on("message", challengeHandler);
221
217
  this.ws.send(JSON.stringify({ type: "challenge_request" }));
222
- // Add a timeout for the challenge request
223
218
  timeoutId = setTimeout(() => {
224
- if (!resolved) {
225
- resolved = true;
226
- cleanup();
227
- reject(new Error("Challenge request timeout"));
228
- }
229
- }, 5000);
219
+ finish(undefined, new Error("Challenge request timeout"));
220
+ }, TIMEOUTS.CHALLENGE);
230
221
  });
231
222
  }
232
223
  handleMessage(message, resolve) {
@@ -256,141 +247,119 @@ class DainTunnel extends events_1.EventEmitter {
256
247
  async handleRequest(request) {
257
248
  try {
258
249
  const response = await this.forwardRequest(request);
259
- this.sendMessage(response);
250
+ this.safeSend(response);
260
251
  this.emit("request_handled", { request, response });
261
252
  }
262
253
  catch (error) {
263
- // Send error response back to server instead of just emitting an event
264
- // This prevents 30-second timeouts when the local service is unreachable
265
254
  const errorMessage = error instanceof Error ? error.message : "Unknown error";
266
- const errorResponse = {
255
+ this.safeSend({
267
256
  type: "response",
268
257
  requestId: request.id,
269
- status: 502, // Bad Gateway - indicates upstream error
258
+ status: 502,
270
259
  headers: { "content-type": "application/json" },
271
260
  body: Buffer.from(JSON.stringify({
272
261
  error: "Bad Gateway",
273
262
  message: `Failed to forward request to local service: ${errorMessage}`
274
263
  })).toString("base64")
275
- };
276
- this.sendMessage(errorResponse);
264
+ });
277
265
  this.emit("request_error", { request, error });
278
266
  }
279
267
  }
280
268
  handleWebSocketConnection(message) {
281
- var _a;
269
+ const sendWsEvent = (event, data) => {
270
+ this.safeSend({ type: 'websocket', id: message.id, event, data });
271
+ };
282
272
  try {
283
273
  const client = new ws_1.default(`ws://localhost:${this.port}${message.path}`, {
284
274
  headers: message.headers
285
275
  });
286
276
  this.webSocketClients.set(message.id, client);
287
277
  client.on('message', (data) => {
288
- var _a;
289
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
290
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'message', data: data.toString('base64') }));
291
- }
278
+ sendWsEvent('message', data.toString('base64'));
292
279
  });
293
280
  client.on('close', () => {
294
- var _a;
295
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
296
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'close' }));
297
- }
281
+ sendWsEvent('close');
298
282
  this.webSocketClients.delete(message.id);
299
283
  });
300
284
  client.on('error', (error) => {
301
- var _a;
302
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
303
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
304
- }
285
+ sendWsEvent('error', error.message);
305
286
  this.webSocketClients.delete(message.id);
306
287
  });
307
288
  this.emit("websocket_connection", { id: message.id, path: message.path });
308
289
  }
309
290
  catch (error) {
310
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
311
- this.ws.send(JSON.stringify({ type: 'websocket', id: message.id, event: 'error', data: error.message }));
312
- }
291
+ sendWsEvent('error', error.message);
313
292
  }
314
293
  }
315
294
  handleWebSocketMessage(message) {
316
295
  const client = this.webSocketClients.get(message.id);
317
296
  if (!client)
318
297
  return;
319
- if (message.event === 'message' && message.data) {
320
- const data = Buffer.from(message.data, 'base64');
321
- if (client.readyState === ws_1.default.OPEN) {
322
- client.send(data);
323
- }
324
- }
325
- else if (message.event === 'close') {
326
- if (client.readyState === ws_1.default.OPEN) {
327
- client.close();
328
- }
329
- 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;
330
310
  }
331
311
  }
332
312
  handleSSEConnection(message) {
333
- var _a;
313
+ const sendSseEvent = (event, data = '') => {
314
+ this.safeSend({ type: 'sse', id: message.id, event, data });
315
+ };
334
316
  try {
317
+ const headers = { ...message.headers };
318
+ delete headers.host;
319
+ headers['accept-encoding'] = 'identity';
335
320
  const req = http_1.default.request({
336
321
  hostname: 'localhost',
337
322
  port: this.port,
338
323
  path: message.path,
339
324
  method: message.method || 'GET',
340
- headers: message.headers,
325
+ headers,
341
326
  agent: this.httpAgent,
342
327
  }, (res) => {
343
- var _a;
344
- // Non-200 response - forward error to tunnel server
345
328
  if (res.statusCode !== 200) {
346
329
  let errorBody = '';
347
330
  res.on('data', (chunk) => { errorBody += chunk.toString(); });
348
331
  res.on('end', () => {
349
- var _a;
350
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
351
- this.ws.send(JSON.stringify({
352
- type: 'sse', id: message.id, event: 'error',
353
- data: `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`
354
- }));
355
- }
332
+ sendSseEvent('error', `Status ${res.statusCode}: ${errorBody.substring(0, 200)}`);
356
333
  });
357
334
  return;
358
335
  }
359
- // Optimize TCP for streaming
360
336
  const socket = res.socket || res.connection;
361
337
  if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
362
338
  socket.setNoDelay(true);
363
- // Notify tunnel server that local service accepted
364
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
365
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'connected', data: '' }));
366
- }
367
- // Stream SSE events to tunnel server
339
+ sendSseEvent('connected');
368
340
  let buffer = '';
369
- // Helper to process SSE events from buffer
370
341
  const processBuffer = () => {
371
- var _a;
372
- // FIX: Normalize CRLF to LF for cross-platform SSE compatibility
373
- // Some SSE implementations use \r\n\r\n instead of \n\n
374
342
  buffer = buffer.replace(/\r\n/g, '\n');
375
343
  while (buffer.includes('\n\n')) {
376
344
  const idx = buffer.indexOf('\n\n');
377
345
  const msgData = buffer.substring(0, idx);
378
346
  buffer = buffer.substring(idx + 2);
379
- // Skip empty messages and keepalive comments
380
347
  if (!msgData.trim() || msgData.startsWith(':'))
381
348
  continue;
382
- let event = 'message', data = '';
349
+ let event = 'message';
350
+ let data = '';
383
351
  for (const line of msgData.split('\n')) {
384
- if (line.startsWith('event:'))
352
+ if (line.startsWith('event:')) {
385
353
  event = line.substring(6).trim();
386
- else if (line.startsWith('data:'))
354
+ }
355
+ else if (line.startsWith('data:')) {
387
356
  data += line.substring(5).trim() + '\n';
357
+ }
388
358
  }
389
359
  if (data.endsWith('\n'))
390
360
  data = data.slice(0, -1);
391
- // Only send if we have actual data
392
- if (data && ((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
393
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event, data }));
361
+ if (data) {
362
+ sendSseEvent(event, data);
394
363
  }
395
364
  }
396
365
  };
@@ -399,34 +368,29 @@ class DainTunnel extends events_1.EventEmitter {
399
368
  processBuffer();
400
369
  });
401
370
  res.on('end', () => {
402
- var _a;
403
- // Process any remaining data in buffer (might be missing final \n\n)
404
371
  if (buffer.trim()) {
405
- // Add missing newlines to ensure processing
406
372
  buffer += '\n\n';
407
373
  processBuffer();
408
374
  }
409
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
410
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: '' }));
411
- }
375
+ sendSseEvent('close');
412
376
  });
413
377
  });
378
+ req.on('socket', (socket) => {
379
+ socket.setNoDelay(true);
380
+ socket.setTimeout(0);
381
+ });
414
382
  req.on('error', (error) => {
415
- var _a;
416
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
417
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
418
- }
383
+ sendSseEvent('error', error.message);
419
384
  });
420
- if (message.body && message.method !== 'GET')
385
+ if (message.body && message.method !== 'GET') {
421
386
  req.write(Buffer.from(message.body, 'base64'));
387
+ }
422
388
  req.end();
423
389
  this.sseClients.set(message.id, req);
424
390
  this.emit("sse_connection", { id: message.id, path: message.path });
425
391
  }
426
392
  catch (error) {
427
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
428
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
429
- }
393
+ sendSseEvent('error', error.message);
430
394
  }
431
395
  }
432
396
  handleSSEClose(message) {
@@ -440,25 +404,20 @@ class DainTunnel extends events_1.EventEmitter {
440
404
  return new Promise((resolve, reject) => {
441
405
  let resolved = false;
442
406
  let timeoutId = null;
443
- const cleanup = () => {
407
+ const finish = (response, error) => {
408
+ if (resolved)
409
+ return;
410
+ resolved = true;
444
411
  if (timeoutId) {
445
412
  clearTimeout(timeoutId);
446
413
  timeoutId = null;
447
414
  }
448
- };
449
- const safeResolve = (response) => {
450
- if (!resolved) {
451
- resolved = true;
452
- cleanup();
453
- resolve(response);
454
- }
455
- };
456
- const safeReject = (error) => {
457
- if (!resolved) {
458
- resolved = true;
459
- cleanup();
415
+ if (error) {
460
416
  reject(error);
461
417
  }
418
+ else {
419
+ resolve(response);
420
+ }
462
421
  };
463
422
  const options = {
464
423
  hostname: 'localhost',
@@ -466,120 +425,69 @@ class DainTunnel extends events_1.EventEmitter {
466
425
  path: request.path,
467
426
  method: request.method,
468
427
  headers: request.headers,
469
- agent: this.httpAgent, // Use connection pooling
470
- timeout: this.REQUEST_TIMEOUT, // Socket timeout
428
+ agent: this.httpAgent,
429
+ timeout: TIMEOUTS.REQUEST,
471
430
  };
472
431
  const req = http_1.default.request(options, (res) => {
473
- let body = Buffer.from([]);
474
- res.on('data', (chunk) => {
475
- body = Buffer.concat([body, chunk]);
476
- });
432
+ const chunks = [];
433
+ res.on('data', (chunk) => chunks.push(chunk));
477
434
  res.on('end', () => {
478
435
  const headers = { ...res.headers };
479
436
  delete headers['transfer-encoding'];
480
437
  delete headers['content-length'];
481
- safeResolve({
438
+ finish({
482
439
  type: 'response',
483
440
  requestId: request.id,
484
441
  status: res.statusCode,
485
442
  headers,
486
- body: body.toString('base64'),
443
+ body: Buffer.concat(chunks).toString('base64'),
487
444
  });
488
445
  });
489
- res.on('error', safeReject);
446
+ res.on('error', (err) => finish(undefined, err));
490
447
  });
491
- // Handle request timeout
492
448
  req.on('timeout', () => {
493
449
  req.destroy();
494
- safeReject(new Error(`Request timeout after ${this.REQUEST_TIMEOUT}ms`));
450
+ finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
495
451
  });
496
- req.on('error', safeReject);
497
- // Additional safety timeout
452
+ req.on('error', (err) => finish(undefined, err));
498
453
  timeoutId = setTimeout(() => {
499
- if (!resolved) {
500
- req.destroy();
501
- safeReject(new Error(`Request timeout after ${this.REQUEST_TIMEOUT}ms`));
502
- }
503
- }, this.REQUEST_TIMEOUT);
454
+ req.destroy();
455
+ finish(undefined, new Error(`Request timeout after ${TIMEOUTS.REQUEST}ms`));
456
+ }, TIMEOUTS.REQUEST);
504
457
  if (request.body && request.method !== 'GET') {
505
458
  req.write(Buffer.from(request.body, 'base64'));
506
459
  }
507
460
  req.end();
508
461
  });
509
462
  }
510
- sendMessage(message) {
511
- if (this.ws && this.ws.readyState === ws_1.default.OPEN) {
512
- this.ws.send(JSON.stringify(message));
513
- }
514
- }
515
463
  attemptReconnect() {
516
- if (this.reconnectAttempts < this.maxReconnectAttempts) {
464
+ if (this.reconnectAttempts < RECONNECT.MAX_ATTEMPTS) {
517
465
  this.reconnectAttempts++;
518
466
  setTimeout(async () => {
519
467
  try {
520
468
  await this.connect();
521
469
  }
522
- catch (error) { /* ignore */ }
523
- }, this.reconnectDelay);
470
+ catch (_a) { }
471
+ }, TIMEOUTS.RECONNECT_DELAY);
524
472
  }
525
473
  else {
526
474
  this.emit("max_reconnect_attempts");
527
475
  }
528
476
  }
529
477
  async stop() {
530
- return new Promise(async (resolve) => {
478
+ this.stopHeartbeat();
479
+ this.cleanupAllClients();
480
+ if (this.ws) {
531
481
  try {
532
- // Stop heartbeat first
533
- this.stopHeartbeat();
534
- // Close all WebSocket clients
535
- for (const [id, client] of this.webSocketClients.entries()) {
536
- try {
537
- if (client.readyState === ws_1.default.OPEN) {
538
- client.close();
539
- }
540
- }
541
- catch (error) {
542
- console.error(`Error closing WebSocket client ${id}:`, error);
543
- }
544
- }
545
- this.webSocketClients.clear();
546
- // Close all SSE clients
547
- for (const [id, client] of this.sseClients.entries()) {
548
- try {
549
- client.destroy();
550
- }
551
- catch (error) {
552
- console.error(`Error destroying SSE client ${id}:`, error);
553
- }
554
- }
555
- this.sseClients.clear();
556
- // Close main WebSocket connection
557
- if (this.ws) {
558
- try {
559
- if (this.ws.readyState === ws_1.default.OPEN) {
560
- this.ws.close();
561
- }
562
- }
563
- catch (error) {
564
- console.error('Error closing main WebSocket:', error);
565
- }
566
- this.ws = null;
567
- }
568
- // Reset tunnel URL so reconnect doesn't happen
569
- this.tunnelUrl = null;
570
- // Destroy the HTTP agent to close pooled connections
571
- if (this.httpAgent) {
572
- this.httpAgent.destroy();
573
- }
574
- // Wait for all connections to close properly
575
- await new Promise(resolve => setTimeout(resolve, 500));
576
- resolve();
577
- }
578
- catch (error) {
579
- console.error('Error in stop method:', error);
580
- resolve(); // Resolve anyway to not block cleaning up
482
+ if (this.ws.readyState === ws_1.default.OPEN)
483
+ this.ws.close();
581
484
  }
582
- });
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));
583
491
  }
584
492
  }
585
493
  exports.DainTunnel = DainTunnel;