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