@wfp99/async-lru-cache 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/README.zh-TW.md +207 -0
- package/dist/index.d.ts +96 -0
- package/dist/index.js +218 -0
- package/package.json +44 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Wang Feng Ping
|
|
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,211 @@
|
|
|
1
|
+
# Async LRU Cache
|
|
2
|
+
|
|
3
|
+
[](https://badge.fury.io/js/@wfp99%2Fasync-lru-cache)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org/)
|
|
6
|
+
|
|
7
|
+
An asynchronous LRU (Least Recently Used) memory cache that supports asynchronous loading and saving operations while automatically handling concurrent requests.
|
|
8
|
+
|
|
9
|
+
## Features
|
|
10
|
+
|
|
11
|
+
- ๐ **Async Support**: Supports asynchronous loader and saver functions
|
|
12
|
+
- ๐ **Automatic Merging**: Automatically merges concurrent GET requests for the same key
|
|
13
|
+
- ๐ **Serialized Writes**: Serializes PUT operations for the same key to ensure execution order
|
|
14
|
+
- ๐๏ธ **LRU Eviction**: Implements LRU algorithm to automatically remove least recently used items
|
|
15
|
+
- ๐ก๏ธ **Error Handling**: Comprehensive error handling with automatic cache cleanup on failures
|
|
16
|
+
- ๐งน **Complete Clearing**: Support for clearing all cache entries at once
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
npm install @wfp99/async-lru-cache
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Requirements
|
|
25
|
+
|
|
26
|
+
- Node.js >= 14.0.0
|
|
27
|
+
|
|
28
|
+
## Usage
|
|
29
|
+
|
|
30
|
+
### Basic Usage
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import { AsyncLRUCache } from '@wfp99/async-lru-cache';
|
|
34
|
+
|
|
35
|
+
// Create a cache with maximum 100 items
|
|
36
|
+
const cache = new AsyncLRUCache({
|
|
37
|
+
capacity: 100
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
// Use get method to retrieve data
|
|
41
|
+
const data = await cache.get('user:123', async () => {
|
|
42
|
+
// Loader function executed on cache miss
|
|
43
|
+
const response = await fetch('/api/users/123');
|
|
44
|
+
return response.json();
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Use put method to store data
|
|
48
|
+
await cache.put('user:456', userData, async (key, value) => {
|
|
49
|
+
// Optional saver function for data persistence
|
|
50
|
+
await saveToDatabase(key, value);
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### Error Handling
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
try {
|
|
58
|
+
const data = await cache.get('problematic-key', async () => {
|
|
59
|
+
throw new Error('Load failed');
|
|
60
|
+
});
|
|
61
|
+
} catch (error) {
|
|
62
|
+
console.error('Cache load failed:', error);
|
|
63
|
+
// Failed items are automatically removed from cache
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Advanced Usage
|
|
68
|
+
|
|
69
|
+
```typescript
|
|
70
|
+
// Working with database
|
|
71
|
+
const cache = new AsyncLRUCache<string, UserData>({ capacity: 500 });
|
|
72
|
+
|
|
73
|
+
// Load from database with automatic caching
|
|
74
|
+
const user = await cache.get(`user:${userId}`, async () => {
|
|
75
|
+
return await database.users.findById(userId);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// Save to cache and database atomically
|
|
79
|
+
await cache.put(`user:${userId}`, updatedUser, async (key, value) => {
|
|
80
|
+
await database.users.update(userId, value);
|
|
81
|
+
});
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Cache Management
|
|
85
|
+
|
|
86
|
+
```typescript
|
|
87
|
+
// Manually remove specific cache entry
|
|
88
|
+
cache.invalidate('user:123');
|
|
89
|
+
|
|
90
|
+
// Clear all cache entries
|
|
91
|
+
cache.clear();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## API Reference
|
|
95
|
+
|
|
96
|
+
### `AsyncLRUCache<K, V>`
|
|
97
|
+
|
|
98
|
+
#### Constructor
|
|
99
|
+
|
|
100
|
+
```typescript
|
|
101
|
+
constructor(option: AsyncLRUCacheOption)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
#### `AsyncLRUCacheOption`
|
|
105
|
+
|
|
106
|
+
| Property | Type | Required | Description |
|
|
107
|
+
|----------|------|----------|-------------|
|
|
108
|
+
| `capacity` | `number` | Yes | Maximum number of items allowed in cache. Must be a positive number. |
|
|
109
|
+
|
|
110
|
+
#### Methods
|
|
111
|
+
|
|
112
|
+
##### `get(key: K, loader: () => Promise<V>): Promise<V>`
|
|
113
|
+
|
|
114
|
+
Retrieves data from cache. If not found, uses the loader to load data.
|
|
115
|
+
|
|
116
|
+
- **key**: Cache key
|
|
117
|
+
- **loader**: Asynchronous loader function executed on cache miss
|
|
118
|
+
- **Returns**: Promise that resolves to the required data
|
|
119
|
+
|
|
120
|
+
##### `put(key: K, value: V, saver?: (key: K, value: V) => Promise<void>): Promise<V>`
|
|
121
|
+
|
|
122
|
+
Puts a value into cache and optionally executes a saver function for persistence.
|
|
123
|
+
|
|
124
|
+
- **key**: Cache key
|
|
125
|
+
- **value**: Value to cache
|
|
126
|
+
- **saver**: Optional asynchronous saver function
|
|
127
|
+
- **Returns**: Promise that resolves to the latest value after saver operation completes
|
|
128
|
+
|
|
129
|
+
##### `invalidate(key: K): void`
|
|
130
|
+
|
|
131
|
+
Invalidates and removes the cache entry for the specified key.
|
|
132
|
+
|
|
133
|
+
- **key**: Cache key to invalidate
|
|
134
|
+
|
|
135
|
+
##### `clear(): void`
|
|
136
|
+
|
|
137
|
+
Clears all cache entries. This method removes all items from cache and manually clears node links to prevent potential memory leaks.
|
|
138
|
+
|
|
139
|
+
## Concurrency Handling
|
|
140
|
+
|
|
141
|
+
### GET Request Merging
|
|
142
|
+
|
|
143
|
+
When multiple concurrent requests fetch the same key, AsyncLRUCache automatically merges these requests, ensuring the loader function executes only once:
|
|
144
|
+
|
|
145
|
+
```typescript
|
|
146
|
+
// These three concurrent requests will share the same loader execution
|
|
147
|
+
const [data1, data2, data3] = await Promise.all([
|
|
148
|
+
cache.get('shared-key', loader),
|
|
149
|
+
cache.get('shared-key', loader),
|
|
150
|
+
cache.get('shared-key', loader)
|
|
151
|
+
]);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### PUT Operation Serialization
|
|
155
|
+
|
|
156
|
+
Multiple PUT operations for the same key are serialized to ensure they execute in order:
|
|
157
|
+
|
|
158
|
+
```typescript
|
|
159
|
+
// These operations will execute sequentially, even if started concurrently
|
|
160
|
+
cache.put('key', 'value1', saver1);
|
|
161
|
+
cache.put('key', 'value2', saver2);
|
|
162
|
+
cache.put('key', 'value3', saver3);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## TypeScript Support
|
|
166
|
+
|
|
167
|
+
This package is fully written in TypeScript and provides complete type support:
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
interface User {
|
|
171
|
+
id: string;
|
|
172
|
+
name: string;
|
|
173
|
+
email: string;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const userCache = new AsyncLRUCache<string, User>({
|
|
177
|
+
capacity: 1000
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
const user: User = await userCache.get('user:123', async () => {
|
|
181
|
+
// Loader must return User type
|
|
182
|
+
return fetchUserFromAPI('123');
|
|
183
|
+
});
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
## Error Handling
|
|
187
|
+
|
|
188
|
+
- **Loader Failures**: If loader function throws an exception, corresponding cache entry is automatically removed
|
|
189
|
+
- **Saver Failures**: If saver function fails, cache entry is also removed to ensure data consistency
|
|
190
|
+
- **Operation Chain Errors**: PUT operations ignore previous operation errors, allowing new operations to proceed
|
|
191
|
+
- **Error Logging**: All errors are automatically logged to console for debugging
|
|
192
|
+
|
|
193
|
+
## Memory Management
|
|
194
|
+
|
|
195
|
+
AsyncLRUCache provides comprehensive memory management:
|
|
196
|
+
|
|
197
|
+
- **Automatic Eviction**: Automatically removes least recently used items based on capacity
|
|
198
|
+
- **Manual Cleanup**: `clear()` method thoroughly cleans all node links to prevent memory leaks
|
|
199
|
+
- **Error Cleanup**: Automatically cleans related cache entries when operations fail
|
|
200
|
+
|
|
201
|
+
## License
|
|
202
|
+
|
|
203
|
+
MIT
|
|
204
|
+
|
|
205
|
+
## Contributing
|
|
206
|
+
|
|
207
|
+
Issues and pull requests are welcome on [GitHub](https://github.com/wfp99/async-lru-cache).
|
|
208
|
+
|
|
209
|
+
## Author
|
|
210
|
+
|
|
211
|
+
Wang Feng Ping
|
package/README.zh-TW.md
ADDED
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Async LRU Cache
|
|
2
|
+
|
|
3
|
+
ไธๅๆฏๆด้ๅๆญฅ่ผๅ
ฅๅไฟๅญๆไฝ็ LRU๏ผLeast Recently Used๏ผ่จๆถ้ซๅฟซๅ้กๅฅ๏ผ่ชๅ่็ไธฆ็ผ่ซๆฑใ
|
|
4
|
+
|
|
5
|
+
## ็น่ฒ
|
|
6
|
+
|
|
7
|
+
- ๐ **้ๅๆญฅๆฏๆด**๏ผๆฏๆด้ๅๆญฅ็่ผๅ
ฅๅจ๏ผloader๏ผๅไฟๅญๅจ๏ผsaver๏ผๅฝๆธ
|
|
8
|
+
- ๐ **่ชๅๅไฝต**๏ผ่ชๅๅไฝต็ธๅ key ็ไธฆ็ผ GET ่ซๆฑ
|
|
9
|
+
- ๐ **ๅบๅๅๅฏซๅ
ฅ**๏ผๅบๅๅๅไธ key ็ PUT ๆไฝ๏ผ็ขบไฟ้ ๅบๅท่ก
|
|
10
|
+
- ๐๏ธ **LRU ๆทๆฑฐ็ญ็ฅ**๏ผๅฏฆ็พ LRU ๆผ็ฎๆณ๏ผ่ชๅ็งป้คๆไน
ๆชไฝฟ็จ็้
็ฎ
|
|
11
|
+
- ๐ก๏ธ **้ฏ่ชค่็**๏ผๅฎๅ็้ฏ่ชค่็ๆฉๅถ๏ผ่ผๅ
ฅๆไฟๅญๅคฑๆๆ่ชๅๆธ
็ๅฟซๅ
|
|
12
|
+
- ๐งน **ๅฎๆดๆธ
้ค**๏ผๆฏๆดไธๆฌกๆงๆธ
้คๆๆๅฟซๅ้
็ฎ
|
|
13
|
+
|
|
14
|
+
## ๅฎ่ฃ
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
npm install @wfp99/async-lru-cache
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## ็ณป็ตฑ้ๆฑ
|
|
21
|
+
|
|
22
|
+
- Node.js >= 14.0.0
|
|
23
|
+
|
|
24
|
+
## ไฝฟ็จๆนๆณ
|
|
25
|
+
|
|
26
|
+
### ๅบๆฌไฝฟ็จ
|
|
27
|
+
|
|
28
|
+
```typescript
|
|
29
|
+
import { AsyncLRUCache } from '@wfp99/async-lru-cache';
|
|
30
|
+
|
|
31
|
+
// ๅตๅปบไธๅๆๅคๅฎน็ด 100 ๅ้
็ฎ็ๅฟซๅ
|
|
32
|
+
const cache = new AsyncLRUCache({
|
|
33
|
+
capacity: 100
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// ไฝฟ็จ get ๆนๆณ็ฒๅ่ณๆ
|
|
37
|
+
const data = await cache.get('user:123', async () => {
|
|
38
|
+
// ๅฟซๅๆชๅฝไธญๆๅท่ก็่ผๅ
ฅๅจๅฝๆธ
|
|
39
|
+
const response = await fetch('/api/users/123');
|
|
40
|
+
return response.json();
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// ไฝฟ็จ put ๆนๆณๅฒๅญ่ณๆ
|
|
44
|
+
await cache.put('user:456', userData, async (key, value) => {
|
|
45
|
+
// ๅฏ้ธ็ไฟๅญๅจๅฝๆธ๏ผ็จๆผๆไน
ๅ่ณๆ
|
|
46
|
+
await saveToDatabase(key, value);
|
|
47
|
+
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### ้ฏ่ชค่็
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
try {
|
|
54
|
+
const data = await cache.get('problematic-key', async () => {
|
|
55
|
+
throw new Error('่ผๅ
ฅๅคฑๆ');
|
|
56
|
+
});
|
|
57
|
+
} catch (error) {
|
|
58
|
+
console.error('ๅฟซๅ่ผๅ
ฅๅคฑๆ:', error);
|
|
59
|
+
// ๅคฑๆ็้
็ฎๆ่ชๅๅพๅฟซๅไธญ็งป้ค
|
|
60
|
+
}
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### ้ฒ้ไฝฟ็จ
|
|
64
|
+
|
|
65
|
+
```typescript
|
|
66
|
+
// ่่ณๆๅบซ้
ๅไฝฟ็จ
|
|
67
|
+
const cache = new AsyncLRUCache<string, UserData>({ capacity: 500 });
|
|
68
|
+
|
|
69
|
+
// ๅพ่ณๆๅบซ่ผๅ
ฅไธฆ่ชๅๅฟซๅ
|
|
70
|
+
const user = await cache.get(`user:${userId}`, async () => {
|
|
71
|
+
return await database.users.findById(userId);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// ๅๆๅฒๅญๅฐๅฟซๅๅ่ณๆๅบซ
|
|
75
|
+
await cache.put(`user:${userId}`, updatedUser, async (key, value) => {
|
|
76
|
+
await database.users.update(userId, value);
|
|
77
|
+
});
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
### ๅฟซๅ็ฎก็
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
// ๆๅ็งป้ค็นๅฎๅฟซๅ้
็ฎ
|
|
84
|
+
cache.invalidate('user:123');
|
|
85
|
+
|
|
86
|
+
// ๆธ
้คๆๆๅฟซๅ้
็ฎ
|
|
87
|
+
cache.clear();
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## API ๅ่
|
|
91
|
+
|
|
92
|
+
### `AsyncLRUCache<K, V>`
|
|
93
|
+
|
|
94
|
+
#### ๅปบๆงๅฝๆธ
|
|
95
|
+
|
|
96
|
+
```typescript
|
|
97
|
+
constructor(option: AsyncLRUCacheOption)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
#### `AsyncLRUCacheOption`
|
|
101
|
+
|
|
102
|
+
| ๅฑฌๆง | ้กๅ | ๅฟ
้ | ๆ่ฟฐ |
|
|
103
|
+
|-----|------|------|------|
|
|
104
|
+
| `capacity` | `number` | ๆฏ | ๅฟซๅไธญๅ
่จฑ็ๆๅคง้
็ฎๆธ้๏ผๅฟ
้ ็บๆญฃๆธ |
|
|
105
|
+
|
|
106
|
+
#### ๆนๆณ
|
|
107
|
+
|
|
108
|
+
##### `get(key: K, loader: () => Promise<V>): Promise<V>`
|
|
109
|
+
|
|
110
|
+
ๅพๅฟซๅไธญ็ฒๅ่ณๆใๅฆๆไธๅญๅจ๏ผไฝฟ็จ่ผๅ
ฅๅจ่ผๅ
ฅ่ณๆใ
|
|
111
|
+
|
|
112
|
+
- **key**: ๅฟซๅ้ต
|
|
113
|
+
- **loader**: ๅฟซๅๆชๅฝไธญๆๅท่ก็้ๅๆญฅ่ผๅ
ฅๅฝๆธ
|
|
114
|
+
- **ๅๅณ**: Promise๏ผ่งฃๆ็บๆ้็่ณๆ
|
|
115
|
+
|
|
116
|
+
##### `put(key: K, value: V, saver?: (key: K, value: V) => Promise<void>): Promise<V>`
|
|
117
|
+
|
|
118
|
+
ๅฐๅผๆพๅ
ฅๅฟซๅไธญ๏ผไธฆๅฏ้ธๆๆงๅฐๅท่กไฟๅญๅจๅฝๆธ้ฒ่กๆไน
ๅใ
|
|
119
|
+
|
|
120
|
+
- **key**: ๅฟซๅ้ต
|
|
121
|
+
- **value**: ่ฆๅฟซๅ็ๅผ
|
|
122
|
+
- **saver**: ๅฏ้ธ็้ๅๆญฅไฟๅญๅฝๆธ
|
|
123
|
+
- **ๅๅณ**: Promise๏ผ่งฃๆ็บไฟๅญๆไฝๅฎๆๅพ็ๆๆฐๅผ
|
|
124
|
+
|
|
125
|
+
##### `invalidate(key: K): void`
|
|
126
|
+
|
|
127
|
+
ไฝฟๆๅฎ้ต็ๅฟซๅ้
็ฎๅคฑๆไธฆ็งป้คใ
|
|
128
|
+
|
|
129
|
+
- **key**: ่ฆๅคฑๆ็ๅฟซๅ้ต
|
|
130
|
+
|
|
131
|
+
##### `clear(): void`
|
|
132
|
+
|
|
133
|
+
ๆธ
้คๆๆๅฟซๅ้
็ฎใๆญคๆนๆณๆ็งป้คๅฟซๅไธญ็ๆๆ้
็ฎไธฆๆๅๆธ
็็ฏ้ป้ฃ็ตไปฅ้ฟๅ
ๆฝๅจ็่จๆถ้ซๆดฉๆผใ
|
|
134
|
+
|
|
135
|
+
## ไธฆ็ผ่็
|
|
136
|
+
|
|
137
|
+
### GET ่ซๆฑๅไฝต
|
|
138
|
+
|
|
139
|
+
็ถๅคๅไธฆ็ผ่ซๆฑ็ฒๅ็ธๅ็ key ๆ๏ผAsyncLRUCache ๆ่ชๅๅไฝต้ไบ่ซๆฑ๏ผ็ขบไฟ่ผๅ
ฅๅจๅฝๆธๅชๅท่กไธๆฌก๏ผ
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
// ้ไธๅไธฆ็ผ่ซๆฑๆๅ
ฑไบซๅไธๅ่ผๅ
ฅๅจๅท่ก
|
|
143
|
+
const [data1, data2, data3] = await Promise.all([
|
|
144
|
+
cache.get('shared-key', loader),
|
|
145
|
+
cache.get('shared-key', loader),
|
|
146
|
+
cache.get('shared-key', loader)
|
|
147
|
+
]);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### PUT ๆไฝๅบๅๅ
|
|
151
|
+
|
|
152
|
+
ๅฐๅไธ key ็ๅคๅ PUT ๆไฝๆ่ขซๅบๅๅ๏ผ็ขบไฟๅฎๅๆ้ ๅบๅท่ก๏ผ
|
|
153
|
+
|
|
154
|
+
```typescript
|
|
155
|
+
// ้ไบๆไฝๆๆ้ ๅบๅท่ก๏ผๅณไฝฟๅฎๅๆฏไธฆ็ผๅๅ็
|
|
156
|
+
cache.put('key', 'value1', saver1);
|
|
157
|
+
cache.put('key', 'value2', saver2);
|
|
158
|
+
cache.put('key', 'value3', saver3);
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
## TypeScript ๆฏๆด
|
|
162
|
+
|
|
163
|
+
ๆญคๅฅไปถๅฎๅ
จไฝฟ็จ TypeScript ็ทจๅฏซ๏ผๆไพๅฎๆด็ๅๅฅๆฏๆด๏ผ
|
|
164
|
+
|
|
165
|
+
```typescript
|
|
166
|
+
interface User {
|
|
167
|
+
id: string;
|
|
168
|
+
name: string;
|
|
169
|
+
email: string;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
const userCache = new AsyncLRUCache<string, User>({
|
|
173
|
+
capacity: 1000
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
const user: User = await userCache.get('user:123', async () => {
|
|
177
|
+
// ่ผๅ
ฅๅจๅฟ
้ ๅๅณ User ๅๅฅ
|
|
178
|
+
return fetchUserFromAPI('123');
|
|
179
|
+
});
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
## ้ฏ่ชค่็ๆฉๅถ
|
|
183
|
+
|
|
184
|
+
- **่ผๅ
ฅๅจๅคฑๆ**๏ผๅฆๆ่ผๅ
ฅๅจๅฝๆธๆๅบ็ฐๅธธ๏ผๅฐๆ็ๅฟซๅ้
็ฎๆ่ขซ่ชๅ็งป้ค
|
|
185
|
+
- **ไฟๅญๅจๅคฑๆ**๏ผๅฆๆไฟๅญๅจๅฝๆธๅคฑๆ๏ผๅฟซๅ้
็ฎไนๆ่ขซ็งป้ค๏ผ็ขบไฟ่ณๆไธ่ดๆง
|
|
186
|
+
- **ๆไฝ้้ฏ่ชค**๏ผPUT ๆไฝๆๅฟฝ็ฅๅไธๅๆไฝ็้ฏ่ชค๏ผ็ขบไฟๆฐๆไฝ่ฝๅค ็นผ็บ้ฒ่ก
|
|
187
|
+
- **้ฏ่ชคๆฅ่ช**๏ผๆๆ้ฏ่ชค้ฝๆ่ชๅ่จ้ๅฐๆงๅถๅฐ๏ผไพฟๆผ้ค้ฏ
|
|
188
|
+
|
|
189
|
+
## ่จๆถ้ซ็ฎก็
|
|
190
|
+
|
|
191
|
+
AsyncLRUCache ๅ
ทๅๅฎๅ็่จๆถ้ซ็ฎก็ๆฉๅถ๏ผ
|
|
192
|
+
|
|
193
|
+
- **่ชๅๆทๆฑฐ**๏ผ็ถๅฟซๅ้
็ฎๆธ้่ถ
้ๅฎน้้ๅถๆ๏ผ่ชๅ็งป้คๆไน
ๆชไฝฟ็จ็้
็ฎ
|
|
194
|
+
- **ๆๅๆธ
็**๏ผ`clear()` ๆนๆณๆๅพนๅบๆธ
็ๆๆ็ฏ้ป้ฃ็ต๏ผ้ฒๆญข่จๆถ้ซๆดฉๆผ
|
|
195
|
+
- **้ฏ่ชคๆธ
็**๏ผๆไฝๅคฑๆๆ่ชๅๆธ
็็ธ้ๅฟซๅ้
็ฎ
|
|
196
|
+
|
|
197
|
+
## ๆๆฌ
|
|
198
|
+
|
|
199
|
+
MIT
|
|
200
|
+
|
|
201
|
+
## ่ฒข็ป
|
|
202
|
+
|
|
203
|
+
ๆญก่ฟๅจ [GitHub](https://github.com/wfp99/async-lru-cache) ไธๆๅบๅ้กๅ็ผ้ Pull Requestใ
|
|
204
|
+
|
|
205
|
+
## ไฝ่
|
|
206
|
+
|
|
207
|
+
Wang Feng Ping
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asynchronous LRU Cache.
|
|
3
|
+
* Supports asynchronous load and save operations, and automatically handles concurrent requests.
|
|
4
|
+
* Uses the LRU strategy to evict the oldest items.
|
|
5
|
+
*
|
|
6
|
+
* @module AsyncLRUCache
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* Configuration options for the AsyncLRUCache.
|
|
10
|
+
*/
|
|
11
|
+
export interface AsyncLRUCacheOption {
|
|
12
|
+
/**
|
|
13
|
+
* Maximum capacity of the cache, this is the maximum number of items allowed.
|
|
14
|
+
* Must not be less than 10.
|
|
15
|
+
*/
|
|
16
|
+
capacity: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Asynchronous LRU Cache class.
|
|
20
|
+
* - Supports asynchronous loaders for data fetching.
|
|
21
|
+
* - Supports asynchronous savers for data persistence.
|
|
22
|
+
* - Automatically merges concurrent requests (get) and serializes writes (put).
|
|
23
|
+
* - Implements LRU eviction strategy.
|
|
24
|
+
*/
|
|
25
|
+
export declare class AsyncLRUCache<K, V> {
|
|
26
|
+
/**
|
|
27
|
+
* Maximum capacity of the cache.
|
|
28
|
+
*/
|
|
29
|
+
private readonly capacity;
|
|
30
|
+
/**
|
|
31
|
+
* Map for storing cache entries. The key is the cache key, and the value is the Node.
|
|
32
|
+
*/
|
|
33
|
+
private dataMap;
|
|
34
|
+
/**
|
|
35
|
+
* Head node of the linked list, pointing to the most recently used item.
|
|
36
|
+
*/
|
|
37
|
+
private head;
|
|
38
|
+
/**
|
|
39
|
+
* Tail node of the linked list, pointing to the least recently used item.
|
|
40
|
+
*/
|
|
41
|
+
private tail;
|
|
42
|
+
/**
|
|
43
|
+
* Creates an instance of AsyncLRUCache with the specified options.
|
|
44
|
+
* @param option - The configuration options for the cache.
|
|
45
|
+
* @throws {Error} Throws an error if the provided capacity is less than 10.
|
|
46
|
+
*/
|
|
47
|
+
constructor(option: AsyncLRUCacheOption);
|
|
48
|
+
/**
|
|
49
|
+
* Checks if eviction is needed according to the cache strategy.
|
|
50
|
+
* When the cache exceeds its capacity, automatically removes the tail (least recently used) node.
|
|
51
|
+
*/
|
|
52
|
+
private _evictIfNeeded;
|
|
53
|
+
/**
|
|
54
|
+
* Moves the specified node to the head of the linked list.
|
|
55
|
+
* This operation is used to mark a node as most recently used.
|
|
56
|
+
* @param node - The node to move to the head of the list.
|
|
57
|
+
*/
|
|
58
|
+
private _moveToHead;
|
|
59
|
+
/**
|
|
60
|
+
* Removes a node from the doubly linked list used by the cache.
|
|
61
|
+
* Updates the previous and next pointers of adjacent nodes to unlink the specified node.
|
|
62
|
+
* @param node - The node to remove from the list.
|
|
63
|
+
*/
|
|
64
|
+
private _removeNode;
|
|
65
|
+
/**
|
|
66
|
+
* Adds the given node to the head (front) of the doubly linked list.
|
|
67
|
+
* @param node - The node to add to the head of the list.
|
|
68
|
+
*/
|
|
69
|
+
private _addToHead;
|
|
70
|
+
/**
|
|
71
|
+
* Retrieves data from the cache. If it does not exist, uses the loader to load it.
|
|
72
|
+
* Automatically merges concurrent requests for the same key.
|
|
73
|
+
* @param key - Cache key.
|
|
74
|
+
* @param loader - Asynchronous loader function to execute on cache miss.
|
|
75
|
+
* @returns A Promise that resolves to the required data.
|
|
76
|
+
*/
|
|
77
|
+
get(key: K, loader: () => Promise<V>): Promise<V>;
|
|
78
|
+
/**
|
|
79
|
+
* Puts a value into the cache, and optionally executes a saver function to persist it.
|
|
80
|
+
* This method serializes multiple put operations for the same key, ensuring they execute in order.
|
|
81
|
+
* @param key - Cache key.
|
|
82
|
+
* @param value - Value to cache.
|
|
83
|
+
* @param saver - Optional asynchronous save function.
|
|
84
|
+
* @returns A Promise that resolves to the latest value after the saver operation completes.
|
|
85
|
+
*/
|
|
86
|
+
put(key: K, value: V, saver?: (key: K, value: V) => Promise<void>): Promise<V>;
|
|
87
|
+
/**
|
|
88
|
+
* Invalidates and removes the cache entry associated with the specified key.
|
|
89
|
+
* @param key - The key of the cache entry to invalidate.
|
|
90
|
+
*/
|
|
91
|
+
invalidate(key: K): void;
|
|
92
|
+
/**
|
|
93
|
+
* Clears the entire cache, removing all entries.
|
|
94
|
+
*/
|
|
95
|
+
clear(): void;
|
|
96
|
+
}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Asynchronous LRU Cache.
|
|
4
|
+
* Supports asynchronous load and save operations, and automatically handles concurrent requests.
|
|
5
|
+
* Uses the LRU strategy to evict the oldest items.
|
|
6
|
+
*
|
|
7
|
+
* @module AsyncLRUCache
|
|
8
|
+
*/
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.AsyncLRUCache = void 0;
|
|
11
|
+
/**
|
|
12
|
+
* Node of the linked list, where value is a Promise representing the eventual data value.
|
|
13
|
+
*/
|
|
14
|
+
class Node {
|
|
15
|
+
/**
|
|
16
|
+
* Creates a new Node instance.
|
|
17
|
+
* @param key - The key associated with this node.
|
|
18
|
+
* @param value - A Promise that resolves to the value of this node.
|
|
19
|
+
*/
|
|
20
|
+
constructor(key, value) {
|
|
21
|
+
/**
|
|
22
|
+
* Reference to the next node in the linked list.
|
|
23
|
+
* This property is `null` if there is no next node.
|
|
24
|
+
*/
|
|
25
|
+
this.next = null;
|
|
26
|
+
/**
|
|
27
|
+
* Reference to the previous node in the linked list.
|
|
28
|
+
* This property is `null` if there is no previous node.
|
|
29
|
+
*/
|
|
30
|
+
this.prev = null;
|
|
31
|
+
this.key = key;
|
|
32
|
+
this.value = value;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Asynchronous LRU Cache class.
|
|
37
|
+
* - Supports asynchronous loaders for data fetching.
|
|
38
|
+
* - Supports asynchronous savers for data persistence.
|
|
39
|
+
* - Automatically merges concurrent requests (get) and serializes writes (put).
|
|
40
|
+
* - Implements LRU eviction strategy.
|
|
41
|
+
*/
|
|
42
|
+
class AsyncLRUCache {
|
|
43
|
+
/**
|
|
44
|
+
* Creates an instance of AsyncLRUCache with the specified options.
|
|
45
|
+
* @param option - The configuration options for the cache.
|
|
46
|
+
* @throws {Error} Throws an error if the provided capacity is less than 10.
|
|
47
|
+
*/
|
|
48
|
+
constructor(option) {
|
|
49
|
+
/**
|
|
50
|
+
* Map for storing cache entries. The key is the cache key, and the value is the Node.
|
|
51
|
+
*/
|
|
52
|
+
this.dataMap = new Map();
|
|
53
|
+
/**
|
|
54
|
+
* Head node of the linked list, pointing to the most recently used item.
|
|
55
|
+
*/
|
|
56
|
+
this.head = null;
|
|
57
|
+
/**
|
|
58
|
+
* Tail node of the linked list, pointing to the least recently used item.
|
|
59
|
+
*/
|
|
60
|
+
this.tail = null;
|
|
61
|
+
if (option.capacity < 10)
|
|
62
|
+
throw new Error("Capacity must be at least 10.");
|
|
63
|
+
this.capacity = option.capacity;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Checks if eviction is needed according to the cache strategy.
|
|
67
|
+
* When the cache exceeds its capacity, automatically removes the tail (least recently used) node.
|
|
68
|
+
*/
|
|
69
|
+
_evictIfNeeded() {
|
|
70
|
+
if (this.dataMap.size > this.capacity) {
|
|
71
|
+
this.dataMap.delete(this.tail.key);
|
|
72
|
+
this._removeNode(this.tail);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Moves the specified node to the head of the linked list.
|
|
77
|
+
* This operation is used to mark a node as most recently used.
|
|
78
|
+
* @param node - The node to move to the head of the list.
|
|
79
|
+
*/
|
|
80
|
+
_moveToHead(node) {
|
|
81
|
+
if (node === this.head)
|
|
82
|
+
return;
|
|
83
|
+
this._removeNode(node);
|
|
84
|
+
this._addToHead(node);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Removes a node from the doubly linked list used by the cache.
|
|
88
|
+
* Updates the previous and next pointers of adjacent nodes to unlink the specified node.
|
|
89
|
+
* @param node - The node to remove from the list.
|
|
90
|
+
*/
|
|
91
|
+
_removeNode(node) {
|
|
92
|
+
if (node.prev)
|
|
93
|
+
node.prev.next = node.next;
|
|
94
|
+
else
|
|
95
|
+
this.head = node.next;
|
|
96
|
+
if (node.next)
|
|
97
|
+
node.next.prev = node.prev;
|
|
98
|
+
else
|
|
99
|
+
this.tail = node.prev;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Adds the given node to the head (front) of the doubly linked list.
|
|
103
|
+
* @param node - The node to add to the head of the list.
|
|
104
|
+
*/
|
|
105
|
+
_addToHead(node) {
|
|
106
|
+
node.prev = null;
|
|
107
|
+
node.next = this.head;
|
|
108
|
+
if (this.head)
|
|
109
|
+
this.head.prev = node;
|
|
110
|
+
this.head = node;
|
|
111
|
+
if (!this.tail)
|
|
112
|
+
this.tail = node;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Retrieves data from the cache. If it does not exist, uses the loader to load it.
|
|
116
|
+
* Automatically merges concurrent requests for the same key.
|
|
117
|
+
* @param key - Cache key.
|
|
118
|
+
* @param loader - Asynchronous loader function to execute on cache miss.
|
|
119
|
+
* @returns A Promise that resolves to the required data.
|
|
120
|
+
*/
|
|
121
|
+
get(key, loader) {
|
|
122
|
+
const node = this.dataMap.get(key);
|
|
123
|
+
if (node) {
|
|
124
|
+
this._moveToHead(node);
|
|
125
|
+
return node.value;
|
|
126
|
+
}
|
|
127
|
+
const loadingPromise = loader();
|
|
128
|
+
const newNode = new Node(key, loadingPromise);
|
|
129
|
+
this.dataMap.set(key, newNode);
|
|
130
|
+
this._addToHead(newNode);
|
|
131
|
+
this._evictIfNeeded();
|
|
132
|
+
loadingPromise.catch(err => {
|
|
133
|
+
console.error(`[AsyncLRUCache ERROR] Loader for key ${key} failed. Removing from cache.`, err);
|
|
134
|
+
if (this.dataMap.get(key) === newNode) {
|
|
135
|
+
this.dataMap.delete(key);
|
|
136
|
+
this._removeNode(newNode);
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
return loadingPromise;
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Puts a value into the cache, and optionally executes a saver function to persist it.
|
|
143
|
+
* This method serializes multiple put operations for the same key, ensuring they execute in order.
|
|
144
|
+
* @param key - Cache key.
|
|
145
|
+
* @param value - Value to cache.
|
|
146
|
+
* @param saver - Optional asynchronous save function.
|
|
147
|
+
* @returns A Promise that resolves to the latest value after the saver operation completes.
|
|
148
|
+
*/
|
|
149
|
+
put(key, value, saver) {
|
|
150
|
+
const lastPromise = this.dataMap.has(key) ? this.dataMap.get(key).value : Promise.resolve(undefined);
|
|
151
|
+
const saverPromise = lastPromise.catch((err) => {
|
|
152
|
+
// Ignore previous operation errors, so the new operation can proceed.
|
|
153
|
+
console.warn(`[AsyncLRUCache WARN] Previous operation for key ${key} failed. Chaining new PUT operation.`, err.message);
|
|
154
|
+
})
|
|
155
|
+
.then(() => {
|
|
156
|
+
// After the previous operation (success or failure) completes, execute this saver.
|
|
157
|
+
if (saver)
|
|
158
|
+
return saver(key, value);
|
|
159
|
+
})
|
|
160
|
+
.then(() => {
|
|
161
|
+
// After saver completes, the final value of this Promise chain is the new value.
|
|
162
|
+
return value;
|
|
163
|
+
});
|
|
164
|
+
let node = this.dataMap.get(key);
|
|
165
|
+
if (!node) {
|
|
166
|
+
node = new Node(key, saverPromise);
|
|
167
|
+
this.dataMap.set(key, node);
|
|
168
|
+
this._addToHead(node);
|
|
169
|
+
this._evictIfNeeded();
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
node.value = saverPromise;
|
|
173
|
+
this._moveToHead(node);
|
|
174
|
+
}
|
|
175
|
+
saverPromise.catch(err => {
|
|
176
|
+
var _a;
|
|
177
|
+
console.error(`[AsyncLRUCache ERROR] Saver operation for key ${key} failed. Removing from cache.`, err);
|
|
178
|
+
if (((_a = this.dataMap.get(key)) === null || _a === void 0 ? void 0 : _a.value) === saverPromise) {
|
|
179
|
+
this.dataMap.delete(key);
|
|
180
|
+
this._removeNode(node);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
return saverPromise;
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Invalidates and removes the cache entry associated with the specified key.
|
|
187
|
+
* @param key - The key of the cache entry to invalidate.
|
|
188
|
+
*/
|
|
189
|
+
invalidate(key) {
|
|
190
|
+
const node = this.dataMap.get(key);
|
|
191
|
+
if (node) {
|
|
192
|
+
this._removeNode(node);
|
|
193
|
+
this.dataMap.delete(key);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Clears the entire cache, removing all entries.
|
|
198
|
+
*/
|
|
199
|
+
clear() {
|
|
200
|
+
this.dataMap.clear();
|
|
201
|
+
// Manually clear all node links to avoid any potential memory leaks.
|
|
202
|
+
let node = this.head;
|
|
203
|
+
while (node !== null) {
|
|
204
|
+
// Save reference to the next node, as we are about to break the current node's next link.
|
|
205
|
+
const next = node.next;
|
|
206
|
+
// Break all internal and external links of the current node and clear data.
|
|
207
|
+
node.prev = null;
|
|
208
|
+
node.next = null;
|
|
209
|
+
node.key = null;
|
|
210
|
+
node.value = Promise.resolve(undefined);
|
|
211
|
+
// Move to the next node.
|
|
212
|
+
node = next;
|
|
213
|
+
}
|
|
214
|
+
this.head = null;
|
|
215
|
+
this.tail = null;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
exports.AsyncLRUCache = AsyncLRUCache;
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@wfp99/async-lru-cache",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A simple memory cache using the LRU algorithm and supporting asynchronous data access.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"cache",
|
|
7
|
+
"LRU",
|
|
8
|
+
"async",
|
|
9
|
+
"TypeScript",
|
|
10
|
+
"node.js"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"author": "Wang Feng Ping <wfp@nebuis.com>",
|
|
14
|
+
"type": "commonjs",
|
|
15
|
+
"main": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"files": [
|
|
18
|
+
"dist/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README.zh-TW.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "git+https://github.com/wfp99/async-lru-cache.git"
|
|
26
|
+
},
|
|
27
|
+
"bugs": {
|
|
28
|
+
"url": "https://github.com/wfp99/async-lru-cache/issues"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/wfp99/async-lru-cache#readme",
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=14.0.0"
|
|
33
|
+
},
|
|
34
|
+
"scripts": {
|
|
35
|
+
"build": "tsc",
|
|
36
|
+
"clean": "rm -rf dist",
|
|
37
|
+
"prepublishOnly": "npm run clean && npm run build",
|
|
38
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^24.0.7",
|
|
42
|
+
"typescript": "^5.8.3"
|
|
43
|
+
}
|
|
44
|
+
}
|