voicecc 1.1.14 → 1.1.15
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.
- package/package.json +1 -1
- package/server/services/tunnel.ts +137 -37
package/package.json
CHANGED
|
@@ -6,6 +6,8 @@
|
|
|
6
6
|
*
|
|
7
7
|
* Responsibilities:
|
|
8
8
|
* - Start a quick tunnel on a given port and capture the public HTTPS URL
|
|
9
|
+
* - Auto-restart the tunnel on unexpected crashes (up to MAX_RESTART_ATTEMPTS)
|
|
10
|
+
* - Log connection-level events (connected/disconnected) for observability
|
|
9
11
|
* - Stop the tunnel and clear state
|
|
10
12
|
* - Expose tunnel state (URL, running status, start time)
|
|
11
13
|
*/
|
|
@@ -18,7 +20,13 @@ import { writeEnvKey } from "./env.js";
|
|
|
18
20
|
// ============================================================================
|
|
19
21
|
|
|
20
22
|
/** Timeout for waiting for the tunnel URL to appear */
|
|
21
|
-
const TUNNEL_URL_TIMEOUT_MS =
|
|
23
|
+
const TUNNEL_URL_TIMEOUT_MS = 15_000;
|
|
24
|
+
|
|
25
|
+
/** Maximum number of automatic restart attempts after an unexpected crash */
|
|
26
|
+
const MAX_RESTART_ATTEMPTS = 5;
|
|
27
|
+
|
|
28
|
+
/** Delay before attempting a restart (ms) */
|
|
29
|
+
const RESTART_DELAY_MS = 3_000;
|
|
22
30
|
|
|
23
31
|
// ============================================================================
|
|
24
32
|
// STATE
|
|
@@ -33,6 +41,15 @@ let tunnelUrl: string | null = null;
|
|
|
33
41
|
/** Timestamp when tunnel URL was obtained */
|
|
34
42
|
let tunnelStartedAt: number | null = null;
|
|
35
43
|
|
|
44
|
+
/** Port used for the current tunnel (needed for auto-restart) */
|
|
45
|
+
let tunnelPort: number | null = null;
|
|
46
|
+
|
|
47
|
+
/** Number of consecutive restart attempts since last successful start */
|
|
48
|
+
let restartAttempts = 0;
|
|
49
|
+
|
|
50
|
+
/** Whether the tunnel was intentionally stopped (skip auto-restart) */
|
|
51
|
+
let manuallyStopped = false;
|
|
52
|
+
|
|
36
53
|
// ============================================================================
|
|
37
54
|
// MAIN HANDLERS
|
|
38
55
|
// ============================================================================
|
|
@@ -50,6 +67,70 @@ export async function startTunnel(port: number): Promise<string> {
|
|
|
50
67
|
throw new Error("Tunnel is already running");
|
|
51
68
|
}
|
|
52
69
|
|
|
70
|
+
manuallyStopped = false;
|
|
71
|
+
tunnelPort = port;
|
|
72
|
+
|
|
73
|
+
const url = await createTunnel(port);
|
|
74
|
+
|
|
75
|
+
// Reset restart counter on successful start
|
|
76
|
+
restartAttempts = 0;
|
|
77
|
+
|
|
78
|
+
tunnelUrl = url;
|
|
79
|
+
tunnelStartedAt = Date.now();
|
|
80
|
+
await writeEnvKey("TWILIO_WEBHOOK_URL", url);
|
|
81
|
+
console.log(`Tunnel URL: ${url}`);
|
|
82
|
+
return url;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Stop the tunnel and clear state. Prevents auto-restart.
|
|
87
|
+
*/
|
|
88
|
+
export function stopTunnel(): void {
|
|
89
|
+
manuallyStopped = true;
|
|
90
|
+
if (activeTunnel) {
|
|
91
|
+
activeTunnel.stop();
|
|
92
|
+
}
|
|
93
|
+
clearTunnelState();
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Return the current public tunnel URL, or null if not running.
|
|
98
|
+
*
|
|
99
|
+
* @returns The public HTTPS URL or null
|
|
100
|
+
*/
|
|
101
|
+
export function getTunnelUrl(): string | null {
|
|
102
|
+
return tunnelUrl;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Return the timestamp when the tunnel URL was obtained, or null.
|
|
107
|
+
*
|
|
108
|
+
* @returns Unix ms timestamp or null
|
|
109
|
+
*/
|
|
110
|
+
export function getTunnelStartedAt(): number | null {
|
|
111
|
+
return tunnelStartedAt;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Check whether the tunnel process is currently alive.
|
|
116
|
+
*
|
|
117
|
+
* @returns True if tunnel is running
|
|
118
|
+
*/
|
|
119
|
+
export function isTunnelRunning(): boolean {
|
|
120
|
+
return activeTunnel !== null;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// ============================================================================
|
|
124
|
+
// HELPER FUNCTIONS
|
|
125
|
+
// ============================================================================
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Create a new tunnel instance, wait for its URL, and attach event handlers.
|
|
129
|
+
*
|
|
130
|
+
* @param port - Local port to tunnel
|
|
131
|
+
* @returns The public HTTPS URL
|
|
132
|
+
*/
|
|
133
|
+
async function createTunnel(port: number): Promise<string> {
|
|
53
134
|
const tunnel = Tunnel.quick(`http://localhost:${port}`);
|
|
54
135
|
activeTunnel = tunnel;
|
|
55
136
|
|
|
@@ -61,15 +142,24 @@ export async function startTunnel(port: number): Promise<string> {
|
|
|
61
142
|
console.log(`[cloudflared] ${data.trim()}`);
|
|
62
143
|
});
|
|
63
144
|
|
|
145
|
+
// Log connection-level events for observability
|
|
146
|
+
tunnel.on("connected", (conn: { id: string; ip: string; location: string }) => {
|
|
147
|
+
console.log(`[cloudflared] Connected: ${conn.location} (${conn.ip})`);
|
|
148
|
+
});
|
|
149
|
+
tunnel.on("disconnected", (conn: { id: string; ip: string; location: string }) => {
|
|
150
|
+
console.log(`[cloudflared] Disconnected: ${conn.location} (${conn.ip})`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// Wait for the tunnel URL or fail
|
|
64
154
|
const url = await new Promise<string>((resolve, reject) => {
|
|
65
155
|
const timeout = setTimeout(() => {
|
|
66
156
|
activeTunnel = null;
|
|
67
|
-
reject(new Error("Timed out waiting for tunnel URL (
|
|
157
|
+
reject(new Error("Timed out waiting for tunnel URL (15s)"));
|
|
68
158
|
}, TUNNEL_URL_TIMEOUT_MS);
|
|
69
159
|
|
|
70
|
-
tunnel.once("url", (
|
|
160
|
+
tunnel.once("url", (emittedUrl: string) => {
|
|
71
161
|
clearTimeout(timeout);
|
|
72
|
-
resolve(
|
|
162
|
+
resolve(emittedUrl);
|
|
73
163
|
});
|
|
74
164
|
|
|
75
165
|
tunnel.once("error", (err: Error) => {
|
|
@@ -85,54 +175,64 @@ export async function startTunnel(port: number): Promise<string> {
|
|
|
85
175
|
});
|
|
86
176
|
});
|
|
87
177
|
|
|
178
|
+
// After successful start, handle unexpected exit with auto-restart
|
|
88
179
|
tunnel.on("exit", () => {
|
|
89
|
-
console.log("cloudflared exited");
|
|
90
|
-
|
|
91
|
-
|
|
180
|
+
console.log("cloudflared exited unexpectedly");
|
|
181
|
+
clearTunnelState();
|
|
182
|
+
scheduleRestart();
|
|
92
183
|
});
|
|
93
184
|
|
|
94
|
-
tunnelUrl = url;
|
|
95
|
-
tunnelStartedAt = Date.now();
|
|
96
|
-
await writeEnvKey("TWILIO_WEBHOOK_URL", url);
|
|
97
|
-
console.log(`Tunnel URL: ${url}`);
|
|
98
185
|
return url;
|
|
99
186
|
}
|
|
100
187
|
|
|
101
188
|
/**
|
|
102
|
-
*
|
|
189
|
+
* Clear all tunnel state variables.
|
|
103
190
|
*/
|
|
104
|
-
|
|
105
|
-
if (activeTunnel) {
|
|
106
|
-
activeTunnel.stop();
|
|
107
|
-
}
|
|
191
|
+
function clearTunnelState(): void {
|
|
108
192
|
activeTunnel = null;
|
|
109
193
|
tunnelUrl = null;
|
|
110
194
|
tunnelStartedAt = null;
|
|
111
195
|
}
|
|
112
196
|
|
|
113
197
|
/**
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* @returns The public HTTPS URL or null
|
|
198
|
+
* Schedule an automatic restart if the tunnel crashed unexpectedly.
|
|
199
|
+
* Skips restart if the tunnel was manually stopped or max attempts exceeded.
|
|
117
200
|
*/
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
201
|
+
function scheduleRestart(): void {
|
|
202
|
+
if (manuallyStopped) {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
121
205
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
*/
|
|
127
|
-
export function getTunnelStartedAt(): number | null {
|
|
128
|
-
return tunnelStartedAt;
|
|
129
|
-
}
|
|
206
|
+
if (tunnelPort === null) {
|
|
207
|
+
console.error("[tunnel] Cannot restart: no port configured");
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
130
210
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
211
|
+
restartAttempts++;
|
|
212
|
+
|
|
213
|
+
if (restartAttempts > MAX_RESTART_ATTEMPTS) {
|
|
214
|
+
console.error(`[tunnel] Giving up after ${MAX_RESTART_ATTEMPTS} restart attempts`);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const port = tunnelPort;
|
|
219
|
+
console.log(`[tunnel] Restarting in ${RESTART_DELAY_MS / 1000}s (attempt ${restartAttempts}/${MAX_RESTART_ATTEMPTS})...`);
|
|
220
|
+
|
|
221
|
+
setTimeout(async () => {
|
|
222
|
+
if (manuallyStopped || activeTunnel) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
const url = await createTunnel(port);
|
|
228
|
+
restartAttempts = 0;
|
|
229
|
+
tunnelUrl = url;
|
|
230
|
+
tunnelStartedAt = Date.now();
|
|
231
|
+
await writeEnvKey("TWILIO_WEBHOOK_URL", url);
|
|
232
|
+
console.log(`[tunnel] Restarted successfully: ${url}`);
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error(`[tunnel] Restart failed: ${err}`);
|
|
235
|
+
scheduleRestart();
|
|
236
|
+
}
|
|
237
|
+
}, RESTART_DELAY_MS);
|
|
138
238
|
}
|