@shipers-dev/multi 0.10.0 → 0.10.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 (3) hide show
  1. package/dist/index.js +28 -20
  2. package/package.json +1 -1
  3. package/src/index.ts +28 -18
package/dist/index.js CHANGED
@@ -5703,7 +5703,7 @@ import { join as join3, dirname as dirname2 } from "path";
5703
5703
  // package.json
5704
5704
  var package_default = {
5705
5705
  name: "@shipers-dev/multi",
5706
- version: "0.10.0",
5706
+ version: "0.10.1",
5707
5707
  type: "module",
5708
5708
  bin: {
5709
5709
  "multi-agent": "./dist/index.js"
@@ -6194,30 +6194,38 @@ async function cmdConnect(apiUrl, config) {
6194
6194
  process.on("SIGINT", () => shutdown("SIGINT"));
6195
6195
  process.on("SIGTERM", () => shutdown("SIGTERM"));
6196
6196
  schedule();
6197
+ let restarting = false;
6197
6198
  const restartTunnel = async (reason) => {
6198
- if (!alive)
6199
+ if (!alive || restarting)
6199
6200
  return;
6200
- log(`\uD83D\uDD01 Restarting tunnel (${reason})`);
6201
+ restarting = true;
6201
6202
  try {
6202
- tunnel?.child.kill();
6203
- } catch {}
6204
- for (let attempt = 1;alive; attempt++) {
6205
- const next = await startTunnel(port);
6206
- if (next) {
6207
- tunnel = next;
6208
- log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
6209
- try {
6210
- const pending = await heartbeat();
6211
- if (pending > 0)
6212
- drainOfflineDispatches(apiUrl, config.deviceId, config.dispatchSecret, db, () => schedule());
6213
- } catch (e) {
6214
- log(`heartbeat error after tunnel restart: ${String(e)}`);
6203
+ log(`\uD83D\uDD01 Restarting tunnel (${reason})`);
6204
+ const old = tunnel;
6205
+ tunnel = null;
6206
+ try {
6207
+ old?.child.kill();
6208
+ } catch {}
6209
+ for (let attempt = 1;alive; attempt++) {
6210
+ const next = await startTunnel(port);
6211
+ if (next) {
6212
+ tunnel = next;
6213
+ log(`\u2601\uFE0F Tunnel up: ${tunnel.url}`);
6214
+ try {
6215
+ const pending = await heartbeat();
6216
+ if (pending > 0)
6217
+ drainOfflineDispatches(apiUrl, config.deviceId, config.dispatchSecret, db, () => schedule());
6218
+ } catch (e) {
6219
+ log(`heartbeat error after tunnel restart: ${String(e)}`);
6220
+ }
6221
+ return;
6215
6222
  }
6216
- return;
6223
+ const wait = Math.min(30000, 2000 * attempt);
6224
+ log(`tunnel restart failed, retry in ${wait}ms`);
6225
+ await sleep(wait);
6217
6226
  }
6218
- const wait = Math.min(30000, 2000 * attempt);
6219
- log(`tunnel restart failed, retry in ${wait}ms`);
6220
- await sleep(wait);
6227
+ } finally {
6228
+ restarting = false;
6221
6229
  }
6222
6230
  };
6223
6231
  (async () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shipers-dev/multi",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "multi-agent": "./dist/index.js"
package/src/index.ts CHANGED
@@ -481,27 +481,36 @@ async function cmdConnect(apiUrl: string, config: Config) {
481
481
  schedule();
482
482
 
483
483
  // Tunnel self-heal: relaunch cloudflared if child exits or DNS stops resolving.
484
+ // `restarting` guards against two entries racing (probe-failure + exit-watcher
485
+ // both firing when we kill the child as part of our own restart).
486
+ let restarting = false;
484
487
  const restartTunnel = async (reason: string) => {
485
- if (!alive) return;
486
- log(`🔁 Restarting tunnel (${reason})`);
487
- try { tunnel?.child.kill(); } catch {}
488
- // Small backoff so we don't spam cloudflared edge under prolonged outage.
489
- for (let attempt = 1; alive; attempt++) {
490
- const next = await startTunnel(port);
491
- if (next) {
492
- tunnel = next;
493
- log(`☁️ Tunnel up: ${tunnel.url}`);
494
- try {
495
- const pending = await heartbeat();
496
- if (pending > 0) void drainOfflineDispatches(apiUrl, config.deviceId!, config.dispatchSecret!, db, () => schedule());
497
- } catch (e) {
498
- log(`heartbeat error after tunnel restart: ${String(e)}`);
488
+ if (!alive || restarting) return;
489
+ restarting = true;
490
+ try {
491
+ log(`🔁 Restarting tunnel (${reason})`);
492
+ const old = tunnel;
493
+ tunnel = null; // null first so the exit-watcher's `tunnel === t` check skips our own kill
494
+ try { old?.child.kill(); } catch {}
495
+ for (let attempt = 1; alive; attempt++) {
496
+ const next = await startTunnel(port);
497
+ if (next) {
498
+ tunnel = next;
499
+ log(`☁️ Tunnel up: ${tunnel.url}`);
500
+ try {
501
+ const pending = await heartbeat();
502
+ if (pending > 0) void drainOfflineDispatches(apiUrl, config.deviceId!, config.dispatchSecret!, db, () => schedule());
503
+ } catch (e) {
504
+ log(`heartbeat error after tunnel restart: ${String(e)}`);
505
+ }
506
+ return;
499
507
  }
500
- return;
508
+ const wait = Math.min(30000, 2000 * attempt);
509
+ log(`tunnel restart failed, retry in ${wait}ms`);
510
+ await sleep(wait);
501
511
  }
502
- const wait = Math.min(30000, 2000 * attempt);
503
- log(`tunnel restart failed, retry in ${wait}ms`);
504
- await sleep(wait);
512
+ } finally {
513
+ restarting = false;
505
514
  }
506
515
  };
507
516
 
@@ -512,6 +521,7 @@ async function cmdConnect(apiUrl: string, config: Config) {
512
521
  if (!t) { await sleep(1000); continue; }
513
522
  const code = await t.child.exited;
514
523
  if (!alive) return;
524
+ // If `tunnel` was nulled by restartTunnel, we killed it ourselves — skip.
515
525
  if (tunnel === t) await restartTunnel(`cloudflared exited code=${code}`);
516
526
  }
517
527
  })();