@neetru/sdk 3.4.0 → 3.4.1

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 (48) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/dist/ai.d.cts +1 -1
  3. package/dist/ai.d.ts +1 -1
  4. package/dist/auth.cjs +193 -30
  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 +193 -30
  9. package/dist/auth.mjs.map +1 -1
  10. package/dist/catalog.d.cts +1 -1
  11. package/dist/catalog.d.ts +1 -1
  12. package/dist/checkout.d.cts +1 -1
  13. package/dist/checkout.d.ts +1 -1
  14. package/dist/db-react.d.cts +1 -1
  15. package/dist/db-react.d.ts +1 -1
  16. package/dist/db.cjs +193 -30
  17. package/dist/db.cjs.map +1 -1
  18. package/dist/db.d.cts +1 -1
  19. package/dist/db.d.ts +1 -1
  20. package/dist/db.mjs +193 -30
  21. package/dist/db.mjs.map +1 -1
  22. package/dist/entitlements.d.cts +1 -1
  23. package/dist/entitlements.d.ts +1 -1
  24. package/dist/firestore-compat.d.cts +1 -1
  25. package/dist/firestore-compat.d.ts +1 -1
  26. package/dist/index.cjs +194 -31
  27. package/dist/index.cjs.map +1 -1
  28. package/dist/index.d.cts +2 -2
  29. package/dist/index.d.ts +2 -2
  30. package/dist/index.mjs +194 -31
  31. package/dist/index.mjs.map +1 -1
  32. package/dist/mocks.d.cts +1 -1
  33. package/dist/mocks.d.ts +1 -1
  34. package/dist/notifications.d.cts +1 -1
  35. package/dist/notifications.d.ts +1 -1
  36. package/dist/react.d.cts +1 -1
  37. package/dist/react.d.ts +1 -1
  38. package/dist/support.d.cts +1 -1
  39. package/dist/support.d.ts +1 -1
  40. package/dist/telemetry.d.cts +1 -1
  41. package/dist/telemetry.d.ts +1 -1
  42. package/dist/{types-DdJyqBy-.d.ts → types-BlOLDMR0.d.ts} +79 -6
  43. package/dist/{types-BoFSCHF_.d.cts → types-D-QdG502.d.cts} +79 -6
  44. package/dist/usage.d.cts +1 -1
  45. package/dist/usage.d.ts +1 -1
  46. package/dist/webhooks.d.cts +1 -1
  47. package/dist/webhooks.d.ts +1 -1
  48. package/package.json +1 -1
package/dist/auth.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { X as NeetruClientConfig, W as NeetruClient } from './types-BoFSCHF_.cjs';
1
+ import { X as NeetruClientConfig, W as NeetruClient } from './types-D-QdG502.cjs';
2
2
  import 'drizzle-orm/node-postgres';
3
3
  import './errors.cjs';
4
4
 
package/dist/auth.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { X as NeetruClientConfig, W as NeetruClient } from './types-DdJyqBy-.js';
1
+ import { X as NeetruClientConfig, W as NeetruClient } from './types-BlOLDMR0.js';
2
2
  import 'drizzle-orm/node-postgres';
3
3
  import './errors.js';
4
4
 
package/dist/auth.mjs CHANGED
@@ -2541,8 +2541,10 @@ var MutationQueue = class {
2541
2541
  await this.store.deleteMutation(mutationId);
2542
2542
  }
2543
2543
  /**
2544
- * Marca uma mutação como `failed` e registra o erro.
2545
- * Incrementa `attempts`. Chamado pelo SyncEngine em falha permanente.
2544
+ * Marca uma mutação como `failed` (dead-letter) e registra o erro.
2545
+ * Incrementa `attempts` e carimba `lastAttemptAt`. Chamado pelo SyncEngine
2546
+ * quando a mutação esgota o teto de tentativas (mutação-veneno): sai do
2547
+ * conjunto `queued`, então `drain()` para de devolvê-la e a fila DESTRAVA.
2546
2548
  */
2547
2549
  async markFailed(mutationId, error) {
2548
2550
  const mut = await this.store.getMutation(mutationId);
@@ -2551,12 +2553,14 @@ var MutationQueue = class {
2551
2553
  ...mut,
2552
2554
  status: "failed",
2553
2555
  lastError: error,
2554
- attempts: mut.attempts + 1
2556
+ attempts: mut.attempts + 1,
2557
+ lastAttemptAt: Date.now()
2555
2558
  });
2556
2559
  }
2557
2560
  /**
2558
- * Incrementa `attempts` e volta para `queued` (falha transiente com backoff).
2559
- * Chamado pelo SyncEngine em falha transiente (5xx, timeout).
2561
+ * Incrementa `attempts`, carimba `lastAttemptAt` e volta para `queued`
2562
+ * (falha transiente com backoff). Chamado pelo SyncEngine em falha
2563
+ * transiente (5xx, timeout) da mutação que REALMENTE foi tentada.
2560
2564
  */
2561
2565
  async markRetry(mutationId, error) {
2562
2566
  const mut = await this.store.getMutation(mutationId);
@@ -2565,9 +2569,24 @@ var MutationQueue = class {
2565
2569
  ...mut,
2566
2570
  status: "queued",
2567
2571
  lastError: error,
2568
- attempts: mut.attempts + 1
2572
+ attempts: mut.attempts + 1,
2573
+ lastAttemptAt: Date.now()
2569
2574
  });
2570
2575
  }
2576
+ /**
2577
+ * Reverte uma mutação `inflight` de volta a `queued` SEM tocar em `attempts`
2578
+ * nem `lastAttemptAt`. Usado para mutações que foram marcadas inflight mas
2579
+ * NUNCA chegaram a ser enviadas (estavam atrás da head no FIFO e o push
2580
+ * parou antes delas): contá-las como tentativa as empurraria pro dead-letter
2581
+ * por engano, e adiaria seu envio via backoff sem que tenham falhado.
2582
+ * No-op se a mutação não está mais `inflight`.
2583
+ */
2584
+ async requeueInflight(mutationId) {
2585
+ const mut = await this.store.getMutation(mutationId);
2586
+ if (!mut) return;
2587
+ if (mut.status !== "inflight") return;
2588
+ await this.store.putMutation({ ...mut, status: "queued" });
2589
+ }
2571
2590
  };
2572
2591
 
2573
2592
  // src/db/field-value.ts
@@ -2612,6 +2631,10 @@ function hasFieldSentinel(data) {
2612
2631
  }
2613
2632
 
2614
2633
  // src/db/offline/sync-engine.ts
2634
+ var MAX_MUTATION_ATTEMPTS = 8;
2635
+ var BACKOFF_BASE_MS = 1e3;
2636
+ var BACKOFF_CAP_MS = 5 * 60 * 1e3;
2637
+ var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
2615
2638
  function hasSentinels(payload) {
2616
2639
  return payload !== null && hasFieldSentinel(payload);
2617
2640
  }
@@ -2646,6 +2669,8 @@ var SyncEngine = class {
2646
2669
  _state;
2647
2670
  /** Listeners de mudança de SyncState. */
2648
2671
  _stateListeners = /* @__PURE__ */ new Set();
2672
+ /** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
2673
+ _errorListeners = /* @__PURE__ */ new Set();
2649
2674
  /** Cleanup functions dos listeners externos. */
2650
2675
  _cleanups = [];
2651
2676
  /** Timer de resync periódico. */
@@ -2704,6 +2729,19 @@ var SyncEngine = class {
2704
2729
  this._stateListeners.delete(cb);
2705
2730
  };
2706
2731
  }
2732
+ /**
2733
+ * Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
2734
+ * (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
2735
+ * mutação-veneno) — o que antes era engolido com "log silencioso".
2736
+ * O mesmo diagnóstico também aparece em `SyncState.lastError`.
2737
+ * Retorna uma função de unsubscribe.
2738
+ */
2739
+ onError(cb) {
2740
+ this._errorListeners.add(cb);
2741
+ return () => {
2742
+ this._errorListeners.delete(cb);
2743
+ };
2744
+ }
2707
2745
  /**
2708
2746
  * Força um sync imediato (I3 §10.3 `flush()`).
2709
2747
  * Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
@@ -2736,8 +2774,10 @@ var SyncEngine = class {
2736
2774
  }
2737
2775
  await this._phase2Pull();
2738
2776
  this._phase3Realtime();
2739
- this._updateState({ status: "idle", lastSyncedAt: Date.now() });
2777
+ this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
2740
2778
  } catch (err) {
2779
+ const msg = err instanceof Error ? err.message : String(err);
2780
+ this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
2741
2781
  this._updateState({ status: "idle" });
2742
2782
  } finally {
2743
2783
  this._syncing = false;
@@ -2772,6 +2812,9 @@ var SyncEngine = class {
2772
2812
  async _phase1Push() {
2773
2813
  const mutations = await this._queue.drain();
2774
2814
  if (mutations.length === 0) return true;
2815
+ if (this._isInBackoff(mutations[0])) {
2816
+ return true;
2817
+ }
2775
2818
  for (const mut of mutations) {
2776
2819
  await this._queue.markInflight(mut.mutationId);
2777
2820
  }
@@ -2779,10 +2822,16 @@ var SyncEngine = class {
2779
2822
  try {
2780
2823
  result = await this._transport.pushMutations(mutations);
2781
2824
  } catch (err) {
2782
- for (const mut of mutations) {
2783
- const errMsg = err instanceof Error ? err.message : String(err);
2784
- await this._queue.markRetry(mut.mutationId, errMsg);
2785
- }
2825
+ const errMsg = err instanceof Error ? err.message : String(err);
2826
+ const busChangesOnThrow = [];
2827
+ const [head, ...behind] = mutations;
2828
+ await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
2829
+ for (const mut of behind) {
2830
+ await this._queue.requeueInflight(mut.mutationId);
2831
+ }
2832
+ if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
2833
+ this._refreshPendingWrites().catch(() => {
2834
+ });
2786
2835
  return false;
2787
2836
  }
2788
2837
  const busChanges = [];
@@ -2809,8 +2858,16 @@ var SyncEngine = class {
2809
2858
  }
2810
2859
  const processedIds = new Set(result.results.map((r) => r.mutationId));
2811
2860
  const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
2812
- for (const mut of unprocessed) {
2813
- await this._queue.markRetry(mut.mutationId, "partial_push: muta\xE7\xE3o n\xE3o processada no lote");
2861
+ if (unprocessed.length > 0) {
2862
+ const [boundary, ...behind] = unprocessed;
2863
+ await this._retryOrDeadLetter(
2864
+ boundary,
2865
+ "partial_push: muta\xE7\xE3o n\xE3o processada no lote",
2866
+ busChanges
2867
+ );
2868
+ for (const mut of behind) {
2869
+ await this._queue.requeueInflight(mut.mutationId);
2870
+ }
2814
2871
  }
2815
2872
  if (busChanges.length > 0) {
2816
2873
  this._bus.emit(busChanges);
@@ -3002,6 +3059,67 @@ var SyncEngine = class {
3002
3059
  });
3003
3060
  }
3004
3061
  }
3062
+ // ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
3063
+ /**
3064
+ * Trata a falha transiente de UMA mutação que foi realmente tentada.
3065
+ *
3066
+ * - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
3067
+ * backoff, `attempts++`).
3068
+ * - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
3069
+ * `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
3070
+ * e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
3071
+ * `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
3072
+ * pending do doc no cache + registra `lastError`/dispara `onError`.
3073
+ */
3074
+ async _retryOrDeadLetter(mut, errMsg, busChanges) {
3075
+ if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
3076
+ await this._queue.markRetry(mut.mutationId, errMsg);
3077
+ return;
3078
+ }
3079
+ await this._queue.markFailed(mut.mutationId, errMsg);
3080
+ const conflictRecord = {
3081
+ collection: mut.collection,
3082
+ docId: mut.docId,
3083
+ mutationId: mut.mutationId,
3084
+ losingData: mut.payload ?? {},
3085
+ winningData: {},
3086
+ reason: "failed_max_attempts",
3087
+ detectedAt: Date.now(),
3088
+ delivered: false
3089
+ };
3090
+ await this._store.appendConflict(conflictRecord);
3091
+ const existing = await this._store.getDoc(mut.collection, mut.docId);
3092
+ if (existing) {
3093
+ await this._store.putDoc({
3094
+ ...existing,
3095
+ meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
3096
+ });
3097
+ busChanges.push({
3098
+ type: "modified",
3099
+ collection: mut.collection,
3100
+ doc: { id: mut.docId, data: existing.data }
3101
+ });
3102
+ }
3103
+ this._recordSyncError(
3104
+ "push",
3105
+ `muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
3106
+ );
3107
+ }
3108
+ /**
3109
+ * `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
3110
+ * desde a última tentativa falha. Só se aplica a mutações já tentadas
3111
+ * (`attempts > 0` e `lastAttemptAt` carimbado).
3112
+ */
3113
+ _isInBackoff(mut) {
3114
+ if (mut.attempts <= 0) return false;
3115
+ const last = mut.lastAttemptAt;
3116
+ if (typeof last !== "number") return false;
3117
+ return Date.now() < last + this._backoffDelayMs(mut.attempts);
3118
+ }
3119
+ /** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
3120
+ _backoffDelayMs(attempts) {
3121
+ return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
3122
+ }
3005
3123
  // ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
3006
3124
  /**
3007
3125
  * Busca mudanças do servidor e aplica ao cache via ConflictResolver.
@@ -3041,10 +3159,14 @@ var SyncEngine = class {
3041
3159
  }
3042
3160
  const busChanges = [];
3043
3161
  await this._applyServerDocs(resyncResult.docs, busChanges, true);
3162
+ const truncatedCollections = resyncResult.truncatedCollections ?? [];
3163
+ const truncatedSet = new Set(truncatedCollections);
3164
+ await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
3044
3165
  const returnedKeys = new Set(
3045
3166
  resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
3046
3167
  );
3047
3168
  for (const col of collections) {
3169
+ if (truncatedSet.has(col)) continue;
3048
3170
  const queryResult = await this._store.listDocs(col, {});
3049
3171
  for (const { id } of queryResult.docs) {
3050
3172
  const key = `${col}::${id}`;
@@ -3204,7 +3326,8 @@ var SyncEngine = class {
3204
3326
  status: isOnline ? "idle" : "offline",
3205
3327
  pendingWrites: 0,
3206
3328
  lastSyncedAt: null,
3207
- isLeaderTab: this._tabCoordinator.isLeader()
3329
+ isLeaderTab: this._tabCoordinator.isLeader(),
3330
+ lastError: null
3208
3331
  };
3209
3332
  }
3210
3333
  /**
@@ -3214,7 +3337,7 @@ var SyncEngine = class {
3214
3337
  _updateState(partial) {
3215
3338
  const prev = this._state;
3216
3339
  const next = { ...prev, ...partial };
3217
- const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
3340
+ const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab || (prev.lastError ?? null) !== (next.lastError ?? null);
3218
3341
  this._state = next;
3219
3342
  if (changed) {
3220
3343
  this._emitState(next);
@@ -3228,6 +3351,21 @@ var SyncEngine = class {
3228
3351
  }
3229
3352
  }
3230
3353
  }
3354
+ /**
3355
+ * P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
3356
+ * Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
3357
+ * dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
3358
+ */
3359
+ _recordSyncError(phase, message) {
3360
+ this._updateState({ lastError: message });
3361
+ const event = { phase, message, at: Date.now() };
3362
+ for (const listener of this._errorListeners) {
3363
+ try {
3364
+ listener({ ...event });
3365
+ } catch {
3366
+ }
3367
+ }
3368
+ }
3231
3369
  /**
3232
3370
  * Atualiza `pendingWrites` no estado.
3233
3371
  * Chamado de forma assíncrona quando necessário.
@@ -4037,7 +4175,9 @@ var DbCollectionRefImpl = class {
4037
4175
  const isOnline = this._isOnline();
4038
4176
  const syncState = this._engine.getSyncState();
4039
4177
  const nextCursor = buildNextCursor(result.docs, effectiveLimit);
4040
- const stale = !isOnline || syncState.status === "offline";
4178
+ const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
4179
+ const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
4180
+ const stale = !isOnline || syncState.status === "offline" || truncated;
4041
4181
  const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
4042
4182
  return {
4043
4183
  docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
@@ -4045,7 +4185,8 @@ var DbCollectionRefImpl = class {
4045
4185
  fromCache,
4046
4186
  stale,
4047
4187
  hasPendingWrites: hasPending,
4048
- changes: []
4188
+ changes: [],
4189
+ truncated
4049
4190
  };
4050
4191
  }
4051
4192
  // ── CRUD ────────────────────────────────────────────────────────────────────
@@ -4491,6 +4632,9 @@ var NeetruDbDocumentsImpl = class {
4491
4632
  async getConflicts() {
4492
4633
  return this._store.listConflicts();
4493
4634
  }
4635
+ onError(cb) {
4636
+ return this._engine.onError(cb);
4637
+ }
4494
4638
  };
4495
4639
  async function createOfflineDocumentsNamespace(opts) {
4496
4640
  const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
@@ -4966,6 +5110,7 @@ var defaultWebSocketFactory = (url) => {
4966
5110
  // src/db/client-db.ts
4967
5111
  var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
4968
5112
  var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
5113
+ var DOCUMENTS_RESYNC_PAGE_CAP = 200;
4969
5114
  function resolveTransport(transport, opts, config) {
4970
5115
  if (opts._transport) {
4971
5116
  return opts._transport;
@@ -5078,6 +5223,7 @@ function createRestSyncTransport(config) {
5078
5223
  const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
5079
5224
  const now = Date.now();
5080
5225
  const docs = [];
5226
+ const truncatedCollections = [];
5081
5227
  for (const collection of collections) {
5082
5228
  let raw;
5083
5229
  try {
@@ -5087,11 +5233,11 @@ function createRestSyncTransport(config) {
5087
5233
  requireAuth: true,
5088
5234
  retries: 2,
5089
5235
  headers: tenantHeaders,
5090
- // Teto do Core é 200. Coleções > 200 docs truncam silenciosamente
5091
- // aqui (paginação cursor é tarefa Core separada) — a detecção de
5092
- // deleção-por-ausência do SyncEngine NÃO deve rodar sobre truncamento,
5093
- // então mantemos o limite no máximo do servidor por ora.
5094
- query: { limit: 200 }
5236
+ // Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
5237
+ // TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
5238
+ // (a) NÃO rodar deleção-por-ausência e (b) sinalizar
5239
+ // `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
5240
+ query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
5095
5241
  });
5096
5242
  } catch (err) {
5097
5243
  const msg = err instanceof Error ? err.message : String(err);
@@ -5102,6 +5248,9 @@ function createRestSyncTransport(config) {
5102
5248
  }
5103
5249
  const resp = raw;
5104
5250
  const items = Array.isArray(resp?.items) ? resp.items : [];
5251
+ if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
5252
+ truncatedCollections.push(collection);
5253
+ }
5105
5254
  for (const item of items) {
5106
5255
  if (!item || typeof item !== "object") continue;
5107
5256
  const record = item;
@@ -5129,7 +5278,7 @@ function createRestSyncTransport(config) {
5129
5278
  });
5130
5279
  }
5131
5280
  }
5132
- return { docs, newWatermark: now };
5281
+ return { docs, newWatermark: now, truncatedCollections };
5133
5282
  }
5134
5283
  };
5135
5284
  }
@@ -5191,20 +5340,20 @@ function buildTicketProvider(config, opts) {
5191
5340
  return ticket;
5192
5341
  };
5193
5342
  }
5194
- var _hasWarnedMissingDbId = false;
5195
- function warnMissingDbIdOnce(config, dbOpts) {
5196
- if (_hasWarnedMissingDbId) return;
5197
- if (dbOpts.dbId) return;
5343
+ var _hasWarnedDbIdIgnored = false;
5344
+ function warnDbIdIgnoredOnce(config, dbOpts) {
5345
+ if (_hasWarnedDbIdIgnored) return;
5346
+ if (!dbOpts.dbId) return;
5198
5347
  if (config.env === "prod") return;
5199
5348
  if (typeof window === "undefined") return;
5200
5349
  if (typeof console === "undefined" || typeof console.warn !== "function") return;
5201
- _hasWarnedMissingDbId = true;
5350
+ _hasWarnedDbIdIgnored = true;
5202
5351
  console.warn(
5203
- "[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."
5352
+ '[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.'
5204
5353
  );
5205
5354
  }
5206
5355
  function createNeetruDb(config, dbOpts = {}) {
5207
- warnMissingDbIdOnce(config, dbOpts);
5356
+ warnDbIdIgnoredOnce(config, dbOpts);
5208
5357
  let resolvedTransport;
5209
5358
  if (dbOpts.transport !== void 0) {
5210
5359
  resolvedTransport = dbOpts.transport;
@@ -5282,6 +5431,20 @@ function createNeetruDb(config, dbOpts = {}) {
5282
5431
  async getConflicts() {
5283
5432
  const docs = await getDocsPromise();
5284
5433
  return docs.getConflicts();
5434
+ },
5435
+ onError(cb) {
5436
+ if (_docsResolved) return _docsResolved.onError(cb);
5437
+ let unsub = null;
5438
+ let cancelled = false;
5439
+ getDocsPromise().then((docs) => {
5440
+ if (!cancelled) {
5441
+ unsub = docs.onError(cb);
5442
+ }
5443
+ });
5444
+ return () => {
5445
+ cancelled = true;
5446
+ unsub?.();
5447
+ };
5285
5448
  }
5286
5449
  };
5287
5450
  }