@node-ts-cache/lru-redis-storage 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 +208 -0
- package/dist/LRUWithRedisStorage.d.ts +21 -0
- package/dist/LRUWithRedisStorage.js +52 -0
- package/dist/LRUWithRedisStorage.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/package.json +57 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2017 Himmet Avsar
|
|
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,208 @@
|
|
|
1
|
+
# @node-ts-cache/lru-redis-storage
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.org/package/@node-ts-cache/lru-redis-storage)
|
|
4
|
+
|
|
5
|
+
Two-tier cache storage adapter for [@node-ts-cache/core](https://www.npmjs.com/package/@node-ts-cache/core) combining local LRU cache with remote Redis fallback.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Two-tier architecture**: Fast local LRU cache + shared Redis backend
|
|
10
|
+
- Local cache for hot data (sub-millisecond access)
|
|
11
|
+
- Redis fallback for cache misses
|
|
12
|
+
- Automatic population of local cache from Redis hits
|
|
13
|
+
- Reduced Redis round-trips
|
|
14
|
+
- Ideal for high-traffic, distributed applications
|
|
15
|
+
|
|
16
|
+
## How It Works
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
┌─────────────────────────────────────────────────────────┐
|
|
20
|
+
│ Application │
|
|
21
|
+
│ │ │
|
|
22
|
+
│ ▼ │
|
|
23
|
+
│ ┌───────────────────────┐ │
|
|
24
|
+
│ │ LRUWithRedisStorage │ │
|
|
25
|
+
│ └───────────┬───────────┘ │
|
|
26
|
+
│ │ │
|
|
27
|
+
│ ┌─────────────┴─────────────┐ │
|
|
28
|
+
│ ▼ ▼ │
|
|
29
|
+
│ ┌─────────────────┐ ┌─────────────────┐ │
|
|
30
|
+
│ │ Local LRU │ miss │ Redis │ │
|
|
31
|
+
│ │ (in-memory) │ ──────> │ (remote) │ │
|
|
32
|
+
│ │ ~0.01ms │ <────── │ ~1-5ms │ │
|
|
33
|
+
│ └─────────────────┘ hit └─────────────────┘ │
|
|
34
|
+
│ (populate) │
|
|
35
|
+
└─────────────────────────────────────────────────────────┘
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
1. **Get**: Check local LRU first. On miss, check Redis. If found in Redis, populate local LRU.
|
|
39
|
+
2. **Set**: Write to both local LRU and Redis.
|
|
40
|
+
3. **Clear**: Clear local LRU cache.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm install @node-ts-cache/core @node-ts-cache/lru-redis-storage ioredis
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Usage
|
|
49
|
+
|
|
50
|
+
### Basic Usage
|
|
51
|
+
|
|
52
|
+
```typescript
|
|
53
|
+
import { Cache, ExpirationStrategy } from '@node-ts-cache/core';
|
|
54
|
+
import { LRUWithRedisStorage } from '@node-ts-cache/lru-redis-storage';
|
|
55
|
+
import Redis from 'ioredis';
|
|
56
|
+
|
|
57
|
+
const redisClient = new Redis({
|
|
58
|
+
host: 'localhost',
|
|
59
|
+
port: 6379
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
const storage = new LRUWithRedisStorage(
|
|
63
|
+
{ max: 1000 }, // LRU options: max 1000 items locally
|
|
64
|
+
() => redisClient // Redis client factory
|
|
65
|
+
);
|
|
66
|
+
|
|
67
|
+
const strategy = new ExpirationStrategy(storage);
|
|
68
|
+
|
|
69
|
+
class UserService {
|
|
70
|
+
@Cache(strategy, { ttl: 300 })
|
|
71
|
+
async getUser(id: string): Promise<User> {
|
|
72
|
+
return await db.users.findById(id);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### With TTL
|
|
78
|
+
|
|
79
|
+
```typescript
|
|
80
|
+
const storage = new LRUWithRedisStorage(
|
|
81
|
+
{
|
|
82
|
+
max: 500,
|
|
83
|
+
ttl: 60 // Local and Redis cache TTL: 1 minute (in seconds)
|
|
84
|
+
},
|
|
85
|
+
() => redisClient
|
|
86
|
+
);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
### Direct API Usage
|
|
90
|
+
|
|
91
|
+
```typescript
|
|
92
|
+
const storage = new LRUWithRedisStorage({ max: 100 }, () => redisClient);
|
|
93
|
+
const strategy = new ExpirationStrategy(storage);
|
|
94
|
+
|
|
95
|
+
// Store (writes to both LRU and Redis)
|
|
96
|
+
await strategy.setItem('user:123', { name: 'John' }, { ttl: 60 });
|
|
97
|
+
|
|
98
|
+
// First get - might hit Redis if not in LRU
|
|
99
|
+
const user1 = await strategy.getItem<User>('user:123');
|
|
100
|
+
|
|
101
|
+
// Second get - hits local LRU (fast!)
|
|
102
|
+
const user2 = await strategy.getItem<User>('user:123');
|
|
103
|
+
|
|
104
|
+
// Clear local LRU cache
|
|
105
|
+
await strategy.clear();
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Constructor
|
|
109
|
+
|
|
110
|
+
```typescript
|
|
111
|
+
new LRUWithRedisStorage(
|
|
112
|
+
options: LRUWithRedisStorageOptions,
|
|
113
|
+
redis: () => Redis.Redis
|
|
114
|
+
)
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
| Parameter | Type | Description |
|
|
118
|
+
| --------- | ---------------------------- | ----------------------------------------- |
|
|
119
|
+
| `options` | `LRUWithRedisStorageOptions` | Options for local LRU cache and TTL |
|
|
120
|
+
| `redis` | `() => Redis.Redis` | Factory function returning ioredis client |
|
|
121
|
+
|
|
122
|
+
### Options
|
|
123
|
+
|
|
124
|
+
| Option | Type | Default | Description |
|
|
125
|
+
| ------ | -------- | ------- | -------------------------------------------------- |
|
|
126
|
+
| `max` | `number` | `500` | Maximum items in local LRU cache |
|
|
127
|
+
| `ttl` | `number` | `86400` | Time to live in **seconds** (for both LRU & Redis) |
|
|
128
|
+
|
|
129
|
+
## Interface
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
interface IAsynchronousCacheType {
|
|
133
|
+
getItem<T>(key: string): Promise<T | undefined>;
|
|
134
|
+
setItem(key: string, content: any, options?: any): Promise<void>;
|
|
135
|
+
clear(): Promise<void>;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
## Use Cases
|
|
140
|
+
|
|
141
|
+
### High-Traffic APIs
|
|
142
|
+
|
|
143
|
+
```typescript
|
|
144
|
+
class ProductAPI {
|
|
145
|
+
@Cache(strategy, { ttl: 60 })
|
|
146
|
+
async getProduct(id: string): Promise<Product> {
|
|
147
|
+
// Hot products served from local LRU (~0.01ms)
|
|
148
|
+
// Cold products fetched from Redis (~1-5ms)
|
|
149
|
+
// Very cold products hit database
|
|
150
|
+
return await db.products.findById(id);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### Distributed Systems
|
|
156
|
+
|
|
157
|
+
Multiple application instances share the same Redis cache while maintaining their own local LRU:
|
|
158
|
+
|
|
159
|
+
```
|
|
160
|
+
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
|
|
161
|
+
│ Instance 1 │ │ Instance 2 │ │ Instance 3 │
|
|
162
|
+
│ ┌────────┐ │ │ ┌────────┐ │ │ ┌────────┐ │
|
|
163
|
+
│ │ LRU │ │ │ │ LRU │ │ │ │ LRU │ │
|
|
164
|
+
│ └────┬───┘ │ │ └────┬───┘ │ │ └────┬───┘ │
|
|
165
|
+
└───────┼──────┘ └───────┼──────┘ └───────┼──────┘
|
|
166
|
+
│ │ │
|
|
167
|
+
└────────────────────┼────────────────────┘
|
|
168
|
+
│
|
|
169
|
+
┌──────┴──────┐
|
|
170
|
+
│ Redis │
|
|
171
|
+
│ (shared) │
|
|
172
|
+
└─────────────┘
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Session Caching
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
class SessionService {
|
|
179
|
+
@Cache(strategy, { ttl: 1800 }) // 30 minutes
|
|
180
|
+
async getSession(token: string): Promise<Session> {
|
|
181
|
+
// Active sessions stay in local LRU
|
|
182
|
+
// Inactive sessions fall back to Redis
|
|
183
|
+
return await db.sessions.findByToken(token);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
## Performance Considerations
|
|
189
|
+
|
|
190
|
+
- **Local LRU hit**: ~0.01ms (in-process memory access)
|
|
191
|
+
- **Redis hit**: ~1-5ms (network round-trip)
|
|
192
|
+
- **Set local `max`** based on your memory budget and access patterns
|
|
193
|
+
- **Shorter TTL** = fresher data but more Redis hits
|
|
194
|
+
- **Longer TTL** = better performance but potentially stale data
|
|
195
|
+
|
|
196
|
+
## Dependencies
|
|
197
|
+
|
|
198
|
+
- `lru-cache` ^10.0.0
|
|
199
|
+
- `ioredis` ^5.3.2
|
|
200
|
+
|
|
201
|
+
## Requirements
|
|
202
|
+
|
|
203
|
+
- Node.js >= 18.0.0
|
|
204
|
+
- Redis server
|
|
205
|
+
|
|
206
|
+
## License
|
|
207
|
+
|
|
208
|
+
MIT
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { IAsynchronousCacheType } from '@node-ts-cache/core';
|
|
2
|
+
import * as Redis from 'ioredis';
|
|
3
|
+
export interface LRUWithRedisStorageOptions {
|
|
4
|
+
/** Maximum number of items in local LRU cache */
|
|
5
|
+
max?: number;
|
|
6
|
+
/** Time to live in seconds for both local and Redis cache */
|
|
7
|
+
ttl?: number;
|
|
8
|
+
}
|
|
9
|
+
export declare class LRUWithRedisStorage implements IAsynchronousCacheType {
|
|
10
|
+
private redis;
|
|
11
|
+
private myCache;
|
|
12
|
+
/** ttl in seconds! */
|
|
13
|
+
private options;
|
|
14
|
+
constructor(options: LRUWithRedisStorageOptions, redis: () => Redis.Redis);
|
|
15
|
+
getItem<T>(key: string): Promise<T | undefined>;
|
|
16
|
+
/** ttl in seconds! */
|
|
17
|
+
setItem<T = unknown>(key: string, content: T | undefined, options?: {
|
|
18
|
+
ttl?: number;
|
|
19
|
+
}): Promise<void>;
|
|
20
|
+
clear(): Promise<void>;
|
|
21
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { LRUCache } from 'lru-cache';
|
|
2
|
+
export class LRUWithRedisStorage {
|
|
3
|
+
constructor(options, redis) {
|
|
4
|
+
this.redis = redis;
|
|
5
|
+
this.options = {
|
|
6
|
+
max: 500,
|
|
7
|
+
ttl: 86400,
|
|
8
|
+
...options
|
|
9
|
+
};
|
|
10
|
+
this.myCache = new LRUCache({
|
|
11
|
+
max: this.options.max,
|
|
12
|
+
ttl: this.options.ttl * 1000 // convert to ms
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
async getItem(key) {
|
|
16
|
+
// check local cache
|
|
17
|
+
let localCache = this.myCache.get(key);
|
|
18
|
+
if (localCache === undefined) {
|
|
19
|
+
// check central cache
|
|
20
|
+
const redisValue = await this.redis().get(key);
|
|
21
|
+
if (redisValue !== null) {
|
|
22
|
+
try {
|
|
23
|
+
localCache = JSON.parse(redisValue);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.error('lru redis cache failed parsing data', err);
|
|
27
|
+
localCache = undefined;
|
|
28
|
+
}
|
|
29
|
+
// if found on central cache, copy it to a local cache
|
|
30
|
+
if (localCache !== undefined) {
|
|
31
|
+
this.myCache.set(key, localCache);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return localCache;
|
|
36
|
+
}
|
|
37
|
+
/** ttl in seconds! */
|
|
38
|
+
async setItem(key, content, options) {
|
|
39
|
+
this.myCache.set(key, content);
|
|
40
|
+
const ttl = options?.ttl || this.options.ttl;
|
|
41
|
+
if (ttl) {
|
|
42
|
+
await this.redis().setex(key, ttl, JSON.stringify(content));
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
await this.redis().set(key, JSON.stringify(content));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
async clear() {
|
|
49
|
+
this.myCache.clear();
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=LRUWithRedisStorage.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LRUWithRedisStorage.js","sourceRoot":"","sources":["../src/LRUWithRedisStorage.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAUrC,MAAM,OAAO,mBAAmB;IAQ/B,YAAY,OAAmC,EAAU,KAAwB;QAAxB,UAAK,GAAL,KAAK,CAAmB;QAChF,IAAI,CAAC,OAAO,GAAG;YACd,GAAG,EAAE,GAAG;YACR,GAAG,EAAE,KAAK;YACV,GAAG,OAAO;SACV,CAAC;QACF,IAAI,CAAC,OAAO,GAAG,IAAI,QAAQ,CAAC;YAC3B,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG;YACrB,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,gBAAgB;SAC7C,CAAC,CAAC;IACJ,CAAC;IAEM,KAAK,CAAC,OAAO,CAAI,GAAW;QAClC,oBAAoB;QACpB,IAAI,UAAU,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAEvC,IAAI,UAAU,KAAK,SAAS,EAAE;YAC7B,sBAAsB;YACtB,MAAM,UAAU,GAAG,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;YAE/C,IAAI,UAAU,KAAK,IAAI,EAAE;gBACxB,IAAI;oBACH,UAAU,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,CAAC;iBACpC;gBAAC,OAAO,GAAG,EAAE;oBACb,OAAO,CAAC,KAAK,CAAC,qCAAqC,EAAE,GAAG,CAAC,CAAC;oBAC1D,UAAU,GAAG,SAAS,CAAC;iBACvB;gBACD,sDAAsD;gBACtD,IAAI,UAAU,KAAK,SAAS,EAAE;oBAC7B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;iBAClC;aACD;SACD;QAED,OAAO,UAA2B,CAAC;IACpC,CAAC;IAED,sBAAsB;IACf,KAAK,CAAC,OAAO,CACnB,GAAW,EACX,OAAsB,EACtB,OAA0B;QAE1B,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QAC/B,MAAM,GAAG,GAAG,OAAO,EAAE,GAAG,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC;QAC7C,IAAI,GAAG,EAAE;YACR,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;SAC5D;aAAM;YACN,MAAM,IAAI,CAAC,KAAK,EAAE,CAAC,GAAG,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;SACrD;IACF,CAAC;IAEM,KAAK,CAAC,KAAK;QACjB,IAAI,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC;IACtB,CAAC;CACD"}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAE/D,eAAe,mBAAmB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@node-ts-cache/lru-redis-storage",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Simple and extensible caching module supporting decorators",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"node",
|
|
7
|
+
"nodejs",
|
|
8
|
+
"cache",
|
|
9
|
+
"typescript",
|
|
10
|
+
"ts",
|
|
11
|
+
"caching",
|
|
12
|
+
"memcache",
|
|
13
|
+
"memory-cache",
|
|
14
|
+
"redis-cache",
|
|
15
|
+
"redis",
|
|
16
|
+
"file-cache",
|
|
17
|
+
"node-cache",
|
|
18
|
+
"ts-cache"
|
|
19
|
+
],
|
|
20
|
+
"homepage": "https://github.com/simllll/node-ts-cache/tree/master/storages/lru-redis#readme",
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/simllll/node-ts-cache/issues"
|
|
23
|
+
},
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "git+https://github.com/simllll/node-ts-cache.git"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": "Simon Tretter <s.tretter@gmail.com>",
|
|
30
|
+
"type": "module",
|
|
31
|
+
"main": "dist/index.js",
|
|
32
|
+
"types": "dist/index.d.ts",
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"ioredis": "^5.3.2",
|
|
38
|
+
"lru-cache": "^10.0.0",
|
|
39
|
+
"@node-ts-cache/core": "1.0.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"ioredis-mock": "^8.9.0"
|
|
43
|
+
},
|
|
44
|
+
"engines": {
|
|
45
|
+
"node": ">=18.0.0"
|
|
46
|
+
},
|
|
47
|
+
"publishConfig": {
|
|
48
|
+
"access": "public"
|
|
49
|
+
},
|
|
50
|
+
"gitHead": "c938eba762060f940a34bc192bec03bc76ea4017",
|
|
51
|
+
"scripts": {
|
|
52
|
+
"build": "tsc -p .",
|
|
53
|
+
"clean": "git clean -fdx src",
|
|
54
|
+
"dev": "tsc -p . -w",
|
|
55
|
+
"test": "mocha --loader=ts-node/esm test/**/*.test.ts"
|
|
56
|
+
}
|
|
57
|
+
}
|