@sevn/reqcache 1.0.0 → 1.1.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 +30 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.esm.js +27 -7
- package/dist/index.esm.js.map +1 -1
- package/dist/index.js +27 -7
- package/dist/index.js.map +1 -1
- package/dist/request-cache.d.ts +17 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -43,6 +43,34 @@ 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
|
+
|
|
46
74
|
## Configuração
|
|
47
75
|
|
|
48
76
|
```ts
|
|
@@ -62,6 +90,8 @@ const cache = new RequestCache({
|
|
|
62
90
|
| `fetchOptions` | `RequestInit` | — | Repassado ao `fetch` nativo (headers, method, signal...). |
|
|
63
91
|
| `staleOnError` | `boolean` | `true` | Devolve o dado antigo se a revalidação falhar. |
|
|
64
92
|
| `fetcher` | `typeof fetch`| `fetch`| `fetch` customizado (útil para testes). |
|
|
93
|
+
| `fallbackUrls` | `string[]` | — | URLs alternativas (outros domínios) tentadas em ordem se `url` falhar. |
|
|
94
|
+
| `onFallback` | `(url, erro) => void` | — | Chamado quando uma URL falha e a lib vai tentar a próxima. |
|
|
65
95
|
|
|
66
96
|
## Limpeza
|
|
67
97
|
|
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> {
|
|
@@ -91,6 +102,12 @@ declare class RequestCache {
|
|
|
91
102
|
clear(): void;
|
|
92
103
|
/** Retorna um número de acesso estritamente crescente (para o LRU). */
|
|
93
104
|
private nextAccess;
|
|
105
|
+
/**
|
|
106
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
107
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
108
|
+
* último erro.
|
|
109
|
+
*/
|
|
110
|
+
private fetchComRedundancia;
|
|
94
111
|
private revalidate;
|
|
95
112
|
/** Insere ou atualiza uma rota, aplicando o limite LRU. */
|
|
96
113
|
private upsert;
|
package/dist/index.esm.js
CHANGED
|
@@ -96,19 +96,39 @@ class RequestCache {
|
|
|
96
96
|
this.tick = Math.max(Date.now(), this.tick + 1);
|
|
97
97
|
return this.tick;
|
|
98
98
|
}
|
|
99
|
+
/**
|
|
100
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
101
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
102
|
+
* último erro.
|
|
103
|
+
*/
|
|
104
|
+
async fetchComRedundancia(candidatos, fetcher, options) {
|
|
105
|
+
var _a;
|
|
106
|
+
let ultimoErro;
|
|
107
|
+
for (const candidato of candidatos) {
|
|
108
|
+
try {
|
|
109
|
+
const res = await fetcher(candidato, options.fetchOptions);
|
|
110
|
+
if (!res.ok)
|
|
111
|
+
throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);
|
|
112
|
+
return (await res.json());
|
|
113
|
+
}
|
|
114
|
+
catch (err) {
|
|
115
|
+
ultimoErro = err;
|
|
116
|
+
(_a = options.onFallback) === null || _a === void 0 ? void 0 : _a.call(options, candidato, err);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
throw ultimoErro;
|
|
120
|
+
}
|
|
99
121
|
async revalidate(url, ttlMs, options, cached) {
|
|
100
|
-
var _a, _b;
|
|
122
|
+
var _a, _b, _c;
|
|
101
123
|
const fetcher = (_a = options.fetcher) !== null && _a !== void 0 ? _a : fetch;
|
|
124
|
+
const candidatos = [url, ...((_b = options.fallbackUrls) !== null && _b !== void 0 ? _b : [])];
|
|
102
125
|
let fresh;
|
|
103
126
|
try {
|
|
104
|
-
|
|
105
|
-
if (!res.ok)
|
|
106
|
-
throw new Error(`HTTP ${res.status} ao buscar ${url}`);
|
|
107
|
-
fresh = (await res.json());
|
|
127
|
+
fresh = await this.fetchComRedundancia(candidatos, fetcher, options);
|
|
108
128
|
}
|
|
109
129
|
catch (err) {
|
|
110
|
-
//
|
|
111
|
-
if (cached && ((
|
|
130
|
+
// Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.
|
|
131
|
+
if (cached && ((_c = options.staleOnError) !== null && _c !== void 0 ? _c : true)) {
|
|
112
132
|
this.upsert({
|
|
113
133
|
...cached,
|
|
114
134
|
expiresAt: Date.now() + ttlMs,
|
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\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":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;AA2BG;MA4DU,YAAY,CAAA;AAQvB,IAAA,WAAA,CAAY,SAA6B,EAAE,EAAA;;AAJnC,QAAA,IAAA,CAAA,QAAQ,GAAG,IAAI,GAAG,EAA4B;;QAE9C,IAAA,CAAA,IAAI,GAAG,CAAC;QAGd,IAAI,CAAC,UAAU,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,UAAU;QACjD,IAAI,CAAC,UAAU,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,GAAG;QAC1C,IAAI,CAAC,OAAO,GAAG,CAAA,EAAA,GAAA,MAAM,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,iBAAiB,EAAE;IACtD;AAEA;;;;;AAKG;IACH,MAAM,QAAQ,CACZ,GAAW,EACX,KAAa,EACb,UAA2B,EAAE,EAAA;AAE7B,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;AACtB,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE;AAC1B,QAAA,MAAM,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,GAAG,CAA8B;;QAG3E,IAAI,MAAM,IAAI,GAAG,GAAG,MAAM,CAAC,SAAS,EAAE;YACpC,MAAM,CAAC,UAAU,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;AACtC,YAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;YAClB,OAAO,MAAM,CAAC,IAAI;QACpB;;;QAIA,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAA2B;AAChE,QAAA,IAAI,OAAO;AAAE,YAAA,OAAO,OAAO;AAE3B,QAAA,MAAM,OAAO,GAAG,IAAI,CAAC,UAAU,CAAI,GAAG,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,KAAA,IAAA,IAAN,MAAM,cAAN,MAAM,GAAI,IAAI;AACnE,aAAA,OAAO,CAAC,MAAM,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAE3C,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC;AAC/B,QAAA,OAAO,OAAO;IAChB;;AAGA,IAAA,UAAU,CAAC,GAAW,EAAA;QACpB,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,GAAG,CAAC;AACxD,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;IACpB;AAEA;;;;AAIG;IACH,OAAO,CAAC,YAAY,GAAG,IAAI,EAAA;QACzB,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,YAAY,GAAG,IAAI;AAC/C,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE;AAC1B,QAAA,MAAM,QAAQ,GAAG,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,SAAS,GAAG,MAAM,CAAC;AACxD,QAAA,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC;AACvB,QAAA,OAAO,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC,MAAM;IACrC;;IAGA,KAAK,GAAA;QACH,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE;AACnB,QAAA,IAAI;YACF,IAAI,CAAC,OAAO,CAAC,UAAU,CAAC,IAAI,CAAC,UAAU,CAAC;QAC1C;AAAE,QAAA,OAAA,EAAA,EAAM;;QAER;IACF;;;IAKQ,UAAU,GAAA;AAChB,QAAA,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,EAAE,EAAE,IAAI,CAAC,IAAI,GAAG,CAAC,CAAC;QAC/C,OAAO,IAAI,CAAC,IAAI;IAClB;AAEA;;;;AAIG;AACK,IAAA,MAAM,mBAAmB,CAC/B,UAAoB,EACpB,OAAqB,EACrB,OAAwB,EAAA;;AAExB,QAAA,IAAI,UAAmB;AAEvB,QAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;AAClC,YAAA,IAAI;gBACF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC;gBAC1D,IAAI,CAAC,GAAG,CAAC,EAAE;oBAAE,MAAM,IAAI,KAAK,CAAC,CAAA,KAAA,EAAQ,GAAG,CAAC,MAAM,CAAA,WAAA,EAAc,SAAS,CAAA,CAAE,CAAC;AACzE,gBAAA,QAAQ,MAAM,GAAG,CAAC,IAAI,EAAE;YAC1B;YAAE,OAAO,GAAG,EAAE;gBACZ,UAAU,GAAG,GAAG;gBAChB,CAAA,EAAA,GAAA,OAAO,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,MAAA,GAAA,EAAA,CAAA,IAAA,CAAA,OAAA,EAAG,SAAS,EAAE,GAAG,CAAC;YACtC;QACF;AAEA,QAAA,MAAM,UAAU;IAClB;IAEQ,MAAM,UAAU,CACtB,GAAW,EACX,KAAa,EACb,OAAwB,EACxB,MAA4B,EAAA;;QAE5B,MAAM,OAAO,GAAG,CAAA,EAAA,GAAA,OAAO,CAAC,OAAO,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,KAAK;AACxC,QAAA,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,IAAI,CAAA,EAAA,GAAA,OAAO,CAAC,YAAY,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,EAAE,CAAC,CAAC;AACzD,QAAA,IAAI,KAAQ;AAEZ,QAAA,IAAI;AACF,YAAA,KAAK,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAI,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC;QACzE;QAAE,OAAO,GAAG,EAAE;;YAEZ,IAAI,MAAM,KAAK,CAAA,EAAA,GAAA,OAAO,CAAC,YAAY,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,IAAI,CAAC,EAAE;gBAC5C,IAAI,CAAC,MAAM,CAAC;AACV,oBAAA,GAAG,MAAM;AACT,oBAAA,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;AAC7B,oBAAA,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;AAC9B,iBAAA,CAAC;gBACF,OAAO,MAAM,CAAC,IAAI;YACpB;AACA,YAAA,MAAM,GAAG;QACX;;AAGA,QAAA,IAAI,MAAM,IAAI,OAAO,CAAC,YAAY,EAAE;AAClC,YAAA,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,EAAE,OAAO,CAAC,YAAY,CAAC;YACzD,MAAM,MAAM,GAAG,OAAO,CAAC,KAAK,EAAE,OAAO,CAAC,YAAY,CAAC;YACnD,MAAM,YAAY,GAChB,OAAO,MAAM,KAAK,QAAQ,IAAI,OAAO,MAAM,KAAK,QAAQ;AAE1D,YAAA,IAAI,YAAY,IAAK,MAAiB,IAAK,MAAiB,EAAE;;gBAE5D,IAAI,CAAC,MAAM,CAAC;AACV,oBAAA,GAAG,MAAM;AACT,oBAAA,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK;AAC7B,oBAAA,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;AAC9B,iBAAA,CAAC;gBACF,OAAO,MAAM,CAAC,IAAI;YACpB;QACF;;AAGA,QAAA,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,EAAE;QACtB,IAAI,CAAC,MAAM,CAAI;AACb,YAAA,IAAI,EAAE,GAAG;AACT,YAAA,IAAI,EAAE,KAAK;AACX,YAAA,SAAS,EAAE,GAAG;YACd,SAAS,EAAE,GAAG,GAAG,KAAK;AACtB,YAAA,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE;AAC9B,SAAA,CAAC;AACF,QAAA,OAAO,KAAK;IACd;;AAGQ,IAAA,MAAM,CAAI,KAAoB,EAAA;QACpC,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,IAAI,KAAK,KAAK,CAAC,IAAI,CAAC;AAC/D,QAAA,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC;AACf,QAAA,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC;AACvB,QAAA,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC;IACpB;;AAGQ,IAAA,aAAa,CAAC,GAAiB,EAAA;QACrC,OAAO,GAAG,CAAC,MAAM,GAAG,IAAI,CAAC,UAAU,EAAE;YACnC,IAAI,aAAa,GAAG,CAAC;AACrB,YAAA,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,GAAG,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE;AACnC,gBAAA,IAAI,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC,UAAU;oBAAE,aAAa,GAAG,CAAC;YAC1E;AACA,YAAA,GAAG,CAAC,MAAM,CAAC,aAAa,EAAE,CAAC,CAAC;QAC9B;IACF;IAEQ,OAAO,GAAA;QACb,IAAI,CAAC,IAAI,CAAC,OAAO;AAAE,YAAA,OAAO,EAAE;AAC5B,QAAA,IAAI;AACF,YAAA,MAAM,GAAG,GAAG,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,CAAC;AACjD,YAAA,IAAI,CAAC,GAAG;AAAE,gBAAA,OAAO,EAAE;YACnB,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC;AAC9B,YAAA,OAAO,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,GAAI,MAAuB,GAAG,EAAE;QAC9D;AAAE,QAAA,OAAA,EAAA,EAAM;YACN,OAAO,EAAE,CAAC;QACZ;IACF;AAEQ,IAAA,QAAQ,CAAC,GAAiB,EAAA;QAChC,IAAI,CAAC,IAAI,CAAC,OAAO;YAAE;AACnB,QAAA,IAAI;AACF,YAAA,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC;QAC5D;QAAE,OAAO,GAAG,EAAE;;YAEZ,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,EAAE;gBACvC,MAAM,QAAQ,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC,UAAU,GAAG,CAAC,CAAC,UAAU,CAAC;AACrE,gBAAA,QAAQ,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;AACnD,gBAAA,IAAI;AACF,oBAAA,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC;gBACjE;AAAE,gBAAA,OAAA,EAAA,EAAM;;gBAER;YACF;;QAEF;IACF;AACD;AAED;AACA,SAAS,OAAO,CAAC,GAAY,EAAE,IAAY,EAAA;AACzC,IAAA,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,MAAM,CAAU,CAAC,GAAG,EAAE,CAAC,KAAI;QAChD,IAAI,GAAG,IAAI,OAAO,GAAG,KAAK,QAAQ,IAAI,CAAC,IAAK,GAAc,EAAE;AAC1D,YAAA,OAAQ,GAA+B,CAAC,CAAC,CAAC;QAC5C;AACA,QAAA,OAAO,SAAS;IAClB,CAAC,EAAE,GAAG,CAAC;AACT;AAEA,SAAS,YAAY,CAAC,GAAY,EAAA;IAChC,QACE,GAAG,YAAY,KAAK;AACpB,SAAC,GAAG,CAAC,IAAI,KAAK,oBAAoB;AAChC,YAAA,GAAG,CAAC,IAAI,KAAK,4BAA4B,CAAC;AAEhD;AAEA;AACA,SAAS,iBAAiB,GAAA;AACxB,IAAA,IAAI;AACF,QAAA,IAAI,OAAO,YAAY,KAAK,WAAW,EAAE;YACvC,MAAM,KAAK,GAAG,oBAAoB;AAClC,YAAA,YAAY,CAAC,OAAO,CAAC,KAAK,EAAE,GAAG,CAAC;AAChC,YAAA,YAAY,CAAC,UAAU,CAAC,KAAK,CAAC;AAC9B,YAAA,OAAO,YAAY;QACrB;IACF;AAAE,IAAA,OAAA,EAAA,EAAM;;IAER;AACA,IAAA,OAAO,IAAI;AACb;AAEA;AACO,MAAM,YAAY,GAAG,IAAI,YAAY;;;;"}
|
package/dist/index.js
CHANGED
|
@@ -102,19 +102,39 @@
|
|
|
102
102
|
this.tick = Math.max(Date.now(), this.tick + 1);
|
|
103
103
|
return this.tick;
|
|
104
104
|
}
|
|
105
|
+
/**
|
|
106
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
107
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
108
|
+
* último erro.
|
|
109
|
+
*/
|
|
110
|
+
async fetchComRedundancia(candidatos, fetcher, options) {
|
|
111
|
+
var _a;
|
|
112
|
+
let ultimoErro;
|
|
113
|
+
for (const candidato of candidatos) {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetcher(candidato, options.fetchOptions);
|
|
116
|
+
if (!res.ok)
|
|
117
|
+
throw new Error(`HTTP ${res.status} ao buscar ${candidato}`);
|
|
118
|
+
return (await res.json());
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
ultimoErro = err;
|
|
122
|
+
(_a = options.onFallback) === null || _a === void 0 ? void 0 : _a.call(options, candidato, err);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
throw ultimoErro;
|
|
126
|
+
}
|
|
105
127
|
async revalidate(url, ttlMs, options, cached) {
|
|
106
|
-
var _a, _b;
|
|
128
|
+
var _a, _b, _c;
|
|
107
129
|
const fetcher = (_a = options.fetcher) !== null && _a !== void 0 ? _a : fetch;
|
|
130
|
+
const candidatos = [url, ...((_b = options.fallbackUrls) !== null && _b !== void 0 ? _b : [])];
|
|
108
131
|
let fresh;
|
|
109
132
|
try {
|
|
110
|
-
|
|
111
|
-
if (!res.ok)
|
|
112
|
-
throw new Error(`HTTP ${res.status} ao buscar ${url}`);
|
|
113
|
-
fresh = (await res.json());
|
|
133
|
+
fresh = await this.fetchComRedundancia(candidatos, fetcher, options);
|
|
114
134
|
}
|
|
115
135
|
catch (err) {
|
|
116
|
-
//
|
|
117
|
-
if (cached && ((
|
|
136
|
+
// Todos os domínios falharam. Se temos dado antigo e staleOnError, devolve o antigo.
|
|
137
|
+
if (cached && ((_c = options.staleOnError) !== null && _c !== void 0 ? _c : true)) {
|
|
118
138
|
this.upsert({
|
|
119
139
|
...cached,
|
|
120
140
|
expiresAt: Date.now() + ttlMs,
|
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":"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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\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":";;;;;;IAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;IA2BG;UA4DU,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;IAEA;;;;IAIG;IACK,IAAA,MAAM,mBAAmB,CAC/B,UAAoB,EACpB,OAAqB,EACrB,OAAwB,EAAA;;IAExB,QAAA,IAAI,UAAmB;IAEvB,QAAA,KAAK,MAAM,SAAS,IAAI,UAAU,EAAE;IAClC,YAAA,IAAI;oBACF,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC,SAAS,EAAE,OAAO,CAAC,YAAY,CAAC;oBAC1D,IAAI,CAAC,GAAG,CAAC,EAAE;wBAAE,MAAM,IAAI,KAAK,CAAC,CAAA,KAAA,EAAQ,GAAG,CAAC,MAAM,CAAA,WAAA,EAAc,SAAS,CAAA,CAAE,CAAC;IACzE,gBAAA,QAAQ,MAAM,GAAG,CAAC,IAAI,EAAE;gBAC1B;gBAAE,OAAO,GAAG,EAAE;oBACZ,UAAU,GAAG,GAAG;oBAChB,CAAA,EAAA,GAAA,OAAO,CAAC,UAAU,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,MAAA,GAAA,EAAA,CAAA,IAAA,CAAA,OAAA,EAAG,SAAS,EAAE,GAAG,CAAC;gBACtC;YACF;IAEA,QAAA,MAAM,UAAU;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,MAAM,UAAU,GAAG,CAAC,GAAG,EAAE,IAAI,CAAA,EAAA,GAAA,OAAO,CAAC,YAAY,MAAA,IAAA,IAAA,EAAA,KAAA,MAAA,GAAA,EAAA,GAAI,EAAE,CAAC,CAAC;IACzD,QAAA,IAAI,KAAQ;IAEZ,QAAA,IAAI;IACF,YAAA,KAAK,GAAG,MAAM,IAAI,CAAC,mBAAmB,CAAI,UAAU,EAAE,OAAO,EAAE,OAAO,CAAC;YACzE;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;;;;;;;;;"}
|
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> {
|
|
@@ -91,6 +102,12 @@ export declare class RequestCache {
|
|
|
91
102
|
clear(): void;
|
|
92
103
|
/** Retorna um número de acesso estritamente crescente (para o LRU). */
|
|
93
104
|
private nextAccess;
|
|
105
|
+
/**
|
|
106
|
+
* Tenta cada URL da lista em ordem (primário, depois os `fallbackUrls`).
|
|
107
|
+
* Devolve o JSON da primeira que responder OK; se todas falharem, lança o
|
|
108
|
+
* último erro.
|
|
109
|
+
*/
|
|
110
|
+
private fetchComRedundancia;
|
|
94
111
|
private revalidate;
|
|
95
112
|
/** Insere ou atualiza uma rota, aplicando o limite LRU. */
|
|
96
113
|
private upsert;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sevn/reqcache",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.1.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",
|