@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/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
|
-
|
|
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
|
-
|
|
937
|
-
|
|
938
|
-
|
|
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
|
|
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`
|
|
1754
|
-
* Chamado pelo SyncEngine em falha
|
|
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
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
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
|
-
|
|
2019
|
-
|
|
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
|
|
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
|
|
4298
|
-
//
|
|
4299
|
-
// deleção-por-ausência
|
|
4300
|
-
//
|
|
4301
|
-
query: { limit:
|
|
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
|
|
4582
|
+
var _hasWarnedDbIdIgnored = false;
|
|
4405
4583
|
function __resetDbIdWarningForTests() {
|
|
4406
|
-
|
|
4584
|
+
_hasWarnedDbIdIgnored = false;
|
|
4407
4585
|
}
|
|
4408
|
-
function
|
|
4409
|
-
if (
|
|
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
|
-
|
|
4592
|
+
_hasWarnedDbIdIgnored = true;
|
|
4415
4593
|
console.warn(
|
|
4416
|
-
|
|
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
|
-
|
|
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
|
}
|