@nmakarov/cli-toolkit 0.79.0 → 0.80.0

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.
@@ -2670,13 +2670,14 @@ var Logger = class _Logger {
2670
2670
  */
2671
2671
  progress(message, opts) {
2672
2672
  const { prefix, count, total } = opts;
2673
- const paddedTotal = String(total).length;
2673
+ const displayTotal = Math.max(Number(total) || 0, Number(count) || 0);
2674
+ const paddedTotal = String(displayTotal).length;
2674
2675
  const paddedCount = String(count).padStart(paddedTotal, " ");
2675
2676
  const payload = {
2676
2677
  level: "progress",
2677
2678
  message,
2678
2679
  count: paddedCount,
2679
- total,
2680
+ total: displayTotal,
2680
2681
  prefix
2681
2682
  };
2682
2683
  const key = prefix ?? "";
@@ -2693,7 +2694,7 @@ var Logger = class _Logger {
2693
2694
  if (wantTimes) {
2694
2695
  let remaining = -1;
2695
2696
  if (itemsPerSec > 0) {
2696
- remaining = (total - count) / itemsPerSec;
2697
+ remaining = Math.max(0, (displayTotal - count) / itemsPerSec);
2697
2698
  }
2698
2699
  payload.elapsed = this.round(elapsedSeconds, 2);
2699
2700
  payload.remaining = remaining >= 0 ? this.round(remaining, 2) : remaining;
@@ -2702,12 +2703,12 @@ var Logger = class _Logger {
2702
2703
  payload.rate = itemsPerSec >= 0 ? this.round(itemsPerSec, 2) : itemsPerSec;
2703
2704
  }
2704
2705
  }
2705
- if (count >= total) {
2706
+ if (count === total) {
2706
2707
  delete this.startTimes[key];
2707
2708
  delete this.startCounts[key];
2708
2709
  delete this.lastProgressTimes[key];
2709
2710
  }
2710
- if (this.shouldOutputProgress(prefix ?? "", count, total)) {
2711
+ if (this.shouldOutputProgress(prefix ?? "", count, displayTotal)) {
2711
2712
  this.out(payload);
2712
2713
  if (this.options.progressThrottle && prefix) {
2713
2714
  this.lastProgressTimes[prefix] = Date.now();
@@ -2866,6 +2867,8 @@ function setup(opts = {}) {
2866
2867
  // a quick "show me the figured params and quit" that skips the flow's
2867
2868
  // actual work. Like --stopAfter=init, this is a hard exit(0) (registered
2868
2869
  // cleanups are skipped); call it once components/params are resolved.
2870
+ _requestExitCode: null,
2871
+ requestExit: null,
2869
2872
  showUsedParamsIfNeeded: () => {
2870
2873
  const mode = params.getShowUsedParamsMode?.();
2871
2874
  if (mode !== "top" && mode !== "stop") return;
@@ -2876,6 +2879,9 @@ function setup(opts = {}) {
2876
2879
  }
2877
2880
  }
2878
2881
  };
2882
+ context.requestExit = (code = 0) => {
2883
+ context._requestExitCode = code;
2884
+ };
2879
2885
  logger.debug("[setup] completed successfully");
2880
2886
  return context;
2881
2887
  }
@@ -2911,9 +2917,25 @@ async function init(flow2, opts = {}) {
2911
2917
  if (cleanupRan) return;
2912
2918
  cleanupRan = true;
2913
2919
  const fns = [...ctx.cleanupFunctions].reverse();
2920
+ const budgetMs = 5e3;
2921
+ const started = Date.now();
2914
2922
  for (const fn of fns) {
2923
+ const left = budgetMs - (Date.now() - started);
2924
+ if (left <= 0) {
2925
+ ctx.logger.warn("[cleanup] budget exhausted \u2014 skipping remaining cleanup");
2926
+ break;
2927
+ }
2915
2928
  try {
2916
- await fn(ctx);
2929
+ await Promise.race([
2930
+ Promise.resolve(fn(ctx)),
2931
+ new Promise((_, reject) => {
2932
+ const t = setTimeout(
2933
+ () => reject(new Error(`cleanup timed out after ${left}ms`)),
2934
+ left
2935
+ );
2936
+ t.unref?.();
2937
+ })
2938
+ ]);
2917
2939
  } catch (error) {
2918
2940
  ctx.logger.warn("[cleanup] error in cleanup function:", error);
2919
2941
  }
@@ -3009,6 +3031,8 @@ async function init(flow2, opts = {}) {
3009
3031
  process.exit(process.exitCode);
3010
3032
  } else if (stop || kill) {
3011
3033
  process.exit(0);
3034
+ } else if (context._requestExitCode != null) {
3035
+ process.exit(context._requestExitCode);
3012
3036
  }
3013
3037
  }
3014
3038
  }
@@ -3132,7 +3156,7 @@ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
3132
3156
  "57P02",
3133
3157
  "57P03"
3134
3158
  ]);
3135
- var CONNECTION_ERROR_MESSAGE_RE = /connection (terminated|ended|closed|destroyed|reset|refused|not open)|Connection terminated unexpectedly|Client has encountered a connection error|server closed the connection|Cannot use a pool after calling end|This socket has been ended|connect ECONNRESET/i;
3159
+ var CONNECTION_ERROR_MESSAGE_RE = /connection (terminated|ended|closed|destroyed|reset|refused|not open)|Connection (terminated|ended) unexpectedly|Client has encountered a connection error|server closed the connection|Cannot use a pool after calling end|This socket has been ended|connect ECONNRESET/i;
3136
3160
  var Db = class _Db {
3137
3161
  static async init(context, options = {}) {
3138
3162
  const buildConfig = async () => {
@@ -3400,6 +3424,10 @@ var Db = class _Db {
3400
3424
  this.isConnected = false;
3401
3425
  this.queriesLog = [];
3402
3426
  this._reconnectPromise = null;
3427
+ this._closed = false;
3428
+ this._liveKnex = /* @__PURE__ */ new Set();
3429
+ this._reconnectCooldownUntil = 0;
3430
+ this._reconnectAcquireTimeoutMs = null;
3403
3431
  this.config = {
3404
3432
  testConnection: true,
3405
3433
  profile: false,
@@ -3484,6 +3512,9 @@ var Db = class _Db {
3484
3512
  return null;
3485
3513
  }
3486
3514
  async connect() {
3515
+ if (this._closed) {
3516
+ throw new ParamError("Db: Connection closed");
3517
+ }
3487
3518
  if (this.isConnected && this.knexInstance) {
3488
3519
  this.logger.warn?.("[Db] Already connected");
3489
3520
  return;
@@ -3499,22 +3530,46 @@ var Db = class _Db {
3499
3530
  connectionString: this.config.connectionString,
3500
3531
  family: 4
3501
3532
  };
3533
+ const acquireTimeout = this._reconnectAcquireTimeoutMs ?? this.config.acquireConnectionTimeout;
3502
3534
  this.knexInstance = (0, import_knex.default)({
3503
3535
  client,
3504
3536
  connection: connectionConfig,
3505
3537
  pool: this.config.pool,
3506
- acquireConnectionTimeout: this.config.acquireConnectionTimeout,
3538
+ acquireConnectionTimeout: acquireTimeout,
3507
3539
  ...this.config.ssl && { ssl: this.config.ssl }
3508
3540
  });
3541
+ this._liveKnex.add(this.knexInstance);
3542
+ this.knexInstance.on?.("error", (err) => {
3543
+ if (this._closed) return;
3544
+ this.logger.warn?.(
3545
+ `[Db] Connection error (${this.getErrorMessage(err)})`
3546
+ );
3547
+ });
3548
+ if (this._closed) {
3549
+ await this._destroyKnex(this.knexInstance, "connect aborted (closed)");
3550
+ this.knexInstance = null;
3551
+ throw new ParamError("Db: Connection closed");
3552
+ }
3509
3553
  if (this.config.profile) {
3510
3554
  this.attachProfiler();
3511
3555
  }
3512
3556
  if (this.config.testConnection) {
3513
3557
  await this.testConnection();
3514
3558
  }
3559
+ if (this._closed) {
3560
+ await this._destroyKnex(this.knexInstance, "connect aborted (closed)");
3561
+ this.knexInstance = null;
3562
+ throw new ParamError("Db: Connection closed");
3563
+ }
3515
3564
  this.isConnected = true;
3516
3565
  this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
3517
3566
  } catch (error) {
3567
+ const failed = this.knexInstance;
3568
+ this.knexInstance = null;
3569
+ this.isConnected = false;
3570
+ if (failed) {
3571
+ await this._destroyKnex(failed, "connect failed");
3572
+ }
3518
3573
  if (error instanceof ParamError) {
3519
3574
  throw error;
3520
3575
  }
@@ -3522,20 +3577,42 @@ var Db = class _Db {
3522
3577
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
3523
3578
  }
3524
3579
  }
3525
- async disconnect() {
3526
- if (!this.knexInstance) {
3527
- return;
3528
- }
3580
+ /**
3581
+ * Destroy a knex pool without hanging exit on stuck TCP sockets (ETIMEDOUT).
3582
+ * @param {import("knex").Knex | null | undefined} knexInst
3583
+ * @param {string} [reason]
3584
+ * @param {number} [timeoutMs]
3585
+ */
3586
+ async _destroyKnex(knexInst, reason = "destroy", timeoutMs = 3e3) {
3587
+ if (!knexInst || typeof knexInst.destroy !== "function") return;
3588
+ this._liveKnex.delete(knexInst);
3529
3589
  try {
3530
- await this.knexInstance.destroy();
3531
- this.knexInstance = null;
3532
- this.isConnected = false;
3533
- this.queriesLog = [];
3534
- this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
3590
+ await Promise.race([
3591
+ knexInst.destroy(),
3592
+ new Promise((_, reject) => {
3593
+ const t = setTimeout(
3594
+ () => reject(new Error(`Db: ${reason} timed out after ${timeoutMs}ms`)),
3595
+ timeoutMs
3596
+ );
3597
+ t.unref?.();
3598
+ })
3599
+ ]);
3535
3600
  } catch (error) {
3536
- const errorMsg = this.getErrorMessage(error);
3537
- this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
3538
- throw error;
3601
+ this.logger.debug?.(
3602
+ `[Db] ${reason}: ${this.getErrorMessage(error)}`
3603
+ );
3604
+ }
3605
+ }
3606
+ async disconnect() {
3607
+ this._closed = true;
3608
+ this.isConnected = false;
3609
+ this.knexInstance = null;
3610
+ this.queriesLog = [];
3611
+ const all = [...this._liveKnex];
3612
+ this._liveKnex.clear();
3613
+ await Promise.all(all.map((inst) => this._destroyKnex(inst, "disconnect")));
3614
+ if (all.length > 0) {
3615
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
3539
3616
  }
3540
3617
  }
3541
3618
  /**
@@ -3558,27 +3635,39 @@ var Db = class _Db {
3558
3635
  }
3559
3636
  /**
3560
3637
  * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
3638
+ * No-ops once disconnect() has closed the handle.
3561
3639
  */
3562
3640
  async reconnect() {
3641
+ if (this._closed) {
3642
+ return;
3643
+ }
3644
+ if (this._reconnectCooldownUntil && Date.now() < this._reconnectCooldownUntil) {
3645
+ return;
3646
+ }
3563
3647
  if (this._reconnectPromise) {
3564
3648
  await this._reconnectPromise;
3565
3649
  return;
3566
3650
  }
3567
3651
  this._reconnectPromise = (async () => {
3652
+ if (this._closed) return;
3568
3653
  const old = this.knexInstance;
3569
3654
  this.isConnected = false;
3570
3655
  this.knexInstance = null;
3571
3656
  this.queriesLog = [];
3572
3657
  if (old) {
3573
- try {
3574
- await old.destroy();
3575
- } catch (error) {
3576
- this.logger.debug?.(
3577
- `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
3578
- );
3579
- }
3658
+ await this._destroyKnex(old, "destroy during reconnect");
3659
+ }
3660
+ if (this._closed) return;
3661
+ this._reconnectAcquireTimeoutMs = 3e3;
3662
+ try {
3663
+ await this.connect();
3664
+ this._reconnectCooldownUntil = 0;
3665
+ } catch (error) {
3666
+ this._reconnectCooldownUntil = Date.now() + 5e3;
3667
+ throw error;
3668
+ } finally {
3669
+ this._reconnectAcquireTimeoutMs = null;
3580
3670
  }
3581
- await this.connect();
3582
3671
  })();
3583
3672
  try {
3584
3673
  await this._reconnectPromise;
@@ -3587,10 +3676,19 @@ var Db = class _Db {
3587
3676
  }
3588
3677
  }
3589
3678
  async reconnectAfterConnectionError(error) {
3679
+ if (this._closed) {
3680
+ return;
3681
+ }
3682
+ if (this._reconnectCooldownUntil && Date.now() < this._reconnectCooldownUntil) {
3683
+ return;
3684
+ }
3590
3685
  this.logger.warn?.(
3591
3686
  `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
3592
3687
  );
3593
- await this.reconnect();
3688
+ try {
3689
+ await this.reconnect();
3690
+ } catch {
3691
+ }
3594
3692
  }
3595
3693
  /**
3596
3694
  * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
@@ -3603,10 +3701,13 @@ var Db = class _Db {
3603
3701
  return await fn();
3604
3702
  } catch (error) {
3605
3703
  lastError = error;
3606
- if (!this.isConnectionError(error) || attempt >= retries) {
3704
+ if (this._closed || !this.isConnectionError(error) || attempt >= retries) {
3607
3705
  throw error;
3608
3706
  }
3609
3707
  await this.reconnectAfterConnectionError(error);
3708
+ if (this._closed) {
3709
+ throw error;
3710
+ }
3610
3711
  }
3611
3712
  }
3612
3713
  throw lastError;
@@ -3630,10 +3731,13 @@ var Db = class _Db {
3630
3731
  try {
3631
3732
  return await protoThen.call(builder);
3632
3733
  } catch (error) {
3633
- if (!inst.isConnectionError(error)) {
3734
+ if (inst._closed || !inst.isConnectionError(error)) {
3634
3735
  throw error;
3635
3736
  }
3636
3737
  await inst.reconnectAfterConnectionError(error);
3738
+ if (inst._closed || !inst.knexInstance) {
3739
+ throw error;
3740
+ }
3637
3741
  if (typeof builder.clone === "function") {
3638
3742
  const retry = builder.clone();
3639
3743
  retry.client = inst.knexInstance.client;