@sabschyks/arca 0.0.1
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 +21 -0
- package/README.md +195 -0
- package/dist/index.d.mts +68 -0
- package/dist/index.d.ts +68 -0
- package/dist/index.js +198 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +159 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +58 -0
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
|
+

|
|
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
|
+
[](https://github.com/sabschyks/Arca/actions/workflows/ci.yml)
|
|
10
|
+
[](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 arca
|
|
55
|
+
|
|
56
|
+
# Using npm
|
|
57
|
+
npm install arca
|
|
58
|
+
|
|
59
|
+
# Using yarn
|
|
60
|
+
yarn add arca
|
|
61
|
+
````
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## ⚡ Quick Start
|
|
66
|
+
|
|
67
|
+
```ts
|
|
68
|
+
import { Arca } from '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
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
|
|
3
|
+
interface CacheEntry<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
ttl: number;
|
|
7
|
+
}
|
|
8
|
+
interface StorageAdapter {
|
|
9
|
+
get<T>(key: string): Promise<CacheEntry<T> | null>;
|
|
10
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
11
|
+
delete(key: string): Promise<void>;
|
|
12
|
+
clear(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
interface ArcaOptions {
|
|
15
|
+
storage?: StorageAdapter;
|
|
16
|
+
defaultTtl?: number;
|
|
17
|
+
}
|
|
18
|
+
interface FetchOptions {
|
|
19
|
+
forceRefresh?: boolean;
|
|
20
|
+
ttl?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare class MemoryAdapter implements StorageAdapter {
|
|
24
|
+
private map;
|
|
25
|
+
get<T>(key: string): Promise<CacheEntry<T> | null>;
|
|
26
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
27
|
+
delete(key: string): Promise<void>;
|
|
28
|
+
clear(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare class RedisAdapter implements StorageAdapter {
|
|
32
|
+
private client;
|
|
33
|
+
constructor(connectionStringOrClient: string | Redis);
|
|
34
|
+
get<T>(key: string): Promise<CacheEntry<T> | null>;
|
|
35
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
36
|
+
delete(key: string): Promise<void>;
|
|
37
|
+
clear(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Método útil para fechar conexão em testes ou shutdown gracioso
|
|
40
|
+
*/
|
|
41
|
+
disconnect(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare class Arca {
|
|
45
|
+
private storage;
|
|
46
|
+
private coalescer;
|
|
47
|
+
private defaultTtl;
|
|
48
|
+
constructor(options?: ArcaOptions);
|
|
49
|
+
/**
|
|
50
|
+
* Busca um dado.
|
|
51
|
+
* Estratégia: State-While-Revalidate
|
|
52
|
+
*/
|
|
53
|
+
get<T>(key: string, fetcher: () => Promise<T>, options?: FetchOptions): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* Executa a busca através do Coalesces e salva no Storage.
|
|
56
|
+
*/
|
|
57
|
+
private resolveFetch;
|
|
58
|
+
/**
|
|
59
|
+
* Wrapper para atualização em background que não trava a resposta principal.
|
|
60
|
+
*/
|
|
61
|
+
private backgroundUpdate;
|
|
62
|
+
/**
|
|
63
|
+
* Limpa uma nova chave manualmente
|
|
64
|
+
*/
|
|
65
|
+
delete(key: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { Arca, type ArcaOptions, type CacheEntry, type FetchOptions, MemoryAdapter, RedisAdapter, type StorageAdapter };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
|
|
3
|
+
interface CacheEntry<T> {
|
|
4
|
+
value: T;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
ttl: number;
|
|
7
|
+
}
|
|
8
|
+
interface StorageAdapter {
|
|
9
|
+
get<T>(key: string): Promise<CacheEntry<T> | null>;
|
|
10
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
11
|
+
delete(key: string): Promise<void>;
|
|
12
|
+
clear(): Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
interface ArcaOptions {
|
|
15
|
+
storage?: StorageAdapter;
|
|
16
|
+
defaultTtl?: number;
|
|
17
|
+
}
|
|
18
|
+
interface FetchOptions {
|
|
19
|
+
forceRefresh?: boolean;
|
|
20
|
+
ttl?: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
declare class MemoryAdapter implements StorageAdapter {
|
|
24
|
+
private map;
|
|
25
|
+
get<T>(key: string): Promise<CacheEntry<T> | null>;
|
|
26
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
27
|
+
delete(key: string): Promise<void>;
|
|
28
|
+
clear(): Promise<void>;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
declare class RedisAdapter implements StorageAdapter {
|
|
32
|
+
private client;
|
|
33
|
+
constructor(connectionStringOrClient: string | Redis);
|
|
34
|
+
get<T>(key: string): Promise<CacheEntry<T> | null>;
|
|
35
|
+
set<T>(key: string, value: T, ttl: number): Promise<void>;
|
|
36
|
+
delete(key: string): Promise<void>;
|
|
37
|
+
clear(): Promise<void>;
|
|
38
|
+
/**
|
|
39
|
+
* Método útil para fechar conexão em testes ou shutdown gracioso
|
|
40
|
+
*/
|
|
41
|
+
disconnect(): Promise<void>;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
declare class Arca {
|
|
45
|
+
private storage;
|
|
46
|
+
private coalescer;
|
|
47
|
+
private defaultTtl;
|
|
48
|
+
constructor(options?: ArcaOptions);
|
|
49
|
+
/**
|
|
50
|
+
* Busca um dado.
|
|
51
|
+
* Estratégia: State-While-Revalidate
|
|
52
|
+
*/
|
|
53
|
+
get<T>(key: string, fetcher: () => Promise<T>, options?: FetchOptions): Promise<T>;
|
|
54
|
+
/**
|
|
55
|
+
* Executa a busca através do Coalesces e salva no Storage.
|
|
56
|
+
*/
|
|
57
|
+
private resolveFetch;
|
|
58
|
+
/**
|
|
59
|
+
* Wrapper para atualização em background que não trava a resposta principal.
|
|
60
|
+
*/
|
|
61
|
+
private backgroundUpdate;
|
|
62
|
+
/**
|
|
63
|
+
* Limpa uma nova chave manualmente
|
|
64
|
+
*/
|
|
65
|
+
delete(key: string): Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { Arca, type ArcaOptions, type CacheEntry, type FetchOptions, MemoryAdapter, RedisAdapter, type StorageAdapter };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,198 @@
|
|
|
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
|
+
|
|
39
|
+
// src/adapters/memory.ts
|
|
40
|
+
var MemoryAdapter = class {
|
|
41
|
+
map = /* @__PURE__ */ new Map();
|
|
42
|
+
async get(key) {
|
|
43
|
+
const entry = this.map.get(key);
|
|
44
|
+
return entry || null;
|
|
45
|
+
}
|
|
46
|
+
async set(key, value, ttl) {
|
|
47
|
+
this.map.set(key, {
|
|
48
|
+
value,
|
|
49
|
+
createdAt: Date.now(),
|
|
50
|
+
ttl
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
async delete(key) {
|
|
54
|
+
this.map.delete(key);
|
|
55
|
+
}
|
|
56
|
+
async clear() {
|
|
57
|
+
this.map.clear();
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// src/core/coalescer.ts
|
|
62
|
+
var Coalescer = class {
|
|
63
|
+
// Armazena as promessas EM VOO (in-flight).
|
|
64
|
+
// Chave -> Promise<Pending>
|
|
65
|
+
inflight = /* @__PURE__ */ new Map();
|
|
66
|
+
/**
|
|
67
|
+
* Executa uma função assíncrona garantindo que, para uma mesma chave,
|
|
68
|
+
* apenas uma execução real ocorra simultaneamente.
|
|
69
|
+
* * @param key Identificador único da operação (ex: 'GET:/api/users/1')
|
|
70
|
+
* @param fn A função que busca o dado real (ex: consulta ao DB)
|
|
71
|
+
*/
|
|
72
|
+
async execute(key, fn) {
|
|
73
|
+
const existing = this.inflight.get(key);
|
|
74
|
+
if (existing) {
|
|
75
|
+
return existing;
|
|
76
|
+
}
|
|
77
|
+
const promise = fn().then((result) => {
|
|
78
|
+
return result;
|
|
79
|
+
}).catch((error) => {
|
|
80
|
+
throw error;
|
|
81
|
+
}).finally(() => {
|
|
82
|
+
this.inflight.delete(key);
|
|
83
|
+
});
|
|
84
|
+
this.inflight.set(key, promise);
|
|
85
|
+
return promise;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Retorna quantas requisições estão pendentes no momento.
|
|
89
|
+
* Útil para métricas e observabilidade.
|
|
90
|
+
*/
|
|
91
|
+
getInflightCount() {
|
|
92
|
+
return this.inflight.size;
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// src/adapters/redis.ts
|
|
97
|
+
var import_ioredis = __toESM(require("ioredis"));
|
|
98
|
+
var RedisAdapter = class {
|
|
99
|
+
client;
|
|
100
|
+
constructor(connectionStringOrClient) {
|
|
101
|
+
if (typeof connectionStringOrClient === "string") {
|
|
102
|
+
this.client = new import_ioredis.default(connectionStringOrClient);
|
|
103
|
+
} else {
|
|
104
|
+
this.client = connectionStringOrClient;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
async get(key) {
|
|
108
|
+
const data = await this.client.get(key);
|
|
109
|
+
if (!data) return null;
|
|
110
|
+
try {
|
|
111
|
+
return JSON.parse(data);
|
|
112
|
+
} catch {
|
|
113
|
+
return null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async set(key, value, ttl) {
|
|
117
|
+
const entry = {
|
|
118
|
+
value,
|
|
119
|
+
createdAt: Date.now(),
|
|
120
|
+
ttl
|
|
121
|
+
};
|
|
122
|
+
await this.client.set(key, JSON.stringify(entry), "PX", ttl);
|
|
123
|
+
}
|
|
124
|
+
async delete(key) {
|
|
125
|
+
await this.client.del(key);
|
|
126
|
+
}
|
|
127
|
+
async clear() {
|
|
128
|
+
await this.client.flushdb();
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Método útil para fechar conexão em testes ou shutdown gracioso
|
|
132
|
+
*/
|
|
133
|
+
async disconnect() {
|
|
134
|
+
await this.client.quit();
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
// src/index.ts
|
|
139
|
+
var Arca = class {
|
|
140
|
+
storage;
|
|
141
|
+
coalescer;
|
|
142
|
+
defaultTtl;
|
|
143
|
+
constructor(options = {}) {
|
|
144
|
+
this.storage = options.storage || new MemoryAdapter();
|
|
145
|
+
this.defaultTtl = options.defaultTtl || 6e4;
|
|
146
|
+
this.coalescer = new Coalescer();
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Busca um dado.
|
|
150
|
+
* Estratégia: State-While-Revalidate
|
|
151
|
+
*/
|
|
152
|
+
async get(key, fetcher, options = {}) {
|
|
153
|
+
const ttl = options.ttl || this.defaultTtl;
|
|
154
|
+
if (!options.forceRefresh) {
|
|
155
|
+
const cached = await this.storage.get(key);
|
|
156
|
+
if (cached) {
|
|
157
|
+
const isExpired = Date.now() - cached.createdAt > cached.ttl;
|
|
158
|
+
if (!isExpired) {
|
|
159
|
+
return cached.value;
|
|
160
|
+
}
|
|
161
|
+
this.backgroundUpdate(key, fetcher, ttl).catch((err) => {
|
|
162
|
+
console.error(`[Arca] Background update failed for key: ${key}`, err);
|
|
163
|
+
});
|
|
164
|
+
return cached.value;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return this.resolveFetch(key, fetcher, ttl);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Executa a busca através do Coalesces e salva no Storage.
|
|
171
|
+
*/
|
|
172
|
+
async resolveFetch(key, fetcher, ttl) {
|
|
173
|
+
return this.coalescer.execute(key, async () => {
|
|
174
|
+
const value = await fetcher();
|
|
175
|
+
await this.storage.set(key, value, ttl);
|
|
176
|
+
return value;
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Wrapper para atualização em background que não trava a resposta principal.
|
|
181
|
+
*/
|
|
182
|
+
async backgroundUpdate(key, fetcher, ttl) {
|
|
183
|
+
await this.resolveFetch(key, fetcher, ttl);
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Limpa uma nova chave manualmente
|
|
187
|
+
*/
|
|
188
|
+
async delete(key) {
|
|
189
|
+
await this.storage.delete(key);
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
193
|
+
0 && (module.exports = {
|
|
194
|
+
Arca,
|
|
195
|
+
MemoryAdapter,
|
|
196
|
+
RedisAdapter
|
|
197
|
+
});
|
|
198
|
+
//# 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 { 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 {\n private storage: StorageAdapter;\n private coalescer: Coalescer;\n private defaultTtl: number;\n\n constructor(options: ArcaOptions = {}) {\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 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 // HIT: Retorna dado fresco\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.backgroundUpdate(key, fetcher, ttl).catch((err) => {\n console.error(`[Arca] Background update failed for key: ${key}`, err);\n });\n\n return cached.value;\n }\n }\n\n // MISS: Não tem no cache ou forceRefresh=true\n // Precisamos buscar (e esperar) o dado novo.\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 return this.coalescer.execute(key, async () => {\n const value = await fetcher();\n await this.storage.set(key, value, ttl);\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,159 @@
|
|
|
1
|
+
// src/adapters/memory.ts
|
|
2
|
+
var MemoryAdapter = class {
|
|
3
|
+
map = /* @__PURE__ */ new Map();
|
|
4
|
+
async get(key) {
|
|
5
|
+
const entry = this.map.get(key);
|
|
6
|
+
return entry || null;
|
|
7
|
+
}
|
|
8
|
+
async set(key, value, ttl) {
|
|
9
|
+
this.map.set(key, {
|
|
10
|
+
value,
|
|
11
|
+
createdAt: Date.now(),
|
|
12
|
+
ttl
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async delete(key) {
|
|
16
|
+
this.map.delete(key);
|
|
17
|
+
}
|
|
18
|
+
async clear() {
|
|
19
|
+
this.map.clear();
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/core/coalescer.ts
|
|
24
|
+
var Coalescer = class {
|
|
25
|
+
// Armazena as promessas EM VOO (in-flight).
|
|
26
|
+
// Chave -> Promise<Pending>
|
|
27
|
+
inflight = /* @__PURE__ */ new Map();
|
|
28
|
+
/**
|
|
29
|
+
* Executa uma função assíncrona garantindo que, para uma mesma chave,
|
|
30
|
+
* apenas uma execução real ocorra simultaneamente.
|
|
31
|
+
* * @param key Identificador único da operação (ex: 'GET:/api/users/1')
|
|
32
|
+
* @param fn A função que busca o dado real (ex: consulta ao DB)
|
|
33
|
+
*/
|
|
34
|
+
async execute(key, fn) {
|
|
35
|
+
const existing = this.inflight.get(key);
|
|
36
|
+
if (existing) {
|
|
37
|
+
return existing;
|
|
38
|
+
}
|
|
39
|
+
const promise = fn().then((result) => {
|
|
40
|
+
return result;
|
|
41
|
+
}).catch((error) => {
|
|
42
|
+
throw error;
|
|
43
|
+
}).finally(() => {
|
|
44
|
+
this.inflight.delete(key);
|
|
45
|
+
});
|
|
46
|
+
this.inflight.set(key, promise);
|
|
47
|
+
return promise;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Retorna quantas requisições estão pendentes no momento.
|
|
51
|
+
* Útil para métricas e observabilidade.
|
|
52
|
+
*/
|
|
53
|
+
getInflightCount() {
|
|
54
|
+
return this.inflight.size;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// src/adapters/redis.ts
|
|
59
|
+
import Redis from "ioredis";
|
|
60
|
+
var RedisAdapter = class {
|
|
61
|
+
client;
|
|
62
|
+
constructor(connectionStringOrClient) {
|
|
63
|
+
if (typeof connectionStringOrClient === "string") {
|
|
64
|
+
this.client = new Redis(connectionStringOrClient);
|
|
65
|
+
} else {
|
|
66
|
+
this.client = connectionStringOrClient;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
async get(key) {
|
|
70
|
+
const data = await this.client.get(key);
|
|
71
|
+
if (!data) return null;
|
|
72
|
+
try {
|
|
73
|
+
return JSON.parse(data);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
async set(key, value, ttl) {
|
|
79
|
+
const entry = {
|
|
80
|
+
value,
|
|
81
|
+
createdAt: Date.now(),
|
|
82
|
+
ttl
|
|
83
|
+
};
|
|
84
|
+
await this.client.set(key, JSON.stringify(entry), "PX", ttl);
|
|
85
|
+
}
|
|
86
|
+
async delete(key) {
|
|
87
|
+
await this.client.del(key);
|
|
88
|
+
}
|
|
89
|
+
async clear() {
|
|
90
|
+
await this.client.flushdb();
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Método útil para fechar conexão em testes ou shutdown gracioso
|
|
94
|
+
*/
|
|
95
|
+
async disconnect() {
|
|
96
|
+
await this.client.quit();
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/index.ts
|
|
101
|
+
var Arca = class {
|
|
102
|
+
storage;
|
|
103
|
+
coalescer;
|
|
104
|
+
defaultTtl;
|
|
105
|
+
constructor(options = {}) {
|
|
106
|
+
this.storage = options.storage || new MemoryAdapter();
|
|
107
|
+
this.defaultTtl = options.defaultTtl || 6e4;
|
|
108
|
+
this.coalescer = new Coalescer();
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Busca um dado.
|
|
112
|
+
* Estratégia: State-While-Revalidate
|
|
113
|
+
*/
|
|
114
|
+
async get(key, fetcher, options = {}) {
|
|
115
|
+
const ttl = options.ttl || this.defaultTtl;
|
|
116
|
+
if (!options.forceRefresh) {
|
|
117
|
+
const cached = await this.storage.get(key);
|
|
118
|
+
if (cached) {
|
|
119
|
+
const isExpired = Date.now() - cached.createdAt > cached.ttl;
|
|
120
|
+
if (!isExpired) {
|
|
121
|
+
return cached.value;
|
|
122
|
+
}
|
|
123
|
+
this.backgroundUpdate(key, fetcher, ttl).catch((err) => {
|
|
124
|
+
console.error(`[Arca] Background update failed for key: ${key}`, err);
|
|
125
|
+
});
|
|
126
|
+
return cached.value;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return this.resolveFetch(key, fetcher, ttl);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Executa a busca através do Coalesces e salva no Storage.
|
|
133
|
+
*/
|
|
134
|
+
async resolveFetch(key, fetcher, ttl) {
|
|
135
|
+
return this.coalescer.execute(key, async () => {
|
|
136
|
+
const value = await fetcher();
|
|
137
|
+
await this.storage.set(key, value, ttl);
|
|
138
|
+
return value;
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Wrapper para atualização em background que não trava a resposta principal.
|
|
143
|
+
*/
|
|
144
|
+
async backgroundUpdate(key, fetcher, ttl) {
|
|
145
|
+
await this.resolveFetch(key, fetcher, ttl);
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Limpa uma nova chave manualmente
|
|
149
|
+
*/
|
|
150
|
+
async delete(key) {
|
|
151
|
+
await this.storage.delete(key);
|
|
152
|
+
}
|
|
153
|
+
};
|
|
154
|
+
export {
|
|
155
|
+
Arca,
|
|
156
|
+
MemoryAdapter,
|
|
157
|
+
RedisAdapter
|
|
158
|
+
};
|
|
159
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/adapters/memory.ts","../src/core/coalescer.ts","../src/adapters/redis.ts","../src/index.ts"],"sourcesContent":["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","/**\n * Arca - High Concurrency Cache\n */\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 {\n private storage: StorageAdapter;\n private coalescer: Coalescer;\n private defaultTtl: number;\n\n constructor(options: ArcaOptions = {}) {\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 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 // HIT: Retorna dado fresco\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.backgroundUpdate(key, fetcher, ttl).catch((err) => {\n console.error(`[Arca] Background update failed for key: ${key}`, err);\n });\n\n return cached.value;\n }\n }\n\n // MISS: Não tem no cache ou forceRefresh=true\n // Precisamos buscar (e esperar) o dado novo.\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 return this.coalescer.execute(key, async () => {\n const value = await fetcher();\n await this.storage.set(key, value, ttl);\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"],"mappings":"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","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@sabschyks/arca",
|
|
3
|
+
"version": "0.0.1",
|
|
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
|
+
"husky": "^9.1.7",
|
|
39
|
+
"ioredis-mock": "^8.13.1",
|
|
40
|
+
"tsup": "^8.5.1",
|
|
41
|
+
"typescript": "^5.9.3",
|
|
42
|
+
"vitest": "^4.0.18"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"ioredis": "^5.9.2"
|
|
46
|
+
},
|
|
47
|
+
"scripts": {
|
|
48
|
+
"build": "tsup",
|
|
49
|
+
"dev": "tsup --watch",
|
|
50
|
+
"test": "vitest run",
|
|
51
|
+
"test:watch": "vitest",
|
|
52
|
+
"test:coverage": "vitest run --coverage",
|
|
53
|
+
"lint": "biome check .",
|
|
54
|
+
"lint:fix": "biome check --write .",
|
|
55
|
+
"typecheck": "tsc --noEmit",
|
|
56
|
+
"release": "pnpm build && changeset publish"
|
|
57
|
+
}
|
|
58
|
+
}
|