@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.
@@ -2788,10 +2788,35 @@ async function ensureSchema(db, spec, options = {}) {
2788
2788
  // src/db/index.js
2789
2789
  var KNEX_DEFAULTS = {
2790
2790
  testConnection: true,
2791
- pool: { min: 2, max: 10 },
2791
+ // min: 0 avoids holding idle sockets that go stale during long-running CLIs
2792
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
2792
2793
  acquireConnectionTimeout: 1e4,
2793
2794
  ssl: { rejectUnauthorized: false }
2794
2795
  };
2796
+ var CONNECTION_ERROR_CODES = /* @__PURE__ */ new Set([
2797
+ "ECONNRESET",
2798
+ "ECONNREFUSED",
2799
+ "EPIPE",
2800
+ "ETIMEDOUT",
2801
+ "ENOTFOUND",
2802
+ "EHOSTUNREACH",
2803
+ "ENETUNREACH",
2804
+ "ECONNABORTED",
2805
+ "CONNECTION_ENDED",
2806
+ "CONNECTION_CLOSED",
2807
+ // PostgreSQL SQLSTATE class 08xxx (connection exception) + admin shutdowns
2808
+ "08000",
2809
+ "08001",
2810
+ "08003",
2811
+ "08004",
2812
+ "08006",
2813
+ "08007",
2814
+ "08P01",
2815
+ "57P01",
2816
+ "57P02",
2817
+ "57P03"
2818
+ ]);
2819
+ 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;
2795
2820
  var Db = class _Db {
2796
2821
  static async init(context, options = {}) {
2797
2822
  const buildConfig = async () => {
@@ -3039,10 +3064,11 @@ var Db = class _Db {
3039
3064
  this.knexInstance = null;
3040
3065
  this.isConnected = false;
3041
3066
  this.queriesLog = [];
3067
+ this._reconnectPromise = null;
3042
3068
  this.config = {
3043
3069
  testConnection: true,
3044
3070
  profile: false,
3045
- pool: { min: 2, max: 10 },
3071
+ pool: { min: 0, max: 10, idleTimeoutMillis: 3e4 },
3046
3072
  acquireConnectionTimeout: 1e4,
3047
3073
  ssl: { rejectUnauthorized: false },
3048
3074
  logger: console,
@@ -3060,7 +3086,7 @@ var Db = class _Db {
3060
3086
  if (!inst.knexInstance) {
3061
3087
  throw new Error("Db: Not connected. Call connect() first.");
3062
3088
  }
3063
- return inst.knexInstance(...argumentsList);
3089
+ return inst.wrapQueryBuilder(inst.knexInstance(...argumentsList));
3064
3090
  },
3065
3091
  get: (target, prop) => {
3066
3092
  if (prop === "_instance") {
@@ -3070,8 +3096,12 @@ var Db = class _Db {
3070
3096
  const ownMethods = [
3071
3097
  "connect",
3072
3098
  "disconnect",
3099
+ "reconnect",
3073
3100
  "testConnection",
3074
3101
  "tableExists",
3102
+ "raw",
3103
+ "withConnectionRetry",
3104
+ "isConnectionError",
3075
3105
  "getQueryLog",
3076
3106
  "getKnex",
3077
3107
  "isConnectedToDb",
@@ -3091,6 +3121,9 @@ var Db = class _Db {
3091
3121
  if (inst.knexInstance) {
3092
3122
  const knexProp = inst.knexInstance[prop];
3093
3123
  if (typeof knexProp === "function") {
3124
+ if (prop === "raw") {
3125
+ return (...args) => inst.raw(...args);
3126
+ }
3094
3127
  return knexProp.bind(inst.knexInstance);
3095
3128
  }
3096
3129
  return knexProp;
@@ -3170,6 +3203,130 @@ var Db = class _Db {
3170
3203
  throw error;
3171
3204
  }
3172
3205
  }
3206
+ /**
3207
+ * True when the error indicates a dead socket / pool that a fresh connect may fix.
3208
+ * Safe to call as `Db.prototype.isConnectionError(err)` or via a connected handle.
3209
+ */
3210
+ isConnectionError(error) {
3211
+ if (!error) {
3212
+ return false;
3213
+ }
3214
+ if (error instanceof AggregateError && Array.isArray(error.errors)) {
3215
+ return error.errors.some((e) => this.isConnectionError(e));
3216
+ }
3217
+ const code = error.code ?? error.errno;
3218
+ if (code != null && CONNECTION_ERROR_CODES.has(String(code))) {
3219
+ return true;
3220
+ }
3221
+ const msg = error instanceof Error ? error.message : String(error);
3222
+ return CONNECTION_ERROR_MESSAGE_RE.test(msg);
3223
+ }
3224
+ /**
3225
+ * Destroy the current knex pool and open a new one. Concurrent callers share one attempt.
3226
+ */
3227
+ async reconnect() {
3228
+ if (this._reconnectPromise) {
3229
+ await this._reconnectPromise;
3230
+ return;
3231
+ }
3232
+ this._reconnectPromise = (async () => {
3233
+ const old = this.knexInstance;
3234
+ this.isConnected = false;
3235
+ this.knexInstance = null;
3236
+ this.queriesLog = [];
3237
+ if (old) {
3238
+ try {
3239
+ await old.destroy();
3240
+ } catch (error) {
3241
+ this.logger.debug?.(
3242
+ `[Db] destroy during reconnect: ${this.getErrorMessage(error)}`
3243
+ );
3244
+ }
3245
+ }
3246
+ await this.connect();
3247
+ })();
3248
+ try {
3249
+ await this._reconnectPromise;
3250
+ } finally {
3251
+ this._reconnectPromise = null;
3252
+ }
3253
+ }
3254
+ async reconnectAfterConnectionError(error) {
3255
+ this.logger.warn?.(
3256
+ `[Db] Connection lost (${this.getErrorMessage(error)}) \u2014 reconnecting\u2026`
3257
+ );
3258
+ await this.reconnect();
3259
+ }
3260
+ /**
3261
+ * Run `fn`; on a connection error, reconnect once (by default) and retry `fn`.
3262
+ * `fn` should look up `this.knexInstance` each call so the retry uses the new client.
3263
+ */
3264
+ async withConnectionRetry(fn, { retries = 1 } = {}) {
3265
+ let lastError;
3266
+ for (let attempt = 0; attempt <= retries; attempt++) {
3267
+ try {
3268
+ return await fn();
3269
+ } catch (error) {
3270
+ lastError = error;
3271
+ if (!this.isConnectionError(error) || attempt >= retries) {
3272
+ throw error;
3273
+ }
3274
+ await this.reconnectAfterConnectionError(error);
3275
+ }
3276
+ }
3277
+ throw lastError;
3278
+ }
3279
+ /**
3280
+ * Wrap a knex QueryBuilder / Raw so that awaiting it retries once after reconnect
3281
+ * when the first attempt dies with a connection error.
3282
+ */
3283
+ wrapQueryBuilder(builder) {
3284
+ if (!builder || typeof builder.then !== "function" || builder.__dbReconnectWrapped) {
3285
+ return builder;
3286
+ }
3287
+ builder.__dbReconnectWrapped = true;
3288
+ const inst = this;
3289
+ const protoThen = Object.getPrototypeOf(builder)?.then;
3290
+ if (typeof protoThen !== "function") {
3291
+ return builder;
3292
+ }
3293
+ builder.then = function(onFulfilled, onRejected) {
3294
+ const run = async () => {
3295
+ try {
3296
+ return await protoThen.call(builder);
3297
+ } catch (error) {
3298
+ if (!inst.isConnectionError(error)) {
3299
+ throw error;
3300
+ }
3301
+ await inst.reconnectAfterConnectionError(error);
3302
+ if (typeof builder.clone === "function") {
3303
+ const retry = builder.clone();
3304
+ retry.client = inst.knexInstance.client;
3305
+ return await protoThen.call(retry);
3306
+ }
3307
+ if (typeof builder.toSQL === "function" && inst.knexInstance) {
3308
+ const sql = builder.toSQL();
3309
+ const statements = Array.isArray(sql) ? sql : [sql];
3310
+ let last;
3311
+ for (const stmt of statements) {
3312
+ last = await inst.knexInstance.raw(stmt.sql, stmt.bindings);
3313
+ }
3314
+ return last;
3315
+ }
3316
+ throw error;
3317
+ }
3318
+ };
3319
+ return run().then(onFulfilled, onRejected);
3320
+ };
3321
+ return builder;
3322
+ }
3323
+ /** knex.raw with auto-reconnect on dead connections. */
3324
+ raw(...args) {
3325
+ if (!this.knexInstance) {
3326
+ throw new Error("Db: Not connected. Call connect() first.");
3327
+ }
3328
+ return this.wrapQueryBuilder(this.knexInstance.raw(...args));
3329
+ }
3173
3330
  getErrorMessage(error) {
3174
3331
  if (error instanceof AggregateError) {
3175
3332
  const errors = error.errors || [];
@@ -3340,7 +3497,9 @@ var Db = class _Db {
3340
3497
  throw new Error("Db: Not connected. Call connect() first.");
3341
3498
  }
3342
3499
  try {
3343
- return await this.knexInstance.schema.hasTable(tableName);
3500
+ return await this.withConnectionRetry(
3501
+ () => this.knexInstance.schema.hasTable(tableName)
3502
+ );
3344
3503
  } catch (error) {
3345
3504
  this.logger.error?.(`[Db] Error checking table existence: ${error.message}`);
3346
3505
  throw error;