@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/db.cjs CHANGED
@@ -683,13 +683,17 @@ var init_lease = __esm({
683
683
  // src/db/sql/sql-client.ts
684
684
  var sql_client_exports = {};
685
685
  __export(sql_client_exports, {
686
+ DEFAULT_POOL_CONNECT_TIMEOUT_MS: () => DEFAULT_POOL_CONNECT_TIMEOUT_MS,
687
+ DEFAULT_POOL_IDLE_MS: () => DEFAULT_POOL_IDLE_MS,
688
+ DEFAULT_POOL_MAX: () => DEFAULT_POOL_MAX,
686
689
  LEASE_ENDPOINT: () => LEASE_ENDPOINT,
687
690
  LEASE_RENEW_ENDPOINT: () => LEASE_RENEW_ENDPOINT,
688
691
  createDrizzleHandle: () => createDrizzleHandle,
689
692
  createHttpLeaseFetcher: () => createHttpLeaseFetcher,
690
693
  createPgPool: () => createPgPool,
691
694
  createSqlClientFromConfig: () => createSqlClientFromConfig,
692
- createSqlClientWithDeps: () => createSqlClientWithDeps
695
+ createSqlClientWithDeps: () => createSqlClientWithDeps,
696
+ resolvePoolTuning: () => resolvePoolTuning
693
697
  });
694
698
  function mapEnvToCore(sdkEnv) {
695
699
  if (sdkEnv === "workspace") return "staging";
@@ -778,6 +782,30 @@ function parseLease(raw) {
778
782
  expiresAt: req("expiresAt", "string")
779
783
  };
780
784
  }
785
+ function readEnvInt(name, def, min, max) {
786
+ const raw = typeof process !== "undefined" ? process.env?.[name] : void 0;
787
+ if (raw === void 0 || raw.trim() === "") return def;
788
+ const n = Number.parseInt(raw.trim(), 10);
789
+ if (!Number.isFinite(n) || n < min || n > max) return def;
790
+ return n;
791
+ }
792
+ function resolvePoolTuning() {
793
+ return {
794
+ // Teto 100: bem abaixo do `max_client_conn = 500` do PgBouncer, com folga
795
+ // para múltiplas instâncias do app dividirem o mesmo front-door.
796
+ max: readEnvInt("NEETRU_DB_POOL_MAX", DEFAULT_POOL_MAX, 1, 100),
797
+ // Teto 15min = janela do cert; piso 1s.
798
+ idleTimeoutMillis: readEnvInt("NEETRU_DB_POOL_IDLE_MS", DEFAULT_POOL_IDLE_MS, 1e3, 9e5),
799
+ connectionTimeoutMillis: readEnvInt(
800
+ "NEETRU_DB_POOL_CONNECT_TIMEOUT_MS",
801
+ DEFAULT_POOL_CONNECT_TIMEOUT_MS,
802
+ 1e3,
803
+ 12e4
804
+ ),
805
+ keepAlive: true,
806
+ keepAliveInitialDelayMillis: 1e4
807
+ };
808
+ }
781
809
  async function createPgPool(lease) {
782
810
  const { Pool } = await import(
783
811
  /* webpackIgnore: true */
@@ -796,9 +824,7 @@ async function createPgPool(lease) {
796
824
  user: lease.user,
797
825
  password: lease.password,
798
826
  ssl,
799
- max: 3,
800
- idleTimeoutMillis: 3e4,
801
- connectionTimeoutMillis: 1e4
827
+ ...resolvePoolTuning()
802
828
  });
803
829
  }
804
830
  async function createDrizzleHandle(pool, schema) {
@@ -933,9 +959,9 @@ async function createSqlClientFromConfig(config, schema, options) {
933
959
  user: lease.user,
934
960
  password: lease.password,
935
961
  ssl,
936
- max: 3,
937
- idleTimeoutMillis: 3e4,
938
- connectionTimeoutMillis: 1e4
962
+ // Tuning centralizado (env-tunável) — ver resolvePoolTuning + auditoria
963
+ // docs/_review/AUDITORIA_DB_CONN_PERF_2026-09-06.md no Core.
964
+ ...resolvePoolTuning()
939
965
  });
940
966
  };
941
967
  const createDrizzle = (pool, s) => {
@@ -958,7 +984,7 @@ async function createSqlClientFromConfig(config, schema, options) {
958
984
  options
959
985
  );
960
986
  }
961
- var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
987
+ var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT, DEFAULT_POOL_MAX, DEFAULT_POOL_IDLE_MS, DEFAULT_POOL_CONNECT_TIMEOUT_MS;
962
988
  var init_sql_client = __esm({
963
989
  "src/db/sql/sql-client.ts"() {
964
990
  init_db_errors();
@@ -967,6 +993,9 @@ var init_sql_client = __esm({
967
993
  init_resilient_pool();
968
994
  LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
969
995
  LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
996
+ DEFAULT_POOL_MAX = 10;
997
+ DEFAULT_POOL_IDLE_MS = 6e4;
998
+ DEFAULT_POOL_CONNECT_TIMEOUT_MS = 1e4;
970
999
  }
971
1000
  });
972
1001
 
@@ -1736,8 +1765,10 @@ var MutationQueue = class {
1736
1765
  await this.store.deleteMutation(mutationId);
1737
1766
  }
1738
1767
  /**
1739
- * Marca uma mutação como `failed` e registra o erro.
1740
- * Incrementa `attempts`. Chamado pelo SyncEngine em falha permanente.
1768
+ * Marca uma mutação como `failed` (dead-letter) e registra o erro.
1769
+ * Incrementa `attempts` e carimba `lastAttemptAt`. Chamado pelo SyncEngine
1770
+ * quando a mutação esgota o teto de tentativas (mutação-veneno): sai do
1771
+ * conjunto `queued`, então `drain()` para de devolvê-la e a fila DESTRAVA.
1741
1772
  */
1742
1773
  async markFailed(mutationId, error) {
1743
1774
  const mut = await this.store.getMutation(mutationId);
@@ -1746,12 +1777,14 @@ var MutationQueue = class {
1746
1777
  ...mut,
1747
1778
  status: "failed",
1748
1779
  lastError: error,
1749
- attempts: mut.attempts + 1
1780
+ attempts: mut.attempts + 1,
1781
+ lastAttemptAt: Date.now()
1750
1782
  });
1751
1783
  }
1752
1784
  /**
1753
- * Incrementa `attempts` e volta para `queued` (falha transiente com backoff).
1754
- * Chamado pelo SyncEngine em falha transiente (5xx, timeout).
1785
+ * Incrementa `attempts`, carimba `lastAttemptAt` e volta para `queued`
1786
+ * (falha transiente com backoff). Chamado pelo SyncEngine em falha
1787
+ * transiente (5xx, timeout) da mutação que REALMENTE foi tentada.
1755
1788
  */
1756
1789
  async markRetry(mutationId, error) {
1757
1790
  const mut = await this.store.getMutation(mutationId);
@@ -1760,9 +1793,24 @@ var MutationQueue = class {
1760
1793
  ...mut,
1761
1794
  status: "queued",
1762
1795
  lastError: error,
1763
- attempts: mut.attempts + 1
1796
+ attempts: mut.attempts + 1,
1797
+ lastAttemptAt: Date.now()
1764
1798
  });
1765
1799
  }
1800
+ /**
1801
+ * Reverte uma mutação `inflight` de volta a `queued` SEM tocar em `attempts`
1802
+ * nem `lastAttemptAt`. Usado para mutações que foram marcadas inflight mas
1803
+ * NUNCA chegaram a ser enviadas (estavam atrás da head no FIFO e o push
1804
+ * parou antes delas): contá-las como tentativa as empurraria pro dead-letter
1805
+ * por engano, e adiaria seu envio via backoff sem que tenham falhado.
1806
+ * No-op se a mutação não está mais `inflight`.
1807
+ */
1808
+ async requeueInflight(mutationId) {
1809
+ const mut = await this.store.getMutation(mutationId);
1810
+ if (!mut) return;
1811
+ if (mut.status !== "inflight") return;
1812
+ await this.store.putMutation({ ...mut, status: "queued" });
1813
+ }
1766
1814
  };
1767
1815
 
1768
1816
  // src/db/field-value.ts
@@ -1818,6 +1866,10 @@ function hasFieldSentinel(data) {
1818
1866
  }
1819
1867
 
1820
1868
  // src/db/offline/sync-engine.ts
1869
+ var MAX_MUTATION_ATTEMPTS = 8;
1870
+ var BACKOFF_BASE_MS = 1e3;
1871
+ var BACKOFF_CAP_MS = 5 * 60 * 1e3;
1872
+ var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
1821
1873
  function hasSentinels(payload) {
1822
1874
  return payload !== null && hasFieldSentinel(payload);
1823
1875
  }
@@ -1852,6 +1904,8 @@ var SyncEngine = class {
1852
1904
  _state;
1853
1905
  /** Listeners de mudança de SyncState. */
1854
1906
  _stateListeners = /* @__PURE__ */ new Set();
1907
+ /** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
1908
+ _errorListeners = /* @__PURE__ */ new Set();
1855
1909
  /** Cleanup functions dos listeners externos. */
1856
1910
  _cleanups = [];
1857
1911
  /** Timer de resync periódico. */
@@ -1910,6 +1964,19 @@ var SyncEngine = class {
1910
1964
  this._stateListeners.delete(cb);
1911
1965
  };
1912
1966
  }
1967
+ /**
1968
+ * Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
1969
+ * (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
1970
+ * mutação-veneno) — o que antes era engolido com "log silencioso".
1971
+ * O mesmo diagnóstico também aparece em `SyncState.lastError`.
1972
+ * Retorna uma função de unsubscribe.
1973
+ */
1974
+ onError(cb) {
1975
+ this._errorListeners.add(cb);
1976
+ return () => {
1977
+ this._errorListeners.delete(cb);
1978
+ };
1979
+ }
1913
1980
  /**
1914
1981
  * Força um sync imediato (I3 §10.3 `flush()`).
1915
1982
  * Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
@@ -1942,8 +2009,10 @@ var SyncEngine = class {
1942
2009
  }
1943
2010
  await this._phase2Pull();
1944
2011
  this._phase3Realtime();
1945
- this._updateState({ status: "idle", lastSyncedAt: Date.now() });
2012
+ this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
1946
2013
  } catch (err) {
2014
+ const msg = err instanceof Error ? err.message : String(err);
2015
+ this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
1947
2016
  this._updateState({ status: "idle" });
1948
2017
  } finally {
1949
2018
  this._syncing = false;
@@ -1978,6 +2047,9 @@ var SyncEngine = class {
1978
2047
  async _phase1Push() {
1979
2048
  const mutations = await this._queue.drain();
1980
2049
  if (mutations.length === 0) return true;
2050
+ if (this._isInBackoff(mutations[0])) {
2051
+ return true;
2052
+ }
1981
2053
  for (const mut of mutations) {
1982
2054
  await this._queue.markInflight(mut.mutationId);
1983
2055
  }
@@ -1985,10 +2057,16 @@ var SyncEngine = class {
1985
2057
  try {
1986
2058
  result = await this._transport.pushMutations(mutations);
1987
2059
  } catch (err) {
1988
- for (const mut of mutations) {
1989
- const errMsg = err instanceof Error ? err.message : String(err);
1990
- await this._queue.markRetry(mut.mutationId, errMsg);
2060
+ const errMsg = err instanceof Error ? err.message : String(err);
2061
+ const busChangesOnThrow = [];
2062
+ const [head, ...behind] = mutations;
2063
+ await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
2064
+ for (const mut of behind) {
2065
+ await this._queue.requeueInflight(mut.mutationId);
1991
2066
  }
2067
+ if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
2068
+ this._refreshPendingWrites().catch(() => {
2069
+ });
1992
2070
  return false;
1993
2071
  }
1994
2072
  const busChanges = [];
@@ -2015,8 +2093,16 @@ var SyncEngine = class {
2015
2093
  }
2016
2094
  const processedIds = new Set(result.results.map((r) => r.mutationId));
2017
2095
  const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
2018
- for (const mut of unprocessed) {
2019
- await this._queue.markRetry(mut.mutationId, "partial_push: muta\xE7\xE3o n\xE3o processada no lote");
2096
+ if (unprocessed.length > 0) {
2097
+ const [boundary, ...behind] = unprocessed;
2098
+ await this._retryOrDeadLetter(
2099
+ boundary,
2100
+ "partial_push: muta\xE7\xE3o n\xE3o processada no lote",
2101
+ busChanges
2102
+ );
2103
+ for (const mut of behind) {
2104
+ await this._queue.requeueInflight(mut.mutationId);
2105
+ }
2020
2106
  }
2021
2107
  if (busChanges.length > 0) {
2022
2108
  this._bus.emit(busChanges);
@@ -2208,6 +2294,67 @@ var SyncEngine = class {
2208
2294
  });
2209
2295
  }
2210
2296
  }
2297
+ // ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
2298
+ /**
2299
+ * Trata a falha transiente de UMA mutação que foi realmente tentada.
2300
+ *
2301
+ * - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
2302
+ * backoff, `attempts++`).
2303
+ * - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
2304
+ * `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
2305
+ * e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
2306
+ * `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
2307
+ * pending do doc no cache + registra `lastError`/dispara `onError`.
2308
+ */
2309
+ async _retryOrDeadLetter(mut, errMsg, busChanges) {
2310
+ if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
2311
+ await this._queue.markRetry(mut.mutationId, errMsg);
2312
+ return;
2313
+ }
2314
+ await this._queue.markFailed(mut.mutationId, errMsg);
2315
+ const conflictRecord = {
2316
+ collection: mut.collection,
2317
+ docId: mut.docId,
2318
+ mutationId: mut.mutationId,
2319
+ losingData: mut.payload ?? {},
2320
+ winningData: {},
2321
+ reason: "failed_max_attempts",
2322
+ detectedAt: Date.now(),
2323
+ delivered: false
2324
+ };
2325
+ await this._store.appendConflict(conflictRecord);
2326
+ const existing = await this._store.getDoc(mut.collection, mut.docId);
2327
+ if (existing) {
2328
+ await this._store.putDoc({
2329
+ ...existing,
2330
+ meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
2331
+ });
2332
+ busChanges.push({
2333
+ type: "modified",
2334
+ collection: mut.collection,
2335
+ doc: { id: mut.docId, data: existing.data }
2336
+ });
2337
+ }
2338
+ this._recordSyncError(
2339
+ "push",
2340
+ `muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
2341
+ );
2342
+ }
2343
+ /**
2344
+ * `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
2345
+ * desde a última tentativa falha. Só se aplica a mutações já tentadas
2346
+ * (`attempts > 0` e `lastAttemptAt` carimbado).
2347
+ */
2348
+ _isInBackoff(mut) {
2349
+ if (mut.attempts <= 0) return false;
2350
+ const last = mut.lastAttemptAt;
2351
+ if (typeof last !== "number") return false;
2352
+ return Date.now() < last + this._backoffDelayMs(mut.attempts);
2353
+ }
2354
+ /** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
2355
+ _backoffDelayMs(attempts) {
2356
+ return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
2357
+ }
2211
2358
  // ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
2212
2359
  /**
2213
2360
  * Busca mudanças do servidor e aplica ao cache via ConflictResolver.
@@ -2247,10 +2394,14 @@ var SyncEngine = class {
2247
2394
  }
2248
2395
  const busChanges = [];
2249
2396
  await this._applyServerDocs(resyncResult.docs, busChanges, true);
2397
+ const truncatedCollections = resyncResult.truncatedCollections ?? [];
2398
+ const truncatedSet = new Set(truncatedCollections);
2399
+ await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
2250
2400
  const returnedKeys = new Set(
2251
2401
  resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
2252
2402
  );
2253
2403
  for (const col of collections) {
2404
+ if (truncatedSet.has(col)) continue;
2254
2405
  const queryResult = await this._store.listDocs(col, {});
2255
2406
  for (const { id } of queryResult.docs) {
2256
2407
  const key = `${col}::${id}`;
@@ -2410,7 +2561,8 @@ var SyncEngine = class {
2410
2561
  status: isOnline ? "idle" : "offline",
2411
2562
  pendingWrites: 0,
2412
2563
  lastSyncedAt: null,
2413
- isLeaderTab: this._tabCoordinator.isLeader()
2564
+ isLeaderTab: this._tabCoordinator.isLeader(),
2565
+ lastError: null
2414
2566
  };
2415
2567
  }
2416
2568
  /**
@@ -2420,7 +2572,7 @@ var SyncEngine = class {
2420
2572
  _updateState(partial) {
2421
2573
  const prev = this._state;
2422
2574
  const next = { ...prev, ...partial };
2423
- const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
2575
+ const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab || (prev.lastError ?? null) !== (next.lastError ?? null);
2424
2576
  this._state = next;
2425
2577
  if (changed) {
2426
2578
  this._emitState(next);
@@ -2434,6 +2586,21 @@ var SyncEngine = class {
2434
2586
  }
2435
2587
  }
2436
2588
  }
2589
+ /**
2590
+ * P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
2591
+ * Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
2592
+ * dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
2593
+ */
2594
+ _recordSyncError(phase, message) {
2595
+ this._updateState({ lastError: message });
2596
+ const event = { phase, message, at: Date.now() };
2597
+ for (const listener of this._errorListeners) {
2598
+ try {
2599
+ listener({ ...event });
2600
+ } catch {
2601
+ }
2602
+ }
2603
+ }
2437
2604
  /**
2438
2605
  * Atualiza `pendingWrites` no estado.
2439
2606
  * Chamado de forma assíncrona quando necessário.
@@ -3243,7 +3410,9 @@ var DbCollectionRefImpl = class {
3243
3410
  const isOnline = this._isOnline();
3244
3411
  const syncState = this._engine.getSyncState();
3245
3412
  const nextCursor = buildNextCursor(result.docs, effectiveLimit);
3246
- const stale = !isOnline || syncState.status === "offline";
3413
+ const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
3414
+ const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
3415
+ const stale = !isOnline || syncState.status === "offline" || truncated;
3247
3416
  const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
3248
3417
  return {
3249
3418
  docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
@@ -3251,7 +3420,8 @@ var DbCollectionRefImpl = class {
3251
3420
  fromCache,
3252
3421
  stale,
3253
3422
  hasPendingWrites: hasPending,
3254
- changes: []
3423
+ changes: [],
3424
+ truncated
3255
3425
  };
3256
3426
  }
3257
3427
  // ── CRUD ────────────────────────────────────────────────────────────────────
@@ -3697,6 +3867,9 @@ var NeetruDbDocumentsImpl = class {
3697
3867
  async getConflicts() {
3698
3868
  return this._store.listConflicts();
3699
3869
  }
3870
+ onError(cb) {
3871
+ return this._engine.onError(cb);
3872
+ }
3700
3873
  };
3701
3874
  async function createOfflineDocumentsNamespace(opts) {
3702
3875
  const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
@@ -4173,6 +4346,7 @@ var defaultWebSocketFactory = (url) => {
4173
4346
  init_db_errors();
4174
4347
  var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
4175
4348
  var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
4349
+ var DOCUMENTS_RESYNC_PAGE_CAP = 200;
4176
4350
  function resolveTransport(transport, opts, config) {
4177
4351
  if (opts._transport) {
4178
4352
  return opts._transport;
@@ -4285,6 +4459,7 @@ function createRestSyncTransport(config) {
4285
4459
  const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
4286
4460
  const now = Date.now();
4287
4461
  const docs = [];
4462
+ const truncatedCollections = [];
4288
4463
  for (const collection of collections) {
4289
4464
  let raw;
4290
4465
  try {
@@ -4294,11 +4469,11 @@ function createRestSyncTransport(config) {
4294
4469
  requireAuth: true,
4295
4470
  retries: 2,
4296
4471
  headers: tenantHeaders,
4297
- // Teto do Core é 200. Coleções > 200 docs truncam silenciosamente
4298
- // aqui (paginação cursor é tarefa Core separada) — a detecção de
4299
- // deleção-por-ausência do SyncEngine NÃO deve rodar sobre truncamento,
4300
- // então mantemos o limite no máximo do servidor por ora.
4301
- query: { limit: 200 }
4472
+ // Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
4473
+ // TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
4474
+ // (a) NÃO rodar deleção-por-ausência e (b) sinalizar
4475
+ // `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
4476
+ query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
4302
4477
  });
4303
4478
  } catch (err) {
4304
4479
  const msg = err instanceof Error ? err.message : String(err);
@@ -4309,6 +4484,9 @@ function createRestSyncTransport(config) {
4309
4484
  }
4310
4485
  const resp = raw;
4311
4486
  const items = Array.isArray(resp?.items) ? resp.items : [];
4487
+ if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
4488
+ truncatedCollections.push(collection);
4489
+ }
4312
4490
  for (const item of items) {
4313
4491
  if (!item || typeof item !== "object") continue;
4314
4492
  const record = item;
@@ -4336,7 +4514,7 @@ function createRestSyncTransport(config) {
4336
4514
  });
4337
4515
  }
4338
4516
  }
4339
- return { docs, newWatermark: now };
4517
+ return { docs, newWatermark: now, truncatedCollections };
4340
4518
  }
4341
4519
  };
4342
4520
  }
@@ -4401,23 +4579,23 @@ function buildTicketProvider(config, opts) {
4401
4579
  function getWebSocketRealtimeClient(gatewayUrl, ticketProvider, dbId, wsFactory) {
4402
4580
  return new NeetruRealtimeClient({ gatewayUrl, ticketProvider, dbId, webSocketFactory: wsFactory });
4403
4581
  }
4404
- var _hasWarnedMissingDbId = false;
4582
+ var _hasWarnedDbIdIgnored = false;
4405
4583
  function __resetDbIdWarningForTests() {
4406
- _hasWarnedMissingDbId = false;
4584
+ _hasWarnedDbIdIgnored = false;
4407
4585
  }
4408
- function warnMissingDbIdOnce(config, dbOpts) {
4409
- if (_hasWarnedMissingDbId) return;
4410
- if (dbOpts.dbId) return;
4586
+ function warnDbIdIgnoredOnce(config, dbOpts) {
4587
+ if (_hasWarnedDbIdIgnored) return;
4588
+ if (!dbOpts.dbId) return;
4411
4589
  if (config.env === "prod") return;
4412
4590
  if (typeof window === "undefined") return;
4413
4591
  if (typeof console === "undefined" || typeof console.warn !== "function") return;
4414
- _hasWarnedMissingDbId = true;
4592
+ _hasWarnedDbIdIgnored = true;
4415
4593
  console.warn(
4416
- "[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."
4594
+ '[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.'
4417
4595
  );
4418
4596
  }
4419
4597
  function createNeetruDb(config, dbOpts = {}) {
4420
- warnMissingDbIdOnce(config, dbOpts);
4598
+ warnDbIdIgnoredOnce(config, dbOpts);
4421
4599
  let resolvedTransport;
4422
4600
  if (dbOpts.transport !== void 0) {
4423
4601
  resolvedTransport = dbOpts.transport;
@@ -4495,6 +4673,20 @@ function createNeetruDb(config, dbOpts = {}) {
4495
4673
  async getConflicts() {
4496
4674
  const docs = await getDocsPromise();
4497
4675
  return docs.getConflicts();
4676
+ },
4677
+ onError(cb) {
4678
+ if (_docsResolved) return _docsResolved.onError(cb);
4679
+ let unsub = null;
4680
+ let cancelled = false;
4681
+ getDocsPromise().then((docs) => {
4682
+ if (!cancelled) {
4683
+ unsub = docs.onError(cb);
4684
+ }
4685
+ });
4686
+ return () => {
4687
+ cancelled = true;
4688
+ unsub?.();
4689
+ };
4498
4690
  }
4499
4691
  };
4500
4692
  }