@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/cache/api/decorators/cached.decorator.d.ts +1 -0
- package/dist/cache/api/decorators/cached.decorator.d.ts.map +1 -1
- package/dist/cache/application/ports/l1-cache-store.port.d.ts +3 -2
- package/dist/cache/application/ports/l1-cache-store.port.d.ts.map +1 -1
- package/dist/cache/application/services/cache.service.d.ts.map +1 -1
- package/dist/cache/infrastructure/adapters/l1-memory-store.adapter.d.ts.map +1 -1
- package/dist/cache.plugin.d.ts +21 -2
- package/dist/cache.plugin.d.ts.map +1 -1
- package/dist/index.js +141 -95
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +141 -95
- package/dist/index.mjs.map +1 -1
- package/dist/shared/types/index.d.ts +2 -0
- package/dist/shared/types/index.d.ts.map +1 -1
- package/dist/swr/infrastructure/swr-manager.service.d.ts.map +1 -1
- package/package.json +1 -1
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
|
-
|
|
119
|
-
|
|
120
|
-
|
|
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:
|
|
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+)
|
|
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
|
|
208
|
+
return stableStringify(arg);
|
|
202
209
|
} catch {
|
|
203
210
|
return "object";
|
|
204
211
|
}
|
|
205
212
|
}
|
|
206
213
|
return "unknown";
|
|
207
214
|
}
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
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
|
-
|
|
231
|
-
|
|
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 =
|
|
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
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
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
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1005
|
+
const deleted = new Array(normalizedKeys.length).fill(false);
|
|
985
1006
|
if (this.l1Enabled) {
|
|
986
|
-
for (
|
|
987
|
-
|
|
988
|
-
|
|
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 (
|
|
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
|
-
|
|
1024
|
+
deleted[i] = true;
|
|
1003
1025
|
}
|
|
1004
1026
|
}
|
|
1005
1027
|
}
|
|
1006
|
-
deletedCount = Math.max(deletedCount, l2Count);
|
|
1007
1028
|
}
|
|
1008
|
-
return
|
|
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,
|
|
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
|
|
3057
|
-
|
|
3058
|
-
|
|
3059
|
-
|
|
3060
|
-
|
|
3061
|
-
|
|
3062
|
-
|
|
3063
|
-
|
|
3064
|
-
|
|
3065
|
-
|
|
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
|
-
|
|
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,
|
|
3133
|
-
if (
|
|
3134
|
-
const rules =
|
|
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;
|