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