Skip to content

Commit 7b94dcc

Browse files
committed
v0.6.9: add application-level heartbeat to prevent proxy idle timeouts
Railway's reverse proxy may not recognize Engine.IO ping/pong as connection activity. Send explicit heartbeat messages every 8s on both expose and reach commands to keep the WebSocket alive.
1 parent 7d928e4 commit 7b94dcc

File tree

4 files changed

+47
-2
lines changed

4 files changed

+47
-2
lines changed

apps/agent/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent",
3-
"version": "0.6.8",
3+
"version": "0.6.9",
44
"private": true,
55
"bin": {
66
"connect": "./dist/cli.js"

apps/agent/src/commands/expose.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,9 +459,30 @@ export async function exposeCommand(target: string, options: ExposeOptions): Pro
459459
}
460460
});
461461

462+
// Application-level keepalive to prevent Railway/proxy idle timeouts.
463+
// Engine.IO ping/pong may not be recognized as activity by all reverse proxies.
464+
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
465+
466+
socket.on('connect', () => {
467+
if (heartbeatInterval) clearInterval(heartbeatInterval);
468+
heartbeatInterval = setInterval(() => {
469+
if (socket.connected) {
470+
socket.emit('heartbeat');
471+
}
472+
}, 8000);
473+
});
474+
475+
socket.on('disconnect', () => {
476+
if (heartbeatInterval) {
477+
clearInterval(heartbeatInterval);
478+
heartbeatInterval = null;
479+
}
480+
});
481+
462482
// Handle process signals
463483
process.on('SIGINT', () => {
464484
console.log(chalk.yellow('\n👋 Stopping exposure...'));
485+
if (heartbeatInterval) clearInterval(heartbeatInterval);
465486
socket.disconnect();
466487
process.exit(0);
467488
});

apps/agent/src/commands/reach.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,9 @@ async function createLocalTunnel(
230230
},
231231
transports: ['websocket'],
232232
reconnection: true,
233+
reconnectionDelay: 1000,
234+
reconnectionDelayMax: 10000,
235+
timeout: 30000,
233236
});
234237

235238
const connections = new Map<string, { localSocket: net.Socket; ready: boolean; buffer: Buffer[] }>();
@@ -401,16 +404,37 @@ async function createLocalTunnel(
401404
});
402405
});
403406

407+
// Application-level keepalive to prevent proxy idle timeouts
408+
let heartbeatInterval: ReturnType<typeof setInterval> | null = null;
409+
410+
socket.on('connect', () => {
411+
if (heartbeatInterval) clearInterval(heartbeatInterval);
412+
heartbeatInterval = setInterval(() => {
413+
if (socket.connected) {
414+
socket.emit('heartbeat');
415+
}
416+
}, 8000);
417+
});
418+
419+
socket.on('disconnect', () => {
420+
if (heartbeatInterval) {
421+
clearInterval(heartbeatInterval);
422+
heartbeatInterval = null;
423+
}
424+
});
425+
404426
// Handle shutdown
405427
process.on('SIGINT', () => {
406428
console.log(chalk.yellow('\n👋 Disconnecting...'));
429+
if (heartbeatInterval) clearInterval(heartbeatInterval);
407430
try { unregisterRoute(service.name); } catch { /* cleanup best-effort */ }
408431
server.close();
409432
socket.disconnect();
410433
process.exit(0);
411434
});
412435

413436
process.on('SIGTERM', () => {
437+
if (heartbeatInterval) clearInterval(heartbeatInterval);
414438
try { unregisterRoute(service.name); } catch { /* cleanup best-effort */ }
415439
server.close();
416440
socket.disconnect();

packages/cli/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "private-connect",
3-
"version": "0.6.8",
3+
"version": "0.6.9",
44
"description": "Access private services by name from anywhere. No VPN setup, no firewall rules. Open source alternative to ngrok and Tailscale for service-level connectivity.",
55
"bin": {
66
"private-connect": "./dist/index.js"

0 commit comments

Comments
 (0)