@sevn/reqcache 1.2.0 → 1.4.0
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/README.md +327 -169
- package/dist/index.d.ts +135 -8
- package/dist/index.esm.js +336 -19
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +337 -18
- package/dist/index.js.map +1 -1
- package/dist/request-cache.d.ts +133 -6
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.
|
|
14
14
|
* 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.
|
|
15
15
|
* 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).
|
|
16
|
-
* Se houver `monotonicKey`, só aceita o novo dado quando
|
|
17
|
-
*
|
|
16
|
+
* Se houver `monotonicKey`, só aceita o novo dado quando TODAS as chaves
|
|
17
|
+
* tiverem AVANÇADO; senão mantém o antigo.
|
|
18
18
|
*
|
|
19
19
|
* Proteção de espaço (limite de ~5 MB do localStorage):
|
|
20
20
|
* - `maxEntries`: ao gravar, se passar do limite, remove a rota menos
|
|
@@ -31,13 +31,70 @@ interface StorageLike {
|
|
|
31
31
|
setItem(key: string, value: string): void;
|
|
32
32
|
removeItem(key: string): void;
|
|
33
33
|
}
|
|
34
|
+
/**
|
|
35
|
+
* Como comparar o valor de uma chave monotônica:
|
|
36
|
+
* - `"number"` — numérica. Aceita `number` e string numérica ("2232575810").
|
|
37
|
+
* - `"date"` — por timestamp. Aceita o que o `Date.parse` resolver
|
|
38
|
+
* ("2026-10-04T18:23:00Z") e também epoch em ms.
|
|
39
|
+
* - `"string"` — lexicográfica.
|
|
40
|
+
*/
|
|
41
|
+
type MonotonicType = "number" | "date" | "string";
|
|
42
|
+
/**
|
|
43
|
+
* Chave(s) monotônica(s). Três formas:
|
|
44
|
+
* - `"summary.ballots_counted"` — uma chave, tipo detectado pelo valor.
|
|
45
|
+
* - `["idg", "summary.last_updated"]` — várias, tipo detectado pelo valor.
|
|
46
|
+
* - `{ idg: "number", "summary.last_updated": "date" }` — várias, com o tipo
|
|
47
|
+
* declarado por nome de chave (recomendado: não depende de adivinhação).
|
|
48
|
+
*/
|
|
49
|
+
type MonotonicKey = string | string[] | Record<string, MonotonicType>;
|
|
50
|
+
/**
|
|
51
|
+
* Qualidade de rede estimada pelo browser (Network Information API).
|
|
52
|
+
* É uma ESTIMATIVA baseada em latência e throughput recentes, não o rádio do
|
|
53
|
+
* aparelho: um 4g congestionado é reportado como "2g" — que é exatamente o
|
|
54
|
+
* caso que interessa aqui.
|
|
55
|
+
*/
|
|
56
|
+
type EffectiveConnectionType = "slow-2g" | "2g" | "3g" | "4g";
|
|
57
|
+
/**
|
|
58
|
+
* Timeout de UMA tentativa de fetch, em ms, por qualidade de rede.
|
|
59
|
+
* `desconhecida` é o que vale em SSR e nos browsers sem a Network Information
|
|
60
|
+
* API (Safari e Firefox).
|
|
61
|
+
*/
|
|
62
|
+
type TimeoutTiers = Partial<Record<EffectiveConnectionType | "desconhecida", number>>;
|
|
63
|
+
/**
|
|
64
|
+
* Falha de HTTP com o status preservado. É o que permite decidir se vale a
|
|
65
|
+
* pena tentar o próximo domínio (503, sim) ou não (404).
|
|
66
|
+
*/
|
|
67
|
+
declare class HttpError extends Error {
|
|
68
|
+
readonly status: number;
|
|
69
|
+
readonly url: string;
|
|
70
|
+
constructor(status: number, url: string);
|
|
71
|
+
}
|
|
34
72
|
interface GetFetchOptions {
|
|
35
73
|
/**
|
|
36
|
-
* Caminho da chave
|
|
37
|
-
*
|
|
38
|
-
*
|
|
74
|
+
* Caminho(s) da chave monotônica. Suporta aninhamento com ponto.
|
|
75
|
+
*
|
|
76
|
+
* monotonicKey: "resultado.votosApurados"
|
|
77
|
+
* monotonicKey: ["idg", "summary.last_updated"]
|
|
78
|
+
* monotonicKey: { idg: "number", "summary.last_updated": "date" }
|
|
79
|
+
*
|
|
80
|
+
* Com mais de uma chave, TODAS são comparadas e o dado novo só é aceito
|
|
81
|
+
* quando todas concordam que ele é mais recente — se qualquer uma vier
|
|
82
|
+
* igual ou menor, a resposta é descartada e o dado em cache é mantido.
|
|
83
|
+
* A comparação é sempre estrita: o valor novo precisa ser MAIOR que o
|
|
84
|
+
* guardado, nunca igual.
|
|
85
|
+
*
|
|
86
|
+
* O tipo de cada chave é resolvido nesta ordem:
|
|
87
|
+
* 1. declarado na forma de mapa;
|
|
88
|
+
* 2. conhecido pelo NOME da chave (`idg` é número, `last_updated` é data —
|
|
89
|
+
* veja `TIPOS_POR_NOME`, extensível via `monotonicTypes` na config);
|
|
90
|
+
* 3. detectado pelo valor: número, string que converte para número finito,
|
|
91
|
+
* string que o `Date.parse` resolve e, por fim, lexicográfica.
|
|
92
|
+
*
|
|
93
|
+
* TODAS as chaves precisam ter crescido. Uma chave incomparável (ausente de
|
|
94
|
+
* um dos lados, ilegível ou fora do tipo esperado) conta como "não cresceu"
|
|
95
|
+
* e derruba a resposta nova junto com as demais.
|
|
39
96
|
*/
|
|
40
|
-
monotonicKey?:
|
|
97
|
+
monotonicKey?: MonotonicKey;
|
|
41
98
|
/** Opções nativas repassadas ao fetch (headers, method, body, signal...). */
|
|
42
99
|
fetchOptions?: RequestInit;
|
|
43
100
|
/**
|
|
@@ -58,6 +115,21 @@ interface GetFetchOptions {
|
|
|
58
115
|
* (`url` + `fallbackUrls`). Útil para observabilidade/log.
|
|
59
116
|
*/
|
|
60
117
|
onFallback?: (failedUrl: string, error: unknown) => void;
|
|
118
|
+
/**
|
|
119
|
+
* Timeout de CADA tentativa, em ms. Quando omitido, sai da tabela por rede
|
|
120
|
+
* (`timeoutTiers` na config). Vale por candidato, não pela fila toda: com
|
|
121
|
+
* dois `fallbackUrls`, o pior caso é 3x esse valor.
|
|
122
|
+
*/
|
|
123
|
+
timeoutMs?: number;
|
|
124
|
+
/**
|
|
125
|
+
* Status HTTP que NÃO devem acionar o fallback. Padrão: `[404]`.
|
|
126
|
+
*
|
|
127
|
+
* Um 404 é uma resposta correta do servidor — "esse recurso não existe" —,
|
|
128
|
+
* não uma indisponibilidade. Repetir o mesmo caminho em outro domínio tende
|
|
129
|
+
* a devolver o mesmo 404, só que N vezes mais devagar, então o erro sobe na
|
|
130
|
+
* hora. Passe `[]` para voltar ao comportamento antigo (tentar todos).
|
|
131
|
+
*/
|
|
132
|
+
statusSemFallback?: number[];
|
|
61
133
|
}
|
|
62
134
|
/** Uma entrada do cache. `rota` é a chave identificadora. */
|
|
63
135
|
interface CacheEntry<T = unknown> {
|
|
@@ -88,6 +160,29 @@ interface RequestCacheConfig {
|
|
|
88
160
|
debug?: boolean;
|
|
89
161
|
/** Logger customizado usado quando `debug` está ligado. Padrão: `console.debug`. */
|
|
90
162
|
logger?: DebugLogFn;
|
|
163
|
+
/**
|
|
164
|
+
* Tipos monotônicos adicionais, por nome de chave. Somado (e com prioridade
|
|
165
|
+
* sobre) a tabela embutida `TIPOS_POR_NOME`, para que o chamador possa
|
|
166
|
+
* continuar passando `monotonicKey` como lista de caminhos e ainda assim ter
|
|
167
|
+
* a comparação certa:
|
|
168
|
+
*
|
|
169
|
+
* new RequestCache({ monotonicTypes: { data_apuracao: "date" } })
|
|
170
|
+
* ...
|
|
171
|
+
* getFetch(url, 10_000, { monotonicKey: ["idg", "data_apuracao"] })
|
|
172
|
+
*
|
|
173
|
+
* Aceita o caminho completo ("summary.last_updated") ou só o último trecho
|
|
174
|
+
* ("last_updated").
|
|
175
|
+
*/
|
|
176
|
+
monotonicTypes?: Record<string, MonotonicType>;
|
|
177
|
+
/**
|
|
178
|
+
* Timeout de cada tentativa de fetch, por qualidade de rede do client.
|
|
179
|
+
* O que for passado aqui é mesclado sobre `TIMEOUTS_POR_REDE`:
|
|
180
|
+
*
|
|
181
|
+
* new RequestCache({ timeoutTiers: { "slow-2g": 120_000 } })
|
|
182
|
+
*
|
|
183
|
+
* Um `timeoutMs` na chamada tem prioridade sobre esta tabela.
|
|
184
|
+
*/
|
|
185
|
+
timeoutTiers?: TimeoutTiers;
|
|
91
186
|
}
|
|
92
187
|
declare class RequestCache {
|
|
93
188
|
private storage;
|
|
@@ -98,6 +193,8 @@ declare class RequestCache {
|
|
|
98
193
|
private tick;
|
|
99
194
|
private debug;
|
|
100
195
|
private logger;
|
|
196
|
+
private monotonicTypes;
|
|
197
|
+
private timeoutTiers;
|
|
101
198
|
constructor(config?: RequestCacheConfig);
|
|
102
199
|
/** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
|
|
103
200
|
setDebug(enabled: boolean): void;
|
|
@@ -122,10 +219,27 @@ declare class RequestCache {
|
|
|
122
219
|
private nextAccess;
|
|
123
220
|
/** Emite uma linha de log, só quando o modo debug está ligado. */
|
|
124
221
|
private log;
|
|
222
|
+
/**
|
|
223
|
+
* Timeout de UMA tentativa, em ms. A ordem de prioridade é:
|
|
224
|
+
* 1. `timeoutMs` passado na chamada;
|
|
225
|
+
* 2. o tier da rede atual do client (`navigator.connection`);
|
|
226
|
+
* 3. o tier `desconhecida`, usado em SSR, Safari e Firefox.
|
|
227
|
+
*
|
|
228
|
+
* A rede é relida a cada tentativa de propósito: numa apuração o client pode
|
|
229
|
+
* sair do wi-fi e cair no 3g no meio da fila de fallback.
|
|
230
|
+
*/
|
|
231
|
+
private resolverTimeout;
|
|
125
232
|
/**
|
|
126
233
|
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
127
234
|
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
128
235
|
* último erro.
|
|
236
|
+
*
|
|
237
|
+
* Duas situações encerram a fila antes do fim, em vez de seguir para o
|
|
238
|
+
* próximo candidato:
|
|
239
|
+
* - um status listado em `statusSemFallback` (padrão: 404). É resposta do
|
|
240
|
+
* servidor, não indisponibilidade — o outro domínio diria o mesmo.
|
|
241
|
+
* - o `signal` do chamador abortado. Foi cancelamento de quem pediu;
|
|
242
|
+
* insistir só trocaria o erro real pelo AbortError do último candidato.
|
|
129
243
|
*/
|
|
130
244
|
private fetchComRedundancia;
|
|
131
245
|
private revalidate;
|
|
@@ -136,8 +250,21 @@ declare class RequestCache {
|
|
|
136
250
|
private readAll;
|
|
137
251
|
private writeAll;
|
|
138
252
|
}
|
|
253
|
+
/**
|
|
254
|
+
* Tier list de timeout por qualidade de rede, em ms, aplicado a CADA tentativa.
|
|
255
|
+
*
|
|
256
|
+
* 4g / desconhecida 30 s — base
|
|
257
|
+
* 3g 45 s — 1,5x
|
|
258
|
+
* 2g 60 s — 2x
|
|
259
|
+
* slow-2g 90 s — 3x; borda de cobertura, interior
|
|
260
|
+
*
|
|
261
|
+
* A folga cresce com a rede ruim para não matar uma resposta que ainda viria
|
|
262
|
+
* num client lento. O custo aparece em `fallbackUrls`, porque a fila é
|
|
263
|
+
* sequencial: o pior caso é N x o tier — veja `timeoutMs` em GetFetchOptions.
|
|
264
|
+
*/
|
|
265
|
+
declare const TIMEOUTS_POR_REDE: Record<EffectiveConnectionType | "desconhecida", number>;
|
|
139
266
|
/** Instância pronta para uso, caso não queira configurar nada. */
|
|
140
267
|
declare const requestCache: RequestCache;
|
|
141
268
|
|
|
142
|
-
export { RequestCache, requestCache };
|
|
143
|
-
export type { CacheEntry, DebugLogFn, GetFetchOptions, RequestCacheConfig, StorageLike };
|
|
269
|
+
export { HttpError, RequestCache, TIMEOUTS_POR_REDE, requestCache };
|
|
270
|
+
export type { CacheEntry, DebugLogFn, EffectiveConnectionType, GetFetchOptions, MonotonicKey, MonotonicType, RequestCacheConfig, StorageLike, TimeoutTiers };
|
package/dist/index.esm.js
CHANGED
|
@@ -13,8 +13,8 @@
|
|
|
13
13
|
* 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.
|
|
14
14
|
* 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.
|
|
15
15
|
* 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).
|
|
16
|
-
* Se houver `monotonicKey`, só aceita o novo dado quando
|
|
17
|
-
*
|
|
16
|
+
* Se houver `monotonicKey`, só aceita o novo dado quando TODAS as chaves
|
|
17
|
+
* tiverem AVANÇADO; senão mantém o antigo.
|
|
18
18
|
*
|
|
19
19
|
* Proteção de espaço (limite de ~5 MB do localStorage):
|
|
20
20
|
* - `maxEntries`: ao gravar, se passar do limite, remove a rota menos
|
|
@@ -26,9 +26,21 @@
|
|
|
26
26
|
* - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.
|
|
27
27
|
* - staleOnError: se a rede/API falhar, devolve o último dado válido.
|
|
28
28
|
*/
|
|
29
|
+
/**
|
|
30
|
+
* Falha de HTTP com o status preservado. É o que permite decidir se vale a
|
|
31
|
+
* pena tentar o próximo domínio (503, sim) ou não (404).
|
|
32
|
+
*/
|
|
33
|
+
class HttpError extends Error {
|
|
34
|
+
constructor(status, url) {
|
|
35
|
+
super(`HTTP ${status} ao buscar ${url}`);
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.url = url;
|
|
38
|
+
this.name = "HttpError";
|
|
39
|
+
}
|
|
40
|
+
}
|
|
29
41
|
class RequestCache {
|
|
30
42
|
constructor(config = {}) {
|
|
31
|
-
var _a, _b, _c, _d, _e;
|
|
43
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
32
44
|
this.inflight = new Map();
|
|
33
45
|
/** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
|
|
34
46
|
this.tick = 0;
|
|
@@ -37,6 +49,8 @@ class RequestCache {
|
|
|
37
49
|
this.storage = (_c = config.storage) !== null && _c !== void 0 ? _c : getDefaultStorage();
|
|
38
50
|
this.debug = (_d = config.debug) !== null && _d !== void 0 ? _d : false;
|
|
39
51
|
this.logger = (_e = config.logger) !== null && _e !== void 0 ? _e : defaultLogger;
|
|
52
|
+
this.monotonicTypes = { ...TIPOS_POR_NOME, ...((_f = config.monotonicTypes) !== null && _f !== void 0 ? _f : {}) };
|
|
53
|
+
this.timeoutTiers = { ...TIMEOUTS_POR_REDE, ...((_g = config.timeoutTiers) !== null && _g !== void 0 ? _g : {}) };
|
|
40
54
|
}
|
|
41
55
|
/** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
|
|
42
56
|
setDebug(enabled) {
|
|
@@ -115,27 +129,78 @@ class RequestCache {
|
|
|
115
129
|
return;
|
|
116
130
|
this.logger(message, details);
|
|
117
131
|
}
|
|
132
|
+
/**
|
|
133
|
+
* Timeout de UMA tentativa, em ms. A ordem de prioridade é:
|
|
134
|
+
* 1. `timeoutMs` passado na chamada;
|
|
135
|
+
* 2. o tier da rede atual do client (`navigator.connection`);
|
|
136
|
+
* 3. o tier `desconhecida`, usado em SSR, Safari e Firefox.
|
|
137
|
+
*
|
|
138
|
+
* A rede é relida a cada tentativa de propósito: numa apuração o client pode
|
|
139
|
+
* sair do wi-fi e cair no 3g no meio da fila de fallback.
|
|
140
|
+
*/
|
|
141
|
+
resolverTimeout(options) {
|
|
142
|
+
var _a;
|
|
143
|
+
if (typeof options.timeoutMs === "number")
|
|
144
|
+
return options.timeoutMs;
|
|
145
|
+
return this.timeoutTiers[(_a = detectarRede()) !== null && _a !== void 0 ? _a : "desconhecida"];
|
|
146
|
+
}
|
|
118
147
|
/**
|
|
119
148
|
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
120
149
|
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
121
150
|
* último erro.
|
|
151
|
+
*
|
|
152
|
+
* Duas situações encerram a fila antes do fim, em vez de seguir para o
|
|
153
|
+
* próximo candidato:
|
|
154
|
+
* - um status listado em `statusSemFallback` (padrão: 404). É resposta do
|
|
155
|
+
* servidor, não indisponibilidade — o outro domínio diria o mesmo.
|
|
156
|
+
* - o `signal` do chamador abortado. Foi cancelamento de quem pediu;
|
|
157
|
+
* insistir só trocaria o erro real pelo AbortError do último candidato.
|
|
122
158
|
*/
|
|
123
159
|
async fetchComRedundancia(candidatos, fetcher, options) {
|
|
124
|
-
var _a;
|
|
160
|
+
var _a, _b, _c, _d, _e;
|
|
161
|
+
const semFallback = new Set((_a = options.statusSemFallback) !== null && _a !== void 0 ? _a : STATUS_SEM_FALLBACK_PADRAO);
|
|
162
|
+
const externo = (_c = (_b = options.fetchOptions) === null || _b === void 0 ? void 0 : _b.signal) !== null && _c !== void 0 ? _c : null;
|
|
125
163
|
let ultimoErro;
|
|
126
164
|
for (const candidato of candidatos) {
|
|
127
|
-
|
|
165
|
+
if (externo === null || externo === void 0 ? void 0 : externo.aborted) {
|
|
166
|
+
this.log("cancelado pelo chamador: fila interrompida", { url: candidato });
|
|
167
|
+
throw (_d = externo.reason) !== null && _d !== void 0 ? _d : new Error("Requisição cancelada pelo chamador");
|
|
168
|
+
}
|
|
169
|
+
const timeoutMs = this.resolverTimeout(options);
|
|
170
|
+
const tentativa = criarSinal(timeoutMs, externo);
|
|
171
|
+
this.log("fetch de rede", { url: candidato, timeoutMs, rede: detectarRede() });
|
|
128
172
|
try {
|
|
129
|
-
const res = await fetcher(candidato,
|
|
173
|
+
const res = await fetcher(candidato, {
|
|
174
|
+
...options.fetchOptions,
|
|
175
|
+
signal: tentativa.signal,
|
|
176
|
+
});
|
|
130
177
|
if (!res.ok)
|
|
131
|
-
throw new
|
|
178
|
+
throw new HttpError(res.status, candidato);
|
|
179
|
+
const json = (await res.json());
|
|
132
180
|
this.log("fetch OK", { url: candidato, status: res.status });
|
|
133
|
-
return
|
|
181
|
+
return json;
|
|
134
182
|
}
|
|
135
|
-
catch (
|
|
183
|
+
catch (bruto) {
|
|
184
|
+
// Um abort disparado pelo nosso timer chega aqui como AbortError
|
|
185
|
+
// genérico; troca por um erro que diz o que de fato aconteceu.
|
|
186
|
+
const err = tentativa.estourou()
|
|
187
|
+
? erroDeTimeout(candidato, timeoutMs)
|
|
188
|
+
: bruto;
|
|
136
189
|
this.log("fetch falhou", { url: candidato, erro: String(err) });
|
|
137
190
|
ultimoErro = err;
|
|
138
|
-
(
|
|
191
|
+
(_e = options.onFallback) === null || _e === void 0 ? void 0 : _e.call(options, candidato, err);
|
|
192
|
+
if (err instanceof HttpError && semFallback.has(err.status)) {
|
|
193
|
+
this.log("status sem fallback: fila interrompida", {
|
|
194
|
+
url: candidato,
|
|
195
|
+
status: err.status,
|
|
196
|
+
});
|
|
197
|
+
throw err;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
finally {
|
|
201
|
+
// Obrigatório: sem isso sobra um timer de minutos pendurado por
|
|
202
|
+
// tentativa, segurando o event loop no Node e vazando no browser.
|
|
203
|
+
tentativa.cancelar();
|
|
139
204
|
}
|
|
140
205
|
}
|
|
141
206
|
throw ultimoErro;
|
|
@@ -161,17 +226,15 @@ class RequestCache {
|
|
|
161
226
|
}
|
|
162
227
|
throw err;
|
|
163
228
|
}
|
|
164
|
-
// 3. Regra monotônica: só aceita o novo dado se
|
|
229
|
+
// 3. Regra monotônica: só aceita o novo dado se TODAS as chaves que
|
|
230
|
+
// conseguiram votar tiverem avançado.
|
|
165
231
|
if (cached && options.monotonicKey) {
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
if (ambosNumeros && newVal <= oldVal) {
|
|
170
|
-
// Valor não aumentou -> mantém o dado antigo, só renova a expiração.
|
|
232
|
+
const veredito = avaliarMonotonicidade(cached.data, fresh, options.monotonicKey, this.monotonicTypes);
|
|
233
|
+
if (!veredito.avancou) {
|
|
234
|
+
// Alguma chave não avançou -> mantém o dado antigo, só renova a expiração.
|
|
171
235
|
this.log("regra monotônica: valor não cresceu, mantendo dado antigo", {
|
|
172
236
|
url,
|
|
173
|
-
|
|
174
|
-
newVal,
|
|
237
|
+
chaves: veredito.detalhes,
|
|
175
238
|
});
|
|
176
239
|
this.upsert({
|
|
177
240
|
...cached,
|
|
@@ -261,6 +324,260 @@ function getPath(obj, path) {
|
|
|
261
324
|
return undefined;
|
|
262
325
|
}, obj);
|
|
263
326
|
}
|
|
327
|
+
/**
|
|
328
|
+
* Descobre como comparar um valor. A ordem das checagens NÃO pode ser trocada:
|
|
329
|
+
* `Date.parse` aceita strings numéricas curtas como data (`Date.parse("9")` cai
|
|
330
|
+
* em setembro, `Date.parse("10")` em outubro), então um `idg` curto viraria
|
|
331
|
+
* data se a checagem de data viesse antes da numérica. No sentido inverso não
|
|
332
|
+
* há risco: `Number("2026-10-04T18:23:00Z")` é NaN.
|
|
333
|
+
*
|
|
334
|
+
* Devolve `null` quando o valor é incomparável.
|
|
335
|
+
*/
|
|
336
|
+
function classificar(valor) {
|
|
337
|
+
if (typeof valor === "number") {
|
|
338
|
+
return Number.isFinite(valor) ? { tipo: "number", ordem: valor } : null;
|
|
339
|
+
}
|
|
340
|
+
if (typeof valor !== "string")
|
|
341
|
+
return null;
|
|
342
|
+
// String vazia ou em branco é incomparável: `Number("")` e `Number(" ")`
|
|
343
|
+
// devolvem 0, e um campo vazio viraria um "zero" comparável.
|
|
344
|
+
if (valor.trim() === "")
|
|
345
|
+
return null;
|
|
346
|
+
const numero = Number(valor);
|
|
347
|
+
if (Number.isFinite(numero))
|
|
348
|
+
return { tipo: "number", ordem: numero };
|
|
349
|
+
const data = Date.parse(valor);
|
|
350
|
+
if (!Number.isNaN(data))
|
|
351
|
+
return { tipo: "date", ordem: data };
|
|
352
|
+
return { tipo: "string", ordem: valor };
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Classifica um valor segundo um tipo DECLARADO pelo chamador (forma de mapa).
|
|
356
|
+
* Como o tipo veio declarado, não há adivinhação: o valor ou serve, ou a chave
|
|
357
|
+
* é incomparável (e derruba a resposta nova). É também mais tolerante que a
|
|
358
|
+
* detecção por valor — com
|
|
359
|
+
* `"number"`, `100` e `"101"` comparam entre si sem problema, já que o
|
|
360
|
+
* chamador afirmou que aquele campo é numérico.
|
|
361
|
+
*/
|
|
362
|
+
function classificarComTipo(valor, tipo) {
|
|
363
|
+
// String vazia ou em branco nunca é comparável: `Number("")` e `Number(" ")`
|
|
364
|
+
// devolvem 0, e um campo vazio viraria um "zero" comparável.
|
|
365
|
+
const texto = typeof valor === "string" ? valor.trim() : null;
|
|
366
|
+
if (texto === "")
|
|
367
|
+
return null;
|
|
368
|
+
if (tipo === "number") {
|
|
369
|
+
if (typeof valor === "number") {
|
|
370
|
+
return Number.isFinite(valor) ? { tipo, ordem: valor } : null;
|
|
371
|
+
}
|
|
372
|
+
if (texto === null)
|
|
373
|
+
return null;
|
|
374
|
+
const numero = Number(texto);
|
|
375
|
+
return Number.isFinite(numero) ? { tipo, ordem: numero } : null;
|
|
376
|
+
}
|
|
377
|
+
if (tipo === "date") {
|
|
378
|
+
// Um número aqui é lido como epoch em ms.
|
|
379
|
+
if (typeof valor === "number") {
|
|
380
|
+
return Number.isFinite(valor) ? { tipo, ordem: valor } : null;
|
|
381
|
+
}
|
|
382
|
+
if (texto === null)
|
|
383
|
+
return null;
|
|
384
|
+
const data = Date.parse(texto);
|
|
385
|
+
return Number.isNaN(data) ? null : { tipo, ordem: data };
|
|
386
|
+
}
|
|
387
|
+
return texto === null ? null : { tipo, ordem: texto };
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Tipos conhecidos por NOME de chave. É o que permite ao front continuar
|
|
391
|
+
* passando só os caminhos — `["idg", "summary.last_updated"]` — e ainda assim
|
|
392
|
+
* ter `idg` comparado como número e `last_updated` como data, sem depender de
|
|
393
|
+
* adivinhação pelo valor.
|
|
394
|
+
*
|
|
395
|
+
* A busca é feita pelo caminho completo e, se não achar, pelo último trecho
|
|
396
|
+
* dele: "summary.last_updated" cai em "last_updated". Nomes fora desta tabela
|
|
397
|
+
* (e de `monotonicTypes`) continuam sendo detectados pelo valor.
|
|
398
|
+
*/
|
|
399
|
+
const TIPOS_POR_NOME = {
|
|
400
|
+
idg: "number",
|
|
401
|
+
versao: "number",
|
|
402
|
+
ballots_counted: "number",
|
|
403
|
+
last_updated: "date",
|
|
404
|
+
};
|
|
405
|
+
/**
|
|
406
|
+
* Procura o tipo de uma chave na tabela de nomes: primeiro pelo caminho
|
|
407
|
+
* completo ("summary.last_updated"), depois só pelo último trecho
|
|
408
|
+
* ("last_updated").
|
|
409
|
+
*/
|
|
410
|
+
function tipoConhecido(chave, tabela) {
|
|
411
|
+
if (chave in tabela)
|
|
412
|
+
return tabela[chave];
|
|
413
|
+
const ultimo = chave.slice(chave.lastIndexOf(".") + 1);
|
|
414
|
+
return ultimo in tabela ? tabela[ultimo] : null;
|
|
415
|
+
}
|
|
416
|
+
/**
|
|
417
|
+
* Reduz as três formas aceitas de `monotonicKey` a uma lista única, já com o
|
|
418
|
+
* tipo de cada chave resolvido: o declarado no mapa vence; senão vale o que a
|
|
419
|
+
* tabela de nomes souber; senão fica `null` e o tipo é detectado pelo valor.
|
|
420
|
+
*/
|
|
421
|
+
function normalizarChaves(monotonicKey, tabela) {
|
|
422
|
+
const semTipo = (chave) => {
|
|
423
|
+
const conhecido = tipoConhecido(chave, tabela);
|
|
424
|
+
return conhecido
|
|
425
|
+
? { chave, tipo: conhecido, origem: "conhecido" }
|
|
426
|
+
: { chave, tipo: null, origem: "detectado" };
|
|
427
|
+
};
|
|
428
|
+
if (typeof monotonicKey === "string")
|
|
429
|
+
return [semTipo(monotonicKey)];
|
|
430
|
+
if (Array.isArray(monotonicKey))
|
|
431
|
+
return monotonicKey.map(semTipo);
|
|
432
|
+
return Object.entries(monotonicKey).map(([chave, tipo]) => ({
|
|
433
|
+
chave,
|
|
434
|
+
tipo,
|
|
435
|
+
origem: "declarado",
|
|
436
|
+
}));
|
|
437
|
+
}
|
|
438
|
+
/** `a > b`, já sabendo que os dois lados são do mesmo tipo. */
|
|
439
|
+
function maior(a, b) {
|
|
440
|
+
return typeof a === "string" ? a > String(b) : a > Number(b);
|
|
441
|
+
}
|
|
442
|
+
/**
|
|
443
|
+
* Compara todas as chaves monotônicas entre o dado em cache e o dado novo.
|
|
444
|
+
*
|
|
445
|
+
* Verificação estrita: o dado novo só entra quando TODAS as chaves cresceram.
|
|
446
|
+
* Basta uma que não cresça para a resposta ser descartada — e uma chave
|
|
447
|
+
* incomparável (ausente de um dos lados, ilegível, fora do tipo esperado ou
|
|
448
|
+
* com tipos divergentes entre as versões) também não cresceu, então também
|
|
449
|
+
* derruba a resposta. Só entra o que comprovadamente cresceu em todas.
|
|
450
|
+
*/
|
|
451
|
+
function avaliarMonotonicidade(oldData, newData, monotonicKey, tabela) {
|
|
452
|
+
const detalhes = [];
|
|
453
|
+
// Sem nenhuma chave configurada não há regra a aplicar, e o dado novo passa.
|
|
454
|
+
let todasCresceram = true;
|
|
455
|
+
// Avalia TODAS as chaves: nenhuma decide sozinha e nenhuma interrompe o laço,
|
|
456
|
+
// para que o log de debug mostre o estado de cada uma.
|
|
457
|
+
for (const { chave, tipo, origem } of normalizarChaves(monotonicKey, tabela)) {
|
|
458
|
+
const oldVal = getPath(oldData, chave);
|
|
459
|
+
const newVal = getPath(newData, chave);
|
|
460
|
+
const oldCmp = tipo ? classificarComTipo(oldVal, tipo) : classificar(oldVal);
|
|
461
|
+
const newCmp = tipo ? classificarComTipo(newVal, tipo) : classificar(newVal);
|
|
462
|
+
// Quando o tipo é conhecido, ele é quem manda — `100` e `"101"` sob
|
|
463
|
+
// "number" são o mesmo campo. Quando foi detectado pelo valor, tipos
|
|
464
|
+
// divergentes entre as versões (número de um lado, string do outro; string
|
|
465
|
+
// numérica vs. string de data) são sinal de que o campo mudou de forma.
|
|
466
|
+
const divergente = tipo === null &&
|
|
467
|
+
(typeof oldVal !== typeof newVal || (oldCmp === null || oldCmp === void 0 ? void 0 : oldCmp.tipo) !== (newCmp === null || newCmp === void 0 ? void 0 : newCmp.tipo));
|
|
468
|
+
// Incomparável não é "neutro": se a lib não consegue afirmar que a chave
|
|
469
|
+
// cresceu, ela não cresceu, e a resposta nova cai.
|
|
470
|
+
if (oldCmp === null || newCmp === null || divergente) {
|
|
471
|
+
todasCresceram = false;
|
|
472
|
+
detalhes.push({ chave, tipo: null, origem, oldVal, newVal, voto: "incomparável" });
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
const avancou = maior(newCmp.ordem, oldCmp.ordem);
|
|
476
|
+
if (!avancou)
|
|
477
|
+
todasCresceram = false;
|
|
478
|
+
detalhes.push({
|
|
479
|
+
chave,
|
|
480
|
+
tipo: newCmp.tipo,
|
|
481
|
+
origem,
|
|
482
|
+
oldVal,
|
|
483
|
+
newVal,
|
|
484
|
+
voto: avancou ? "avançou" : "não avançou",
|
|
485
|
+
});
|
|
486
|
+
}
|
|
487
|
+
return { avancou: todasCresceram, detalhes };
|
|
488
|
+
}
|
|
489
|
+
/** Status que, por padrão, NÃO acionam o fallback: o servidor já respondeu. */
|
|
490
|
+
const STATUS_SEM_FALLBACK_PADRAO = [404];
|
|
491
|
+
/**
|
|
492
|
+
* Tier list de timeout por qualidade de rede, em ms, aplicado a CADA tentativa.
|
|
493
|
+
*
|
|
494
|
+
* 4g / desconhecida 30 s — base
|
|
495
|
+
* 3g 45 s — 1,5x
|
|
496
|
+
* 2g 60 s — 2x
|
|
497
|
+
* slow-2g 90 s — 3x; borda de cobertura, interior
|
|
498
|
+
*
|
|
499
|
+
* A folga cresce com a rede ruim para não matar uma resposta que ainda viria
|
|
500
|
+
* num client lento. O custo aparece em `fallbackUrls`, porque a fila é
|
|
501
|
+
* sequencial: o pior caso é N x o tier — veja `timeoutMs` em GetFetchOptions.
|
|
502
|
+
*/
|
|
503
|
+
const TIMEOUTS_POR_REDE = {
|
|
504
|
+
"slow-2g": 90000,
|
|
505
|
+
"2g": 60000,
|
|
506
|
+
"3g": 45000,
|
|
507
|
+
"4g": 30000,
|
|
508
|
+
desconhecida: 30000,
|
|
509
|
+
};
|
|
510
|
+
const REDES_CONHECIDAS = ["slow-2g", "2g", "3g", "4g"];
|
|
511
|
+
/**
|
|
512
|
+
* Lê `navigator.connection.effectiveType` (Network Information API).
|
|
513
|
+
* Só Chromium e Android implementam; Safari, Firefox e SSR devolvem `null` e
|
|
514
|
+
* caem no tier `desconhecida`.
|
|
515
|
+
*/
|
|
516
|
+
function detectarRede() {
|
|
517
|
+
var _a, _b;
|
|
518
|
+
try {
|
|
519
|
+
if (typeof navigator === "undefined")
|
|
520
|
+
return null;
|
|
521
|
+
const nav = navigator;
|
|
522
|
+
const conexao = (_b = (_a = nav.connection) !== null && _a !== void 0 ? _a : nav.mozConnection) !== null && _b !== void 0 ? _b : nav.webkitConnection;
|
|
523
|
+
const tipo = conexao === null || conexao === void 0 ? void 0 : conexao.effectiveType;
|
|
524
|
+
if (!tipo)
|
|
525
|
+
return null;
|
|
526
|
+
return REDES_CONHECIDAS.indexOf(tipo) >= 0
|
|
527
|
+
? tipo
|
|
528
|
+
: null;
|
|
529
|
+
}
|
|
530
|
+
catch (_c) {
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
/** Erro dedicado do timeout, para não subir como um `AbortError` sem contexto. */
|
|
535
|
+
function erroDeTimeout(url, timeoutMs) {
|
|
536
|
+
const err = new Error(`Timeout de ${timeoutMs}ms ao buscar ${url}`);
|
|
537
|
+
err.name = "TimeoutError";
|
|
538
|
+
return err;
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Monta o `signal` de UMA tentativa: o timeout da vez encadeado com o `signal`
|
|
542
|
+
* que o chamador passou em `fetchOptions`, para que cancelar por fora continue
|
|
543
|
+
* funcionando.
|
|
544
|
+
*
|
|
545
|
+
* Feito na mão com `AbortController` em vez de `AbortSignal.any` +
|
|
546
|
+
* `AbortSignal.timeout`, que só existem em Safari 17.4+ e Node 20+. Também
|
|
547
|
+
* evita o problema de um `AbortSignal.timeout` passado pelo chamador: o
|
|
548
|
+
* relógio dele começa na criação, então viraria um orçamento da fila inteira,
|
|
549
|
+
* deixando os fallbacks sem tempo nenhum.
|
|
550
|
+
*/
|
|
551
|
+
function criarSinal(timeoutMs, externo) {
|
|
552
|
+
if (!(timeoutMs > 0) || typeof AbortController === "undefined") {
|
|
553
|
+
return {
|
|
554
|
+
signal: externo !== null && externo !== void 0 ? externo : undefined,
|
|
555
|
+
estourou: () => false,
|
|
556
|
+
cancelar: () => { },
|
|
557
|
+
};
|
|
558
|
+
}
|
|
559
|
+
const ctrl = new AbortController();
|
|
560
|
+
let porTimeout = false;
|
|
561
|
+
const timer = setTimeout(() => {
|
|
562
|
+
porTimeout = true;
|
|
563
|
+
ctrl.abort();
|
|
564
|
+
}, timeoutMs);
|
|
565
|
+
const repassar = () => ctrl.abort();
|
|
566
|
+
if (externo) {
|
|
567
|
+
if (externo.aborted)
|
|
568
|
+
ctrl.abort();
|
|
569
|
+
else
|
|
570
|
+
externo.addEventListener("abort", repassar);
|
|
571
|
+
}
|
|
572
|
+
return {
|
|
573
|
+
signal: ctrl.signal,
|
|
574
|
+
estourou: () => porTimeout,
|
|
575
|
+
cancelar: () => {
|
|
576
|
+
clearTimeout(timer);
|
|
577
|
+
externo === null || externo === void 0 ? void 0 : externo.removeEventListener("abort", repassar);
|
|
578
|
+
},
|
|
579
|
+
};
|
|
580
|
+
}
|
|
264
581
|
function isQuotaError(err) {
|
|
265
582
|
return (err instanceof Error &&
|
|
266
583
|
(err.name === "QuotaExceededError" ||
|
|
@@ -293,5 +610,5 @@ function getDefaultStorage() {
|
|
|
293
610
|
/** Instância pronta para uso, caso não queira configurar nada. */
|
|
294
611
|
const requestCache = new RequestCache();
|
|
295
612
|
|
|
296
|
-
export { RequestCache, requestCache };
|
|
613
|
+
export { HttpError, RequestCache, TIMEOUTS_POR_REDE, requestCache };
|
|
297
614
|
//# sourceMappingURL=index.esm.js.map
|