@neetru/sdk 3.2.2 → 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/index.cjs CHANGED
@@ -277,6 +277,95 @@ var init_db_errors = __esm({
277
277
  }
278
278
  });
279
279
 
280
+ // src/db/sql/resilient-pool.ts
281
+ function isResilientPool(x) {
282
+ return typeof x === "object" && x !== null && x[RESILIENT_POOL_BRAND] === true;
283
+ }
284
+ function isConnectionEstablishmentError(err) {
285
+ const code = err && typeof err === "object" && "code" in err ? err.code : void 0;
286
+ if (code === "ECONNREFUSED" || code === "ETIMEDOUT" || code === "ENOTFOUND" || code === "EHOSTUNREACH" || code === "EAI_AGAIN") {
287
+ return true;
288
+ }
289
+ if (typeof code === "string") {
290
+ const upper = code.toUpperCase();
291
+ if (upper === "CERT_HAS_EXPIRED" || upper.startsWith("ERR_SSL") || upper.startsWith("ERR_TLS") || upper.includes("CERTIFICATE_EXPIRED")) {
292
+ return true;
293
+ }
294
+ }
295
+ const rawMessage = err instanceof Error ? err.message : typeof err === "string" ? err : "";
296
+ const message = rawMessage.toLowerCase();
297
+ if (message.length === 0) return false;
298
+ 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")) {
299
+ return true;
300
+ }
301
+ if (message.includes("econnrefused") || message.includes("etimedout") || message.includes("enotfound") || message.includes("ehostunreach") || message.includes("getaddrinfo")) {
302
+ return true;
303
+ }
304
+ return false;
305
+ }
306
+ function createResilientPool(initialInner) {
307
+ const state = { current: initialInner };
308
+ let reLease = null;
309
+ const controls = {
310
+ __neetruSwapInner(newInner) {
311
+ const old = state.current;
312
+ state.current = newInner;
313
+ return old;
314
+ },
315
+ __neetruGetInner() {
316
+ return state.current;
317
+ },
318
+ __neetruSetReLease(fn) {
319
+ reLease = fn;
320
+ }
321
+ };
322
+ const invokeQuery = (pool, args) => pool.query(...args);
323
+ const wrappedQuery = (...args) => {
324
+ if (args.length > 0 && typeof args[args.length - 1] === "function") {
325
+ return invokeQuery(state.current, args);
326
+ }
327
+ return (async () => {
328
+ try {
329
+ return await invokeQuery(state.current, args);
330
+ } catch (err) {
331
+ const fn = reLease;
332
+ if (fn === null || !isConnectionEstablishmentError(err)) {
333
+ throw err;
334
+ }
335
+ try {
336
+ await fn();
337
+ } catch {
338
+ throw err;
339
+ }
340
+ return await invokeQuery(state.current, args);
341
+ }
342
+ })();
343
+ };
344
+ const target = Object.create(Object.getPrototypeOf(initialInner));
345
+ const proxy = new Proxy(target, {
346
+ get(_t, prop) {
347
+ if (prop === RESILIENT_POOL_BRAND) return true;
348
+ if (prop === "__neetruSwapInner") return controls.__neetruSwapInner;
349
+ if (prop === "__neetruGetInner") return controls.__neetruGetInner;
350
+ if (prop === "__neetruSetReLease") return controls.__neetruSetReLease;
351
+ if (prop === "query") return wrappedQuery;
352
+ if (prop === "constructor") {
353
+ return state.current.constructor;
354
+ }
355
+ const current = state.current;
356
+ const value = Reflect.get(current, prop, current);
357
+ return typeof value === "function" ? value.bind(current) : value;
358
+ }
359
+ });
360
+ return proxy;
361
+ }
362
+ var RESILIENT_POOL_BRAND;
363
+ var init_resilient_pool = __esm({
364
+ "src/db/sql/resilient-pool.ts"() {
365
+ RESILIENT_POOL_BRAND = /* @__PURE__ */ Symbol.for("neetru.sdk.resilientPool");
366
+ }
367
+ });
368
+
280
369
  // src/db/sql/lease.ts
281
370
  function hasConnectionChanged(oldLease, newLease) {
282
371
  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;
@@ -306,6 +395,7 @@ var RENEWAL_THRESHOLD, MIN_RENEWAL_DELAY_MS, SqlLeaseManager;
306
395
  var init_lease = __esm({
307
396
  "src/db/sql/lease.ts"() {
308
397
  init_db_errors();
398
+ init_resilient_pool();
309
399
  RENEWAL_THRESHOLD = 0.8;
310
400
  MIN_RENEWAL_DELAY_MS = 3e4;
311
401
  SqlLeaseManager = class {
@@ -314,6 +404,12 @@ var init_lease = __esm({
314
404
  _orm;
315
405
  _renewalTimer = null;
316
406
  _closed = false;
407
+ /**
408
+ * Promessa de re-lease REATIVO em voo — coalesce chamadas concorrentes
409
+ * (N queries falhando ao mesmo tempo disparam UM único re-lease). Ver
410
+ * `_reactiveReLease`.
411
+ */
412
+ _reLeaseInFlight = null;
317
413
  _deps;
318
414
  constructor(lease, pool, orm, deps) {
319
415
  this._lease = lease;
@@ -408,6 +504,49 @@ var init_lease = __esm({
408
504
  }
409
505
  this._scheduleRenewal();
410
506
  }
507
+ // ─── Re-lease REATIVO (P1.5) ───────────────────────────────────────────────
508
+ /**
509
+ * Recuperação REATIVA: chamado pelo wrapper resiliente (`resilient-pool.ts`)
510
+ * quando uma query plana falha num erro de ESTABELECIMENTO de conexão (cert
511
+ * mTLS expirado no meio de uma request, ou timer de renovação que não
512
+ * disparou). Emite um lease NOVO e faz o swap do pool interno do wrapper com
513
+ * o certificado fresco; o wrapper então re-tenta a query 1x no pool novo.
514
+ *
515
+ * Vai DIRETO ao lease novo (sem `leaseId`) — a conexão falhou porque o
516
+ * certificado do lease atual está ruim, então `renew` (que depende do lease
517
+ * antigo estar vivo) não é o caminho mais robusto. Emitir do zero é o que já
518
+ * cura o death-spiral no caminho proativo (ver `_doRenew`).
519
+ *
520
+ * Coalesce concorrência: N queries falhando ao mesmo tempo compartilham UM
521
+ * único re-lease (não N leases). Após o swap, cada query re-tenta no pool
522
+ * vigente.
523
+ *
524
+ * NÃO engole erro: se o re-lease falhar, propaga — o wrapper então relança o
525
+ * erro ORIGINAL da query pro consumer.
526
+ */
527
+ async _reactiveReLease() {
528
+ if (this._closed) {
529
+ throw new exports.NeetruDbError(
530
+ "db_unavailable",
531
+ "SqlLeaseManager foi fechado \u2014 re-lease reativo abortado."
532
+ );
533
+ }
534
+ if (this._reLeaseInFlight !== null) return this._reLeaseInFlight;
535
+ const run = (async () => {
536
+ const withDb = (o) => {
537
+ if (this._deps.database !== void 0) o.database = this._deps.database;
538
+ return o;
539
+ };
540
+ const fresh = await this._deps.fetchLease(withDb({}));
541
+ await this._swapPool(fresh);
542
+ })();
543
+ this._reLeaseInFlight = run;
544
+ try {
545
+ await run;
546
+ } finally {
547
+ this._reLeaseInFlight = null;
548
+ }
549
+ }
411
550
  /**
412
551
  * Hot-swap do pool: cria pool novo, atualiza ponteiros, drena pool antigo.
413
552
  *
@@ -419,6 +558,13 @@ var init_lease = __esm({
419
558
  return;
420
559
  }
421
560
  const newPool = this._deps.createPool(newLease);
561
+ if (isResilientPool(this._pool)) {
562
+ const oldInner = this._pool.__neetruSwapInner(newPool);
563
+ this._lease = newLease;
564
+ void oldInner.end().catch(() => {
565
+ });
566
+ return;
567
+ }
422
568
  const newOrm = this._deps.createDrizzle(newPool, this._deps.schema);
423
569
  const oldPool = this._pool;
424
570
  this._lease = newLease;
@@ -573,23 +719,26 @@ async function createSqlClientWithDeps(deps, options) {
573
719
  const msg = err instanceof Error ? err.message : String(err);
574
720
  throw new exports.NeetruDbError("db_unavailable", `Falha ao obter lease inicial: ${msg}`);
575
721
  }
576
- let pool;
722
+ let innerPool;
577
723
  try {
578
- pool = depsWithDb.createPool(lease);
724
+ innerPool = depsWithDb.createPool(lease);
579
725
  } catch (err) {
580
726
  const msg = err instanceof Error ? err.message : String(err);
581
727
  throw new exports.NeetruDbError("db_unavailable", `Falha ao abrir pool: ${msg}`);
582
728
  }
729
+ const pool = createResilientPool(innerPool);
583
730
  let orm;
584
731
  try {
585
732
  orm = depsWithDb.createDrizzle(pool, depsWithDb.schema);
586
733
  } catch (err) {
587
- await pool.end().catch(() => {
734
+ await innerPool.end().catch(() => {
588
735
  });
589
736
  const msg = err instanceof Error ? err.message : String(err);
590
737
  throw new exports.NeetruDbError("db_unavailable", `Falha ao criar handle Drizzle: ${msg}`);
591
738
  }
592
- return new SqlLeaseManager(lease, pool, orm, depsWithDb);
739
+ const manager = new SqlLeaseManager(lease, pool, orm, depsWithDb);
740
+ pool.__neetruSetReLease(() => manager._reactiveReLease());
741
+ return manager;
593
742
  }
594
743
  async function createSqlClientFromConfig(config, schema, options) {
595
744
  const fetchLease = createHttpLeaseFetcher(config);
@@ -648,6 +797,7 @@ var init_sql_client = __esm({
648
797
  "src/db/sql/sql-client.ts"() {
649
798
  init_db_errors();
650
799
  init_lease();
800
+ init_resilient_pool();
651
801
  LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
652
802
  LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
653
803
  }
@@ -7128,7 +7278,7 @@ function createNeetruClient(config = {}) {
7128
7278
  init_errors();
7129
7279
  init_db_errors();
7130
7280
  init_http();
7131
- var VERSION = "3.2.2";
7281
+ var VERSION = "3.2.3";
7132
7282
 
7133
7283
  exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
7134
7284
  exports.DEV_FIXTURE_USER = DEV_FIXTURE_USER;