@sevn/reqcache 1.1.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 CHANGED
@@ -71,6 +71,37 @@ await requestCache.getFetch(url, 10_000, {
71
71
  });
72
72
  ```
73
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
+
74
105
  ## Configuração
75
106
 
76
107
  ```ts
@@ -82,6 +113,16 @@ const cache = new RequestCache({
82
113
  });
83
114
  ```
84
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
+
85
126
  ### Opções de `getFetch(url, ttlMs, options)`
86
127
 
87
128
  | Opção | Tipo | Padrão | Descrição |
package/dist/index.d.ts CHANGED
@@ -67,6 +67,10 @@ interface CacheEntry<T = unknown> {
67
67
  expiresAt: number;
68
68
  lastAccess: number;
69
69
  }
70
+ /** Uma linha de log do modo debug. */
71
+ interface DebugLogFn {
72
+ (message: string, details?: Record<string, unknown>): void;
73
+ }
70
74
  interface RequestCacheConfig {
71
75
  /** Storage a usar. Padrão: localStorage (se disponível). */
72
76
  storage?: StorageLike;
@@ -74,6 +78,16 @@ interface RequestCacheConfig {
74
78
  storageKey?: string;
75
79
  /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */
76
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;
77
91
  }
78
92
  declare class RequestCache {
79
93
  private storage;
@@ -82,7 +96,11 @@ declare class RequestCache {
82
96
  private inflight;
83
97
  /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
84
98
  private tick;
99
+ private debug;
100
+ private logger;
85
101
  constructor(config?: RequestCacheConfig);
102
+ /** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
103
+ setDebug(enabled: boolean): void;
86
104
  /**
87
105
  * Busca uma URL usando cache.
88
106
  * @param url Rota da requisição (também é a chave do cache).
@@ -102,6 +120,8 @@ declare class RequestCache {
102
120
  clear(): void;
103
121
  /** Retorna um número de acesso estritamente crescente (para o LRU). */
104
122
  private nextAccess;
123
+ /** Emite uma linha de log, só quando o modo debug está ligado. */
124
+ private log;
105
125
  /**
106
126
  * Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
107
127
  * Devolve o JSON da primeira que responder OK; se todas falharem, lança o
@@ -120,4 +140,4 @@ declare class RequestCache {
120
140
  declare const requestCache: RequestCache;
121
141
 
122
142
  export { RequestCache, requestCache };
123
- 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
- return all.length - mantidas.length;
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,6 +109,12 @@ 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
+ }
99
118
  /**
100
119
  * Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
101
120
  * Devolve o JSON da primeira que responder OK; se todas falharem, lança o
@@ -105,13 +124,16 @@ class RequestCache {
105
124
  var _a;
106
125
  let ultimoErro;
107
126
  for (const candidato of candidatos) {
127
+ this.log("fetch de rede", { url: candidato });
108
128
  try {
109
129
  const res = await fetcher(candidato, options.fetchOptions);
110
130
  if (!res.ok)
111
131
  throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);
132
+ this.log("fetch OK", { url: candidato, status: res.status });
112
133
  return (await res.json());
113
134
  }
114
135
  catch (err) {
136
+ this.log("fetch falhou", { url: candidato, erro: String(err) });
115
137
  ultimoErro = err;
116
138
  (_a = options.onFallback) === null || _a === void 0 ? void 0 : _a.call(options, candidato, err);
117
139
  }
@@ -129,6 +151,7 @@ class RequestCache {
129
151
  catch (err) {
130
152
  // Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.
131
153
  if (cached && ((_c = options.staleOnError) !== null && _c !== void 0 ? _c : true)) {
154
+ this.log("staleOnError: devolvendo dado antigo", { url });
132
155
  this.upsert({
133
156
  ...cached,
134
157
  expiresAt: Date.now() + ttlMs,
@@ -145,6 +168,11 @@ class RequestCache {
145
168
  const ambosNumeros = typeof oldVal === "number" && typeof newVal === "number";
146
169
  if (ambosNumeros && newVal <= oldVal) {
147
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
+ });
148
176
  this.upsert({
149
177
  ...cached,
150
178
  expiresAt: Date.now() + ttlMs,
@@ -155,6 +183,7 @@ class RequestCache {
155
183
  }
156
184
  // 4. Grava e devolve o dado novo.
157
185
  const now = Date.now();
186
+ this.log("cache gravado", { url, ttlMs });
158
187
  this.upsert({
159
188
  rota: url,
160
189
  data: fresh,
@@ -179,6 +208,7 @@ class RequestCache {
179
208
  if (all[i].lastAccess < all[idxMaisAntigo].lastAccess)
180
209
  idxMaisAntigo = i;
181
210
  }
211
+ this.log("LRU: removendo rota menos usada", { url: all[idxMaisAntigo].rota });
182
212
  all.splice(idxMaisAntigo, 1);
183
213
  }
184
214
  }
@@ -207,6 +237,10 @@ class RequestCache {
207
237
  if (isQuotaError(err) && all.length > 0) {
208
238
  const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);
209
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
+ });
210
244
  try {
211
245
  this.storage.setItem(this.storageKey, JSON.stringify(reduzido));
212
246
  }
@@ -232,6 +266,15 @@ function isQuotaError(err) {
232
266
  (err.name === "QuotaExceededError" ||
233
267
  err.name === "NS_ERROR_DOM_QUOTA_REACHED"));
234
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
+ }
235
278
  /** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */
236
279
  function getDefaultStorage() {
237
280
  try {
@@ -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 * 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\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 /**\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 try {\n const res = await fetcher(candidato, options.fetchOptions);\n if (!res.ok) throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);\n return (await res.json()) as T;\n } catch (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.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
- return all.length - mantidas.length;
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,6 +115,12 @@
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
+ }
105
124
  /**
106
125
  * Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
107
126
  * Devolve o JSON da primeira que responder OK; se todas falharem, lança o
@@ -111,13 +130,16 @@
111
130
  var _a;
112
131
  let ultimoErro;
113
132
  for (const candidato of candidatos) {
133
+ this.log("fetch de rede", { url: candidato });
114
134
  try {
115
135
  const res = await fetcher(candidato, options.fetchOptions);
116
136
  if (!res.ok)
117
137
  throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);
138
+ this.log("fetch OK", { url: candidato, status: res.status });
118
139
  return (await res.json());
119
140
  }
120
141
  catch (err) {
142
+ this.log("fetch falhou", { url: candidato, erro: String(err) });
121
143
  ultimoErro = err;
122
144
  (_a = options.onFallback) === null || _a === void 0 ? void 0 : _a.call(options, candidato, err);
123
145
  }
@@ -135,6 +157,7 @@
135
157
  catch (err) {
136
158
  // Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.
137
159
  if (cached && ((_c = options.staleOnError) !== null && _c !== void 0 ? _c : true)) {
160
+ this.log("staleOnError: devolvendo dado antigo", { url });
138
161
  this.upsert({
139
162
  ...cached,
140
163
  expiresAt: Date.now() + ttlMs,
@@ -151,6 +174,11 @@
151
174
  const ambosNumeros = typeof oldVal === "number" && typeof newVal === "number";
152
175
  if (ambosNumeros && newVal <= oldVal) {
153
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
+ });
154
182
  this.upsert({
155
183
  ...cached,
156
184
  expiresAt: Date.now() + ttlMs,
@@ -161,6 +189,7 @@
161
189
  }
162
190
  // 4. Grava e devolve o dado novo.
163
191
  const now = Date.now();
192
+ this.log("cache gravado", { url, ttlMs });
164
193
  this.upsert({
165
194
  rota: url,
166
195
  data: fresh,
@@ -185,6 +214,7 @@
185
214
  if (all[i].lastAccess < all[idxMaisAntigo].lastAccess)
186
215
  idxMaisAntigo = i;
187
216
  }
217
+ this.log("LRU: removendo rota menos usada", { url: all[idxMaisAntigo].rota });
188
218
  all.splice(idxMaisAntigo, 1);
189
219
  }
190
220
  }
@@ -213,6 +243,10 @@
213
243
  if (isQuotaError(err) && all.length > 0) {
214
244
  const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);
215
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
+ });
216
250
  try {
217
251
  this.storage.setItem(this.storageKey, JSON.stringify(reduzido));
218
252
  }
@@ -238,6 +272,15 @@
238
272
  (err.name === "QuotaExceededError" ||
239
273
  err.name === "NS_ERROR_DOM_QUOTA_REACHED"));
240
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
+ }
241
284
  /** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */
242
285
  function getDefaultStorage() {
243
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 * 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\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 /**\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 try {\n const res = await fetcher(candidato, options.fetchOptions);\n if (!res.ok) throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);\n return (await res.json()) as T;\n } catch (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.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.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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"}
@@ -67,6 +67,10 @@ export interface CacheEntry<T = unknown> {
67
67
  expiresAt: number;
68
68
  lastAccess: number;
69
69
  }
70
+ /** Uma linha de log do modo debug. */
71
+ export interface DebugLogFn {
72
+ (message: string, details?: Record<string, unknown>): void;
73
+ }
70
74
  export interface RequestCacheConfig {
71
75
  /** Storage a usar. Padrão: localStorage (se disponível). */
72
76
  storage?: StorageLike;
@@ -74,6 +78,16 @@ export interface RequestCacheConfig {
74
78
  storageKey?: string;
75
79
  /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */
76
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;
77
91
  }
78
92
  export declare class RequestCache {
79
93
  private storage;
@@ -82,7 +96,11 @@ export declare class RequestCache {
82
96
  private inflight;
83
97
  /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
84
98
  private tick;
99
+ private debug;
100
+ private logger;
85
101
  constructor(config?: RequestCacheConfig);
102
+ /** Liga/desliga o modo debug em runtime, sem precisar recriar a instância. */
103
+ setDebug(enabled: boolean): void;
86
104
  /**
87
105
  * Busca uma URL usando cache.
88
106
  * @param url Rota da requisição (também é a chave do cache).
@@ -102,6 +120,8 @@ export declare class RequestCache {
102
120
  clear(): void;
103
121
  /** Retorna um número de acesso estritamente crescente (para o LRU). */
104
122
  private nextAccess;
123
+ /** Emite uma linha de log, só quando o modo debug está ligado. */
124
+ private log;
105
125
  /**
106
126
  * Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
107
127
  * Devolve o JSON da primeira que responder OK; se todas falharem, lança o
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sevn/reqcache",
3
- "version": "1.1.0",
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",