@open-mercato/cache 0.3.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/ENV.md +173 -0
- package/README.md +177 -0
- package/dist/errors.d.ts +7 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +9 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +7 -0
- package/dist/service.d.ts +40 -0
- package/dist/service.d.ts.map +1 -0
- package/dist/service.js +321 -0
- package/dist/strategies/jsonfile.d.ts +10 -0
- package/dist/strategies/jsonfile.d.ts.map +1 -0
- package/dist/strategies/jsonfile.js +205 -0
- package/dist/strategies/memory.d.ts +9 -0
- package/dist/strategies/memory.d.ts.map +1 -0
- package/dist/strategies/memory.js +166 -0
- package/dist/strategies/redis.d.ts +5 -0
- package/dist/strategies/redis.d.ts.map +1 -0
- package/dist/strategies/redis.js +388 -0
- package/dist/strategies/sqlite.d.ts +13 -0
- package/dist/strategies/sqlite.d.ts.map +1 -0
- package/dist/strategies/sqlite.js +217 -0
- package/dist/tenantContext.d.ts +4 -0
- package/dist/tenantContext.d.ts.map +1 -0
- package/dist/tenantContext.js +9 -0
- package/dist/types.d.ts +86 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +1 -0
- package/jest.config.js +19 -0
- package/package.json +39 -0
- package/src/__tests__/memory.strategy.test.ts +245 -0
- package/src/__tests__/service.test.ts +189 -0
- package/src/errors.ts +14 -0
- package/src/index.ts +7 -0
- package/src/service.ts +367 -0
- package/src/strategies/jsonfile.ts +249 -0
- package/src/strategies/memory.ts +185 -0
- package/src/strategies/redis.ts +443 -0
- package/src/strategies/sqlite.ts +285 -0
- package/src/tenantContext.ts +13 -0
- package/src/types.ts +100 -0
- package/tsconfig.build.json +5 -0
- package/tsconfig.json +12 -0
package/dist/service.js
ADDED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
import { createMemoryStrategy } from './strategies/memory';
|
|
2
|
+
import { createRedisStrategy } from './strategies/redis';
|
|
3
|
+
import { createSqliteStrategy } from './strategies/sqlite';
|
|
4
|
+
import { createJsonFileStrategy } from './strategies/jsonfile';
|
|
5
|
+
import { getCurrentCacheTenant } from './tenantContext';
|
|
6
|
+
import { createHash } from 'node:crypto';
|
|
7
|
+
import { CacheDependencyUnavailableError } from './errors';
|
|
8
|
+
function normalizeTenantKey(raw) {
|
|
9
|
+
const value = typeof raw === 'string' ? raw.trim() : '';
|
|
10
|
+
if (!value)
|
|
11
|
+
return 'global';
|
|
12
|
+
return value.replace(/[^a-zA-Z0-9._-]/g, '_');
|
|
13
|
+
}
|
|
14
|
+
function isCacheMetadata(value) {
|
|
15
|
+
if (typeof value !== 'object' || value === null) {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
const record = value;
|
|
19
|
+
const hasValidKey = typeof record.key === 'string';
|
|
20
|
+
const hasValidExpiresAt = !('expiresAt' in record)
|
|
21
|
+
|| record.expiresAt === null
|
|
22
|
+
|| typeof record.expiresAt === 'number';
|
|
23
|
+
return hasValidKey && hasValidExpiresAt;
|
|
24
|
+
}
|
|
25
|
+
const KNOWN_STRATEGIES = ['memory', 'redis', 'sqlite', 'jsonfile'];
|
|
26
|
+
function isCacheStrategyName(value) {
|
|
27
|
+
if (!value)
|
|
28
|
+
return false;
|
|
29
|
+
return KNOWN_STRATEGIES.includes(value);
|
|
30
|
+
}
|
|
31
|
+
function resolveTenantPrefixes() {
|
|
32
|
+
const tenant = normalizeTenantKey(getCurrentCacheTenant());
|
|
33
|
+
const base = `tenant:${tenant}:`;
|
|
34
|
+
return {
|
|
35
|
+
keyPrefix: `${base}key:`,
|
|
36
|
+
tagPrefix: `${base}tag:`,
|
|
37
|
+
scopeTag: `${base}tag:__scope__`,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function hashIdentifier(input) {
|
|
41
|
+
return createHash('sha1').update(input).digest('hex');
|
|
42
|
+
}
|
|
43
|
+
function storageKey(originalKey, prefixes) {
|
|
44
|
+
return `${prefixes.keyPrefix}k:${hashIdentifier(originalKey)}`;
|
|
45
|
+
}
|
|
46
|
+
function metaKey(originalKey, prefixes) {
|
|
47
|
+
return `${prefixes.keyPrefix}meta:${hashIdentifier(originalKey)}`;
|
|
48
|
+
}
|
|
49
|
+
function hashedTag(tag, prefixes) {
|
|
50
|
+
return `${prefixes.tagPrefix}t:${hashIdentifier(tag)}`;
|
|
51
|
+
}
|
|
52
|
+
function buildTagSet(tags, prefixes, includeScope) {
|
|
53
|
+
const scoped = new Set();
|
|
54
|
+
if (includeScope)
|
|
55
|
+
scoped.add(prefixes.scopeTag);
|
|
56
|
+
if (Array.isArray(tags)) {
|
|
57
|
+
for (const tag of tags) {
|
|
58
|
+
if (typeof tag === 'string' && tag.length > 0)
|
|
59
|
+
scoped.add(hashedTag(tag, prefixes));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return Array.from(scoped);
|
|
63
|
+
}
|
|
64
|
+
function matchPattern(value, pattern) {
|
|
65
|
+
const regexPattern = pattern
|
|
66
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
67
|
+
.replace(/\*/g, '.*')
|
|
68
|
+
.replace(/\?/g, '.');
|
|
69
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
70
|
+
return regex.test(value);
|
|
71
|
+
}
|
|
72
|
+
function createTenantAwareWrapper(base) {
|
|
73
|
+
function normalizeDeletionCount(raw) {
|
|
74
|
+
if (!raw)
|
|
75
|
+
return raw;
|
|
76
|
+
if (!Number.isFinite(raw))
|
|
77
|
+
return raw;
|
|
78
|
+
return Math.ceil(raw / 2);
|
|
79
|
+
}
|
|
80
|
+
const get = async (key, options) => {
|
|
81
|
+
const prefixes = resolveTenantPrefixes();
|
|
82
|
+
return base.get(storageKey(key, prefixes), options);
|
|
83
|
+
};
|
|
84
|
+
const set = async (key, value, options) => {
|
|
85
|
+
var _a;
|
|
86
|
+
const prefixes = resolveTenantPrefixes();
|
|
87
|
+
const hashedTags = buildTagSet(options === null || options === void 0 ? void 0 : options.tags, prefixes, true);
|
|
88
|
+
const ttl = (_a = options === null || options === void 0 ? void 0 : options.ttl) !== null && _a !== void 0 ? _a : undefined;
|
|
89
|
+
const nextOptions = options
|
|
90
|
+
? Object.assign(Object.assign({}, options), { tags: hashedTags }) : { tags: hashedTags };
|
|
91
|
+
await base.set(storageKey(key, prefixes), value, nextOptions);
|
|
92
|
+
const metaPayload = { key, expiresAt: ttl ? Date.now() + ttl : null };
|
|
93
|
+
await base.set(metaKey(key, prefixes), metaPayload, {
|
|
94
|
+
ttl,
|
|
95
|
+
tags: hashedTags,
|
|
96
|
+
});
|
|
97
|
+
};
|
|
98
|
+
const has = async (key) => {
|
|
99
|
+
const prefixes = resolveTenantPrefixes();
|
|
100
|
+
return base.has(storageKey(key, prefixes));
|
|
101
|
+
};
|
|
102
|
+
const del = async (key) => {
|
|
103
|
+
const prefixes = resolveTenantPrefixes();
|
|
104
|
+
const primary = await base.delete(storageKey(key, prefixes));
|
|
105
|
+
await base.delete(metaKey(key, prefixes));
|
|
106
|
+
return primary;
|
|
107
|
+
};
|
|
108
|
+
const deleteByTags = async (tags) => {
|
|
109
|
+
const prefixes = resolveTenantPrefixes();
|
|
110
|
+
const scopedTags = buildTagSet(tags, prefixes, false);
|
|
111
|
+
if (!scopedTags.length)
|
|
112
|
+
return 0;
|
|
113
|
+
const removed = await base.deleteByTags(scopedTags);
|
|
114
|
+
return normalizeDeletionCount(removed);
|
|
115
|
+
};
|
|
116
|
+
const clear = async () => {
|
|
117
|
+
const prefixes = resolveTenantPrefixes();
|
|
118
|
+
const removed = await base.deleteByTags([prefixes.scopeTag]);
|
|
119
|
+
return normalizeDeletionCount(removed);
|
|
120
|
+
};
|
|
121
|
+
const keys = async (pattern) => {
|
|
122
|
+
const prefixes = resolveTenantPrefixes();
|
|
123
|
+
const metaPattern = `${prefixes.keyPrefix}meta:*`;
|
|
124
|
+
const metaKeys = await base.keys(metaPattern);
|
|
125
|
+
const originals = [];
|
|
126
|
+
for (const metaKey of metaKeys) {
|
|
127
|
+
const metaValue = await base.get(metaKey, { returnExpired: true });
|
|
128
|
+
if (!metaValue)
|
|
129
|
+
continue;
|
|
130
|
+
const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null);
|
|
131
|
+
const original = typeof metaValue === 'string' ? metaValue : metadata === null || metadata === void 0 ? void 0 : metadata.key;
|
|
132
|
+
if (!original)
|
|
133
|
+
continue;
|
|
134
|
+
if (pattern && !matchPattern(original, pattern))
|
|
135
|
+
continue;
|
|
136
|
+
originals.push(original);
|
|
137
|
+
}
|
|
138
|
+
return originals;
|
|
139
|
+
};
|
|
140
|
+
const stats = async () => {
|
|
141
|
+
var _a;
|
|
142
|
+
const prefixes = resolveTenantPrefixes();
|
|
143
|
+
const metaKeys = await base.keys(`${prefixes.keyPrefix}meta:*`);
|
|
144
|
+
let size = 0;
|
|
145
|
+
let expired = 0;
|
|
146
|
+
const now = Date.now();
|
|
147
|
+
for (const metaKey of metaKeys) {
|
|
148
|
+
const metaValue = await base.get(metaKey, { returnExpired: true });
|
|
149
|
+
if (!metaValue)
|
|
150
|
+
continue;
|
|
151
|
+
const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null);
|
|
152
|
+
const original = typeof metaValue === 'string' ? metaValue : metadata === null || metadata === void 0 ? void 0 : metadata.key;
|
|
153
|
+
if (!original)
|
|
154
|
+
continue;
|
|
155
|
+
size++;
|
|
156
|
+
const expiresAt = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.expiresAt) !== null && _a !== void 0 ? _a : null;
|
|
157
|
+
if (expiresAt !== null && expiresAt <= now)
|
|
158
|
+
expired++;
|
|
159
|
+
}
|
|
160
|
+
return { size, expired };
|
|
161
|
+
};
|
|
162
|
+
const cleanup = base.cleanup
|
|
163
|
+
? async () => normalizeDeletionCount(await base.cleanup())
|
|
164
|
+
: undefined;
|
|
165
|
+
const close = base.close
|
|
166
|
+
? async () => base.close()
|
|
167
|
+
: undefined;
|
|
168
|
+
return {
|
|
169
|
+
get,
|
|
170
|
+
set,
|
|
171
|
+
has,
|
|
172
|
+
delete: del,
|
|
173
|
+
deleteByTags,
|
|
174
|
+
clear,
|
|
175
|
+
keys,
|
|
176
|
+
stats,
|
|
177
|
+
cleanup,
|
|
178
|
+
close,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Cache service that provides a unified interface to different cache strategies
|
|
183
|
+
*
|
|
184
|
+
* Configuration via environment variables:
|
|
185
|
+
* - CACHE_STRATEGY: 'memory' | 'redis' | 'sqlite' | 'jsonfile' (default: 'memory')
|
|
186
|
+
* - CACHE_TTL: Default TTL in milliseconds (optional)
|
|
187
|
+
* - CACHE_REDIS_URL: Redis connection URL (for redis strategy)
|
|
188
|
+
* - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)
|
|
189
|
+
* - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)
|
|
190
|
+
*
|
|
191
|
+
* @example
|
|
192
|
+
* const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })
|
|
193
|
+
* await cache.set('user:123', { name: 'John' }, { tags: ['users', 'user:123'] })
|
|
194
|
+
* const user = await cache.get('user:123')
|
|
195
|
+
* await cache.deleteByTags(['users']) // Invalidate all user-related cache
|
|
196
|
+
*/
|
|
197
|
+
export function createCacheService(options) {
|
|
198
|
+
var _a, _b, _c;
|
|
199
|
+
const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)
|
|
200
|
+
? process.env.CACHE_STRATEGY
|
|
201
|
+
: undefined;
|
|
202
|
+
const strategyType = (_b = (_a = options === null || options === void 0 ? void 0 : options.strategy) !== null && _a !== void 0 ? _a : envStrategy) !== null && _b !== void 0 ? _b : 'memory';
|
|
203
|
+
const envTtl = process.env.CACHE_TTL;
|
|
204
|
+
const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined;
|
|
205
|
+
const defaultTtl = (_c = options === null || options === void 0 ? void 0 : options.defaultTtl) !== null && _c !== void 0 ? _c : (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined);
|
|
206
|
+
const baseStrategy = createStrategyForType(strategyType, options, defaultTtl);
|
|
207
|
+
const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl);
|
|
208
|
+
return createTenantAwareWrapper(resilientStrategy);
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* CacheService class wrapper for DI integration
|
|
212
|
+
* Provides the same interface as the functional API but as a class
|
|
213
|
+
*/
|
|
214
|
+
export class CacheService {
|
|
215
|
+
constructor(options) {
|
|
216
|
+
this.strategy = createCacheService(options);
|
|
217
|
+
}
|
|
218
|
+
async get(key, options) {
|
|
219
|
+
return this.strategy.get(key, options);
|
|
220
|
+
}
|
|
221
|
+
async set(key, value, options) {
|
|
222
|
+
return this.strategy.set(key, value, options);
|
|
223
|
+
}
|
|
224
|
+
async has(key) {
|
|
225
|
+
return this.strategy.has(key);
|
|
226
|
+
}
|
|
227
|
+
async delete(key) {
|
|
228
|
+
return this.strategy.delete(key);
|
|
229
|
+
}
|
|
230
|
+
async deleteByTags(tags) {
|
|
231
|
+
return this.strategy.deleteByTags(tags);
|
|
232
|
+
}
|
|
233
|
+
async clear() {
|
|
234
|
+
return this.strategy.clear();
|
|
235
|
+
}
|
|
236
|
+
async keys(pattern) {
|
|
237
|
+
return this.strategy.keys(pattern);
|
|
238
|
+
}
|
|
239
|
+
async stats() {
|
|
240
|
+
return this.strategy.stats();
|
|
241
|
+
}
|
|
242
|
+
async cleanup() {
|
|
243
|
+
if (this.strategy.cleanup) {
|
|
244
|
+
return this.strategy.cleanup();
|
|
245
|
+
}
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
async close() {
|
|
249
|
+
if (this.strategy.close) {
|
|
250
|
+
return this.strategy.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
function createStrategyForType(strategyType, options, defaultTtl) {
|
|
255
|
+
switch (strategyType) {
|
|
256
|
+
case 'redis':
|
|
257
|
+
return createRedisStrategy(options === null || options === void 0 ? void 0 : options.redisUrl, { defaultTtl });
|
|
258
|
+
case 'sqlite':
|
|
259
|
+
return createSqliteStrategy(options === null || options === void 0 ? void 0 : options.sqlitePath, { defaultTtl });
|
|
260
|
+
case 'jsonfile':
|
|
261
|
+
return createJsonFileStrategy(options === null || options === void 0 ? void 0 : options.jsonFilePath, { defaultTtl });
|
|
262
|
+
case 'memory':
|
|
263
|
+
default:
|
|
264
|
+
return createMemoryStrategy({ defaultTtl });
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
function withDependencyFallback(strategy, strategyType, defaultTtl) {
|
|
268
|
+
if (strategyType === 'memory')
|
|
269
|
+
return strategy;
|
|
270
|
+
let activeStrategy = strategy;
|
|
271
|
+
let fallbackStrategy = null;
|
|
272
|
+
let warned = false;
|
|
273
|
+
const ensureFallback = (error) => {
|
|
274
|
+
if (!fallbackStrategy) {
|
|
275
|
+
fallbackStrategy = createMemoryStrategy({ defaultTtl });
|
|
276
|
+
}
|
|
277
|
+
if (!warned) {
|
|
278
|
+
const dependencyMessage = error.dependency
|
|
279
|
+
? ` (missing dependency: ${error.dependency})`
|
|
280
|
+
: '';
|
|
281
|
+
console.warn(`[cache] ${error.strategy} strategy unavailable${dependencyMessage}. Falling back to memory strategy.`);
|
|
282
|
+
warned = true;
|
|
283
|
+
}
|
|
284
|
+
activeStrategy = fallbackStrategy;
|
|
285
|
+
};
|
|
286
|
+
const wrapMethod = (method) => {
|
|
287
|
+
const handler = async (...args) => {
|
|
288
|
+
const fn = activeStrategy[method];
|
|
289
|
+
if (!fn) {
|
|
290
|
+
return undefined;
|
|
291
|
+
}
|
|
292
|
+
try {
|
|
293
|
+
return await fn(...args);
|
|
294
|
+
}
|
|
295
|
+
catch (error) {
|
|
296
|
+
if (error instanceof CacheDependencyUnavailableError) {
|
|
297
|
+
ensureFallback(error);
|
|
298
|
+
const fallbackFn = activeStrategy[method];
|
|
299
|
+
if (!fallbackFn) {
|
|
300
|
+
return undefined;
|
|
301
|
+
}
|
|
302
|
+
return fallbackFn(...args);
|
|
303
|
+
}
|
|
304
|
+
throw error;
|
|
305
|
+
}
|
|
306
|
+
};
|
|
307
|
+
return handler;
|
|
308
|
+
};
|
|
309
|
+
return {
|
|
310
|
+
get: wrapMethod('get'),
|
|
311
|
+
set: wrapMethod('set'),
|
|
312
|
+
has: wrapMethod('has'),
|
|
313
|
+
delete: wrapMethod('delete'),
|
|
314
|
+
deleteByTags: wrapMethod('deleteByTags'),
|
|
315
|
+
clear: wrapMethod('clear'),
|
|
316
|
+
keys: wrapMethod('keys'),
|
|
317
|
+
stats: wrapMethod('stats'),
|
|
318
|
+
cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,
|
|
319
|
+
close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CacheStrategy } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* JSON file cache strategy with tag support
|
|
4
|
+
* Persistent across process restarts, stored in JSON files
|
|
5
|
+
* Simple and requires no external dependencies, but not suitable for high-performance scenarios
|
|
6
|
+
*/
|
|
7
|
+
export declare function createJsonFileStrategy(filePath?: string, options?: {
|
|
8
|
+
defaultTtl?: number;
|
|
9
|
+
}): CacheStrategy;
|
|
10
|
+
//# sourceMappingURL=jsonfile.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonfile.d.ts","sourceRoot":"","sources":["../../src/strategies/jsonfile.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAA4D,MAAM,UAAU,CAAA;AAIvG;;;;GAIG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,CAAC,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CA+O1G"}
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
/**
|
|
4
|
+
* JSON file cache strategy with tag support
|
|
5
|
+
* Persistent across process restarts, stored in JSON files
|
|
6
|
+
* Simple and requires no external dependencies, but not suitable for high-performance scenarios
|
|
7
|
+
*/
|
|
8
|
+
export function createJsonFileStrategy(filePath, options) {
|
|
9
|
+
const defaultTtl = options === null || options === void 0 ? void 0 : options.defaultTtl;
|
|
10
|
+
const cacheFile = filePath || process.env.CACHE_JSON_FILE_PATH || '.cache.json';
|
|
11
|
+
const dir = path.dirname(cacheFile);
|
|
12
|
+
function ensureDir() {
|
|
13
|
+
if (!fs.existsSync(dir)) {
|
|
14
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
function readData() {
|
|
18
|
+
ensureDir();
|
|
19
|
+
if (!fs.existsSync(cacheFile)) {
|
|
20
|
+
return { entries: {}, tagIndex: {} };
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
const content = fs.readFileSync(cacheFile, 'utf8');
|
|
24
|
+
return JSON.parse(content);
|
|
25
|
+
}
|
|
26
|
+
catch (_a) {
|
|
27
|
+
return { entries: {}, tagIndex: {} };
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
function writeData(data) {
|
|
31
|
+
ensureDir();
|
|
32
|
+
fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8');
|
|
33
|
+
}
|
|
34
|
+
function isExpired(entry) {
|
|
35
|
+
if (entry.expiresAt === null)
|
|
36
|
+
return false;
|
|
37
|
+
return Date.now() > entry.expiresAt;
|
|
38
|
+
}
|
|
39
|
+
function matchPattern(key, pattern) {
|
|
40
|
+
const regexPattern = pattern
|
|
41
|
+
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
42
|
+
.replace(/\*/g, '.*')
|
|
43
|
+
.replace(/\?/g, '.');
|
|
44
|
+
const regex = new RegExp(`^${regexPattern}$`);
|
|
45
|
+
return regex.test(key);
|
|
46
|
+
}
|
|
47
|
+
function addToTagIndex(data, key, tags) {
|
|
48
|
+
for (const tag of tags) {
|
|
49
|
+
if (!data.tagIndex[tag]) {
|
|
50
|
+
data.tagIndex[tag] = [];
|
|
51
|
+
}
|
|
52
|
+
if (!data.tagIndex[tag].includes(key)) {
|
|
53
|
+
data.tagIndex[tag].push(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function removeFromTagIndex(data, key, tags) {
|
|
58
|
+
for (const tag of tags) {
|
|
59
|
+
if (data.tagIndex[tag]) {
|
|
60
|
+
data.tagIndex[tag] = data.tagIndex[tag].filter((k) => k !== key);
|
|
61
|
+
if (data.tagIndex[tag].length === 0) {
|
|
62
|
+
delete data.tagIndex[tag];
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const get = async (key, options) => {
|
|
68
|
+
const data = readData();
|
|
69
|
+
const entry = data.entries[key];
|
|
70
|
+
if (!entry)
|
|
71
|
+
return null;
|
|
72
|
+
if (isExpired(entry)) {
|
|
73
|
+
if (options === null || options === void 0 ? void 0 : options.returnExpired) {
|
|
74
|
+
return entry.value;
|
|
75
|
+
}
|
|
76
|
+
// Clean up expired entry
|
|
77
|
+
removeFromTagIndex(data, key, entry.tags);
|
|
78
|
+
delete data.entries[key];
|
|
79
|
+
writeData(data);
|
|
80
|
+
return null;
|
|
81
|
+
}
|
|
82
|
+
return entry.value;
|
|
83
|
+
};
|
|
84
|
+
const set = async (key, value, options) => {
|
|
85
|
+
var _a;
|
|
86
|
+
const data = readData();
|
|
87
|
+
// Remove old entry from tag index if it exists
|
|
88
|
+
const oldEntry = data.entries[key];
|
|
89
|
+
if (oldEntry) {
|
|
90
|
+
removeFromTagIndex(data, key, oldEntry.tags);
|
|
91
|
+
}
|
|
92
|
+
const ttl = (_a = options === null || options === void 0 ? void 0 : options.ttl) !== null && _a !== void 0 ? _a : defaultTtl;
|
|
93
|
+
const tags = (options === null || options === void 0 ? void 0 : options.tags) || [];
|
|
94
|
+
const expiresAt = ttl ? Date.now() + ttl : null;
|
|
95
|
+
const entry = {
|
|
96
|
+
key,
|
|
97
|
+
value,
|
|
98
|
+
tags,
|
|
99
|
+
expiresAt,
|
|
100
|
+
createdAt: Date.now(),
|
|
101
|
+
};
|
|
102
|
+
data.entries[key] = entry;
|
|
103
|
+
addToTagIndex(data, key, tags);
|
|
104
|
+
writeData(data);
|
|
105
|
+
};
|
|
106
|
+
const has = async (key) => {
|
|
107
|
+
const data = readData();
|
|
108
|
+
const entry = data.entries[key];
|
|
109
|
+
if (!entry)
|
|
110
|
+
return false;
|
|
111
|
+
if (isExpired(entry)) {
|
|
112
|
+
removeFromTagIndex(data, key, entry.tags);
|
|
113
|
+
delete data.entries[key];
|
|
114
|
+
writeData(data);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
return true;
|
|
118
|
+
};
|
|
119
|
+
const deleteKey = async (key) => {
|
|
120
|
+
const data = readData();
|
|
121
|
+
const entry = data.entries[key];
|
|
122
|
+
if (!entry)
|
|
123
|
+
return false;
|
|
124
|
+
removeFromTagIndex(data, key, entry.tags);
|
|
125
|
+
delete data.entries[key];
|
|
126
|
+
writeData(data);
|
|
127
|
+
return true;
|
|
128
|
+
};
|
|
129
|
+
const deleteByTags = async (tags) => {
|
|
130
|
+
const data = readData();
|
|
131
|
+
const keysToDelete = new Set();
|
|
132
|
+
// Collect all keys that have any of the specified tags
|
|
133
|
+
for (const tag of tags) {
|
|
134
|
+
const keys = data.tagIndex[tag] || [];
|
|
135
|
+
for (const key of keys) {
|
|
136
|
+
keysToDelete.add(key);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
// Delete all collected keys
|
|
140
|
+
for (const key of keysToDelete) {
|
|
141
|
+
const entry = data.entries[key];
|
|
142
|
+
if (entry) {
|
|
143
|
+
removeFromTagIndex(data, key, entry.tags);
|
|
144
|
+
delete data.entries[key];
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
writeData(data);
|
|
148
|
+
return keysToDelete.size;
|
|
149
|
+
};
|
|
150
|
+
const clear = async () => {
|
|
151
|
+
const data = readData();
|
|
152
|
+
const size = Object.keys(data.entries).length;
|
|
153
|
+
writeData({ entries: {}, tagIndex: {} });
|
|
154
|
+
return size;
|
|
155
|
+
};
|
|
156
|
+
const keys = async (pattern) => {
|
|
157
|
+
const data = readData();
|
|
158
|
+
const allKeys = Object.keys(data.entries);
|
|
159
|
+
if (!pattern)
|
|
160
|
+
return allKeys;
|
|
161
|
+
return allKeys.filter((key) => matchPattern(key, pattern));
|
|
162
|
+
};
|
|
163
|
+
const stats = async () => {
|
|
164
|
+
const data = readData();
|
|
165
|
+
const allEntries = Object.values(data.entries);
|
|
166
|
+
let expired = 0;
|
|
167
|
+
for (const entry of allEntries) {
|
|
168
|
+
if (isExpired(entry)) {
|
|
169
|
+
expired++;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
return { size: allEntries.length, expired };
|
|
173
|
+
};
|
|
174
|
+
const cleanup = async () => {
|
|
175
|
+
const data = readData();
|
|
176
|
+
let removed = 0;
|
|
177
|
+
const keysToRemove = [];
|
|
178
|
+
for (const [key, entry] of Object.entries(data.entries)) {
|
|
179
|
+
if (isExpired(entry)) {
|
|
180
|
+
keysToRemove.push(key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const key of keysToRemove) {
|
|
184
|
+
const entry = data.entries[key];
|
|
185
|
+
removeFromTagIndex(data, key, entry.tags);
|
|
186
|
+
delete data.entries[key];
|
|
187
|
+
removed++;
|
|
188
|
+
}
|
|
189
|
+
if (removed > 0) {
|
|
190
|
+
writeData(data);
|
|
191
|
+
}
|
|
192
|
+
return removed;
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
get,
|
|
196
|
+
set,
|
|
197
|
+
has,
|
|
198
|
+
delete: deleteKey,
|
|
199
|
+
deleteByTags,
|
|
200
|
+
clear,
|
|
201
|
+
keys,
|
|
202
|
+
stats,
|
|
203
|
+
cleanup,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CacheStrategy } from '../types';
|
|
2
|
+
/**
|
|
3
|
+
* In-memory cache strategy with tag support
|
|
4
|
+
* Fast but data is lost when process restarts
|
|
5
|
+
*/
|
|
6
|
+
export declare function createMemoryStrategy(options?: {
|
|
7
|
+
defaultTtl?: number;
|
|
8
|
+
}): CacheStrategy;
|
|
9
|
+
//# sourceMappingURL=memory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory.d.ts","sourceRoot":"","sources":["../../src/strategies/memory.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAA4D,MAAM,UAAU,CAAA;AAEvG;;;GAGG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,CAAC,EAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,aAAa,CAkLrF"}
|