@sevn/reqcache 1.0.0 → 1.2.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 +71 -0
- package/dist/index.d.ts +38 -1
- package/dist/index.esm.js +73 -10
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +73 -10
- package/dist/index.js.map +1 -1
- package/dist/request-cache.d.ts +37 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,65 @@ const resultado = await requestCache.getFetch(
|
|
|
43
43
|
Se, na revalidação, a API devolver um `votosApurados` **menor ou igual** ao
|
|
44
44
|
guardado, a resposta nova é descartada e o dado antigo é mantido.
|
|
45
45
|
|
|
46
|
+
## Redundância entre domínios (fallback)
|
|
47
|
+
|
|
48
|
+
Se o endpoint principal cair, a lib pode tentar automaticamente uma lista de
|
|
49
|
+
URLs alternativas (outra API/domínio) antes de desistir:
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const resultado = await requestCache.getFetch(
|
|
53
|
+
"https://api1.eleicoes.gov/resultado/presidente",
|
|
54
|
+
10_000,
|
|
55
|
+
{ fallbackUrls: ["https://api2.eleicoes.gov/resultado/presidente"] }
|
|
56
|
+
);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
- As URLs são tentadas em ordem; a primeira que responder com sucesso é usada.
|
|
60
|
+
- A chave do cache continua sendo a URL primária (`url`) — as alternativas não
|
|
61
|
+
criam entradas novas no cache.
|
|
62
|
+
- Se **todas** falharem, entra a regra de `staleOnError` normalmente (devolve o
|
|
63
|
+
último dado válido, se houver, ou lança o erro da última tentativa).
|
|
64
|
+
- Use `onFallback(url, erro)` para logar/observar quando uma URL falhou e a lib
|
|
65
|
+
está tentando a próxima:
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
await requestCache.getFetch(url, 10_000, {
|
|
69
|
+
fallbackUrls: ["https://api2.eleicoes.gov/resultado/presidente"],
|
|
70
|
+
onFallback: (urlComFalha, erro) => console.warn("caiu:", urlComFalha, erro),
|
|
71
|
+
});
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Modo debug
|
|
75
|
+
|
|
76
|
+
Para ver exatamente o que a lib está fazendo — se uma chamada foi atendida
|
|
77
|
+
pelo cache, disparou uma requisição de rede, entrou em deduplicação, etc. —
|
|
78
|
+
ligue o modo debug:
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const cache = new RequestCache({ debug: true });
|
|
82
|
+
|
|
83
|
+
await cache.getFetch(url, 10_000); // imprime no console cada passo
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Útil para identificar se estão sendo feitas mais chamadas de rede do que o
|
|
87
|
+
necessário. Pode ser ligado/desligado em runtime, sem recriar a instância:
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
cache.setDebug(true);
|
|
91
|
+
// ...
|
|
92
|
+
cache.setDebug(false);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Por padrão os logs vão para `console.debug`, prefixados com `[reqcache]`. Para
|
|
96
|
+
mandar para outro lugar (ex.: seu sistema de logging), passe um `logger`:
|
|
97
|
+
|
|
98
|
+
```ts
|
|
99
|
+
const cache = new RequestCache({
|
|
100
|
+
debug: true,
|
|
101
|
+
logger: (mensagem, detalhes) => meuLogger.debug(mensagem, detalhes),
|
|
102
|
+
});
|
|
103
|
+
```
|
|
104
|
+
|
|
46
105
|
## Configuração
|
|
47
106
|
|
|
48
107
|
```ts
|
|
@@ -54,6 +113,16 @@ const cache = new RequestCache({
|
|
|
54
113
|
});
|
|
55
114
|
```
|
|
56
115
|
|
|
116
|
+
### Opções de `new RequestCache(config)`
|
|
117
|
+
|
|
118
|
+
| Opção | Tipo | Padrão | Descrição |
|
|
119
|
+
| ------------ | --------------------------- | ------------ | ----------------------------------------------------------------- |
|
|
120
|
+
| `storage` | `StorageLike` | localStorage | Storage customizado (permite trocar por IndexedDB, por exemplo). |
|
|
121
|
+
| `storageKey` | `string` | `"reqcache"` | Chave única onde o cache é guardado. |
|
|
122
|
+
| `maxEntries` | `number` | `100` | Máximo de rotas em cache; ao exceder, remove a menos usada (LRU). |
|
|
123
|
+
| `debug` | `boolean` | `false` | Liga logs detalhados de cada operação. Veja "Modo debug" acima. |
|
|
124
|
+
| `logger` | `(msg, detalhes) => void` | `console.debug` | Logger customizado usado quando `debug: true`. |
|
|
125
|
+
|
|
57
126
|
### Opções de `getFetch(url, ttlMs, options)`
|
|
58
127
|
|
|
59
128
|
| Opção | Tipo | Padrão | Descrição |
|
|
@@ -62,6 +131,8 @@ const cache = new RequestCache({
|
|
|
62
131
|
| `fetchOptions` | `RequestInit` | — | Repassado ao `fetch` nativo (headers, method, signal...). |
|
|
63
132
|
| `staleOnError` | `boolean` | `true` | Devolve o dado antigo se a revalidação falhar. |
|
|
64
133
|
| `fetcher` | `typeof fetch`| `fetch`| `fetch` customizado (útil para testes). |
|
|
134
|
+
| `fallbackUrls` | `string[]` | — | URLs alternativas (outros domínios) tentadas em ordem se `url` falhar. |
|
|
135
|
+
| `onFallback` | `(url, erro) => void` | — | Chamado quando uma URL falha e a lib vai tentar a próxima. |
|
|
65
136
|
|
|
66
137
|
## Limpeza
|
|
67
138
|
|
package/dist/index.d.ts
CHANGED
|
@@ -47,6 +47,17 @@ interface GetFetchOptions {
|
|
|
47
47
|
staleOnError?: boolean;
|
|
48
48
|
/** fetch customizado — útil para testes ou ambientes sem fetch global. */
|
|
49
49
|
fetcher?: typeof fetch;
|
|
50
|
+
/**
|
|
51
|
+
* URLs alternativas (outros domínios/APIs) tentadas em ordem, caso `url`
|
|
52
|
+
* falhe. A rota do cache continua sendo `url` — as alternativas só entram
|
|
53
|
+
* na requisição, não criam entradas novas no cache.
|
|
54
|
+
*/
|
|
55
|
+
fallbackUrls?: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Chamado quando uma URL falha e a lib vai tentar a próxima da lista
|
|
58
|
+
* (`url` + `fallbackUrls`). Útil para observabilidade/log.
|
|
59
|
+
*/
|
|
60
|
+
onFallback?: (failedUrl: string, error: unknown) => void;
|
|
50
61
|
}
|
|
51
62
|
/** Uma entrada do cache. `rota` é a chave identificadora. */
|
|
52
63
|
interface CacheEntry<T = unknown> {
|
|
@@ -56,6 +67,10 @@ interface CacheEntry<T = unknown> {
|
|
|
56
67
|
expiresAt: number;
|
|
57
68
|
lastAccess: number;
|
|
58
69
|
}
|
|
70
|
+
/** Uma linha de log do modo debug. */
|
|
71
|
+
interface DebugLogFn {
|
|
72
|
+
(message: string, details?: Record<string, unknown>): void;
|
|
73
|
+
}
|
|
59
74
|
interface RequestCacheConfig {
|
|
60
75
|
/** Storage a usar. Padrão: localStorage (se disponível). */
|
|
61
76
|
storage?: StorageLike;
|
|
@@ -63,6 +78,16 @@ interface RequestCacheConfig {
|
|
|
63
78
|
storageKey?: string;
|
|
64
79
|
/** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */
|
|
65
80
|
maxEntries?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Se true, imprime logs detalhados de cada operação (fetch de rede,
|
|
83
|
+
* cache hit, deduplicação, fallback, LRU, etc.). Padrão false.
|
|
84
|
+
* Útil para identificar se estão sendo feitas mais chamadas de rede
|
|
85
|
+
* do que o necessário. Pode ser ligado/desligado em runtime com
|
|
86
|
+
* `setDebug()`.
|
|
87
|
+
*/
|
|
88
|
+
debug?: boolean;
|
|
89
|
+
/** Logger customizado usado quando `debug` está ligado. Padrão: `console.debug`. */
|
|
90
|
+
logger?: DebugLogFn;
|
|
66
91
|
}
|
|
67
92
|
declare class RequestCache {
|
|
68
93
|
private storage;
|
|
@@ -71,7 +96,11 @@ declare class RequestCache {
|
|
|
71
96
|
private inflight;
|
|
72
97
|
/** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
|
|
73
98
|
private tick;
|
|
99
|
+
private debug;
|
|
100
|
+
private logger;
|
|
74
101
|
constructor(config?: RequestCacheConfig);
|
|
102
|
+
/** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
|
|
103
|
+
setDebug(enabled: boolean): void;
|
|
75
104
|
/**
|
|
76
105
|
* Busca uma URL usando cache.
|
|
77
106
|
* @param url Rota da requisição (também é a chave do cache).
|
|
@@ -91,6 +120,14 @@ declare class RequestCache {
|
|
|
91
120
|
clear(): void;
|
|
92
121
|
/** Retorna um número de acesso estritamente crescente (para o LRU). */
|
|
93
122
|
private nextAccess;
|
|
123
|
+
/** Emite uma linha de log, só quando o modo debug está ligado. */
|
|
124
|
+
private log;
|
|
125
|
+
/**
|
|
126
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
127
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
128
|
+
* último erro.
|
|
129
|
+
*/
|
|
130
|
+
private fetchComRedundancia;
|
|
94
131
|
private revalidate;
|
|
95
132
|
/** Insere ou atualiza uma rota, aplicando o limite LRU. */
|
|
96
133
|
private upsert;
|
|
@@ -103,4 +140,4 @@ declare class RequestCache {
|
|
|
103
140
|
declare const requestCache: RequestCache;
|
|
104
141
|
|
|
105
142
|
export { RequestCache, requestCache };
|
|
106
|
-
export type { CacheEntry, GetFetchOptions, RequestCacheConfig, StorageLike };
|
|
143
|
+
export type { CacheEntry, DebugLogFn, GetFetchOptions, RequestCacheConfig, StorageLike };
|
package/dist/index.esm.js
CHANGED
|
@@ -28,13 +28,19 @@
|
|
|
28
28
|
*/
|
|
29
29
|
class RequestCache {
|
|
30
30
|
constructor(config = {}) {
|
|
31
|
-
var _a, _b, _c;
|
|
31
|
+
var _a, _b, _c, _d, _e;
|
|
32
32
|
this.inflight = new Map();
|
|
33
33
|
/** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
|
|
34
34
|
this.tick = 0;
|
|
35
35
|
this.storageKey = (_a = config.storageKey) !== null && _a !== void 0 ? _a : "reqcache";
|
|
36
36
|
this.maxEntries = (_b = config.maxEntries) !== null && _b !== void 0 ? _b : 100;
|
|
37
37
|
this.storage = (_c = config.storage) !== null && _c !== void 0 ? _c : getDefaultStorage();
|
|
38
|
+
this.debug = (_d = config.debug) !== null && _d !== void 0 ? _d : false;
|
|
39
|
+
this.logger = (_e = config.logger) !== null && _e !== void 0 ? _e : defaultLogger;
|
|
40
|
+
}
|
|
41
|
+
/** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
|
|
42
|
+
setDebug(enabled) {
|
|
43
|
+
this.debug = enabled;
|
|
38
44
|
}
|
|
39
45
|
/**
|
|
40
46
|
* Busca uma URL usando cache.
|
|
@@ -50,13 +56,17 @@ class RequestCache {
|
|
|
50
56
|
if (cached && now < cached.expiresAt) {
|
|
51
57
|
cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU
|
|
52
58
|
this.writeAll(all);
|
|
59
|
+
this.log("cache hit", { url, expiresAt: new Date(cached.expiresAt).toISOString() });
|
|
53
60
|
return cached.data;
|
|
54
61
|
}
|
|
55
62
|
// 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,
|
|
56
63
|
// todas as chamadas concorrentes aguardam a mesma Promise.
|
|
57
64
|
const pending = this.inflight.get(url);
|
|
58
|
-
if (pending)
|
|
65
|
+
if (pending) {
|
|
66
|
+
this.log("dedup: aguardando requisição em andamento", { url });
|
|
59
67
|
return pending;
|
|
68
|
+
}
|
|
69
|
+
this.log(cached ? "cache expirado, revalidando" : "cache miss", { url });
|
|
60
70
|
const promise = this.revalidate(url, ttlMs, options, cached !== null && cached !== void 0 ? cached : null)
|
|
61
71
|
.finally(() => this.inflight.delete(url));
|
|
62
72
|
this.inflight.set(url, promise);
|
|
@@ -77,7 +87,10 @@ class RequestCache {
|
|
|
77
87
|
const all = this.readAll();
|
|
78
88
|
const mantidas = all.filter((e) => e.expiresAt > limite);
|
|
79
89
|
this.writeAll(mantidas);
|
|
80
|
-
|
|
90
|
+
const removidas = all.length - mantidas.length;
|
|
91
|
+
if (removidas > 0)
|
|
92
|
+
this.log("cleanup: rotas expiradas removidas", { removidas });
|
|
93
|
+
return removidas;
|
|
81
94
|
}
|
|
82
95
|
/** Esvazia todo o cache. */
|
|
83
96
|
clear() {
|
|
@@ -96,19 +109,49 @@ class RequestCache {
|
|
|
96
109
|
this.tick = Math.max(Date.now(), this.tick + 1);
|
|
97
110
|
return this.tick;
|
|
98
111
|
}
|
|
112
|
+
/** Emite uma linha de log, só quando o modo debug está ligado. */
|
|
113
|
+
log(message, details) {
|
|
114
|
+
if (!this.debug)
|
|
115
|
+
return;
|
|
116
|
+
this.logger(message, details);
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
120
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
121
|
+
* último erro.
|
|
122
|
+
*/
|
|
123
|
+
async fetchComRedundancia(candidatos, fetcher, options) {
|
|
124
|
+
var _a;
|
|
125
|
+
let ultimoErro;
|
|
126
|
+
for (const candidato of candidatos) {
|
|
127
|
+
this.log("fetch de rede", { url: candidato });
|
|
128
|
+
try {
|
|
129
|
+
const res = await fetcher(candidato, options.fetchOptions);
|
|
130
|
+
if (!res.ok)
|
|
131
|
+
throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);
|
|
132
|
+
this.log("fetch OK", { url: candidato, status: res.status });
|
|
133
|
+
return (await res.json());
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
this.log("fetch falhou", { url: candidato, erro: String(err) });
|
|
137
|
+
ultimoErro = err;
|
|
138
|
+
(_a = options.onFallback) === null || _a === void 0 ? void 0 : _a.call(options, candidato, err);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
throw ultimoErro;
|
|
142
|
+
}
|
|
99
143
|
async revalidate(url, ttlMs, options, cached) {
|
|
100
|
-
var _a, _b;
|
|
144
|
+
var _a, _b, _c;
|
|
101
145
|
const fetcher = (_a = options.fetcher) !== null && _a !== void 0 ? _a : fetch;
|
|
146
|
+
const candidatos = [url, ...((_b = options.fallbackUrls) !== null && _b !== void 0 ? _b : [])];
|
|
102
147
|
let fresh;
|
|
103
148
|
try {
|
|
104
|
-
|
|
105
|
-
if (!res.ok)
|
|
106
|
-
throw new Error(`HTTP ${res.status} ao buscar ${url}`);
|
|
107
|
-
fresh = (await res.json());
|
|
149
|
+
fresh = await this.fetchComRedundancia(candidatos, fetcher, options);
|
|
108
150
|
}
|
|
109
151
|
catch (err) {
|
|
110
|
-
//
|
|
111
|
-
if (cached && ((
|
|
152
|
+
// Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.
|
|
153
|
+
if (cached && ((_c = options.staleOnError) !== null && _c !== void 0 ? _c : true)) {
|
|
154
|
+
this.log("staleOnError: devolvendo dado antigo", { url });
|
|
112
155
|
this.upsert({
|
|
113
156
|
...cached,
|
|
114
157
|
expiresAt: Date.now() + ttlMs,
|
|
@@ -125,6 +168,11 @@ class RequestCache {
|
|
|
125
168
|
const ambosNumeros = typeof oldVal === "number" && typeof newVal === "number";
|
|
126
169
|
if (ambosNumeros && newVal <= oldVal) {
|
|
127
170
|
// Valor não aumentou -> mantém o dado antigo, só renova a expiração.
|
|
171
|
+
this.log("regra monotônica: valor não cresceu, mantendo dado antigo", {
|
|
172
|
+
url,
|
|
173
|
+
oldVal,
|
|
174
|
+
newVal,
|
|
175
|
+
});
|
|
128
176
|
this.upsert({
|
|
129
177
|
...cached,
|
|
130
178
|
expiresAt: Date.now() + ttlMs,
|
|
@@ -135,6 +183,7 @@ class RequestCache {
|
|
|
135
183
|
}
|
|
136
184
|
// 4. Grava e devolve o dado novo.
|
|
137
185
|
const now = Date.now();
|
|
186
|
+
this.log("cache gravado", { url, ttlMs });
|
|
138
187
|
this.upsert({
|
|
139
188
|
rota: url,
|
|
140
189
|
data: fresh,
|
|
@@ -159,6 +208,7 @@ class RequestCache {
|
|
|
159
208
|
if (all[i].lastAccess < all[idxMaisAntigo].lastAccess)
|
|
160
209
|
idxMaisAntigo = i;
|
|
161
210
|
}
|
|
211
|
+
this.log("LRU: removendo rota menos usada", { url: all[idxMaisAntigo].rota });
|
|
162
212
|
all.splice(idxMaisAntigo, 1);
|
|
163
213
|
}
|
|
164
214
|
}
|
|
@@ -187,6 +237,10 @@ class RequestCache {
|
|
|
187
237
|
if (isQuotaError(err) && all.length > 0) {
|
|
188
238
|
const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);
|
|
189
239
|
reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade
|
|
240
|
+
this.log("quota excedida: descartando metade das entradas mais antigas", {
|
|
241
|
+
totalAntes: all.length,
|
|
242
|
+
totalDepois: reduzido.length,
|
|
243
|
+
});
|
|
190
244
|
try {
|
|
191
245
|
this.storage.setItem(this.storageKey, JSON.stringify(reduzido));
|
|
192
246
|
}
|
|
@@ -212,6 +266,15 @@ function isQuotaError(err) {
|
|
|
212
266
|
(err.name === "QuotaExceededError" ||
|
|
213
267
|
err.name === "NS_ERROR_DOM_QUOTA_REACHED"));
|
|
214
268
|
}
|
|
269
|
+
/** Logger padrão do modo debug: imprime no console com um prefixo fixo. */
|
|
270
|
+
function defaultLogger(message, details) {
|
|
271
|
+
if (details) {
|
|
272
|
+
console.debug(`[reqcache] ${message}`, details);
|
|
273
|
+
}
|
|
274
|
+
else {
|
|
275
|
+
console.debug(`[reqcache] ${message}`);
|
|
276
|
+
}
|
|
277
|
+
}
|
|
215
278
|
/** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */
|
|
216
279
|
function getDefaultStorage() {
|
|
217
280
|
try {
|
package/dist/index.esm.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.esm.js","sources":["../src/request-cache.ts"],"sourcesContent":["/**\n * request-cache\n * -------------\n * Cache de requisições HTTP em localStorage, pensado para cenários de\n * alto volume em curto período (ex.: apuração de eleições).\n *\n * Modelo de armazenamento:\n * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.\n * Cada objeto é indexado pela `rota` (a URL requisitada):\n * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]\n *\n * Comportamento:\n * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.\n * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.\n * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).\n * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico\n * dessa chave for MAIOR que o guardado; senão mantém o antigo.\n *\n * Proteção de espaço (limite de ~5 MB do localStorage):\n * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos\n * recentemente usada (LRU). Roda sozinho, sem timer.\n * - Em erro de quota, remove as entradas mais antigas e tenta de novo.\n * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.\n *\n * Extras para o caso de eleição:\n * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.\n * - staleOnError: se a rede/API falhar, devolve o último dado válido.\n */\n\nexport interface StorageLike {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface GetFetchOptions {\n /**\n * Caminho da chave numérica monotônica. Suporta aninhamento com ponto.\n * Ex.: \"total\", \"resultado.votosApurados\", \"data.candidato.votos\".\n * Se o valor novo NÃO for maior que o antigo, mantém o dado em cache.\n */\n monotonicKey?: string;\n\n /** Opções nativas repassadas ao fetch (headers, method, body, signal...). */\n fetchOptions?: RequestInit;\n\n /**\n * Se true (padrão), quando a nova requisição falhar e existir dado antigo,\n * devolve esse dado antigo em vez de lançar erro.\n */\n staleOnError?: boolean;\n\n /** fetch customizado — útil para testes ou ambientes sem fetch global. */\n fetcher?: typeof fetch;\n}\n\n/** Uma entrada do cache. `rota` é a chave identificadora. */\nexport interface CacheEntry<T = unknown> {\n rota: string;\n data: T;\n createdAt: number;\n expiresAt: number;\n lastAccess: number;\n}\n\nexport interface RequestCacheConfig {\n /** Storage a usar. Padrão: localStorage (se disponível). */\n storage?: StorageLike;\n /** Chave única onde o array é guardado. Padrão \"reqcache\". */\n storageKey?: string;\n /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */\n maxEntries?: number;\n}\n\nexport class RequestCache {\n private storage: StorageLike | null;\n private storageKey: string;\n private maxEntries: number;\n private inflight = new Map<string, Promise<unknown>>();\n /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */\n private tick = 0;\n\n constructor(config: RequestCacheConfig = {}) {\n this.storageKey = config.storageKey ?? \"reqcache\";\n this.maxEntries = config.maxEntries ?? 100;\n this.storage = config.storage ?? getDefaultStorage();\n }\n\n /**\n * Busca uma URL usando cache.\n * @param url Rota da requisição (também é a chave do cache).\n * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).\n * @param options Regra monotônica, opções de fetch, etc.\n */\n async getFetch<T = unknown>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions = {},\n ): Promise<T> {\n const now = Date.now();\n const all = this.readAll();\n const cached = all.find((e) => e.rota === url) as CacheEntry<T> | undefined;\n\n // 1. Cache ainda válido -> devolve sem tocar na rede.\n if (cached && now < cached.expiresAt) {\n cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU\n this.writeAll(all);\n return cached.data;\n }\n\n // 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,\n // todas as chamadas concorrentes aguardam a mesma Promise.\n const pending = this.inflight.get(url) as Promise<T> | undefined;\n if (pending) return pending;\n\n const promise = this.revalidate<T>(url, ttlMs, options, cached ?? null)\n .finally(() => this.inflight.delete(url));\n\n this.inflight.set(url, promise);\n return promise;\n }\n\n /** Remove uma rota específica do cache. */\n invalidate(url: string): void {\n const all = this.readAll().filter((e) => e.rota !== url);\n this.writeAll(all);\n }\n\n /**\n * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).\n * Chame na inicialização do app, ou de tempos em tempos.\n * @returns quantidade de rotas removidas.\n */\n cleanup(graceSeconds = 3600): number {\n const limite = Date.now() - graceSeconds * 1000;\n const all = this.readAll();\n const mantidas = all.filter((e) => e.expiresAt > limite);\n this.writeAll(mantidas);\n return all.length - mantidas.length;\n }\n\n /** Esvazia todo o cache. */\n clear(): void {\n if (!this.storage) return;\n try {\n this.storage.removeItem(this.storageKey);\n } catch {\n /* storage indisponível */\n }\n }\n\n // --- interno ------------------------------------------------------------\n\n /** Retorna um número de acesso estritamente crescente (para o LRU). */\n private nextAccess(): number {\n this.tick = Math.max(Date.now(), this.tick + 1);\n return this.tick;\n }\n\n private async revalidate<T>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions,\n cached: CacheEntry<T> | null,\n ): Promise<T> {\n const fetcher = options.fetcher ?? fetch;\n let fresh: T;\n\n try {\n const res = await fetcher(url, options.fetchOptions);\n if (!res.ok) throw new Error(`HTTP ${res.status} ao buscar ${url}`);\n fresh = (await res.json()) as T;\n } catch (err) {\n // Rede/API falhou. Se temos dado antigo e staleOnError, devolve o antigo.\n if (cached && (options.staleOnError ?? true)) {\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n throw err;\n }\n\n // 3. Regra monotônica: só aceita o novo dado se a chave numérica CRESCEU.\n if (cached && options.monotonicKey) {\n const oldVal = getPath(cached.data, options.monotonicKey);\n const newVal = getPath(fresh, options.monotonicKey);\n const ambosNumeros =\n typeof oldVal === \"number\" && typeof newVal === \"number\";\n\n if (ambosNumeros && (newVal as number) <= (oldVal as number)) {\n // Valor não aumentou -> mantém o dado antigo, só renova a expiração.\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n }\n\n // 4. Grava e devolve o dado novo.\n const now = Date.now();\n this.upsert<T>({\n rota: url,\n data: fresh,\n createdAt: now,\n expiresAt: now + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return fresh;\n }\n\n /** Insere ou atualiza uma rota, aplicando o limite LRU. */\n private upsert<T>(entry: CacheEntry<T>): void {\n const all = this.readAll().filter((e) => e.rota !== entry.rota);\n all.push(entry);\n this.evictIfNeeded(all);\n this.writeAll(all);\n }\n\n /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */\n private evictIfNeeded(all: CacheEntry[]): void {\n while (all.length > this.maxEntries) {\n let idxMaisAntigo = 0;\n for (let i = 1; i < all.length; i++) {\n if (all[i].lastAccess < all[idxMaisAntigo].lastAccess) idxMaisAntigo = i;\n }\n all.splice(idxMaisAntigo, 1);\n }\n }\n\n private readAll(): CacheEntry[] {\n if (!this.storage) return [];\n try {\n const raw = this.storage.getItem(this.storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? (parsed as CacheEntry[]) : [];\n } catch {\n return []; // JSON corrompido -> trata como cache vazio\n }\n }\n\n private writeAll(all: CacheEntry[]): void {\n if (!this.storage) return;\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(all));\n } catch (err) {\n // Quota estourada: remove os mais antigos e tenta de novo.\n if (isQuotaError(err) && all.length > 0) {\n const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);\n reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(reduzido));\n } catch {\n /* ainda assim falhou -> desiste de persistir; dados já retornam */\n }\n }\n // Falha silenciosa: os dados ainda são devolvidos ao chamador.\n }\n }\n}\n\n/** Lê um caminho aninhado (\"a.b.c\") de um objeto de forma segura. */\nfunction getPath(obj: unknown, path: string): unknown {\n return path.split(\".\").reduce<unknown>((acc, k) => {\n if (acc && typeof acc === \"object\" && k in (acc as object)) {\n return (acc as Record<string, unknown>)[k];\n }\n return undefined;\n }, obj);\n}\n\nfunction isQuotaError(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.name === \"QuotaExceededError\" ||\n err.name === \"NS_ERROR_DOM_QUOTA_REACHED\")\n );\n}\n\n/** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */\nfunction getDefaultStorage(): StorageLike | null {\n try {\n if (typeof localStorage !== \"undefined\") {\n const probe = \"__reqcache_probe__\";\n localStorage.setItem(probe, \"1\");\n localStorage.removeItem(probe);\n return localStorage;\n }\n } catch {\n /* Safari em modo privado antigo lança aqui */\n }\n return null;\n}\n\n/** Instância pronta para uso, caso não queira configurar nada. */\nexport const requestCache = new RequestCache();\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.esm.js","sources":["../src/request-cache.ts"],"sourcesContent":["/**\n * request-cache\n * -------------\n * Cache de requisições HTTP em localStorage, pensado para cenários de\n * alto volume em curto período (ex.: apuração de eleições).\n *\n * Modelo de armazenamento:\n * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.\n * Cada objeto é indexado pela `rota` (a URL requisitada):\n * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]\n *\n * Comportamento:\n * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.\n * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.\n * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).\n * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico\n * dessa chave for MAIOR que o guardado; senão mantém o antigo.\n *\n * Proteção de espaço (limite de ~5 MB do localStorage):\n * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos\n * recentemente usada (LRU). Roda sozinho, sem timer.\n * - Em erro de quota, remove as entradas mais antigas e tenta de novo.\n * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.\n *\n * Extras para o caso de eleição:\n * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.\n * - staleOnError: se a rede/API falhar, devolve o último dado válido.\n */\n\nexport interface StorageLike {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface GetFetchOptions {\n /**\n * Caminho da chave numérica monotônica. Suporta aninhamento com ponto.\n * Ex.: \"total\", \"resultado.votosApurados\", \"data.candidato.votos\".\n * Se o valor novo NÃO for maior que o antigo, mantém o dado em cache.\n */\n monotonicKey?: string;\n\n /** Opções nativas repassadas ao fetch (headers, method, body, signal...). */\n fetchOptions?: RequestInit;\n\n /**\n * Se true (padrão), quando a nova requisição falhar e existir dado antigo,\n * devolve esse dado antigo em vez de lançar erro.\n */\n staleOnError?: boolean;\n\n /** fetch customizado — útil para testes ou ambientes sem fetch global. */\n fetcher?: typeof fetch;\n\n /**\n * URLs alternativas (outros domínios/APIs) tentadas em ordem, caso `url`\n * falhe. A rota do cache continua sendo `url` — as alternativas só entram\n * na requisição, não criam entradas novas no cache.\n */\n fallbackUrls?: string[];\n\n /**\n * Chamado quando uma URL falha e a lib vai tentar a próxima da lista\n * (`url` + `fallbackUrls`). Útil para observabilidade/log.\n */\n onFallback?: (failedUrl: string, error: unknown) => void;\n}\n\n/** Uma entrada do cache. `rota` é a chave identificadora. */\nexport interface CacheEntry<T = unknown> {\n rota: string;\n data: T;\n createdAt: number;\n expiresAt: number;\n lastAccess: number;\n}\n\n/** Uma linha de log do modo debug. */\nexport interface DebugLogFn {\n (message: string, details?: Record<string, unknown>): void;\n}\n\nexport interface RequestCacheConfig {\n /** Storage a usar. Padrão: localStorage (se disponível). */\n storage?: StorageLike;\n /** Chave única onde o array é guardado. Padrão \"reqcache\". */\n storageKey?: string;\n /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */\n maxEntries?: number;\n\n /**\n * Se true, imprime logs detalhados de cada operação (fetch de rede,\n * cache hit, deduplicação, fallback, LRU, etc.). Padrão false.\n * Útil para identificar se estão sendo feitas mais chamadas de rede\n * do que o necessário. Pode ser ligado/desligado em runtime com\n * `setDebug()`.\n */\n debug?: boolean;\n\n /** Logger customizado usado quando `debug` está ligado. Padrão: `console.debug`. */\n logger?: DebugLogFn;\n}\n\nexport class RequestCache {\n private storage: StorageLike | null;\n private storageKey: string;\n private maxEntries: number;\n private inflight = new Map<string, Promise<unknown>>();\n /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */\n private tick = 0;\n private debug: boolean;\n private logger: DebugLogFn;\n\n constructor(config: RequestCacheConfig = {}) {\n this.storageKey = config.storageKey ?? \"reqcache\";\n this.maxEntries = config.maxEntries ?? 100;\n this.storage = config.storage ?? getDefaultStorage();\n this.debug = config.debug ?? false;\n this.logger = config.logger ?? defaultLogger;\n }\n\n /** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */\n setDebug(enabled: boolean): void {\n this.debug = enabled;\n }\n\n /**\n * Busca uma URL usando cache.\n * @param url Rota da requisição (também é a chave do cache).\n * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).\n * @param options Regra monotônica, opções de fetch, etc.\n */\n async getFetch<T = unknown>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions = {},\n ): Promise<T> {\n const now = Date.now();\n const all = this.readAll();\n const cached = all.find((e) => e.rota === url) as CacheEntry<T> | undefined;\n\n // 1. Cache ainda válido -> devolve sem tocar na rede.\n if (cached && now < cached.expiresAt) {\n cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU\n this.writeAll(all);\n this.log(\"cache hit\", { url, expiresAt: new Date(cached.expiresAt).toISOString() });\n return cached.data;\n }\n\n // 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,\n // todas as chamadas concorrentes aguardam a mesma Promise.\n const pending = this.inflight.get(url) as Promise<T> | undefined;\n if (pending) {\n this.log(\"dedup: aguardando requisição em andamento\", { url });\n return pending;\n }\n\n this.log(cached ? \"cache expirado, revalidando\" : \"cache miss\", { url });\n\n const promise = this.revalidate<T>(url, ttlMs, options, cached ?? null)\n .finally(() => this.inflight.delete(url));\n\n this.inflight.set(url, promise);\n return promise;\n }\n\n /** Remove uma rota específica do cache. */\n invalidate(url: string): void {\n const all = this.readAll().filter((e) => e.rota !== url);\n this.writeAll(all);\n }\n\n /**\n * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).\n * Chame na inicialização do app, ou de tempos em tempos.\n * @returns quantidade de rotas removidas.\n */\n cleanup(graceSeconds = 3600): number {\n const limite = Date.now() - graceSeconds * 1000;\n const all = this.readAll();\n const mantidas = all.filter((e) => e.expiresAt > limite);\n this.writeAll(mantidas);\n const removidas = all.length - mantidas.length;\n if (removidas > 0) this.log(\"cleanup: rotas expiradas removidas\", { removidas });\n return removidas;\n }\n\n /** Esvazia todo o cache. */\n clear(): void {\n if (!this.storage) return;\n try {\n this.storage.removeItem(this.storageKey);\n } catch {\n /* storage indisponível */\n }\n }\n\n // --- interno ------------------------------------------------------------\n\n /** Retorna um número de acesso estritamente crescente (para o LRU). */\n private nextAccess(): number {\n this.tick = Math.max(Date.now(), this.tick + 1);\n return this.tick;\n }\n\n /** Emite uma linha de log, só quando o modo debug está ligado. */\n private log(message: string, details?: Record<string, unknown>): void {\n if (!this.debug) return;\n this.logger(message, details);\n }\n\n /**\n * Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).\n * Devolve o JSON da primeira que responder OK; se todas falharem, lança o\n * último erro.\n */\n private async fetchComRedundancia<T>(\n candidatos: string[],\n fetcher: typeof fetch,\n options: GetFetchOptions,\n ): Promise<T> {\n let ultimoErro: unknown;\n\n for (const candidato of candidatos) {\n this.log(\"fetch de rede\", { url: candidato });\n try {\n const res = await fetcher(candidato, options.fetchOptions);\n if (!res.ok) throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);\n this.log(\"fetch OK\", { url: candidato, status: res.status });\n return (await res.json()) as T;\n } catch (err) {\n this.log(\"fetch falhou\", { url: candidato, erro: String(err) });\n ultimoErro = err;\n options.onFallback?.(candidato, err);\n }\n }\n\n throw ultimoErro;\n }\n\n private async revalidate<T>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions,\n cached: CacheEntry<T> | null,\n ): Promise<T> {\n const fetcher = options.fetcher ?? fetch;\n const candidatos = [url, ...(options.fallbackUrls ?? [])];\n let fresh: T;\n\n try {\n fresh = await this.fetchComRedundancia<T>(candidatos, fetcher, options);\n } catch (err) {\n // Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.\n if (cached && (options.staleOnError ?? true)) {\n this.log(\"staleOnError: devolvendo dado antigo\", { url });\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n throw err;\n }\n\n // 3. Regra monotônica: só aceita o novo dado se a chave numérica CRESCEU.\n if (cached && options.monotonicKey) {\n const oldVal = getPath(cached.data, options.monotonicKey);\n const newVal = getPath(fresh, options.monotonicKey);\n const ambosNumeros =\n typeof oldVal === \"number\" && typeof newVal === \"number\";\n\n if (ambosNumeros && (newVal as number) <= (oldVal as number)) {\n // Valor não aumentou -> mantém o dado antigo, só renova a expiração.\n this.log(\"regra monotônica: valor não cresceu, mantendo dado antigo\", {\n url,\n oldVal,\n newVal,\n });\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n }\n\n // 4. Grava e devolve o dado novo.\n const now = Date.now();\n this.log(\"cache gravado\", { url, ttlMs });\n this.upsert<T>({\n rota: url,\n data: fresh,\n createdAt: now,\n expiresAt: now + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return fresh;\n }\n\n /** Insere ou atualiza uma rota, aplicando o limite LRU. */\n private upsert<T>(entry: CacheEntry<T>): void {\n const all = this.readAll().filter((e) => e.rota !== entry.rota);\n all.push(entry);\n this.evictIfNeeded(all);\n this.writeAll(all);\n }\n\n /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */\n private evictIfNeeded(all: CacheEntry[]): void {\n while (all.length > this.maxEntries) {\n let idxMaisAntigo = 0;\n for (let i = 1; i < all.length; i++) {\n if (all[i].lastAccess < all[idxMaisAntigo].lastAccess) idxMaisAntigo = i;\n }\n this.log(\"LRU: removendo rota menos usada\", { url: all[idxMaisAntigo].rota });\n all.splice(idxMaisAntigo, 1);\n }\n }\n\n private readAll(): CacheEntry[] {\n if (!this.storage) return [];\n try {\n const raw = this.storage.getItem(this.storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? (parsed as CacheEntry[]) : [];\n } catch {\n return []; // JSON corrompido -> trata como cache vazio\n }\n }\n\n private writeAll(all: CacheEntry[]): void {\n if (!this.storage) return;\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(all));\n } catch (err) {\n // Quota estourada: remove os mais antigos e tenta de novo.\n if (isQuotaError(err) && all.length > 0) {\n const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);\n reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade\n this.log(\"quota excedida: descartando metade das entradas mais antigas\", {\n totalAntes: all.length,\n totalDepois: reduzido.length,\n });\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(reduzido));\n } catch {\n /* ainda assim falhou -> desiste de persistir; dados já retornam */\n }\n }\n // Falha silenciosa: os dados ainda são devolvidos ao chamador.\n }\n }\n}\n\n/** Lê um caminho aninhado (\"a.b.c\") de um objeto de forma segura. */\nfunction getPath(obj: unknown, path: string): unknown {\n return path.split(\".\").reduce<unknown>((acc, k) => {\n if (acc && typeof acc === \"object\" && k in (acc as object)) {\n return (acc as Record<string, unknown>)[k];\n }\n return undefined;\n }, obj);\n}\n\nfunction isQuotaError(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.name === \"QuotaExceededError\" ||\n err.name === \"NS_ERROR_DOM_QUOTA_REACHED\")\n );\n}\n\n/** Logger padrão do modo debug: imprime no console com um prefixo fixo. */\nfunction defaultLogger(message: string, details?: Record<string, unknown>): void {\n if (details) {\n console.debug(`[reqcache] ${message}`, details);\n } else {\n console.debug(`[reqcache] ${message}`);\n }\n}\n\n/** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */\nfunction getDefaultStorage(): StorageLike | null {\n try {\n if (typeof localStorage !== \"undefined\") {\n const probe = \"__reqcache_probe__\";\n localStorage.setItem(probe, \"1\");\n localStorage.removeItem(probe);\n return localStorage;\n }\n } catch {\n /* Safari em modo privado antigo lança aqui */\n }\n return null;\n}\n\n/** Instância pronta para uso, caso não queira configurar nada. */\nexport const requestCache = new RequestCache();\n"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -34,13 +34,19 @@
|
|
|
34
34
|
*/
|
|
35
35
|
class RequestCache {
|
|
36
36
|
constructor(config = {}) {
|
|
37
|
-
var _a, _b, _c;
|
|
37
|
+
var _a, _b, _c, _d, _e;
|
|
38
38
|
this.inflight = new Map();
|
|
39
39
|
/** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
|
|
40
40
|
this.tick = 0;
|
|
41
41
|
this.storageKey = (_a = config.storageKey) !== null && _a !== void 0 ? _a : "reqcache";
|
|
42
42
|
this.maxEntries = (_b = config.maxEntries) !== null && _b !== void 0 ? _b : 100;
|
|
43
43
|
this.storage = (_c = config.storage) !== null && _c !== void 0 ? _c : getDefaultStorage();
|
|
44
|
+
this.debug = (_d = config.debug) !== null && _d !== void 0 ? _d : false;
|
|
45
|
+
this.logger = (_e = config.logger) !== null && _e !== void 0 ? _e : defaultLogger;
|
|
46
|
+
}
|
|
47
|
+
/** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
|
|
48
|
+
setDebug(enabled) {
|
|
49
|
+
this.debug = enabled;
|
|
44
50
|
}
|
|
45
51
|
/**
|
|
46
52
|
* Busca uma URL usando cache.
|
|
@@ -56,13 +62,17 @@
|
|
|
56
62
|
if (cached && now < cached.expiresAt) {
|
|
57
63
|
cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU
|
|
58
64
|
this.writeAll(all);
|
|
65
|
+
this.log("cache hit", { url, expiresAt: new Date(cached.expiresAt).toISOString() });
|
|
59
66
|
return cached.data;
|
|
60
67
|
}
|
|
61
68
|
// 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,
|
|
62
69
|
// todas as chamadas concorrentes aguardam a mesma Promise.
|
|
63
70
|
const pending = this.inflight.get(url);
|
|
64
|
-
if (pending)
|
|
71
|
+
if (pending) {
|
|
72
|
+
this.log("dedup: aguardando requisição em andamento", { url });
|
|
65
73
|
return pending;
|
|
74
|
+
}
|
|
75
|
+
this.log(cached ? "cache expirado, revalidando" : "cache miss", { url });
|
|
66
76
|
const promise = this.revalidate(url, ttlMs, options, cached !== null && cached !== void 0 ? cached : null)
|
|
67
77
|
.finally(() => this.inflight.delete(url));
|
|
68
78
|
this.inflight.set(url, promise);
|
|
@@ -83,7 +93,10 @@
|
|
|
83
93
|
const all = this.readAll();
|
|
84
94
|
const mantidas = all.filter((e) => e.expiresAt > limite);
|
|
85
95
|
this.writeAll(mantidas);
|
|
86
|
-
|
|
96
|
+
const removidas = all.length - mantidas.length;
|
|
97
|
+
if (removidas > 0)
|
|
98
|
+
this.log("cleanup: rotas expiradas removidas", { removidas });
|
|
99
|
+
return removidas;
|
|
87
100
|
}
|
|
88
101
|
/** Esvazia todo o cache. */
|
|
89
102
|
clear() {
|
|
@@ -102,19 +115,49 @@
|
|
|
102
115
|
this.tick = Math.max(Date.now(), this.tick + 1);
|
|
103
116
|
return this.tick;
|
|
104
117
|
}
|
|
118
|
+
/** Emite uma linha de log, só quando o modo debug está ligado. */
|
|
119
|
+
log(message, details) {
|
|
120
|
+
if (!this.debug)
|
|
121
|
+
return;
|
|
122
|
+
this.logger(message, details);
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
126
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
127
|
+
* último erro.
|
|
128
|
+
*/
|
|
129
|
+
async fetchComRedundancia(candidatos, fetcher, options) {
|
|
130
|
+
var _a;
|
|
131
|
+
let ultimoErro;
|
|
132
|
+
for (const candidato of candidatos) {
|
|
133
|
+
this.log("fetch de rede", { url: candidato });
|
|
134
|
+
try {
|
|
135
|
+
const res = await fetcher(candidato, options.fetchOptions);
|
|
136
|
+
if (!res.ok)
|
|
137
|
+
throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);
|
|
138
|
+
this.log("fetch OK", { url: candidato, status: res.status });
|
|
139
|
+
return (await res.json());
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
this.log("fetch falhou", { url: candidato, erro: String(err) });
|
|
143
|
+
ultimoErro = err;
|
|
144
|
+
(_a = options.onFallback) === null || _a === void 0 ? void 0 : _a.call(options, candidato, err);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
throw ultimoErro;
|
|
148
|
+
}
|
|
105
149
|
async revalidate(url, ttlMs, options, cached) {
|
|
106
|
-
var _a, _b;
|
|
150
|
+
var _a, _b, _c;
|
|
107
151
|
const fetcher = (_a = options.fetcher) !== null && _a !== void 0 ? _a : fetch;
|
|
152
|
+
const candidatos = [url, ...((_b = options.fallbackUrls) !== null && _b !== void 0 ? _b : [])];
|
|
108
153
|
let fresh;
|
|
109
154
|
try {
|
|
110
|
-
|
|
111
|
-
if (!res.ok)
|
|
112
|
-
throw new Error(`HTTP ${res.status} ao buscar ${url}`);
|
|
113
|
-
fresh = (await res.json());
|
|
155
|
+
fresh = await this.fetchComRedundancia(candidatos, fetcher, options);
|
|
114
156
|
}
|
|
115
157
|
catch (err) {
|
|
116
|
-
//
|
|
117
|
-
if (cached && ((
|
|
158
|
+
// Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.
|
|
159
|
+
if (cached && ((_c = options.staleOnError) !== null && _c !== void 0 ? _c : true)) {
|
|
160
|
+
this.log("staleOnError: devolvendo dado antigo", { url });
|
|
118
161
|
this.upsert({
|
|
119
162
|
...cached,
|
|
120
163
|
expiresAt: Date.now() + ttlMs,
|
|
@@ -131,6 +174,11 @@
|
|
|
131
174
|
const ambosNumeros = typeof oldVal === "number" && typeof newVal === "number";
|
|
132
175
|
if (ambosNumeros && newVal <= oldVal) {
|
|
133
176
|
// Valor não aumentou -> mantém o dado antigo, só renova a expiração.
|
|
177
|
+
this.log("regra monotônica: valor não cresceu, mantendo dado antigo", {
|
|
178
|
+
url,
|
|
179
|
+
oldVal,
|
|
180
|
+
newVal,
|
|
181
|
+
});
|
|
134
182
|
this.upsert({
|
|
135
183
|
...cached,
|
|
136
184
|
expiresAt: Date.now() + ttlMs,
|
|
@@ -141,6 +189,7 @@
|
|
|
141
189
|
}
|
|
142
190
|
// 4. Grava e devolve o dado novo.
|
|
143
191
|
const now = Date.now();
|
|
192
|
+
this.log("cache gravado", { url, ttlMs });
|
|
144
193
|
this.upsert({
|
|
145
194
|
rota: url,
|
|
146
195
|
data: fresh,
|
|
@@ -165,6 +214,7 @@
|
|
|
165
214
|
if (all[i].lastAccess < all[idxMaisAntigo].lastAccess)
|
|
166
215
|
idxMaisAntigo = i;
|
|
167
216
|
}
|
|
217
|
+
this.log("LRU: removendo rota menos usada", { url: all[idxMaisAntigo].rota });
|
|
168
218
|
all.splice(idxMaisAntigo, 1);
|
|
169
219
|
}
|
|
170
220
|
}
|
|
@@ -193,6 +243,10 @@
|
|
|
193
243
|
if (isQuotaError(err) && all.length > 0) {
|
|
194
244
|
const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);
|
|
195
245
|
reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade
|
|
246
|
+
this.log("quota excedida: descartando metade das entradas mais antigas", {
|
|
247
|
+
totalAntes: all.length,
|
|
248
|
+
totalDepois: reduzido.length,
|
|
249
|
+
});
|
|
196
250
|
try {
|
|
197
251
|
this.storage.setItem(this.storageKey, JSON.stringify(reduzido));
|
|
198
252
|
}
|
|
@@ -218,6 +272,15 @@
|
|
|
218
272
|
(err.name === "QuotaExceededError" ||
|
|
219
273
|
err.name === "NS_ERROR_DOM_QUOTA_REACHED"));
|
|
220
274
|
}
|
|
275
|
+
/** Logger padrão do modo debug: imprime no console com um prefixo fixo. */
|
|
276
|
+
function defaultLogger(message, details) {
|
|
277
|
+
if (details) {
|
|
278
|
+
console.debug(`[reqcache] ${message}`, details);
|
|
279
|
+
}
|
|
280
|
+
else {
|
|
281
|
+
console.debug(`[reqcache] ${message}`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
221
284
|
/** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */
|
|
222
285
|
function getDefaultStorage() {
|
|
223
286
|
try {
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../src/request-cache.ts"],"sourcesContent":["/**\n * request-cache\n * -------------\n * Cache de requisições HTTP em localStorage, pensado para cenários de\n * alto volume em curto período (ex.: apuração de eleições).\n *\n * Modelo de armazenamento:\n * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.\n * Cada objeto é indexado pela `rota` (a URL requisitada):\n * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]\n *\n * Comportamento:\n * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.\n * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.\n * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).\n * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico\n * dessa chave for MAIOR que o guardado; senão mantém o antigo.\n *\n * Proteção de espaço (limite de ~5 MB do localStorage):\n * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos\n * recentemente usada (LRU). Roda sozinho, sem timer.\n * - Em erro de quota, remove as entradas mais antigas e tenta de novo.\n * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.\n *\n * Extras para o caso de eleição:\n * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.\n * - staleOnError: se a rede/API falhar, devolve o último dado válido.\n */\n\nexport interface StorageLike {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface GetFetchOptions {\n /**\n * Caminho da chave numérica monotônica. Suporta aninhamento com ponto.\n * Ex.: \"total\", \"resultado.votosApurados\", \"data.candidato.votos\".\n * Se o valor novo NÃO for maior que o antigo, mantém o dado em cache.\n */\n monotonicKey?: string;\n\n /** Opções nativas repassadas ao fetch (headers, method, body, signal...). */\n fetchOptions?: RequestInit;\n\n /**\n * Se true (padrão), quando a nova requisição falhar e existir dado antigo,\n * devolve esse dado antigo em vez de lançar erro.\n */\n staleOnError?: boolean;\n\n /** fetch customizado — útil para testes ou ambientes sem fetch global. */\n fetcher?: typeof fetch;\n}\n\n/** Uma entrada do cache. `rota` é a chave identificadora. */\nexport interface CacheEntry<T = unknown> {\n rota: string;\n data: T;\n createdAt: number;\n expiresAt: number;\n lastAccess: number;\n}\n\nexport interface RequestCacheConfig {\n /** Storage a usar. Padrão: localStorage (se disponível). */\n storage?: StorageLike;\n /** Chave única onde o array é guardado. Padrão \"reqcache\". */\n storageKey?: string;\n /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */\n maxEntries?: number;\n}\n\nexport class RequestCache {\n private storage: StorageLike | null;\n private storageKey: string;\n private maxEntries: number;\n private inflight = new Map<string, Promise<unknown>>();\n /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */\n private tick = 0;\n\n constructor(config: RequestCacheConfig = {}) {\n this.storageKey = config.storageKey ?? \"reqcache\";\n this.maxEntries = config.maxEntries ?? 100;\n this.storage = config.storage ?? getDefaultStorage();\n }\n\n /**\n * Busca uma URL usando cache.\n * @param url Rota da requisição (também é a chave do cache).\n * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).\n * @param options Regra monotônica, opções de fetch, etc.\n */\n async getFetch<T = unknown>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions = {},\n ): Promise<T> {\n const now = Date.now();\n const all = this.readAll();\n const cached = all.find((e) => e.rota === url) as CacheEntry<T> | undefined;\n\n // 1. Cache ainda válido -> devolve sem tocar na rede.\n if (cached && now < cached.expiresAt) {\n cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU\n this.writeAll(all);\n return cached.data;\n }\n\n // 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,\n // todas as chamadas concorrentes aguardam a mesma Promise.\n const pending = this.inflight.get(url) as Promise<T> | undefined;\n if (pending) return pending;\n\n const promise = this.revalidate<T>(url, ttlMs, options, cached ?? null)\n .finally(() => this.inflight.delete(url));\n\n this.inflight.set(url, promise);\n return promise;\n }\n\n /** Remove uma rota específica do cache. */\n invalidate(url: string): void {\n const all = this.readAll().filter((e) => e.rota !== url);\n this.writeAll(all);\n }\n\n /**\n * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).\n * Chame na inicialização do app, ou de tempos em tempos.\n * @returns quantidade de rotas removidas.\n */\n cleanup(graceSeconds = 3600): number {\n const limite = Date.now() - graceSeconds * 1000;\n const all = this.readAll();\n const mantidas = all.filter((e) => e.expiresAt > limite);\n this.writeAll(mantidas);\n return all.length - mantidas.length;\n }\n\n /** Esvazia todo o cache. */\n clear(): void {\n if (!this.storage) return;\n try {\n this.storage.removeItem(this.storageKey);\n } catch {\n /* storage indisponível */\n }\n }\n\n // --- interno ------------------------------------------------------------\n\n /** Retorna um número de acesso estritamente crescente (para o LRU). */\n private nextAccess(): number {\n this.tick = Math.max(Date.now(), this.tick + 1);\n return this.tick;\n }\n\n private async revalidate<T>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions,\n cached: CacheEntry<T> | null,\n ): Promise<T> {\n const fetcher = options.fetcher ?? fetch;\n let fresh: T;\n\n try {\n const res = await fetcher(url, options.fetchOptions);\n if (!res.ok) throw new Error(`HTTP ${res.status} ao buscar ${url}`);\n fresh = (await res.json()) as T;\n } catch (err) {\n // Rede/API falhou. Se temos dado antigo e staleOnError, devolve o antigo.\n if (cached && (options.staleOnError ?? true)) {\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n throw err;\n }\n\n // 3. Regra monotônica: só aceita o novo dado se a chave numérica CRESCEU.\n if (cached && options.monotonicKey) {\n const oldVal = getPath(cached.data, options.monotonicKey);\n const newVal = getPath(fresh, options.monotonicKey);\n const ambosNumeros =\n typeof oldVal === \"number\" && typeof newVal === \"number\";\n\n if (ambosNumeros && (newVal as number) <= (oldVal as number)) {\n // Valor não aumentou -> mantém o dado antigo, só renova a expiração.\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n }\n\n // 4. Grava e devolve o dado novo.\n const now = Date.now();\n this.upsert<T>({\n rota: url,\n data: fresh,\n createdAt: now,\n expiresAt: now + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return fresh;\n }\n\n /** Insere ou atualiza uma rota, aplicando o limite LRU. */\n private upsert<T>(entry: CacheEntry<T>): void {\n const all = this.readAll().filter((e) => e.rota !== entry.rota);\n all.push(entry);\n this.evictIfNeeded(all);\n this.writeAll(all);\n }\n\n /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */\n private evictIfNeeded(all: CacheEntry[]): void {\n while (all.length > this.maxEntries) {\n let idxMaisAntigo = 0;\n for (let i = 1; i < all.length; i++) {\n if (all[i].lastAccess < all[idxMaisAntigo].lastAccess) idxMaisAntigo = i;\n }\n all.splice(idxMaisAntigo, 1);\n }\n }\n\n private readAll(): CacheEntry[] {\n if (!this.storage) return [];\n try {\n const raw = this.storage.getItem(this.storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? (parsed as CacheEntry[]) : [];\n } catch {\n return []; // JSON corrompido -> trata como cache vazio\n }\n }\n\n private writeAll(all: CacheEntry[]): void {\n if (!this.storage) return;\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(all));\n } catch (err) {\n // Quota estourada: remove os mais antigos e tenta de novo.\n if (isQuotaError(err) && all.length > 0) {\n const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);\n reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(reduzido));\n } catch {\n /* ainda assim falhou -> desiste de persistir; dados já retornam */\n }\n }\n // Falha silenciosa: os dados ainda são devolvidos ao chamador.\n }\n }\n}\n\n/** Lê um caminho aninhado (\"a.b.c\") de um objeto de forma segura. */\nfunction getPath(obj: unknown, path: string): unknown {\n return path.split(\".\").reduce<unknown>((acc, k) => {\n if (acc && typeof acc === \"object\" && k in (acc as object)) {\n return (acc as Record<string, unknown>)[k];\n }\n return undefined;\n }, obj);\n}\n\nfunction isQuotaError(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.name === \"QuotaExceededError\" ||\n err.name === \"NS_ERROR_DOM_QUOTA_REACHED\")\n );\n}\n\n/** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */\nfunction getDefaultStorage(): StorageLike | null {\n try {\n if (typeof localStorage !== \"undefined\") {\n const probe = \"__reqcache_probe__\";\n localStorage.setItem(probe, \"1\");\n localStorage.removeItem(probe);\n return localStorage;\n }\n } catch {\n /* Safari em modo privado antigo lança aqui */\n }\n return null;\n}\n\n/** Instância pronta para uso, caso não queira configurar nada. */\nexport const requestCache = new RequestCache();\n"],"names":[],"mappings":";;;;;;IAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2BG;UA+CU,YAAY,CAAA;IAQvB,IAAA,WAAA,CAAY,SAA6B,EAAE,EAAA;;IAJnC,QAAA,IAAA,CAAA,QAAQ,GAAG,IAAI,GAAG,EAA4B;;YAE9C,IAAA,CAAA,IAAI,GAAG,CAAC;YAGd,IAAI,CAAC,UAAU,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,UAAU;YACjD,IAAI,CAAC,UAAU,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,GAAG;YAC1C,IAAI,CAAC,OAAO,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,iBAAiB,EAAE;QACtD;IAEA;;;;;IAKG;QACH,MAAM,QAAQ,CACZ,GAAW,EACX,KAAa,EACb,UAA2B,EAAE,EAAA;IAE7B,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;IACtB,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE;IAC1B,QAAA,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,GAAG,CAA8B;;YAG3E,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE;gBACpC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;IACtC,YAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;gBAClB,OAAO,MAAM,CAAC,IAAI;YACpB;;;YAIA,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAA2B;IAChE,QAAA,IAAI,OAAO;IAAE,YAAA,OAAO,OAAO;IAE3B,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAI,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,KAAA,IAAA,IAAN,MAAM,cAAN,MAAM,GAAI,IAAI;IACnE,aAAA,OAAO,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;YAE3C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC;IAC/B,QAAA,OAAO,OAAO;QAChB;;IAGA,IAAA,UAAU,CAAC,GAAW,EAAA;YACpB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC;IACxD,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QACpB;IAEA;;;;IAIG;QACH,OAAO,CAAC,YAAY,GAAG,IAAI,EAAA;YACzB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,GAAG,IAAI;IAC/C,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE;IAC1B,QAAA,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC;IACxD,QAAA,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;IACvB,QAAA,OAAO,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;QACrC;;QAGA,KAAK,GAAA;YACH,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE;IACnB,QAAA,IAAI;gBACF,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;YAC1C;IAAE,QAAA,OAAA,EAAA,EAAM;;YAER;QACF;;;QAKQ,UAAU,GAAA;IAChB,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;YAC/C,OAAO,IAAI,CAAC,IAAI;QAClB;QAEQ,MAAM,UAAU,CACtB,GAAW,EACX,KAAa,EACb,OAAwB,EACxB,MAA4B,EAAA;;YAE5B,MAAM,OAAO,GAAG,CAAA,EAAA,GAAA,OAAO,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,KAAK;IACxC,QAAA,IAAI,KAAQ;IAEZ,QAAA,IAAI;gBACF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,OAAO,CAAC,YAAY,CAAC;gBACpD,IAAI,CAAC,GAAG,CAAC,EAAE;oBAAE,MAAM,IAAI,KAAK,CAAC,CAAA,KAAA,EAAQ,GAAG,CAAC,MAAM,CAAA,WAAA,EAAc,GAAG,CAAA,CAAE,CAAC;gBACnE,KAAK,IAAI,MAAM,GAAG,CAAC,IAAI,EAAE,CAAM;YACjC;YAAE,OAAO,GAAG,EAAE;;gBAEZ,IAAI,MAAM,KAAK,CAAA,EAAA,GAAA,OAAO,CAAC,YAAY,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,IAAI,CAAC,EAAE;oBAC5C,IAAI,CAAC,MAAM,CAAC;IACV,oBAAA,GAAG,MAAM;IACT,oBAAA,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;IAC7B,oBAAA,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;IAC9B,iBAAA,CAAC;oBACF,OAAO,MAAM,CAAC,IAAI;gBACpB;IACA,YAAA,MAAM,GAAG;YACX;;IAGA,QAAA,IAAI,MAAM,IAAI,OAAO,CAAC,YAAY,EAAE;IAClC,YAAA,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC;gBACzD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC;gBACnD,MAAM,YAAY,GAChB,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,QAAQ;IAE1D,YAAA,IAAI,YAAY,IAAK,MAAiB,IAAK,MAAiB,EAAE;;oBAE5D,IAAI,CAAC,MAAM,CAAC;IACV,oBAAA,GAAG,MAAM;IACT,oBAAA,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;IAC7B,oBAAA,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;IAC9B,iBAAA,CAAC;oBACF,OAAO,MAAM,CAAC,IAAI;gBACpB;YACF;;IAGA,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;YACtB,IAAI,CAAC,MAAM,CAAI;IACb,YAAA,IAAI,EAAE,GAAG;IACT,YAAA,IAAI,EAAE,KAAK;IACX,YAAA,SAAS,EAAE,GAAG;gBACd,SAAS,EAAE,GAAG,GAAG,KAAK;IACtB,YAAA,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;IAC9B,SAAA,CAAC;IACF,QAAA,OAAO,KAAK;QACd;;IAGQ,IAAA,MAAM,CAAI,KAAoB,EAAA;YACpC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC;IAC/D,QAAA,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;IACf,QAAA,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC;IACvB,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;QACpB;;IAGQ,IAAA,aAAa,CAAC,GAAiB,EAAA;YACrC,OAAO,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE;gBACnC,IAAI,aAAa,GAAG,CAAC;IACrB,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;IACnC,gBAAA,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,UAAU;wBAAE,aAAa,GAAG,CAAC;gBAC1E;IACA,YAAA,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;YAC9B;QACF;QAEQ,OAAO,GAAA;YACb,IAAI,CAAC,IAAI,CAAC,OAAO;IAAE,YAAA,OAAO,EAAE;IAC5B,QAAA,IAAI;IACF,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;IACjD,YAAA,IAAI,CAAC,GAAG;IAAE,gBAAA,OAAO,EAAE;gBACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;IAC9B,YAAA,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAI,MAAuB,GAAG,EAAE;YAC9D;IAAE,QAAA,OAAA,EAAA,EAAM;gBACN,OAAO,EAAE,CAAC;YACZ;QACF;IAEQ,IAAA,QAAQ,CAAC,GAAiB,EAAA;YAChC,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE;IACnB,QAAA,IAAI;IACF,YAAA,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;YAC5D;YAAE,OAAO,GAAG,EAAE;;gBAEZ,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE;oBACvC,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC;IACrE,gBAAA,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;IACnD,gBAAA,IAAI;IACF,oBAAA,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;oBACjE;IAAE,gBAAA,OAAA,EAAA,EAAM;;oBAER;gBACF;;YAEF;QACF;IACD;IAED;IACA,SAAS,OAAO,CAAC,GAAY,EAAE,IAAY,EAAA;IACzC,IAAA,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAU,CAAC,GAAG,EAAE,CAAC,KAAI;YAChD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,IAAK,GAAc,EAAE;IAC1D,YAAA,OAAQ,GAA+B,CAAC,CAAC,CAAC;YAC5C;IACA,QAAA,OAAO,SAAS;QAClB,CAAC,EAAE,GAAG,CAAC;IACT;IAEA,SAAS,YAAY,CAAC,GAAY,EAAA;QAChC,QACE,GAAG,YAAY,KAAK;IACpB,SAAC,GAAG,CAAC,IAAI,KAAK,oBAAoB;IAChC,YAAA,GAAG,CAAC,IAAI,KAAK,4BAA4B,CAAC;IAEhD;IAEA;IACA,SAAS,iBAAiB,GAAA;IACxB,IAAA,IAAI;IACF,QAAA,IAAI,OAAO,YAAY,KAAK,WAAW,EAAE;gBACvC,MAAM,KAAK,GAAG,oBAAoB;IAClC,YAAA,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;IAChC,YAAA,YAAY,CAAC,UAAU,CAAC,KAAK,CAAC;IAC9B,YAAA,OAAO,YAAY;YACrB;QACF;IAAE,IAAA,OAAA,EAAA,EAAM;;QAER;IACA,IAAA,OAAO,IAAI;IACb;IAEA;AACO,UAAM,YAAY,GAAG,IAAI,YAAY;;;;;;;;;"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../src/request-cache.ts"],"sourcesContent":["/**\n * request-cache\n * -------------\n * Cache de requisições HTTP em localStorage, pensado para cenários de\n * alto volume em curto período (ex.: apuração de eleições).\n *\n * Modelo de armazenamento:\n * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.\n * Cada objeto é indexado pela `rota` (a URL requisitada):\n * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]\n *\n * Comportamento:\n * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.\n * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.\n * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).\n * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico\n * dessa chave for MAIOR que o guardado; senão mantém o antigo.\n *\n * Proteção de espaço (limite de ~5 MB do localStorage):\n * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos\n * recentemente usada (LRU). Roda sozinho, sem timer.\n * - Em erro de quota, remove as entradas mais antigas e tenta de novo.\n * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.\n *\n * Extras para o caso de eleição:\n * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.\n * - staleOnError: se a rede/API falhar, devolve o último dado válido.\n */\n\nexport interface StorageLike {\n getItem(key: string): string | null;\n setItem(key: string, value: string): void;\n removeItem(key: string): void;\n}\n\nexport interface GetFetchOptions {\n /**\n * Caminho da chave numérica monotônica. Suporta aninhamento com ponto.\n * Ex.: \"total\", \"resultado.votosApurados\", \"data.candidato.votos\".\n * Se o valor novo NÃO for maior que o antigo, mantém o dado em cache.\n */\n monotonicKey?: string;\n\n /** Opções nativas repassadas ao fetch (headers, method, body, signal...). */\n fetchOptions?: RequestInit;\n\n /**\n * Se true (padrão), quando a nova requisição falhar e existir dado antigo,\n * devolve esse dado antigo em vez de lançar erro.\n */\n staleOnError?: boolean;\n\n /** fetch customizado — útil para testes ou ambientes sem fetch global. */\n fetcher?: typeof fetch;\n\n /**\n * URLs alternativas (outros domínios/APIs) tentadas em ordem, caso `url`\n * falhe. A rota do cache continua sendo `url` — as alternativas só entram\n * na requisição, não criam entradas novas no cache.\n */\n fallbackUrls?: string[];\n\n /**\n * Chamado quando uma URL falha e a lib vai tentar a próxima da lista\n * (`url` + `fallbackUrls`). Útil para observabilidade/log.\n */\n onFallback?: (failedUrl: string, error: unknown) => void;\n}\n\n/** Uma entrada do cache. `rota` é a chave identificadora. */\nexport interface CacheEntry<T = unknown> {\n rota: string;\n data: T;\n createdAt: number;\n expiresAt: number;\n lastAccess: number;\n}\n\n/** Uma linha de log do modo debug. */\nexport interface DebugLogFn {\n (message: string, details?: Record<string, unknown>): void;\n}\n\nexport interface RequestCacheConfig {\n /** Storage a usar. Padrão: localStorage (se disponível). */\n storage?: StorageLike;\n /** Chave única onde o array é guardado. Padrão \"reqcache\". */\n storageKey?: string;\n /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */\n maxEntries?: number;\n\n /**\n * Se true, imprime logs detalhados de cada operação (fetch de rede,\n * cache hit, deduplicação, fallback, LRU, etc.). Padrão false.\n * Útil para identificar se estão sendo feitas mais chamadas de rede\n * do que o necessário. Pode ser ligado/desligado em runtime com\n * `setDebug()`.\n */\n debug?: boolean;\n\n /** Logger customizado usado quando `debug` está ligado. Padrão: `console.debug`. */\n logger?: DebugLogFn;\n}\n\nexport class RequestCache {\n private storage: StorageLike | null;\n private storageKey: string;\n private maxEntries: number;\n private inflight = new Map<string, Promise<unknown>>();\n /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */\n private tick = 0;\n private debug: boolean;\n private logger: DebugLogFn;\n\n constructor(config: RequestCacheConfig = {}) {\n this.storageKey = config.storageKey ?? \"reqcache\";\n this.maxEntries = config.maxEntries ?? 100;\n this.storage = config.storage ?? getDefaultStorage();\n this.debug = config.debug ?? false;\n this.logger = config.logger ?? defaultLogger;\n }\n\n /** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */\n setDebug(enabled: boolean): void {\n this.debug = enabled;\n }\n\n /**\n * Busca uma URL usando cache.\n * @param url Rota da requisição (também é a chave do cache).\n * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).\n * @param options Regra monotônica, opções de fetch, etc.\n */\n async getFetch<T = unknown>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions = {},\n ): Promise<T> {\n const now = Date.now();\n const all = this.readAll();\n const cached = all.find((e) => e.rota === url) as CacheEntry<T> | undefined;\n\n // 1. Cache ainda válido -> devolve sem tocar na rede.\n if (cached && now < cached.expiresAt) {\n cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU\n this.writeAll(all);\n this.log(\"cache hit\", { url, expiresAt: new Date(cached.expiresAt).toISOString() });\n return cached.data;\n }\n\n // 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,\n // todas as chamadas concorrentes aguardam a mesma Promise.\n const pending = this.inflight.get(url) as Promise<T> | undefined;\n if (pending) {\n this.log(\"dedup: aguardando requisição em andamento\", { url });\n return pending;\n }\n\n this.log(cached ? \"cache expirado, revalidando\" : \"cache miss\", { url });\n\n const promise = this.revalidate<T>(url, ttlMs, options, cached ?? null)\n .finally(() => this.inflight.delete(url));\n\n this.inflight.set(url, promise);\n return promise;\n }\n\n /** Remove uma rota específica do cache. */\n invalidate(url: string): void {\n const all = this.readAll().filter((e) => e.rota !== url);\n this.writeAll(all);\n }\n\n /**\n * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).\n * Chame na inicialização do app, ou de tempos em tempos.\n * @returns quantidade de rotas removidas.\n */\n cleanup(graceSeconds = 3600): number {\n const limite = Date.now() - graceSeconds * 1000;\n const all = this.readAll();\n const mantidas = all.filter((e) => e.expiresAt > limite);\n this.writeAll(mantidas);\n const removidas = all.length - mantidas.length;\n if (removidas > 0) this.log(\"cleanup: rotas expiradas removidas\", { removidas });\n return removidas;\n }\n\n /** Esvazia todo o cache. */\n clear(): void {\n if (!this.storage) return;\n try {\n this.storage.removeItem(this.storageKey);\n } catch {\n /* storage indisponível */\n }\n }\n\n // --- interno ------------------------------------------------------------\n\n /** Retorna um número de acesso estritamente crescente (para o LRU). */\n private nextAccess(): number {\n this.tick = Math.max(Date.now(), this.tick + 1);\n return this.tick;\n }\n\n /** Emite uma linha de log, só quando o modo debug está ligado. */\n private log(message: string, details?: Record<string, unknown>): void {\n if (!this.debug) return;\n this.logger(message, details);\n }\n\n /**\n * Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).\n * Devolve o JSON da primeira que responder OK; se todas falharem, lança o\n * último erro.\n */\n private async fetchComRedundancia<T>(\n candidatos: string[],\n fetcher: typeof fetch,\n options: GetFetchOptions,\n ): Promise<T> {\n let ultimoErro: unknown;\n\n for (const candidato of candidatos) {\n this.log(\"fetch de rede\", { url: candidato });\n try {\n const res = await fetcher(candidato, options.fetchOptions);\n if (!res.ok) throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);\n this.log(\"fetch OK\", { url: candidato, status: res.status });\n return (await res.json()) as T;\n } catch (err) {\n this.log(\"fetch falhou\", { url: candidato, erro: String(err) });\n ultimoErro = err;\n options.onFallback?.(candidato, err);\n }\n }\n\n throw ultimoErro;\n }\n\n private async revalidate<T>(\n url: string,\n ttlMs: number,\n options: GetFetchOptions,\n cached: CacheEntry<T> | null,\n ): Promise<T> {\n const fetcher = options.fetcher ?? fetch;\n const candidatos = [url, ...(options.fallbackUrls ?? [])];\n let fresh: T;\n\n try {\n fresh = await this.fetchComRedundancia<T>(candidatos, fetcher, options);\n } catch (err) {\n // Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.\n if (cached && (options.staleOnError ?? true)) {\n this.log(\"staleOnError: devolvendo dado antigo\", { url });\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n throw err;\n }\n\n // 3. Regra monotônica: só aceita o novo dado se a chave numérica CRESCEU.\n if (cached && options.monotonicKey) {\n const oldVal = getPath(cached.data, options.monotonicKey);\n const newVal = getPath(fresh, options.monotonicKey);\n const ambosNumeros =\n typeof oldVal === \"number\" && typeof newVal === \"number\";\n\n if (ambosNumeros && (newVal as number) <= (oldVal as number)) {\n // Valor não aumentou -> mantém o dado antigo, só renova a expiração.\n this.log(\"regra monotônica: valor não cresceu, mantendo dado antigo\", {\n url,\n oldVal,\n newVal,\n });\n this.upsert({\n ...cached,\n expiresAt: Date.now() + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return cached.data;\n }\n }\n\n // 4. Grava e devolve o dado novo.\n const now = Date.now();\n this.log(\"cache gravado\", { url, ttlMs });\n this.upsert<T>({\n rota: url,\n data: fresh,\n createdAt: now,\n expiresAt: now + ttlMs,\n lastAccess: this.nextAccess(),\n });\n return fresh;\n }\n\n /** Insere ou atualiza uma rota, aplicando o limite LRU. */\n private upsert<T>(entry: CacheEntry<T>): void {\n const all = this.readAll().filter((e) => e.rota !== entry.rota);\n all.push(entry);\n this.evictIfNeeded(all);\n this.writeAll(all);\n }\n\n /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */\n private evictIfNeeded(all: CacheEntry[]): void {\n while (all.length > this.maxEntries) {\n let idxMaisAntigo = 0;\n for (let i = 1; i < all.length; i++) {\n if (all[i].lastAccess < all[idxMaisAntigo].lastAccess) idxMaisAntigo = i;\n }\n this.log(\"LRU: removendo rota menos usada\", { url: all[idxMaisAntigo].rota });\n all.splice(idxMaisAntigo, 1);\n }\n }\n\n private readAll(): CacheEntry[] {\n if (!this.storage) return [];\n try {\n const raw = this.storage.getItem(this.storageKey);\n if (!raw) return [];\n const parsed = JSON.parse(raw);\n return Array.isArray(parsed) ? (parsed as CacheEntry[]) : [];\n } catch {\n return []; // JSON corrompido -> trata como cache vazio\n }\n }\n\n private writeAll(all: CacheEntry[]): void {\n if (!this.storage) return;\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(all));\n } catch (err) {\n // Quota estourada: remove os mais antigos e tenta de novo.\n if (isQuotaError(err) && all.length > 0) {\n const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);\n reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade\n this.log(\"quota excedida: descartando metade das entradas mais antigas\", {\n totalAntes: all.length,\n totalDepois: reduzido.length,\n });\n try {\n this.storage.setItem(this.storageKey, JSON.stringify(reduzido));\n } catch {\n /* ainda assim falhou -> desiste de persistir; dados já retornam */\n }\n }\n // Falha silenciosa: os dados ainda são devolvidos ao chamador.\n }\n }\n}\n\n/** Lê um caminho aninhado (\"a.b.c\") de um objeto de forma segura. */\nfunction getPath(obj: unknown, path: string): unknown {\n return path.split(\".\").reduce<unknown>((acc, k) => {\n if (acc && typeof acc === \"object\" && k in (acc as object)) {\n return (acc as Record<string, unknown>)[k];\n }\n return undefined;\n }, obj);\n}\n\nfunction isQuotaError(err: unknown): boolean {\n return (\n err instanceof Error &&\n (err.name === \"QuotaExceededError\" ||\n err.name === \"NS_ERROR_DOM_QUOTA_REACHED\")\n );\n}\n\n/** Logger padrão do modo debug: imprime no console com um prefixo fixo. */\nfunction defaultLogger(message: string, details?: Record<string, unknown>): void {\n if (details) {\n console.debug(`[reqcache] ${message}`, details);\n } else {\n console.debug(`[reqcache] ${message}`);\n }\n}\n\n/** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */\nfunction getDefaultStorage(): StorageLike | null {\n try {\n if (typeof localStorage !== \"undefined\") {\n const probe = \"__reqcache_probe__\";\n localStorage.setItem(probe, \"1\");\n localStorage.removeItem(probe);\n return localStorage;\n }\n } catch {\n /* Safari em modo privado antigo lança aqui */\n }\n return null;\n}\n\n/** Instância pronta para uso, caso não queira configurar nada. */\nexport const requestCache = new RequestCache();\n"],"names":[],"mappings":"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"}
|
package/dist/request-cache.d.ts
CHANGED
|
@@ -47,6 +47,17 @@ export interface GetFetchOptions {
|
|
|
47
47
|
staleOnError?: boolean;
|
|
48
48
|
/** fetch customizado — útil para testes ou ambientes sem fetch global. */
|
|
49
49
|
fetcher?: typeof fetch;
|
|
50
|
+
/**
|
|
51
|
+
* URLs alternativas (outros domínios/APIs) tentadas em ordem, caso `url`
|
|
52
|
+
* falhe. A rota do cache continua sendo `url` — as alternativas só entram
|
|
53
|
+
* na requisição, não criam entradas novas no cache.
|
|
54
|
+
*/
|
|
55
|
+
fallbackUrls?: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Chamado quando uma URL falha e a lib vai tentar a próxima da lista
|
|
58
|
+
* (`url` + `fallbackUrls`). Útil para observabilidade/log.
|
|
59
|
+
*/
|
|
60
|
+
onFallback?: (failedUrl: string, error: unknown) => void;
|
|
50
61
|
}
|
|
51
62
|
/** Uma entrada do cache. `rota` é a chave identificadora. */
|
|
52
63
|
export interface CacheEntry<T = unknown> {
|
|
@@ -56,6 +67,10 @@ export interface CacheEntry<T = unknown> {
|
|
|
56
67
|
expiresAt: number;
|
|
57
68
|
lastAccess: number;
|
|
58
69
|
}
|
|
70
|
+
/** Uma linha de log do modo debug. */
|
|
71
|
+
export interface DebugLogFn {
|
|
72
|
+
(message: string, details?: Record<string, unknown>): void;
|
|
73
|
+
}
|
|
59
74
|
export interface RequestCacheConfig {
|
|
60
75
|
/** Storage a usar. Padrão: localStorage (se disponível). */
|
|
61
76
|
storage?: StorageLike;
|
|
@@ -63,6 +78,16 @@ export interface RequestCacheConfig {
|
|
|
63
78
|
storageKey?: string;
|
|
64
79
|
/** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */
|
|
65
80
|
maxEntries?: number;
|
|
81
|
+
/**
|
|
82
|
+
* Se true, imprime logs detalhados de cada operação (fetch de rede,
|
|
83
|
+
* cache hit, deduplicação, fallback, LRU, etc.). Padrão false.
|
|
84
|
+
* Útil para identificar se estão sendo feitas mais chamadas de rede
|
|
85
|
+
* do que o necessário. Pode ser ligado/desligado em runtime com
|
|
86
|
+
* `setDebug()`.
|
|
87
|
+
*/
|
|
88
|
+
debug?: boolean;
|
|
89
|
+
/** Logger customizado usado quando `debug` está ligado. Padrão: `console.debug`. */
|
|
90
|
+
logger?: DebugLogFn;
|
|
66
91
|
}
|
|
67
92
|
export declare class RequestCache {
|
|
68
93
|
private storage;
|
|
@@ -71,7 +96,11 @@ export declare class RequestCache {
|
|
|
71
96
|
private inflight;
|
|
72
97
|
/** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
|
|
73
98
|
private tick;
|
|
99
|
+
private debug;
|
|
100
|
+
private logger;
|
|
74
101
|
constructor(config?: RequestCacheConfig);
|
|
102
|
+
/** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
|
|
103
|
+
setDebug(enabled: boolean): void;
|
|
75
104
|
/**
|
|
76
105
|
* Busca uma URL usando cache.
|
|
77
106
|
* @param url Rota da requisição (também é a chave do cache).
|
|
@@ -91,6 +120,14 @@ export declare class RequestCache {
|
|
|
91
120
|
clear(): void;
|
|
92
121
|
/** Retorna um número de acesso estritamente crescente (para o LRU). */
|
|
93
122
|
private nextAccess;
|
|
123
|
+
/** Emite uma linha de log, só quando o modo debug está ligado. */
|
|
124
|
+
private log;
|
|
125
|
+
/**
|
|
126
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
127
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
128
|
+
* último erro.
|
|
129
|
+
*/
|
|
130
|
+
private fetchComRedundancia;
|
|
94
131
|
private revalidate;
|
|
95
132
|
/** Insere ou atualiza uma rota, aplicando o limite LRU. */
|
|
96
133
|
private upsert;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sevn/reqcache",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"description": "Cache de requisicoes HTTP em localStorage, com regra monotonica, deduplicacao e limite LRU. Pensado para cenarios de alto volume (ex.: apuracao de eleicoes).",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"module": "dist/index.esm.js",
|