@nmakarov/cli-toolkit 0.44.0 → 0.46.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
@@ -3478,10 +3478,35 @@ async function ensureSchemaEverywhere(dbs, spec, options = {}) {
3478
3478
  // src/db/index.js
3479
3479
  var KNEX_DEFAULTS = {
3480
3480
  testConnection: true,
3481
- pool: { min: 2, max: 10 },
3481
+ // min: 0 avoids holding idle sockets that go stale during long-running CLIs
3482
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
3482
3483
  acquireConnectionTimeout: 1e4,
3483
3484
  ssl: { rejectUnauthorized: false }
3484
3485
  };
3486
+ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
3487
+ "ECONNRESET",
3488
+ "ECONNREFUSED",
3489
+ "EPIPE",
3490
+ "ETIMEDOUT",
3491
+ "ENOTFOUND",
3492
+ "EHOSTUNREACH",
3493
+ "ENETUNREACH",
3494
+ "ECONNABORTED",
3495
+ "CONNECTION_ENDED",
3496
+ "CONNECTION_CLOSED",
3497
+ // PostgreSQL SQLSTATE class 08xxx (connection exception) + admin shutdowns
3498
+ "08000",
3499
+ "08001",
3500
+ "08003",
3501
+ "08004",
3502
+ "08006",
3503
+ "08007",
3504
+ "08P01",
3505
+ "57P01",
3506
+ "57P02",
3507
+ "57P03"
3508
+ ]);
3509
+ 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|Timeout acquiring a connection/i;
3485
3510
  var Db = class _Db {
3486
3511
  static async init(context, options = {}) {
3487
3512
  const buildConfig = async () => {
@@ -3729,10 +3754,11 @@ var Db = class _Db {
3729
3754
  this.knexInstance = null;
3730
3755
  this.isConnected = false;
3731
3756
  this.queriesLog = [];
3757
+ this._reconnectPromise = null;
3732
3758
  this.config = {
3733
3759
  testConnection: true,
3734
3760
  profile: false,
3735
- pool: { min: 2, max: 10 },
3761
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
3736
3762
  acquireConnectionTimeout: 1e4,
3737
3763
  ssl: { rejectUnauthorized: false },
3738
3764
  logger: console,
@@ -3750,7 +3776,7 @@ var Db = class _Db {
3750
3776
  if (!inst.knexInstance) {
3751
3777
  throw new Error("Db: Not connected. Call connect() first.");
3752
3778
  }
3753
- return inst.knexInstance(...argumentsList);
3779
+ return inst.wrapQueryBuilder(inst.knexInstance(...argumentsList));
3754
3780
  },
3755
3781
  get: (target, prop) => {
3756
3782
  if (prop === "_instance") {
@@ -3760,8 +3786,12 @@ var Db = class _Db {
3760
3786
  const ownMethods = [
3761
3787
  "connect",
3762
3788
  "disconnect",
3789
+ "reconnect",
3763
3790
  "testConnection",
3764
3791
  "tableExists",
3792
+ "raw",
3793
+ "withConnectionRetry",
3794
+ "isConnectionError",
3765
3795
  "getQueryLog",
3766
3796
  "getKnex",
3767
3797
  "isConnectedToDb",
@@ -3781,6 +3811,9 @@ var Db = class _Db {
3781
3811
  if (inst.knexInstance) {
3782
3812
  const knexProp = inst.knexInstance[prop];
3783
3813
  if (typeof knexProp === "function") {
3814
+ if (prop === "raw") {
3815
+ return (...args) => inst.raw(...args);
3816
+ }
3784
3817
  return knexProp.bind(inst.knexInstance);
3785
3818
  }
3786
3819
  return knexProp;
@@ -3860,6 +3893,130 @@ var Db = class _Db {
3860
3893
  throw error;
3861
3894
  }
3862
3895
  }
3896
+ /**
3897
+ * True when the error indicates a dead socket / pool that a fresh connect may fix.
3898
+ * Safe to call as `Db.prototype.isConnectionError(err)` or via a connected handle.
3899
+ */
3900
+ isConnectionError(error) {
3901
+ if (!error) {
3902
+ return false;
3903
+ }
3904
+ if (error instanceof AggregateError && Array.isArray(error.errors)) {
3905
+ return error.errors.some((e) => this.isConnectionError(e));
3906
+ }
3907
+ const code = error.code ?? error.errno;
3908
+ if (code != null && CONNECTION_ERROR_CODES.has(String(code))) {
3909
+ return true;
3910
+ }
3911
+ const msg = error instanceof Error ? error.message : String(error);
3912
+ return CONNECTION_ERROR_MESSAGE_RE.test(msg);
3913
+ }
3914
+ /**
3915
+ * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
3916
+ */
3917
+ async reconnect() {
3918
+ if (this._reconnectPromise) {
3919
+ await this._reconnectPromise;
3920
+ return;
3921
+ }
3922
+ this._reconnectPromise = (async () => {
3923
+ const old = this.knexInstance;
3924
+ this.isConnected = false;
3925
+ this.knexInstance = null;
3926
+ this.queriesLog = [];
3927
+ if (old) {
3928
+ try {
3929
+ await old.destroy();
3930
+ } catch (error) {
3931
+ this.logger.debug?.(
3932
+ `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
3933
+ );
3934
+ }
3935
+ }
3936
+ await this.connect();
3937
+ })();
3938
+ try {
3939
+ await this._reconnectPromise;
3940
+ } finally {
3941
+ this._reconnectPromise = null;
3942
+ }
3943
+ }
3944
+ async reconnectAfterConnectionError(error) {
3945
+ this.logger.warn?.(
3946
+ `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
3947
+ );
3948
+ await this.reconnect();
3949
+ }
3950
+ /**
3951
+ * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
3952
+ * `fn` should look up `this.knexInstance` each call so the retry uses the new client.
3953
+ */
3954
+ async withConnectionRetry(fn, { retries = 1 } = {}) {
3955
+ let lastError;
3956
+ for (let attempt = 0; attempt <= retries; attempt++) {
3957
+ try {
3958
+ return await fn();
3959
+ } catch (error) {
3960
+ lastError = error;
3961
+ if (!this.isConnectionError(error) || attempt >= retries) {
3962
+ throw error;
3963
+ }
3964
+ await this.reconnectAfterConnectionError(error);
3965
+ }
3966
+ }
3967
+ throw lastError;
3968
+ }
3969
+ /**
3970
+ * Wrap a knex QueryBuilder / Raw so that awaiting it retries once after reconnect
3971
+ * when the first attempt dies with a connection error.
3972
+ */
3973
+ wrapQueryBuilder(builder) {
3974
+ if (!builder || typeof builder.then !== "function" || builder.__dbReconnectWrapped) {
3975
+ return builder;
3976
+ }
3977
+ builder.__dbReconnectWrapped = true;
3978
+ const inst = this;
3979
+ const protoThen = Object.getPrototypeOf(builder)?.then;
3980
+ if (typeof protoThen !== "function") {
3981
+ return builder;
3982
+ }
3983
+ builder.then = function(onFulfilled, onRejected) {
3984
+ const run2 = async () => {
3985
+ try {
3986
+ return await protoThen.call(builder);
3987
+ } catch (error) {
3988
+ if (!inst.isConnectionError(error)) {
3989
+ throw error;
3990
+ }
3991
+ await inst.reconnectAfterConnectionError(error);
3992
+ if (typeof builder.clone === "function") {
3993
+ const retry = builder.clone();
3994
+ retry.client = inst.knexInstance.client;
3995
+ return await protoThen.call(retry);
3996
+ }
3997
+ if (typeof builder.toSQL === "function" && inst.knexInstance) {
3998
+ const sql = builder.toSQL();
3999
+ const statements = Array.isArray(sql) ? sql : [sql];
4000
+ let last;
4001
+ for (const stmt of statements) {
4002
+ last = await inst.knexInstance.raw(stmt.sql, stmt.bindings);
4003
+ }
4004
+ return last;
4005
+ }
4006
+ throw error;
4007
+ }
4008
+ };
4009
+ return run2().then(onFulfilled, onRejected);
4010
+ };
4011
+ return builder;
4012
+ }
4013
+ /** knex.raw with auto-reconnect on dead connections. */
4014
+ raw(...args) {
4015
+ if (!this.knexInstance) {
4016
+ throw new Error("Db: Not connected. Call connect() first.");
4017
+ }
4018
+ return this.wrapQueryBuilder(this.knexInstance.raw(...args));
4019
+ }
3863
4020
  getErrorMessage(error) {
3864
4021
  if (error instanceof AggregateError) {
3865
4022
  const errors = error.errors || [];
@@ -4030,7 +4187,9 @@ var Db = class _Db {
4030
4187
  throw new Error("Db: Not connected. Call connect() first.");
4031
4188
  }
4032
4189
  try {
4033
- return await this.knexInstance.schema.hasTable(tableName);
4190
+ return await this.withConnectionRetry(
4191
+ () => this.knexInstance.schema.hasTable(tableName)
4192
+ );
4034
4193
  } catch (error) {
4035
4194
  this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
4036
4195
  throw error;