reqly-cli 0.3.0 → 0.3.1

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.
Files changed (2) hide show
  1. package/lib/tunnel.mjs +60 -10
  2. package/package.json +1 -1
package/lib/tunnel.mjs CHANGED
@@ -182,8 +182,14 @@ async function registerTunnel(serverUrl, token, name, port, binSlug, passthrough
182
182
  /**
183
183
  * Poll for new captures and forward them.
184
184
  * More reliable than SSE through reverse proxies (Traefik, nginx).
185
+ *
186
+ * The server's tunnel registry is in-memory with a short TTL, so after the
187
+ * laptop sleeps (poll loop frozen) or the server restarts, the tunnelId is
188
+ * forgotten and poll returns 404/401. When that happens we re-register via
189
+ * `reregister()` to get a fresh tunnelId and resume — so the tunnel
190
+ * self-heals instead of looping forever on a dead id.
185
191
  */
186
- async function pollForCaptures(serverUrl, token, tunnelId, localOrigin, pollIntervalMs = 1000) {
192
+ async function pollForCaptures({ serverUrl, token, tunnelId, localOrigin, pollIntervalMs = 1000, reregister }) {
187
193
  let aborted = false;
188
194
  let lastSeen = new Date().toISOString();
189
195
  let consecutiveErrors = 0;
@@ -209,6 +215,22 @@ async function pollForCaptures(serverUrl, token, tunnelId, localOrigin, pollInte
209
215
  headers: { Authorization: `Bearer ${token}` },
210
216
  });
211
217
 
218
+ // 404 = server forgot this tunnel (sleep/restart); 401 = token re-auth
219
+ // needed. Either way, re-register to get a fresh tunnelId and resume.
220
+ if (res.status === 404 || res.status === 401) {
221
+ console.log(
222
+ `${DIM}[${timestamp()}]${RESET} ${YELLOW}Tunnel dropped (${res.status}) — reconnecting...${RESET}`
223
+ );
224
+ const fresh = await reregister();
225
+ tunnelId = fresh.tunnelId;
226
+ consecutiveErrors = 0;
227
+ console.log(
228
+ `${DIM}[${timestamp()}]${RESET} ${GREEN}Reconnected.${RESET} ${DIM}Listening for requests...${RESET}`
229
+ );
230
+ await new Promise((r) => setTimeout(r, pollIntervalMs));
231
+ continue;
232
+ }
233
+
212
234
  if (!res.ok) {
213
235
  throw new Error(`Poll failed (${res.status})`);
214
236
  }
@@ -291,18 +313,45 @@ export async function startTunnel({ port, bin, name, url, token: tokenOverride,
291
313
  }
292
314
  const localOrigin = `http://localhost:${port}`;
293
315
 
294
- // 1. Register the tunnel
295
- let registration;
296
- try {
297
- process.stdout.write(`${DIM}Registering tunnel...${RESET}`);
298
- registration = await registerTunnel(
316
+ // Re-register against the SAME bin slug each time so the public URL stays
317
+ // stable across reconnects. On the first call `bin` may be undefined (the
318
+ // server auto-creates a bin); afterwards we reuse the slug it returned.
319
+ let knownBinSlug = bin;
320
+ async function register() {
321
+ const reg = await registerTunnel(
299
322
  serverUrl,
300
323
  token,
301
324
  name || `cli-${port}`,
302
325
  port,
303
- bin,
326
+ knownBinSlug,
304
327
  passthrough
305
328
  );
329
+ knownBinSlug = reg.binSlug;
330
+ return reg;
331
+ }
332
+
333
+ // Resilient re-register: keep retrying with backoff until the server is
334
+ // reachable again (e.g. it's still booting after a restart).
335
+ async function reregister() {
336
+ let delay = 1000;
337
+ for (;;) {
338
+ try {
339
+ return await register();
340
+ } catch (err) {
341
+ console.log(
342
+ `${DIM}[${timestamp()}]${RESET} ${YELLOW}Reconnect failed: ${err.message} — retrying in ${Math.round(delay / 1000)}s${RESET}`
343
+ );
344
+ await new Promise((r) => setTimeout(r, delay));
345
+ delay = Math.min(delay * 2, 30_000);
346
+ }
347
+ }
348
+ }
349
+
350
+ // 1. Register the tunnel
351
+ let registration;
352
+ try {
353
+ process.stdout.write(`${DIM}Registering tunnel...${RESET}`);
354
+ registration = await register();
306
355
  console.log(` ${GREEN}done${RESET}`);
307
356
  } catch (err) {
308
357
  console.log(` ${RED}failed${RESET}`);
@@ -323,11 +372,12 @@ export async function startTunnel({ port, bin, name, url, token: tokenOverride,
323
372
  }
324
373
 
325
374
  // 3. Start forwarding. Passthrough holds the caller open, so poll faster.
326
- await pollForCaptures(
375
+ await pollForCaptures({
327
376
  serverUrl,
328
377
  token,
329
378
  tunnelId,
330
379
  localOrigin,
331
- passthrough ? 250 : 1000
332
- );
380
+ pollIntervalMs: passthrough ? 250 : 1000,
381
+ reregister,
382
+ });
333
383
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "reqly-cli",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
4
4
  "description": "Reqly CLI — tunnel webhooks to your local dev server",
5
5
  "type": "module",
6
6
  "bin": {