@neetru/sdk 3.4.0 → 3.4.2

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.
Files changed (58) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/dist/ai.d.cts +1 -1
  3. package/dist/ai.d.ts +1 -1
  4. package/dist/auth.cjs +230 -38
  5. package/dist/auth.cjs.map +1 -1
  6. package/dist/auth.d.cts +1 -1
  7. package/dist/auth.d.ts +1 -1
  8. package/dist/auth.mjs +230 -38
  9. package/dist/auth.mjs.map +1 -1
  10. package/dist/build-guard.cjs +41 -6
  11. package/dist/build-guard.cjs.map +1 -1
  12. package/dist/build-guard.d.cts +24 -1
  13. package/dist/build-guard.d.ts +24 -1
  14. package/dist/build-guard.mjs +40 -7
  15. package/dist/build-guard.mjs.map +1 -1
  16. package/dist/catalog.d.cts +1 -1
  17. package/dist/catalog.d.ts +1 -1
  18. package/dist/checkout.d.cts +1 -1
  19. package/dist/checkout.d.ts +1 -1
  20. package/dist/db-react.d.cts +1 -1
  21. package/dist/db-react.d.ts +1 -1
  22. package/dist/db.cjs +230 -38
  23. package/dist/db.cjs.map +1 -1
  24. package/dist/db.d.cts +1 -1
  25. package/dist/db.d.ts +1 -1
  26. package/dist/db.mjs +230 -38
  27. package/dist/db.mjs.map +1 -1
  28. package/dist/entitlements.d.cts +1 -1
  29. package/dist/entitlements.d.ts +1 -1
  30. package/dist/firestore-compat.d.cts +1 -1
  31. package/dist/firestore-compat.d.ts +1 -1
  32. package/dist/index.cjs +231 -39
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +2 -2
  35. package/dist/index.d.ts +2 -2
  36. package/dist/index.mjs +231 -39
  37. package/dist/index.mjs.map +1 -1
  38. package/dist/mocks.d.cts +1 -1
  39. package/dist/mocks.d.ts +1 -1
  40. package/dist/notifications.d.cts +1 -1
  41. package/dist/notifications.d.ts +1 -1
  42. package/dist/react.cjs +593 -31
  43. package/dist/react.cjs.map +1 -1
  44. package/dist/react.d.cts +97 -2
  45. package/dist/react.d.ts +97 -2
  46. package/dist/react.mjs +590 -31
  47. package/dist/react.mjs.map +1 -1
  48. package/dist/support.d.cts +1 -1
  49. package/dist/support.d.ts +1 -1
  50. package/dist/telemetry.d.cts +1 -1
  51. package/dist/telemetry.d.ts +1 -1
  52. package/dist/{types-DdJyqBy-.d.ts → types-BlOLDMR0.d.ts} +79 -6
  53. package/dist/{types-BoFSCHF_.d.cts → types-D-QdG502.d.cts} +79 -6
  54. package/dist/usage.d.cts +1 -1
  55. package/dist/usage.d.ts +1 -1
  56. package/dist/webhooks.d.cts +1 -1
  57. package/dist/webhooks.d.ts +1 -1
  58. package/package.json +1 -1
package/dist/index.d.cts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createNeetruClient } from './auth.cjs';
2
2
  export { NeetruError, NeetruErrorCode } from './errors.cjs';
3
- export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, aq as TelemetryEventAck, ar as TelemetryEventInput, av as UsageEventInput, aw as UsageNamespace, ax as UsageQuota, ay as VerifyTokenOptions, az as VerifyWebhookRequestOptions, aA as VerifyWebhookRequestResult, aB as WebhookEndpoint, aC as WebhookEvent, aD as WebhookScopeOptions, aE as WebhookTestResult, aF as WebhooksNamespace, aG as WithFieldSentinels, aI as createAiNamespace, aJ as createCheckoutNamespace, aK as createNeetruDb, aL as createNotificationsNamespace, aN as createWebhooksNamespace, aP as increment, aQ as isFieldSentinel, aR as isIncrementSentinel, aS as isServerTimestampSentinel, aT as serverTimestamp, aU as verifyWebhookRequest, aV as verifyWebhookSignature } from './types-BoFSCHF_.cjs';
3
+ export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, ar as TelemetryEventAck, as as TelemetryEventInput, aw as UsageEventInput, ax as UsageNamespace, ay as UsageQuota, az as VerifyTokenOptions, aA as VerifyWebhookRequestOptions, aB as VerifyWebhookRequestResult, aC as WebhookEndpoint, aD as WebhookEvent, aE as WebhookScopeOptions, aF as WebhookTestResult, aG as WebhooksNamespace, aH as WithFieldSentinels, aJ as createAiNamespace, aK as createCheckoutNamespace, aL as createNeetruDb, aM as createNotificationsNamespace, aO as createWebhooksNamespace, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp, aV as verifyWebhookRequest, aW as verifyWebhookSignature } from './types-D-QdG502.cjs';
4
4
  export { DEV_FIXTURE_USER, MockAi, MockAuth, MockAuthOptions, MockDb, MockEntitlements, MockSupport, MockUsage } from './mocks.cjs';
5
5
  import 'drizzle-orm/node-postgres';
6
6
 
@@ -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.4.0";
45
+ declare const VERSION: "3.4.1";
46
46
 
47
47
  export { VERSION, checkCoreVersionCompat };
package/dist/index.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  export { createNeetruClient } from './auth.js';
2
2
  export { NeetruError, NeetruErrorCode } from './errors.js';
3
- export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, aq as TelemetryEventAck, ar as TelemetryEventInput, av as UsageEventInput, aw as UsageNamespace, ax as UsageQuota, ay as VerifyTokenOptions, az as VerifyWebhookRequestOptions, aA as VerifyWebhookRequestResult, aB as WebhookEndpoint, aC as WebhookEvent, aD as WebhookScopeOptions, aE as WebhookTestResult, aF as WebhooksNamespace, aG as WithFieldSentinels, aI as createAiNamespace, aJ as createCheckoutNamespace, aK as createNeetruDb, aL as createNotificationsNamespace, aN as createWebhooksNamespace, aP as increment, aQ as isFieldSentinel, aR as isIncrementSentinel, aS as isServerTimestampSentinel, aT as serverTimestamp, aU as verifyWebhookRequest, aV as verifyWebhookSignature } from './types-DdJyqBy-.js';
3
+ export { A as AiChatChoice, a as AiChatMessage, b as AiChatOptions, c as AiChatResult, d as AiChatUsage, e as AiContentPart, f as AiImageUrlPart, g as AiNamespace, h as AiResponseFormat, i as AiStreamHandle, j as AiStreamMeta, k as AiTextPart, l as AuthNamespace, m as AuthStateListener, C as CatalogListOptions, n as CatalogListResponse, p as CheckoutIntentInfo, q as CheckoutIntentStatus, r as CheckoutNamespace, s as CheckoutStartInput, t as CheckoutStartResult, u as CheckoutTenantType, v as CompleteSignInResult, y as CreateTicketInput, D as DEFAULT_BASE_URL, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, J as DbNamespace, K as DbQuery, L as DbSqlLease, M as DbWhereFilter, O as EntitlementCheck, P as FieldSentinel, Q as IncrementSentinel, R as ListNotificationsOptions, S as MockCheckout, T as MockNotifications, U as MockWebhooks, V as NEETRU_SENTINEL_KEY, W as NeetruClient, X as NeetruClientConfig, Y as NeetruDb, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a3 as NeetruEnv, a4 as NeetruSqlClient, a5 as NeetruUser, a6 as NotificationScopeOptions, a7 as NotificationsNamespace, a8 as Product, a9 as ProductNotification, aa as ProductNotificationSeverity, ab as ProductPlan, ac as ProductStatus, ae as RedirectCallbackResult, af as RegisterWebhookInput, ag as ResolvedConfig, ah as SendNotificationInput, ai as ServerTimestampSentinel, aj as SignInOptions, al as SupportNamespace, am as SupportSeverity, an as SupportStatus, ao as SupportTicket, ar as TelemetryEventAck, as as TelemetryEventInput, aw as UsageEventInput, ax as UsageNamespace, ay as UsageQuota, az as VerifyTokenOptions, aA as VerifyWebhookRequestOptions, aB as VerifyWebhookRequestResult, aC as WebhookEndpoint, aD as WebhookEvent, aE as WebhookScopeOptions, aF as WebhookTestResult, aG as WebhooksNamespace, aH as WithFieldSentinels, aJ as createAiNamespace, aK as createCheckoutNamespace, aL as createNeetruDb, aM as createNotificationsNamespace, aO as createWebhooksNamespace, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp, aV as verifyWebhookRequest, aW as verifyWebhookSignature } from './types-BlOLDMR0.js';
4
4
  export { DEV_FIXTURE_USER, MockAi, MockAuth, MockAuthOptions, MockDb, MockEntitlements, MockSupport, MockUsage } from './mocks.js';
5
5
  import 'drizzle-orm/node-postgres';
6
6
 
@@ -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.4.0";
45
+ declare const VERSION: "3.4.1";
46
46
 
47
47
  export { VERSION, checkCoreVersionCompat };
package/dist/index.mjs CHANGED
@@ -682,13 +682,17 @@ var init_lease = __esm({
682
682
  // src/db/sql/sql-client.ts
683
683
  var sql_client_exports = {};
684
684
  __export(sql_client_exports, {
685
+ DEFAULT_POOL_CONNECT_TIMEOUT_MS: () => DEFAULT_POOL_CONNECT_TIMEOUT_MS,
686
+ DEFAULT_POOL_IDLE_MS: () => DEFAULT_POOL_IDLE_MS,
687
+ DEFAULT_POOL_MAX: () => DEFAULT_POOL_MAX,
685
688
  LEASE_ENDPOINT: () => LEASE_ENDPOINT,
686
689
  LEASE_RENEW_ENDPOINT: () => LEASE_RENEW_ENDPOINT,
687
690
  createDrizzleHandle: () => createDrizzleHandle,
688
691
  createHttpLeaseFetcher: () => createHttpLeaseFetcher,
689
692
  createPgPool: () => createPgPool,
690
693
  createSqlClientFromConfig: () => createSqlClientFromConfig,
691
- createSqlClientWithDeps: () => createSqlClientWithDeps
694
+ createSqlClientWithDeps: () => createSqlClientWithDeps,
695
+ resolvePoolTuning: () => resolvePoolTuning
692
696
  });
693
697
  function mapEnvToCore(sdkEnv) {
694
698
  if (sdkEnv === "workspace") return "staging";
@@ -777,6 +781,30 @@ function parseLease(raw) {
777
781
  expiresAt: req("expiresAt", "string")
778
782
  };
779
783
  }
784
+ function readEnvInt(name, def, min, max) {
785
+ const raw = typeof process !== "undefined" ? process.env?.[name] : void 0;
786
+ if (raw === void 0 || raw.trim() === "") return def;
787
+ const n = Number.parseInt(raw.trim(), 10);
788
+ if (!Number.isFinite(n) || n < min || n > max) return def;
789
+ return n;
790
+ }
791
+ function resolvePoolTuning() {
792
+ return {
793
+ // Teto 100: bem abaixo do `max_client_conn = 500` do PgBouncer, com folga
794
+ // para múltiplas instâncias do app dividirem o mesmo front-door.
795
+ max: readEnvInt("NEETRU_DB_POOL_MAX", DEFAULT_POOL_MAX, 1, 100),
796
+ // Teto 15min = janela do cert; piso 1s.
797
+ idleTimeoutMillis: readEnvInt("NEETRU_DB_POOL_IDLE_MS", DEFAULT_POOL_IDLE_MS, 1e3, 9e5),
798
+ connectionTimeoutMillis: readEnvInt(
799
+ "NEETRU_DB_POOL_CONNECT_TIMEOUT_MS",
800
+ DEFAULT_POOL_CONNECT_TIMEOUT_MS,
801
+ 1e3,
802
+ 12e4
803
+ ),
804
+ keepAlive: true,
805
+ keepAliveInitialDelayMillis: 1e4
806
+ };
807
+ }
780
808
  async function createPgPool(lease) {
781
809
  const { Pool } = await import(
782
810
  /* webpackIgnore: true */
@@ -795,9 +823,7 @@ async function createPgPool(lease) {
795
823
  user: lease.user,
796
824
  password: lease.password,
797
825
  ssl,
798
- max: 3,
799
- idleTimeoutMillis: 3e4,
800
- connectionTimeoutMillis: 1e4
826
+ ...resolvePoolTuning()
801
827
  });
802
828
  }
803
829
  async function createDrizzleHandle(pool, schema) {
@@ -932,9 +958,9 @@ async function createSqlClientFromConfig(config, schema, options) {
932
958
  user: lease.user,
933
959
  password: lease.password,
934
960
  ssl,
935
- max: 3,
936
- idleTimeoutMillis: 3e4,
937
- connectionTimeoutMillis: 1e4
961
+ // Tuning centralizado (env-tunável) — ver resolvePoolTuning + auditoria
962
+ // docs/_review/AUDITORIA_DB_CONN_PERF_2026-09-06.md no Core.
963
+ ...resolvePoolTuning()
938
964
  });
939
965
  };
940
966
  const createDrizzle = (pool, s) => {
@@ -957,7 +983,7 @@ async function createSqlClientFromConfig(config, schema, options) {
957
983
  options
958
984
  );
959
985
  }
960
- var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
986
+ var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT, DEFAULT_POOL_MAX, DEFAULT_POOL_IDLE_MS, DEFAULT_POOL_CONNECT_TIMEOUT_MS;
961
987
  var init_sql_client = __esm({
962
988
  "src/db/sql/sql-client.ts"() {
963
989
  init_db_errors();
@@ -966,6 +992,9 @@ var init_sql_client = __esm({
966
992
  init_resilient_pool();
967
993
  LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
968
994
  LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
995
+ DEFAULT_POOL_MAX = 10;
996
+ DEFAULT_POOL_IDLE_MS = 6e4;
997
+ DEFAULT_POOL_CONNECT_TIMEOUT_MS = 1e4;
969
998
  }
970
999
  });
971
1000
 
@@ -2541,8 +2570,10 @@ var MutationQueue = class {
2541
2570
  await this.store.deleteMutation(mutationId);
2542
2571
  }
2543
2572
  /**
2544
- * Marca uma mutação como `failed` e registra o erro.
2545
- * Incrementa `attempts`. Chamado pelo SyncEngine em falha permanente.
2573
+ * Marca uma mutação como `failed` (dead-letter) e registra o erro.
2574
+ * Incrementa `attempts` e carimba `lastAttemptAt`. Chamado pelo SyncEngine
2575
+ * quando a mutação esgota o teto de tentativas (mutação-veneno): sai do
2576
+ * conjunto `queued`, então `drain()` para de devolvê-la e a fila DESTRAVA.
2546
2577
  */
2547
2578
  async markFailed(mutationId, error) {
2548
2579
  const mut = await this.store.getMutation(mutationId);
@@ -2551,12 +2582,14 @@ var MutationQueue = class {
2551
2582
  ...mut,
2552
2583
  status: "failed",
2553
2584
  lastError: error,
2554
- attempts: mut.attempts + 1
2585
+ attempts: mut.attempts + 1,
2586
+ lastAttemptAt: Date.now()
2555
2587
  });
2556
2588
  }
2557
2589
  /**
2558
- * Incrementa `attempts` e volta para `queued` (falha transiente com backoff).
2559
- * Chamado pelo SyncEngine em falha transiente (5xx, timeout).
2590
+ * Incrementa `attempts`, carimba `lastAttemptAt` e volta para `queued`
2591
+ * (falha transiente com backoff). Chamado pelo SyncEngine em falha
2592
+ * transiente (5xx, timeout) da mutação que REALMENTE foi tentada.
2560
2593
  */
2561
2594
  async markRetry(mutationId, error) {
2562
2595
  const mut = await this.store.getMutation(mutationId);
@@ -2565,9 +2598,24 @@ var MutationQueue = class {
2565
2598
  ...mut,
2566
2599
  status: "queued",
2567
2600
  lastError: error,
2568
- attempts: mut.attempts + 1
2601
+ attempts: mut.attempts + 1,
2602
+ lastAttemptAt: Date.now()
2569
2603
  });
2570
2604
  }
2605
+ /**
2606
+ * Reverte uma mutação `inflight` de volta a `queued` SEM tocar em `attempts`
2607
+ * nem `lastAttemptAt`. Usado para mutações que foram marcadas inflight mas
2608
+ * NUNCA chegaram a ser enviadas (estavam atrás da head no FIFO e o push
2609
+ * parou antes delas): contá-las como tentativa as empurraria pro dead-letter
2610
+ * por engano, e adiaria seu envio via backoff sem que tenham falhado.
2611
+ * No-op se a mutação não está mais `inflight`.
2612
+ */
2613
+ async requeueInflight(mutationId) {
2614
+ const mut = await this.store.getMutation(mutationId);
2615
+ if (!mut) return;
2616
+ if (mut.status !== "inflight") return;
2617
+ await this.store.putMutation({ ...mut, status: "queued" });
2618
+ }
2571
2619
  };
2572
2620
 
2573
2621
  // src/db/field-value.ts
@@ -2623,6 +2671,10 @@ function hasFieldSentinel(data) {
2623
2671
  }
2624
2672
 
2625
2673
  // src/db/offline/sync-engine.ts
2674
+ var MAX_MUTATION_ATTEMPTS = 8;
2675
+ var BACKOFF_BASE_MS = 1e3;
2676
+ var BACKOFF_CAP_MS = 5 * 60 * 1e3;
2677
+ var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
2626
2678
  function hasSentinels(payload) {
2627
2679
  return payload !== null && hasFieldSentinel(payload);
2628
2680
  }
@@ -2657,6 +2709,8 @@ var SyncEngine = class {
2657
2709
  _state;
2658
2710
  /** Listeners de mudança de SyncState. */
2659
2711
  _stateListeners = /* @__PURE__ */ new Set();
2712
+ /** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
2713
+ _errorListeners = /* @__PURE__ */ new Set();
2660
2714
  /** Cleanup functions dos listeners externos. */
2661
2715
  _cleanups = [];
2662
2716
  /** Timer de resync periódico. */
@@ -2715,6 +2769,19 @@ var SyncEngine = class {
2715
2769
  this._stateListeners.delete(cb);
2716
2770
  };
2717
2771
  }
2772
+ /**
2773
+ * Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
2774
+ * (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
2775
+ * mutação-veneno) — o que antes era engolido com "log silencioso".
2776
+ * O mesmo diagnóstico também aparece em `SyncState.lastError`.
2777
+ * Retorna uma função de unsubscribe.
2778
+ */
2779
+ onError(cb) {
2780
+ this._errorListeners.add(cb);
2781
+ return () => {
2782
+ this._errorListeners.delete(cb);
2783
+ };
2784
+ }
2718
2785
  /**
2719
2786
  * Força um sync imediato (I3 §10.3 `flush()`).
2720
2787
  * Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
@@ -2747,8 +2814,10 @@ var SyncEngine = class {
2747
2814
  }
2748
2815
  await this._phase2Pull();
2749
2816
  this._phase3Realtime();
2750
- this._updateState({ status: "idle", lastSyncedAt: Date.now() });
2817
+ this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
2751
2818
  } catch (err) {
2819
+ const msg = err instanceof Error ? err.message : String(err);
2820
+ this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
2752
2821
  this._updateState({ status: "idle" });
2753
2822
  } finally {
2754
2823
  this._syncing = false;
@@ -2783,6 +2852,9 @@ var SyncEngine = class {
2783
2852
  async _phase1Push() {
2784
2853
  const mutations = await this._queue.drain();
2785
2854
  if (mutations.length === 0) return true;
2855
+ if (this._isInBackoff(mutations[0])) {
2856
+ return true;
2857
+ }
2786
2858
  for (const mut of mutations) {
2787
2859
  await this._queue.markInflight(mut.mutationId);
2788
2860
  }
@@ -2790,10 +2862,16 @@ var SyncEngine = class {
2790
2862
  try {
2791
2863
  result = await this._transport.pushMutations(mutations);
2792
2864
  } catch (err) {
2793
- for (const mut of mutations) {
2794
- const errMsg = err instanceof Error ? err.message : String(err);
2795
- await this._queue.markRetry(mut.mutationId, errMsg);
2796
- }
2865
+ const errMsg = err instanceof Error ? err.message : String(err);
2866
+ const busChangesOnThrow = [];
2867
+ const [head, ...behind] = mutations;
2868
+ await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
2869
+ for (const mut of behind) {
2870
+ await this._queue.requeueInflight(mut.mutationId);
2871
+ }
2872
+ if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
2873
+ this._refreshPendingWrites().catch(() => {
2874
+ });
2797
2875
  return false;
2798
2876
  }
2799
2877
  const busChanges = [];
@@ -2820,8 +2898,16 @@ var SyncEngine = class {
2820
2898
  }
2821
2899
  const processedIds = new Set(result.results.map((r) => r.mutationId));
2822
2900
  const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
2823
- for (const mut of unprocessed) {
2824
- await this._queue.markRetry(mut.mutationId, "partial_push: muta\xE7\xE3o n\xE3o processada no lote");
2901
+ if (unprocessed.length > 0) {
2902
+ const [boundary, ...behind] = unprocessed;
2903
+ await this._retryOrDeadLetter(
2904
+ boundary,
2905
+ "partial_push: muta\xE7\xE3o n\xE3o processada no lote",
2906
+ busChanges
2907
+ );
2908
+ for (const mut of behind) {
2909
+ await this._queue.requeueInflight(mut.mutationId);
2910
+ }
2825
2911
  }
2826
2912
  if (busChanges.length > 0) {
2827
2913
  this._bus.emit(busChanges);
@@ -3013,6 +3099,67 @@ var SyncEngine = class {
3013
3099
  });
3014
3100
  }
3015
3101
  }
3102
+ // ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
3103
+ /**
3104
+ * Trata a falha transiente de UMA mutação que foi realmente tentada.
3105
+ *
3106
+ * - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
3107
+ * backoff, `attempts++`).
3108
+ * - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
3109
+ * `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
3110
+ * e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
3111
+ * `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
3112
+ * pending do doc no cache + registra `lastError`/dispara `onError`.
3113
+ */
3114
+ async _retryOrDeadLetter(mut, errMsg, busChanges) {
3115
+ if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
3116
+ await this._queue.markRetry(mut.mutationId, errMsg);
3117
+ return;
3118
+ }
3119
+ await this._queue.markFailed(mut.mutationId, errMsg);
3120
+ const conflictRecord = {
3121
+ collection: mut.collection,
3122
+ docId: mut.docId,
3123
+ mutationId: mut.mutationId,
3124
+ losingData: mut.payload ?? {},
3125
+ winningData: {},
3126
+ reason: "failed_max_attempts",
3127
+ detectedAt: Date.now(),
3128
+ delivered: false
3129
+ };
3130
+ await this._store.appendConflict(conflictRecord);
3131
+ const existing = await this._store.getDoc(mut.collection, mut.docId);
3132
+ if (existing) {
3133
+ await this._store.putDoc({
3134
+ ...existing,
3135
+ meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
3136
+ });
3137
+ busChanges.push({
3138
+ type: "modified",
3139
+ collection: mut.collection,
3140
+ doc: { id: mut.docId, data: existing.data }
3141
+ });
3142
+ }
3143
+ this._recordSyncError(
3144
+ "push",
3145
+ `muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
3146
+ );
3147
+ }
3148
+ /**
3149
+ * `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
3150
+ * desde a última tentativa falha. Só se aplica a mutações já tentadas
3151
+ * (`attempts > 0` e `lastAttemptAt` carimbado).
3152
+ */
3153
+ _isInBackoff(mut) {
3154
+ if (mut.attempts <= 0) return false;
3155
+ const last = mut.lastAttemptAt;
3156
+ if (typeof last !== "number") return false;
3157
+ return Date.now() < last + this._backoffDelayMs(mut.attempts);
3158
+ }
3159
+ /** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
3160
+ _backoffDelayMs(attempts) {
3161
+ return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
3162
+ }
3016
3163
  // ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
3017
3164
  /**
3018
3165
  * Busca mudanças do servidor e aplica ao cache via ConflictResolver.
@@ -3052,10 +3199,14 @@ var SyncEngine = class {
3052
3199
  }
3053
3200
  const busChanges = [];
3054
3201
  await this._applyServerDocs(resyncResult.docs, busChanges, true);
3202
+ const truncatedCollections = resyncResult.truncatedCollections ?? [];
3203
+ const truncatedSet = new Set(truncatedCollections);
3204
+ await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
3055
3205
  const returnedKeys = new Set(
3056
3206
  resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
3057
3207
  );
3058
3208
  for (const col of collections) {
3209
+ if (truncatedSet.has(col)) continue;
3059
3210
  const queryResult = await this._store.listDocs(col, {});
3060
3211
  for (const { id } of queryResult.docs) {
3061
3212
  const key = `${col}::${id}`;
@@ -3215,7 +3366,8 @@ var SyncEngine = class {
3215
3366
  status: isOnline ? "idle" : "offline",
3216
3367
  pendingWrites: 0,
3217
3368
  lastSyncedAt: null,
3218
- isLeaderTab: this._tabCoordinator.isLeader()
3369
+ isLeaderTab: this._tabCoordinator.isLeader(),
3370
+ lastError: null
3219
3371
  };
3220
3372
  }
3221
3373
  /**
@@ -3225,7 +3377,7 @@ var SyncEngine = class {
3225
3377
  _updateState(partial) {
3226
3378
  const prev = this._state;
3227
3379
  const next = { ...prev, ...partial };
3228
- const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
3380
+ const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab || (prev.lastError ?? null) !== (next.lastError ?? null);
3229
3381
  this._state = next;
3230
3382
  if (changed) {
3231
3383
  this._emitState(next);
@@ -3239,6 +3391,21 @@ var SyncEngine = class {
3239
3391
  }
3240
3392
  }
3241
3393
  }
3394
+ /**
3395
+ * P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
3396
+ * Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
3397
+ * dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
3398
+ */
3399
+ _recordSyncError(phase, message) {
3400
+ this._updateState({ lastError: message });
3401
+ const event = { phase, message, at: Date.now() };
3402
+ for (const listener of this._errorListeners) {
3403
+ try {
3404
+ listener({ ...event });
3405
+ } catch {
3406
+ }
3407
+ }
3408
+ }
3242
3409
  /**
3243
3410
  * Atualiza `pendingWrites` no estado.
3244
3411
  * Chamado de forma assíncrona quando necessário.
@@ -4048,7 +4215,9 @@ var DbCollectionRefImpl = class {
4048
4215
  const isOnline = this._isOnline();
4049
4216
  const syncState = this._engine.getSyncState();
4050
4217
  const nextCursor = buildNextCursor(result.docs, effectiveLimit);
4051
- const stale = !isOnline || syncState.status === "offline";
4218
+ const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
4219
+ const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
4220
+ const stale = !isOnline || syncState.status === "offline" || truncated;
4052
4221
  const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
4053
4222
  return {
4054
4223
  docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
@@ -4056,7 +4225,8 @@ var DbCollectionRefImpl = class {
4056
4225
  fromCache,
4057
4226
  stale,
4058
4227
  hasPendingWrites: hasPending,
4059
- changes: []
4228
+ changes: [],
4229
+ truncated
4060
4230
  };
4061
4231
  }
4062
4232
  // ── CRUD ────────────────────────────────────────────────────────────────────
@@ -4502,6 +4672,9 @@ var NeetruDbDocumentsImpl = class {
4502
4672
  async getConflicts() {
4503
4673
  return this._store.listConflicts();
4504
4674
  }
4675
+ onError(cb) {
4676
+ return this._engine.onError(cb);
4677
+ }
4505
4678
  };
4506
4679
  async function createOfflineDocumentsNamespace(opts) {
4507
4680
  const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
@@ -4977,6 +5150,7 @@ var defaultWebSocketFactory = (url) => {
4977
5150
  // src/db/client-db.ts
4978
5151
  var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
4979
5152
  var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
5153
+ var DOCUMENTS_RESYNC_PAGE_CAP = 200;
4980
5154
  function resolveTransport(transport, opts, config) {
4981
5155
  if (opts._transport) {
4982
5156
  return opts._transport;
@@ -5089,6 +5263,7 @@ function createRestSyncTransport(config) {
5089
5263
  const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
5090
5264
  const now = Date.now();
5091
5265
  const docs = [];
5266
+ const truncatedCollections = [];
5092
5267
  for (const collection of collections) {
5093
5268
  let raw;
5094
5269
  try {
@@ -5098,11 +5273,11 @@ function createRestSyncTransport(config) {
5098
5273
  requireAuth: true,
5099
5274
  retries: 2,
5100
5275
  headers: tenantHeaders,
5101
- // Teto do Core é 200. Coleções > 200 docs truncam silenciosamente
5102
- // aqui (paginação cursor é tarefa Core separada) — a detecção de
5103
- // deleção-por-ausência do SyncEngine NÃO deve rodar sobre truncamento,
5104
- // então mantemos o limite no máximo do servidor por ora.
5105
- query: { limit: 200 }
5276
+ // Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
5277
+ // TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
5278
+ // (a) NÃO rodar deleção-por-ausência e (b) sinalizar
5279
+ // `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
5280
+ query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
5106
5281
  });
5107
5282
  } catch (err) {
5108
5283
  const msg = err instanceof Error ? err.message : String(err);
@@ -5113,6 +5288,9 @@ function createRestSyncTransport(config) {
5113
5288
  }
5114
5289
  const resp = raw;
5115
5290
  const items = Array.isArray(resp?.items) ? resp.items : [];
5291
+ if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
5292
+ truncatedCollections.push(collection);
5293
+ }
5116
5294
  for (const item of items) {
5117
5295
  if (!item || typeof item !== "object") continue;
5118
5296
  const record = item;
@@ -5140,7 +5318,7 @@ function createRestSyncTransport(config) {
5140
5318
  });
5141
5319
  }
5142
5320
  }
5143
- return { docs, newWatermark: now };
5321
+ return { docs, newWatermark: now, truncatedCollections };
5144
5322
  }
5145
5323
  };
5146
5324
  }
@@ -5202,20 +5380,20 @@ function buildTicketProvider(config, opts) {
5202
5380
  return ticket;
5203
5381
  };
5204
5382
  }
5205
- var _hasWarnedMissingDbId = false;
5206
- function warnMissingDbIdOnce(config, dbOpts) {
5207
- if (_hasWarnedMissingDbId) return;
5208
- if (dbOpts.dbId) return;
5383
+ var _hasWarnedDbIdIgnored = false;
5384
+ function warnDbIdIgnoredOnce(config, dbOpts) {
5385
+ if (_hasWarnedDbIdIgnored) return;
5386
+ if (!dbOpts.dbId) return;
5209
5387
  if (config.env === "prod") return;
5210
5388
  if (typeof window === "undefined") return;
5211
5389
  if (typeof console === "undefined" || typeof console.warn !== "function") return;
5212
- _hasWarnedMissingDbId = true;
5390
+ _hasWarnedDbIdIgnored = true;
5213
5391
  console.warn(
5214
- "[neetru/sdk] client.db sem `dbId` expl\xEDcito: escritas client-side (add/set/update/remove/list) caem no datastore gen\xE9rico do Core, escopado por tenant/produto (header x-neetru-tenant) \u2014 `dbId` s\xF3 nomeia o cache offline LOCAL (IndexedDB), n\xE3o roteia nem escopa nada no servidor. Veja o JSDoc de NeetruDbOptions.dbId."
5392
+ '[neetru/sdk] client.db recebeu `dbId` ("' + dbOpts.dbId + '") mas ele N\xC3O roteia nem escopa nada no servidor: as escritas do mundo Documentos (add/set/update/remove/list) SEMPRE caem no datastore gen\xE9rico do Core, escopado s\xF3 por tenant/produto (header x-neetru-tenant). `dbId` apenas nomeia o cache offline LOCAL (IndexedDB). ATEN\xC7\xC3O: usar dbIds distintos no MESMO tenant/produto N\xC3O isola os dados no servidor \u2014 eles se misturam (co-mingling). Veja o JSDoc de NeetruDbOptions.dbId.'
5215
5393
  );
5216
5394
  }
5217
5395
  function createNeetruDb(config, dbOpts = {}) {
5218
- warnMissingDbIdOnce(config, dbOpts);
5396
+ warnDbIdIgnoredOnce(config, dbOpts);
5219
5397
  let resolvedTransport;
5220
5398
  if (dbOpts.transport !== void 0) {
5221
5399
  resolvedTransport = dbOpts.transport;
@@ -5293,6 +5471,20 @@ function createNeetruDb(config, dbOpts = {}) {
5293
5471
  async getConflicts() {
5294
5472
  const docs = await getDocsPromise();
5295
5473
  return docs.getConflicts();
5474
+ },
5475
+ onError(cb) {
5476
+ if (_docsResolved) return _docsResolved.onError(cb);
5477
+ let unsub = null;
5478
+ let cancelled = false;
5479
+ getDocsPromise().then((docs) => {
5480
+ if (!cancelled) {
5481
+ unsub = docs.onError(cb);
5482
+ }
5483
+ });
5484
+ return () => {
5485
+ cancelled = true;
5486
+ unsub?.();
5487
+ };
5296
5488
  }
5297
5489
  };
5298
5490
  }
@@ -7839,7 +8031,7 @@ function createNeetruClient(config = {}) {
7839
8031
  init_errors();
7840
8032
  init_db_errors();
7841
8033
  init_http();
7842
- var VERSION = "3.4.0";
8034
+ var VERSION = "3.4.1";
7843
8035
 
7844
8036
  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 };
7845
8037
  //# sourceMappingURL=index.mjs.map