@nmakarov/cli-toolkit 0.78.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.
package/dist/index.cjs CHANGED
@@ -3972,7 +3972,7 @@ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
3972
3972
  "57P02",
3973
3973
  "57P03"
3974
3974
  ]);
3975
- 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;
3975
+ 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;
3976
3976
  var Db = class _Db {
3977
3977
  static async init(context, options = {}) {
3978
3978
  const buildConfig = async () => {
@@ -4240,6 +4240,10 @@ var Db = class _Db {
4240
4240
  this.isConnected = false;
4241
4241
  this.queriesLog = [];
4242
4242
  this._reconnectPromise = null;
4243
+ this._closed = false;
4244
+ this._liveKnex = /* @__PURE__ */ new Set();
4245
+ this._reconnectCooldownUntil = 0;
4246
+ this._reconnectAcquireTimeoutMs = null;
4243
4247
  this.config = {
4244
4248
  testConnection: true,
4245
4249
  profile: false,
@@ -4324,6 +4328,9 @@ var Db = class _Db {
4324
4328
  return null;
4325
4329
  }
4326
4330
  async connect() {
4331
+ if (this._closed) {
4332
+ throw new ParamError("Db: Connection closed");
4333
+ }
4327
4334
  if (this.isConnected && this.knexInstance) {
4328
4335
  this.logger.warn?.("[Db] Already connected");
4329
4336
  return;
@@ -4339,22 +4346,46 @@ var Db = class _Db {
4339
4346
  connectionString: this.config.connectionString,
4340
4347
  family: 4
4341
4348
  };
4349
+ const acquireTimeout = this._reconnectAcquireTimeoutMs ?? this.config.acquireConnectionTimeout;
4342
4350
  this.knexInstance = (0, import_knex.default)({
4343
4351
  client,
4344
4352
  connection: connectionConfig,
4345
4353
  pool: this.config.pool,
4346
- acquireConnectionTimeout: this.config.acquireConnectionTimeout,
4354
+ acquireConnectionTimeout: acquireTimeout,
4347
4355
  ...this.config.ssl && { ssl: this.config.ssl }
4348
4356
  });
4357
+ this._liveKnex.add(this.knexInstance);
4358
+ this.knexInstance.on?.("error", (err) => {
4359
+ if (this._closed) return;
4360
+ this.logger.warn?.(
4361
+ `[Db] Connection error (${this.getErrorMessage(err)})`
4362
+ );
4363
+ });
4364
+ if (this._closed) {
4365
+ await this._destroyKnex(this.knexInstance, "connect aborted (closed)");
4366
+ this.knexInstance = null;
4367
+ throw new ParamError("Db: Connection closed");
4368
+ }
4349
4369
  if (this.config.profile) {
4350
4370
  this.attachProfiler();
4351
4371
  }
4352
4372
  if (this.config.testConnection) {
4353
4373
  await this.testConnection();
4354
4374
  }
4375
+ if (this._closed) {
4376
+ await this._destroyKnex(this.knexInstance, "connect aborted (closed)");
4377
+ this.knexInstance = null;
4378
+ throw new ParamError("Db: Connection closed");
4379
+ }
4355
4380
  this.isConnected = true;
4356
4381
  this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
4357
4382
  } catch (error) {
4383
+ const failed = this.knexInstance;
4384
+ this.knexInstance = null;
4385
+ this.isConnected = false;
4386
+ if (failed) {
4387
+ await this._destroyKnex(failed, "connect failed");
4388
+ }
4358
4389
  if (error instanceof ParamError) {
4359
4390
  throw error;
4360
4391
  }
@@ -4362,20 +4393,42 @@ var Db = class _Db {
4362
4393
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
4363
4394
  }
4364
4395
  }
4365
- async disconnect() {
4366
- if (!this.knexInstance) {
4367
- return;
4368
- }
4396
+ /**
4397
+ * Destroy a knex pool without hanging exit on stuck TCP sockets (ETIMEDOUT).
4398
+ * @param {import("knex").Knex | null | undefined} knexInst
4399
+ * @param {string} [reason]
4400
+ * @param {number} [timeoutMs]
4401
+ */
4402
+ async _destroyKnex(knexInst, reason = "destroy", timeoutMs = 3e3) {
4403
+ if (!knexInst || typeof knexInst.destroy !== "function") return;
4404
+ this._liveKnex.delete(knexInst);
4369
4405
  try {
4370
- await this.knexInstance.destroy();
4371
- this.knexInstance = null;
4372
- this.isConnected = false;
4373
- this.queriesLog = [];
4374
- this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
4406
+ await Promise.race([
4407
+ knexInst.destroy(),
4408
+ new Promise((_, reject) => {
4409
+ const t = setTimeout(
4410
+ () => reject(new Error(`Db: ${reason} timed out after ${timeoutMs}ms`)),
4411
+ timeoutMs
4412
+ );
4413
+ t.unref?.();
4414
+ })
4415
+ ]);
4375
4416
  } catch (error) {
4376
- const errorMsg = this.getErrorMessage(error);
4377
- this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
4378
- throw error;
4417
+ this.logger.debug?.(
4418
+ `[Db] ${reason}: ${this.getErrorMessage(error)}`
4419
+ );
4420
+ }
4421
+ }
4422
+ async disconnect() {
4423
+ this._closed = true;
4424
+ this.isConnected = false;
4425
+ this.knexInstance = null;
4426
+ this.queriesLog = [];
4427
+ const all = [...this._liveKnex];
4428
+ this._liveKnex.clear();
4429
+ await Promise.all(all.map((inst) => this._destroyKnex(inst, "disconnect")));
4430
+ if (all.length > 0) {
4431
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
4379
4432
  }
4380
4433
  }
4381
4434
  /**
@@ -4398,27 +4451,39 @@ var Db = class _Db {
4398
4451
  }
4399
4452
  /**
4400
4453
  * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
4454
+ * No-ops once disconnect() has closed the handle.
4401
4455
  */
4402
4456
  async reconnect() {
4457
+ if (this._closed) {
4458
+ return;
4459
+ }
4460
+ if (this._reconnectCooldownUntil && Date.now() < this._reconnectCooldownUntil) {
4461
+ return;
4462
+ }
4403
4463
  if (this._reconnectPromise) {
4404
4464
  await this._reconnectPromise;
4405
4465
  return;
4406
4466
  }
4407
4467
  this._reconnectPromise = (async () => {
4468
+ if (this._closed) return;
4408
4469
  const old = this.knexInstance;
4409
4470
  this.isConnected = false;
4410
4471
  this.knexInstance = null;
4411
4472
  this.queriesLog = [];
4412
4473
  if (old) {
4413
- try {
4414
- await old.destroy();
4415
- } catch (error) {
4416
- this.logger.debug?.(
4417
- `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
4418
- );
4419
- }
4474
+ await this._destroyKnex(old, "destroy during reconnect");
4475
+ }
4476
+ if (this._closed) return;
4477
+ this._reconnectAcquireTimeoutMs = 3e3;
4478
+ try {
4479
+ await this.connect();
4480
+ this._reconnectCooldownUntil = 0;
4481
+ } catch (error) {
4482
+ this._reconnectCooldownUntil = Date.now() + 5e3;
4483
+ throw error;
4484
+ } finally {
4485
+ this._reconnectAcquireTimeoutMs = null;
4420
4486
  }
4421
- await this.connect();
4422
4487
  })();
4423
4488
  try {
4424
4489
  await this._reconnectPromise;
@@ -4427,10 +4492,19 @@ var Db = class _Db {
4427
4492
  }
4428
4493
  }
4429
4494
  async reconnectAfterConnectionError(error) {
4495
+ if (this._closed) {
4496
+ return;
4497
+ }
4498
+ if (this._reconnectCooldownUntil && Date.now() < this._reconnectCooldownUntil) {
4499
+ return;
4500
+ }
4430
4501
  this.logger.warn?.(
4431
4502
  `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
4432
4503
  );
4433
- await this.reconnect();
4504
+ try {
4505
+ await this.reconnect();
4506
+ } catch {
4507
+ }
4434
4508
  }
4435
4509
  /**
4436
4510
  * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
@@ -4443,10 +4517,13 @@ var Db = class _Db {
4443
4517
  return await fn();
4444
4518
  } catch (error) {
4445
4519
  lastError = error;
4446
- if (!this.isConnectionError(error) || attempt >= retries) {
4520
+ if (this._closed || !this.isConnectionError(error) || attempt >= retries) {
4447
4521
  throw error;
4448
4522
  }
4449
4523
  await this.reconnectAfterConnectionError(error);
4524
+ if (this._closed) {
4525
+ throw error;
4526
+ }
4450
4527
  }
4451
4528
  }
4452
4529
  throw lastError;
@@ -4470,10 +4547,13 @@ var Db = class _Db {
4470
4547
  try {
4471
4548
  return await protoThen.call(builder);
4472
4549
  } catch (error) {
4473
- if (!inst.isConnectionError(error)) {
4550
+ if (inst._closed || !inst.isConnectionError(error)) {
4474
4551
  throw error;
4475
4552
  }
4476
4553
  await inst.reconnectAfterConnectionError(error);
4554
+ if (inst._closed || !inst.knexInstance) {
4555
+ throw error;
4556
+ }
4477
4557
  if (typeof builder.clone === "function") {
4478
4558
  const retry = builder.clone();
4479
4559
  retry.client = inst.knexInstance.client;
@@ -5495,13 +5575,14 @@ var Logger = class _Logger {
5495
5575
  */
5496
5576
  progress(message, opts) {
5497
5577
  const { prefix, count, total } = opts;
5498
- const paddedTotal = String(total).length;
5578
+ const displayTotal = Math.max(Number(total) || 0, Number(count) || 0);
5579
+ const paddedTotal = String(displayTotal).length;
5499
5580
  const paddedCount = String(count).padStart(paddedTotal, " ");
5500
5581
  const payload = {
5501
5582
  level: "progress",
5502
5583
  message,
5503
5584
  count: paddedCount,
5504
- total,
5585
+ total: displayTotal,
5505
5586
  prefix
5506
5587
  };
5507
5588
  const key = prefix ?? "";
@@ -5518,7 +5599,7 @@ var Logger = class _Logger {
5518
5599
  if (wantTimes) {
5519
5600
  let remaining = -1;
5520
5601
  if (itemsPerSec > 0) {
5521
- remaining = (total - count) / itemsPerSec;
5602
+ remaining = Math.max(0, (displayTotal - count) / itemsPerSec);
5522
5603
  }
5523
5604
  payload.elapsed = this.round(elapsedSeconds, 2);
5524
5605
  payload.remaining = remaining >= 0 ? this.round(remaining, 2) : remaining;
@@ -5527,12 +5608,12 @@ var Logger = class _Logger {
5527
5608
  payload.rate = itemsPerSec >= 0 ? this.round(itemsPerSec, 2) : itemsPerSec;
5528
5609
  }
5529
5610
  }
5530
- if (count >= total) {
5611
+ if (count === total) {
5531
5612
  delete this.startTimes[key];
5532
5613
  delete this.startCounts[key];
5533
5614
  delete this.lastProgressTimes[key];
5534
5615
  }
5535
- if (this.shouldOutputProgress(prefix ?? "", count, total)) {
5616
+ if (this.shouldOutputProgress(prefix ?? "", count, displayTotal)) {
5536
5617
  this.out(payload);
5537
5618
  if (this.options.progressThrottle && prefix) {
5538
5619
  this.lastProgressTimes[prefix] = Date.now();
@@ -5691,6 +5772,8 @@ function setup(opts = {}) {
5691
5772
  // a quick "show me the figured params and quit" that skips the flow's
5692
5773
  // actual work. Like --stopAfter=init, this is a hard exit(0) (registered
5693
5774
  // cleanups are skipped); call it once components/params are resolved.
5775
+ _requestExitCode: null,
5776
+ requestExit: null,
5694
5777
  showUsedParamsIfNeeded: () => {
5695
5778
  const mode = params.getShowUsedParamsMode?.();
5696
5779
  if (mode !== "top" && mode !== "stop") return;
@@ -5701,6 +5784,9 @@ function setup(opts = {}) {
5701
5784
  }
5702
5785
  }
5703
5786
  };
5787
+ context.requestExit = (code = 0) => {
5788
+ context._requestExitCode = code;
5789
+ };
5704
5790
  logger.debug("[setup] completed successfully");
5705
5791
  return context;
5706
5792
  }
@@ -6506,6 +6592,12 @@ function resolveKillTimeoutMs(pm2) {
6506
6592
  if (Number.isFinite(stopSec) && stopSec > 0) return Math.floor(stopSec * 1e3) + 5e3;
6507
6593
  return 65e3;
6508
6594
  }
6595
+ var DEFAULT_KILL_RETRY_TIME_MS = 1e4;
6596
+ function resolveKillRetryTimeMs(pm2) {
6597
+ const explicit = Number(pm2?.killRetryTime);
6598
+ if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
6599
+ return DEFAULT_KILL_RETRY_TIME_MS;
6600
+ }
6509
6601
  function resolvePm2Args(pm2) {
6510
6602
  const base = String(pm2?.args ?? "").trim();
6511
6603
  const stopSec = Number(pm2?.stopAllowance);
@@ -6529,6 +6621,7 @@ function buildEcosystemConfig(service, paths) {
6529
6621
  const outLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.out.log`);
6530
6622
  const errLog = (0, import_node_path8.join)(paths.logs, `${pm2.appName}.err.log`);
6531
6623
  const killTimeoutMs = resolveKillTimeoutMs(pm2);
6624
+ const killRetryTimeMs = resolveKillRetryTimeMs(pm2);
6532
6625
  const args = resolvePm2Args(pm2);
6533
6626
  const ensureEnv = paths.ensureEnv;
6534
6627
  return `/**
@@ -6554,6 +6647,9 @@ module.exports = {
6554
6647
  max_memory_restart: "1500M",
6555
6648
  // Grace window after SIGINT/SIGTERM before SIGKILL (ms). Align with --stopAllowance.
6556
6649
  kill_timeout: ${killTimeoutMs},
6650
+ // How often pm2 re-checks / logs "failed to kill - retrying in \u2026"
6651
+ // while waiting for graceful exit (pm2 default 100ms \u2192 noisy logs).
6652
+ kill_retry_time: ${killRetryTimeMs},
6557
6653
  out_file: "${outLog}",
6558
6654
  error_file: "${errLog}",
6559
6655
  merge_logs: true,