@nestjs-redisx/cache 1.0.1 → 1.0.2

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