ts-cache-mongoose 2.1.0 → 2.2.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 CHANGED
@@ -12,6 +12,10 @@ Cache query and aggregate in mongoose using in-memory or redis
12
12
  [![Reliability Rating](https://sonarcloud.io/api/project_badges/measure?project=ilovepixelart_ts-cache-mongoose&metric=reliability_rating)](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-cache-mongoose)
13
13
  [![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=ilovepixelart_ts-cache-mongoose&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-cache-mongoose)
14
14
  [![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=ilovepixelart_ts-cache-mongoose&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=ilovepixelart_ts-cache-mongoose)
15
+ \
16
+ [![Socket Badge](https://badge.socket.dev/npm/package/ts-cache-mongoose)](https://socket.dev/npm/package/ts-cache-mongoose)
17
+ [![OpenSSF Scorecard](https://api.securityscorecards.dev/projects/github.com/ilovepixelart/ts-cache-mongoose/badge)](https://securityscorecards.dev/viewer/?uri=github.com/ilovepixelart/ts-cache-mongoose)
18
+ [![OpenSSF Best Practices](https://www.bestpractices.dev/projects/12484/badge?v=1)](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.x || 7.x || 8.x || 9.x",
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
- constructor() {
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 || item.expiresAt < Date.now()) {
71
- this.del(key);
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.set(key, {
80
- value,
81
- expiresAt: Date.now() + actualTTL
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
- constructor(options) {
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
- console.error(err);
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(convertToObject(value));
170
+ const serializedValue = bson.EJSON.stringify(converted);
130
171
  await this.#client.setex(key, Math.ceil(actualTTL / 1e3), serializedValue);
131
172
  } catch (err) {
132
- console.error(err);
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
- console.error(err);
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
- console.error(err);
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
- console.error(err);
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
- console.error(err);
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;
@@ -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;;;AAIjB,UAAW,WAAW;0BACJ,OAAO,CAAC,SAAS,IAAI,SAAS;8BAC1B,SAAS,QAAQ,QAAQ,KAAK,OAAO;0BACzC,OAAO;iBAChB,OAAO;iBACP,OAAO;;;ACVtB;;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":[]}
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;
@@ -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;;;AAIjB,UAAW,WAAW;0BACJ,OAAO,CAAC,SAAS,IAAI,SAAS;8BAC1B,SAAS,QAAQ,QAAQ,KAAK,OAAO;0BACzC,OAAO;iBAChB,OAAO;iBACP,OAAO;;;ACVtB;;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":[]}
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
- constructor() {
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 || item.expiresAt < Date.now()) {
69
- this.del(key);
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.set(key, {
78
- value,
79
- expiresAt: Date.now() + actualTTL
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
- constructor(options) {
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
- console.error(err);
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(convertToObject(value));
168
+ const serializedValue = EJSON.stringify(converted);
128
169
  await this.#client.setex(key, Math.ceil(actualTTL / 1e3), serializedValue);
129
170
  } catch (err) {
130
- console.error(err);
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
- console.error(err);
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
- console.error(err);
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
- console.error(err);
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
- console.error(err);
389
+ cache.onError(err);
334
390
  });
335
391
  return result;
336
392
  };
@@ -3,6 +3,7 @@
3
3
  var common = require('@nestjs/common');
4
4
  var mongoose = require('mongoose');
5
5
  var index = require('../index.cjs');
6
+ require('node:v8');
6
7
  require('bson');
7
8
  require('ioredis');
8
9
  require('node:crypto');
@@ -1,6 +1,7 @@
1
1
  import { Logger, Module } from '@nestjs/common';
2
2
  import mongoose from 'mongoose';
3
3
  import CacheMongoose from '../index.mjs';
4
+ import 'node:v8';
4
5
  import 'bson';
5
6
  import 'ioredis';
6
7
  import 'node:crypto';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-cache-mongoose",
3
- "version": "2.1.0",
3
+ "version": "2.2.0",
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": ">=18"
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,44 @@
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.7",
97
- "@nestjs/common": "11.1.16",
98
- "@types/node": "25.5.0",
99
- "@vitest/coverage-v8": "4.1.0",
92
+ "@biomejs/biome": "2.4.11",
93
+ "@nestjs/common": "11.1.18",
94
+ "@nestjs/core": "11.1.18",
95
+ "@nestjs/testing": "11.1.18",
96
+ "@types/node": "25.6.0",
97
+ "@vitest/coverage-v8": "4.1.4",
100
98
  "bson": "7.2.0",
99
+ "fast-check": "4.6.0",
101
100
  "mongodb-memory-server": "11.0.1",
102
- "mongoose": "9.3.0",
103
- "np": "11.0.2",
104
- "open-cli": "8.0.0",
101
+ "mongoose": "9.4.1",
102
+ "np": "11.0.3",
103
+ "open-cli": "9.0.0",
105
104
  "pkgroll": "2.27.0",
106
105
  "simple-git-hooks": "2.13.1",
107
106
  "typescript": "5.9.3",
108
- "vitest": "4.1.0"
107
+ "vitest": "4.1.4"
109
108
  },
110
109
  "peerDependencies": {
111
- "@nestjs/common": ">=9.0.0",
110
+ "@nestjs/common": ">=9.0.0 < 12",
112
111
  "bson": ">=4.7.2 < 8",
113
112
  "mongoose": ">=6.6.0 < 10"
114
113
  },
@@ -125,7 +124,6 @@
125
124
  "publish": false
126
125
  },
127
126
  "overrides": {
128
- "tmp": "0.2.5",
129
- "file-type": "21.3.2"
127
+ "tmp": "0.2.5"
130
128
  }
131
129
  }
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
- }