@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.
@@ -2804,10 +2804,35 @@ async function ensureSchema(db, spec, options = {}) {
2804
2804
  // src/db/index.js
2805
2805
  var KNEX_DEFAULTS = {
2806
2806
  testConnection: true,
2807
- pool: { min: 2, max: 10 },
2807
+ // min: 0 avoids holding idle sockets that go stale during long-running CLIs
2808
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
2808
2809
  acquireConnectionTimeout: 1e4,
2809
2810
  ssl: { rejectUnauthorized: false }
2810
2811
  };
2812
+ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
2813
+ "ECONNRESET",
2814
+ "ECONNREFUSED",
2815
+ "EPIPE",
2816
+ "ETIMEDOUT",
2817
+ "ENOTFOUND",
2818
+ "EHOSTUNREACH",
2819
+ "ENETUNREACH",
2820
+ "ECONNABORTED",
2821
+ "CONNECTION_ENDED",
2822
+ "CONNECTION_CLOSED",
2823
+ // PostgreSQL SQLSTATE class 08xxx (connection exception) + admin shutdowns
2824
+ "08000",
2825
+ "08001",
2826
+ "08003",
2827
+ "08004",
2828
+ "08006",
2829
+ "08007",
2830
+ "08P01",
2831
+ "57P01",
2832
+ "57P02",
2833
+ "57P03"
2834
+ ]);
2835
+ 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;
2811
2836
  var Db = class _Db {
2812
2837
  static async init(context, options = {}) {
2813
2838
  const buildConfig = async () => {
@@ -3055,10 +3080,11 @@ var Db = class _Db {
3055
3080
  this.knexInstance = null;
3056
3081
  this.isConnected = false;
3057
3082
  this.queriesLog = [];
3083
+ this._reconnectPromise = null;
3058
3084
  this.config = {
3059
3085
  testConnection: true,
3060
3086
  profile: false,
3061
- pool: { min: 2, max: 10 },
3087
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
3062
3088
  acquireConnectionTimeout: 1e4,
3063
3089
  ssl: { rejectUnauthorized: false },
3064
3090
  logger: console,
@@ -3076,7 +3102,7 @@ var Db = class _Db {
3076
3102
  if (!inst.knexInstance) {
3077
3103
  throw new Error("Db: Not connected. Call connect() first.");
3078
3104
  }
3079
- return inst.knexInstance(...argumentsList);
3105
+ return inst.wrapQueryBuilder(inst.knexInstance(...argumentsList));
3080
3106
  },
3081
3107
  get: (target, prop) => {
3082
3108
  if (prop === "_instance") {
@@ -3086,8 +3112,12 @@ var Db = class _Db {
3086
3112
  const ownMethods = [
3087
3113
  "connect",
3088
3114
  "disconnect",
3115
+ "reconnect",
3089
3116
  "testConnection",
3090
3117
  "tableExists",
3118
+ "raw",
3119
+ "withConnectionRetry",
3120
+ "isConnectionError",
3091
3121
  "getQueryLog",
3092
3122
  "getKnex",
3093
3123
  "isConnectedToDb",
@@ -3107,6 +3137,9 @@ var Db = class _Db {
3107
3137
  if (inst.knexInstance) {
3108
3138
  const knexProp = inst.knexInstance[prop];
3109
3139
  if (typeof knexProp === "function") {
3140
+ if (prop === "raw") {
3141
+ return (...args) => inst.raw(...args);
3142
+ }
3110
3143
  return knexProp.bind(inst.knexInstance);
3111
3144
  }
3112
3145
  return knexProp;
@@ -3186,6 +3219,130 @@ var Db = class _Db {
3186
3219
  throw error;
3187
3220
  }
3188
3221
  }
3222
+ /**
3223
+ * True when the error indicates a dead socket / pool that a fresh connect may fix.
3224
+ * Safe to call as `Db.prototype.isConnectionError(err)` or via a connected handle.
3225
+ */
3226
+ isConnectionError(error) {
3227
+ if (!error) {
3228
+ return false;
3229
+ }
3230
+ if (error instanceof AggregateError && Array.isArray(error.errors)) {
3231
+ return error.errors.some((e) => this.isConnectionError(e));
3232
+ }
3233
+ const code = error.code ?? error.errno;
3234
+ if (code != null && CONNECTION_ERROR_CODES.has(String(code))) {
3235
+ return true;
3236
+ }
3237
+ const msg = error instanceof Error ? error.message : String(error);
3238
+ return CONNECTION_ERROR_MESSAGE_RE.test(msg);
3239
+ }
3240
+ /**
3241
+ * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
3242
+ */
3243
+ async reconnect() {
3244
+ if (this._reconnectPromise) {
3245
+ await this._reconnectPromise;
3246
+ return;
3247
+ }
3248
+ this._reconnectPromise = (async () => {
3249
+ const old = this.knexInstance;
3250
+ this.isConnected = false;
3251
+ this.knexInstance = null;
3252
+ this.queriesLog = [];
3253
+ if (old) {
3254
+ try {
3255
+ await old.destroy();
3256
+ } catch (error) {
3257
+ this.logger.debug?.(
3258
+ `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
3259
+ );
3260
+ }
3261
+ }
3262
+ await this.connect();
3263
+ })();
3264
+ try {
3265
+ await this._reconnectPromise;
3266
+ } finally {
3267
+ this._reconnectPromise = null;
3268
+ }
3269
+ }
3270
+ async reconnectAfterConnectionError(error) {
3271
+ this.logger.warn?.(
3272
+ `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
3273
+ );
3274
+ await this.reconnect();
3275
+ }
3276
+ /**
3277
+ * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
3278
+ * `fn` should look up `this.knexInstance` each call so the retry uses the new client.
3279
+ */
3280
+ async withConnectionRetry(fn, { retries = 1 } = {}) {
3281
+ let lastError;
3282
+ for (let attempt = 0; attempt <= retries; attempt++) {
3283
+ try {
3284
+ return await fn();
3285
+ } catch (error) {
3286
+ lastError = error;
3287
+ if (!this.isConnectionError(error) || attempt >= retries) {
3288
+ throw error;
3289
+ }
3290
+ await this.reconnectAfterConnectionError(error);
3291
+ }
3292
+ }
3293
+ throw lastError;
3294
+ }
3295
+ /**
3296
+ * Wrap a knex QueryBuilder / Raw so that awaiting it retries once after reconnect
3297
+ * when the first attempt dies with a connection error.
3298
+ */
3299
+ wrapQueryBuilder(builder) {
3300
+ if (!builder || typeof builder.then !== "function" || builder.__dbReconnectWrapped) {
3301
+ return builder;
3302
+ }
3303
+ builder.__dbReconnectWrapped = true;
3304
+ const inst = this;
3305
+ const protoThen = Object.getPrototypeOf(builder)?.then;
3306
+ if (typeof protoThen !== "function") {
3307
+ return builder;
3308
+ }
3309
+ builder.then = function(onFulfilled, onRejected) {
3310
+ const run = async () => {
3311
+ try {
3312
+ return await protoThen.call(builder);
3313
+ } catch (error) {
3314
+ if (!inst.isConnectionError(error)) {
3315
+ throw error;
3316
+ }
3317
+ await inst.reconnectAfterConnectionError(error);
3318
+ if (typeof builder.clone === "function") {
3319
+ const retry = builder.clone();
3320
+ retry.client = inst.knexInstance.client;
3321
+ return await protoThen.call(retry);
3322
+ }
3323
+ if (typeof builder.toSQL === "function" && inst.knexInstance) {
3324
+ const sql = builder.toSQL();
3325
+ const statements = Array.isArray(sql) ? sql : [sql];
3326
+ let last;
3327
+ for (const stmt of statements) {
3328
+ last = await inst.knexInstance.raw(stmt.sql, stmt.bindings);
3329
+ }
3330
+ return last;
3331
+ }
3332
+ throw error;
3333
+ }
3334
+ };
3335
+ return run().then(onFulfilled, onRejected);
3336
+ };
3337
+ return builder;
3338
+ }
3339
+ /** knex.raw with auto-reconnect on dead connections. */
3340
+ raw(...args) {
3341
+ if (!this.knexInstance) {
3342
+ throw new Error("Db: Not connected. Call connect() first.");
3343
+ }
3344
+ return this.wrapQueryBuilder(this.knexInstance.raw(...args));
3345
+ }
3189
3346
  getErrorMessage(error) {
3190
3347
  if (error instanceof AggregateError) {
3191
3348
  const errors = error.errors || [];
@@ -3356,7 +3513,9 @@ var Db = class _Db {
3356
3513
  throw new Error("Db: Not connected. Call connect() first.");
3357
3514
  }
3358
3515
  try {
3359
- return await this.knexInstance.schema.hasTable(tableName);
3516
+ return await this.withConnectionRetry(
3517
+ () => this.knexInstance.schema.hasTable(tableName)
3518
+ );
3360
3519
  } catch (error) {
3361
3520
  this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
3362
3521
  throw error;