@nestjs-redisx/cache 1.0.1 → 1.0.3

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/dist/index.mjs CHANGED
@@ -23,6 +23,9 @@ var __decorateClass = (decorators, target, key, kind) => {
23
23
  };
24
24
  var __decorateParam = (index, decorator) => (target, key) => decorator(target, key, index);
25
25
 
26
+ // package.json
27
+ var version = "1.0.3";
28
+
26
29
  // src/shared/constants/index.ts
27
30
  var CACHE_PLUGIN_OPTIONS = /* @__PURE__ */ Symbol.for("CACHE_PLUGIN_OPTIONS");
28
31
  var CACHE_SERVICE = /* @__PURE__ */ Symbol.for("CACHE_SERVICE");
@@ -114,20 +117,19 @@ function Cached(options = {}) {
114
117
  return originalMethod.apply(this, args);
115
118
  }
116
119
  const key = buildCacheKey(this, propertyKey.toString(), args, options);
120
+ const tags = typeof options.tags === "function" ? options.tags(...args) : options.tags;
117
121
  try {
118
- const cached = await cacheService.get(key);
119
- if (cached !== null) {
120
- return cached;
121
- }
122
+ return await cacheService.getOrSet(key, () => originalMethod.apply(this, args), {
123
+ ttl: options.ttl,
124
+ tags,
125
+ strategy: options.strategy,
126
+ swr: options.swr,
127
+ unless: options.unless ? (result) => options.unless(result, ...args) : void 0
128
+ });
122
129
  } catch (error) {
123
- logger.error(`@Cached: Cache get error for key ${key}:`, error);
124
- }
125
- const result = await originalMethod.apply(this, args);
126
- if (options.unless?.(result, ...args)) {
127
- return result;
130
+ logger.error(`@Cached: getOrSet error for key ${key}:`, error);
131
+ return originalMethod.apply(this, args);
128
132
  }
129
- await cacheResult(cacheService, key, result, options, args);
130
- return result;
131
133
  };
132
134
  Object.defineProperty(descriptor.value, "name", {
133
135
  value: originalMethod.name,
@@ -159,6 +161,10 @@ function enrichWithContext(key, options) {
159
161
  for (const ctxKey of contextKeys) {
160
162
  const value = pluginOpts.contextProvider.get(ctxKey);
161
163
  if (value !== void 0 && value !== null) {
164
+ if (typeof value === "object") {
165
+ logger.warn(`Context key "${ctxKey}" has object value, skipping (use primitives for context keys)`);
166
+ continue;
167
+ }
162
168
  contextMap.set(ctxKey, String(value));
163
169
  }
164
170
  }
@@ -167,6 +173,10 @@ function enrichWithContext(key, options) {
167
173
  if (!contextMap.has(name)) {
168
174
  const value = pluginOpts.contextProvider.get(name);
169
175
  if (value !== void 0 && value !== null) {
176
+ if (typeof value === "object") {
177
+ logger.warn(`varyBy key "${name}" has object value, skipping (use primitives for varyBy keys)`);
178
+ continue;
179
+ }
170
180
  contextMap.set(name, String(value));
171
181
  }
172
182
  }
@@ -181,7 +191,7 @@ function sanitizeForKey(value) {
181
191
  return String(value).replace(/[^a-zA-Z0-9\-_]/g, "_");
182
192
  }
183
193
  function interpolateKey(template, args) {
184
- return template.replace(/\{(\d+)\}/g, (match, index) => {
194
+ return template.replace(/\{(\d+)}/g, (match, index) => {
185
195
  const argIndex = parseInt(index, 10);
186
196
  if (argIndex < args.length) {
187
197
  return serializeArg(args[argIndex]);
@@ -198,38 +208,43 @@ function serializeArg(arg) {
198
208
  }
199
209
  if (typeof arg === "object") {
200
210
  try {
201
- return JSON.stringify(arg);
211
+ return stableStringify(arg);
202
212
  } catch {
203
213
  return "object";
204
214
  }
205
215
  }
206
216
  return "unknown";
207
217
  }
208
- async function cacheResult(cacheService, key, value, options, args) {
209
- try {
210
- const tags = typeof options.tags === "function" ? options.tags(...args) : options.tags;
211
- if (options.swr?.enabled) {
212
- await cacheService.getOrSet(
213
- key,
214
- // eslint-disable-next-line @typescript-eslint/require-await
215
- async () => value,
216
- {
217
- ttl: options.ttl,
218
- tags,
219
- strategy: options.strategy,
220
- swr: options.swr
221
- }
222
- );
223
- } else {
224
- await cacheService.set(key, value, {
225
- ttl: options.ttl,
226
- tags,
227
- strategy: options.strategy
228
- });
218
+ function stableStringify(value) {
219
+ if (value === null || value === void 0) {
220
+ return "null";
221
+ }
222
+ if (typeof value === "function" || typeof value === "symbol") {
223
+ return "null";
224
+ }
225
+ if (typeof value !== "object") {
226
+ if (typeof value === "bigint") {
227
+ return String(value);
228
+ }
229
+ return JSON.stringify(value);
230
+ }
231
+ if (Array.isArray(value)) {
232
+ return "[" + value.map((item) => item === void 0 || typeof item === "function" || typeof item === "symbol" ? "null" : stableStringify(item)).join(",") + "]";
233
+ }
234
+ if (value instanceof Date) {
235
+ return JSON.stringify(value);
236
+ }
237
+ const obj = value;
238
+ const keys = Object.keys(obj).sort();
239
+ const parts = [];
240
+ for (const key of keys) {
241
+ const v = obj[key];
242
+ if (v === void 0 || typeof v === "function" || typeof v === "symbol") {
243
+ continue;
229
244
  }
230
- } catch (error) {
231
- logger.error(`@Cached: Failed to cache result for key ${key}:`, error);
245
+ parts.push(JSON.stringify(key) + ":" + stableStringify(v));
232
246
  }
247
+ return "{" + parts.join(",") + "}";
233
248
  }
234
249
 
235
250
  // src/invalidation/infrastructure/decorators/invalidate-on.decorator.ts
@@ -871,7 +886,7 @@ var CacheService = class {
871
886
  async getOrSet(key, loader, options = {}) {
872
887
  const normalizedKey = this.validateAndNormalizeKey(key);
873
888
  const enrichedKey = this.enrichKeyWithContext(normalizedKey, options.varyBy);
874
- const swrEnabled = this.swrEnabled && (options.swr?.enabled ?? true);
889
+ const swrEnabled = options.swr?.enabled ?? this.swrEnabled;
875
890
  if (swrEnabled) {
876
891
  const swrEntry = await this.l2Store.getSwr(enrichedKey);
877
892
  if (swrEntry) {
@@ -883,11 +898,14 @@ var CacheService = class {
883
898
  enrichedKey,
884
899
  loader,
885
900
  async (freshValue) => {
886
- await this.set(enrichedKey, freshValue, {
887
- ttl: options.ttl,
888
- tags: options.tags,
889
- strategy: options.strategy
890
- });
901
+ const staleTime = options.swr?.staleTime ?? this.options.swr?.defaultStaleTime ?? 60;
902
+ const ttl = options.ttl ?? this.options.l2?.defaultTtl ?? 3600;
903
+ const swrEntryNew = this.swrManager.createSwrEntry(freshValue, ttl, staleTime);
904
+ await this.l2Store.setSwr(enrichedKey, swrEntryNew);
905
+ if (this.l1Enabled) {
906
+ const entry = CacheEntry.create(freshValue, ttl);
907
+ await this.l1Store.set(enrichedKey, entry, this.options.l1?.ttl);
908
+ }
891
909
  },
892
910
  (error) => {
893
911
  this.logger.error(`SWR revalidation failed for key ${enrichedKey}:`, error);
@@ -898,10 +916,12 @@ var CacheService = class {
898
916
  }
899
917
  }
900
918
  const value = await this.loadWithStampede(enrichedKey, loader, options);
901
- const staleTime = options.swr?.staleTime ?? this.options.swr?.defaultStaleTime ?? 60;
902
- const ttl = options.ttl ?? this.options.l2?.defaultTtl ?? 3600;
903
- const swrEntryNew = this.swrManager.createSwrEntry(value, ttl, staleTime);
904
- await this.l2Store.setSwr(enrichedKey, swrEntryNew);
919
+ if (!options.unless?.(value)) {
920
+ const staleTime = options.swr?.staleTime ?? this.options.swr?.defaultStaleTime ?? 60;
921
+ const ttl = options.ttl ?? this.options.l2?.defaultTtl ?? 3600;
922
+ const swrEntryNew = this.swrManager.createSwrEntry(value, ttl, staleTime);
923
+ await this.l2Store.setSwr(enrichedKey, swrEntryNew);
924
+ }
905
925
  return value;
906
926
  }
907
927
  const cached = await this.get(enrichedKey);
@@ -926,19 +946,23 @@ var CacheService = class {
926
946
  this.metrics?.incrementCounter("redisx_cache_stampede_prevented_total");
927
947
  return result.value;
928
948
  }
929
- await this.set(key, result.value, {
949
+ if (!options.unless?.(result.value)) {
950
+ await this.set(key, result.value, {
951
+ ttl: options.ttl,
952
+ tags: options.tags,
953
+ strategy: options.strategy
954
+ });
955
+ }
956
+ return result.value;
957
+ }
958
+ const value = await loader();
959
+ if (!options.unless?.(value)) {
960
+ await this.set(key, value, {
930
961
  ttl: options.ttl,
931
962
  tags: options.tags,
932
963
  strategy: options.strategy
933
964
  });
934
- return result.value;
935
965
  }
936
- const value = await loader();
937
- await this.set(key, value, {
938
- ttl: options.ttl,
939
- tags: options.tags,
940
- strategy: options.strategy
941
- });
942
966
  return value;
943
967
  }
944
968
  async delete(key) {
@@ -981,11 +1005,12 @@ var CacheService = class {
981
1005
  if (normalizedKeys.length === 0) {
982
1006
  return 0;
983
1007
  }
984
- let deletedCount = 0;
1008
+ const deleted = new Array(normalizedKeys.length).fill(false);
985
1009
  if (this.l1Enabled) {
986
- for (const key of normalizedKeys) {
987
- const deleted = await this.l1Store.delete(key);
988
- if (deleted) deletedCount++;
1010
+ for (let i = 0; i < normalizedKeys.length; i++) {
1011
+ if (await this.l1Store.delete(normalizedKeys[i])) {
1012
+ deleted[i] = true;
1013
+ }
989
1014
  }
990
1015
  }
991
1016
  if (this.l2Enabled && normalizedKeys.length > 0) {
@@ -995,17 +1020,16 @@ var CacheService = class {
995
1020
  pipeline.del(fullKey);
996
1021
  }
997
1022
  const results = await pipeline.exec();
998
- let l2Count = 0;
999
1023
  if (results) {
1000
- for (const [error, result] of results) {
1024
+ for (let i = 0; i < results.length; i++) {
1025
+ const [error, result] = results[i];
1001
1026
  if (!error && typeof result === "number" && result > 0) {
1002
- l2Count++;
1027
+ deleted[i] = true;
1003
1028
  }
1004
1029
  }
1005
1030
  }
1006
- deletedCount = Math.max(deletedCount, l2Count);
1007
1031
  }
1008
- return deletedCount;
1032
+ return deleted.filter(Boolean).length;
1009
1033
  } catch (error) {
1010
1034
  throw new CacheError(`Failed to delete multiple keys: ${error.message}`, ErrorCode.CACHE_DELETE_FAILED, error);
1011
1035
  }
@@ -1142,10 +1166,10 @@ var CacheService = class {
1142
1166
  const maxTtl = this.options.l2?.maxTtl ?? 86400;
1143
1167
  const defaultTtl = this.options.l2?.defaultTtl ?? 3600;
1144
1168
  const cacheEntries = entries.map(({ key, value, ttl }) => {
1145
- const entryTtl = ttl ?? defaultTtl;
1169
+ const entryTtl = Math.min(ttl ?? defaultTtl, maxTtl);
1146
1170
  return {
1147
1171
  key: this.enrichKeyWithContext(this.validateAndNormalizeKey(key)),
1148
- entry: CacheEntry.create(value, Math.min(entryTtl, maxTtl)),
1172
+ entry: CacheEntry.create(value, entryTtl),
1149
1173
  ttl: entryTtl
1150
1174
  };
1151
1175
  });
@@ -1270,6 +1294,10 @@ var CacheService = class {
1270
1294
  for (const ctxKey of contextKeys) {
1271
1295
  const value = contextProvider.get(ctxKey);
1272
1296
  if (value !== void 0 && value !== null) {
1297
+ if (typeof value === "object") {
1298
+ this.logger.warn(`Context key "${ctxKey}" has object value, skipping (use primitives for context keys)`);
1299
+ continue;
1300
+ }
1273
1301
  contextMap.set(ctxKey, String(value));
1274
1302
  }
1275
1303
  }
@@ -1507,12 +1535,6 @@ var L1MemoryStoreAdapter = class {
1507
1535
  }
1508
1536
  // eslint-disable-next-line @typescript-eslint/require-await
1509
1537
  async size() {
1510
- const now = Date.now();
1511
- for (const [key, node] of this.cache.entries()) {
1512
- if (now > node.expiresAt) {
1513
- void this.delete(key);
1514
- }
1515
- }
1516
1538
  return this.cache.size;
1517
1539
  }
1518
1540
  moveToFront(node) {
@@ -2710,9 +2732,6 @@ var SwrManagerService = class {
2710
2732
  async delete(_key) {
2711
2733
  }
2712
2734
  isStale(entry) {
2713
- if (!this.enabled) {
2714
- return false;
2715
- }
2716
2735
  const now = Date.now();
2717
2736
  return now > entry.staleAt;
2718
2737
  }
@@ -3045,33 +3064,63 @@ LuaScriptLoader = __decorateClass([
3045
3064
  ], LuaScriptLoader);
3046
3065
 
3047
3066
  // src/cache.plugin.ts
3048
- var CachePlugin = class {
3067
+ var CachePlugin = class _CachePlugin {
3049
3068
  constructor(options = {}) {
3050
3069
  this.options = options;
3051
3070
  }
3052
3071
  name = "cache";
3053
- version = "0.1.0";
3072
+ version = version;
3054
3073
  description = "Advanced caching with L1+L2, anti-stampede, SWR, and tag invalidation";
3074
+ asyncOptions;
3075
+ /**
3076
+ * Create a CachePlugin with async configuration from DI.
3077
+ *
3078
+ * @example
3079
+ * ```typescript
3080
+ * CachePlugin.registerAsync({
3081
+ * imports: [ConfigModule],
3082
+ * inject: [ConfigService],
3083
+ * useFactory: (config: ConfigService) => ({
3084
+ * l1: { maxSize: config.get('CACHE_L1_MAX_SIZE', 1000) },
3085
+ * swr: { enabled: config.get('CACHE_SWR_ENABLED', false) },
3086
+ * }),
3087
+ * })
3088
+ * ```
3089
+ */
3090
+ static registerAsync(asyncOptions) {
3091
+ const plugin = new _CachePlugin();
3092
+ plugin.asyncOptions = asyncOptions;
3093
+ return plugin;
3094
+ }
3095
+ static mergeDefaults(options) {
3096
+ return {
3097
+ l1: { ...DEFAULT_CACHE_CONFIG.l1, ...options.l1 },
3098
+ l2: { ...DEFAULT_CACHE_CONFIG.l2, ...options.l2 },
3099
+ stampede: { ...DEFAULT_CACHE_CONFIG.stampede, ...options.stampede },
3100
+ swr: { ...DEFAULT_CACHE_CONFIG.swr, ...options.swr },
3101
+ tags: { ...DEFAULT_CACHE_CONFIG.tags, ...options.tags },
3102
+ warmup: { ...DEFAULT_CACHE_CONFIG.warmup, ...options.warmup },
3103
+ keys: { ...DEFAULT_CACHE_CONFIG.keys, ...options.keys },
3104
+ invalidation: { ...DEFAULT_CACHE_CONFIG.invalidation, ...options.invalidation }
3105
+ };
3106
+ }
3107
+ getImports() {
3108
+ return this.asyncOptions?.imports ?? [];
3109
+ }
3055
3110
  getProviders() {
3056
- const config = {
3057
- l1: { ...DEFAULT_CACHE_CONFIG.l1, ...this.options.l1 },
3058
- l2: { ...DEFAULT_CACHE_CONFIG.l2, ...this.options.l2 },
3059
- stampede: { ...DEFAULT_CACHE_CONFIG.stampede, ...this.options.stampede },
3060
- swr: { ...DEFAULT_CACHE_CONFIG.swr, ...this.options.swr },
3061
- tags: { ...DEFAULT_CACHE_CONFIG.tags, ...this.options.tags },
3062
- warmup: { ...DEFAULT_CACHE_CONFIG.warmup, ...this.options.warmup },
3063
- keys: { ...DEFAULT_CACHE_CONFIG.keys, ...this.options.keys },
3064
- invalidation: {
3065
- ...DEFAULT_CACHE_CONFIG.invalidation,
3066
- ...this.options.invalidation
3067
- }
3111
+ const optionsProvider = this.asyncOptions ? {
3112
+ provide: CACHE_PLUGIN_OPTIONS,
3113
+ useFactory: async (...args) => {
3114
+ const userOptions = await this.asyncOptions.useFactory(...args);
3115
+ return _CachePlugin.mergeDefaults(userOptions);
3116
+ },
3117
+ inject: this.asyncOptions.inject || []
3118
+ } : {
3119
+ provide: CACHE_PLUGIN_OPTIONS,
3120
+ useValue: _CachePlugin.mergeDefaults(this.options)
3068
3121
  };
3069
3122
  return [
3070
- // Configuration
3071
- {
3072
- provide: CACHE_PLUGIN_OPTIONS,
3073
- useValue: config
3074
- },
3123
+ optionsProvider,
3075
3124
  // Domain services
3076
3125
  {
3077
3126
  provide: SERIALIZER,
@@ -3129,9 +3178,9 @@ var CachePlugin = class {
3129
3178
  // Factory for registering static invalidation rules
3130
3179
  {
3131
3180
  provide: INVALIDATION_RULES_INIT,
3132
- useFactory: (registry, config2) => {
3133
- if (config2.invalidation?.rules && config2.invalidation.rules.length > 0) {
3134
- const rules = config2.invalidation.rules.map((ruleProps) => InvalidationRule.create(ruleProps));
3181
+ useFactory: (registry, config) => {
3182
+ if (config.invalidation?.rules && config.invalidation.rules.length > 0) {
3183
+ const rules = config.invalidation.rules.map((ruleProps) => InvalidationRule.create(ruleProps));
3135
3184
  registry.registerMany(rules);
3136
3185
  }
3137
3186
  return true;
@@ -3997,8 +4046,8 @@ var KeyBuilder = class _KeyBuilder {
3997
4046
  * @param version - Version value (e.g., 'v1', 'v2')
3998
4047
  * @returns This builder for chaining
3999
4048
  */
4000
- version(version) {
4001
- return this.segment(version);
4049
+ version(version2) {
4050
+ return this.segment(version2);
4002
4051
  }
4003
4052
  /**
4004
4053
  * Adds a segment to the key.