@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.js CHANGED
@@ -3760,7 +3760,7 @@ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
3760
3760
  "57P02",
3761
3761
  "57P03"
3762
3762
  ]);
3763
- 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;
3763
+ 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;
3764
3764
  var Db = class _Db {
3765
3765
  static async init(context, options = {}) {
3766
3766
  const buildConfig = async () => {
@@ -4028,6 +4028,10 @@ var Db = class _Db {
4028
4028
  this.isConnected = false;
4029
4029
  this.queriesLog = [];
4030
4030
  this._reconnectPromise = null;
4031
+ this._closed = false;
4032
+ this._liveKnex = /* @__PURE__ */ new Set();
4033
+ this._reconnectCooldownUntil = 0;
4034
+ this._reconnectAcquireTimeoutMs = null;
4031
4035
  this.config = {
4032
4036
  testConnection: true,
4033
4037
  profile: false,
@@ -4112,6 +4116,9 @@ var Db = class _Db {
4112
4116
  return null;
4113
4117
  }
4114
4118
  async connect() {
4119
+ if (this._closed) {
4120
+ throw new ParamError("Db: Connection closed");
4121
+ }
4115
4122
  if (this.isConnected && this.knexInstance) {
4116
4123
  this.logger.warn?.("[Db] Already connected");
4117
4124
  return;
@@ -4127,22 +4134,46 @@ var Db = class _Db {
4127
4134
  connectionString: this.config.connectionString,
4128
4135
  family: 4
4129
4136
  };
4137
+ const acquireTimeout = this._reconnectAcquireTimeoutMs ?? this.config.acquireConnectionTimeout;
4130
4138
  this.knexInstance = knex({
4131
4139
  client,
4132
4140
  connection: connectionConfig,
4133
4141
  pool: this.config.pool,
4134
- acquireConnectionTimeout: this.config.acquireConnectionTimeout,
4142
+ acquireConnectionTimeout: acquireTimeout,
4135
4143
  ...this.config.ssl && { ssl: this.config.ssl }
4136
4144
  });
4145
+ this._liveKnex.add(this.knexInstance);
4146
+ this.knexInstance.on?.("error", (err) => {
4147
+ if (this._closed) return;
4148
+ this.logger.warn?.(
4149
+ `[Db] Connection error (${this.getErrorMessage(err)})`
4150
+ );
4151
+ });
4152
+ if (this._closed) {
4153
+ await this._destroyKnex(this.knexInstance, "connect aborted (closed)");
4154
+ this.knexInstance = null;
4155
+ throw new ParamError("Db: Connection closed");
4156
+ }
4137
4157
  if (this.config.profile) {
4138
4158
  this.attachProfiler();
4139
4159
  }
4140
4160
  if (this.config.testConnection) {
4141
4161
  await this.testConnection();
4142
4162
  }
4163
+ if (this._closed) {
4164
+ await this._destroyKnex(this.knexInstance, "connect aborted (closed)");
4165
+ this.knexInstance = null;
4166
+ throw new ParamError("Db: Connection closed");
4167
+ }
4143
4168
  this.isConnected = true;
4144
4169
  this.logger.debug?.(formatDbConnectMessage(this.config.name, this.config.connectionString));
4145
4170
  } catch (error) {
4171
+ const failed = this.knexInstance;
4172
+ this.knexInstance = null;
4173
+ this.isConnected = false;
4174
+ if (failed) {
4175
+ await this._destroyKnex(failed, "connect failed");
4176
+ }
4146
4177
  if (error instanceof ParamError) {
4147
4178
  throw error;
4148
4179
  }
@@ -4150,20 +4181,42 @@ var Db = class _Db {
4150
4181
  throw new ParamError(`Db: Connection failed - ${errorMsg}`);
4151
4182
  }
4152
4183
  }
4153
- async disconnect() {
4154
- if (!this.knexInstance) {
4155
- return;
4156
- }
4184
+ /**
4185
+ * Destroy a knex pool without hanging exit on stuck TCP sockets (ETIMEDOUT).
4186
+ * @param {import("knex").Knex | null | undefined} knexInst
4187
+ * @param {string} [reason]
4188
+ * @param {number} [timeoutMs]
4189
+ */
4190
+ async _destroyKnex(knexInst, reason = "destroy", timeoutMs = 3e3) {
4191
+ if (!knexInst || typeof knexInst.destroy !== "function") return;
4192
+ this._liveKnex.delete(knexInst);
4157
4193
  try {
4158
- await this.knexInstance.destroy();
4159
- this.knexInstance = null;
4160
- this.isConnected = false;
4161
- this.queriesLog = [];
4162
- this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
4194
+ await Promise.race([
4195
+ knexInst.destroy(),
4196
+ new Promise((_, reject) => {
4197
+ const t = setTimeout(
4198
+ () => reject(new Error(`Db: ${reason} timed out after ${timeoutMs}ms`)),
4199
+ timeoutMs
4200
+ );
4201
+ t.unref?.();
4202
+ })
4203
+ ]);
4163
4204
  } catch (error) {
4164
- const errorMsg = this.getErrorMessage(error);
4165
- this.logger.error?.(`[Db] Error disconnecting: ${errorMsg}`);
4166
- throw error;
4205
+ this.logger.debug?.(
4206
+ `[Db] ${reason}: ${this.getErrorMessage(error)}`
4207
+ );
4208
+ }
4209
+ }
4210
+ async disconnect() {
4211
+ this._closed = true;
4212
+ this.isConnected = false;
4213
+ this.knexInstance = null;
4214
+ this.queriesLog = [];
4215
+ const all = [...this._liveKnex];
4216
+ this._liveKnex.clear();
4217
+ await Promise.all(all.map((inst) => this._destroyKnex(inst, "disconnect")));
4218
+ if (all.length > 0) {
4219
+ this.logger.debug?.(formatDbDisconnectMessage(this.config.name, this.config.connectionString));
4167
4220
  }
4168
4221
  }
4169
4222
  /**
@@ -4186,27 +4239,39 @@ var Db = class _Db {
4186
4239
  }
4187
4240
  /**
4188
4241
  * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
4242
+ * No-ops once disconnect() has closed the handle.
4189
4243
  */
4190
4244
  async reconnect() {
4245
+ if (this._closed) {
4246
+ return;
4247
+ }
4248
+ if (this._reconnectCooldownUntil && Date.now() < this._reconnectCooldownUntil) {
4249
+ return;
4250
+ }
4191
4251
  if (this._reconnectPromise) {
4192
4252
  await this._reconnectPromise;
4193
4253
  return;
4194
4254
  }
4195
4255
  this._reconnectPromise = (async () => {
4256
+ if (this._closed) return;
4196
4257
  const old = this.knexInstance;
4197
4258
  this.isConnected = false;
4198
4259
  this.knexInstance = null;
4199
4260
  this.queriesLog = [];
4200
4261
  if (old) {
4201
- try {
4202
- await old.destroy();
4203
- } catch (error) {
4204
- this.logger.debug?.(
4205
- `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
4206
- );
4207
- }
4262
+ await this._destroyKnex(old, "destroy during reconnect");
4263
+ }
4264
+ if (this._closed) return;
4265
+ this._reconnectAcquireTimeoutMs = 3e3;
4266
+ try {
4267
+ await this.connect();
4268
+ this._reconnectCooldownUntil = 0;
4269
+ } catch (error) {
4270
+ this._reconnectCooldownUntil = Date.now() + 5e3;
4271
+ throw error;
4272
+ } finally {
4273
+ this._reconnectAcquireTimeoutMs = null;
4208
4274
  }
4209
- await this.connect();
4210
4275
  })();
4211
4276
  try {
4212
4277
  await this._reconnectPromise;
@@ -4215,10 +4280,19 @@ var Db = class _Db {
4215
4280
  }
4216
4281
  }
4217
4282
  async reconnectAfterConnectionError(error) {
4283
+ if (this._closed) {
4284
+ return;
4285
+ }
4286
+ if (this._reconnectCooldownUntil && Date.now() < this._reconnectCooldownUntil) {
4287
+ return;
4288
+ }
4218
4289
  this.logger.warn?.(
4219
4290
  `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
4220
4291
  );
4221
- await this.reconnect();
4292
+ try {
4293
+ await this.reconnect();
4294
+ } catch {
4295
+ }
4222
4296
  }
4223
4297
  /**
4224
4298
  * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
@@ -4231,10 +4305,13 @@ var Db = class _Db {
4231
4305
  return await fn();
4232
4306
  } catch (error) {
4233
4307
  lastError = error;
4234
- if (!this.isConnectionError(error) || attempt >= retries) {
4308
+ if (this._closed || !this.isConnectionError(error) || attempt >= retries) {
4235
4309
  throw error;
4236
4310
  }
4237
4311
  await this.reconnectAfterConnectionError(error);
4312
+ if (this._closed) {
4313
+ throw error;
4314
+ }
4238
4315
  }
4239
4316
  }
4240
4317
  throw lastError;
@@ -4258,10 +4335,13 @@ var Db = class _Db {
4258
4335
  try {
4259
4336
  return await protoThen.call(builder);
4260
4337
  } catch (error) {
4261
- if (!inst.isConnectionError(error)) {
4338
+ if (inst._closed || !inst.isConnectionError(error)) {
4262
4339
  throw error;
4263
4340
  }
4264
4341
  await inst.reconnectAfterConnectionError(error);
4342
+ if (inst._closed || !inst.knexInstance) {
4343
+ throw error;
4344
+ }
4265
4345
  if (typeof builder.clone === "function") {
4266
4346
  const retry = builder.clone();
4267
4347
  retry.client = inst.knexInstance.client;
@@ -5309,13 +5389,14 @@ var Logger = class _Logger {
5309
5389
  */
5310
5390
  progress(message, opts) {
5311
5391
  const { prefix, count, total } = opts;
5312
- const paddedTotal = String(total).length;
5392
+ const displayTotal = Math.max(Number(total) || 0, Number(count) || 0);
5393
+ const paddedTotal = String(displayTotal).length;
5313
5394
  const paddedCount = String(count).padStart(paddedTotal, " ");
5314
5395
  const payload = {
5315
5396
  level: "progress",
5316
5397
  message,
5317
5398
  count: paddedCount,
5318
- total,
5399
+ total: displayTotal,
5319
5400
  prefix
5320
5401
  };
5321
5402
  const key = prefix ?? "";
@@ -5332,7 +5413,7 @@ var Logger = class _Logger {
5332
5413
  if (wantTimes) {
5333
5414
  let remaining = -1;
5334
5415
  if (itemsPerSec > 0) {
5335
- remaining = (total - count) / itemsPerSec;
5416
+ remaining = Math.max(0, (displayTotal - count) / itemsPerSec);
5336
5417
  }
5337
5418
  payload.elapsed = this.round(elapsedSeconds, 2);
5338
5419
  payload.remaining = remaining >= 0 ? this.round(remaining, 2) : remaining;
@@ -5341,12 +5422,12 @@ var Logger = class _Logger {
5341
5422
  payload.rate = itemsPerSec >= 0 ? this.round(itemsPerSec, 2) : itemsPerSec;
5342
5423
  }
5343
5424
  }
5344
- if (count >= total) {
5425
+ if (count === total) {
5345
5426
  delete this.startTimes[key];
5346
5427
  delete this.startCounts[key];
5347
5428
  delete this.lastProgressTimes[key];
5348
5429
  }
5349
- if (this.shouldOutputProgress(prefix ?? "", count, total)) {
5430
+ if (this.shouldOutputProgress(prefix ?? "", count, displayTotal)) {
5350
5431
  this.out(payload);
5351
5432
  if (this.options.progressThrottle && prefix) {
5352
5433
  this.lastProgressTimes[prefix] = Date.now();
@@ -5505,6 +5586,8 @@ function setup(opts = {}) {
5505
5586
  // a quick "show me the figured params and quit" that skips the flow's
5506
5587
  // actual work. Like --stopAfter=init, this is a hard exit(0) (registered
5507
5588
  // cleanups are skipped); call it once components/params are resolved.
5589
+ _requestExitCode: null,
5590
+ requestExit: null,
5508
5591
  showUsedParamsIfNeeded: () => {
5509
5592
  const mode = params.getShowUsedParamsMode?.();
5510
5593
  if (mode !== "top" && mode !== "stop") return;
@@ -5515,6 +5598,9 @@ function setup(opts = {}) {
5515
5598
  }
5516
5599
  }
5517
5600
  };
5601
+ context.requestExit = (code = 0) => {
5602
+ context._requestExitCode = code;
5603
+ };
5518
5604
  logger.debug("[setup] completed successfully");
5519
5605
  return context;
5520
5606
  }
@@ -6320,6 +6406,12 @@ function resolveKillTimeoutMs(pm2) {
6320
6406
  if (Number.isFinite(stopSec) && stopSec > 0) return Math.floor(stopSec * 1e3) + 5e3;
6321
6407
  return 65e3;
6322
6408
  }
6409
+ var DEFAULT_KILL_RETRY_TIME_MS = 1e4;
6410
+ function resolveKillRetryTimeMs(pm2) {
6411
+ const explicit = Number(pm2?.killRetryTime);
6412
+ if (Number.isFinite(explicit) && explicit > 0) return Math.floor(explicit);
6413
+ return DEFAULT_KILL_RETRY_TIME_MS;
6414
+ }
6323
6415
  function resolvePm2Args(pm2) {
6324
6416
  const base = String(pm2?.args ?? "").trim();
6325
6417
  const stopSec = Number(pm2?.stopAllowance);
@@ -6343,6 +6435,7 @@ function buildEcosystemConfig(service, paths) {
6343
6435
  const outLog = join9(paths.logs, `${pm2.appName}.out.log`);
6344
6436
  const errLog = join9(paths.logs, `${pm2.appName}.err.log`);
6345
6437
  const killTimeoutMs = resolveKillTimeoutMs(pm2);
6438
+ const killRetryTimeMs = resolveKillRetryTimeMs(pm2);
6346
6439
  const args = resolvePm2Args(pm2);
6347
6440
  const ensureEnv = paths.ensureEnv;
6348
6441
  return `/**
@@ -6368,6 +6461,9 @@ module.exports = {
6368
6461
  max_memory_restart: "1500M",
6369
6462
  // Grace window after SIGINT/SIGTERM before SIGKILL (ms). Align with --stopAllowance.
6370
6463
  kill_timeout: ${killTimeoutMs},
6464
+ // How often pm2 re-checks / logs "failed to kill - retrying in \u2026"
6465
+ // while waiting for graceful exit (pm2 default 100ms \u2192 noisy logs).
6466
+ kill_retry_time: ${killRetryTimeMs},
6371
6467
  out_file: "${outLog}",
6372
6468
  error_file: "${errLog}",
6373
6469
  merge_logs: true,