@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.d.cts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { w as ConflictRecord, x as CreateOfflineDocumentsOptions, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, K as DbQuery, L as DbSqlLease, N as DbWhereFilter, P as FieldSentinel, Q as IncrementSentinel, V as NEETRU_SENTINEL_KEY, Y as NeetruDb, Z as NeetruDbDocuments, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a4 as NeetruSqlClient, ad as RealtimeTransport, ai as ServerTimestampSentinel, ak as SqlOptions, ap as SyncState,
|
|
1
|
+
export { w as ConflictRecord, x as CreateOfflineDocumentsOptions, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, K as DbQuery, L as DbSqlLease, N as DbWhereFilter, P as FieldSentinel, Q as IncrementSentinel, V as NEETRU_SENTINEL_KEY, Y as NeetruDb, Z as NeetruDbDocuments, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a4 as NeetruSqlClient, ad as RealtimeTransport, ai as ServerTimestampSentinel, ak as SqlOptions, ap as SyncErrorEvent, aq as SyncState, aH as WithFieldSentinels, aI as __resetDbIdWarningForTests, aL as createNeetruDb, aN as createRestSyncTransport, aP as getWebSocketRealtimeClient, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp } from './types-D-QdG502.cjs';
|
|
2
2
|
import 'drizzle-orm/node-postgres';
|
|
3
3
|
import './errors.cjs';
|
package/dist/db.d.ts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
export { w as ConflictRecord, x as CreateOfflineDocumentsOptions, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, K as DbQuery, L as DbSqlLease, N as DbWhereFilter, P as FieldSentinel, Q as IncrementSentinel, V as NEETRU_SENTINEL_KEY, Y as NeetruDb, Z as NeetruDbDocuments, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a4 as NeetruSqlClient, ad as RealtimeTransport, ai as ServerTimestampSentinel, ak as SqlOptions, ap as SyncState,
|
|
1
|
+
export { w as ConflictRecord, x as CreateOfflineDocumentsOptions, z as DbBatchOp, B as DbChangeType, E as DbCollectionRef, F as DbDoc, G as DbDocRef, H as DbGetResult, I as DbListResult, K as DbQuery, L as DbSqlLease, N as DbWhereFilter, P as FieldSentinel, Q as IncrementSentinel, V as NEETRU_SENTINEL_KEY, Y as NeetruDb, Z as NeetruDbDocuments, _ as NeetruDbEngine, $ as NeetruDbError, a0 as NeetruDbErrorCode, a1 as NeetruDbOptions, a2 as NeetruDbTransport, a4 as NeetruSqlClient, ad as RealtimeTransport, ai as ServerTimestampSentinel, ak as SqlOptions, ap as SyncErrorEvent, aq as SyncState, aH as WithFieldSentinels, aI as __resetDbIdWarningForTests, aL as createNeetruDb, aN as createRestSyncTransport, aP as getWebSocketRealtimeClient, aQ as increment, aR as isFieldSentinel, aS as isIncrementSentinel, aT as isServerTimestampSentinel, aU as serverTimestamp } from './types-BlOLDMR0.js';
|
|
2
2
|
import 'drizzle-orm/node-postgres';
|
|
3
3
|
import './errors.js';
|
package/dist/db.mjs
CHANGED
|
@@ -681,13 +681,17 @@ var init_lease = __esm({
|
|
|
681
681
|
// src/db/sql/sql-client.ts
|
|
682
682
|
var sql_client_exports = {};
|
|
683
683
|
__export(sql_client_exports, {
|
|
684
|
+
DEFAULT_POOL_CONNECT_TIMEOUT_MS: () => DEFAULT_POOL_CONNECT_TIMEOUT_MS,
|
|
685
|
+
DEFAULT_POOL_IDLE_MS: () => DEFAULT_POOL_IDLE_MS,
|
|
686
|
+
DEFAULT_POOL_MAX: () => DEFAULT_POOL_MAX,
|
|
684
687
|
LEASE_ENDPOINT: () => LEASE_ENDPOINT,
|
|
685
688
|
LEASE_RENEW_ENDPOINT: () => LEASE_RENEW_ENDPOINT,
|
|
686
689
|
createDrizzleHandle: () => createDrizzleHandle,
|
|
687
690
|
createHttpLeaseFetcher: () => createHttpLeaseFetcher,
|
|
688
691
|
createPgPool: () => createPgPool,
|
|
689
692
|
createSqlClientFromConfig: () => createSqlClientFromConfig,
|
|
690
|
-
createSqlClientWithDeps: () => createSqlClientWithDeps
|
|
693
|
+
createSqlClientWithDeps: () => createSqlClientWithDeps,
|
|
694
|
+
resolvePoolTuning: () => resolvePoolTuning
|
|
691
695
|
});
|
|
692
696
|
function mapEnvToCore(sdkEnv) {
|
|
693
697
|
if (sdkEnv === "workspace") return "staging";
|
|
@@ -776,6 +780,30 @@ function parseLease(raw) {
|
|
|
776
780
|
expiresAt: req("expiresAt", "string")
|
|
777
781
|
};
|
|
778
782
|
}
|
|
783
|
+
function readEnvInt(name, def, min, max) {
|
|
784
|
+
const raw = typeof process !== "undefined" ? process.env?.[name] : void 0;
|
|
785
|
+
if (raw === void 0 || raw.trim() === "") return def;
|
|
786
|
+
const n = Number.parseInt(raw.trim(), 10);
|
|
787
|
+
if (!Number.isFinite(n) || n < min || n > max) return def;
|
|
788
|
+
return n;
|
|
789
|
+
}
|
|
790
|
+
function resolvePoolTuning() {
|
|
791
|
+
return {
|
|
792
|
+
// Teto 100: bem abaixo do `max_client_conn = 500` do PgBouncer, com folga
|
|
793
|
+
// para múltiplas instâncias do app dividirem o mesmo front-door.
|
|
794
|
+
max: readEnvInt("NEETRU_DB_POOL_MAX", DEFAULT_POOL_MAX, 1, 100),
|
|
795
|
+
// Teto 15min = janela do cert; piso 1s.
|
|
796
|
+
idleTimeoutMillis: readEnvInt("NEETRU_DB_POOL_IDLE_MS", DEFAULT_POOL_IDLE_MS, 1e3, 9e5),
|
|
797
|
+
connectionTimeoutMillis: readEnvInt(
|
|
798
|
+
"NEETRU_DB_POOL_CONNECT_TIMEOUT_MS",
|
|
799
|
+
DEFAULT_POOL_CONNECT_TIMEOUT_MS,
|
|
800
|
+
1e3,
|
|
801
|
+
12e4
|
|
802
|
+
),
|
|
803
|
+
keepAlive: true,
|
|
804
|
+
keepAliveInitialDelayMillis: 1e4
|
|
805
|
+
};
|
|
806
|
+
}
|
|
779
807
|
async function createPgPool(lease) {
|
|
780
808
|
const { Pool } = await import(
|
|
781
809
|
/* webpackIgnore: true */
|
|
@@ -794,9 +822,7 @@ async function createPgPool(lease) {
|
|
|
794
822
|
user: lease.user,
|
|
795
823
|
password: lease.password,
|
|
796
824
|
ssl,
|
|
797
|
-
|
|
798
|
-
idleTimeoutMillis: 3e4,
|
|
799
|
-
connectionTimeoutMillis: 1e4
|
|
825
|
+
...resolvePoolTuning()
|
|
800
826
|
});
|
|
801
827
|
}
|
|
802
828
|
async function createDrizzleHandle(pool, schema) {
|
|
@@ -931,9 +957,9 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
931
957
|
user: lease.user,
|
|
932
958
|
password: lease.password,
|
|
933
959
|
ssl,
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
960
|
+
// Tuning centralizado (env-tunável) — ver resolvePoolTuning + auditoria
|
|
961
|
+
// docs/_review/AUDITORIA_DB_CONN_PERF_2026-09-06.md no Core.
|
|
962
|
+
...resolvePoolTuning()
|
|
937
963
|
});
|
|
938
964
|
};
|
|
939
965
|
const createDrizzle = (pool, s) => {
|
|
@@ -956,7 +982,7 @@ async function createSqlClientFromConfig(config, schema, options) {
|
|
|
956
982
|
options
|
|
957
983
|
);
|
|
958
984
|
}
|
|
959
|
-
var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT;
|
|
985
|
+
var LEASE_ENDPOINT, LEASE_RENEW_ENDPOINT, DEFAULT_POOL_MAX, DEFAULT_POOL_IDLE_MS, DEFAULT_POOL_CONNECT_TIMEOUT_MS;
|
|
960
986
|
var init_sql_client = __esm({
|
|
961
987
|
"src/db/sql/sql-client.ts"() {
|
|
962
988
|
init_db_errors();
|
|
@@ -965,6 +991,9 @@ var init_sql_client = __esm({
|
|
|
965
991
|
init_resilient_pool();
|
|
966
992
|
LEASE_ENDPOINT = "/api/sdk/v1/db/lease";
|
|
967
993
|
LEASE_RENEW_ENDPOINT = "/api/sdk/v1/db/lease/renew";
|
|
994
|
+
DEFAULT_POOL_MAX = 10;
|
|
995
|
+
DEFAULT_POOL_IDLE_MS = 6e4;
|
|
996
|
+
DEFAULT_POOL_CONNECT_TIMEOUT_MS = 1e4;
|
|
968
997
|
}
|
|
969
998
|
});
|
|
970
999
|
|
|
@@ -1734,8 +1763,10 @@ var MutationQueue = class {
|
|
|
1734
1763
|
await this.store.deleteMutation(mutationId);
|
|
1735
1764
|
}
|
|
1736
1765
|
/**
|
|
1737
|
-
* Marca uma mutação como `failed` e registra o erro.
|
|
1738
|
-
* Incrementa `attempts`. Chamado pelo SyncEngine
|
|
1766
|
+
* Marca uma mutação como `failed` (dead-letter) e registra o erro.
|
|
1767
|
+
* Incrementa `attempts` e carimba `lastAttemptAt`. Chamado pelo SyncEngine
|
|
1768
|
+
* quando a mutação esgota o teto de tentativas (mutação-veneno): sai do
|
|
1769
|
+
* conjunto `queued`, então `drain()` para de devolvê-la e a fila DESTRAVA.
|
|
1739
1770
|
*/
|
|
1740
1771
|
async markFailed(mutationId, error) {
|
|
1741
1772
|
const mut = await this.store.getMutation(mutationId);
|
|
@@ -1744,12 +1775,14 @@ var MutationQueue = class {
|
|
|
1744
1775
|
...mut,
|
|
1745
1776
|
status: "failed",
|
|
1746
1777
|
lastError: error,
|
|
1747
|
-
attempts: mut.attempts + 1
|
|
1778
|
+
attempts: mut.attempts + 1,
|
|
1779
|
+
lastAttemptAt: Date.now()
|
|
1748
1780
|
});
|
|
1749
1781
|
}
|
|
1750
1782
|
/**
|
|
1751
|
-
* Incrementa `attempts` e volta para `queued`
|
|
1752
|
-
* Chamado pelo SyncEngine em falha
|
|
1783
|
+
* Incrementa `attempts`, carimba `lastAttemptAt` e volta para `queued`
|
|
1784
|
+
* (falha transiente com backoff). Chamado pelo SyncEngine em falha
|
|
1785
|
+
* transiente (5xx, timeout) da mutação que REALMENTE foi tentada.
|
|
1753
1786
|
*/
|
|
1754
1787
|
async markRetry(mutationId, error) {
|
|
1755
1788
|
const mut = await this.store.getMutation(mutationId);
|
|
@@ -1758,9 +1791,24 @@ var MutationQueue = class {
|
|
|
1758
1791
|
...mut,
|
|
1759
1792
|
status: "queued",
|
|
1760
1793
|
lastError: error,
|
|
1761
|
-
attempts: mut.attempts + 1
|
|
1794
|
+
attempts: mut.attempts + 1,
|
|
1795
|
+
lastAttemptAt: Date.now()
|
|
1762
1796
|
});
|
|
1763
1797
|
}
|
|
1798
|
+
/**
|
|
1799
|
+
* Reverte uma mutação `inflight` de volta a `queued` SEM tocar em `attempts`
|
|
1800
|
+
* nem `lastAttemptAt`. Usado para mutações que foram marcadas inflight mas
|
|
1801
|
+
* NUNCA chegaram a ser enviadas (estavam atrás da head no FIFO e o push
|
|
1802
|
+
* parou antes delas): contá-las como tentativa as empurraria pro dead-letter
|
|
1803
|
+
* por engano, e adiaria seu envio via backoff sem que tenham falhado.
|
|
1804
|
+
* No-op se a mutação não está mais `inflight`.
|
|
1805
|
+
*/
|
|
1806
|
+
async requeueInflight(mutationId) {
|
|
1807
|
+
const mut = await this.store.getMutation(mutationId);
|
|
1808
|
+
if (!mut) return;
|
|
1809
|
+
if (mut.status !== "inflight") return;
|
|
1810
|
+
await this.store.putMutation({ ...mut, status: "queued" });
|
|
1811
|
+
}
|
|
1764
1812
|
};
|
|
1765
1813
|
|
|
1766
1814
|
// src/db/field-value.ts
|
|
@@ -1816,6 +1864,10 @@ function hasFieldSentinel(data) {
|
|
|
1816
1864
|
}
|
|
1817
1865
|
|
|
1818
1866
|
// src/db/offline/sync-engine.ts
|
|
1867
|
+
var MAX_MUTATION_ATTEMPTS = 8;
|
|
1868
|
+
var BACKOFF_BASE_MS = 1e3;
|
|
1869
|
+
var BACKOFF_CAP_MS = 5 * 60 * 1e3;
|
|
1870
|
+
var TRUNCATED_COLLECTIONS_META_KEY = "truncatedCollections";
|
|
1819
1871
|
function hasSentinels(payload) {
|
|
1820
1872
|
return payload !== null && hasFieldSentinel(payload);
|
|
1821
1873
|
}
|
|
@@ -1850,6 +1902,8 @@ var SyncEngine = class {
|
|
|
1850
1902
|
_state;
|
|
1851
1903
|
/** Listeners de mudança de SyncState. */
|
|
1852
1904
|
_stateListeners = /* @__PURE__ */ new Set();
|
|
1905
|
+
/** Listeners de erro de ciclo de sync (P2-C — não mais engolido em silêncio). */
|
|
1906
|
+
_errorListeners = /* @__PURE__ */ new Set();
|
|
1853
1907
|
/** Cleanup functions dos listeners externos. */
|
|
1854
1908
|
_cleanups = [];
|
|
1855
1909
|
/** Timer de resync periódico. */
|
|
@@ -1908,6 +1962,19 @@ var SyncEngine = class {
|
|
|
1908
1962
|
this._stateListeners.delete(cb);
|
|
1909
1963
|
};
|
|
1910
1964
|
}
|
|
1965
|
+
/**
|
|
1966
|
+
* Registra um hook de erro de sync (P2-C). Disparado quando um ciclo falha
|
|
1967
|
+
* (exceção inesperada nas fases, falha de pull/resync ou dead-letter de uma
|
|
1968
|
+
* mutação-veneno) — o que antes era engolido com "log silencioso".
|
|
1969
|
+
* O mesmo diagnóstico também aparece em `SyncState.lastError`.
|
|
1970
|
+
* Retorna uma função de unsubscribe.
|
|
1971
|
+
*/
|
|
1972
|
+
onError(cb) {
|
|
1973
|
+
this._errorListeners.add(cb);
|
|
1974
|
+
return () => {
|
|
1975
|
+
this._errorListeners.delete(cb);
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1911
1978
|
/**
|
|
1912
1979
|
* Força um sync imediato (I3 §10.3 `flush()`).
|
|
1913
1980
|
* Resolve quando a fila esvazia OU rejeita se o engine estiver destruído.
|
|
@@ -1940,8 +2007,10 @@ var SyncEngine = class {
|
|
|
1940
2007
|
}
|
|
1941
2008
|
await this._phase2Pull();
|
|
1942
2009
|
this._phase3Realtime();
|
|
1943
|
-
this._updateState({ status: "idle", lastSyncedAt: Date.now() });
|
|
2010
|
+
this._updateState({ status: "idle", lastSyncedAt: Date.now(), lastError: null });
|
|
1944
2011
|
} catch (err) {
|
|
2012
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
2013
|
+
this._recordSyncError("cycle", `ciclo de sync falhou: ${msg}`);
|
|
1945
2014
|
this._updateState({ status: "idle" });
|
|
1946
2015
|
} finally {
|
|
1947
2016
|
this._syncing = false;
|
|
@@ -1976,6 +2045,9 @@ var SyncEngine = class {
|
|
|
1976
2045
|
async _phase1Push() {
|
|
1977
2046
|
const mutations = await this._queue.drain();
|
|
1978
2047
|
if (mutations.length === 0) return true;
|
|
2048
|
+
if (this._isInBackoff(mutations[0])) {
|
|
2049
|
+
return true;
|
|
2050
|
+
}
|
|
1979
2051
|
for (const mut of mutations) {
|
|
1980
2052
|
await this._queue.markInflight(mut.mutationId);
|
|
1981
2053
|
}
|
|
@@ -1983,10 +2055,16 @@ var SyncEngine = class {
|
|
|
1983
2055
|
try {
|
|
1984
2056
|
result = await this._transport.pushMutations(mutations);
|
|
1985
2057
|
} catch (err) {
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
2058
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2059
|
+
const busChangesOnThrow = [];
|
|
2060
|
+
const [head, ...behind] = mutations;
|
|
2061
|
+
await this._retryOrDeadLetter(head, errMsg, busChangesOnThrow);
|
|
2062
|
+
for (const mut of behind) {
|
|
2063
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
1989
2064
|
}
|
|
2065
|
+
if (busChangesOnThrow.length > 0) this._bus.emit(busChangesOnThrow);
|
|
2066
|
+
this._refreshPendingWrites().catch(() => {
|
|
2067
|
+
});
|
|
1990
2068
|
return false;
|
|
1991
2069
|
}
|
|
1992
2070
|
const busChanges = [];
|
|
@@ -2013,8 +2091,16 @@ var SyncEngine = class {
|
|
|
2013
2091
|
}
|
|
2014
2092
|
const processedIds = new Set(result.results.map((r) => r.mutationId));
|
|
2015
2093
|
const unprocessed = mutations.filter((m) => !processedIds.has(m.mutationId));
|
|
2016
|
-
|
|
2017
|
-
|
|
2094
|
+
if (unprocessed.length > 0) {
|
|
2095
|
+
const [boundary, ...behind] = unprocessed;
|
|
2096
|
+
await this._retryOrDeadLetter(
|
|
2097
|
+
boundary,
|
|
2098
|
+
"partial_push: muta\xE7\xE3o n\xE3o processada no lote",
|
|
2099
|
+
busChanges
|
|
2100
|
+
);
|
|
2101
|
+
for (const mut of behind) {
|
|
2102
|
+
await this._queue.requeueInflight(mut.mutationId);
|
|
2103
|
+
}
|
|
2018
2104
|
}
|
|
2019
2105
|
if (busChanges.length > 0) {
|
|
2020
2106
|
this._bus.emit(busChanges);
|
|
@@ -2206,6 +2292,67 @@ var SyncEngine = class {
|
|
|
2206
2292
|
});
|
|
2207
2293
|
}
|
|
2208
2294
|
}
|
|
2295
|
+
// ─── Dead-letter + backoff (P1-C) ────────────────────────────────────────────
|
|
2296
|
+
/**
|
|
2297
|
+
* Trata a falha transiente de UMA mutação que foi realmente tentada.
|
|
2298
|
+
*
|
|
2299
|
+
* - Se ainda há orçamento de tentativas → `markRetry` (volta pra fila com
|
|
2300
|
+
* backoff, `attempts++`).
|
|
2301
|
+
* - Se esgotou (`attempts+1 >= MAX_MUTATION_ATTEMPTS`) → DEAD-LETTER:
|
|
2302
|
+
* `markFailed` (sai do conjunto `queued`, então `drain()` para de devolvê-la
|
|
2303
|
+
* e a fila FIFO DESTRAVA para as demais coleções) + grava um ConflictRecord
|
|
2304
|
+
* `failed_max_attempts` (o produto vê via `getConflicts()`) + limpa o estado
|
|
2305
|
+
* pending do doc no cache + registra `lastError`/dispara `onError`.
|
|
2306
|
+
*/
|
|
2307
|
+
async _retryOrDeadLetter(mut, errMsg, busChanges) {
|
|
2308
|
+
if (mut.attempts + 1 < MAX_MUTATION_ATTEMPTS) {
|
|
2309
|
+
await this._queue.markRetry(mut.mutationId, errMsg);
|
|
2310
|
+
return;
|
|
2311
|
+
}
|
|
2312
|
+
await this._queue.markFailed(mut.mutationId, errMsg);
|
|
2313
|
+
const conflictRecord = {
|
|
2314
|
+
collection: mut.collection,
|
|
2315
|
+
docId: mut.docId,
|
|
2316
|
+
mutationId: mut.mutationId,
|
|
2317
|
+
losingData: mut.payload ?? {},
|
|
2318
|
+
winningData: {},
|
|
2319
|
+
reason: "failed_max_attempts",
|
|
2320
|
+
detectedAt: Date.now(),
|
|
2321
|
+
delivered: false
|
|
2322
|
+
};
|
|
2323
|
+
await this._store.appendConflict(conflictRecord);
|
|
2324
|
+
const existing = await this._store.getDoc(mut.collection, mut.docId);
|
|
2325
|
+
if (existing) {
|
|
2326
|
+
await this._store.putDoc({
|
|
2327
|
+
...existing,
|
|
2328
|
+
meta: { ...existing.meta, state: "synced", pendingMutationIds: [] }
|
|
2329
|
+
});
|
|
2330
|
+
busChanges.push({
|
|
2331
|
+
type: "modified",
|
|
2332
|
+
collection: mut.collection,
|
|
2333
|
+
doc: { id: mut.docId, data: existing.data }
|
|
2334
|
+
});
|
|
2335
|
+
}
|
|
2336
|
+
this._recordSyncError(
|
|
2337
|
+
"push",
|
|
2338
|
+
`muta\xE7\xE3o ${mut.mutationId} (${mut.collection}/${mut.docId}) descartada ap\xF3s ${mut.attempts + 1} tentativas: ${errMsg}`
|
|
2339
|
+
);
|
|
2340
|
+
}
|
|
2341
|
+
/**
|
|
2342
|
+
* `true` se a mutação (head do FIFO) ainda está dentro da janela de backoff
|
|
2343
|
+
* desde a última tentativa falha. Só se aplica a mutações já tentadas
|
|
2344
|
+
* (`attempts > 0` e `lastAttemptAt` carimbado).
|
|
2345
|
+
*/
|
|
2346
|
+
_isInBackoff(mut) {
|
|
2347
|
+
if (mut.attempts <= 0) return false;
|
|
2348
|
+
const last = mut.lastAttemptAt;
|
|
2349
|
+
if (typeof last !== "number") return false;
|
|
2350
|
+
return Date.now() < last + this._backoffDelayMs(mut.attempts);
|
|
2351
|
+
}
|
|
2352
|
+
/** Backoff exponencial keyed em `attempts`, com teto `BACKOFF_CAP_MS`. */
|
|
2353
|
+
_backoffDelayMs(attempts) {
|
|
2354
|
+
return Math.min(BACKOFF_BASE_MS * 2 ** (attempts - 1), BACKOFF_CAP_MS);
|
|
2355
|
+
}
|
|
2209
2356
|
// ─── FASE 2 — pull (reconciliar o cache) ─────────────────────────────────────
|
|
2210
2357
|
/**
|
|
2211
2358
|
* Busca mudanças do servidor e aplica ao cache via ConflictResolver.
|
|
@@ -2245,10 +2392,14 @@ var SyncEngine = class {
|
|
|
2245
2392
|
}
|
|
2246
2393
|
const busChanges = [];
|
|
2247
2394
|
await this._applyServerDocs(resyncResult.docs, busChanges, true);
|
|
2395
|
+
const truncatedCollections = resyncResult.truncatedCollections ?? [];
|
|
2396
|
+
const truncatedSet = new Set(truncatedCollections);
|
|
2397
|
+
await this._store.setMeta(TRUNCATED_COLLECTIONS_META_KEY, truncatedCollections);
|
|
2248
2398
|
const returnedKeys = new Set(
|
|
2249
2399
|
resyncResult.docs.map((d) => `${d.collection}::${d.id}`)
|
|
2250
2400
|
);
|
|
2251
2401
|
for (const col of collections) {
|
|
2402
|
+
if (truncatedSet.has(col)) continue;
|
|
2252
2403
|
const queryResult = await this._store.listDocs(col, {});
|
|
2253
2404
|
for (const { id } of queryResult.docs) {
|
|
2254
2405
|
const key = `${col}::${id}`;
|
|
@@ -2408,7 +2559,8 @@ var SyncEngine = class {
|
|
|
2408
2559
|
status: isOnline ? "idle" : "offline",
|
|
2409
2560
|
pendingWrites: 0,
|
|
2410
2561
|
lastSyncedAt: null,
|
|
2411
|
-
isLeaderTab: this._tabCoordinator.isLeader()
|
|
2562
|
+
isLeaderTab: this._tabCoordinator.isLeader(),
|
|
2563
|
+
lastError: null
|
|
2412
2564
|
};
|
|
2413
2565
|
}
|
|
2414
2566
|
/**
|
|
@@ -2418,7 +2570,7 @@ var SyncEngine = class {
|
|
|
2418
2570
|
_updateState(partial) {
|
|
2419
2571
|
const prev = this._state;
|
|
2420
2572
|
const next = { ...prev, ...partial };
|
|
2421
|
-
const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab;
|
|
2573
|
+
const changed = prev.status !== next.status || prev.pendingWrites !== next.pendingWrites || prev.lastSyncedAt !== next.lastSyncedAt || prev.isLeaderTab !== next.isLeaderTab || (prev.lastError ?? null) !== (next.lastError ?? null);
|
|
2422
2574
|
this._state = next;
|
|
2423
2575
|
if (changed) {
|
|
2424
2576
|
this._emitState(next);
|
|
@@ -2432,6 +2584,21 @@ var SyncEngine = class {
|
|
|
2432
2584
|
}
|
|
2433
2585
|
}
|
|
2434
2586
|
}
|
|
2587
|
+
/**
|
|
2588
|
+
* P2-C: registra a falha de um ciclo de sync — antes engolida em silêncio.
|
|
2589
|
+
* Publica em `SyncState.lastError` (visível via `onSyncStateChanged`) e
|
|
2590
|
+
* dispara o hook `onError`. Nunca lança (diagnóstico é best-effort).
|
|
2591
|
+
*/
|
|
2592
|
+
_recordSyncError(phase, message) {
|
|
2593
|
+
this._updateState({ lastError: message });
|
|
2594
|
+
const event = { phase, message, at: Date.now() };
|
|
2595
|
+
for (const listener of this._errorListeners) {
|
|
2596
|
+
try {
|
|
2597
|
+
listener({ ...event });
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2435
2602
|
/**
|
|
2436
2603
|
* Atualiza `pendingWrites` no estado.
|
|
2437
2604
|
* Chamado de forma assíncrona quando necessário.
|
|
@@ -3241,7 +3408,9 @@ var DbCollectionRefImpl = class {
|
|
|
3241
3408
|
const isOnline = this._isOnline();
|
|
3242
3409
|
const syncState = this._engine.getSyncState();
|
|
3243
3410
|
const nextCursor = buildNextCursor(result.docs, effectiveLimit);
|
|
3244
|
-
const
|
|
3411
|
+
const truncatedRaw = await this._store.getMeta(TRUNCATED_COLLECTIONS_META_KEY);
|
|
3412
|
+
const truncated = Array.isArray(truncatedRaw) && truncatedRaw.includes(this._collection);
|
|
3413
|
+
const stale = !isOnline || syncState.status === "offline" || truncated;
|
|
3245
3414
|
const fromCache = hasPending || syncState.lastSyncedAt === null || stale;
|
|
3246
3415
|
return {
|
|
3247
3416
|
docs: result.docs.map((d) => ({ id: d.id, data: d.data })),
|
|
@@ -3249,7 +3418,8 @@ var DbCollectionRefImpl = class {
|
|
|
3249
3418
|
fromCache,
|
|
3250
3419
|
stale,
|
|
3251
3420
|
hasPendingWrites: hasPending,
|
|
3252
|
-
changes: []
|
|
3421
|
+
changes: [],
|
|
3422
|
+
truncated
|
|
3253
3423
|
};
|
|
3254
3424
|
}
|
|
3255
3425
|
// ── CRUD ────────────────────────────────────────────────────────────────────
|
|
@@ -3695,6 +3865,9 @@ var NeetruDbDocumentsImpl = class {
|
|
|
3695
3865
|
async getConflicts() {
|
|
3696
3866
|
return this._store.listConflicts();
|
|
3697
3867
|
}
|
|
3868
|
+
onError(cb) {
|
|
3869
|
+
return this._engine.onError(cb);
|
|
3870
|
+
}
|
|
3698
3871
|
};
|
|
3699
3872
|
async function createOfflineDocumentsNamespace(opts) {
|
|
3700
3873
|
const store = isIndexedDBUnavailable() ? new MemoryStore() : new LocalStore(opts.dbName);
|
|
@@ -4171,6 +4344,7 @@ var defaultWebSocketFactory = (url) => {
|
|
|
4171
4344
|
init_db_errors();
|
|
4172
4345
|
var DATASTORE_COLLECTION_ENDPOINT = (collection) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}`;
|
|
4173
4346
|
var DATASTORE_DOC_ENDPOINT = (collection, docId) => `/api/sdk/v1/datastore/${encodeURIComponent(collection)}/${encodeURIComponent(docId)}`;
|
|
4347
|
+
var DOCUMENTS_RESYNC_PAGE_CAP = 200;
|
|
4174
4348
|
function resolveTransport(transport, opts, config) {
|
|
4175
4349
|
if (opts._transport) {
|
|
4176
4350
|
return opts._transport;
|
|
@@ -4283,6 +4457,7 @@ function createRestSyncTransport(config) {
|
|
|
4283
4457
|
const tenantHeaders = tenantScopeId ? { "x-neetru-tenant": tenantScopeId } : void 0;
|
|
4284
4458
|
const now = Date.now();
|
|
4285
4459
|
const docs = [];
|
|
4460
|
+
const truncatedCollections = [];
|
|
4286
4461
|
for (const collection of collections) {
|
|
4287
4462
|
let raw;
|
|
4288
4463
|
try {
|
|
@@ -4292,11 +4467,11 @@ function createRestSyncTransport(config) {
|
|
|
4292
4467
|
requireAuth: true,
|
|
4293
4468
|
retries: 2,
|
|
4294
4469
|
headers: tenantHeaders,
|
|
4295
|
-
// Teto do Core
|
|
4296
|
-
//
|
|
4297
|
-
// deleção-por-ausência
|
|
4298
|
-
//
|
|
4299
|
-
query: { limit:
|
|
4470
|
+
// Teto do Core (DOCUMENTS_RESYNC_PAGE_CAP). Coleções acima dele vêm
|
|
4471
|
+
// TRUNCADAS — reportadas em `truncatedCollections` p/ o SyncEngine
|
|
4472
|
+
// (a) NÃO rodar deleção-por-ausência e (b) sinalizar
|
|
4473
|
+
// `DbListResult.truncated`. Paginação por cursor é tarefa Core (gated).
|
|
4474
|
+
query: { limit: DOCUMENTS_RESYNC_PAGE_CAP }
|
|
4300
4475
|
});
|
|
4301
4476
|
} catch (err) {
|
|
4302
4477
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -4307,6 +4482,9 @@ function createRestSyncTransport(config) {
|
|
|
4307
4482
|
}
|
|
4308
4483
|
const resp = raw;
|
|
4309
4484
|
const items = Array.isArray(resp?.items) ? resp.items : [];
|
|
4485
|
+
if (items.length >= DOCUMENTS_RESYNC_PAGE_CAP) {
|
|
4486
|
+
truncatedCollections.push(collection);
|
|
4487
|
+
}
|
|
4310
4488
|
for (const item of items) {
|
|
4311
4489
|
if (!item || typeof item !== "object") continue;
|
|
4312
4490
|
const record = item;
|
|
@@ -4334,7 +4512,7 @@ function createRestSyncTransport(config) {
|
|
|
4334
4512
|
});
|
|
4335
4513
|
}
|
|
4336
4514
|
}
|
|
4337
|
-
return { docs, newWatermark: now };
|
|
4515
|
+
return { docs, newWatermark: now, truncatedCollections };
|
|
4338
4516
|
}
|
|
4339
4517
|
};
|
|
4340
4518
|
}
|
|
@@ -4399,23 +4577,23 @@ function buildTicketProvider(config, opts) {
|
|
|
4399
4577
|
function getWebSocketRealtimeClient(gatewayUrl, ticketProvider, dbId, wsFactory) {
|
|
4400
4578
|
return new NeetruRealtimeClient({ gatewayUrl, ticketProvider, dbId, webSocketFactory: wsFactory });
|
|
4401
4579
|
}
|
|
4402
|
-
var
|
|
4580
|
+
var _hasWarnedDbIdIgnored = false;
|
|
4403
4581
|
function __resetDbIdWarningForTests() {
|
|
4404
|
-
|
|
4582
|
+
_hasWarnedDbIdIgnored = false;
|
|
4405
4583
|
}
|
|
4406
|
-
function
|
|
4407
|
-
if (
|
|
4408
|
-
if (dbOpts.dbId) return;
|
|
4584
|
+
function warnDbIdIgnoredOnce(config, dbOpts) {
|
|
4585
|
+
if (_hasWarnedDbIdIgnored) return;
|
|
4586
|
+
if (!dbOpts.dbId) return;
|
|
4409
4587
|
if (config.env === "prod") return;
|
|
4410
4588
|
if (typeof window === "undefined") return;
|
|
4411
4589
|
if (typeof console === "undefined" || typeof console.warn !== "function") return;
|
|
4412
|
-
|
|
4590
|
+
_hasWarnedDbIdIgnored = true;
|
|
4413
4591
|
console.warn(
|
|
4414
|
-
|
|
4592
|
+
'[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.'
|
|
4415
4593
|
);
|
|
4416
4594
|
}
|
|
4417
4595
|
function createNeetruDb(config, dbOpts = {}) {
|
|
4418
|
-
|
|
4596
|
+
warnDbIdIgnoredOnce(config, dbOpts);
|
|
4419
4597
|
let resolvedTransport;
|
|
4420
4598
|
if (dbOpts.transport !== void 0) {
|
|
4421
4599
|
resolvedTransport = dbOpts.transport;
|
|
@@ -4493,6 +4671,20 @@ function createNeetruDb(config, dbOpts = {}) {
|
|
|
4493
4671
|
async getConflicts() {
|
|
4494
4672
|
const docs = await getDocsPromise();
|
|
4495
4673
|
return docs.getConflicts();
|
|
4674
|
+
},
|
|
4675
|
+
onError(cb) {
|
|
4676
|
+
if (_docsResolved) return _docsResolved.onError(cb);
|
|
4677
|
+
let unsub = null;
|
|
4678
|
+
let cancelled = false;
|
|
4679
|
+
getDocsPromise().then((docs) => {
|
|
4680
|
+
if (!cancelled) {
|
|
4681
|
+
unsub = docs.onError(cb);
|
|
4682
|
+
}
|
|
4683
|
+
});
|
|
4684
|
+
return () => {
|
|
4685
|
+
cancelled = true;
|
|
4686
|
+
unsub?.();
|
|
4687
|
+
};
|
|
4496
4688
|
}
|
|
4497
4689
|
};
|
|
4498
4690
|
}
|