@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/auth.d.cts
CHANGED
package/dist/auth.d.ts
CHANGED
package/dist/auth.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
|
-
|
|
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
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
|
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`
|
|
2559
|
-
* Chamado pelo SyncEngine em falha
|
|
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
|
|
@@ -2612,6 +2660,10 @@ function hasFieldSentinel(data) {
|
|
|
2612
2660
|
}
|
|
2613
2661
|
|
|
2614
2662
|
// src/db/offline/sync-engine.ts
|
|
2663
|
+
var MAX_MUTATION_ATTEMPTS = 8;
|
|
2664
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
2665
|
+
var BACKOFF_CAP_MS = 5 * 60 * 1e3;
|
|
2666
|
+
var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
|
|
2615
2667
|
function hasSentinels(payload) {
|
|
2616
2668
|
return payload !== null && hasFieldSentinel(payload);
|
|
2617
2669
|
}
|
|
@@ -2646,6 +2698,8 @@ var SyncEngine = class {
|
|
|
2646
2698
|
_state;
|
|
2647
2699
|
/** Listeners de mudança de SyncState. */
|
|
2648
2700
|
_stateListeners = /* @__PURE__ */ new Set();
|
|
2701
|
+
/** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
|
|
2702
|
+
_errorListeners = /* @__PURE__ */ new Set();
|
|
2649
2703
|
/** Cleanup functions dos listeners externos. */
|
|
2650
2704
|
_cleanups = [];
|
|
2651
2705
|
/** Timer de resync periódico. */
|
|
@@ -2704,6 +2758,19 @@ var SyncEngine = class {
|
|
|
2704
2758
|
this._stateListeners.delete(cb);
|
|
2705
2759
|
};
|
|
2706
2760
|
}
|
|
2761
|
+
/**
|
|
2762
|
+
* Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
|
|
2763
|
+
* (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
|
|
2764
|
+
* mutação-veneno) — o que antes era engolido com "log silencioso".
|
|
2765
|
+
* O mesmo diagnóstico também aparece em `SyncState.lastError`.
|
|
2766
|
+
* Retorna uma função de unsubscribe.
|
|
2767
|
+
*/
|
|
2768
|
+
onError(cb) {
|
|
2769
|
+
this._errorListeners.add(cb);
|
|
2770
|
+
return () => {
|
|
2771
|
+
this._errorListeners.delete(cb);
|
|
2772
|
+
};
|
|
2773
|
+
}
|
|
2707
2774
|
/**
|
|
2708
2775
|
* Força um sync imediato (I3 §10.3 `flush()`).
|
|
2709
2776
|
* Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
|
|
@@ -2736,8 +2803,10 @@ var SyncEngine = class {
|
|
|
2736
2803
|
}
|
|
2737
2804
|
await this._phase2Pull();
|
|
2738
2805
|
this._phase3Realtime();
|
|
2739
|
-
this._updateState({ status: "idle", lastSyncedAt: Date.now() });
|
|
2806
|
+
this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
|
|
2740
2807
|
} catch (err) {
|
|
2808
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2809
|
+
this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
|
|
2741
2810
|
this._updateState({ status: "idle" });
|
|
2742
2811
|
} finally {
|
|
2743
2812
|
this._syncing = false;
|
|
@@ -2772,6 +2841,9 @@ var SyncEngine = class {
|
|
|
2772
2841
|
async _phase1Push() {
|
|
2773
2842
|
const mutations = await this._queue.drain();
|
|
2774
2843
|
if (mutations.length === 0) return true;
|
|
2844
|
+
if (this._isInBackoff(mutations[0])) {
|
|
2845
|
+
return true;
|
|
2846
|
+
}
|
|
2775
2847
|
for (const mut of mutations) {
|
|
2776
2848
|
await this._queue.markInflight(mut.mutationId);
|
|
2777
2849
|
}
|
|
@@ -2779,10 +2851,16 @@ var SyncEngine = class {
|
|
|
2779
2851
|
try {
|
|
2780
2852
|
result = await this._transport.pushMutations(mutations);
|
|
2781
2853
|
} catch (err) {
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2854
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2855
|
+
const busChangesOnThrow = [];
|
|
2856
|
+
const [head, ...behind] = mutations;
|
|
2857
|
+
await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
|
|
2858
|
+
for (const mut of behind) {
|
|
2859
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
2860
|
+
}
|
|
2861
|
+
if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
|
|
2862
|
+
this._refreshPendingWrites().catch(() => {
|
|
2863
|
+
});
|
|
2786
2864
|
return false;
|
|
2787
2865
|
}
|
|
2788
2866
|
const busChanges = [];
|
|
@@ -2809,8 +2887,16 @@ var SyncEngine = class {
|
|
|
2809
2887
|
}
|
|
2810
2888
|
const processedIds = new Set(result.results.map((r) => r.mutationId));
|
|
2811
2889
|
const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
|
|
2812
|
-
|
|
2813
|
-
|
|
2890
|
+
if (unprocessed.length > 0) {
|
|
2891
|
+
const [boundary, ...behind] = unprocessed;
|
|
2892
|
+
await this._retryOrDeadLetter(
|
|
2893
|
+
boundary,
|
|
2894
|
+
"partial_push: muta\xE7\xE3o n\xE3o processada no lote",
|
|
2895
|
+
busChanges
|
|
2896
|
+
);
|
|
2897
|
+
for (const mut of behind) {
|
|
2898
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
2899
|
+
}
|
|
2814
2900
|
}
|
|
2815
2901
|
if (busChanges.length > 0) {
|
|
2816
2902
|
this._bus.emit(busChanges);
|
|
@@ -3002,6 +3088,67 @@ var SyncEngine = class {
|
|
|
3002
3088
|
});
|
|
3003
3089
|
}
|
|
3004
3090
|
}
|
|
3091
|
+
// ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
|
|
3092
|
+
/**
|
|
3093
|
+
* Trata a falha transiente de UMA mutação que foi realmente tentada.
|
|
3094
|
+
*
|
|
3095
|
+
* - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
|
|
3096
|
+
* backoff, `attempts++`).
|
|
3097
|
+
* - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
|
|
3098
|
+
* `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
|
|
3099
|
+
* e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
|
|
3100
|
+
* `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
|
|
3101
|
+
* pending do doc no cache + registra `lastError`/dispara `onError`.
|
|
3102
|
+
*/
|
|
3103
|
+
async _retryOrDeadLetter(mut, errMsg, busChanges) {
|
|
3104
|
+
if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
|
|
3105
|
+
await this._queue.markRetry(mut.mutationId, errMsg);
|
|
3106
|
+
return;
|
|
3107
|
+
}
|
|
3108
|
+
await this._queue.markFailed(mut.mutationId, errMsg);
|
|
3109
|
+
const conflictRecord = {
|
|
3110
|
+
collection: mut.collection,
|
|
3111
|
+
docId: mut.docId,
|
|
3112
|
+
mutationId: mut.mutationId,
|
|
3113
|
+
losingData: mut.payload ?? {},
|
|
3114
|
+
winningData: {},
|
|
3115
|
+
reason: "failed_max_attempts",
|
|
3116
|
+
detectedAt: Date.now(),
|
|
3117
|
+
delivered: false
|
|
3118
|
+
};
|
|
3119
|
+
await this._store.appendConflict(conflictRecord);
|
|
3120
|
+
const existing = await this._store.getDoc(mut.collection, mut.docId);
|
|
3121
|
+
if (existing) {
|
|
3122
|
+
await this._store.putDoc({
|
|
3123
|
+
...existing,
|
|
3124
|
+
meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
|
|
3125
|
+
});
|
|
3126
|
+
busChanges.push({
|
|
3127
|
+
type: "modified",
|
|
3128
|
+
collection: mut.collection,
|
|
3129
|
+
doc: { id: mut.docId, data: existing.data }
|
|
3130
|
+
});
|
|
3131
|
+
}
|
|
3132
|
+
this._recordSyncError(
|
|
3133
|
+
"push",
|
|
3134
|
+
`muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
|
|
3135
|
+
);
|
|
3136
|
+
}
|
|
3137
|
+
/**
|
|
3138
|
+
* `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
|
|
3139
|
+
* desde a última tentativa falha. Só se aplica a mutações já tentadas
|
|
3140
|
+
* (`attempts > 0` e `lastAttemptAt` carimbado).
|
|
3141
|
+
*/
|
|
3142
|
+
_isInBackoff(mut) {
|
|
3143
|
+
if (mut.attempts <= 0) return false;
|
|
3144
|
+
const last = mut.lastAttemptAt;
|
|
3145
|
+
if (typeof last !== "number") return false;
|
|
3146
|
+
return Date.now() < last + this._backoffDelayMs(mut.attempts);
|
|
3147
|
+
}
|
|
3148
|
+
/** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
|
|
3149
|
+
_backoffDelayMs(attempts) {
|
|
3150
|
+
return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
|
|
3151
|
+
}
|
|
3005
3152
|
// ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
|
|
3006
3153
|
/**
|
|
3007
3154
|
* Busca mudanças do servidor e aplica ao cache via ConflictResolver.
|
|
@@ -3041,10 +3188,14 @@ var SyncEngine = class {
|
|
|
3041
3188
|
}
|
|
3042
3189
|
const busChanges = [];
|
|
3043
3190
|
await this._applyServerDocs(resyncResult.docs, busChanges, true);
|
|
3191
|
+
const truncatedCollections = resyncResult.truncatedCollections ?? [];
|
|
3192
|
+
const truncatedSet = new Set(truncatedCollections);
|
|
3193
|
+
await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
|
|
3044
3194
|
const returnedKeys = new Set(
|
|
3045
3195
|
resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
|
|
3046
3196
|
);
|
|
3047
3197
|
for (const col of collections) {
|
|
3198
|
+
if (truncatedSet.has(col)) continue;
|
|
3048
3199
|
const queryResult = await this._store.listDocs(col, {});
|
|
3049
3200
|
for (const { id } of queryResult.docs) {
|
|
3050
3201
|
const key = `${col}::${id}`;
|
|
@@ -3204,7 +3355,8 @@ var SyncEngine = class {
|
|
|
3204
3355
|
status: isOnline ? "idle" : "offline",
|
|
3205
3356
|
pendingWrites: 0,
|
|
3206
3357
|
lastSyncedAt: null,
|
|
3207
|
-
isLeaderTab: this._tabCoordinator.isLeader()
|
|
3358
|
+
isLeaderTab: this._tabCoordinator.isLeader(),
|
|
3359
|
+
lastError: null
|
|
3208
3360
|
};
|
|
3209
3361
|
}
|
|
3210
3362
|
/**
|
|
@@ -3214,7 +3366,7 @@ var SyncEngine = class {
|
|
|
3214
3366
|
_updateState(partial) {
|
|
3215
3367
|
const prev = this._state;
|
|
3216
3368
|
const next = { ...prev, ...partial };
|
|
3217
|
-
const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
|
|
3369
|
+
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
3370
|
this._state = next;
|
|
3219
3371
|
if (changed) {
|
|
3220
3372
|
this._emitState(next);
|
|
@@ -3228,6 +3380,21 @@ var SyncEngine = class {
|
|
|
3228
3380
|
}
|
|
3229
3381
|
}
|
|
3230
3382
|
}
|
|
3383
|
+
/**
|
|
3384
|
+
* P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
|
|
3385
|
+
* Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
|
|
3386
|
+
* dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
|
|
3387
|
+
*/
|
|
3388
|
+
_recordSyncError(phase, message) {
|
|
3389
|
+
this._updateState({ lastError: message });
|
|
3390
|
+
const event = { phase, message, at: Date.now() };
|
|
3391
|
+
for (const listener of this._errorListeners) {
|
|
3392
|
+
try {
|
|
3393
|
+
listener({ ...event });
|
|
3394
|
+
} catch {
|
|
3395
|
+
}
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3231
3398
|
/**
|
|
3232
3399
|
* Atualiza `pendingWrites` no estado.
|
|
3233
3400
|
* Chamado de forma assíncrona quando necessário.
|
|
@@ -4037,7 +4204,9 @@ var DbCollectionRefImpl = class {
|
|
|
4037
4204
|
const isOnline = this._isOnline();
|
|
4038
4205
|
const syncState = this._engine.getSyncState();
|
|
4039
4206
|
const nextCursor = buildNextCursor(result.docs, effectiveLimit);
|
|
4040
|
-
const
|
|
4207
|
+
const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
|
|
4208
|
+
const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
|
|
4209
|
+
const stale = !isOnline || syncState.status === "offline" || truncated;
|
|
4041
4210
|
const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
|
|
4042
4211
|
return {
|
|
4043
4212
|
docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
|
|
@@ -4045,7 +4214,8 @@ var DbCollectionRefImpl = class {
|
|
|
4045
4214
|
fromCache,
|
|
4046
4215
|
stale,
|
|
4047
4216
|
hasPendingWrites: hasPending,
|
|
4048
|
-
changes: []
|
|
4217
|
+
changes: [],
|
|
4218
|
+
truncated
|
|
4049
4219
|
};
|
|
4050
4220
|
}
|
|
4051
4221
|
// ── CRUD ────────────────────────────────────────────────────────────────────
|
|
@@ -4491,6 +4661,9 @@ var NeetruDbDocumentsImpl = class {
|
|
|
4491
4661
|
async getConflicts() {
|
|
4492
4662
|
return this._store.listConflicts();
|
|
4493
4663
|
}
|
|
4664
|
+
onError(cb) {
|
|
4665
|
+
return this._engine.onError(cb);
|
|
4666
|
+
}
|
|
4494
4667
|
};
|
|
4495
4668
|
async function createOfflineDocumentsNamespace(opts) {
|
|
4496
4669
|
const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
|
|
@@ -4966,6 +5139,7 @@ var defaultWebSocketFactory = (url) => {
|
|
|
4966
5139
|
// src/db/client-db.ts
|
|
4967
5140
|
var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
|
|
4968
5141
|
var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
|
|
5142
|
+
var DOCUMENTS_RESYNC_PAGE_CAP = 200;
|
|
4969
5143
|
function resolveTransport(transport, opts, config) {
|
|
4970
5144
|
if (opts._transport) {
|
|
4971
5145
|
return opts._transport;
|
|
@@ -5078,6 +5252,7 @@ function createRestSyncTransport(config) {
|
|
|
5078
5252
|
const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
|
|
5079
5253
|
const now = Date.now();
|
|
5080
5254
|
const docs = [];
|
|
5255
|
+
const truncatedCollections = [];
|
|
5081
5256
|
for (const collection of collections) {
|
|
5082
5257
|
let raw;
|
|
5083
5258
|
try {
|
|
@@ -5087,11 +5262,11 @@ function createRestSyncTransport(config) {
|
|
|
5087
5262
|
requireAuth: true,
|
|
5088
5263
|
retries: 2,
|
|
5089
5264
|
headers: tenantHeaders,
|
|
5090
|
-
// Teto do Core
|
|
5091
|
-
//
|
|
5092
|
-
// deleção-por-ausência
|
|
5093
|
-
//
|
|
5094
|
-
query: { limit:
|
|
5265
|
+
// Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
|
|
5266
|
+
// TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
|
|
5267
|
+
// (a) NÃO rodar deleção-por-ausência e (b) sinalizar
|
|
5268
|
+
// `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
|
|
5269
|
+
query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
|
|
5095
5270
|
});
|
|
5096
5271
|
} catch (err) {
|
|
5097
5272
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -5102,6 +5277,9 @@ function createRestSyncTransport(config) {
|
|
|
5102
5277
|
}
|
|
5103
5278
|
const resp = raw;
|
|
5104
5279
|
const items = Array.isArray(resp?.items) ? resp.items : [];
|
|
5280
|
+
if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
|
|
5281
|
+
truncatedCollections.push(collection);
|
|
5282
|
+
}
|
|
5105
5283
|
for (const item of items) {
|
|
5106
5284
|
if (!item || typeof item !== "object") continue;
|
|
5107
5285
|
const record = item;
|
|
@@ -5129,7 +5307,7 @@ function createRestSyncTransport(config) {
|
|
|
5129
5307
|
});
|
|
5130
5308
|
}
|
|
5131
5309
|
}
|
|
5132
|
-
return { docs, newWatermark: now };
|
|
5310
|
+
return { docs, newWatermark: now, truncatedCollections };
|
|
5133
5311
|
}
|
|
5134
5312
|
};
|
|
5135
5313
|
}
|
|
@@ -5191,20 +5369,20 @@ function buildTicketProvider(config, opts) {
|
|
|
5191
5369
|
return ticket;
|
|
5192
5370
|
};
|
|
5193
5371
|
}
|
|
5194
|
-
var
|
|
5195
|
-
function
|
|
5196
|
-
if (
|
|
5197
|
-
if (dbOpts.dbId) return;
|
|
5372
|
+
var _hasWarnedDbIdIgnored = false;
|
|
5373
|
+
function warnDbIdIgnoredOnce(config, dbOpts) {
|
|
5374
|
+
if (_hasWarnedDbIdIgnored) return;
|
|
5375
|
+
if (!dbOpts.dbId) return;
|
|
5198
5376
|
if (config.env === "prod") return;
|
|
5199
5377
|
if (typeof window === "undefined") return;
|
|
5200
5378
|
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
5201
|
-
|
|
5379
|
+
_hasWarnedDbIdIgnored = true;
|
|
5202
5380
|
console.warn(
|
|
5203
|
-
|
|
5381
|
+
'[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
5382
|
);
|
|
5205
5383
|
}
|
|
5206
5384
|
function createNeetruDb(config, dbOpts = {}) {
|
|
5207
|
-
|
|
5385
|
+
warnDbIdIgnoredOnce(config, dbOpts);
|
|
5208
5386
|
let resolvedTransport;
|
|
5209
5387
|
if (dbOpts.transport !== void 0) {
|
|
5210
5388
|
resolvedTransport = dbOpts.transport;
|
|
@@ -5282,6 +5460,20 @@ function createNeetruDb(config, dbOpts = {}) {
|
|
|
5282
5460
|
async getConflicts() {
|
|
5283
5461
|
const docs = await getDocsPromise();
|
|
5284
5462
|
return docs.getConflicts();
|
|
5463
|
+
},
|
|
5464
|
+
onError(cb) {
|
|
5465
|
+
if (_docsResolved) return _docsResolved.onError(cb);
|
|
5466
|
+
let unsub = null;
|
|
5467
|
+
let cancelled = false;
|
|
5468
|
+
getDocsPromise().then((docs) => {
|
|
5469
|
+
if (!cancelled) {
|
|
5470
|
+
unsub = docs.onError(cb);
|
|
5471
|
+
}
|
|
5472
|
+
});
|
|
5473
|
+
return () => {
|
|
5474
|
+
cancelled = true;
|
|
5475
|
+
unsub?.();
|
|
5476
|
+
};
|
|
5285
5477
|
}
|
|
5286
5478
|
};
|
|
5287
5479
|
}
|