@sabschyks/arca 0.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 Sabrinna Guimarães
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,195 @@
1
+ <div align="center">
2
+
3
+ ![Arca Banner](https://placehold.co/1200x300/1a1a1a/ffffff?text=ARCA&font=montserrat)
4
+
5
+ **High-Concurrency Cache Coalescing & State Management for Node.js**
6
+
7
+ Prevent cache stampedes, eliminate duplicated fetches, and keep your APIs fast under extreme load.
8
+
9
+ [![CI](https://github.com/sabschyks/Arca/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/sabschyks/Arca/actions/workflows/ci.yml)
10
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
11
+
12
+ </div>
13
+
14
+ ---
15
+
16
+ ## 🚨 The Problem: Cache Stampede
17
+
18
+ In high-scale systems, when a popular cache key expires, **hundreds or thousands of concurrent requests** may hit your database at the same time before the cache is repopulated.
19
+
20
+ This phenomenon is known as the **Cache Stampede** or **Thundering Herd** problem.
21
+
22
+ Most traditional caching libraries (e.g. simple Redis wrappers):
23
+
24
+ - Only store values
25
+ - Do not coordinate concurrent requests
26
+ - Do not protect your database under high contention
27
+
28
+ ---
29
+
30
+ ## 🛡️ The Solution: Arca
31
+
32
+ **Arca** is more than a cache client — it’s a **concurrency shield** for Node.js applications.
33
+
34
+ ### Core Features
35
+
36
+ 1. **Request Coalescing (Singleflight)**
37
+ If 1,000 requests ask for the same key simultaneously, Arca executes the fetcher **only once**.
38
+ All requests await the same Promise.
39
+
40
+ 2. **Stale-While-Revalidate (SWR)**
41
+ Serve stale data instantly (near-zero latency) while refreshing the cache in the background.
42
+
43
+ 3. **Adapter-Agnostic Storage**
44
+ Works out of the box with:
45
+ - In-Memory storage (default)
46
+ - Redis (for distributed systems)
47
+
48
+ ---
49
+
50
+ ## 📦 Installation
51
+
52
+ ```bash
53
+ # Using pnpm (recommended)
54
+ pnpm add @sabschyks/arca
55
+
56
+ # Using npm
57
+ npm install @sabschyks/arca
58
+
59
+ # Using yarn
60
+ yarn add @sabschyks/arca
61
+ ````
62
+
63
+ ---
64
+
65
+ ## ⚡ Quick Start
66
+
67
+ ```ts
68
+ import { Arca } from '@sabschyks/arca';
69
+
70
+ // 1. Initialize Arca (defaults to in-memory storage)
71
+ const arca = new Arca({ defaultTtl: 60_000 }); // 1 minute
72
+
73
+ async function getUserProfile(userId: string) {
74
+ // 2. Wrap your expensive operation
75
+ return arca.get(`user:${userId}`, async () => {
76
+ console.log('Fetching from database...');
77
+ return db.query('SELECT * FROM users WHERE id = ?', [userId]);
78
+ });
79
+ }
80
+
81
+ // 3. Simulate concurrent traffic
82
+ Promise.all([
83
+ getUserProfile('123'),
84
+ getUserProfile('123'),
85
+ getUserProfile('123'),
86
+ ]);
87
+
88
+ // ✅ Result:
89
+ // The database is hit only once.
90
+ // All requests resolve with the same data.
91
+ ```
92
+
93
+ ---
94
+
95
+ ## 🔄 Stale-While-Revalidate (SWR) Explained
96
+
97
+ Arca implements **SWR** to keep your application fast even when cached data expires.
98
+
99
+ **Example timeline:**
100
+
101
+ 1. **Time 0s**
102
+ Data is cached with a TTL of 60s.
103
+
104
+ 2. **Time 61s**
105
+ A request arrives. The cache entry is expired.
106
+
107
+ 3. **Arca behavior**
108
+
109
+ * Immediately returns the stale value (latency ≈ 0ms)
110
+ * Triggers a background refresh (singleflight-protected)
111
+
112
+ 4. **Next request**
113
+
114
+ * Receives the fresh data
115
+
116
+ This guarantees:
117
+
118
+ * Low latency
119
+ * No traffic spikes
120
+ * No duplicated fetches
121
+
122
+ ---
123
+
124
+ ## 🌐 Redis Adapter (Production Ready)
125
+
126
+ For distributed environments such as **Kubernetes**, **Serverless**, or **multi-instance APIs**, Arca supports Redis.
127
+
128
+ ### Install Redis client
129
+
130
+ ```bash
131
+ pnpm add ioredis
132
+ ```
133
+
134
+ ### Configure Arca with Redis
135
+
136
+ ```ts
137
+ import { Arca, RedisAdapter } from 'arca';
138
+
139
+ const arca = new Arca({
140
+ storage: new RedisAdapter('redis://localhost:6379'),
141
+ defaultTtl: 1000 * 60 * 5, // 5 minutes
142
+ });
143
+ ```
144
+
145
+ ---
146
+
147
+ ## 📚 API Reference
148
+
149
+ ### `new Arca(options)`
150
+
151
+ | Option | Type | Description |
152
+ | ------------ | ---------------- | -------------------------------------------- |
153
+ | `storage` | `StorageAdapter` | Cache backend (default: `MemoryAdapter`). |
154
+ | `defaultTtl` | `number` | Default TTL in milliseconds (default: 60000). |
155
+
156
+ ---
157
+
158
+ ### `arca.get<T>(key, fetcher, options?)`
159
+
160
+ Retrieve a value from cache or compute it safely under concurrency.
161
+
162
+ **Parameters:**
163
+
164
+ * `key: string`
165
+ Unique cache identifier.
166
+
167
+ * `fetcher: () => Promise<T>`
168
+ Function executed when the value is missing or stale.
169
+
170
+ * `options?:`
171
+
172
+ * `ttl?: number` – Override TTL for this key.
173
+ * `forceRefresh?: boolean` – Bypass cache and fetch fresh data.
174
+
175
+ ---
176
+
177
+ ### `arca.delete(key)`
178
+
179
+ Manually invalidate a cache entry.
180
+
181
+ ```ts
182
+ arca.delete('user:123');
183
+ ```
184
+
185
+ ---
186
+
187
+ ## 🧠 When Should You Use Arca?
188
+
189
+ Arca shines when:
190
+
191
+ * You have **high-traffic endpoints**.
192
+ * Requests often target the **same resources**.
193
+ * Cache expiration causes **database spikes**.
194
+ * You want **zero-config protection** against stampedes.
195
+
@@ -0,0 +1,80 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Redis } from 'ioredis';
3
+
4
+ interface CacheEntry<T> {
5
+ value: T;
6
+ createdAt: number;
7
+ ttl: number;
8
+ }
9
+ interface StorageAdapter {
10
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
11
+ set<T>(key: string, value: T, ttl: number): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ clear(): Promise<void>;
14
+ }
15
+ interface ArcaOptions {
16
+ storage?: StorageAdapter;
17
+ defaultTtl?: number;
18
+ }
19
+ interface FetchOptions {
20
+ forceRefresh?: boolean;
21
+ ttl?: number;
22
+ }
23
+ type ArcaEvents = {
24
+ hit: (key: string) => void;
25
+ miss: (key: string) => void;
26
+ stale: (key: string) => void;
27
+ coalesced: (key: string) => void;
28
+ error: (err: Error) => void;
29
+ };
30
+
31
+ declare class MemoryAdapter implements StorageAdapter {
32
+ private map;
33
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
34
+ set<T>(key: string, value: T, ttl: number): Promise<void>;
35
+ delete(key: string): Promise<void>;
36
+ clear(): Promise<void>;
37
+ }
38
+
39
+ declare class RedisAdapter implements StorageAdapter {
40
+ private client;
41
+ constructor(connectionStringOrClient: string | Redis);
42
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
43
+ set<T>(key: string, value: T, ttl: number): Promise<void>;
44
+ delete(key: string): Promise<void>;
45
+ clear(): Promise<void>;
46
+ /**
47
+ * Método útil para fechar conexão em testes ou shutdown gracioso
48
+ */
49
+ disconnect(): Promise<void>;
50
+ }
51
+
52
+ /**
53
+ * Arca - High Concurrency Cache
54
+ */
55
+
56
+ declare class Arca extends EventEmitter {
57
+ private storage;
58
+ private coalescer;
59
+ private defaultTtl;
60
+ constructor(options?: ArcaOptions);
61
+ /**
62
+ * Busca um dado.
63
+ * Estratégia: State-While-Revalidate
64
+ */
65
+ get<T>(key: string, fetcher: () => Promise<T>, options?: FetchOptions): Promise<T>;
66
+ /**
67
+ * Executa a busca através do Coalesces e salva no Storage.
68
+ */
69
+ private resolveFetch;
70
+ /**
71
+ * Wrapper para atualização em background que não trava a resposta principal.
72
+ */
73
+ private backgroundUpdate;
74
+ /**
75
+ * Limpa uma nova chave manualmente
76
+ */
77
+ delete(key: string): Promise<void>;
78
+ }
79
+
80
+ export { Arca, type ArcaEvents, type ArcaOptions, type CacheEntry, type FetchOptions, MemoryAdapter, RedisAdapter, type StorageAdapter };
@@ -0,0 +1,80 @@
1
+ import { EventEmitter } from 'node:events';
2
+ import { Redis } from 'ioredis';
3
+
4
+ interface CacheEntry<T> {
5
+ value: T;
6
+ createdAt: number;
7
+ ttl: number;
8
+ }
9
+ interface StorageAdapter {
10
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
11
+ set<T>(key: string, value: T, ttl: number): Promise<void>;
12
+ delete(key: string): Promise<void>;
13
+ clear(): Promise<void>;
14
+ }
15
+ interface ArcaOptions {
16
+ storage?: StorageAdapter;
17
+ defaultTtl?: number;
18
+ }
19
+ interface FetchOptions {
20
+ forceRefresh?: boolean;
21
+ ttl?: number;
22
+ }
23
+ type ArcaEvents = {
24
+ hit: (key: string) => void;
25
+ miss: (key: string) => void;
26
+ stale: (key: string) => void;
27
+ coalesced: (key: string) => void;
28
+ error: (err: Error) => void;
29
+ };
30
+
31
+ declare class MemoryAdapter implements StorageAdapter {
32
+ private map;
33
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
34
+ set<T>(key: string, value: T, ttl: number): Promise<void>;
35
+ delete(key: string): Promise<void>;
36
+ clear(): Promise<void>;
37
+ }
38
+
39
+ declare class RedisAdapter implements StorageAdapter {
40
+ private client;
41
+ constructor(connectionStringOrClient: string | Redis);
42
+ get<T>(key: string): Promise<CacheEntry<T> | null>;
43
+ set<T>(key: string, value: T, ttl: number): Promise<void>;
44
+ delete(key: string): Promise<void>;
45
+ clear(): Promise<void>;
46
+ /**
47
+ * Método útil para fechar conexão em testes ou shutdown gracioso
48
+ */
49
+ disconnect(): Promise<void>;
50
+ }
51
+
52
+ /**
53
+ * Arca - High Concurrency Cache
54
+ */
55
+
56
+ declare class Arca extends EventEmitter {
57
+ private storage;
58
+ private coalescer;
59
+ private defaultTtl;
60
+ constructor(options?: ArcaOptions);
61
+ /**
62
+ * Busca um dado.
63
+ * Estratégia: State-While-Revalidate
64
+ */
65
+ get<T>(key: string, fetcher: () => Promise<T>, options?: FetchOptions): Promise<T>;
66
+ /**
67
+ * Executa a busca através do Coalesces e salva no Storage.
68
+ */
69
+ private resolveFetch;
70
+ /**
71
+ * Wrapper para atualização em background que não trava a resposta principal.
72
+ */
73
+ private backgroundUpdate;
74
+ /**
75
+ * Limpa uma nova chave manualmente
76
+ */
77
+ delete(key: string): Promise<void>;
78
+ }
79
+
80
+ export { Arca, type ArcaEvents, type ArcaOptions, type CacheEntry, type FetchOptions, MemoryAdapter, RedisAdapter, type StorageAdapter };
package/dist/index.js ADDED
@@ -0,0 +1,211 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ Arca: () => Arca,
34
+ MemoryAdapter: () => MemoryAdapter,
35
+ RedisAdapter: () => RedisAdapter
36
+ });
37
+ module.exports = __toCommonJS(index_exports);
38
+ var import_node_events = require("events");
39
+
40
+ // src/adapters/memory.ts
41
+ var MemoryAdapter = class {
42
+ map = /* @__PURE__ */ new Map();
43
+ async get(key) {
44
+ const entry = this.map.get(key);
45
+ return entry || null;
46
+ }
47
+ async set(key, value, ttl) {
48
+ this.map.set(key, {
49
+ value,
50
+ createdAt: Date.now(),
51
+ ttl
52
+ });
53
+ }
54
+ async delete(key) {
55
+ this.map.delete(key);
56
+ }
57
+ async clear() {
58
+ this.map.clear();
59
+ }
60
+ };
61
+
62
+ // src/core/coalescer.ts
63
+ var Coalescer = class {
64
+ // Armazena as promessas EM VOO (in-flight).
65
+ // Chave -> Promise<Pending>
66
+ inflight = /* @__PURE__ */ new Map();
67
+ /**
68
+ * Executa uma função assíncrona garantindo que, para uma mesma chave,
69
+ * apenas uma execução real ocorra simultaneamente.
70
+ * * @param key Identificador único da operação (ex: 'GET:/api/users/1')
71
+ * @param fn A função que busca o dado real (ex: consulta ao DB)
72
+ */
73
+ async execute(key, fn) {
74
+ const existing = this.inflight.get(key);
75
+ if (existing) {
76
+ return existing;
77
+ }
78
+ const promise = fn().then((result) => {
79
+ return result;
80
+ }).catch((error) => {
81
+ throw error;
82
+ }).finally(() => {
83
+ this.inflight.delete(key);
84
+ });
85
+ this.inflight.set(key, promise);
86
+ return promise;
87
+ }
88
+ /**
89
+ * Retorna quantas requisições estão pendentes no momento.
90
+ * Útil para métricas e observabilidade.
91
+ */
92
+ getInflightCount() {
93
+ return this.inflight.size;
94
+ }
95
+ };
96
+
97
+ // src/adapters/redis.ts
98
+ var import_ioredis = __toESM(require("ioredis"));
99
+ var RedisAdapter = class {
100
+ client;
101
+ constructor(connectionStringOrClient) {
102
+ if (typeof connectionStringOrClient === "string") {
103
+ this.client = new import_ioredis.default(connectionStringOrClient);
104
+ } else {
105
+ this.client = connectionStringOrClient;
106
+ }
107
+ }
108
+ async get(key) {
109
+ const data = await this.client.get(key);
110
+ if (!data) return null;
111
+ try {
112
+ return JSON.parse(data);
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+ async set(key, value, ttl) {
118
+ const entry = {
119
+ value,
120
+ createdAt: Date.now(),
121
+ ttl
122
+ };
123
+ await this.client.set(key, JSON.stringify(entry), "PX", ttl);
124
+ }
125
+ async delete(key) {
126
+ await this.client.del(key);
127
+ }
128
+ async clear() {
129
+ await this.client.flushdb();
130
+ }
131
+ /**
132
+ * Método útil para fechar conexão em testes ou shutdown gracioso
133
+ */
134
+ async disconnect() {
135
+ await this.client.quit();
136
+ }
137
+ };
138
+
139
+ // src/index.ts
140
+ var Arca = class extends import_node_events.EventEmitter {
141
+ storage;
142
+ coalescer;
143
+ defaultTtl;
144
+ constructor(options = {}) {
145
+ super();
146
+ this.storage = options.storage || new MemoryAdapter();
147
+ this.defaultTtl = options.defaultTtl || 6e4;
148
+ this.coalescer = new Coalescer();
149
+ }
150
+ /**
151
+ * Busca um dado.
152
+ * Estratégia: State-While-Revalidate
153
+ */
154
+ async get(key, fetcher, options = {}) {
155
+ const ttl = options.ttl || this.defaultTtl;
156
+ if (!options.forceRefresh) {
157
+ try {
158
+ const cached = await this.storage.get(key);
159
+ if (cached) {
160
+ const isExpired = Date.now() - cached.createdAt > cached.ttl;
161
+ if (!isExpired) {
162
+ this.emit("hit", key);
163
+ return cached.value;
164
+ }
165
+ this.emit("stale", key);
166
+ this.backgroundUpdate(key, fetcher, ttl).catch((err) => {
167
+ this.emit("error", err);
168
+ });
169
+ return cached.value;
170
+ }
171
+ } catch (err) {
172
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
173
+ }
174
+ }
175
+ this.emit("miss", key);
176
+ return this.resolveFetch(key, fetcher, ttl);
177
+ }
178
+ /**
179
+ * Executa a busca através do Coalesces e salva no Storage.
180
+ */
181
+ async resolveFetch(key, fetcher, ttl) {
182
+ return this.coalescer.execute(key, async () => {
183
+ const value = await fetcher();
184
+ try {
185
+ await this.storage.set(key, value, ttl);
186
+ } catch (err) {
187
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
188
+ }
189
+ return value;
190
+ });
191
+ }
192
+ /**
193
+ * Wrapper para atualização em background que não trava a resposta principal.
194
+ */
195
+ async backgroundUpdate(key, fetcher, ttl) {
196
+ await this.resolveFetch(key, fetcher, ttl);
197
+ }
198
+ /**
199
+ * Limpa uma nova chave manualmente
200
+ */
201
+ async delete(key) {
202
+ await this.storage.delete(key);
203
+ }
204
+ };
205
+ // Annotate the CommonJS export names for ESM import in node:
206
+ 0 && (module.exports = {
207
+ Arca,
208
+ MemoryAdapter,
209
+ RedisAdapter
210
+ });
211
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/adapters/memory.ts","../src/core/coalescer.ts","../src/adapters/redis.ts"],"sourcesContent":["/**\n * Arca - High Concurrency Cache\n */\nimport { EventEmitter } from \"node:events\";\nimport { MemoryAdapter } from \"./adapters/memory\";\nimport { Coalescer } from \"./core/coalescer\";\nimport type { ArcaOptions, FetchOptions, StorageAdapter } from \"./types\";\n\nexport * from \"./adapters/memory\";\nexport * from \"./adapters/redis\";\nexport * from \"./types\";\n\nexport class Arca extends EventEmitter {\n private storage: StorageAdapter;\n private coalescer: Coalescer;\n private defaultTtl: number;\n\n constructor(options: ArcaOptions = {}) {\n super();\n this.storage = options.storage || new MemoryAdapter();\n this.defaultTtl = options.defaultTtl || 60000; // 1 minuto padrão\n this.coalescer = new Coalescer();\n }\n\n /**\n * Busca um dado.\n * Estratégia: State-While-Revalidate\n */\n public async get<T>(\n key: string,\n fetcher: () => Promise<T>,\n options: FetchOptions = {},\n ): Promise<T> {\n const ttl = options.ttl || this.defaultTtl;\n\n // 1. Tentar pegar do cache (se não forçado a ignorar)\n if (!options.forceRefresh) {\n try {\n const cached = await this.storage.get<T>(key);\n\n if (cached) {\n const isExpired = Date.now() - cached.createdAt > cached.ttl;\n\n if (!isExpired) {\n this.emit(\"hit\", key);\n return cached.value;\n }\n\n // STALE: O dado existe mas venceu.\n // Retornamos o dado velho IMEDIATAMENTE e atualizamos em background.\n // Usamos o coalescer para garantir que apenas UM background update ocorra.\n this.emit(\"stale\", key);\n this.backgroundUpdate(key, fetcher, ttl).catch((err) => {\n this.emit(\"error\", err);\n });\n\n return cached.value;\n }\n } catch (err) {\n // Se falar o storage (ex: Redis cai), locamos e prosseguimos para o fetcher\n this.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n // MISS: Não tem no cache ou forceRefresh=true\n // Precisamos buscar (e esperar) o dado novo.\n this.emit(\"miss\", key);\n return this.resolveFetch(key, fetcher, ttl);\n }\n\n /**\n * Executa a busca através do Coalesces e salva no Storage.\n */\n private async resolveFetch<T>(key: string, fetcher: () => Promise<T>, ttl: number): Promise<T> {\n // Verifica se já existe uma promessa em voo antes de executar\n\n return this.coalescer.execute(key, async () => {\n // Se o contador não mudou, significa que fomos \"coalesced\" (aproveitamos a carona)\n // Se mudou, nós somos a request original.\n // *Nota: Lógica simplificada. Para precisão exata de \"coalesced\"\n // precisaríamos modificar o Coalescer para retornar um status.\n // Por hora, vamos emitir 'coalesced' apenas se NÃO formos quem executa o fetch real?\n // Não, melhor: O Coalescer esconde isso.\n // Vamos emitir 'miss' apenas se realmente buscamos no banco.\n\n const value = await fetcher();\n try {\n await this.storage.set(key, value, ttl);\n } catch (err) {\n this.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n return value;\n });\n }\n\n /**\n * Wrapper para atualização em background que não trava a resposta principal.\n */\n private async backgroundUpdate<T>(\n key: string,\n fetcher: () => Promise<T>,\n ttl: number,\n ): Promise<void> {\n // Apenas chamamos o resolverFetch. O Coalescer cuida de não duplicar.\n await this.resolveFetch(key, fetcher, ttl);\n }\n\n /**\n * Limpa uma nova chave manualmente\n */\n public async delete(key: string): Promise<void> {\n await this.storage.delete(key);\n }\n}\n","import type { CacheEntry, StorageAdapter } from \"../types\";\n\nexport class MemoryAdapter implements StorageAdapter {\n private map = new Map<string, CacheEntry<unknown>>();\n\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = this.map.get(key);\n return (entry as CacheEntry<T>) || null;\n }\n\n async set<T>(key: string, value: T, ttl: number): Promise<void> {\n this.map.set(key, {\n value,\n createdAt: Date.now(),\n ttl,\n });\n }\n\n async delete(key: string): Promise<void> {\n this.map.delete(key);\n }\n\n async clear(): Promise<void> {\n this.map.clear();\n }\n}\n","/**\n * Implementação de Coalescência de Requisições.\n * Também conhecido como padrão \"SingleFlight\".\n * * Objetivo: Eliminar requisições pendentes idênticas duplicadas para evitar\n * problemas de \"Thundering Herd\" / \"Cache Stampede\".\n */\nexport class Coalescer {\n // Armazena as promessas EM VOO (in-flight).\n // Chave -> Promise<Pending>\n private inflight = new Map<string, Promise<unknown>>();\n\n /**\n * Executa uma função assíncrona garantindo que, para uma mesma chave,\n * apenas uma execução real ocorra simultaneamente.\n * * @param key Identificador único da operação (ex: 'GET:/api/users/1')\n * @param fn A função que busca o dado real (ex: consulta ao DB)\n */\n public async execute<T>(key: string, fn: () => Promise<T>): Promise<T> {\n // 1. Se já existe uma promessa pendente para essa chave, retorne-a.\n // Isso é o \"Coalescing\" acontecendo.\n const existing = this.inflight.get(key);\n if (existing) {\n return existing as Promise<T>;\n }\n\n // 2. Se não existe, criamos a promessa.\n const promise = fn()\n .then((result) => {\n // Sucesso: Retorna o valor.\n return result;\n })\n .catch((error) => {\n // Erro: Propaga o erro.\n throw error;\n })\n .finally(() => {\n // 3. Limpeza.\n // Independente de sucesso ou falha, removemos a promessa do mapa.\n // Se não fizermos isso, futuras chamadas receberiam uma promessa já resolvida (stale)\n // ou nunca mais executariam a função novamente (memory leak/deadlock lógico).\n this.inflight.delete(key);\n });\n\n this.inflight.set(key, promise);\n\n return promise as Promise<T>;\n }\n\n /**\n * Retorna quantas requisições estão pendentes no momento.\n * Útil para métricas e observabilidade.\n */\n public getInflightCount(): number {\n return this.inflight.size;\n }\n}\n","import Redis, { type Redis as RedisClient } from \"ioredis\";\nimport type { CacheEntry, StorageAdapter } from \"../types\";\n\nexport class RedisAdapter implements StorageAdapter {\n private client: RedisClient;\n\n constructor(connectionStringOrClient: string | RedisClient) {\n if (typeof connectionStringOrClient === \"string\") {\n this.client = new Redis(connectionStringOrClient);\n } else {\n this.client = connectionStringOrClient;\n }\n }\n\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n const data = await this.client.get(key);\n\n if (!data) return null;\n\n try {\n // O Redis retorna string, precisamos recompor o objeto CacheEntry\n return JSON.parse(data) as CacheEntry<T>;\n } catch {\n // Se o JSON estiver corrompido, tratamos como miss\n return null;\n }\n }\n\n async set<T>(key: string, value: T, ttl: number): Promise<void> {\n const entry: CacheEntry<T> = {\n value,\n createdAt: Date.now(),\n ttl,\n };\n\n // 'PX' define o TTL em milissegundos nativamente no Redis\n await this.client.set(key, JSON.stringify(entry), \"PX\", ttl);\n }\n\n async delete(key: string): Promise<void> {\n await this.client.del(key);\n }\n\n async clear(): Promise<void> {\n await this.client.flushdb();\n }\n\n /**\n * Método útil para fechar conexão em testes ou shutdown gracioso\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n}\n"],"mappings":"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","names":["Redis"]}
package/dist/index.mjs ADDED
@@ -0,0 +1,174 @@
1
+ // src/index.ts
2
+ import { EventEmitter } from "events";
3
+
4
+ // src/adapters/memory.ts
5
+ var MemoryAdapter = class {
6
+ map = /* @__PURE__ */ new Map();
7
+ async get(key) {
8
+ const entry = this.map.get(key);
9
+ return entry || null;
10
+ }
11
+ async set(key, value, ttl) {
12
+ this.map.set(key, {
13
+ value,
14
+ createdAt: Date.now(),
15
+ ttl
16
+ });
17
+ }
18
+ async delete(key) {
19
+ this.map.delete(key);
20
+ }
21
+ async clear() {
22
+ this.map.clear();
23
+ }
24
+ };
25
+
26
+ // src/core/coalescer.ts
27
+ var Coalescer = class {
28
+ // Armazena as promessas EM VOO (in-flight).
29
+ // Chave -> Promise<Pending>
30
+ inflight = /* @__PURE__ */ new Map();
31
+ /**
32
+ * Executa uma função assíncrona garantindo que, para uma mesma chave,
33
+ * apenas uma execução real ocorra simultaneamente.
34
+ * * @param key Identificador único da operação (ex: 'GET:/api/users/1')
35
+ * @param fn A função que busca o dado real (ex: consulta ao DB)
36
+ */
37
+ async execute(key, fn) {
38
+ const existing = this.inflight.get(key);
39
+ if (existing) {
40
+ return existing;
41
+ }
42
+ const promise = fn().then((result) => {
43
+ return result;
44
+ }).catch((error) => {
45
+ throw error;
46
+ }).finally(() => {
47
+ this.inflight.delete(key);
48
+ });
49
+ this.inflight.set(key, promise);
50
+ return promise;
51
+ }
52
+ /**
53
+ * Retorna quantas requisições estão pendentes no momento.
54
+ * Útil para métricas e observabilidade.
55
+ */
56
+ getInflightCount() {
57
+ return this.inflight.size;
58
+ }
59
+ };
60
+
61
+ // src/adapters/redis.ts
62
+ import Redis from "ioredis";
63
+ var RedisAdapter = class {
64
+ client;
65
+ constructor(connectionStringOrClient) {
66
+ if (typeof connectionStringOrClient === "string") {
67
+ this.client = new Redis(connectionStringOrClient);
68
+ } else {
69
+ this.client = connectionStringOrClient;
70
+ }
71
+ }
72
+ async get(key) {
73
+ const data = await this.client.get(key);
74
+ if (!data) return null;
75
+ try {
76
+ return JSON.parse(data);
77
+ } catch {
78
+ return null;
79
+ }
80
+ }
81
+ async set(key, value, ttl) {
82
+ const entry = {
83
+ value,
84
+ createdAt: Date.now(),
85
+ ttl
86
+ };
87
+ await this.client.set(key, JSON.stringify(entry), "PX", ttl);
88
+ }
89
+ async delete(key) {
90
+ await this.client.del(key);
91
+ }
92
+ async clear() {
93
+ await this.client.flushdb();
94
+ }
95
+ /**
96
+ * Método útil para fechar conexão em testes ou shutdown gracioso
97
+ */
98
+ async disconnect() {
99
+ await this.client.quit();
100
+ }
101
+ };
102
+
103
+ // src/index.ts
104
+ var Arca = class extends EventEmitter {
105
+ storage;
106
+ coalescer;
107
+ defaultTtl;
108
+ constructor(options = {}) {
109
+ super();
110
+ this.storage = options.storage || new MemoryAdapter();
111
+ this.defaultTtl = options.defaultTtl || 6e4;
112
+ this.coalescer = new Coalescer();
113
+ }
114
+ /**
115
+ * Busca um dado.
116
+ * Estratégia: State-While-Revalidate
117
+ */
118
+ async get(key, fetcher, options = {}) {
119
+ const ttl = options.ttl || this.defaultTtl;
120
+ if (!options.forceRefresh) {
121
+ try {
122
+ const cached = await this.storage.get(key);
123
+ if (cached) {
124
+ const isExpired = Date.now() - cached.createdAt > cached.ttl;
125
+ if (!isExpired) {
126
+ this.emit("hit", key);
127
+ return cached.value;
128
+ }
129
+ this.emit("stale", key);
130
+ this.backgroundUpdate(key, fetcher, ttl).catch((err) => {
131
+ this.emit("error", err);
132
+ });
133
+ return cached.value;
134
+ }
135
+ } catch (err) {
136
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
137
+ }
138
+ }
139
+ this.emit("miss", key);
140
+ return this.resolveFetch(key, fetcher, ttl);
141
+ }
142
+ /**
143
+ * Executa a busca através do Coalesces e salva no Storage.
144
+ */
145
+ async resolveFetch(key, fetcher, ttl) {
146
+ return this.coalescer.execute(key, async () => {
147
+ const value = await fetcher();
148
+ try {
149
+ await this.storage.set(key, value, ttl);
150
+ } catch (err) {
151
+ this.emit("error", err instanceof Error ? err : new Error(String(err)));
152
+ }
153
+ return value;
154
+ });
155
+ }
156
+ /**
157
+ * Wrapper para atualização em background que não trava a resposta principal.
158
+ */
159
+ async backgroundUpdate(key, fetcher, ttl) {
160
+ await this.resolveFetch(key, fetcher, ttl);
161
+ }
162
+ /**
163
+ * Limpa uma nova chave manualmente
164
+ */
165
+ async delete(key) {
166
+ await this.storage.delete(key);
167
+ }
168
+ };
169
+ export {
170
+ Arca,
171
+ MemoryAdapter,
172
+ RedisAdapter
173
+ };
174
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/adapters/memory.ts","../src/core/coalescer.ts","../src/adapters/redis.ts"],"sourcesContent":["/**\n * Arca - High Concurrency Cache\n */\nimport { EventEmitter } from \"node:events\";\nimport { MemoryAdapter } from \"./adapters/memory\";\nimport { Coalescer } from \"./core/coalescer\";\nimport type { ArcaOptions, FetchOptions, StorageAdapter } from \"./types\";\n\nexport * from \"./adapters/memory\";\nexport * from \"./adapters/redis\";\nexport * from \"./types\";\n\nexport class Arca extends EventEmitter {\n private storage: StorageAdapter;\n private coalescer: Coalescer;\n private defaultTtl: number;\n\n constructor(options: ArcaOptions = {}) {\n super();\n this.storage = options.storage || new MemoryAdapter();\n this.defaultTtl = options.defaultTtl || 60000; // 1 minuto padrão\n this.coalescer = new Coalescer();\n }\n\n /**\n * Busca um dado.\n * Estratégia: State-While-Revalidate\n */\n public async get<T>(\n key: string,\n fetcher: () => Promise<T>,\n options: FetchOptions = {},\n ): Promise<T> {\n const ttl = options.ttl || this.defaultTtl;\n\n // 1. Tentar pegar do cache (se não forçado a ignorar)\n if (!options.forceRefresh) {\n try {\n const cached = await this.storage.get<T>(key);\n\n if (cached) {\n const isExpired = Date.now() - cached.createdAt > cached.ttl;\n\n if (!isExpired) {\n this.emit(\"hit\", key);\n return cached.value;\n }\n\n // STALE: O dado existe mas venceu.\n // Retornamos o dado velho IMEDIATAMENTE e atualizamos em background.\n // Usamos o coalescer para garantir que apenas UM background update ocorra.\n this.emit(\"stale\", key);\n this.backgroundUpdate(key, fetcher, ttl).catch((err) => {\n this.emit(\"error\", err);\n });\n\n return cached.value;\n }\n } catch (err) {\n // Se falar o storage (ex: Redis cai), locamos e prosseguimos para o fetcher\n this.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n }\n\n // MISS: Não tem no cache ou forceRefresh=true\n // Precisamos buscar (e esperar) o dado novo.\n this.emit(\"miss\", key);\n return this.resolveFetch(key, fetcher, ttl);\n }\n\n /**\n * Executa a busca através do Coalesces e salva no Storage.\n */\n private async resolveFetch<T>(key: string, fetcher: () => Promise<T>, ttl: number): Promise<T> {\n // Verifica se já existe uma promessa em voo antes de executar\n\n return this.coalescer.execute(key, async () => {\n // Se o contador não mudou, significa que fomos \"coalesced\" (aproveitamos a carona)\n // Se mudou, nós somos a request original.\n // *Nota: Lógica simplificada. Para precisão exata de \"coalesced\"\n // precisaríamos modificar o Coalescer para retornar um status.\n // Por hora, vamos emitir 'coalesced' apenas se NÃO formos quem executa o fetch real?\n // Não, melhor: O Coalescer esconde isso.\n // Vamos emitir 'miss' apenas se realmente buscamos no banco.\n\n const value = await fetcher();\n try {\n await this.storage.set(key, value, ttl);\n } catch (err) {\n this.emit(\"error\", err instanceof Error ? err : new Error(String(err)));\n }\n return value;\n });\n }\n\n /**\n * Wrapper para atualização em background que não trava a resposta principal.\n */\n private async backgroundUpdate<T>(\n key: string,\n fetcher: () => Promise<T>,\n ttl: number,\n ): Promise<void> {\n // Apenas chamamos o resolverFetch. O Coalescer cuida de não duplicar.\n await this.resolveFetch(key, fetcher, ttl);\n }\n\n /**\n * Limpa uma nova chave manualmente\n */\n public async delete(key: string): Promise<void> {\n await this.storage.delete(key);\n }\n}\n","import type { CacheEntry, StorageAdapter } from \"../types\";\n\nexport class MemoryAdapter implements StorageAdapter {\n private map = new Map<string, CacheEntry<unknown>>();\n\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n const entry = this.map.get(key);\n return (entry as CacheEntry<T>) || null;\n }\n\n async set<T>(key: string, value: T, ttl: number): Promise<void> {\n this.map.set(key, {\n value,\n createdAt: Date.now(),\n ttl,\n });\n }\n\n async delete(key: string): Promise<void> {\n this.map.delete(key);\n }\n\n async clear(): Promise<void> {\n this.map.clear();\n }\n}\n","/**\n * Implementação de Coalescência de Requisições.\n * Também conhecido como padrão \"SingleFlight\".\n * * Objetivo: Eliminar requisições pendentes idênticas duplicadas para evitar\n * problemas de \"Thundering Herd\" / \"Cache Stampede\".\n */\nexport class Coalescer {\n // Armazena as promessas EM VOO (in-flight).\n // Chave -> Promise<Pending>\n private inflight = new Map<string, Promise<unknown>>();\n\n /**\n * Executa uma função assíncrona garantindo que, para uma mesma chave,\n * apenas uma execução real ocorra simultaneamente.\n * * @param key Identificador único da operação (ex: 'GET:/api/users/1')\n * @param fn A função que busca o dado real (ex: consulta ao DB)\n */\n public async execute<T>(key: string, fn: () => Promise<T>): Promise<T> {\n // 1. Se já existe uma promessa pendente para essa chave, retorne-a.\n // Isso é o \"Coalescing\" acontecendo.\n const existing = this.inflight.get(key);\n if (existing) {\n return existing as Promise<T>;\n }\n\n // 2. Se não existe, criamos a promessa.\n const promise = fn()\n .then((result) => {\n // Sucesso: Retorna o valor.\n return result;\n })\n .catch((error) => {\n // Erro: Propaga o erro.\n throw error;\n })\n .finally(() => {\n // 3. Limpeza.\n // Independente de sucesso ou falha, removemos a promessa do mapa.\n // Se não fizermos isso, futuras chamadas receberiam uma promessa já resolvida (stale)\n // ou nunca mais executariam a função novamente (memory leak/deadlock lógico).\n this.inflight.delete(key);\n });\n\n this.inflight.set(key, promise);\n\n return promise as Promise<T>;\n }\n\n /**\n * Retorna quantas requisições estão pendentes no momento.\n * Útil para métricas e observabilidade.\n */\n public getInflightCount(): number {\n return this.inflight.size;\n }\n}\n","import Redis, { type Redis as RedisClient } from \"ioredis\";\nimport type { CacheEntry, StorageAdapter } from \"../types\";\n\nexport class RedisAdapter implements StorageAdapter {\n private client: RedisClient;\n\n constructor(connectionStringOrClient: string | RedisClient) {\n if (typeof connectionStringOrClient === \"string\") {\n this.client = new Redis(connectionStringOrClient);\n } else {\n this.client = connectionStringOrClient;\n }\n }\n\n async get<T>(key: string): Promise<CacheEntry<T> | null> {\n const data = await this.client.get(key);\n\n if (!data) return null;\n\n try {\n // O Redis retorna string, precisamos recompor o objeto CacheEntry\n return JSON.parse(data) as CacheEntry<T>;\n } catch {\n // Se o JSON estiver corrompido, tratamos como miss\n return null;\n }\n }\n\n async set<T>(key: string, value: T, ttl: number): Promise<void> {\n const entry: CacheEntry<T> = {\n value,\n createdAt: Date.now(),\n ttl,\n };\n\n // 'PX' define o TTL em milissegundos nativamente no Redis\n await this.client.set(key, JSON.stringify(entry), \"PX\", ttl);\n }\n\n async delete(key: string): Promise<void> {\n await this.client.del(key);\n }\n\n async clear(): Promise<void> {\n await this.client.flushdb();\n }\n\n /**\n * Método útil para fechar conexão em testes ou shutdown gracioso\n */\n async disconnect(): Promise<void> {\n await this.client.quit();\n }\n}\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,60 @@
1
+ {
2
+ "name": "@sabschyks/arca",
3
+ "version": "0.0.0",
4
+ "description": "High-concurrency cache coalescing and state management library.",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "require": "./dist/index.js",
11
+ "import": "./dist/index.mjs"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "README.md",
17
+ "LICENSE"
18
+ ],
19
+ "keywords": [
20
+ "cache",
21
+ "concurrency",
22
+ "promise",
23
+ "deduplication",
24
+ "swr",
25
+ "typescript"
26
+ ],
27
+ "author": "Sabrinna Guimarães (sabschyks)",
28
+ "license": "MIT",
29
+ "engines": {
30
+ "node": ">=18"
31
+ },
32
+ "devDependencies": {
33
+ "@biomejs/biome": "^2.3.14",
34
+ "@changesets/cli": "^2.29.8",
35
+ "@types/ioredis-mock": "^8.2.6",
36
+ "@types/node": "^25.2.1",
37
+ "@vitest/coverage-v8": "^4.0.18",
38
+ "benny": "^3.7.1",
39
+ "husky": "^9.1.7",
40
+ "ioredis-mock": "^8.13.1",
41
+ "tsup": "^8.5.1",
42
+ "typescript": "^5.9.3",
43
+ "vitest": "^4.0.18"
44
+ },
45
+ "dependencies": {
46
+ "ioredis": "^5.9.2"
47
+ },
48
+ "scripts": {
49
+ "build": "tsup",
50
+ "dev": "tsup --watch",
51
+ "test": "vitest run",
52
+ "test:watch": "vitest",
53
+ "test:coverage": "vitest run --coverage",
54
+ "lint": "biome check .",
55
+ "lint:fix": "biome check --write .",
56
+ "typecheck": "tsc --noEmit",
57
+ "release": "pnpm build && changeset publish",
58
+ "bench": "npx tsx benchmarks/run.ts"
59
+ }
60
+ }