@sevn/reqcache 1.0.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 SEVN
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,98 @@
1
+ # @sevn/reqcache
2
+
3
+ Cache de requisições HTTP em `localStorage`, pensado para cenários de alto
4
+ volume em curto período (ex.: apuração de eleições).
5
+
6
+ - **Lazy**: só revalida quando você chama `getFetch` e o cache expirou. Nada roda em segundo plano.
7
+ - **Regra monotônica**: após expirar, só aceita o dado novo se um valor numérico tiver aumentado (protege contra respostas inconsistentes da API).
8
+ - **Deduplicação**: chamadas simultâneas à mesma rota disparam um único fetch.
9
+ - **Limite de espaço**: `maxEntries` remove a rota menos usada (LRU) automaticamente; nunca estoura o `localStorage`.
10
+ - **Resiliência**: se a API falhar, devolve o último dado válido (`staleOnError`).
11
+
12
+ ## Instalação
13
+
14
+ ```bash
15
+ npm install @sevn/reqcache
16
+ ```
17
+
18
+ ## Uso básico
19
+
20
+ ```ts
21
+ import { requestCache } from "@sevn/reqcache";
22
+
23
+ // 1ª chamada: bate na API e cacheia por 10s.
24
+ // Chamadas dentro dos 10s: vêm do cache.
25
+ const dados = await requestCache.getFetch(
26
+ "https://api.eleicoes.gov/resultado/presidente",
27
+ 10_000 // TTL em milissegundos
28
+ );
29
+ ```
30
+
31
+ ## Com a regra monotônica
32
+
33
+ Ideal para contagens que só devem crescer (votos apurados, por exemplo):
34
+
35
+ ```ts
36
+ const resultado = await requestCache.getFetch(
37
+ "https://api.eleicoes.gov/resultado/presidente",
38
+ 10_000,
39
+ { monotonicKey: "resultado.votosApurados" }
40
+ );
41
+ ```
42
+
43
+ Se, na revalidação, a API devolver um `votosApurados` **menor ou igual** ao
44
+ guardado, a resposta nova é descartada e o dado antigo é mantido.
45
+
46
+ ## Configuração
47
+
48
+ ```ts
49
+ import { RequestCache } from "@sevn/reqcache";
50
+
51
+ const cache = new RequestCache({
52
+ storageKey: "eleicoes-cache", // chave única no localStorage
53
+ maxEntries: 200, // limite de rotas antes de acionar o LRU
54
+ });
55
+ ```
56
+
57
+ ### Opções de `getFetch(url, ttlMs, options)`
58
+
59
+ | Opção | Tipo | Padrão | Descrição |
60
+ | -------------- | ------------- | ------ | ---------------------------------------------------------------- |
61
+ | `monotonicKey` | `string` | — | Caminho da chave numérica ("a.b.c") que só pode crescer. |
62
+ | `fetchOptions` | `RequestInit` | — | Repassado ao `fetch` nativo (headers, method, signal...). |
63
+ | `staleOnError` | `boolean` | `true` | Devolve o dado antigo se a revalidação falhar. |
64
+ | `fetcher` | `typeof fetch`| `fetch`| `fetch` customizado (útil para testes). |
65
+
66
+ ## Limpeza
67
+
68
+ O `maxEntries` já protege o espaço automaticamente a cada gravação. Para uma
69
+ limpeza explícita de rotas abandonadas, chame `cleanup` na inicialização do app:
70
+
71
+ ```ts
72
+ requestCache.cleanup(); // remove rotas expiradas há mais de 1h (padrão)
73
+ ```
74
+
75
+ Outros métodos: `invalidate(url)` remove uma rota, `clear()` esvazia tudo.
76
+
77
+ ## Quando migrar para IndexedDB
78
+
79
+ O `localStorage` é síncrono e limitado a ~5 MB por domínio. Para respostas
80
+ pequenas de placar/apuração, é suficiente. Se você for cachear payloads grandes
81
+ (ex.: resultado seção por seção) ou notar travadinhas no pico, troque o backend
82
+ por IndexedDB — o `storage` é injetável, então a lógica de cache não muda:
83
+
84
+ ```ts
85
+ new RequestCache({ storage: meuAdaptadorIndexedDB });
86
+ ```
87
+
88
+ ## Desenvolvimento
89
+
90
+ ```bash
91
+ npm run typecheck # checagem de tipos
92
+ npm test # roda os testes (vitest)
93
+ npm run build # gera dist/ (JS + tipos)
94
+ ```
95
+
96
+ ## Licença
97
+
98
+ MIT
@@ -0,0 +1,106 @@
1
+ /**
2
+ * request-cache
3
+ * -------------
4
+ * Cache de requisições HTTP em localStorage, pensado para cenários de
5
+ * alto volume em curto período (ex.: apuração de eleições).
6
+ *
7
+ * Modelo de armazenamento:
8
+ * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.
9
+ * Cada objeto é indexado pela `rota` (a URL requisitada):
10
+ * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]
11
+ *
12
+ * Comportamento:
13
+ * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.
14
+ * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.
15
+ * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).
16
+ * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico
17
+ * dessa chave for MAIOR que o guardado; senão mantém o antigo.
18
+ *
19
+ * Proteção de espaço (limite de ~5 MB do localStorage):
20
+ * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos
21
+ * recentemente usada (LRU). Roda sozinho, sem timer.
22
+ * - Em erro de quota, remove as entradas mais antigas e tenta de novo.
23
+ * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.
24
+ *
25
+ * Extras para o caso de eleição:
26
+ * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.
27
+ * - staleOnError: se a rede/API falhar, devolve o último dado válido.
28
+ */
29
+ interface StorageLike {
30
+ getItem(key: string): string | null;
31
+ setItem(key: string, value: string): void;
32
+ removeItem(key: string): void;
33
+ }
34
+ interface GetFetchOptions {
35
+ /**
36
+ * Caminho da chave numérica monotônica. Suporta aninhamento com ponto.
37
+ * Ex.: "total", "resultado.votosApurados", "data.candidato.votos".
38
+ * Se o valor novo NÃO for maior que o antigo, mantém o dado em cache.
39
+ */
40
+ monotonicKey?: string;
41
+ /** Opções nativas repassadas ao fetch (headers, method, body, signal...). */
42
+ fetchOptions?: RequestInit;
43
+ /**
44
+ * Se true (padrão), quando a nova requisição falhar e existir dado antigo,
45
+ * devolve esse dado antigo em vez de lançar erro.
46
+ */
47
+ staleOnError?: boolean;
48
+ /** fetch customizado — útil para testes ou ambientes sem fetch global. */
49
+ fetcher?: typeof fetch;
50
+ }
51
+ /** Uma entrada do cache. `rota` é a chave identificadora. */
52
+ interface CacheEntry<T = unknown> {
53
+ rota: string;
54
+ data: T;
55
+ createdAt: number;
56
+ expiresAt: number;
57
+ lastAccess: number;
58
+ }
59
+ interface RequestCacheConfig {
60
+ /** Storage a usar. Padrão: localStorage (se disponível). */
61
+ storage?: StorageLike;
62
+ /** Chave única onde o array é guardado. Padrão "reqcache". */
63
+ storageKey?: string;
64
+ /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */
65
+ maxEntries?: number;
66
+ }
67
+ declare class RequestCache {
68
+ private storage;
69
+ private storageKey;
70
+ private maxEntries;
71
+ private inflight;
72
+ /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
73
+ private tick;
74
+ constructor(config?: RequestCacheConfig);
75
+ /**
76
+ * Busca uma URL usando cache.
77
+ * @param url Rota da requisição (também é a chave do cache).
78
+ * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).
79
+ * @param options Regra monotônica, opções de fetch, etc.
80
+ */
81
+ getFetch<T = unknown>(url: string, ttlMs: number, options?: GetFetchOptions): Promise<T>;
82
+ /** Remove uma rota específica do cache. */
83
+ invalidate(url: string): void;
84
+ /**
85
+ * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).
86
+ * Chame na inicialização do app, ou de tempos em tempos.
87
+ * @returns quantidade de rotas removidas.
88
+ */
89
+ cleanup(graceSeconds?: number): number;
90
+ /** Esvazia todo o cache. */
91
+ clear(): void;
92
+ /** Retorna um número de acesso estritamente crescente (para o LRU). */
93
+ private nextAccess;
94
+ private revalidate;
95
+ /** Insere ou atualiza uma rota, aplicando o limite LRU. */
96
+ private upsert;
97
+ /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */
98
+ private evictIfNeeded;
99
+ private readAll;
100
+ private writeAll;
101
+ }
102
+ /** Instância pronta para uso, caso não queira configurar nada. */
103
+ declare const requestCache: RequestCache;
104
+
105
+ export { RequestCache, requestCache };
106
+ export type { CacheEntry, GetFetchOptions, RequestCacheConfig, StorageLike };
@@ -0,0 +1,234 @@
1
+ /**
2
+ * request-cache
3
+ * -------------
4
+ * Cache de requisições HTTP em localStorage, pensado para cenários de
5
+ * alto volume em curto período (ex.: apuração de eleições).
6
+ *
7
+ * Modelo de armazenamento:
8
+ * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.
9
+ * Cada objeto é indexado pela `rota` (a URL requisitada):
10
+ * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]
11
+ *
12
+ * Comportamento:
13
+ * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.
14
+ * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.
15
+ * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).
16
+ * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico
17
+ * dessa chave for MAIOR que o guardado; senão mantém o antigo.
18
+ *
19
+ * Proteção de espaço (limite de ~5 MB do localStorage):
20
+ * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos
21
+ * recentemente usada (LRU). Roda sozinho, sem timer.
22
+ * - Em erro de quota, remove as entradas mais antigas e tenta de novo.
23
+ * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.
24
+ *
25
+ * Extras para o caso de eleição:
26
+ * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.
27
+ * - staleOnError: se a rede/API falhar, devolve o último dado válido.
28
+ */
29
+ class RequestCache {
30
+ constructor(config = {}) {
31
+ var _a, _b, _c;
32
+ this.inflight = new Map();
33
+ /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
34
+ this.tick = 0;
35
+ this.storageKey = (_a = config.storageKey) !== null && _a !== void 0 ? _a : "reqcache";
36
+ this.maxEntries = (_b = config.maxEntries) !== null && _b !== void 0 ? _b : 100;
37
+ this.storage = (_c = config.storage) !== null && _c !== void 0 ? _c : getDefaultStorage();
38
+ }
39
+ /**
40
+ * Busca uma URL usando cache.
41
+ * @param url Rota da requisição (também é a chave do cache).
42
+ * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).
43
+ * @param options Regra monotônica, opções de fetch, etc.
44
+ */
45
+ async getFetch(url, ttlMs, options = {}) {
46
+ const now = Date.now();
47
+ const all = this.readAll();
48
+ const cached = all.find((e) => e.rota === url);
49
+ // 1. Cache ainda válido -> devolve sem tocar na rede.
50
+ if (cached && now < cached.expiresAt) {
51
+ cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU
52
+ this.writeAll(all);
53
+ return cached.data;
54
+ }
55
+ // 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,
56
+ // todas as chamadas concorrentes aguardam a mesma Promise.
57
+ const pending = this.inflight.get(url);
58
+ if (pending)
59
+ return pending;
60
+ const promise = this.revalidate(url, ttlMs, options, cached !== null && cached !== void 0 ? cached : null)
61
+ .finally(() => this.inflight.delete(url));
62
+ this.inflight.set(url, promise);
63
+ return promise;
64
+ }
65
+ /** Remove uma rota específica do cache. */
66
+ invalidate(url) {
67
+ const all = this.readAll().filter((e) => e.rota !== url);
68
+ this.writeAll(all);
69
+ }
70
+ /**
71
+ * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).
72
+ * Chame na inicialização do app, ou de tempos em tempos.
73
+ * @returns quantidade de rotas removidas.
74
+ */
75
+ cleanup(graceSeconds = 3600) {
76
+ const limite = Date.now() - graceSeconds * 1000;
77
+ const all = this.readAll();
78
+ const mantidas = all.filter((e) => e.expiresAt > limite);
79
+ this.writeAll(mantidas);
80
+ return all.length - mantidas.length;
81
+ }
82
+ /** Esvazia todo o cache. */
83
+ clear() {
84
+ if (!this.storage)
85
+ return;
86
+ try {
87
+ this.storage.removeItem(this.storageKey);
88
+ }
89
+ catch (_a) {
90
+ /* storage indisponível */
91
+ }
92
+ }
93
+ // --- interno ------------------------------------------------------------
94
+ /** Retorna um número de acesso estritamente crescente (para o LRU). */
95
+ nextAccess() {
96
+ this.tick = Math.max(Date.now(), this.tick + 1);
97
+ return this.tick;
98
+ }
99
+ async revalidate(url, ttlMs, options, cached) {
100
+ var _a, _b;
101
+ const fetcher = (_a = options.fetcher) !== null && _a !== void 0 ? _a : fetch;
102
+ let fresh;
103
+ try {
104
+ const res = await fetcher(url, options.fetchOptions);
105
+ if (!res.ok)
106
+ throw new Error(`HTTP ${res.status} ao buscar ${url}`);
107
+ fresh = (await res.json());
108
+ }
109
+ catch (err) {
110
+ // Rede/API falhou. Se temos dado antigo e staleOnError, devolve o antigo.
111
+ if (cached && ((_b = options.staleOnError) !== null && _b !== void 0 ? _b : true)) {
112
+ this.upsert({
113
+ ...cached,
114
+ expiresAt: Date.now() + ttlMs,
115
+ lastAccess: this.nextAccess(),
116
+ });
117
+ return cached.data;
118
+ }
119
+ throw err;
120
+ }
121
+ // 3. Regra monotônica: só aceita o novo dado se a chave numérica CRESCEU.
122
+ if (cached && options.monotonicKey) {
123
+ const oldVal = getPath(cached.data, options.monotonicKey);
124
+ const newVal = getPath(fresh, options.monotonicKey);
125
+ const ambosNumeros = typeof oldVal === "number" && typeof newVal === "number";
126
+ if (ambosNumeros && newVal <= oldVal) {
127
+ // Valor não aumentou -> mantém o dado antigo, só renova a expiração.
128
+ this.upsert({
129
+ ...cached,
130
+ expiresAt: Date.now() + ttlMs,
131
+ lastAccess: this.nextAccess(),
132
+ });
133
+ return cached.data;
134
+ }
135
+ }
136
+ // 4. Grava e devolve o dado novo.
137
+ const now = Date.now();
138
+ this.upsert({
139
+ rota: url,
140
+ data: fresh,
141
+ createdAt: now,
142
+ expiresAt: now + ttlMs,
143
+ lastAccess: this.nextAccess(),
144
+ });
145
+ return fresh;
146
+ }
147
+ /** Insere ou atualiza uma rota, aplicando o limite LRU. */
148
+ upsert(entry) {
149
+ const all = this.readAll().filter((e) => e.rota !== entry.rota);
150
+ all.push(entry);
151
+ this.evictIfNeeded(all);
152
+ this.writeAll(all);
153
+ }
154
+ /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */
155
+ evictIfNeeded(all) {
156
+ while (all.length > this.maxEntries) {
157
+ let idxMaisAntigo = 0;
158
+ for (let i = 1; i < all.length; i++) {
159
+ if (all[i].lastAccess < all[idxMaisAntigo].lastAccess)
160
+ idxMaisAntigo = i;
161
+ }
162
+ all.splice(idxMaisAntigo, 1);
163
+ }
164
+ }
165
+ readAll() {
166
+ if (!this.storage)
167
+ return [];
168
+ try {
169
+ const raw = this.storage.getItem(this.storageKey);
170
+ if (!raw)
171
+ return [];
172
+ const parsed = JSON.parse(raw);
173
+ return Array.isArray(parsed) ? parsed : [];
174
+ }
175
+ catch (_a) {
176
+ return []; // JSON corrompido -> trata como cache vazio
177
+ }
178
+ }
179
+ writeAll(all) {
180
+ if (!this.storage)
181
+ return;
182
+ try {
183
+ this.storage.setItem(this.storageKey, JSON.stringify(all));
184
+ }
185
+ catch (err) {
186
+ // Quota estourada: remove os mais antigos e tenta de novo.
187
+ if (isQuotaError(err) && all.length > 0) {
188
+ const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);
189
+ reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade
190
+ try {
191
+ this.storage.setItem(this.storageKey, JSON.stringify(reduzido));
192
+ }
193
+ catch (_a) {
194
+ /* ainda assim falhou -> desiste de persistir; dados já retornam */
195
+ }
196
+ }
197
+ // Falha silenciosa: os dados ainda são devolvidos ao chamador.
198
+ }
199
+ }
200
+ }
201
+ /** Lê um caminho aninhado ("a.b.c") de um objeto de forma segura. */
202
+ function getPath(obj, path) {
203
+ return path.split(".").reduce((acc, k) => {
204
+ if (acc && typeof acc === "object" && k in acc) {
205
+ return acc[k];
206
+ }
207
+ return undefined;
208
+ }, obj);
209
+ }
210
+ function isQuotaError(err) {
211
+ return (err instanceof Error &&
212
+ (err.name === "QuotaExceededError" ||
213
+ err.name === "NS_ERROR_DOM_QUOTA_REACHED"));
214
+ }
215
+ /** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */
216
+ function getDefaultStorage() {
217
+ try {
218
+ if (typeof localStorage !== "undefined") {
219
+ const probe = "__reqcache_probe__";
220
+ localStorage.setItem(probe, "1");
221
+ localStorage.removeItem(probe);
222
+ return localStorage;
223
+ }
224
+ }
225
+ catch (_a) {
226
+ /* Safari em modo privado antigo lança aqui */
227
+ }
228
+ return null;
229
+ }
230
+ /** Instância pronta para uso, caso não queira configurar nada. */
231
+ const requestCache = new RequestCache();
232
+
233
+ export { RequestCache, requestCache };
234
+ //# sourceMappingURL=index.esm.js.map
@@ -0,0 +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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"}
package/dist/index.js ADDED
@@ -0,0 +1,243 @@
1
+ (function (global, factory) {
2
+ typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) :
3
+ typeof define === 'function' && define.amd ? define(['exports'], factory) :
4
+ (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.SevnRequestCache = {}));
5
+ })(this, (function (exports) { 'use strict';
6
+
7
+ /**
8
+ * request-cache
9
+ * -------------
10
+ * Cache de requisições HTTP em localStorage, pensado para cenários de
11
+ * alto volume em curto período (ex.: apuração de eleições).
12
+ *
13
+ * Modelo de armazenamento:
14
+ * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.
15
+ * Cada objeto é indexado pela `rota` (a URL requisitada):
16
+ * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]
17
+ *
18
+ * Comportamento:
19
+ * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.
20
+ * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.
21
+ * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).
22
+ * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico
23
+ * dessa chave for MAIOR que o guardado; senão mantém o antigo.
24
+ *
25
+ * Proteção de espaço (limite de ~5 MB do localStorage):
26
+ * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos
27
+ * recentemente usada (LRU). Roda sozinho, sem timer.
28
+ * - Em erro de quota, remove as entradas mais antigas e tenta de novo.
29
+ * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.
30
+ *
31
+ * Extras para o caso de eleição:
32
+ * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.
33
+ * - staleOnError: se a rede/API falhar, devolve o último dado válido.
34
+ */
35
+ class RequestCache {
36
+ constructor(config = {}) {
37
+ var _a, _b, _c;
38
+ this.inflight = new Map();
39
+ /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
40
+ this.tick = 0;
41
+ this.storageKey = (_a = config.storageKey) !== null && _a !== void 0 ? _a : "reqcache";
42
+ this.maxEntries = (_b = config.maxEntries) !== null && _b !== void 0 ? _b : 100;
43
+ this.storage = (_c = config.storage) !== null && _c !== void 0 ? _c : getDefaultStorage();
44
+ }
45
+ /**
46
+ * Busca uma URL usando cache.
47
+ * @param url Rota da requisição (também é a chave do cache).
48
+ * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).
49
+ * @param options Regra monotônica, opções de fetch, etc.
50
+ */
51
+ async getFetch(url, ttlMs, options = {}) {
52
+ const now = Date.now();
53
+ const all = this.readAll();
54
+ const cached = all.find((e) => e.rota === url);
55
+ // 1. Cache ainda válido -> devolve sem tocar na rede.
56
+ if (cached && now < cached.expiresAt) {
57
+ cached.lastAccess = this.nextAccess(); // marca uso p/ o LRU
58
+ this.writeAll(all);
59
+ return cached.data;
60
+ }
61
+ // 2. Deduplicação: se já há uma requisição em andamento p/ essa rota,
62
+ // todas as chamadas concorrentes aguardam a mesma Promise.
63
+ const pending = this.inflight.get(url);
64
+ if (pending)
65
+ return pending;
66
+ const promise = this.revalidate(url, ttlMs, options, cached !== null && cached !== void 0 ? cached : null)
67
+ .finally(() => this.inflight.delete(url));
68
+ this.inflight.set(url, promise);
69
+ return promise;
70
+ }
71
+ /** Remove uma rota específica do cache. */
72
+ invalidate(url) {
73
+ const all = this.readAll().filter((e) => e.rota !== url);
74
+ this.writeAll(all);
75
+ }
76
+ /**
77
+ * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).
78
+ * Chame na inicialização do app, ou de tempos em tempos.
79
+ * @returns quantidade de rotas removidas.
80
+ */
81
+ cleanup(graceSeconds = 3600) {
82
+ const limite = Date.now() - graceSeconds * 1000;
83
+ const all = this.readAll();
84
+ const mantidas = all.filter((e) => e.expiresAt > limite);
85
+ this.writeAll(mantidas);
86
+ return all.length - mantidas.length;
87
+ }
88
+ /** Esvazia todo o cache. */
89
+ clear() {
90
+ if (!this.storage)
91
+ return;
92
+ try {
93
+ this.storage.removeItem(this.storageKey);
94
+ }
95
+ catch (_a) {
96
+ /* storage indisponível */
97
+ }
98
+ }
99
+ // --- interno ------------------------------------------------------------
100
+ /** Retorna um número de acesso estritamente crescente (para o LRU). */
101
+ nextAccess() {
102
+ this.tick = Math.max(Date.now(), this.tick + 1);
103
+ return this.tick;
104
+ }
105
+ async revalidate(url, ttlMs, options, cached) {
106
+ var _a, _b;
107
+ const fetcher = (_a = options.fetcher) !== null && _a !== void 0 ? _a : fetch;
108
+ let fresh;
109
+ try {
110
+ const res = await fetcher(url, options.fetchOptions);
111
+ if (!res.ok)
112
+ throw new Error(`HTTP ${res.status} ao buscar ${url}`);
113
+ fresh = (await res.json());
114
+ }
115
+ catch (err) {
116
+ // Rede/API falhou. Se temos dado antigo e staleOnError, devolve o antigo.
117
+ if (cached && ((_b = options.staleOnError) !== null && _b !== void 0 ? _b : true)) {
118
+ this.upsert({
119
+ ...cached,
120
+ expiresAt: Date.now() + ttlMs,
121
+ lastAccess: this.nextAccess(),
122
+ });
123
+ return cached.data;
124
+ }
125
+ throw err;
126
+ }
127
+ // 3. Regra monotônica: só aceita o novo dado se a chave numérica CRESCEU.
128
+ if (cached && options.monotonicKey) {
129
+ const oldVal = getPath(cached.data, options.monotonicKey);
130
+ const newVal = getPath(fresh, options.monotonicKey);
131
+ const ambosNumeros = typeof oldVal === "number" && typeof newVal === "number";
132
+ if (ambosNumeros && newVal <= oldVal) {
133
+ // Valor não aumentou -> mantém o dado antigo, só renova a expiração.
134
+ this.upsert({
135
+ ...cached,
136
+ expiresAt: Date.now() + ttlMs,
137
+ lastAccess: this.nextAccess(),
138
+ });
139
+ return cached.data;
140
+ }
141
+ }
142
+ // 4. Grava e devolve o dado novo.
143
+ const now = Date.now();
144
+ this.upsert({
145
+ rota: url,
146
+ data: fresh,
147
+ createdAt: now,
148
+ expiresAt: now + ttlMs,
149
+ lastAccess: this.nextAccess(),
150
+ });
151
+ return fresh;
152
+ }
153
+ /** Insere ou atualiza uma rota, aplicando o limite LRU. */
154
+ upsert(entry) {
155
+ const all = this.readAll().filter((e) => e.rota !== entry.rota);
156
+ all.push(entry);
157
+ this.evictIfNeeded(all);
158
+ this.writeAll(all);
159
+ }
160
+ /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */
161
+ evictIfNeeded(all) {
162
+ while (all.length > this.maxEntries) {
163
+ let idxMaisAntigo = 0;
164
+ for (let i = 1; i < all.length; i++) {
165
+ if (all[i].lastAccess < all[idxMaisAntigo].lastAccess)
166
+ idxMaisAntigo = i;
167
+ }
168
+ all.splice(idxMaisAntigo, 1);
169
+ }
170
+ }
171
+ readAll() {
172
+ if (!this.storage)
173
+ return [];
174
+ try {
175
+ const raw = this.storage.getItem(this.storageKey);
176
+ if (!raw)
177
+ return [];
178
+ const parsed = JSON.parse(raw);
179
+ return Array.isArray(parsed) ? parsed : [];
180
+ }
181
+ catch (_a) {
182
+ return []; // JSON corrompido -> trata como cache vazio
183
+ }
184
+ }
185
+ writeAll(all) {
186
+ if (!this.storage)
187
+ return;
188
+ try {
189
+ this.storage.setItem(this.storageKey, JSON.stringify(all));
190
+ }
191
+ catch (err) {
192
+ // Quota estourada: remove os mais antigos e tenta de novo.
193
+ if (isQuotaError(err) && all.length > 0) {
194
+ const reduzido = [...all].sort((a, b) => a.lastAccess - b.lastAccess);
195
+ reduzido.splice(0, Math.ceil(reduzido.length / 2)); // descarta metade
196
+ try {
197
+ this.storage.setItem(this.storageKey, JSON.stringify(reduzido));
198
+ }
199
+ catch (_a) {
200
+ /* ainda assim falhou -> desiste de persistir; dados já retornam */
201
+ }
202
+ }
203
+ // Falha silenciosa: os dados ainda são devolvidos ao chamador.
204
+ }
205
+ }
206
+ }
207
+ /** Lê um caminho aninhado ("a.b.c") de um objeto de forma segura. */
208
+ function getPath(obj, path) {
209
+ return path.split(".").reduce((acc, k) => {
210
+ if (acc && typeof acc === "object" && k in acc) {
211
+ return acc[k];
212
+ }
213
+ return undefined;
214
+ }, obj);
215
+ }
216
+ function isQuotaError(err) {
217
+ return (err instanceof Error &&
218
+ (err.name === "QuotaExceededError" ||
219
+ err.name === "NS_ERROR_DOM_QUOTA_REACHED"));
220
+ }
221
+ /** Retorna localStorage se existir e funcionar; senão null (SSR, modo privado...). */
222
+ function getDefaultStorage() {
223
+ try {
224
+ if (typeof localStorage !== "undefined") {
225
+ const probe = "__reqcache_probe__";
226
+ localStorage.setItem(probe, "1");
227
+ localStorage.removeItem(probe);
228
+ return localStorage;
229
+ }
230
+ }
231
+ catch (_a) {
232
+ /* Safari em modo privado antigo lança aqui */
233
+ }
234
+ return null;
235
+ }
236
+ /** Instância pronta para uso, caso não queira configurar nada. */
237
+ const requestCache = new RequestCache();
238
+
239
+ exports.RequestCache = RequestCache;
240
+ exports.requestCache = requestCache;
241
+
242
+ }));
243
+ //# sourceMappingURL=index.js.map
@@ -0,0 +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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"}
@@ -0,0 +1,103 @@
1
+ /**
2
+ * request-cache
3
+ * -------------
4
+ * Cache de requisições HTTP em localStorage, pensado para cenários de
5
+ * alto volume em curto período (ex.: apuração de eleições).
6
+ *
7
+ * Modelo de armazenamento:
8
+ * Tudo fica sob UMA chave no storage, contendo um ARRAY de objetos.
9
+ * Cada objeto é indexado pela `rota` (a URL requisitada):
10
+ * [{ rota, data, createdAt, expiresAt, lastAccess }, ...]
11
+ *
12
+ * Comportamento:
13
+ * 1. Primeira chamada -> faz o fetch e grava a entrada da rota no array.
14
+ * 2. Durante o TTL -> devolve os dados do cache, sem tocar na rede.
15
+ * 3. Após expirar -> SÓ revalida quando o client chamar de novo (lazy).
16
+ * Se houver `monotonicKey`, só aceita o novo dado quando o valor numérico
17
+ * dessa chave for MAIOR que o guardado; senão mantém o antigo.
18
+ *
19
+ * Proteção de espaço (limite de ~5 MB do localStorage):
20
+ * - `maxEntries`: ao gravar, se passar do limite, remove a rota menos
21
+ * recentemente usada (LRU). Roda sozinho, sem timer.
22
+ * - Em erro de quota, remove as entradas mais antigas e tenta de novo.
23
+ * - `cleanup()`: remove rotas expiradas há mais de `graceSeconds`.
24
+ *
25
+ * Extras para o caso de eleição:
26
+ * - Deduplicação: chamadas simultâneas à mesma rota disparam 1 só fetch.
27
+ * - staleOnError: se a rede/API falhar, devolve o último dado válido.
28
+ */
29
+ export interface StorageLike {
30
+ getItem(key: string): string | null;
31
+ setItem(key: string, value: string): void;
32
+ removeItem(key: string): void;
33
+ }
34
+ export interface GetFetchOptions {
35
+ /**
36
+ * Caminho da chave numérica monotônica. Suporta aninhamento com ponto.
37
+ * Ex.: "total", "resultado.votosApurados", "data.candidato.votos".
38
+ * Se o valor novo NÃO for maior que o antigo, mantém o dado em cache.
39
+ */
40
+ monotonicKey?: string;
41
+ /** Opções nativas repassadas ao fetch (headers, method, body, signal...). */
42
+ fetchOptions?: RequestInit;
43
+ /**
44
+ * Se true (padrão), quando a nova requisição falhar e existir dado antigo,
45
+ * devolve esse dado antigo em vez de lançar erro.
46
+ */
47
+ staleOnError?: boolean;
48
+ /** fetch customizado — útil para testes ou ambientes sem fetch global. */
49
+ fetcher?: typeof fetch;
50
+ }
51
+ /** Uma entrada do cache. `rota` é a chave identificadora. */
52
+ export interface CacheEntry<T = unknown> {
53
+ rota: string;
54
+ data: T;
55
+ createdAt: number;
56
+ expiresAt: number;
57
+ lastAccess: number;
58
+ }
59
+ export interface RequestCacheConfig {
60
+ /** Storage a usar. Padrão: localStorage (se disponível). */
61
+ storage?: StorageLike;
62
+ /** Chave única onde o array é guardado. Padrão "reqcache". */
63
+ storageKey?: string;
64
+ /** Máximo de rotas em cache. Excedeu -> remove a menos usada (LRU). Padrão 100. */
65
+ maxEntries?: number;
66
+ }
67
+ export declare class RequestCache {
68
+ private storage;
69
+ private storageKey;
70
+ private maxEntries;
71
+ private inflight;
72
+ /** Relógio de acesso monotônico: sempre cresce, mesmo com acessos no mesmo ms. */
73
+ private tick;
74
+ constructor(config?: RequestCacheConfig);
75
+ /**
76
+ * Busca uma URL usando cache.
77
+ * @param url Rota da requisição (também é a chave do cache).
78
+ * @param ttlMs Tempo de cache em milissegundos (ex.: 10000).
79
+ * @param options Regra monotônica, opções de fetch, etc.
80
+ */
81
+ getFetch<T = unknown>(url: string, ttlMs: number, options?: GetFetchOptions): Promise<T>;
82
+ /** Remove uma rota específica do cache. */
83
+ invalidate(url: string): void;
84
+ /**
85
+ * Remove rotas expiradas há mais de `graceSeconds` (padrão 3600 = 1h).
86
+ * Chame na inicialização do app, ou de tempos em tempos.
87
+ * @returns quantidade de rotas removidas.
88
+ */
89
+ cleanup(graceSeconds?: number): number;
90
+ /** Esvazia todo o cache. */
91
+ clear(): void;
92
+ /** Retorna um número de acesso estritamente crescente (para o LRU). */
93
+ private nextAccess;
94
+ private revalidate;
95
+ /** Insere ou atualiza uma rota, aplicando o limite LRU. */
96
+ private upsert;
97
+ /** Enquanto passar de maxEntries, remove a rota menos recentemente usada. */
98
+ private evictIfNeeded;
99
+ private readAll;
100
+ private writeAll;
101
+ }
102
+ /** Instância pronta para uso, caso não queira configurar nada. */
103
+ export declare const requestCache: RequestCache;
package/package.json ADDED
@@ -0,0 +1,41 @@
1
+ {
2
+ "name": "@sevn/reqcache",
3
+ "version": "1.0.0",
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
+ "main": "dist/index.js",
6
+ "module": "dist/index.esm.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
11
+ "scripts": {
12
+ "build": "rollup -c",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "prepublishOnly": "npm run typecheck && npm run test && npm run build"
17
+ },
18
+ "keywords": [
19
+ "cache",
20
+ "fetch",
21
+ "localstorage",
22
+ "http",
23
+ "ttl",
24
+ "eleicoes",
25
+ "sevn"
26
+ ],
27
+ "author": "Sevn Technologies",
28
+ "license": "MIT",
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "devDependencies": {
33
+ "@rollup/plugin-typescript": "^11.1.6",
34
+ "@types/node": "^20.14.0",
35
+ "rollup": "^4.18.0",
36
+ "rollup-plugin-dts": "^6.1.1",
37
+ "tslib": "^2.6.3",
38
+ "typescript": "^5.4.0",
39
+ "vitest": "^1.6.0"
40
+ }
41
+ }