@wfp99/async-lru-cache 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +95 -0
- package/README.zh-TW.md +95 -0
- package/dist/index.d.ts +58 -2
- package/dist/index.js +126 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -12,9 +12,51 @@ An asynchronous LRU (Least Recently Used) memory cache that supports asynchronou
|
|
|
12
12
|
- 🔄 **Automatic Merging**: Automatically merges concurrent GET requests for the same key
|
|
13
13
|
- 📝 **Serialized Writes**: Serializes PUT operations for the same key to ensure execution order
|
|
14
14
|
- 🗑️ **LRU Eviction**: Implements LRU algorithm to automatically remove least recently used items
|
|
15
|
+
- ⏰ **TTL Support**: Automatic expiration of cache entries with configurable Time To Live
|
|
15
16
|
- 🛡️ **Error Handling**: Comprehensive error handling with automatic cache cleanup on failures
|
|
16
17
|
- 🧹 **Complete Clearing**: Support for clearing all cache entries at once
|
|
17
18
|
|
|
19
|
+
## Why AsyncLRUCache?
|
|
20
|
+
|
|
21
|
+
### The Problem
|
|
22
|
+
Traditional caching solutions often struggle with asynchronous operations:
|
|
23
|
+
- **Race Conditions**: Multiple concurrent requests for the same resource can trigger duplicate expensive operations
|
|
24
|
+
- **Memory Leaks**: Failed operations may leave stale entries in cache without proper cleanup
|
|
25
|
+
- **Complex State Management**: Coordinating cache updates with external persistence layers becomes error-prone
|
|
26
|
+
- **External Dependencies**: Many caching solutions require external services (Redis, Memcached) adding infrastructure complexity, network latency, and deployment overhead
|
|
27
|
+
|
|
28
|
+
### The Solution
|
|
29
|
+
AsyncLRUCache addresses these challenges by providing:
|
|
30
|
+
- **Smart Request Merging**: Multiple concurrent GET calls for the same key automatically share a single loader execution
|
|
31
|
+
- **Automatic Error Recovery**: Failed operations are automatically cleaned up, preventing memory leaks and stale data
|
|
32
|
+
- **Operation Serialization**: PUT operations for the same key are queued and executed sequentially, ensuring data consistency
|
|
33
|
+
- **Zero External Dependencies**: Pure in-memory solution eliminates the need for external caching services, reducing infrastructure complexity, setup overhead, and network latency while increasing reliability and performance
|
|
34
|
+
|
|
35
|
+
### When to Use
|
|
36
|
+
AsyncLRUCache is perfect for scenarios involving:
|
|
37
|
+
- **API Response Caching**: Cache expensive HTTP requests with automatic deduplication
|
|
38
|
+
- **Database Query Results**: Reduce database load while maintaining data consistency
|
|
39
|
+
- **Computed Values**: Cache expensive calculations with built-in invalidation
|
|
40
|
+
- **File System Operations**: Cache file reads/writes with persistence hooks
|
|
41
|
+
- **Microservices Communication**: Reduce inter-service calls with intelligent caching
|
|
42
|
+
|
|
43
|
+
### Why Choose In-Memory Over External Cache Services?
|
|
44
|
+
|
|
45
|
+
**Simplicity & Performance**
|
|
46
|
+
- **No Setup Required**: Install and use immediately without configuring external services
|
|
47
|
+
- **Zero Network Latency**: Direct memory access provides microsecond response times
|
|
48
|
+
- **Reduced Infrastructure**: Eliminate cache servers, reducing operational complexity and costs
|
|
49
|
+
|
|
50
|
+
**Reliability & Predictability**
|
|
51
|
+
- **No Network Dependencies**: Cache operations never fail due to network issues
|
|
52
|
+
- **Consistent Performance**: No variable network latency affecting cache performance
|
|
53
|
+
- **Simplified Deployment**: Deploy as part of your application without managing separate cache infrastructure
|
|
54
|
+
|
|
55
|
+
**Development Efficiency**
|
|
56
|
+
- **Instant Development**: Start coding immediately without setting up Redis, Memcached, etc.
|
|
57
|
+
- **Easy Testing**: Unit tests run without external service dependencies
|
|
58
|
+
- **Simplified Debugging**: Cache behavior is predictable and traceable within your application process
|
|
59
|
+
|
|
18
60
|
## Installation
|
|
19
61
|
|
|
20
62
|
```bash
|
|
@@ -64,6 +106,59 @@ try {
|
|
|
64
106
|
}
|
|
65
107
|
```
|
|
66
108
|
|
|
109
|
+
### TTL (Time To Live) Support
|
|
110
|
+
|
|
111
|
+
AsyncLRUCache supports automatic expiration of cache entries using TTL (Time To Live):
|
|
112
|
+
|
|
113
|
+
```typescript
|
|
114
|
+
import { AsyncLRUCache } from '@wfp99/async-lru-cache';
|
|
115
|
+
|
|
116
|
+
// Create cache with TTL support
|
|
117
|
+
const cache = new AsyncLRUCache({
|
|
118
|
+
capacity: 100,
|
|
119
|
+
defaultTtlMs: 5000, // Default TTL of 5 seconds
|
|
120
|
+
cleanupIntervalMs: 10000 // Cleanup expired entries every 10 seconds
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// Use default TTL
|
|
124
|
+
const data1 = await cache.get('key1', async () => {
|
|
125
|
+
return await fetchData();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// Override TTL for specific entries
|
|
129
|
+
const data2 = await cache.get('key2', async () => {
|
|
130
|
+
return await fetchCriticalData();
|
|
131
|
+
}, 30000); // 30 seconds TTL
|
|
132
|
+
|
|
133
|
+
// Put with custom TTL
|
|
134
|
+
await cache.put('key3', value, async (key, val) => {
|
|
135
|
+
await saveToDb(key, val);
|
|
136
|
+
}, 60000); // 1 minute TTL
|
|
137
|
+
|
|
138
|
+
// Manual cleanup of expired entries
|
|
139
|
+
cache.cleanupExpired();
|
|
140
|
+
|
|
141
|
+
// Check if key exists and is not expired
|
|
142
|
+
if (cache.has('key1')) {
|
|
143
|
+
console.log('Key exists and is valid');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Get cache size
|
|
147
|
+
console.log('Current cache size:', cache.size());
|
|
148
|
+
|
|
149
|
+
// Destroy cache (stops cleanup timer and clears all data)
|
|
150
|
+
cache.destroy();
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
#### TTL Configuration Options
|
|
154
|
+
|
|
155
|
+
- **`defaultTtlMs`**: Default TTL for all cache entries (optional)
|
|
156
|
+
- **`cleanupIntervalMs`**: Automatic cleanup interval for expired entries (optional)
|
|
157
|
+
- Individual `get()` and `put()` methods accept TTL overrides
|
|
158
|
+
- Expired entries are automatically removed during normal operations
|
|
159
|
+
- Call `cleanupExpired()` for manual cleanup
|
|
160
|
+
- Call `destroy()` to stop timers and prevent memory leaks
|
|
161
|
+
|
|
67
162
|
### Advanced Usage
|
|
68
163
|
|
|
69
164
|
```typescript
|
package/README.zh-TW.md
CHANGED
|
@@ -8,9 +8,51 @@
|
|
|
8
8
|
- 🔄 **自動合併**:自動合併相同 key 的並發 GET 請求
|
|
9
9
|
- 📝 **序列化寫入**:序列化同一 key 的 PUT 操作,確保順序執行
|
|
10
10
|
- 🗑️ **LRU 淘汰策略**:實現 LRU 演算法,自動移除最久未使用的項目
|
|
11
|
+
- ⏰ **TTL 支援**:支援可配置的生存時間(Time To Live),自動過期快取項目
|
|
11
12
|
- 🛡️ **錯誤處理**:完善的錯誤處理機制,載入或保存失敗時自動清理快取
|
|
12
13
|
- 🧹 **完整清除**:支援一次性清除所有快取項目
|
|
13
14
|
|
|
15
|
+
## 為什麼選擇 AsyncLRUCache?
|
|
16
|
+
|
|
17
|
+
### 問題所在
|
|
18
|
+
傳統的快取解決方案在處理非同步操作時經常遇到困難:
|
|
19
|
+
- **競態條件**:對同一資源的多個並發請求可能觸發重複的昂貴操作
|
|
20
|
+
- **記憶體洩漏**:失敗的操作可能在快取中留下過期項目而沒有適當的清理
|
|
21
|
+
- **複雜的狀態管理**:協調快取更新與外部持久化層變得容易出錯
|
|
22
|
+
- **外部依賴**:許多快取解決方案需要外部服務(Redis、Memcached),增加基礎架構複雜性、網路延遲和部署成本
|
|
23
|
+
|
|
24
|
+
### 解決方案
|
|
25
|
+
AsyncLRUCache 透過以下方式解決這些挑戰:
|
|
26
|
+
- **智慧請求合併**:對同一 key 的多個並發 GET 呼叫自動共享單一載入器執行
|
|
27
|
+
- **自動錯誤恢復**:失敗的操作會自動清理,防止記憶體洩漏和過期資料
|
|
28
|
+
- **操作序列化**:同一 key 的 PUT 操作會排隊並依序執行,確保資料一致性
|
|
29
|
+
- **零外部依賴**:純記憶體內解決方案,無需外部快取服務,減少基礎架構複雜性、設定成本和網路延遲,同時提高可靠性和效能
|
|
30
|
+
|
|
31
|
+
### 適用場景
|
|
32
|
+
AsyncLRUCache 非常適合以下情境:
|
|
33
|
+
- **API 回應快取**:快取昂貴的 HTTP 請求並自動去重複
|
|
34
|
+
- **資料庫查詢結果**:減少資料庫負載同時維持資料一致性
|
|
35
|
+
- **計算值**:快取昂貴的計算結果並內建失效機制
|
|
36
|
+
- **檔案系統操作**:快取檔案讀寫並提供持久化串接
|
|
37
|
+
- **微服務通訊**:透過快取減少服務間呼叫
|
|
38
|
+
|
|
39
|
+
### 為什麼選擇記憶體內快取而非外部快取服務?
|
|
40
|
+
|
|
41
|
+
**簡單性與效能**
|
|
42
|
+
- **無需設定**:安裝後立即使用,無需配置外部服務
|
|
43
|
+
- **零網路延遲**:直接記憶體存取提供微秒級回應時間
|
|
44
|
+
- **簡化基礎架構**:省去快取伺服器,減少營運複雜性和成本
|
|
45
|
+
|
|
46
|
+
**可靠性與可預測性**
|
|
47
|
+
- **無網路依賴**:快取操作不會因網路問題而失敗
|
|
48
|
+
- **一致性效能**:沒有可變的網路延遲影響快取效能
|
|
49
|
+
- **簡化部署**:作為應用程式的一部分部署,無需管理獨立的快取基礎架構
|
|
50
|
+
|
|
51
|
+
**開發效率**
|
|
52
|
+
- **即時開發**:無需設定 Redis、Memcached 等即可開始編碼
|
|
53
|
+
- **輕鬆測試**:單元測試無需外部服務依賴即可執行
|
|
54
|
+
- **簡化除錯**:快取行為在應用程式流程內可預測且可追蹤
|
|
55
|
+
|
|
14
56
|
## 安裝
|
|
15
57
|
|
|
16
58
|
```bash
|
|
@@ -77,6 +119,59 @@ await cache.put(`user:${userId}`, updatedUser, async (key, value) => {
|
|
|
77
119
|
});
|
|
78
120
|
```
|
|
79
121
|
|
|
122
|
+
### TTL(生存時間)支援
|
|
123
|
+
|
|
124
|
+
AsyncLRUCache 支援使用 TTL(Time To Live)自動過期快取項目:
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
import { AsyncLRUCache } from '@wfp99/async-lru-cache';
|
|
128
|
+
|
|
129
|
+
// 建立支援 TTL 的快取
|
|
130
|
+
const cache = new AsyncLRUCache({
|
|
131
|
+
capacity: 100,
|
|
132
|
+
defaultTtlMs: 5000, // 預設 TTL 為 5 秒
|
|
133
|
+
cleanupIntervalMs: 10000 // 每 10 秒清理過期項目
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
// 使用預設 TTL
|
|
137
|
+
const data1 = await cache.get('key1', async () => {
|
|
138
|
+
return await fetchData();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
// 為特定項目覆寫 TTL
|
|
142
|
+
const data2 = await cache.get('key2', async () => {
|
|
143
|
+
return await fetchCriticalData();
|
|
144
|
+
}, 30000); // 30 秒 TTL
|
|
145
|
+
|
|
146
|
+
// 使用自訂 TTL 進行 Put 操作
|
|
147
|
+
await cache.put('key3', value, async (key, val) => {
|
|
148
|
+
await saveToDb(key, val);
|
|
149
|
+
}, 60000); // 1 分鐘 TTL
|
|
150
|
+
|
|
151
|
+
// 手動清理過期項目
|
|
152
|
+
cache.cleanupExpired();
|
|
153
|
+
|
|
154
|
+
// 檢查 key 是否存在且未過期
|
|
155
|
+
if (cache.has('key1')) {
|
|
156
|
+
console.log('Key 存在且有效');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// 取得快取大小
|
|
160
|
+
console.log('目前快取大小:', cache.size());
|
|
161
|
+
|
|
162
|
+
// 銷毀快取(停止清理計時器並清除所有資料)
|
|
163
|
+
cache.destroy();
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
#### TTL 配置選項
|
|
167
|
+
|
|
168
|
+
- **`defaultTtlMs`**:所有快取項目的預設 TTL(可選)
|
|
169
|
+
- **`cleanupIntervalMs`**:過期項目的自動清理間隔(可選)
|
|
170
|
+
- 個別的 `get()` 和 `put()` 方法接受 TTL 覆寫
|
|
171
|
+
- 過期項目在正常操作期間會自動移除
|
|
172
|
+
- 呼叫 `cleanupExpired()` 進行手動清理
|
|
173
|
+
- 呼叫 `destroy()` 停止計時器並防止記憶體洩漏
|
|
174
|
+
|
|
80
175
|
### 快取管理
|
|
81
176
|
|
|
82
177
|
```typescript
|
package/dist/index.d.ts
CHANGED
|
@@ -14,6 +14,18 @@ export interface AsyncLRUCacheOption {
|
|
|
14
14
|
* Must not be less than 10.
|
|
15
15
|
*/
|
|
16
16
|
capacity: number;
|
|
17
|
+
/**
|
|
18
|
+
* Default time to live for cache entries in milliseconds.
|
|
19
|
+
* If not specified, entries will not expire automatically.
|
|
20
|
+
* Individual entries can override this value.
|
|
21
|
+
*/
|
|
22
|
+
defaultTtlMs?: number;
|
|
23
|
+
/**
|
|
24
|
+
* Interval in milliseconds for automatic cleanup of expired entries.
|
|
25
|
+
* If not specified, cleanup will only happen during normal operations.
|
|
26
|
+
* Setting this enables periodic background cleanup.
|
|
27
|
+
*/
|
|
28
|
+
cleanupIntervalMs?: number;
|
|
17
29
|
}
|
|
18
30
|
/**
|
|
19
31
|
* Asynchronous LRU Cache class.
|
|
@@ -21,12 +33,17 @@ export interface AsyncLRUCacheOption {
|
|
|
21
33
|
* - Supports asynchronous savers for data persistence.
|
|
22
34
|
* - Automatically merges concurrent requests (get) and serializes writes (put).
|
|
23
35
|
* - Implements LRU eviction strategy.
|
|
36
|
+
* - Supports TTL (Time To Live) for automatic expiration of entries.
|
|
24
37
|
*/
|
|
25
38
|
export declare class AsyncLRUCache<K, V> {
|
|
26
39
|
/**
|
|
27
40
|
* Maximum capacity of the cache.
|
|
28
41
|
*/
|
|
29
42
|
private readonly capacity;
|
|
43
|
+
/**
|
|
44
|
+
* Default TTL for cache entries in milliseconds.
|
|
45
|
+
*/
|
|
46
|
+
private readonly defaultTtlMs?;
|
|
30
47
|
/**
|
|
31
48
|
* Map for storing cache entries. The key is the cache key, and the value is the Node.
|
|
32
49
|
*/
|
|
@@ -39,12 +56,28 @@ export declare class AsyncLRUCache<K, V> {
|
|
|
39
56
|
* Tail node of the linked list, pointing to the least recently used item.
|
|
40
57
|
*/
|
|
41
58
|
private tail;
|
|
59
|
+
/**
|
|
60
|
+
* Timer ID for periodic cleanup of expired entries.
|
|
61
|
+
*/
|
|
62
|
+
private cleanupTimer?;
|
|
42
63
|
/**
|
|
43
64
|
* Creates an instance of AsyncLRUCache with the specified options.
|
|
44
65
|
* @param option - The configuration options for the cache.
|
|
45
66
|
* @throws {Error} Throws an error if the provided capacity is less than 10.
|
|
46
67
|
*/
|
|
47
68
|
constructor(option: AsyncLRUCacheOption);
|
|
69
|
+
/**
|
|
70
|
+
* Cleans up expired entries from the cache.
|
|
71
|
+
* This method removes all nodes that have exceeded their TTL.
|
|
72
|
+
*/
|
|
73
|
+
private _cleanupExpired;
|
|
74
|
+
/**
|
|
75
|
+
* Checks if a node is expired and removes it if so.
|
|
76
|
+
* @param key - The key to check.
|
|
77
|
+
* @param node - The node to check.
|
|
78
|
+
* @returns True if the node was expired and removed, false otherwise.
|
|
79
|
+
*/
|
|
80
|
+
private _checkAndRemoveExpired;
|
|
48
81
|
/**
|
|
49
82
|
* Checks if eviction is needed according to the cache strategy.
|
|
50
83
|
* When the cache exceeds its capacity, automatically removes the tail (least recently used) node.
|
|
@@ -72,18 +105,20 @@ export declare class AsyncLRUCache<K, V> {
|
|
|
72
105
|
* Automatically merges concurrent requests for the same key.
|
|
73
106
|
* @param key - Cache key.
|
|
74
107
|
* @param loader - Asynchronous loader function to execute on cache miss.
|
|
108
|
+
* @param ttlMs - Optional TTL override for this entry in milliseconds.
|
|
75
109
|
* @returns A Promise that resolves to the required data.
|
|
76
110
|
*/
|
|
77
|
-
get(key: K, loader: () => Promise<V
|
|
111
|
+
get(key: K, loader: () => Promise<V>, ttlMs?: number): Promise<V>;
|
|
78
112
|
/**
|
|
79
113
|
* Puts a value into the cache, and optionally executes a saver function to persist it.
|
|
80
114
|
* This method serializes multiple put operations for the same key, ensuring they execute in order.
|
|
81
115
|
* @param key - Cache key.
|
|
82
116
|
* @param value - Value to cache.
|
|
83
117
|
* @param saver - Optional asynchronous save function.
|
|
118
|
+
* @param ttlMs - Optional TTL override for this entry in milliseconds.
|
|
84
119
|
* @returns A Promise that resolves to the latest value after the saver operation completes.
|
|
85
120
|
*/
|
|
86
|
-
put(key: K, value: V, saver?: (key: K, value: V) => Promise<void
|
|
121
|
+
put(key: K, value: V, saver?: (key: K, value: V) => Promise<void>, ttlMs?: number): Promise<V>;
|
|
87
122
|
/**
|
|
88
123
|
* Invalidates and removes the cache entry associated with the specified key.
|
|
89
124
|
* @param key - The key of the cache entry to invalidate.
|
|
@@ -93,4 +128,25 @@ export declare class AsyncLRUCache<K, V> {
|
|
|
93
128
|
* Clears the entire cache, removing all entries.
|
|
94
129
|
*/
|
|
95
130
|
clear(): void;
|
|
131
|
+
/**
|
|
132
|
+
* Destroys the cache instance, clearing all data and stopping any background timers.
|
|
133
|
+
* Call this method when you no longer need the cache to prevent memory leaks.
|
|
134
|
+
*/
|
|
135
|
+
destroy(): void;
|
|
136
|
+
/**
|
|
137
|
+
* Manually triggers cleanup of expired entries.
|
|
138
|
+
* This is useful if you want to force cleanup without waiting for the automatic interval.
|
|
139
|
+
*/
|
|
140
|
+
cleanupExpired(): void;
|
|
141
|
+
/**
|
|
142
|
+
* Gets the current number of entries in the cache.
|
|
143
|
+
* @returns The number of entries currently in the cache.
|
|
144
|
+
*/
|
|
145
|
+
size(): number;
|
|
146
|
+
/**
|
|
147
|
+
* Checks if a key exists in the cache and is not expired.
|
|
148
|
+
* @param key - The key to check.
|
|
149
|
+
* @returns True if the key exists and is not expired, false otherwise.
|
|
150
|
+
*/
|
|
151
|
+
has(key: K): boolean;
|
|
96
152
|
}
|
package/dist/index.js
CHANGED
|
@@ -16,8 +16,9 @@ class Node {
|
|
|
16
16
|
* Creates a new Node instance.
|
|
17
17
|
* @param key - The key associated with this node.
|
|
18
18
|
* @param value - A Promise that resolves to the value of this node.
|
|
19
|
+
* @param ttlMs - Time to live in milliseconds. If provided, sets the expiration time.
|
|
19
20
|
*/
|
|
20
|
-
constructor(key, value) {
|
|
21
|
+
constructor(key, value, ttlMs) {
|
|
21
22
|
/**
|
|
22
23
|
* Reference to the next node in the linked list.
|
|
23
24
|
* This property is `null` if there is no next node.
|
|
@@ -28,8 +29,23 @@ class Node {
|
|
|
28
29
|
* This property is `null` if there is no previous node.
|
|
29
30
|
*/
|
|
30
31
|
this.prev = null;
|
|
32
|
+
/**
|
|
33
|
+
* The timestamp when this node expires (in milliseconds).
|
|
34
|
+
* If null, the node never expires.
|
|
35
|
+
*/
|
|
36
|
+
this.expiresAt = null;
|
|
31
37
|
this.key = key;
|
|
32
38
|
this.value = value;
|
|
39
|
+
if (ttlMs !== undefined && ttlMs > 0) {
|
|
40
|
+
this.expiresAt = Date.now() + ttlMs;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Checks if this node has expired.
|
|
45
|
+
* @returns True if the node has expired, false otherwise.
|
|
46
|
+
*/
|
|
47
|
+
isExpired() {
|
|
48
|
+
return this.expiresAt !== null && Date.now() > this.expiresAt;
|
|
33
49
|
}
|
|
34
50
|
}
|
|
35
51
|
/**
|
|
@@ -38,6 +54,7 @@ class Node {
|
|
|
38
54
|
* - Supports asynchronous savers for data persistence.
|
|
39
55
|
* - Automatically merges concurrent requests (get) and serializes writes (put).
|
|
40
56
|
* - Implements LRU eviction strategy.
|
|
57
|
+
* - Supports TTL (Time To Live) for automatic expiration of entries.
|
|
41
58
|
*/
|
|
42
59
|
class AsyncLRUCache {
|
|
43
60
|
/**
|
|
@@ -61,6 +78,48 @@ class AsyncLRUCache {
|
|
|
61
78
|
if (option.capacity < 10)
|
|
62
79
|
throw new Error("Capacity must be at least 10.");
|
|
63
80
|
this.capacity = option.capacity;
|
|
81
|
+
this.defaultTtlMs = option.defaultTtlMs;
|
|
82
|
+
// Setup periodic cleanup if specified
|
|
83
|
+
if (option.cleanupIntervalMs && option.cleanupIntervalMs > 0) {
|
|
84
|
+
this.cleanupTimer = setInterval(() => {
|
|
85
|
+
this._cleanupExpired();
|
|
86
|
+
}, option.cleanupIntervalMs);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Cleans up expired entries from the cache.
|
|
91
|
+
* This method removes all nodes that have exceeded their TTL.
|
|
92
|
+
*/
|
|
93
|
+
_cleanupExpired() {
|
|
94
|
+
const expiredKeys = [];
|
|
95
|
+
// Collect expired keys
|
|
96
|
+
for (const [key, node] of this.dataMap) {
|
|
97
|
+
if (node.isExpired()) {
|
|
98
|
+
expiredKeys.push(key);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
// Remove expired entries
|
|
102
|
+
for (const key of expiredKeys) {
|
|
103
|
+
const node = this.dataMap.get(key);
|
|
104
|
+
if (node) {
|
|
105
|
+
this.dataMap.delete(key);
|
|
106
|
+
this._removeNode(node);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Checks if a node is expired and removes it if so.
|
|
112
|
+
* @param key - The key to check.
|
|
113
|
+
* @param node - The node to check.
|
|
114
|
+
* @returns True if the node was expired and removed, false otherwise.
|
|
115
|
+
*/
|
|
116
|
+
_checkAndRemoveExpired(key, node) {
|
|
117
|
+
if (node.isExpired()) {
|
|
118
|
+
this.dataMap.delete(key);
|
|
119
|
+
this._removeNode(node);
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
64
123
|
}
|
|
65
124
|
/**
|
|
66
125
|
* Checks if eviction is needed according to the cache strategy.
|
|
@@ -116,16 +175,24 @@ class AsyncLRUCache {
|
|
|
116
175
|
* Automatically merges concurrent requests for the same key.
|
|
117
176
|
* @param key - Cache key.
|
|
118
177
|
* @param loader - Asynchronous loader function to execute on cache miss.
|
|
178
|
+
* @param ttlMs - Optional TTL override for this entry in milliseconds.
|
|
119
179
|
* @returns A Promise that resolves to the required data.
|
|
120
180
|
*/
|
|
121
|
-
get(key, loader) {
|
|
181
|
+
get(key, loader, ttlMs) {
|
|
122
182
|
const node = this.dataMap.get(key);
|
|
123
183
|
if (node) {
|
|
124
|
-
|
|
125
|
-
|
|
184
|
+
// Check if the node has expired
|
|
185
|
+
if (this._checkAndRemoveExpired(key, node)) {
|
|
186
|
+
// Node was expired and removed, proceed to load fresh data
|
|
187
|
+
}
|
|
188
|
+
else {
|
|
189
|
+
this._moveToHead(node);
|
|
190
|
+
return node.value;
|
|
191
|
+
}
|
|
126
192
|
}
|
|
127
193
|
const loadingPromise = loader();
|
|
128
|
-
const
|
|
194
|
+
const effectiveTtlMs = ttlMs !== null && ttlMs !== void 0 ? ttlMs : this.defaultTtlMs;
|
|
195
|
+
const newNode = new Node(key, loadingPromise, effectiveTtlMs);
|
|
129
196
|
this.dataMap.set(key, newNode);
|
|
130
197
|
this._addToHead(newNode);
|
|
131
198
|
this._evictIfNeeded();
|
|
@@ -144,9 +211,10 @@ class AsyncLRUCache {
|
|
|
144
211
|
* @param key - Cache key.
|
|
145
212
|
* @param value - Value to cache.
|
|
146
213
|
* @param saver - Optional asynchronous save function.
|
|
214
|
+
* @param ttlMs - Optional TTL override for this entry in milliseconds.
|
|
147
215
|
* @returns A Promise that resolves to the latest value after the saver operation completes.
|
|
148
216
|
*/
|
|
149
|
-
put(key, value, saver) {
|
|
217
|
+
put(key, value, saver, ttlMs) {
|
|
150
218
|
const lastPromise = this.dataMap.has(key) ? this.dataMap.get(key).value : Promise.resolve(undefined);
|
|
151
219
|
const saverPromise = lastPromise.catch((err) => {
|
|
152
220
|
// Ignore previous operation errors, so the new operation can proceed.
|
|
@@ -162,14 +230,22 @@ class AsyncLRUCache {
|
|
|
162
230
|
return value;
|
|
163
231
|
});
|
|
164
232
|
let node = this.dataMap.get(key);
|
|
233
|
+
const effectiveTtlMs = ttlMs !== null && ttlMs !== void 0 ? ttlMs : this.defaultTtlMs;
|
|
165
234
|
if (!node) {
|
|
166
|
-
node = new Node(key, saverPromise);
|
|
235
|
+
node = new Node(key, saverPromise, effectiveTtlMs);
|
|
167
236
|
this.dataMap.set(key, node);
|
|
168
237
|
this._addToHead(node);
|
|
169
238
|
this._evictIfNeeded();
|
|
170
239
|
}
|
|
171
240
|
else {
|
|
241
|
+
// Update existing node with new value and TTL
|
|
172
242
|
node.value = saverPromise;
|
|
243
|
+
if (effectiveTtlMs !== undefined && effectiveTtlMs > 0) {
|
|
244
|
+
node.expiresAt = Date.now() + effectiveTtlMs;
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
node.expiresAt = null;
|
|
248
|
+
}
|
|
173
249
|
this._moveToHead(node);
|
|
174
250
|
}
|
|
175
251
|
saverPromise.catch(err => {
|
|
@@ -208,11 +284,54 @@ class AsyncLRUCache {
|
|
|
208
284
|
node.next = null;
|
|
209
285
|
node.key = null;
|
|
210
286
|
node.value = Promise.resolve(undefined);
|
|
287
|
+
node.expiresAt = null;
|
|
211
288
|
// Move to the next node.
|
|
212
289
|
node = next;
|
|
213
290
|
}
|
|
214
291
|
this.head = null;
|
|
215
292
|
this.tail = null;
|
|
216
293
|
}
|
|
294
|
+
/**
|
|
295
|
+
* Destroys the cache instance, clearing all data and stopping any background timers.
|
|
296
|
+
* Call this method when you no longer need the cache to prevent memory leaks.
|
|
297
|
+
*/
|
|
298
|
+
destroy() {
|
|
299
|
+
// Stop the cleanup timer if it exists
|
|
300
|
+
if (this.cleanupTimer) {
|
|
301
|
+
clearInterval(this.cleanupTimer);
|
|
302
|
+
this.cleanupTimer = undefined;
|
|
303
|
+
}
|
|
304
|
+
// Clear all cache data
|
|
305
|
+
this.clear();
|
|
306
|
+
}
|
|
307
|
+
/**
|
|
308
|
+
* Manually triggers cleanup of expired entries.
|
|
309
|
+
* This is useful if you want to force cleanup without waiting for the automatic interval.
|
|
310
|
+
*/
|
|
311
|
+
cleanupExpired() {
|
|
312
|
+
this._cleanupExpired();
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Gets the current number of entries in the cache.
|
|
316
|
+
* @returns The number of entries currently in the cache.
|
|
317
|
+
*/
|
|
318
|
+
size() {
|
|
319
|
+
return this.dataMap.size;
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Checks if a key exists in the cache and is not expired.
|
|
323
|
+
* @param key - The key to check.
|
|
324
|
+
* @returns True if the key exists and is not expired, false otherwise.
|
|
325
|
+
*/
|
|
326
|
+
has(key) {
|
|
327
|
+
const node = this.dataMap.get(key);
|
|
328
|
+
if (!node) {
|
|
329
|
+
return false;
|
|
330
|
+
}
|
|
331
|
+
if (this._checkAndRemoveExpired(key, node)) {
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
217
336
|
}
|
|
218
337
|
exports.AsyncLRUCache = AsyncLRUCache;
|