@dainprotocol/tunnel 1.1.18 → 1.1.21

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.
@@ -233,33 +233,41 @@ class DainTunnel extends events_1.EventEmitter {
233
233
  }
234
234
  handleSSEConnection(message) {
235
235
  var _a;
236
- const startTime = Date.now();
237
- console.log(`[SSE E2E] Client: Establishing SSE connection ${message.id} to localhost:${this.port}${message.path}`);
238
236
  try {
239
- const options = {
237
+ const req = http_1.default.request({
240
238
  hostname: 'localhost',
241
239
  port: this.port,
242
240
  path: message.path,
243
241
  method: message.method || 'GET',
244
242
  headers: message.headers,
245
243
  agent: this.httpAgent,
246
- };
247
- const req = http_1.default.request(options, (res) => {
244
+ }, (res) => {
248
245
  var _a;
246
+ // Non-200 response - forward error to tunnel server
249
247
  if (res.statusCode !== 200) {
250
- console.log(`[SSE E2E] Client: Service returned status ${res.statusCode} for ${message.id}`);
251
- res.resume();
252
- if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
253
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: `Status ${res.statusCode}` }));
254
- }
248
+ let errorBody = '';
249
+ res.on('data', (chunk) => { errorBody += chunk.toString(); });
250
+ 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
+ }
258
+ });
255
259
  return;
256
260
  }
257
- console.log(`[SSE E2E] Client: Service accepted SSE connection ${message.id}, streaming started`);
261
+ // Optimize TCP for streaming
258
262
  const socket = res.socket || res.connection;
259
263
  if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
260
264
  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
261
270
  let buffer = '';
262
- let eventCount = 0;
263
271
  res.on('data', (chunk) => {
264
272
  var _a;
265
273
  buffer += chunk.toString();
@@ -276,28 +284,20 @@ class DainTunnel extends events_1.EventEmitter {
276
284
  }
277
285
  if (data.endsWith('\n'))
278
286
  data = data.slice(0, -1);
279
- eventCount++;
280
- console.log(`[SSE E2E] Client: Received event #${eventCount} from service for ${message.id}: ${event} (${Date.now() - startTime}ms)`);
281
287
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
282
288
  this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event, data }));
283
- console.log(`[SSE E2E] Client: Forwarded event to tunnel server for ${message.id}`);
284
- }
285
- else {
286
- console.log(`[SSE E2E] Client: WebSocket closed, cannot forward event for ${message.id}`);
287
289
  }
288
290
  }
289
291
  });
290
292
  res.on('end', () => {
291
293
  var _a;
292
- console.log(`[SSE E2E] Client: Service closed connection for ${message.id} after ${eventCount} events (${Date.now() - startTime}ms)`);
293
294
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
294
- this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: 'Connection closed' }));
295
+ this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'close', data: '' }));
295
296
  }
296
297
  });
297
298
  });
298
299
  req.on('error', (error) => {
299
300
  var _a;
300
- console.log(`[SSE E2E] Client: Request error for ${message.id}: ${error.message}`);
301
301
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
302
302
  this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
303
303
  }
@@ -309,23 +309,17 @@ class DainTunnel extends events_1.EventEmitter {
309
309
  this.emit("sse_connection", { id: message.id, path: message.path });
310
310
  }
311
311
  catch (error) {
312
- console.log(`[SSE E2E] Client: Exception for ${message.id}: ${error.message}`);
313
312
  if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === ws_1.default.OPEN) {
314
313
  this.ws.send(JSON.stringify({ type: 'sse', id: message.id, event: 'error', data: error.message }));
315
314
  }
316
315
  }
317
316
  }
318
317
  handleSSEClose(message) {
319
- console.log(`[SSE E2E] Client: Received close command from server for ${message.id}`);
320
318
  const client = this.sseClients.get(message.id);
321
319
  if (client) {
322
- console.log(`[SSE E2E] Client: Destroying local service connection for ${message.id}`);
323
320
  client.destroy();
324
321
  this.sseClients.delete(message.id);
325
322
  }
326
- else {
327
- console.log(`[SSE E2E] Client: No active connection found for ${message.id} (already closed?)`);
328
- }
329
323
  }
330
324
  forwardRequest(request) {
331
325
  return new Promise((resolve, reject) => {
@@ -20,6 +20,7 @@ declare class DainTunnelServer {
20
20
  private handleStartMessage;
21
21
  private handleResponseMessage;
22
22
  private handleSSEMessage;
23
+ private cleanupSSEConnection;
23
24
  private handleWebSocketMessage;
24
25
  private handleRequest;
25
26
  private handleSSERequest;
@@ -324,48 +324,61 @@ class DainTunnelServer {
324
324
  }
325
325
  handleSSEMessage(data) {
326
326
  const connection = this.sseConnections.get(data.id);
327
- if (!connection) {
328
- console.log(`[SSE Debug] handleSSEMessage: No connection found for ${data.id}, event=${data.event}`);
327
+ if (!connection)
329
328
  return;
330
- }
331
329
  const { res, tunnelId } = connection;
332
- const clientDisconnected = connection.clientDisconnected;
333
- const isCloseEvent = data.event === 'close';
334
- console.log(`[SSE Debug] handleSSEMessage: id=${data.id}, event=${data.event}, clientDisconnected=${clientDisconnected}, isClose=${isCloseEvent}`);
335
- if (isCloseEvent) {
330
+ const conn = connection;
331
+ // Skip 'connected' event - we already wrote headers
332
+ if (data.event === 'connected')
333
+ return;
334
+ // Handle close event
335
+ if (data.event === 'close') {
336
336
  const tunnel = this.tunnels.get(tunnelId);
337
- if (tunnel) {
337
+ if (tunnel)
338
338
  tunnel.ws.send(JSON.stringify({ type: "sse_close", id: data.id }));
339
+ if (!conn.clientDisconnected && res.writable) {
340
+ try {
341
+ res.end();
342
+ }
343
+ catch (_a) { }
339
344
  }
340
- if (!clientDisconnected && res.writable) {
345
+ this.cleanupSSEConnection(data.id, tunnelId);
346
+ return;
347
+ }
348
+ // Handle error - send as SSE error event (headers already written)
349
+ if (data.event === 'error') {
350
+ if (!conn.clientDisconnected && res.writable) {
341
351
  try {
342
- res.write(`event: ${data.event}\ndata: ${data.data}\n\n`);
352
+ res.write(`event: error\ndata: ${data.data}\n\n`);
343
353
  flushSSE(res);
344
354
  res.end();
345
355
  }
346
- catch (error) { /* ignore */ }
356
+ catch (_b) { }
347
357
  }
348
- // Decrement request counter for backpressure tracking
349
- const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
350
- this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
351
- this.sseConnections.delete(data.id);
358
+ this.cleanupSSEConnection(data.id, tunnelId);
352
359
  return;
353
360
  }
354
- if (clientDisconnected || !res.writable) {
355
- connection.clientDisconnected = true;
361
+ // Skip if client disconnected
362
+ if (conn.clientDisconnected || !res.writable) {
363
+ conn.clientDisconnected = true;
356
364
  return;
357
365
  }
366
+ // Forward SSE event
358
367
  try {
359
368
  if (data.event)
360
369
  res.write(`event: ${data.event}\n`);
361
- const escapedData = data.data.replace(/\n/g, '\ndata: ');
362
- res.write(`data: ${escapedData}\n\n`);
370
+ res.write(`data: ${data.data.replace(/\n/g, '\ndata: ')}\n\n`);
363
371
  flushSSE(res);
364
372
  }
365
- catch (error) {
366
- connection.clientDisconnected = true;
373
+ catch (_c) {
374
+ conn.clientDisconnected = true;
367
375
  }
368
376
  }
377
+ cleanupSSEConnection(id, tunnelId) {
378
+ const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
379
+ this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
380
+ this.sseConnections.delete(id);
381
+ }
369
382
  handleWebSocketMessage(data) {
370
383
  const connection = this.wsConnections.get(data.id);
371
384
  if (!connection)
@@ -453,27 +466,24 @@ class DainTunnelServer {
453
466
  }
454
467
  handleSSERequest(req, res, tunnelId, tunnel) {
455
468
  const sseId = fastId();
456
- // Optimize TCP socket for low-latency streaming
469
+ // Optimize TCP for streaming
457
470
  const socket = req.socket || req.connection;
458
- if (socket && socket.setNoDelay) {
459
- socket.setNoDelay(true); // Disable Nagle's algorithm for immediate transmission
460
- }
461
- // Set SSE headers with anti-buffering directives
462
- // Note: Do NOT set 'Connection: keep-alive' - it's forbidden in HTTP/2 and will cause immediate disconnect
471
+ if (socket === null || socket === void 0 ? void 0 : socket.setNoDelay)
472
+ socket.setNoDelay(true);
473
+ // Write SSE headers immediately to keep browser connection alive
463
474
  res.writeHead(200, {
464
475
  'Content-Type': 'text/event-stream',
465
476
  'Cache-Control': 'no-cache',
466
- 'X-Accel-Buffering': 'no', // Disable nginx buffering
477
+ 'X-Accel-Buffering': 'no',
467
478
  });
468
- // Flush headers immediately to establish SSE connection
469
- // Without this, headers may be buffered, delaying connection setup
470
479
  res.flushHeaders();
471
- // Send SSE comment to keep connection alive and verify it's working
472
- // Comments start with : and are ignored by EventSource but flush the buffer
473
480
  res.write(': connected\n\n');
474
- // Flush the initial write
475
481
  flushSSE(res);
476
- this.sseConnections.set(sseId, { req, res, id: sseId, tunnelId });
482
+ this.sseConnections.set(sseId, {
483
+ req, res, id: sseId, tunnelId,
484
+ clientDisconnected: false
485
+ });
486
+ // Forward to tunnel client
477
487
  tunnel.ws.send(JSON.stringify({
478
488
  type: "sse_connection",
479
489
  id: sseId,
@@ -482,31 +492,22 @@ class DainTunnelServer {
482
492
  headers: req.headers,
483
493
  body: req.method !== "GET" && req.body ? req.body.toString("base64") : undefined
484
494
  }));
485
- req.on('close', () => {
486
- console.log(`[SSE Debug] req.close fired for ${sseId} - client disconnected`);
495
+ // Track response close (not request close) - for POST SSE, req closes immediately
496
+ res.on('close', () => {
487
497
  const connection = this.sseConnections.get(sseId);
488
498
  if (connection) {
489
499
  connection.clientDisconnected = true;
490
- // Clean up after a short delay to allow any pending events to be handled
491
500
  setTimeout(() => {
492
501
  if (this.sseConnections.has(sseId)) {
493
- console.log(`[SSE Debug] Cleaning up SSE connection ${sseId} after client disconnect`);
494
- // Decrement request counter for backpressure tracking
495
- const currentCount = this.tunnelRequestCount.get(tunnelId) || 1;
496
- this.tunnelRequestCount.set(tunnelId, Math.max(0, currentCount - 1));
497
- this.sseConnections.delete(sseId);
498
- // Notify tunnel client to close the connection
499
- const tunnel = this.tunnels.get(tunnelId);
500
- if (tunnel && tunnel.ws.readyState === ws_1.default.OPEN) {
501
- tunnel.ws.send(JSON.stringify({ type: "sse_close", id: sseId }));
502
+ this.cleanupSSEConnection(sseId, tunnelId);
503
+ const t = this.tunnels.get(tunnelId);
504
+ if ((t === null || t === void 0 ? void 0 : t.ws.readyState) === ws_1.default.OPEN) {
505
+ t.ws.send(JSON.stringify({ type: "sse_close", id: sseId }));
502
506
  }
503
507
  }
504
508
  }, 100);
505
509
  }
506
510
  });
507
- req.on('error', (err) => {
508
- console.log(`[SSE Debug] req.error fired for ${sseId}:`, err.message);
509
- });
510
511
  }
511
512
  removeTunnel(ws) {
512
513
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dainprotocol/tunnel",
3
- "version": "1.1.18",
3
+ "version": "1.1.21",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "private": false,
@@ -19,7 +19,7 @@
19
19
  "author": "Ryan",
20
20
  "license": "ISC",
21
21
  "dependencies": {
22
- "@dainprotocol/service-sdk": "2.0.56",
22
+ "@dainprotocol/service-sdk": "2.0.57",
23
23
  "@types/body-parser": "^1.19.5",
24
24
  "@types/cors": "^2.8.17",
25
25
  "@types/eventsource": "^3.0.0",
@@ -77,4 +77,4 @@
77
77
  ]
78
78
  }
79
79
  }
80
- }
80
+ }