@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.
- package/CHANGELOG.md +31 -0
- package/dist/ai.d.cts +1 -1
- package/dist/ai.d.ts +1 -1
- package/dist/auth.cjs +230 -38
- package/dist/auth.cjs.map +1 -1
- package/dist/auth.d.cts +1 -1
- package/dist/auth.d.ts +1 -1
- package/dist/auth.mjs +230 -38
- package/dist/auth.mjs.map +1 -1
- package/dist/build-guard.cjs +41 -6
- package/dist/build-guard.cjs.map +1 -1
- package/dist/build-guard.d.cts +24 -1
- package/dist/build-guard.d.ts +24 -1
- package/dist/build-guard.mjs +40 -7
- package/dist/build-guard.mjs.map +1 -1
- package/dist/catalog.d.cts +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/checkout.d.cts +1 -1
- package/dist/checkout.d.ts +1 -1
- package/dist/db-react.d.cts +1 -1
- package/dist/db-react.d.ts +1 -1
- package/dist/db.cjs +230 -38
- package/dist/db.cjs.map +1 -1
- package/dist/db.d.cts +1 -1
- package/dist/db.d.ts +1 -1
- package/dist/db.mjs +230 -38
- package/dist/db.mjs.map +1 -1
- package/dist/entitlements.d.cts +1 -1
- package/dist/entitlements.d.ts +1 -1
- package/dist/firestore-compat.d.cts +1 -1
- package/dist/firestore-compat.d.ts +1 -1
- package/dist/index.cjs +231 -39
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.mjs +231 -39
- package/dist/index.mjs.map +1 -1
- package/dist/mocks.d.cts +1 -1
- package/dist/mocks.d.ts +1 -1
- package/dist/notifications.d.cts +1 -1
- package/dist/notifications.d.ts +1 -1
- package/dist/react.cjs +593 -31
- package/dist/react.cjs.map +1 -1
- package/dist/react.d.cts +97 -2
- package/dist/react.d.ts +97 -2
- package/dist/react.mjs +590 -31
- package/dist/react.mjs.map +1 -1
- package/dist/support.d.cts +1 -1
- package/dist/support.d.ts +1 -1
- package/dist/telemetry.d.cts +1 -1
- package/dist/telemetry.d.ts +1 -1
- package/dist/{types-DdJyqBy-.d.ts → types-BlOLDMR0.d.ts} +79 -6
- package/dist/{types-BoFSCHF_.d.cts → types-D-QdG502.d.cts} +79 -6
- package/dist/usage.d.cts +1 -1
- package/dist/usage.d.ts +1 -1
- package/dist/webhooks.d.cts +1 -1
- package/dist/webhooks.d.ts +1 -1
- package/package.json +1 -1
package/dist/entitlements.d.cts
CHANGED
package/dist/entitlements.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Y as NeetruDb, E as DbCollectionRef, P as FieldSentinel, K as DbQuery } from './types-
|
|
1
|
+
import { Y as NeetruDb, E as DbCollectionRef, P as FieldSentinel, K as DbQuery } from './types-D-QdG502.cjs';
|
|
2
2
|
import 'drizzle-orm/node-postgres';
|
|
3
3
|
import './errors.cjs';
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Y as NeetruDb, E as DbCollectionRef, P as FieldSentinel, K as DbQuery } from './types-
|
|
1
|
+
import { Y as NeetruDb, E as DbCollectionRef, P as FieldSentinel, K as DbQuery } from './types-BlOLDMR0.js';
|
|
2
2
|
import 'drizzle-orm/node-postgres';
|
|
3
3
|
import './errors.js';
|
|
4
4
|
|
package/dist/index.cjs
CHANGED
|
@@ -684,13 +684,17 @@ var init_lease = __esm({
|
|
|
684
684
|
// src/db/sql/sql-client.ts
|
|
685
685
|
var sql_client_exports = {};
|
|
686
686
|
__export(sql_client_exports, {
|
|
687
|
+
DEFAULT_POOL_CONNECT_TIMEOUT_MS: () => DEFAULT_POOL_CONNECT_TIMEOUT_MS,
|
|
688
|
+
DEFAULT_POOL_IDLE_MS: () => DEFAULT_POOL_IDLE_MS,
|
|
689
|
+
DEFAULT_POOL_MAX: () => DEFAULT_POOL_MAX,
|
|
687
690
|
LEASE_ENDPOINT: () => LEASE_ENDPOINT,
|
|
688
691
|
LEASE_RENEW_ENDPOINT: () => LEASE_RENEW_ENDPOINT,
|
|
689
692
|
createDrizzleHandle: () => createDrizzleHandle,
|
|
690
693
|
createHttpLeaseFetcher: () => createHttpLeaseFetcher,
|
|
691
694
|
createPgPool: () => createPgPool,
|
|
692
695
|
createSqlClientFromConfig: () => createSqlClientFromConfig,
|
|
693
|
-
createSqlClientWithDeps: () => createSqlClientWithDeps
|
|
696
|
+
createSqlClientWithDeps: () => createSqlClientWithDeps,
|
|
697
|
+
resolvePoolTuning: () => resolvePoolTuning
|
|
694
698
|
});
|
|
695
699
|
function mapEnvToCore(sdkEnv) {
|
|
696
700
|
if (sdkEnv === "workspace") return "staging";
|
|
@@ -779,6 +783,30 @@ function parseLease(raw) {
|
|
|
779
783
|
expiresAt: req("expiresAt", "string")
|
|
780
784
|
};
|
|
781
785
|
}
|
|
786
|
+
function readEnvInt(name, def, min, max) {
|
|
787
|
+
const raw = typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
788
|
+
if (raw === void 0 || raw.trim() === "") return def;
|
|
789
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
790
|
+
if (!Number.isFinite(n) || n < min || n > max) return def;
|
|
791
|
+
return n;
|
|
792
|
+
}
|
|
793
|
+
function resolvePoolTuning() {
|
|
794
|
+
return {
|
|
795
|
+
// Teto 100: bem abaixo do `max_client_conn = 500` do PgBouncer, com folga
|
|
796
|
+
// para múltiplas instâncias do app dividirem o mesmo front-door.
|
|
797
|
+
max: readEnvInt("NEETRU_DB_POOL_MAX", DEFAULT_POOL_MAX, 1, 100),
|
|
798
|
+
// Teto 15min = janela do cert; piso 1s.
|
|
799
|
+
idleTimeoutMillis: readEnvInt("NEETRU_DB_POOL_IDLE_MS", DEFAULT_POOL_IDLE_MS, 1e3, 9e5),
|
|
800
|
+
connectionTimeoutMillis: readEnvInt(
|
|
801
|
+
"NEETRU_DB_POOL_CONNECT_TIMEOUT_MS",
|
|
802
|
+
DEFAULT_POOL_CONNECT_TIMEOUT_MS,
|
|
803
|
+
1e3,
|
|
804
|
+
12e4
|
|
805
|
+
),
|
|
806
|
+
keepAlive: true,
|
|
807
|
+
keepAliveInitialDelayMillis: 1e4
|
|
808
|
+
};
|
|
809
|
+
}
|
|
782
810
|
async function createPgPool(lease) {
|
|
783
811
|
const { Pool } = await import(
|
|
784
812
|
/* webpackIgnore: true */
|
|
@@ -797,9 +825,7 @@ async function createPgPool(lease) {
|
|
|
797
825
|
user: lease.user,
|
|
798
826
|
password: lease.password,
|
|
799
827
|
ssl,
|
|
800
|
-
|
|
801
|
-
idleTimeoutMillis: 3e4,
|
|
802
|
-
connectionTimeoutMillis: 1e4
|
|
828
|
+
...resolvePoolTuning()
|
|
803
829
|
});
|
|
804
830
|
}
|
|
805
831
|
async function createDrizzleHandle(pool, schema) {
|
|
@@ -934,9 +960,9 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
934
960
|
user: lease.user,
|
|
935
961
|
password: lease.password,
|
|
936
962
|
ssl,
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
963
|
+
// Tuning centralizado (env-tunável) — ver resolvePoolTuning + auditoria
|
|
964
|
+
// docs/_review/AUDITORIA_DB_CONN_PERF_2026-09-06.md no Core.
|
|
965
|
+
...resolvePoolTuning()
|
|
940
966
|
});
|
|
941
967
|
};
|
|
942
968
|
const createDrizzle = (pool, s) => {
|
|
@@ -959,7 +985,7 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
959
985
|
options
|
|
960
986
|
);
|
|
961
987
|
}
|
|
962
|
-
var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
|
|
988
|
+
var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT, DEFAULT_POOL_MAX, DEFAULT_POOL_IDLE_MS, DEFAULT_POOL_CONNECT_TIMEOUT_MS;
|
|
963
989
|
var init_sql_client = __esm({
|
|
964
990
|
"src/db/sql/sql-client.ts"() {
|
|
965
991
|
init_db_errors();
|
|
@@ -968,6 +994,9 @@ var init_sql_client = __esm({
|
|
|
968
994
|
init_resilient_pool();
|
|
969
995
|
LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
|
|
970
996
|
LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
|
|
997
|
+
DEFAULT_POOL_MAX = 10;
|
|
998
|
+
DEFAULT_POOL_IDLE_MS = 6e4;
|
|
999
|
+
DEFAULT_POOL_CONNECT_TIMEOUT_MS = 1e4;
|
|
971
1000
|
}
|
|
972
1001
|
});
|
|
973
1002
|
|
|
@@ -2543,8 +2572,10 @@ var MutationQueue = class {
|
|
|
2543
2572
|
await this.store.deleteMutation(mutationId);
|
|
2544
2573
|
}
|
|
2545
2574
|
/**
|
|
2546
|
-
* Marca uma mutação como `failed` e registra o erro.
|
|
2547
|
-
* Incrementa `attempts`. Chamado pelo SyncEngine
|
|
2575
|
+
* Marca uma mutação como `failed` (dead-letter) e registra o erro.
|
|
2576
|
+
* Incrementa `attempts` e carimba `lastAttemptAt`. Chamado pelo SyncEngine
|
|
2577
|
+
* quando a mutação esgota o teto de tentativas (mutação-veneno): sai do
|
|
2578
|
+
* conjunto `queued`, então `drain()` para de devolvê-la e a fila DESTRAVA.
|
|
2548
2579
|
*/
|
|
2549
2580
|
async markFailed(mutationId, error) {
|
|
2550
2581
|
const mut = await this.store.getMutation(mutationId);
|
|
@@ -2553,12 +2584,14 @@ var MutationQueue = class {
|
|
|
2553
2584
|
...mut,
|
|
2554
2585
|
status: "failed",
|
|
2555
2586
|
lastError: error,
|
|
2556
|
-
attempts: mut.attempts + 1
|
|
2587
|
+
attempts: mut.attempts + 1,
|
|
2588
|
+
lastAttemptAt: Date.now()
|
|
2557
2589
|
});
|
|
2558
2590
|
}
|
|
2559
2591
|
/**
|
|
2560
|
-
* Incrementa `attempts` e volta para `queued`
|
|
2561
|
-
* Chamado pelo SyncEngine em falha
|
|
2592
|
+
* Incrementa `attempts`, carimba `lastAttemptAt` e volta para `queued`
|
|
2593
|
+
* (falha transiente com backoff). Chamado pelo SyncEngine em falha
|
|
2594
|
+
* transiente (5xx, timeout) da mutação que REALMENTE foi tentada.
|
|
2562
2595
|
*/
|
|
2563
2596
|
async markRetry(mutationId, error) {
|
|
2564
2597
|
const mut = await this.store.getMutation(mutationId);
|
|
@@ -2567,9 +2600,24 @@ var MutationQueue = class {
|
|
|
2567
2600
|
...mut,
|
|
2568
2601
|
status: "queued",
|
|
2569
2602
|
lastError: error,
|
|
2570
|
-
attempts: mut.attempts + 1
|
|
2603
|
+
attempts: mut.attempts + 1,
|
|
2604
|
+
lastAttemptAt: Date.now()
|
|
2571
2605
|
});
|
|
2572
2606
|
}
|
|
2607
|
+
/**
|
|
2608
|
+
* Reverte uma mutação `inflight` de volta a `queued` SEM tocar em `attempts`
|
|
2609
|
+
* nem `lastAttemptAt`. Usado para mutações que foram marcadas inflight mas
|
|
2610
|
+
* NUNCA chegaram a ser enviadas (estavam atrás da head no FIFO e o push
|
|
2611
|
+
* parou antes delas): contá-las como tentativa as empurraria pro dead-letter
|
|
2612
|
+
* por engano, e adiaria seu envio via backoff sem que tenham falhado.
|
|
2613
|
+
* No-op se a mutação não está mais `inflight`.
|
|
2614
|
+
*/
|
|
2615
|
+
async requeueInflight(mutationId) {
|
|
2616
|
+
const mut = await this.store.getMutation(mutationId);
|
|
2617
|
+
if (!mut) return;
|
|
2618
|
+
if (mut.status !== "inflight") return;
|
|
2619
|
+
await this.store.putMutation({ ...mut, status: "queued" });
|
|
2620
|
+
}
|
|
2573
2621
|
};
|
|
2574
2622
|
|
|
2575
2623
|
// src/db/field-value.ts
|
|
@@ -2625,6 +2673,10 @@ function hasFieldSentinel(data) {
|
|
|
2625
2673
|
}
|
|
2626
2674
|
|
|
2627
2675
|
// src/db/offline/sync-engine.ts
|
|
2676
|
+
var MAX_MUTATION_ATTEMPTS = 8;
|
|
2677
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
2678
|
+
var BACKOFF_CAP_MS = 5 * 60 * 1e3;
|
|
2679
|
+
var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
|
|
2628
2680
|
function hasSentinels(payload) {
|
|
2629
2681
|
return payload !== null && hasFieldSentinel(payload);
|
|
2630
2682
|
}
|
|
@@ -2659,6 +2711,8 @@ var SyncEngine = class {
|
|
|
2659
2711
|
_state;
|
|
2660
2712
|
/** Listeners de mudança de SyncState. */
|
|
2661
2713
|
_stateListeners = /* @__PURE__ */ new Set();
|
|
2714
|
+
/** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
|
|
2715
|
+
_errorListeners = /* @__PURE__ */ new Set();
|
|
2662
2716
|
/** Cleanup functions dos listeners externos. */
|
|
2663
2717
|
_cleanups = [];
|
|
2664
2718
|
/** Timer de resync periódico. */
|
|
@@ -2717,6 +2771,19 @@ var SyncEngine = class {
|
|
|
2717
2771
|
this._stateListeners.delete(cb);
|
|
2718
2772
|
};
|
|
2719
2773
|
}
|
|
2774
|
+
/**
|
|
2775
|
+
* Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
|
|
2776
|
+
* (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
|
|
2777
|
+
* mutação-veneno) — o que antes era engolido com "log silencioso".
|
|
2778
|
+
* O mesmo diagnóstico também aparece em `SyncState.lastError`.
|
|
2779
|
+
* Retorna uma função de unsubscribe.
|
|
2780
|
+
*/
|
|
2781
|
+
onError(cb) {
|
|
2782
|
+
this._errorListeners.add(cb);
|
|
2783
|
+
return () => {
|
|
2784
|
+
this._errorListeners.delete(cb);
|
|
2785
|
+
};
|
|
2786
|
+
}
|
|
2720
2787
|
/**
|
|
2721
2788
|
* Força um sync imediato (I3 §10.3 `flush()`).
|
|
2722
2789
|
* Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
|
|
@@ -2749,8 +2816,10 @@ var SyncEngine = class {
|
|
|
2749
2816
|
}
|
|
2750
2817
|
await this._phase2Pull();
|
|
2751
2818
|
this._phase3Realtime();
|
|
2752
|
-
this._updateState({ status: "idle", lastSyncedAt: Date.now() });
|
|
2819
|
+
this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
|
|
2753
2820
|
} catch (err) {
|
|
2821
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2822
|
+
this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
|
|
2754
2823
|
this._updateState({ status: "idle" });
|
|
2755
2824
|
} finally {
|
|
2756
2825
|
this._syncing = false;
|
|
@@ -2785,6 +2854,9 @@ var SyncEngine = class {
|
|
|
2785
2854
|
async _phase1Push() {
|
|
2786
2855
|
const mutations = await this._queue.drain();
|
|
2787
2856
|
if (mutations.length === 0) return true;
|
|
2857
|
+
if (this._isInBackoff(mutations[0])) {
|
|
2858
|
+
return true;
|
|
2859
|
+
}
|
|
2788
2860
|
for (const mut of mutations) {
|
|
2789
2861
|
await this._queue.markInflight(mut.mutationId);
|
|
2790
2862
|
}
|
|
@@ -2792,10 +2864,16 @@ var SyncEngine = class {
|
|
|
2792
2864
|
try {
|
|
2793
2865
|
result = await this._transport.pushMutations(mutations);
|
|
2794
2866
|
} catch (err) {
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2867
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2868
|
+
const busChangesOnThrow = [];
|
|
2869
|
+
const [head, ...behind] = mutations;
|
|
2870
|
+
await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
|
|
2871
|
+
for (const mut of behind) {
|
|
2872
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
2873
|
+
}
|
|
2874
|
+
if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
|
|
2875
|
+
this._refreshPendingWrites().catch(() => {
|
|
2876
|
+
});
|
|
2799
2877
|
return false;
|
|
2800
2878
|
}
|
|
2801
2879
|
const busChanges = [];
|
|
@@ -2822,8 +2900,16 @@ var SyncEngine = class {
|
|
|
2822
2900
|
}
|
|
2823
2901
|
const processedIds = new Set(result.results.map((r) => r.mutationId));
|
|
2824
2902
|
const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
|
|
2825
|
-
|
|
2826
|
-
|
|
2903
|
+
if (unprocessed.length > 0) {
|
|
2904
|
+
const [boundary, ...behind] = unprocessed;
|
|
2905
|
+
await this._retryOrDeadLetter(
|
|
2906
|
+
boundary,
|
|
2907
|
+
"partial_push: muta\xE7\xE3o n\xE3o processada no lote",
|
|
2908
|
+
busChanges
|
|
2909
|
+
);
|
|
2910
|
+
for (const mut of behind) {
|
|
2911
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
2912
|
+
}
|
|
2827
2913
|
}
|
|
2828
2914
|
if (busChanges.length > 0) {
|
|
2829
2915
|
this._bus.emit(busChanges);
|
|
@@ -3015,6 +3101,67 @@ var SyncEngine = class {
|
|
|
3015
3101
|
});
|
|
3016
3102
|
}
|
|
3017
3103
|
}
|
|
3104
|
+
// ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
|
|
3105
|
+
/**
|
|
3106
|
+
* Trata a falha transiente de UMA mutação que foi realmente tentada.
|
|
3107
|
+
*
|
|
3108
|
+
* - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
|
|
3109
|
+
* backoff, `attempts++`).
|
|
3110
|
+
* - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
|
|
3111
|
+
* `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
|
|
3112
|
+
* e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
|
|
3113
|
+
* `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
|
|
3114
|
+
* pending do doc no cache + registra `lastError`/dispara `onError`.
|
|
3115
|
+
*/
|
|
3116
|
+
async _retryOrDeadLetter(mut, errMsg, busChanges) {
|
|
3117
|
+
if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
|
|
3118
|
+
await this._queue.markRetry(mut.mutationId, errMsg);
|
|
3119
|
+
return;
|
|
3120
|
+
}
|
|
3121
|
+
await this._queue.markFailed(mut.mutationId, errMsg);
|
|
3122
|
+
const conflictRecord = {
|
|
3123
|
+
collection: mut.collection,
|
|
3124
|
+
docId: mut.docId,
|
|
3125
|
+
mutationId: mut.mutationId,
|
|
3126
|
+
losingData: mut.payload ?? {},
|
|
3127
|
+
winningData: {},
|
|
3128
|
+
reason: "failed_max_attempts",
|
|
3129
|
+
detectedAt: Date.now(),
|
|
3130
|
+
delivered: false
|
|
3131
|
+
};
|
|
3132
|
+
await this._store.appendConflict(conflictRecord);
|
|
3133
|
+
const existing = await this._store.getDoc(mut.collection, mut.docId);
|
|
3134
|
+
if (existing) {
|
|
3135
|
+
await this._store.putDoc({
|
|
3136
|
+
...existing,
|
|
3137
|
+
meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
|
|
3138
|
+
});
|
|
3139
|
+
busChanges.push({
|
|
3140
|
+
type: "modified",
|
|
3141
|
+
collection: mut.collection,
|
|
3142
|
+
doc: { id: mut.docId, data: existing.data }
|
|
3143
|
+
});
|
|
3144
|
+
}
|
|
3145
|
+
this._recordSyncError(
|
|
3146
|
+
"push",
|
|
3147
|
+
`muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
|
|
3148
|
+
);
|
|
3149
|
+
}
|
|
3150
|
+
/**
|
|
3151
|
+
* `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
|
|
3152
|
+
* desde a última tentativa falha. Só se aplica a mutações já tentadas
|
|
3153
|
+
* (`attempts > 0` e `lastAttemptAt` carimbado).
|
|
3154
|
+
*/
|
|
3155
|
+
_isInBackoff(mut) {
|
|
3156
|
+
if (mut.attempts <= 0) return false;
|
|
3157
|
+
const last = mut.lastAttemptAt;
|
|
3158
|
+
if (typeof last !== "number") return false;
|
|
3159
|
+
return Date.now() < last + this._backoffDelayMs(mut.attempts);
|
|
3160
|
+
}
|
|
3161
|
+
/** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
|
|
3162
|
+
_backoffDelayMs(attempts) {
|
|
3163
|
+
return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
|
|
3164
|
+
}
|
|
3018
3165
|
// ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
|
|
3019
3166
|
/**
|
|
3020
3167
|
* Busca mudanças do servidor e aplica ao cache via ConflictResolver.
|
|
@@ -3054,10 +3201,14 @@ var SyncEngine = class {
|
|
|
3054
3201
|
}
|
|
3055
3202
|
const busChanges = [];
|
|
3056
3203
|
await this._applyServerDocs(resyncResult.docs, busChanges, true);
|
|
3204
|
+
const truncatedCollections = resyncResult.truncatedCollections ?? [];
|
|
3205
|
+
const truncatedSet = new Set(truncatedCollections);
|
|
3206
|
+
await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
|
|
3057
3207
|
const returnedKeys = new Set(
|
|
3058
3208
|
resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
|
|
3059
3209
|
);
|
|
3060
3210
|
for (const col of collections) {
|
|
3211
|
+
if (truncatedSet.has(col)) continue;
|
|
3061
3212
|
const queryResult = await this._store.listDocs(col, {});
|
|
3062
3213
|
for (const { id } of queryResult.docs) {
|
|
3063
3214
|
const key = `${col}::${id}`;
|
|
@@ -3217,7 +3368,8 @@ var SyncEngine = class {
|
|
|
3217
3368
|
status: isOnline ? "idle" : "offline",
|
|
3218
3369
|
pendingWrites: 0,
|
|
3219
3370
|
lastSyncedAt: null,
|
|
3220
|
-
isLeaderTab: this._tabCoordinator.isLeader()
|
|
3371
|
+
isLeaderTab: this._tabCoordinator.isLeader(),
|
|
3372
|
+
lastError: null
|
|
3221
3373
|
};
|
|
3222
3374
|
}
|
|
3223
3375
|
/**
|
|
@@ -3227,7 +3379,7 @@ var SyncEngine = class {
|
|
|
3227
3379
|
_updateState(partial) {
|
|
3228
3380
|
const prev = this._state;
|
|
3229
3381
|
const next = { ...prev, ...partial };
|
|
3230
|
-
const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
|
|
3382
|
+
const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab || (prev.lastError ?? null) !== (next.lastError ?? null);
|
|
3231
3383
|
this._state = next;
|
|
3232
3384
|
if (changed) {
|
|
3233
3385
|
this._emitState(next);
|
|
@@ -3241,6 +3393,21 @@ var SyncEngine = class {
|
|
|
3241
3393
|
}
|
|
3242
3394
|
}
|
|
3243
3395
|
}
|
|
3396
|
+
/**
|
|
3397
|
+
* P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
|
|
3398
|
+
* Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
|
|
3399
|
+
* dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
|
|
3400
|
+
*/
|
|
3401
|
+
_recordSyncError(phase, message) {
|
|
3402
|
+
this._updateState({ lastError: message });
|
|
3403
|
+
const event = { phase, message, at: Date.now() };
|
|
3404
|
+
for (const listener of this._errorListeners) {
|
|
3405
|
+
try {
|
|
3406
|
+
listener({ ...event });
|
|
3407
|
+
} catch {
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
}
|
|
3244
3411
|
/**
|
|
3245
3412
|
* Atualiza `pendingWrites` no estado.
|
|
3246
3413
|
* Chamado de forma assíncrona quando necessário.
|
|
@@ -4050,7 +4217,9 @@ var DbCollectionRefImpl = class {
|
|
|
4050
4217
|
const isOnline = this._isOnline();
|
|
4051
4218
|
const syncState = this._engine.getSyncState();
|
|
4052
4219
|
const nextCursor = buildNextCursor(result.docs, effectiveLimit);
|
|
4053
|
-
const
|
|
4220
|
+
const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
|
|
4221
|
+
const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
|
|
4222
|
+
const stale = !isOnline || syncState.status === "offline" || truncated;
|
|
4054
4223
|
const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
|
|
4055
4224
|
return {
|
|
4056
4225
|
docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
|
|
@@ -4058,7 +4227,8 @@ var DbCollectionRefImpl = class {
|
|
|
4058
4227
|
fromCache,
|
|
4059
4228
|
stale,
|
|
4060
4229
|
hasPendingWrites: hasPending,
|
|
4061
|
-
changes: []
|
|
4230
|
+
changes: [],
|
|
4231
|
+
truncated
|
|
4062
4232
|
};
|
|
4063
4233
|
}
|
|
4064
4234
|
// ── CRUD ────────────────────────────────────────────────────────────────────
|
|
@@ -4504,6 +4674,9 @@ var NeetruDbDocumentsImpl = class {
|
|
|
4504
4674
|
async getConflicts() {
|
|
4505
4675
|
return this._store.listConflicts();
|
|
4506
4676
|
}
|
|
4677
|
+
onError(cb) {
|
|
4678
|
+
return this._engine.onError(cb);
|
|
4679
|
+
}
|
|
4507
4680
|
};
|
|
4508
4681
|
async function createOfflineDocumentsNamespace(opts) {
|
|
4509
4682
|
const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
|
|
@@ -4979,6 +5152,7 @@ var defaultWebSocketFactory = (url) => {
|
|
|
4979
5152
|
// src/db/client-db.ts
|
|
4980
5153
|
var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
|
|
4981
5154
|
var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
|
|
5155
|
+
var DOCUMENTS_RESYNC_PAGE_CAP = 200;
|
|
4982
5156
|
function resolveTransport(transport, opts, config) {
|
|
4983
5157
|
if (opts._transport) {
|
|
4984
5158
|
return opts._transport;
|
|
@@ -5091,6 +5265,7 @@ function createRestSyncTransport(config) {
|
|
|
5091
5265
|
const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
|
|
5092
5266
|
const now = Date.now();
|
|
5093
5267
|
const docs = [];
|
|
5268
|
+
const truncatedCollections = [];
|
|
5094
5269
|
for (const collection of collections) {
|
|
5095
5270
|
let raw;
|
|
5096
5271
|
try {
|
|
@@ -5100,11 +5275,11 @@ function createRestSyncTransport(config) {
|
|
|
5100
5275
|
requireAuth: true,
|
|
5101
5276
|
retries: 2,
|
|
5102
5277
|
headers: tenantHeaders,
|
|
5103
|
-
// Teto do Core
|
|
5104
|
-
//
|
|
5105
|
-
// deleção-por-ausência
|
|
5106
|
-
//
|
|
5107
|
-
query: { limit:
|
|
5278
|
+
// Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
|
|
5279
|
+
// TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
|
|
5280
|
+
// (a) NÃO rodar deleção-por-ausência e (b) sinalizar
|
|
5281
|
+
// `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
|
|
5282
|
+
query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
|
|
5108
5283
|
});
|
|
5109
5284
|
} catch (err) {
|
|
5110
5285
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -5115,6 +5290,9 @@ function createRestSyncTransport(config) {
|
|
|
5115
5290
|
}
|
|
5116
5291
|
const resp = raw;
|
|
5117
5292
|
const items = Array.isArray(resp?.items) ? resp.items : [];
|
|
5293
|
+
if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
|
|
5294
|
+
truncatedCollections.push(collection);
|
|
5295
|
+
}
|
|
5118
5296
|
for (const item of items) {
|
|
5119
5297
|
if (!item || typeof item !== "object") continue;
|
|
5120
5298
|
const record = item;
|
|
@@ -5142,7 +5320,7 @@ function createRestSyncTransport(config) {
|
|
|
5142
5320
|
});
|
|
5143
5321
|
}
|
|
5144
5322
|
}
|
|
5145
|
-
return { docs, newWatermark: now };
|
|
5323
|
+
return { docs, newWatermark: now, truncatedCollections };
|
|
5146
5324
|
}
|
|
5147
5325
|
};
|
|
5148
5326
|
}
|
|
@@ -5204,20 +5382,20 @@ function buildTicketProvider(config, opts) {
|
|
|
5204
5382
|
return ticket;
|
|
5205
5383
|
};
|
|
5206
5384
|
}
|
|
5207
|
-
var
|
|
5208
|
-
function
|
|
5209
|
-
if (
|
|
5210
|
-
if (dbOpts.dbId) return;
|
|
5385
|
+
var _hasWarnedDbIdIgnored = false;
|
|
5386
|
+
function warnDbIdIgnoredOnce(config, dbOpts) {
|
|
5387
|
+
if (_hasWarnedDbIdIgnored) return;
|
|
5388
|
+
if (!dbOpts.dbId) return;
|
|
5211
5389
|
if (config.env === "prod") return;
|
|
5212
5390
|
if (typeof window === "undefined") return;
|
|
5213
5391
|
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
5214
|
-
|
|
5392
|
+
_hasWarnedDbIdIgnored = true;
|
|
5215
5393
|
console.warn(
|
|
5216
|
-
|
|
5394
|
+
'[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.'
|
|
5217
5395
|
);
|
|
5218
5396
|
}
|
|
5219
5397
|
function createNeetruDb(config, dbOpts = {}) {
|
|
5220
|
-
|
|
5398
|
+
warnDbIdIgnoredOnce(config, dbOpts);
|
|
5221
5399
|
let resolvedTransport;
|
|
5222
5400
|
if (dbOpts.transport !== void 0) {
|
|
5223
5401
|
resolvedTransport = dbOpts.transport;
|
|
@@ -5295,6 +5473,20 @@ function createNeetruDb(config, dbOpts = {}) {
|
|
|
5295
5473
|
async getConflicts() {
|
|
5296
5474
|
const docs = await getDocsPromise();
|
|
5297
5475
|
return docs.getConflicts();
|
|
5476
|
+
},
|
|
5477
|
+
onError(cb) {
|
|
5478
|
+
if (_docsResolved) return _docsResolved.onError(cb);
|
|
5479
|
+
let unsub = null;
|
|
5480
|
+
let cancelled = false;
|
|
5481
|
+
getDocsPromise().then((docs) => {
|
|
5482
|
+
if (!cancelled) {
|
|
5483
|
+
unsub = docs.onError(cb);
|
|
5484
|
+
}
|
|
5485
|
+
});
|
|
5486
|
+
return () => {
|
|
5487
|
+
cancelled = true;
|
|
5488
|
+
unsub?.();
|
|
5489
|
+
};
|
|
5298
5490
|
}
|
|
5299
5491
|
};
|
|
5300
5492
|
}
|
|
@@ -7841,7 +8033,7 @@ function createNeetruClient(config = {}) {
|
|
|
7841
8033
|
init_errors();
|
|
7842
8034
|
init_db_errors();
|
|
7843
8035
|
init_http();
|
|
7844
|
-
var VERSION = "3.4.
|
|
8036
|
+
var VERSION = "3.4.1";
|
|
7845
8037
|
|
|
7846
8038
|
exports.DEFAULT_BASE_URL = DEFAULT_BASE_URL;
|
|
7847
8039
|
exports.DEV_FIXTURE_USER = DEV_FIXTURE_USER;
|