ts-cache-mongoose 2.1.0 → 2.2.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/README.md +54 -1
- package/dist/index.cjs +73 -17
- package/dist/index.d.cts +4 -0
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.mts +4 -0
- package/dist/index.d.mts.map +1 -1
- package/dist/index.mjs +73 -17
- package/dist/nest/index.cjs +1 -0
- package/dist/nest/index.mjs +1 -0
- package/package.json +20 -26
- package/biome.json +0 -47
- package/src/cache/Cache.ts +0 -72
- package/src/cache/engine/MemoryCacheEngine.ts +0 -41
- package/src/cache/engine/RedisCacheEngine.ts +0 -52
- package/src/extend/aggregate.ts +0 -51
- package/src/extend/query.ts +0 -81
- package/src/index.ts +0 -68
- package/src/key.ts +0 -10
- package/src/ms.ts +0 -66
- package/src/nest/cache.module.ts +0 -79
- package/src/nest/cache.service.ts +0 -37
- package/src/nest/index.ts +0 -4
- package/src/nest/interfaces.ts +0 -17
- package/src/sort-keys.ts +0 -38
- package/src/types.ts +0 -21
- package/src/version.ts +0 -18
- package/tests/cache-debug.test.ts +0 -73
- package/tests/cache-memory.test.ts +0 -217
- package/tests/cache-options.test.ts +0 -83
- package/tests/cache-redis.test.ts +0 -521
- package/tests/key.test.ts +0 -103
- package/tests/models/Story.ts +0 -29
- package/tests/models/User.ts +0 -39
- package/tests/mongo/.gitignore +0 -3
- package/tests/mongo/server.ts +0 -29
- package/tests/ms.test.ts +0 -113
- package/tests/nest.test.ts +0 -158
- package/tests/sort-keys.test.ts +0 -80
- package/tsconfig.json +0 -33
- package/vite.config.mts +0 -23
package/README.md
CHANGED
|
@@ -12,6 +12,10 @@ Cache query and aggregate in mongoose using in-memory or redis
|
|
|
12
12
|
[](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-cache-mongoose)
|
|
13
13
|
[](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-cache-mongoose)
|
|
14
14
|
[](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-cache-mongoose)
|
|
15
|
+
\
|
|
16
|
+
[](https://socket.dev/npm/package/ts-cache-mongoose)
|
|
17
|
+
[](https://securityscorecards.dev/viewer/?uri=github.com/ilovepixelart/ts-cache-mongoose)
|
|
18
|
+
[](https://www.bestpractices.dev/en/projects/12484)
|
|
15
19
|
|
|
16
20
|
## Motivation
|
|
17
21
|
|
|
@@ -24,10 +28,12 @@ I need a way to cache mongoose queries and aggregations to improve application p
|
|
|
24
28
|
```json
|
|
25
29
|
{
|
|
26
30
|
"node": "20.x || 22.x || 24.x",
|
|
27
|
-
"mongoose": ">=6.6.
|
|
31
|
+
"mongoose": ">=6.6.0 <10"
|
|
28
32
|
}
|
|
29
33
|
```
|
|
30
34
|
|
|
35
|
+
CI tests against mongoose `6.12.2`, `7.6.4`, `8.23.0`, and `9.4.1`.
|
|
36
|
+
|
|
31
37
|
## Features
|
|
32
38
|
|
|
33
39
|
- In-memory caching
|
|
@@ -96,6 +102,53 @@ const books = await Book.aggregate([
|
|
|
96
102
|
]).cache('1 minute').exec()
|
|
97
103
|
```
|
|
98
104
|
|
|
105
|
+
### Bounded in-memory cache
|
|
106
|
+
|
|
107
|
+
The in-memory engine is unbounded by default. For workloads where query keys are driven by user input (search, filters, pagination), cap the cache so a caller generating unique cache keys cannot grow the map without limit. Two bounds are available and can be combined — eviction is LRU, and whichever bound is hit first triggers it:
|
|
108
|
+
|
|
109
|
+
```typescript
|
|
110
|
+
cache.init(mongoose, {
|
|
111
|
+
engine: 'memory',
|
|
112
|
+
defaultTTL: '60 seconds',
|
|
113
|
+
maxEntries: 10_000, // cap by entry count
|
|
114
|
+
maxBytes: 50 * 1024 * 1024, // cap by serialized bytes (50 MB)
|
|
115
|
+
})
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`maxBytes` measures entry size via `node:v8.serialize(value).byteLength` by default — handles circular references (mongoose `populate` parent-refs), single C++ call per `set`, works on Node / Bun / Deno. Provide your own `sizeCalculation` callback if you want an O(1) estimate instead:
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
cache.init(mongoose, {
|
|
122
|
+
engine: 'memory',
|
|
123
|
+
maxBytes: 50 * 1024 * 1024,
|
|
124
|
+
sizeCalculation: (value) => {
|
|
125
|
+
if (Array.isArray(value)) return value.length * 512
|
|
126
|
+
return 512
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
Eviction is soft: the just-written entry is never dropped, even if its own size exceeds `maxBytes`. Everything older gets evicted until both bounds are satisfied (or only the new entry remains).
|
|
132
|
+
|
|
133
|
+
Both options are ignored for the Redis engine — use Redis's own `maxmemory` + `maxmemory-policy` instead.
|
|
134
|
+
|
|
135
|
+
### Custom error handling
|
|
136
|
+
|
|
137
|
+
By default, cache engine failures (Redis disconnects, serialization errors, etc.) are logged via `console.error` and the query falls through to the database. Pass an `onError` callback to route them somewhere else — e.g. a structured logger, Sentry, or a metric counter:
|
|
138
|
+
|
|
139
|
+
```typescript
|
|
140
|
+
cache.init(mongoose, {
|
|
141
|
+
engine: 'redis',
|
|
142
|
+
defaultTTL: '60 seconds',
|
|
143
|
+
engineOptions: { host: 'localhost', port: 6379 },
|
|
144
|
+
onError: (error) => {
|
|
145
|
+
logger.warn({ err: error }, 'cache engine failure')
|
|
146
|
+
},
|
|
147
|
+
})
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
The callback receives the raw `Error`. Cache reads and writes never throw — a failing engine degrades to a cache miss.
|
|
151
|
+
|
|
99
152
|
### Cache invalidation
|
|
100
153
|
|
|
101
154
|
```typescript
|
package/dist/index.cjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
var node_v8 = require('node:v8');
|
|
3
4
|
var bson = require('bson');
|
|
4
5
|
var IORedis = require('ioredis');
|
|
5
6
|
var mongoose = require('mongoose');
|
|
@@ -60,32 +61,66 @@ const ms = (val) => {
|
|
|
60
61
|
return n * (UNITS[type] ?? 0);
|
|
61
62
|
};
|
|
62
63
|
|
|
64
|
+
const defaultSizer = (value) => node_v8.serialize(value).byteLength;
|
|
63
65
|
class MemoryCacheEngine {
|
|
64
66
|
#cache;
|
|
65
|
-
|
|
67
|
+
#maxEntries;
|
|
68
|
+
#maxBytes;
|
|
69
|
+
#sizeOf;
|
|
70
|
+
#totalBytes;
|
|
71
|
+
constructor(options) {
|
|
66
72
|
this.#cache = /* @__PURE__ */ new Map();
|
|
73
|
+
this.#maxEntries = options?.maxEntries != null && options.maxEntries > 0 ? options.maxEntries : Number.POSITIVE_INFINITY;
|
|
74
|
+
this.#maxBytes = options?.maxBytes != null && options.maxBytes > 0 ? options.maxBytes : Number.POSITIVE_INFINITY;
|
|
75
|
+
this.#sizeOf = options?.sizeCalculation ?? defaultSizer;
|
|
76
|
+
this.#totalBytes = 0;
|
|
77
|
+
}
|
|
78
|
+
get totalBytes() {
|
|
79
|
+
return this.#totalBytes;
|
|
80
|
+
}
|
|
81
|
+
get size() {
|
|
82
|
+
return this.#cache.size;
|
|
67
83
|
}
|
|
68
84
|
get(key) {
|
|
69
85
|
const item = this.#cache.get(key);
|
|
70
|
-
if (!item
|
|
71
|
-
|
|
86
|
+
if (!item) return void 0;
|
|
87
|
+
if (item.expiresAt < Date.now()) {
|
|
88
|
+
this.#cache.delete(key);
|
|
89
|
+
this.#totalBytes -= item.bytes;
|
|
72
90
|
return void 0;
|
|
73
91
|
}
|
|
92
|
+
this.#cache.delete(key);
|
|
93
|
+
this.#cache.set(key, item);
|
|
74
94
|
return item.value;
|
|
75
95
|
}
|
|
76
96
|
set(key, value, ttl) {
|
|
77
97
|
const givenTTL = ttl == null ? void 0 : ms(ttl);
|
|
78
98
|
const actualTTL = givenTTL ?? Number.POSITIVE_INFINITY;
|
|
79
|
-
this.#cache.
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
99
|
+
const existing = this.#cache.get(key);
|
|
100
|
+
if (existing) {
|
|
101
|
+
this.#cache.delete(key);
|
|
102
|
+
this.#totalBytes -= existing.bytes;
|
|
103
|
+
}
|
|
104
|
+
const bytes = this.#sizeOf(value);
|
|
105
|
+
this.#cache.set(key, { value, expiresAt: Date.now() + actualTTL, bytes });
|
|
106
|
+
this.#totalBytes += bytes;
|
|
107
|
+
while ((this.#cache.size > this.#maxEntries || this.#totalBytes > this.#maxBytes) && this.#cache.size > 1) {
|
|
108
|
+
const oldestKey = this.#cache.keys().next().value;
|
|
109
|
+
if (oldestKey === void 0 || oldestKey === key) break;
|
|
110
|
+
const oldest = this.#cache.get(oldestKey);
|
|
111
|
+
this.#cache.delete(oldestKey);
|
|
112
|
+
if (oldest) this.#totalBytes -= oldest.bytes;
|
|
113
|
+
}
|
|
83
114
|
}
|
|
84
115
|
del(key) {
|
|
116
|
+
const item = this.#cache.get(key);
|
|
117
|
+
if (!item) return;
|
|
85
118
|
this.#cache.delete(key);
|
|
119
|
+
this.#totalBytes -= item.bytes;
|
|
86
120
|
}
|
|
87
121
|
clear() {
|
|
88
122
|
this.#cache.clear();
|
|
123
|
+
this.#totalBytes = 0;
|
|
89
124
|
}
|
|
90
125
|
close() {
|
|
91
126
|
}
|
|
@@ -106,9 +141,11 @@ const convertToObject = (value) => {
|
|
|
106
141
|
|
|
107
142
|
class RedisCacheEngine {
|
|
108
143
|
#client;
|
|
109
|
-
|
|
144
|
+
#onError;
|
|
145
|
+
constructor(options, onError) {
|
|
110
146
|
options.keyPrefix ??= "cache-mongoose:";
|
|
111
147
|
this.#client = new IORedis(options);
|
|
148
|
+
this.#onError = onError;
|
|
112
149
|
}
|
|
113
150
|
async get(key) {
|
|
114
151
|
try {
|
|
@@ -118,18 +155,22 @@ class RedisCacheEngine {
|
|
|
118
155
|
}
|
|
119
156
|
return bson.EJSON.parse(value);
|
|
120
157
|
} catch (err) {
|
|
121
|
-
|
|
158
|
+
this.#onError(err);
|
|
122
159
|
return void 0;
|
|
123
160
|
}
|
|
124
161
|
}
|
|
125
162
|
async set(key, value, ttl) {
|
|
126
163
|
try {
|
|
164
|
+
const converted = convertToObject(value);
|
|
165
|
+
if (converted === void 0) {
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
127
168
|
const givenTTL = ttl == null ? void 0 : ms(ttl);
|
|
128
169
|
const actualTTL = givenTTL ?? Number.POSITIVE_INFINITY;
|
|
129
|
-
const serializedValue = bson.EJSON.stringify(
|
|
170
|
+
const serializedValue = bson.EJSON.stringify(converted);
|
|
130
171
|
await this.#client.setex(key, Math.ceil(actualTTL / 1e3), serializedValue);
|
|
131
172
|
} catch (err) {
|
|
132
|
-
|
|
173
|
+
this.#onError(err);
|
|
133
174
|
}
|
|
134
175
|
}
|
|
135
176
|
async del(key) {
|
|
@@ -147,6 +188,7 @@ class Cache {
|
|
|
147
188
|
#engine;
|
|
148
189
|
#defaultTTL;
|
|
149
190
|
#debug;
|
|
191
|
+
#onError;
|
|
150
192
|
#engines = ["memory", "redis"];
|
|
151
193
|
constructor(cacheOptions) {
|
|
152
194
|
if (!this.#engines.includes(cacheOptions.engine)) {
|
|
@@ -157,14 +199,22 @@ class Cache {
|
|
|
157
199
|
}
|
|
158
200
|
cacheOptions.defaultTTL ??= "1 minute";
|
|
159
201
|
this.#defaultTTL = ms(cacheOptions.defaultTTL);
|
|
202
|
+
this.#onError = cacheOptions.onError ?? console.error;
|
|
160
203
|
if (cacheOptions.engine === "redis" && cacheOptions.engineOptions) {
|
|
161
|
-
this.#engine = new RedisCacheEngine(cacheOptions.engineOptions);
|
|
204
|
+
this.#engine = new RedisCacheEngine(cacheOptions.engineOptions, this.#onError);
|
|
162
205
|
}
|
|
163
206
|
if (cacheOptions.engine === "memory") {
|
|
164
|
-
this.#engine = new MemoryCacheEngine(
|
|
207
|
+
this.#engine = new MemoryCacheEngine({
|
|
208
|
+
maxEntries: cacheOptions.maxEntries,
|
|
209
|
+
maxBytes: cacheOptions.maxBytes,
|
|
210
|
+
sizeCalculation: cacheOptions.sizeCalculation
|
|
211
|
+
});
|
|
165
212
|
}
|
|
166
213
|
this.#debug = cacheOptions.debug === true;
|
|
167
214
|
}
|
|
215
|
+
get onError() {
|
|
216
|
+
return this.#onError;
|
|
217
|
+
}
|
|
168
218
|
async get(key) {
|
|
169
219
|
const cacheEntry = await this.#engine.get(key);
|
|
170
220
|
if (this.#debug) {
|
|
@@ -176,6 +226,12 @@ class Cache {
|
|
|
176
226
|
async set(key, value, ttl) {
|
|
177
227
|
const givenTTL = ttl == null ? null : ms(ttl);
|
|
178
228
|
const actualTTL = givenTTL ?? this.#defaultTTL;
|
|
229
|
+
if (Number.isNaN(actualTTL) || actualTTL <= 0) {
|
|
230
|
+
if (this.#debug) {
|
|
231
|
+
console.log(`[ts-cache-mongoose] SET '${key}' - skipped (non-positive ttl: ${String(actualTTL)} ms)`);
|
|
232
|
+
}
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
179
235
|
await this.#engine.set(key, value, actualTTL);
|
|
180
236
|
if (this.#debug) {
|
|
181
237
|
console.log(`[ts-cache-mongoose] SET '${key}' - ttl: ${actualTTL.toFixed(0)} ms`);
|
|
@@ -263,14 +319,14 @@ function extendAggregate(mongoose, cache) {
|
|
|
263
319
|
const key = this.getCacheKey();
|
|
264
320
|
const ttl = this.getDuration();
|
|
265
321
|
const resultCache = await cache.get(key).catch((err) => {
|
|
266
|
-
|
|
322
|
+
cache.onError(err);
|
|
267
323
|
});
|
|
268
324
|
if (resultCache) {
|
|
269
325
|
return resultCache;
|
|
270
326
|
}
|
|
271
327
|
const result = await mongooseExec.call(this);
|
|
272
328
|
await cache.set(key, result, ttl).catch((err) => {
|
|
273
|
-
|
|
329
|
+
cache.onError(err);
|
|
274
330
|
});
|
|
275
331
|
return result;
|
|
276
332
|
};
|
|
@@ -316,7 +372,7 @@ function extendQuery(mongoose, cache) {
|
|
|
316
372
|
const isDistinct = this.op === "distinct";
|
|
317
373
|
const model = this.model.modelName;
|
|
318
374
|
const resultCache = await cache.get(key).catch((err) => {
|
|
319
|
-
|
|
375
|
+
cache.onError(err);
|
|
320
376
|
});
|
|
321
377
|
if (resultCache) {
|
|
322
378
|
if (isCount || isDistinct || mongooseOptions.lean) {
|
|
@@ -332,7 +388,7 @@ function extendQuery(mongoose, cache) {
|
|
|
332
388
|
}
|
|
333
389
|
const result = await mongooseExec.call(this);
|
|
334
390
|
await cache.set(key, result, ttl).catch((err) => {
|
|
335
|
-
|
|
391
|
+
cache.onError(err);
|
|
336
392
|
});
|
|
337
393
|
return result;
|
|
338
394
|
};
|
package/dist/index.d.cts
CHANGED
|
@@ -46,6 +46,10 @@ type CacheOptions = {
|
|
|
46
46
|
engineOptions?: RedisOptions;
|
|
47
47
|
defaultTTL?: Duration;
|
|
48
48
|
debug?: boolean;
|
|
49
|
+
onError?: (error: Error) => void;
|
|
50
|
+
maxEntries?: number;
|
|
51
|
+
maxBytes?: number;
|
|
52
|
+
sizeCalculation?: (value: CacheData) => number;
|
|
49
53
|
};
|
|
50
54
|
interface CacheEngine {
|
|
51
55
|
get: (key: string) => Promise<CacheData> | CacheData;
|
package/dist/index.d.cts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.cts","sources":["../src/ms.ts","../src/types.ts","../src/index.ts"],"mappings":";;;AAQA,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC1C9E,KAAM,SAAS,GAAG,MAAM,oBAAoB,MAAM;AAElD,KAAM,YAAY;;oBAEN,YAAY;iBACf,QAAQ;;;
|
|
1
|
+
{"version":3,"file":"index.d.cts","sources":["../src/ms.ts","../src/types.ts","../src/index.ts"],"mappings":";;;AAQA,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC1C9E,KAAM,SAAS,GAAG,MAAM,oBAAoB,MAAM;AAElD,KAAM,YAAY;;oBAEN,YAAY;iBACf,QAAQ;;sBAEH,KAAK;;;8BAGG,SAAS;;AAG/B,UAAW,WAAW;0BACJ,OAAO,CAAC,SAAS,IAAI,SAAS;8BAC1B,SAAS,QAAQ,QAAQ,KAAK,OAAO;0BACzC,OAAO;iBAChB,OAAO;iBACP,OAAO;;;ACdtB;;8EAE0E,QAAQ;;;cAGxE,QAAQ;iFAC2D,QAAQ;;;;;;;;mDAStC,QAAQ;;;cAG7C,QAAQ;sDACgC,QAAQ;;;AAI1D,cAAM,aAAa;;;;0BAQY,QAAQ,gBAAgB,YAAY,GAAG,aAAa;+BAczC,OAAO;aAQzB,OAAO","names":[]}
|
package/dist/index.d.mts
CHANGED
|
@@ -46,6 +46,10 @@ type CacheOptions = {
|
|
|
46
46
|
engineOptions?: RedisOptions;
|
|
47
47
|
defaultTTL?: Duration;
|
|
48
48
|
debug?: boolean;
|
|
49
|
+
onError?: (error: Error) => void;
|
|
50
|
+
maxEntries?: number;
|
|
51
|
+
maxBytes?: number;
|
|
52
|
+
sizeCalculation?: (value: CacheData) => number;
|
|
49
53
|
};
|
|
50
54
|
interface CacheEngine {
|
|
51
55
|
get: (key: string) => Promise<CacheData> | CacheData;
|
package/dist/index.d.mts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.mts","sources":["../src/ms.ts","../src/types.ts","../src/index.ts"],"mappings":";;;AAQA,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC1C9E,KAAM,SAAS,GAAG,MAAM,oBAAoB,MAAM;AAElD,KAAM,YAAY;;oBAEN,YAAY;iBACf,QAAQ;;;
|
|
1
|
+
{"version":3,"file":"index.d.mts","sources":["../src/ms.ts","../src/types.ts","../src/index.ts"],"mappings":";;;AAQA,cAAa,KAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqCZ,KAAM,IAAI,gBAAgB,KAAK;AAE/B,KAAM,QAAQ,sCAAsC,IAAI,kBAAkB,IAAI;;AC1C9E,KAAM,SAAS,GAAG,MAAM,oBAAoB,MAAM;AAElD,KAAM,YAAY;;oBAEN,YAAY;iBACf,QAAQ;;sBAEH,KAAK;;;8BAGG,SAAS;;AAG/B,UAAW,WAAW;0BACJ,OAAO,CAAC,SAAS,IAAI,SAAS;8BAC1B,SAAS,QAAQ,QAAQ,KAAK,OAAO;0BACzC,OAAO;iBAChB,OAAO;iBACP,OAAO;;;ACdtB;;8EAE0E,QAAQ;;;cAGxE,QAAQ;iFAC2D,QAAQ;;;;;;;;mDAStC,QAAQ;;;cAG7C,QAAQ;sDACgC,QAAQ;;;AAI1D,cAAM,aAAa;;;;0BAQY,QAAQ,gBAAgB,YAAY,GAAG,aAAa;+BAczC,OAAO;aAQzB,OAAO","names":[]}
|
package/dist/index.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { serialize } from 'node:v8';
|
|
1
2
|
import { EJSON } from 'bson';
|
|
2
3
|
import IORedis from 'ioredis';
|
|
3
4
|
import mongoose from 'mongoose';
|
|
@@ -58,32 +59,66 @@ const ms = (val) => {
|
|
|
58
59
|
return n * (UNITS[type] ?? 0);
|
|
59
60
|
};
|
|
60
61
|
|
|
62
|
+
const defaultSizer = (value) => serialize(value).byteLength;
|
|
61
63
|
class MemoryCacheEngine {
|
|
62
64
|
#cache;
|
|
63
|
-
|
|
65
|
+
#maxEntries;
|
|
66
|
+
#maxBytes;
|
|
67
|
+
#sizeOf;
|
|
68
|
+
#totalBytes;
|
|
69
|
+
constructor(options) {
|
|
64
70
|
this.#cache = /* @__PURE__ */ new Map();
|
|
71
|
+
this.#maxEntries = options?.maxEntries != null && options.maxEntries > 0 ? options.maxEntries : Number.POSITIVE_INFINITY;
|
|
72
|
+
this.#maxBytes = options?.maxBytes != null && options.maxBytes > 0 ? options.maxBytes : Number.POSITIVE_INFINITY;
|
|
73
|
+
this.#sizeOf = options?.sizeCalculation ?? defaultSizer;
|
|
74
|
+
this.#totalBytes = 0;
|
|
75
|
+
}
|
|
76
|
+
get totalBytes() {
|
|
77
|
+
return this.#totalBytes;
|
|
78
|
+
}
|
|
79
|
+
get size() {
|
|
80
|
+
return this.#cache.size;
|
|
65
81
|
}
|
|
66
82
|
get(key) {
|
|
67
83
|
const item = this.#cache.get(key);
|
|
68
|
-
if (!item
|
|
69
|
-
|
|
84
|
+
if (!item) return void 0;
|
|
85
|
+
if (item.expiresAt < Date.now()) {
|
|
86
|
+
this.#cache.delete(key);
|
|
87
|
+
this.#totalBytes -= item.bytes;
|
|
70
88
|
return void 0;
|
|
71
89
|
}
|
|
90
|
+
this.#cache.delete(key);
|
|
91
|
+
this.#cache.set(key, item);
|
|
72
92
|
return item.value;
|
|
73
93
|
}
|
|
74
94
|
set(key, value, ttl) {
|
|
75
95
|
const givenTTL = ttl == null ? void 0 : ms(ttl);
|
|
76
96
|
const actualTTL = givenTTL ?? Number.POSITIVE_INFINITY;
|
|
77
|
-
this.#cache.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
97
|
+
const existing = this.#cache.get(key);
|
|
98
|
+
if (existing) {
|
|
99
|
+
this.#cache.delete(key);
|
|
100
|
+
this.#totalBytes -= existing.bytes;
|
|
101
|
+
}
|
|
102
|
+
const bytes = this.#sizeOf(value);
|
|
103
|
+
this.#cache.set(key, { value, expiresAt: Date.now() + actualTTL, bytes });
|
|
104
|
+
this.#totalBytes += bytes;
|
|
105
|
+
while ((this.#cache.size > this.#maxEntries || this.#totalBytes > this.#maxBytes) && this.#cache.size > 1) {
|
|
106
|
+
const oldestKey = this.#cache.keys().next().value;
|
|
107
|
+
if (oldestKey === void 0 || oldestKey === key) break;
|
|
108
|
+
const oldest = this.#cache.get(oldestKey);
|
|
109
|
+
this.#cache.delete(oldestKey);
|
|
110
|
+
if (oldest) this.#totalBytes -= oldest.bytes;
|
|
111
|
+
}
|
|
81
112
|
}
|
|
82
113
|
del(key) {
|
|
114
|
+
const item = this.#cache.get(key);
|
|
115
|
+
if (!item) return;
|
|
83
116
|
this.#cache.delete(key);
|
|
117
|
+
this.#totalBytes -= item.bytes;
|
|
84
118
|
}
|
|
85
119
|
clear() {
|
|
86
120
|
this.#cache.clear();
|
|
121
|
+
this.#totalBytes = 0;
|
|
87
122
|
}
|
|
88
123
|
close() {
|
|
89
124
|
}
|
|
@@ -104,9 +139,11 @@ const convertToObject = (value) => {
|
|
|
104
139
|
|
|
105
140
|
class RedisCacheEngine {
|
|
106
141
|
#client;
|
|
107
|
-
|
|
142
|
+
#onError;
|
|
143
|
+
constructor(options, onError) {
|
|
108
144
|
options.keyPrefix ??= "cache-mongoose:";
|
|
109
145
|
this.#client = new IORedis(options);
|
|
146
|
+
this.#onError = onError;
|
|
110
147
|
}
|
|
111
148
|
async get(key) {
|
|
112
149
|
try {
|
|
@@ -116,18 +153,22 @@ class RedisCacheEngine {
|
|
|
116
153
|
}
|
|
117
154
|
return EJSON.parse(value);
|
|
118
155
|
} catch (err) {
|
|
119
|
-
|
|
156
|
+
this.#onError(err);
|
|
120
157
|
return void 0;
|
|
121
158
|
}
|
|
122
159
|
}
|
|
123
160
|
async set(key, value, ttl) {
|
|
124
161
|
try {
|
|
162
|
+
const converted = convertToObject(value);
|
|
163
|
+
if (converted === void 0) {
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
125
166
|
const givenTTL = ttl == null ? void 0 : ms(ttl);
|
|
126
167
|
const actualTTL = givenTTL ?? Number.POSITIVE_INFINITY;
|
|
127
|
-
const serializedValue = EJSON.stringify(
|
|
168
|
+
const serializedValue = EJSON.stringify(converted);
|
|
128
169
|
await this.#client.setex(key, Math.ceil(actualTTL / 1e3), serializedValue);
|
|
129
170
|
} catch (err) {
|
|
130
|
-
|
|
171
|
+
this.#onError(err);
|
|
131
172
|
}
|
|
132
173
|
}
|
|
133
174
|
async del(key) {
|
|
@@ -145,6 +186,7 @@ class Cache {
|
|
|
145
186
|
#engine;
|
|
146
187
|
#defaultTTL;
|
|
147
188
|
#debug;
|
|
189
|
+
#onError;
|
|
148
190
|
#engines = ["memory", "redis"];
|
|
149
191
|
constructor(cacheOptions) {
|
|
150
192
|
if (!this.#engines.includes(cacheOptions.engine)) {
|
|
@@ -155,14 +197,22 @@ class Cache {
|
|
|
155
197
|
}
|
|
156
198
|
cacheOptions.defaultTTL ??= "1 minute";
|
|
157
199
|
this.#defaultTTL = ms(cacheOptions.defaultTTL);
|
|
200
|
+
this.#onError = cacheOptions.onError ?? console.error;
|
|
158
201
|
if (cacheOptions.engine === "redis" && cacheOptions.engineOptions) {
|
|
159
|
-
this.#engine = new RedisCacheEngine(cacheOptions.engineOptions);
|
|
202
|
+
this.#engine = new RedisCacheEngine(cacheOptions.engineOptions, this.#onError);
|
|
160
203
|
}
|
|
161
204
|
if (cacheOptions.engine === "memory") {
|
|
162
|
-
this.#engine = new MemoryCacheEngine(
|
|
205
|
+
this.#engine = new MemoryCacheEngine({
|
|
206
|
+
maxEntries: cacheOptions.maxEntries,
|
|
207
|
+
maxBytes: cacheOptions.maxBytes,
|
|
208
|
+
sizeCalculation: cacheOptions.sizeCalculation
|
|
209
|
+
});
|
|
163
210
|
}
|
|
164
211
|
this.#debug = cacheOptions.debug === true;
|
|
165
212
|
}
|
|
213
|
+
get onError() {
|
|
214
|
+
return this.#onError;
|
|
215
|
+
}
|
|
166
216
|
async get(key) {
|
|
167
217
|
const cacheEntry = await this.#engine.get(key);
|
|
168
218
|
if (this.#debug) {
|
|
@@ -174,6 +224,12 @@ class Cache {
|
|
|
174
224
|
async set(key, value, ttl) {
|
|
175
225
|
const givenTTL = ttl == null ? null : ms(ttl);
|
|
176
226
|
const actualTTL = givenTTL ?? this.#defaultTTL;
|
|
227
|
+
if (Number.isNaN(actualTTL) || actualTTL <= 0) {
|
|
228
|
+
if (this.#debug) {
|
|
229
|
+
console.log(`[ts-cache-mongoose] SET '${key}' - skipped (non-positive ttl: ${String(actualTTL)} ms)`);
|
|
230
|
+
}
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
177
233
|
await this.#engine.set(key, value, actualTTL);
|
|
178
234
|
if (this.#debug) {
|
|
179
235
|
console.log(`[ts-cache-mongoose] SET '${key}' - ttl: ${actualTTL.toFixed(0)} ms`);
|
|
@@ -261,14 +317,14 @@ function extendAggregate(mongoose, cache) {
|
|
|
261
317
|
const key = this.getCacheKey();
|
|
262
318
|
const ttl = this.getDuration();
|
|
263
319
|
const resultCache = await cache.get(key).catch((err) => {
|
|
264
|
-
|
|
320
|
+
cache.onError(err);
|
|
265
321
|
});
|
|
266
322
|
if (resultCache) {
|
|
267
323
|
return resultCache;
|
|
268
324
|
}
|
|
269
325
|
const result = await mongooseExec.call(this);
|
|
270
326
|
await cache.set(key, result, ttl).catch((err) => {
|
|
271
|
-
|
|
327
|
+
cache.onError(err);
|
|
272
328
|
});
|
|
273
329
|
return result;
|
|
274
330
|
};
|
|
@@ -314,7 +370,7 @@ function extendQuery(mongoose, cache) {
|
|
|
314
370
|
const isDistinct = this.op === "distinct";
|
|
315
371
|
const model = this.model.modelName;
|
|
316
372
|
const resultCache = await cache.get(key).catch((err) => {
|
|
317
|
-
|
|
373
|
+
cache.onError(err);
|
|
318
374
|
});
|
|
319
375
|
if (resultCache) {
|
|
320
376
|
if (isCount || isDistinct || mongooseOptions.lean) {
|
|
@@ -330,7 +386,7 @@ function extendQuery(mongoose, cache) {
|
|
|
330
386
|
}
|
|
331
387
|
const result = await mongooseExec.call(this);
|
|
332
388
|
await cache.set(key, result, ttl).catch((err) => {
|
|
333
|
-
|
|
389
|
+
cache.onError(err);
|
|
334
390
|
});
|
|
335
391
|
return result;
|
|
336
392
|
};
|
package/dist/nest/index.cjs
CHANGED
package/dist/nest/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ts-cache-mongoose",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "Cache plugin for mongoose Queries and Aggregate (in-memory, redis)",
|
|
5
5
|
"author": "ilovepixelart",
|
|
6
6
|
"license": "MIT",
|
|
@@ -12,9 +12,6 @@
|
|
|
12
12
|
"url": "https://github.com/ilovepixelart/ts-cache-mongoose/issues"
|
|
13
13
|
},
|
|
14
14
|
"homepage": "https://github.com/ilovepixelart/ts-cache-mongoose#readme",
|
|
15
|
-
"directories": {
|
|
16
|
-
"examples": "examples"
|
|
17
|
-
},
|
|
18
15
|
"keywords": [
|
|
19
16
|
"backend",
|
|
20
17
|
"mongo",
|
|
@@ -35,15 +32,10 @@
|
|
|
35
32
|
"aggregate"
|
|
36
33
|
],
|
|
37
34
|
"engines": {
|
|
38
|
-
"node": ">=
|
|
35
|
+
"node": ">=20"
|
|
39
36
|
},
|
|
40
37
|
"files": [
|
|
41
|
-
"dist"
|
|
42
|
-
"src",
|
|
43
|
-
"tests",
|
|
44
|
-
"tsconfig.json",
|
|
45
|
-
"vite.config.mts",
|
|
46
|
-
"biome.json"
|
|
38
|
+
"dist"
|
|
47
39
|
],
|
|
48
40
|
"type": "module",
|
|
49
41
|
"exports": {
|
|
@@ -78,37 +70,43 @@
|
|
|
78
70
|
"main": "./dist/index.cjs",
|
|
79
71
|
"module": "./dist/index.mjs",
|
|
80
72
|
"types": "./dist/index.d.cts",
|
|
73
|
+
"publishConfig": {
|
|
74
|
+
"access": "public",
|
|
75
|
+
"provenance": true
|
|
76
|
+
},
|
|
81
77
|
"scripts": {
|
|
82
|
-
"prepare": "simple-git-hooks",
|
|
83
78
|
"biome": "npx @biomejs/biome check",
|
|
84
79
|
"biome:fix": "npx @biomejs/biome check --write .",
|
|
85
80
|
"test": "vitest run --coverage",
|
|
86
81
|
"test:open": "vitest run --coverage && open-cli coverage/lcov-report/index.html",
|
|
87
82
|
"clean": "rm -rf ./dist",
|
|
88
83
|
"type:check": "tsc --noEmit",
|
|
84
|
+
"type:check:tests": "tsc --noEmit -p tests/tsconfig.json",
|
|
89
85
|
"build": "pkgroll --clean-dist",
|
|
90
|
-
"release": "npm install && npm run biome && npm run type:check && npm run build && np --no-publish"
|
|
86
|
+
"release": "npm install && npm run biome && npm run type:check && npm run type:check:tests && npm run build && np --no-publish"
|
|
91
87
|
},
|
|
92
88
|
"dependencies": {
|
|
93
89
|
"ioredis": "5.10.0"
|
|
94
90
|
},
|
|
95
91
|
"devDependencies": {
|
|
96
|
-
"@biomejs/biome": "2.4.
|
|
97
|
-
"@nestjs/common": "11.1.
|
|
98
|
-
"@
|
|
99
|
-
"@
|
|
92
|
+
"@biomejs/biome": "2.4.14",
|
|
93
|
+
"@nestjs/common": "11.1.18",
|
|
94
|
+
"@nestjs/core": "11.1.18",
|
|
95
|
+
"@nestjs/testing": "11.1.18",
|
|
96
|
+
"@types/node": "25.6.1",
|
|
97
|
+
"@vitest/coverage-v8": "4.1.5",
|
|
100
98
|
"bson": "7.2.0",
|
|
99
|
+
"fast-check": "4.7.0",
|
|
101
100
|
"mongodb-memory-server": "11.0.1",
|
|
102
|
-
"mongoose": "9.
|
|
103
|
-
"
|
|
104
|
-
"open-cli": "8.0.0",
|
|
101
|
+
"mongoose": "9.4.1",
|
|
102
|
+
"open-cli": "9.0.0",
|
|
105
103
|
"pkgroll": "2.27.0",
|
|
106
104
|
"simple-git-hooks": "2.13.1",
|
|
107
105
|
"typescript": "5.9.3",
|
|
108
|
-
"vitest": "4.1.
|
|
106
|
+
"vitest": "4.1.5"
|
|
109
107
|
},
|
|
110
108
|
"peerDependencies": {
|
|
111
|
-
"@nestjs/common": ">=9.0.0",
|
|
109
|
+
"@nestjs/common": ">=9.0.0 < 12",
|
|
112
110
|
"bson": ">=4.7.2 < 8",
|
|
113
111
|
"mongoose": ">=6.6.0 < 10"
|
|
114
112
|
},
|
|
@@ -123,9 +121,5 @@
|
|
|
123
121
|
},
|
|
124
122
|
"np": {
|
|
125
123
|
"publish": false
|
|
126
|
-
},
|
|
127
|
-
"overrides": {
|
|
128
|
-
"tmp": "0.2.5",
|
|
129
|
-
"file-type": "21.3.2"
|
|
130
124
|
}
|
|
131
125
|
}
|
package/biome.json
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"$schema": "https://biomejs.dev/schemas/2.4.7/schema.json",
|
|
3
|
-
"vcs": { "enabled": false, "clientKind": "git", "useIgnoreFile": false },
|
|
4
|
-
"files": {
|
|
5
|
-
"ignoreUnknown": false,
|
|
6
|
-
"includes": ["src/**/*.ts", "tests/**/*.ts"]
|
|
7
|
-
},
|
|
8
|
-
"formatter": { "enabled": true, "indentStyle": "space", "indentWidth": 2 },
|
|
9
|
-
"assist": {
|
|
10
|
-
"actions": {
|
|
11
|
-
"source": {
|
|
12
|
-
"organizeImports": {
|
|
13
|
-
"level": "on",
|
|
14
|
-
"options": {
|
|
15
|
-
"groups": [
|
|
16
|
-
"vitest",
|
|
17
|
-
":BLANK_LINE:",
|
|
18
|
-
":NODE:",
|
|
19
|
-
{ "type": false },
|
|
20
|
-
":BLANK_LINE:"
|
|
21
|
-
]
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
},
|
|
27
|
-
"linter": {
|
|
28
|
-
"enabled": true,
|
|
29
|
-
"rules": {
|
|
30
|
-
"recommended": true
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"javascript": {
|
|
34
|
-
"formatter": {
|
|
35
|
-
"trailingCommas": "all",
|
|
36
|
-
"quoteStyle": "single",
|
|
37
|
-
"semicolons": "asNeeded",
|
|
38
|
-
"lineWidth": 320
|
|
39
|
-
},
|
|
40
|
-
"globals": ["Atomics", "SharedArrayBuffer"]
|
|
41
|
-
},
|
|
42
|
-
"json": {
|
|
43
|
-
"formatter": {
|
|
44
|
-
"trailingCommas": "none"
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
}
|