@neetru/sdk 3.2.1 → 3.2.3

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/db.cjs CHANGED
@@ -276,7 +276,99 @@ var init_http = __esm({
276
276
  }
277
277
  });
278
278
 
279
+ // src/db/sql/resilient-pool.ts
280
+ function isResilientPool(x) {
281
+ return typeof x === "object" && x !== null && x[RESILIENT_POOL_BRAND] === true;
282
+ }
283
+ function isConnectionEstablishmentError(err) {
284
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
285
+ if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "EHOSTUNREACH" || code === "EAI_AGAIN") {
286
+ return true;
287
+ }
288
+ if (typeof code === "string") {
289
+ const upper = code.toUpperCase();
290
+ if (upper === "CERT_HAS_EXPIRED" || upper.startsWith("ERR_SSL") || upper.startsWith("ERR_TLS") || upper.includes("CERTIFICATE_EXPIRED")) {
291
+ return true;
292
+ }
293
+ }
294
+ const rawMessage = err instanceof Error ? err.message : typeof err === "string" ? err : "";
295
+ const message = rawMessage.toLowerCase();
296
+ if (message.length === 0) return false;
297
+ if (message.includes("certificate expired") || message.includes("certificate has expired") || message.includes("cert_has_expired") || message.includes("alert certificate expired") || message.includes("err_ssl") || message.includes("sslv3 alert") || message.includes("tlsv1 alert")) {
298
+ return true;
299
+ }
300
+ if (message.includes("econnrefused") || message.includes("etimedout") || message.includes("enotfound") || message.includes("ehostunreach") || message.includes("getaddrinfo")) {
301
+ return true;
302
+ }
303
+ return false;
304
+ }
305
+ function createResilientPool(initialInner) {
306
+ const state = { current: initialInner };
307
+ let reLease = null;
308
+ const controls = {
309
+ __neetruSwapInner(newInner) {
310
+ const old = state.current;
311
+ state.current = newInner;
312
+ return old;
313
+ },
314
+ __neetruGetInner() {
315
+ return state.current;
316
+ },
317
+ __neetruSetReLease(fn) {
318
+ reLease = fn;
319
+ }
320
+ };
321
+ const invokeQuery = (pool, args) => pool.query(...args);
322
+ const wrappedQuery = (...args) => {
323
+ if (args.length > 0 && typeof args[args.length - 1] === "function") {
324
+ return invokeQuery(state.current, args);
325
+ }
326
+ return (async () => {
327
+ try {
328
+ return await invokeQuery(state.current, args);
329
+ } catch (err) {
330
+ const fn = reLease;
331
+ if (fn === null || !isConnectionEstablishmentError(err)) {
332
+ throw err;
333
+ }
334
+ try {
335
+ await fn();
336
+ } catch {
337
+ throw err;
338
+ }
339
+ return await invokeQuery(state.current, args);
340
+ }
341
+ })();
342
+ };
343
+ const target = Object.create(Object.getPrototypeOf(initialInner));
344
+ const proxy = new Proxy(target, {
345
+ get(_t, prop) {
346
+ if (prop === RESILIENT_POOL_BRAND) return true;
347
+ if (prop === "__neetruSwapInner") return controls.__neetruSwapInner;
348
+ if (prop === "__neetruGetInner") return controls.__neetruGetInner;
349
+ if (prop === "__neetruSetReLease") return controls.__neetruSetReLease;
350
+ if (prop === "query") return wrappedQuery;
351
+ if (prop === "constructor") {
352
+ return state.current.constructor;
353
+ }
354
+ const current = state.current;
355
+ const value = Reflect.get(current, prop, current);
356
+ return typeof value === "function" ? value.bind(current) : value;
357
+ }
358
+ });
359
+ return proxy;
360
+ }
361
+ var RESILIENT_POOL_BRAND;
362
+ var init_resilient_pool = __esm({
363
+ "src/db/sql/resilient-pool.ts"() {
364
+ RESILIENT_POOL_BRAND = /* @__PURE__ */ Symbol.for("neetru.sdk.resilientPool");
365
+ }
366
+ });
367
+
279
368
  // src/db/sql/lease.ts
369
+ function hasConnectionChanged(oldLease, newLease) {
370
+ return oldLease.leaseId !== newLease.leaseId || oldLease.credentialVersion !== newLease.credentialVersion || oldLease.host !== newLease.host || oldLease.port !== newLease.port || oldLease.dbName !== newLease.dbName || oldLease.user !== newLease.user || oldLease.password !== newLease.password || oldLease.sslca !== newLease.sslca || oldLease.clientCert !== newLease.clientCert || oldLease.clientKey !== newLease.clientKey;
371
+ }
280
372
  function mapPoolError(err) {
281
373
  if (err instanceof exports.NeetruDbError) return err;
282
374
  const message = err instanceof Error ? err.message : String(err);
@@ -302,6 +394,7 @@ var RENEWAL_THRESHOLD, MIN_RENEWAL_DELAY_MS, SqlLeaseManager;
302
394
  var init_lease = __esm({
303
395
  "src/db/sql/lease.ts"() {
304
396
  init_db_errors();
397
+ init_resilient_pool();
305
398
  RENEWAL_THRESHOLD = 0.8;
306
399
  MIN_RENEWAL_DELAY_MS = 3e4;
307
400
  SqlLeaseManager = class {
@@ -310,6 +403,12 @@ var init_lease = __esm({
310
403
  _orm;
311
404
  _renewalTimer = null;
312
405
  _closed = false;
406
+ /**
407
+ * Promessa de re-lease REATIVO em voo — coalesce chamadas concorrentes
408
+ * (N queries falhando ao mesmo tempo disparam UM único re-lease). Ver
409
+ * `_reactiveReLease`.
410
+ */
411
+ _reLeaseInFlight = null;
313
412
  _deps;
314
413
  constructor(lease, pool, orm, deps) {
315
414
  this._lease = lease;
@@ -381,37 +480,90 @@ var init_lease = __esm({
381
480
  async _doRenew() {
382
481
  if (this._closed) return;
383
482
  try {
384
- const renewOpts = {
385
- leaseId: this._lease.leaseId
483
+ const withDb = (o) => {
484
+ if (this._deps.database !== void 0) o.database = this._deps.database;
485
+ return o;
386
486
  };
387
- if (this._deps.database !== void 0) {
388
- renewOpts.database = this._deps.database;
487
+ let newLease;
488
+ try {
489
+ newLease = await this._deps.fetchLease(withDb({ leaseId: this._lease.leaseId }));
490
+ } catch (renewErr) {
491
+ newLease = await this._deps.fetchLease(withDb({}));
389
492
  }
390
- const newLease = await this._deps.fetchLease(renewOpts);
391
493
  await this._swapPool(newLease);
392
494
  } catch (err) {
393
495
  this._renewalTimer = setTimeout(() => {
394
496
  void this._doRenew();
395
497
  }, MIN_RENEWAL_DELAY_MS);
396
- if (process.env["NODE_ENV"] !== "production") {
397
- console.warn("[SqlLeaseManager] Lease renewal failed, will retry:", err);
398
- }
498
+ console.warn(
499
+ `[SqlLeaseManager] renova\xE7\xE3o E emiss\xE3o de lease novo falharam; re-tentando em ${MIN_RENEWAL_DELAY_MS}ms (cert mTLS expira ~15min ap\xF3s o \xFAltimo swap):`,
500
+ err instanceof Error ? err.message : err
501
+ );
399
502
  return;
400
503
  }
401
504
  this._scheduleRenewal();
402
505
  }
506
+ // ─── Re-lease REATIVO (P1.5) ───────────────────────────────────────────────
507
+ /**
508
+ * Recuperação REATIVA: chamado pelo wrapper resiliente (`resilient-pool.ts`)
509
+ * quando uma query plana falha num erro de ESTABELECIMENTO de conexão (cert
510
+ * mTLS expirado no meio de uma request, ou timer de renovação que não
511
+ * disparou). Emite um lease NOVO e faz o swap do pool interno do wrapper com
512
+ * o certificado fresco; o wrapper então re-tenta a query 1x no pool novo.
513
+ *
514
+ * Vai DIRETO ao lease novo (sem `leaseId`) — a conexão falhou porque o
515
+ * certificado do lease atual está ruim, então `renew` (que depende do lease
516
+ * antigo estar vivo) não é o caminho mais robusto. Emitir do zero é o que já
517
+ * cura o death-spiral no caminho proativo (ver `_doRenew`).
518
+ *
519
+ * Coalesce concorrência: N queries falhando ao mesmo tempo compartilham UM
520
+ * único re-lease (não N leases). Após o swap, cada query re-tenta no pool
521
+ * vigente.
522
+ *
523
+ * NÃO engole erro: se o re-lease falhar, propaga — o wrapper então relança o
524
+ * erro ORIGINAL da query pro consumer.
525
+ */
526
+ async _reactiveReLease() {
527
+ if (this._closed) {
528
+ throw new exports.NeetruDbError(
529
+ "db_unavailable",
530
+ "SqlLeaseManager foi fechado \u2014 re-lease reativo abortado."
531
+ );
532
+ }
533
+ if (this._reLeaseInFlight !== null) return this._reLeaseInFlight;
534
+ const run = (async () => {
535
+ const withDb = (o) => {
536
+ if (this._deps.database !== void 0) o.database = this._deps.database;
537
+ return o;
538
+ };
539
+ const fresh = await this._deps.fetchLease(withDb({}));
540
+ await this._swapPool(fresh);
541
+ })();
542
+ this._reLeaseInFlight = run;
543
+ try {
544
+ await run;
545
+ } finally {
546
+ this._reLeaseInFlight = null;
547
+ }
548
+ }
403
549
  /**
404
550
  * Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
405
551
  *
406
552
  * Exposto como método protegido para testes.
407
553
  */
408
554
  async _swapPool(newLease) {
409
- const credRotated = newLease.credentialVersion !== this._lease.credentialVersion;
410
- if (!credRotated && newLease.leaseId === this._lease.leaseId) {
555
+ if (!hasConnectionChanged(this._lease, newLease)) {
411
556
  this._lease = newLease;
412
557
  return;
413
558
  }
414
559
  const newPool = this._deps.createPool(newLease);
560
+ if (isResilientPool(this._pool)) {
561
+ const oldInner = this._pool.__neetruSwapInner(newPool);
562
+ this._lease = newLease;
563
+ void oldInner.end().catch(() => {
564
+ });
565
+ return;
566
+ }
415
567
  const newOrm = this._deps.createDrizzle(newPool, this._deps.schema);
416
568
  const oldPool = this._pool;
417
569
  this._lease = newLease;
@@ -566,23 +718,26 @@ async function createSqlClientWithDeps(deps, options) {
566
718
  const msg = err instanceof Error ? err.message : String(err);
567
719
  throw new exports.NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
568
720
  }
569
- let pool;
721
+ let innerPool;
570
722
  try {
571
- pool = depsWithDb.createPool(lease);
723
+ innerPool = depsWithDb.createPool(lease);
572
724
  } catch (err) {
573
725
  const msg = err instanceof Error ? err.message : String(err);
574
726
  throw new exports.NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
575
727
  }
728
+ const pool = createResilientPool(innerPool);
576
729
  let orm;
577
730
  try {
578
731
  orm = depsWithDb.createDrizzle(pool, depsWithDb.schema);
579
732
  } catch (err) {
580
- await pool.end().catch(() => {
733
+ await innerPool.end().catch(() => {
581
734
  });
582
735
  const msg = err instanceof Error ? err.message : String(err);
583
736
  throw new exports.NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
584
737
  }
585
- return new SqlLeaseManager(lease, pool, orm, depsWithDb);
738
+ const manager = new SqlLeaseManager(lease, pool, orm, depsWithDb);
739
+ pool.__neetruSetReLease(() => manager._reactiveReLease());
740
+ return manager;
586
741
  }
587
742
  async function createSqlClientFromConfig(config, schema, options) {
588
743
  const fetchLease = createHttpLeaseFetcher(config);
@@ -641,6 +796,7 @@ var init_sql_client = __esm({
641
796
  "src/db/sql/sql-client.ts"() {
642
797
  init_db_errors();
643
798
  init_lease();
799
+ init_resilient_pool();
644
800
  LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
645
801
  LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
646
802
  }