@open-mercato/cache 0.6.3-develop.3562.1.71049275ff → 0.6.3-develop.3576.1.51c9149577
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/.turbo/turbo-build.log +1 -1
- package/dist/patterns.js +36 -0
- package/dist/patterns.js.map +7 -0
- package/dist/service.js +2 -6
- package/dist/service.js.map +2 -2
- package/dist/strategies/jsonfile.js +2 -6
- package/dist/strategies/jsonfile.js.map +2 -2
- package/dist/strategies/memory.js +2 -6
- package/dist/strategies/memory.js.map +2 -2
- package/dist/strategies/redis.js +2 -6
- package/dist/strategies/redis.js.map +2 -2
- package/dist/strategies/sqlite.js +2 -6
- package/dist/strategies/sqlite.js.map +2 -2
- package/package.json +1 -1
- package/src/__tests__/patterns.test.ts +16 -0
- package/src/patterns.ts +40 -0
- package/src/service.ts +2 -10
- package/src/strategies/jsonfile.ts +2 -10
- package/src/strategies/memory.ts +2 -10
- package/src/strategies/redis.ts +2 -10
- package/src/strategies/sqlite.ts +2 -10
package/.turbo/turbo-build.log
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
[build:cache] found
|
|
1
|
+
[build:cache] found 11 entry points
|
|
2
2
|
[build:cache] built successfully
|
package/dist/patterns.js
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
function matchCacheKeyPattern(key, pattern) {
|
|
2
|
+
let patternIndex = 0;
|
|
3
|
+
let keyIndex = 0;
|
|
4
|
+
let starIndex = -1;
|
|
5
|
+
let keyRetryIndex = 0;
|
|
6
|
+
while (keyIndex < key.length) {
|
|
7
|
+
const patternChar = pattern[patternIndex];
|
|
8
|
+
const keyChar = key[keyIndex];
|
|
9
|
+
if (patternChar === "*") {
|
|
10
|
+
starIndex = patternIndex;
|
|
11
|
+
keyRetryIndex = keyIndex;
|
|
12
|
+
patternIndex += 1;
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
if (patternChar === "?" || patternChar === keyChar) {
|
|
16
|
+
patternIndex += 1;
|
|
17
|
+
keyIndex += 1;
|
|
18
|
+
continue;
|
|
19
|
+
}
|
|
20
|
+
if (starIndex !== -1) {
|
|
21
|
+
patternIndex = starIndex + 1;
|
|
22
|
+
keyRetryIndex += 1;
|
|
23
|
+
keyIndex = keyRetryIndex;
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
while (pattern[patternIndex] === "*") {
|
|
29
|
+
patternIndex += 1;
|
|
30
|
+
}
|
|
31
|
+
return patternIndex === pattern.length;
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
matchCacheKeyPattern
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=patterns.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/patterns.ts"],
|
|
4
|
+
"sourcesContent": ["export function matchCacheKeyPattern(key: string, pattern: string): boolean {\n let patternIndex = 0\n let keyIndex = 0\n let starIndex = -1\n let keyRetryIndex = 0\n\n while (keyIndex < key.length) {\n const patternChar = pattern[patternIndex]\n const keyChar = key[keyIndex]\n\n if (patternChar === '*') {\n starIndex = patternIndex\n keyRetryIndex = keyIndex\n patternIndex += 1\n continue\n }\n\n if (patternChar === '?' || patternChar === keyChar) {\n patternIndex += 1\n keyIndex += 1\n continue\n }\n\n if (starIndex !== -1) {\n patternIndex = starIndex + 1\n keyRetryIndex += 1\n keyIndex = keyRetryIndex\n continue\n }\n\n return false\n }\n\n while (pattern[patternIndex] === '*') {\n patternIndex += 1\n }\n\n return patternIndex === pattern.length\n}\n\n"],
|
|
5
|
+
"mappings": "AAAO,SAAS,qBAAqB,KAAa,SAA0B;AAC1E,MAAI,eAAe;AACnB,MAAI,WAAW;AACf,MAAI,YAAY;AAChB,MAAI,gBAAgB;AAEpB,SAAO,WAAW,IAAI,QAAQ;AAC5B,UAAM,cAAc,QAAQ,YAAY;AACxC,UAAM,UAAU,IAAI,QAAQ;AAE5B,QAAI,gBAAgB,KAAK;AACvB,kBAAY;AACZ,sBAAgB;AAChB,sBAAgB;AAChB;AAAA,IACF;AAEA,QAAI,gBAAgB,OAAO,gBAAgB,SAAS;AAClD,sBAAgB;AAChB,kBAAY;AACZ;AAAA,IACF;AAEA,QAAI,cAAc,IAAI;AACpB,qBAAe,YAAY;AAC3B,uBAAiB;AACjB,iBAAW;AACX;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,SAAO,QAAQ,YAAY,MAAM,KAAK;AACpC,oBAAgB;AAAA,EAClB;AAEA,SAAO,iBAAiB,QAAQ;AAClC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/service.js
CHANGED
|
@@ -5,6 +5,7 @@ import { createJsonFileStrategy } from "./strategies/jsonfile.js";
|
|
|
5
5
|
import { getCurrentCacheTenant } from "./tenantContext.js";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { CacheDependencyUnavailableError } from "./errors.js";
|
|
8
|
+
import { matchCacheKeyPattern } from "./patterns.js";
|
|
8
9
|
function normalizeTenantKey(raw) {
|
|
9
10
|
const value = typeof raw === "string" ? raw.trim() : "";
|
|
10
11
|
if (!value) return "global";
|
|
@@ -55,11 +56,6 @@ function buildTagSet(tags, prefixes, includeScope) {
|
|
|
55
56
|
}
|
|
56
57
|
return Array.from(scoped);
|
|
57
58
|
}
|
|
58
|
-
function matchPattern(value, pattern) {
|
|
59
|
-
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
60
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
61
|
-
return regex.test(value);
|
|
62
|
-
}
|
|
63
59
|
function createTenantAwareWrapper(base) {
|
|
64
60
|
function normalizeDeletionCount(raw) {
|
|
65
61
|
if (!raw) return raw;
|
|
@@ -115,7 +111,7 @@ function createTenantAwareWrapper(base) {
|
|
|
115
111
|
const metadata = typeof metaValue === "string" ? null : isCacheMetadata(metaValue) ? metaValue : null;
|
|
116
112
|
const original = typeof metaValue === "string" ? metaValue : metadata?.key;
|
|
117
113
|
if (!original) continue;
|
|
118
|
-
if (pattern && !
|
|
114
|
+
if (pattern && !matchCacheKeyPattern(original, pattern)) continue;
|
|
119
115
|
originals.push(original);
|
|
120
116
|
}
|
|
121
117
|
return originals;
|
package/dist/service.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/service.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CacheStrategy, CacheServiceOptions, CacheGetOptions, CacheSetOptions, CacheValue } from './types'\nimport { createMemoryStrategy } from './strategies/memory'\nimport { createRedisStrategy } from './strategies/redis'\nimport { createSqliteStrategy } from './strategies/sqlite'\nimport { createJsonFileStrategy } from './strategies/jsonfile'\nimport { getCurrentCacheTenant } from './tenantContext'\nimport { createHash } from 'node:crypto'\nimport { CacheDependencyUnavailableError } from './errors'\n\nfunction normalizeTenantKey(raw: string | null | undefined): string {\n const value = typeof raw === 'string' ? raw.trim() : ''\n if (!value) return 'global'\n return value.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\ntype TenantPrefixes = {\n keyPrefix: string\n tagPrefix: string\n scopeTag: string\n}\n\ntype CacheMetadata = {\n key: string\n expiresAt: number | null\n}\n\nfunction isCacheMetadata(value: CacheValue | null): value is CacheMetadata {\n if (typeof value !== 'object' || value === null) {\n return false\n }\n const record = value as Record<string, unknown>\n const hasValidKey = typeof record.key === 'string'\n const hasValidExpiresAt =\n !('expiresAt' in record)\n || record.expiresAt === null\n || typeof record.expiresAt === 'number'\n\n return hasValidKey && hasValidExpiresAt\n}\n\ntype CacheStrategyName = NonNullable<CacheServiceOptions['strategy']>\nconst KNOWN_STRATEGIES: CacheStrategyName[] = ['memory', 'redis', 'sqlite', 'jsonfile']\n\nfunction isCacheStrategyName(value: string | undefined): value is CacheStrategyName {\n if (!value) return false\n return KNOWN_STRATEGIES.includes(value as CacheStrategyName)\n}\n\nfunction resolveTenantPrefixes(): TenantPrefixes {\n const tenant = normalizeTenantKey(getCurrentCacheTenant())\n const base = `tenant:${tenant}:`\n return {\n keyPrefix: `${base}key:`,\n tagPrefix: `${base}tag:`,\n scopeTag: `${base}tag:__scope__`,\n }\n}\n\nfunction hashIdentifier(input: string): string {\n return createHash('sha1').update(input).digest('hex')\n}\n\nfunction storageKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}k:${hashIdentifier(originalKey)}`\n}\n\nfunction metaKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}meta:${hashIdentifier(originalKey)}`\n}\n\nfunction hashedTag(tag: string, prefixes: TenantPrefixes): string {\n return `${prefixes.tagPrefix}t:${hashIdentifier(tag)}`\n}\n\nfunction buildTagSet(tags: string[] | undefined, prefixes: TenantPrefixes, includeScope: boolean): string[] {\n const scoped = new Set<string>()\n if (includeScope) scoped.add(prefixes.scopeTag)\n if (Array.isArray(tags)) {\n for (const tag of tags) {\n if (typeof tag === 'string' && tag.length > 0) scoped.add(hashedTag(tag, prefixes))\n }\n }\n return Array.from(scoped)\n}\n\nfunction matchPattern(value: string, pattern: string): boolean {\n const regexPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*')\n .replace(/\\?/g, '.')\n const regex = new RegExp(`^${regexPattern}$`)\n return regex.test(value)\n}\n\nfunction createTenantAwareWrapper(base: CacheStrategy): CacheStrategy {\n function normalizeDeletionCount(raw: number): number {\n if (!raw) return raw\n if (!Number.isFinite(raw)) return raw\n return Math.ceil(raw / 2)\n }\n\n const get = async (key: string, options?: CacheGetOptions) => {\n const prefixes = resolveTenantPrefixes()\n return base.get(storageKey(key, prefixes), options)\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions) => {\n const prefixes = resolveTenantPrefixes()\n const hashedTags = buildTagSet(options?.tags, prefixes, true)\n const ttl = options?.ttl ?? undefined\n const nextOptions: CacheSetOptions | undefined = options\n ? { ...options, tags: hashedTags }\n : { tags: hashedTags }\n await base.set(storageKey(key, prefixes), value, nextOptions)\n const metaPayload: CacheMetadata = { key, expiresAt: ttl ? Date.now() + ttl : null }\n await base.set(metaKey(key, prefixes), metaPayload, {\n ttl,\n tags: hashedTags,\n })\n }\n\n const has = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n return base.has(storageKey(key, prefixes))\n }\n\n const del = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n const primary = await base.delete(storageKey(key, prefixes))\n await base.delete(metaKey(key, prefixes))\n return primary\n }\n\n const deleteByTags = async (tags: string[]) => {\n const prefixes = resolveTenantPrefixes()\n const scopedTags = buildTagSet(tags, prefixes, false)\n if (!scopedTags.length) return 0\n const removed = await base.deleteByTags(scopedTags)\n return normalizeDeletionCount(removed)\n }\n\n const clear = async () => {\n const prefixes = resolveTenantPrefixes()\n const removed = await base.deleteByTags([prefixes.scopeTag])\n return normalizeDeletionCount(removed)\n }\n\n const keys = async (pattern?: string) => {\n const prefixes = resolveTenantPrefixes()\n const metaPattern = `${prefixes.keyPrefix}meta:*`\n const metaKeys = await base.keys(metaPattern)\n const originals: string[] = []\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n if (pattern && !matchPattern(original, pattern)) continue\n originals.push(original)\n }\n return originals\n }\n\n const stats = async () => {\n const prefixes = resolveTenantPrefixes()\n const metaKeys = await base.keys(`${prefixes.keyPrefix}meta:*`)\n let size = 0\n let expired = 0\n const now = Date.now()\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n size++\n const expiresAt = metadata?.expiresAt ?? null\n if (expiresAt !== null && expiresAt <= now) expired++\n }\n return { size, expired }\n }\n\n const cleanup = base.cleanup\n ? async () => normalizeDeletionCount(await base.cleanup!())\n : undefined\n\n const close = base.close\n ? async () => base.close!()\n : undefined\n\n return {\n get,\n set,\n has,\n delete: del,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n\n/**\n * Cache service that provides a unified interface to different cache strategies\n * \n * Configuration via environment variables:\n * - CACHE_STRATEGY: 'memory' | 'redis' | 'sqlite' | 'jsonfile' (default: 'memory')\n * - CACHE_TTL: Default TTL in milliseconds (optional)\n * - CACHE_REDIS_URL: Redis connection URL (for redis strategy)\n * - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)\n * - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)\n * \n * @example\n * const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })\n * await cache.set('user:123', { name: 'John' }, { tags: ['users', 'user:123'] })\n * const user = await cache.get('user:123')\n * await cache.deleteByTags(['users']) // Invalidate all user-related cache\n */\nexport function createCacheService(options?: CacheServiceOptions): CacheStrategy {\n const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)\n ? process.env.CACHE_STRATEGY\n : undefined\n const strategyType: CacheStrategyName = options?.strategy ?? envStrategy ?? 'memory'\n\n const envTtl = process.env.CACHE_TTL\n const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined\n const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)\n\n const baseStrategy = createStrategyForType(strategyType, options, defaultTtl)\n const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl)\n\n return createTenantAwareWrapper(resilientStrategy)\n}\n\n/**\n * CacheService class wrapper for DI integration\n * Provides the same interface as the functional API but as a class\n */\nexport class CacheService implements CacheStrategy {\n private strategy: CacheStrategy\n\n constructor(options?: CacheServiceOptions) {\n this.strategy = createCacheService(options)\n }\n\n async get(key: string, options?: CacheGetOptions): Promise<CacheValue | null> {\n return this.strategy.get(key, options)\n }\n\n async set(key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> {\n return this.strategy.set(key, value, options)\n }\n\n async has(key: string): Promise<boolean> {\n return this.strategy.has(key)\n }\n\n async delete(key: string): Promise<boolean> {\n return this.strategy.delete(key)\n }\n\n async deleteByTags(tags: string[]): Promise<number> {\n return this.strategy.deleteByTags(tags)\n }\n\n async clear(): Promise<number> {\n return this.strategy.clear()\n }\n\n async keys(pattern?: string): Promise<string[]> {\n return this.strategy.keys(pattern)\n }\n\n async stats(): Promise<{ size: number; expired: number }> {\n return this.strategy.stats()\n }\n\n async cleanup(): Promise<number> {\n if (this.strategy.cleanup) {\n return this.strategy.cleanup()\n }\n return 0\n }\n\n async close(): Promise<void> {\n if (this.strategy.close) {\n return this.strategy.close()\n }\n }\n}\n\nfunction createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number): CacheStrategy {\n switch (strategyType) {\n case 'redis':\n return createRedisStrategy(options?.redisUrl, { defaultTtl })\n case 'sqlite':\n return createSqliteStrategy(options?.sqlitePath, { defaultTtl })\n case 'jsonfile':\n return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })\n case 'memory':\n default:\n return createMemoryStrategy({ defaultTtl })\n }\n}\n\nfunction describeDependencyFailure(error: CacheDependencyUnavailableError): string {\n const originalError = error.originalError\n if (!(originalError instanceof Error)) {\n return `missing dependency: ${error.dependency}`\n }\n\n const message = originalError.message.trim()\n if (message.length === 0) {\n return `missing dependency: ${error.dependency}`\n }\n\n if (message.includes('compiled against a different Node.js version') || message.includes('NODE_MODULE_VERSION')) {\n return `${error.dependency} native module needs rebuild for the current Node.js version`\n }\n\n if (message.includes('Cannot find module')) {\n return `missing dependency: ${error.dependency}`\n }\n\n return `${error.dependency} failed to load`\n}\n\nfunction withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStrategyName, defaultTtl?: number): CacheStrategy {\n if (strategyType === 'memory') return strategy\n\n let activeStrategy = strategy\n let fallbackStrategy: CacheStrategy | null = null\n let warned = false\n\n const ensureFallback = (error: CacheDependencyUnavailableError) => {\n if (!fallbackStrategy) {\n fallbackStrategy = createMemoryStrategy({ defaultTtl })\n }\n if (!warned) {\n const dependencyMessage = error.dependency\n ? ` (${describeDependencyFailure(error)})`\n : ''\n console.warn(`[cache] ${error.strategy} strategy unavailable${dependencyMessage}. Falling back to memory strategy.`)\n warned = true\n }\n activeStrategy = fallbackStrategy\n }\n\n const wrapMethod = <K extends keyof CacheStrategy>(method: K): CacheStrategy[K] => {\n const handler = async (...args: Parameters<NonNullable<CacheStrategy[K]>>) => {\n const fn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n\n try {\n return await fn(...args)\n } catch (error) {\n if (error instanceof CacheDependencyUnavailableError) {\n ensureFallback(error)\n const fallbackFn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fallbackFn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n return fallbackFn(...args)\n }\n throw error\n }\n }\n\n return handler as CacheStrategy[K]\n }\n\n return {\n get: wrapMethod('get'),\n set: wrapMethod('set'),\n has: wrapMethod('has'),\n delete: wrapMethod('delete'),\n deleteByTags: wrapMethod('deleteByTags'),\n clear: wrapMethod('clear'),\n keys: wrapMethod('keys'),\n stats: wrapMethod('stats'),\n cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,\n close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAC3B,SAAS,uCAAuC;
|
|
4
|
+
"sourcesContent": ["import type { CacheStrategy, CacheServiceOptions, CacheGetOptions, CacheSetOptions, CacheValue } from './types'\nimport { createMemoryStrategy } from './strategies/memory'\nimport { createRedisStrategy } from './strategies/redis'\nimport { createSqliteStrategy } from './strategies/sqlite'\nimport { createJsonFileStrategy } from './strategies/jsonfile'\nimport { getCurrentCacheTenant } from './tenantContext'\nimport { createHash } from 'node:crypto'\nimport { CacheDependencyUnavailableError } from './errors'\nimport { matchCacheKeyPattern } from './patterns'\n\nfunction normalizeTenantKey(raw: string | null | undefined): string {\n const value = typeof raw === 'string' ? raw.trim() : ''\n if (!value) return 'global'\n return value.replace(/[^a-zA-Z0-9._-]/g, '_')\n}\n\ntype TenantPrefixes = {\n keyPrefix: string\n tagPrefix: string\n scopeTag: string\n}\n\ntype CacheMetadata = {\n key: string\n expiresAt: number | null\n}\n\nfunction isCacheMetadata(value: CacheValue | null): value is CacheMetadata {\n if (typeof value !== 'object' || value === null) {\n return false\n }\n const record = value as Record<string, unknown>\n const hasValidKey = typeof record.key === 'string'\n const hasValidExpiresAt =\n !('expiresAt' in record)\n || record.expiresAt === null\n || typeof record.expiresAt === 'number'\n\n return hasValidKey && hasValidExpiresAt\n}\n\ntype CacheStrategyName = NonNullable<CacheServiceOptions['strategy']>\nconst KNOWN_STRATEGIES: CacheStrategyName[] = ['memory', 'redis', 'sqlite', 'jsonfile']\n\nfunction isCacheStrategyName(value: string | undefined): value is CacheStrategyName {\n if (!value) return false\n return KNOWN_STRATEGIES.includes(value as CacheStrategyName)\n}\n\nfunction resolveTenantPrefixes(): TenantPrefixes {\n const tenant = normalizeTenantKey(getCurrentCacheTenant())\n const base = `tenant:${tenant}:`\n return {\n keyPrefix: `${base}key:`,\n tagPrefix: `${base}tag:`,\n scopeTag: `${base}tag:__scope__`,\n }\n}\n\nfunction hashIdentifier(input: string): string {\n return createHash('sha1').update(input).digest('hex')\n}\n\nfunction storageKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}k:${hashIdentifier(originalKey)}`\n}\n\nfunction metaKey(originalKey: string, prefixes: TenantPrefixes): string {\n return `${prefixes.keyPrefix}meta:${hashIdentifier(originalKey)}`\n}\n\nfunction hashedTag(tag: string, prefixes: TenantPrefixes): string {\n return `${prefixes.tagPrefix}t:${hashIdentifier(tag)}`\n}\n\nfunction buildTagSet(tags: string[] | undefined, prefixes: TenantPrefixes, includeScope: boolean): string[] {\n const scoped = new Set<string>()\n if (includeScope) scoped.add(prefixes.scopeTag)\n if (Array.isArray(tags)) {\n for (const tag of tags) {\n if (typeof tag === 'string' && tag.length > 0) scoped.add(hashedTag(tag, prefixes))\n }\n }\n return Array.from(scoped)\n}\n\nfunction createTenantAwareWrapper(base: CacheStrategy): CacheStrategy {\n function normalizeDeletionCount(raw: number): number {\n if (!raw) return raw\n if (!Number.isFinite(raw)) return raw\n return Math.ceil(raw / 2)\n }\n\n const get = async (key: string, options?: CacheGetOptions) => {\n const prefixes = resolveTenantPrefixes()\n return base.get(storageKey(key, prefixes), options)\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions) => {\n const prefixes = resolveTenantPrefixes()\n const hashedTags = buildTagSet(options?.tags, prefixes, true)\n const ttl = options?.ttl ?? undefined\n const nextOptions: CacheSetOptions | undefined = options\n ? { ...options, tags: hashedTags }\n : { tags: hashedTags }\n await base.set(storageKey(key, prefixes), value, nextOptions)\n const metaPayload: CacheMetadata = { key, expiresAt: ttl ? Date.now() + ttl : null }\n await base.set(metaKey(key, prefixes), metaPayload, {\n ttl,\n tags: hashedTags,\n })\n }\n\n const has = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n return base.has(storageKey(key, prefixes))\n }\n\n const del = async (key: string) => {\n const prefixes = resolveTenantPrefixes()\n const primary = await base.delete(storageKey(key, prefixes))\n await base.delete(metaKey(key, prefixes))\n return primary\n }\n\n const deleteByTags = async (tags: string[]) => {\n const prefixes = resolveTenantPrefixes()\n const scopedTags = buildTagSet(tags, prefixes, false)\n if (!scopedTags.length) return 0\n const removed = await base.deleteByTags(scopedTags)\n return normalizeDeletionCount(removed)\n }\n\n const clear = async () => {\n const prefixes = resolveTenantPrefixes()\n const removed = await base.deleteByTags([prefixes.scopeTag])\n return normalizeDeletionCount(removed)\n }\n\n const keys = async (pattern?: string) => {\n const prefixes = resolveTenantPrefixes()\n const metaPattern = `${prefixes.keyPrefix}meta:*`\n const metaKeys = await base.keys(metaPattern)\n const originals: string[] = []\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n if (pattern && !matchCacheKeyPattern(original, pattern)) continue\n originals.push(original)\n }\n return originals\n }\n\n const stats = async () => {\n const prefixes = resolveTenantPrefixes()\n const metaKeys = await base.keys(`${prefixes.keyPrefix}meta:*`)\n let size = 0\n let expired = 0\n const now = Date.now()\n for (const metaKey of metaKeys) {\n const metaValue = await base.get(metaKey, { returnExpired: true })\n if (!metaValue) continue\n const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)\n const original = typeof metaValue === 'string' ? metaValue : metadata?.key\n if (!original) continue\n size++\n const expiresAt = metadata?.expiresAt ?? null\n if (expiresAt !== null && expiresAt <= now) expired++\n }\n return { size, expired }\n }\n\n const cleanup = base.cleanup\n ? async () => normalizeDeletionCount(await base.cleanup!())\n : undefined\n\n const close = base.close\n ? async () => base.close!()\n : undefined\n\n return {\n get,\n set,\n has,\n delete: del,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n\n/**\n * Cache service that provides a unified interface to different cache strategies\n * \n * Configuration via environment variables:\n * - CACHE_STRATEGY: 'memory' | 'redis' | 'sqlite' | 'jsonfile' (default: 'memory')\n * - CACHE_TTL: Default TTL in milliseconds (optional)\n * - CACHE_REDIS_URL: Redis connection URL (for redis strategy)\n * - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)\n * - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)\n * \n * @example\n * const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })\n * await cache.set('user:123', { name: 'John' }, { tags: ['users', 'user:123'] })\n * const user = await cache.get('user:123')\n * await cache.deleteByTags(['users']) // Invalidate all user-related cache\n */\nexport function createCacheService(options?: CacheServiceOptions): CacheStrategy {\n const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)\n ? process.env.CACHE_STRATEGY\n : undefined\n const strategyType: CacheStrategyName = options?.strategy ?? envStrategy ?? 'memory'\n\n const envTtl = process.env.CACHE_TTL\n const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined\n const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)\n\n const baseStrategy = createStrategyForType(strategyType, options, defaultTtl)\n const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl)\n\n return createTenantAwareWrapper(resilientStrategy)\n}\n\n/**\n * CacheService class wrapper for DI integration\n * Provides the same interface as the functional API but as a class\n */\nexport class CacheService implements CacheStrategy {\n private strategy: CacheStrategy\n\n constructor(options?: CacheServiceOptions) {\n this.strategy = createCacheService(options)\n }\n\n async get(key: string, options?: CacheGetOptions): Promise<CacheValue | null> {\n return this.strategy.get(key, options)\n }\n\n async set(key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> {\n return this.strategy.set(key, value, options)\n }\n\n async has(key: string): Promise<boolean> {\n return this.strategy.has(key)\n }\n\n async delete(key: string): Promise<boolean> {\n return this.strategy.delete(key)\n }\n\n async deleteByTags(tags: string[]): Promise<number> {\n return this.strategy.deleteByTags(tags)\n }\n\n async clear(): Promise<number> {\n return this.strategy.clear()\n }\n\n async keys(pattern?: string): Promise<string[]> {\n return this.strategy.keys(pattern)\n }\n\n async stats(): Promise<{ size: number; expired: number }> {\n return this.strategy.stats()\n }\n\n async cleanup(): Promise<number> {\n if (this.strategy.cleanup) {\n return this.strategy.cleanup()\n }\n return 0\n }\n\n async close(): Promise<void> {\n if (this.strategy.close) {\n return this.strategy.close()\n }\n }\n}\n\nfunction createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number): CacheStrategy {\n switch (strategyType) {\n case 'redis':\n return createRedisStrategy(options?.redisUrl, { defaultTtl })\n case 'sqlite':\n return createSqliteStrategy(options?.sqlitePath, { defaultTtl })\n case 'jsonfile':\n return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })\n case 'memory':\n default:\n return createMemoryStrategy({ defaultTtl })\n }\n}\n\nfunction describeDependencyFailure(error: CacheDependencyUnavailableError): string {\n const originalError = error.originalError\n if (!(originalError instanceof Error)) {\n return `missing dependency: ${error.dependency}`\n }\n\n const message = originalError.message.trim()\n if (message.length === 0) {\n return `missing dependency: ${error.dependency}`\n }\n\n if (message.includes('compiled against a different Node.js version') || message.includes('NODE_MODULE_VERSION')) {\n return `${error.dependency} native module needs rebuild for the current Node.js version`\n }\n\n if (message.includes('Cannot find module')) {\n return `missing dependency: ${error.dependency}`\n }\n\n return `${error.dependency} failed to load`\n}\n\nfunction withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStrategyName, defaultTtl?: number): CacheStrategy {\n if (strategyType === 'memory') return strategy\n\n let activeStrategy = strategy\n let fallbackStrategy: CacheStrategy | null = null\n let warned = false\n\n const ensureFallback = (error: CacheDependencyUnavailableError) => {\n if (!fallbackStrategy) {\n fallbackStrategy = createMemoryStrategy({ defaultTtl })\n }\n if (!warned) {\n const dependencyMessage = error.dependency\n ? ` (${describeDependencyFailure(error)})`\n : ''\n console.warn(`[cache] ${error.strategy} strategy unavailable${dependencyMessage}. Falling back to memory strategy.`)\n warned = true\n }\n activeStrategy = fallbackStrategy\n }\n\n const wrapMethod = <K extends keyof CacheStrategy>(method: K): CacheStrategy[K] => {\n const handler = async (...args: Parameters<NonNullable<CacheStrategy[K]>>) => {\n const fn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n\n try {\n return await fn(...args)\n } catch (error) {\n if (error instanceof CacheDependencyUnavailableError) {\n ensureFallback(error)\n const fallbackFn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fallbackFn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n return fallbackFn(...args)\n }\n throw error\n }\n }\n\n return handler as CacheStrategy[K]\n }\n\n return {\n get: wrapMethod('get'),\n set: wrapMethod('set'),\n has: wrapMethod('has'),\n delete: wrapMethod('delete'),\n deleteByTags: wrapMethod('deleteByTags'),\n clear: wrapMethod('clear'),\n keys: wrapMethod('keys'),\n stats: wrapMethod('stats'),\n cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,\n close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,\n }\n}\n"],
|
|
5
|
+
"mappings": "AACA,SAAS,4BAA4B;AACrC,SAAS,2BAA2B;AACpC,SAAS,4BAA4B;AACrC,SAAS,8BAA8B;AACvC,SAAS,6BAA6B;AACtC,SAAS,kBAAkB;AAC3B,SAAS,uCAAuC;AAChD,SAAS,4BAA4B;AAErC,SAAS,mBAAmB,KAAwC;AAClE,QAAM,QAAQ,OAAO,QAAQ,WAAW,IAAI,KAAK,IAAI;AACrD,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,MAAM,QAAQ,oBAAoB,GAAG;AAC9C;AAaA,SAAS,gBAAgB,OAAkD;AACzE,MAAI,OAAO,UAAU,YAAY,UAAU,MAAM;AAC/C,WAAO;AAAA,EACT;AACA,QAAM,SAAS;AACf,QAAM,cAAc,OAAO,OAAO,QAAQ;AAC1C,QAAM,oBACJ,EAAE,eAAe,WACd,OAAO,cAAc,QACrB,OAAO,OAAO,cAAc;AAEjC,SAAO,eAAe;AACxB;AAGA,MAAM,mBAAwC,CAAC,UAAU,SAAS,UAAU,UAAU;AAEtF,SAAS,oBAAoB,OAAuD;AAClF,MAAI,CAAC,MAAO,QAAO;AACnB,SAAO,iBAAiB,SAAS,KAA0B;AAC7D;AAEA,SAAS,wBAAwC;AAC/C,QAAM,SAAS,mBAAmB,sBAAsB,CAAC;AACzD,QAAM,OAAO,UAAU,MAAM;AAC7B,SAAO;AAAA,IACL,WAAW,GAAG,IAAI;AAAA,IAClB,WAAW,GAAG,IAAI;AAAA,IAClB,UAAU,GAAG,IAAI;AAAA,EACnB;AACF;AAEA,SAAS,eAAe,OAAuB;AAC7C,SAAO,WAAW,MAAM,EAAE,OAAO,KAAK,EAAE,OAAO,KAAK;AACtD;AAEA,SAAS,WAAW,aAAqB,UAAkC;AACzE,SAAO,GAAG,SAAS,SAAS,KAAK,eAAe,WAAW,CAAC;AAC9D;AAEA,SAAS,QAAQ,aAAqB,UAAkC;AACtE,SAAO,GAAG,SAAS,SAAS,QAAQ,eAAe,WAAW,CAAC;AACjE;AAEA,SAAS,UAAU,KAAa,UAAkC;AAChE,SAAO,GAAG,SAAS,SAAS,KAAK,eAAe,GAAG,CAAC;AACtD;AAEA,SAAS,YAAY,MAA4B,UAA0B,cAAiC;AAC1G,QAAM,SAAS,oBAAI,IAAY;AAC/B,MAAI,aAAc,QAAO,IAAI,SAAS,QAAQ;AAC9C,MAAI,MAAM,QAAQ,IAAI,GAAG;AACvB,eAAW,OAAO,MAAM;AACtB,UAAI,OAAO,QAAQ,YAAY,IAAI,SAAS,EAAG,QAAO,IAAI,UAAU,KAAK,QAAQ,CAAC;AAAA,IACpF;AAAA,EACF;AACA,SAAO,MAAM,KAAK,MAAM;AAC1B;AAEA,SAAS,yBAAyB,MAAoC;AACpE,WAAS,uBAAuB,KAAqB;AACnD,QAAI,CAAC,IAAK,QAAO;AACjB,QAAI,CAAC,OAAO,SAAS,GAAG,EAAG,QAAO;AAClC,WAAO,KAAK,KAAK,MAAM,CAAC;AAAA,EAC1B;AAEA,QAAM,MAAM,OAAO,KAAa,YAA8B;AAC5D,UAAM,WAAW,sBAAsB;AACvC,WAAO,KAAK,IAAI,WAAW,KAAK,QAAQ,GAAG,OAAO;AAAA,EACpD;AAEA,QAAM,MAAM,OAAO,KAAa,OAAmB,YAA8B;AAC/E,UAAM,WAAW,sBAAsB;AACvC,UAAM,aAAa,YAAY,SAAS,MAAM,UAAU,IAAI;AAC5D,UAAM,MAAM,SAAS,OAAO;AAC5B,UAAM,cAA2C,UAC7C,EAAE,GAAG,SAAS,MAAM,WAAW,IAC/B,EAAE,MAAM,WAAW;AACvB,UAAM,KAAK,IAAI,WAAW,KAAK,QAAQ,GAAG,OAAO,WAAW;AAC5D,UAAM,cAA6B,EAAE,KAAK,WAAW,MAAM,KAAK,IAAI,IAAI,MAAM,KAAK;AACnF,UAAM,KAAK,IAAI,QAAQ,KAAK,QAAQ,GAAG,aAAa;AAAA,MAClD;AAAA,MACA,MAAM;AAAA,IACR,CAAC;AAAA,EACH;AAEA,QAAM,MAAM,OAAO,QAAgB;AACjC,UAAM,WAAW,sBAAsB;AACvC,WAAO,KAAK,IAAI,WAAW,KAAK,QAAQ,CAAC;AAAA,EAC3C;AAEA,QAAM,MAAM,OAAO,QAAgB;AACjC,UAAM,WAAW,sBAAsB;AACvC,UAAM,UAAU,MAAM,KAAK,OAAO,WAAW,KAAK,QAAQ,CAAC;AAC3D,UAAM,KAAK,OAAO,QAAQ,KAAK,QAAQ,CAAC;AACxC,WAAO;AAAA,EACT;AAEA,QAAM,eAAe,OAAO,SAAmB;AAC7C,UAAM,WAAW,sBAAsB;AACvC,UAAM,aAAa,YAAY,MAAM,UAAU,KAAK;AACpD,QAAI,CAAC,WAAW,OAAQ,QAAO;AAC/B,UAAM,UAAU,MAAM,KAAK,aAAa,UAAU;AAClD,WAAO,uBAAuB,OAAO;AAAA,EACvC;AAEA,QAAM,QAAQ,YAAY;AACxB,UAAM,WAAW,sBAAsB;AACvC,UAAM,UAAU,MAAM,KAAK,aAAa,CAAC,SAAS,QAAQ,CAAC;AAC3D,WAAO,uBAAuB,OAAO;AAAA,EACvC;AAEA,QAAM,OAAO,OAAO,YAAqB;AACvC,UAAM,WAAW,sBAAsB;AACvC,UAAM,cAAc,GAAG,SAAS,SAAS;AACzC,UAAM,WAAW,MAAM,KAAK,KAAK,WAAW;AAC5C,UAAM,YAAsB,CAAC;AAC7B,eAAWA,YAAW,UAAU;AAC9B,YAAM,YAAY,MAAM,KAAK,IAAIA,UAAS,EAAE,eAAe,KAAK,CAAC;AACjE,UAAI,CAAC,UAAW;AAChB,YAAM,WAAW,OAAO,cAAc,WAAW,OAAQ,gBAAgB,SAAS,IAAI,YAAY;AAClG,YAAM,WAAW,OAAO,cAAc,WAAW,YAAY,UAAU;AACvE,UAAI,CAAC,SAAU;AACf,UAAI,WAAW,CAAC,qBAAqB,UAAU,OAAO,EAAG;AACzD,gBAAU,KAAK,QAAQ;AAAA,IACzB;AACA,WAAO;AAAA,EACT;AAEA,QAAM,QAAQ,YAAY;AACxB,UAAM,WAAW,sBAAsB;AACvC,UAAM,WAAW,MAAM,KAAK,KAAK,GAAG,SAAS,SAAS,QAAQ;AAC9D,QAAI,OAAO;AACX,QAAI,UAAU;AACd,UAAM,MAAM,KAAK,IAAI;AACrB,eAAWA,YAAW,UAAU;AAC9B,YAAM,YAAY,MAAM,KAAK,IAAIA,UAAS,EAAE,eAAe,KAAK,CAAC;AACjE,UAAI,CAAC,UAAW;AAChB,YAAM,WAAW,OAAO,cAAc,WAAW,OAAQ,gBAAgB,SAAS,IAAI,YAAY;AAClG,YAAM,WAAW,OAAO,cAAc,WAAW,YAAY,UAAU;AACvE,UAAI,CAAC,SAAU;AACf;AACA,YAAM,YAAY,UAAU,aAAa;AACzC,UAAI,cAAc,QAAQ,aAAa,IAAK;AAAA,IAC9C;AACA,WAAO,EAAE,MAAM,QAAQ;AAAA,EACzB;AAEA,QAAM,UAAU,KAAK,UACjB,YAAY,uBAAuB,MAAM,KAAK,QAAS,CAAC,IACxD;AAEJ,QAAM,QAAQ,KAAK,QACf,YAAY,KAAK,MAAO,IACxB;AAEJ,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA,QAAQ;AAAA,IACR;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;AAkBO,SAAS,mBAAmB,SAA8C;AAC/E,QAAM,cAAc,oBAAoB,QAAQ,IAAI,cAAc,IAC9D,QAAQ,IAAI,iBACZ;AACJ,QAAM,eAAkC,SAAS,YAAY,eAAe;AAE5E,QAAM,SAAS,QAAQ,IAAI;AAC3B,QAAM,eAAe,SAAS,OAAO,SAAS,QAAQ,EAAE,IAAI;AAC5D,QAAM,aAAa,SAAS,eAAe,OAAO,iBAAiB,YAAY,OAAO,SAAS,YAAY,IAAI,eAAe;AAE9H,QAAM,eAAe,sBAAsB,cAAc,SAAS,UAAU;AAC5E,QAAM,oBAAoB,uBAAuB,cAAc,cAAc,UAAU;AAEvF,SAAO,yBAAyB,iBAAiB;AACnD;AAMO,MAAM,aAAsC;AAAA,EAGjD,YAAY,SAA+B;AACzC,SAAK,WAAW,mBAAmB,OAAO;AAAA,EAC5C;AAAA,EAEA,MAAM,IAAI,KAAa,SAAuD;AAC5E,WAAO,KAAK,SAAS,IAAI,KAAK,OAAO;AAAA,EACvC;AAAA,EAEA,MAAM,IAAI,KAAa,OAAmB,SAA0C;AAClF,WAAO,KAAK,SAAS,IAAI,KAAK,OAAO,OAAO;AAAA,EAC9C;AAAA,EAEA,MAAM,IAAI,KAA+B;AACvC,WAAO,KAAK,SAAS,IAAI,GAAG;AAAA,EAC9B;AAAA,EAEA,MAAM,OAAO,KAA+B;AAC1C,WAAO,KAAK,SAAS,OAAO,GAAG;AAAA,EACjC;AAAA,EAEA,MAAM,aAAa,MAAiC;AAClD,WAAO,KAAK,SAAS,aAAa,IAAI;AAAA,EACxC;AAAA,EAEA,MAAM,QAAyB;AAC7B,WAAO,KAAK,SAAS,MAAM;AAAA,EAC7B;AAAA,EAEA,MAAM,KAAK,SAAqC;AAC9C,WAAO,KAAK,SAAS,KAAK,OAAO;AAAA,EACnC;AAAA,EAEA,MAAM,QAAoD;AACxD,WAAO,KAAK,SAAS,MAAM;AAAA,EAC7B;AAAA,EAEA,MAAM,UAA2B;AAC/B,QAAI,KAAK,SAAS,SAAS;AACzB,aAAO,KAAK,SAAS,QAAQ;AAAA,IAC/B;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,QAAuB;AAC3B,QAAI,KAAK,SAAS,OAAO;AACvB,aAAO,KAAK,SAAS,MAAM;AAAA,IAC7B;AAAA,EACF;AACF;AAEA,SAAS,sBAAsB,cAAiC,SAA+B,YAAoC;AACjI,UAAQ,cAAc;AAAA,IACpB,KAAK;AACH,aAAO,oBAAoB,SAAS,UAAU,EAAE,WAAW,CAAC;AAAA,IAC9D,KAAK;AACH,aAAO,qBAAqB,SAAS,YAAY,EAAE,WAAW,CAAC;AAAA,IACjE,KAAK;AACH,aAAO,uBAAuB,SAAS,cAAc,EAAE,WAAW,CAAC;AAAA,IACrE,KAAK;AAAA,IACL;AACE,aAAO,qBAAqB,EAAE,WAAW,CAAC;AAAA,EAC9C;AACF;AAEA,SAAS,0BAA0B,OAAgD;AACjF,QAAM,gBAAgB,MAAM;AAC5B,MAAI,EAAE,yBAAyB,QAAQ;AACrC,WAAO,uBAAuB,MAAM,UAAU;AAAA,EAChD;AAEA,QAAM,UAAU,cAAc,QAAQ,KAAK;AAC3C,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,uBAAuB,MAAM,UAAU;AAAA,EAChD;AAEA,MAAI,QAAQ,SAAS,8CAA8C,KAAK,QAAQ,SAAS,qBAAqB,GAAG;AAC/G,WAAO,GAAG,MAAM,UAAU;AAAA,EAC5B;AAEA,MAAI,QAAQ,SAAS,oBAAoB,GAAG;AAC1C,WAAO,uBAAuB,MAAM,UAAU;AAAA,EAChD;AAEA,SAAO,GAAG,MAAM,UAAU;AAC5B;AAEA,SAAS,uBAAuB,UAAyB,cAAiC,YAAoC;AAC5H,MAAI,iBAAiB,SAAU,QAAO;AAEtC,MAAI,iBAAiB;AACrB,MAAI,mBAAyC;AAC7C,MAAI,SAAS;AAEb,QAAM,iBAAiB,CAAC,UAA2C;AACjE,QAAI,CAAC,kBAAkB;AACrB,yBAAmB,qBAAqB,EAAE,WAAW,CAAC;AAAA,IACxD;AACA,QAAI,CAAC,QAAQ;AACX,YAAM,oBAAoB,MAAM,aAC5B,KAAK,0BAA0B,KAAK,CAAC,MACrC;AACJ,cAAQ,KAAK,WAAW,MAAM,QAAQ,wBAAwB,iBAAiB,oCAAoC;AACnH,eAAS;AAAA,IACX;AACA,qBAAiB;AAAA,EACnB;AAEA,QAAM,aAAa,CAAgC,WAAgC;AACjF,UAAM,UAAU,UAAU,SAAoD;AAC5E,YAAM,KAAK,eAAe,MAAM;AAChC,UAAI,CAAC,IAAI;AACP,eAAO;AAAA,MACT;AAEA,UAAI;AACF,eAAO,MAAM,GAAG,GAAG,IAAI;AAAA,MACzB,SAAS,OAAO;AACd,YAAI,iBAAiB,iCAAiC;AACpD,yBAAe,KAAK;AACpB,gBAAM,aAAa,eAAe,MAAM;AACxC,cAAI,CAAC,YAAY;AACf,mBAAO;AAAA,UACT;AACA,iBAAO,WAAW,GAAG,IAAI;AAAA,QAC3B;AACA,cAAM;AAAA,MACR;AAAA,IACF;AAEA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,KAAK,WAAW,KAAK;AAAA,IACrB,KAAK,WAAW,KAAK;AAAA,IACrB,KAAK,WAAW,KAAK;AAAA,IACrB,QAAQ,WAAW,QAAQ;AAAA,IAC3B,cAAc,WAAW,cAAc;AAAA,IACvC,OAAO,WAAW,OAAO;AAAA,IACzB,MAAM,WAAW,MAAM;AAAA,IACvB,OAAO,WAAW,OAAO;AAAA,IACzB,SAAS,OAAO,SAAS,YAAY,aAAa,WAAW,SAAS,IAAI;AAAA,IAC1E,OAAO,OAAO,SAAS,UAAU,aAAa,WAAW,OAAO,IAAI;AAAA,EACtE;AACF;",
|
|
6
6
|
"names": ["metaKey"]
|
|
7
7
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { DEFAULT_JSON_FILE_CACHE_PATH } from "../defaults.js";
|
|
4
|
+
import { matchCacheKeyPattern } from "../patterns.js";
|
|
4
5
|
function createJsonFileStrategy(filePath, options) {
|
|
5
6
|
const defaultTtl = options?.defaultTtl;
|
|
6
7
|
const cacheFile = filePath || process.env.CACHE_JSON_FILE_PATH || DEFAULT_JSON_FILE_CACHE_PATH;
|
|
@@ -30,11 +31,6 @@ function createJsonFileStrategy(filePath, options) {
|
|
|
30
31
|
if (entry.expiresAt === null) return false;
|
|
31
32
|
return Date.now() > entry.expiresAt;
|
|
32
33
|
}
|
|
33
|
-
function matchPattern(key, pattern) {
|
|
34
|
-
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
35
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
36
|
-
return regex.test(key);
|
|
37
|
-
}
|
|
38
34
|
function addToTagIndex(data, key, tags) {
|
|
39
35
|
for (const tag of tags) {
|
|
40
36
|
if (!data.tagIndex[tag]) {
|
|
@@ -140,7 +136,7 @@ function createJsonFileStrategy(filePath, options) {
|
|
|
140
136
|
const data = readData();
|
|
141
137
|
const allKeys = Object.keys(data.entries);
|
|
142
138
|
if (!pattern) return allKeys;
|
|
143
|
-
return allKeys.filter((key) =>
|
|
139
|
+
return allKeys.filter((key) => matchCacheKeyPattern(key, pattern));
|
|
144
140
|
};
|
|
145
141
|
const stats = async () => {
|
|
146
142
|
const data = readData();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/strategies/jsonfile.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { DEFAULT_JSON_FILE_CACHE_PATH } from '../defaults'\n\n/**\n * JSON file cache strategy with tag support\n * Persistent across process restarts, stored in JSON files\n * Simple and requires no external dependencies, but not suitable for high-performance scenarios\n */\nexport function createJsonFileStrategy(filePath?: string, options?: { defaultTtl?: number }): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const cacheFile = filePath || process.env.CACHE_JSON_FILE_PATH || DEFAULT_JSON_FILE_CACHE_PATH\n const dir = path.dirname(cacheFile)\n\n type StorageData = {\n entries: Record<string, CacheEntry>\n tagIndex: Record<string, string[]> // tag -> array of keys\n }\n\n function ensureDir(): void {\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n }\n\n function readData(): StorageData {\n ensureDir()\n if (!fs.existsSync(cacheFile)) {\n return { entries: {}, tagIndex: {} }\n }\n try {\n const content = fs.readFileSync(cacheFile, 'utf8')\n return JSON.parse(content) as StorageData\n } catch {\n return { entries: {}, tagIndex: {} }\n }\n }\n\n function writeData(data: StorageData): void {\n ensureDir()\n fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8')\n }\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n function
|
|
5
|
-
"mappings": "AACA,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,SAAS,oCAAoC;
|
|
4
|
+
"sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { DEFAULT_JSON_FILE_CACHE_PATH } from '../defaults'\nimport { matchCacheKeyPattern } from '../patterns'\n\n/**\n * JSON file cache strategy with tag support\n * Persistent across process restarts, stored in JSON files\n * Simple and requires no external dependencies, but not suitable for high-performance scenarios\n */\nexport function createJsonFileStrategy(filePath?: string, options?: { defaultTtl?: number }): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const cacheFile = filePath || process.env.CACHE_JSON_FILE_PATH || DEFAULT_JSON_FILE_CACHE_PATH\n const dir = path.dirname(cacheFile)\n\n type StorageData = {\n entries: Record<string, CacheEntry>\n tagIndex: Record<string, string[]> // tag -> array of keys\n }\n\n function ensureDir(): void {\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n }\n\n function readData(): StorageData {\n ensureDir()\n if (!fs.existsSync(cacheFile)) {\n return { entries: {}, tagIndex: {} }\n }\n try {\n const content = fs.readFileSync(cacheFile, 'utf8')\n return JSON.parse(content) as StorageData\n } catch {\n return { entries: {}, tagIndex: {} }\n }\n }\n\n function writeData(data: StorageData): void {\n ensureDir()\n fs.writeFileSync(cacheFile, JSON.stringify(data, null, 2), 'utf8')\n }\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n function addToTagIndex(data: StorageData, key: string, tags: string[]): void {\n for (const tag of tags) {\n if (!data.tagIndex[tag]) {\n data.tagIndex[tag] = []\n }\n if (!data.tagIndex[tag].includes(key)) {\n data.tagIndex[tag].push(key)\n }\n }\n }\n\n function removeFromTagIndex(data: StorageData, key: string, tags: string[]): void {\n for (const tag of tags) {\n if (data.tagIndex[tag]) {\n data.tagIndex[tag] = data.tagIndex[tag].filter((k) => k !== key)\n if (data.tagIndex[tag].length === 0) {\n delete data.tagIndex[tag]\n }\n }\n }\n }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const data = readData()\n const entry = data.entries[key]\n\n if (!entry) return null\n\n if (isExpired(entry)) {\n if (options?.returnExpired) {\n return entry.value\n }\n // Clean up expired entry\n removeFromTagIndex(data, key, entry.tags)\n delete data.entries[key]\n writeData(data)\n return null\n }\n\n return entry.value\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const data = readData()\n\n // Remove old entry from tag index if it exists\n const oldEntry = data.entries[key]\n if (oldEntry) {\n removeFromTagIndex(data, key, oldEntry.tags)\n }\n\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n\n const entry: CacheEntry = {\n key,\n value,\n tags,\n expiresAt,\n createdAt: Date.now(),\n }\n\n data.entries[key] = entry\n addToTagIndex(data, key, tags)\n\n writeData(data)\n }\n\n const has = async (key: string): Promise<boolean> => {\n const data = readData()\n const entry = data.entries[key]\n\n if (!entry) return false\n\n if (isExpired(entry)) {\n removeFromTagIndex(data, key, entry.tags)\n delete data.entries[key]\n writeData(data)\n return false\n }\n\n return true\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const data = readData()\n const entry = data.entries[key]\n\n if (!entry) return false\n\n removeFromTagIndex(data, key, entry.tags)\n delete data.entries[key]\n writeData(data)\n\n return true\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const data = readData()\n const keysToDelete = new Set<string>()\n\n // Collect all keys that have any of the specified tags\n for (const tag of tags) {\n const keys = data.tagIndex[tag] || []\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n for (const key of keysToDelete) {\n const entry = data.entries[key]\n if (entry) {\n removeFromTagIndex(data, key, entry.tags)\n delete data.entries[key]\n }\n }\n\n writeData(data)\n return keysToDelete.size\n }\n\n const clear = async (): Promise<number> => {\n const data = readData()\n const size = Object.keys(data.entries).length\n\n writeData({ entries: {}, tagIndex: {} })\n\n return size\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const data = readData()\n const allKeys = Object.keys(data.entries)\n\n if (!pattern) return allKeys\n\n return allKeys.filter((key) => matchCacheKeyPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const data = readData()\n const allEntries = Object.values(data.entries)\n\n let expired = 0\n for (const entry of allEntries) {\n if (isExpired(entry)) {\n expired++\n }\n }\n\n return { size: allEntries.length, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const data = readData()\n let removed = 0\n\n const keysToRemove: string[] = []\n for (const [key, entry] of Object.entries(data.entries)) {\n if (isExpired(entry)) {\n keysToRemove.push(key)\n }\n }\n\n for (const key of keysToRemove) {\n const entry = data.entries[key]\n removeFromTagIndex(data, key, entry.tags)\n delete data.entries[key]\n removed++\n }\n\n if (removed > 0) {\n writeData(data)\n }\n\n return removed\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["options", "keys"]
|
|
7
7
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { matchCacheKeyPattern } from "../patterns.js";
|
|
1
2
|
function createMemoryStrategy(options) {
|
|
2
3
|
const store = /* @__PURE__ */ new Map();
|
|
3
4
|
const tagIndex = /* @__PURE__ */ new Map();
|
|
@@ -37,11 +38,6 @@ function createMemoryStrategy(options) {
|
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
40
|
}
|
|
40
|
-
function matchPattern(key, pattern) {
|
|
41
|
-
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
42
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
43
|
-
return regex.test(key);
|
|
44
|
-
}
|
|
45
41
|
const get = async (key, options2) => {
|
|
46
42
|
const entry = store.get(key);
|
|
47
43
|
if (!entry) return null;
|
|
@@ -113,7 +109,7 @@ function createMemoryStrategy(options) {
|
|
|
113
109
|
const keys = async (pattern) => {
|
|
114
110
|
const allKeys = Array.from(store.keys());
|
|
115
111
|
if (!pattern) return allKeys;
|
|
116
|
-
return allKeys.filter((key) =>
|
|
112
|
+
return allKeys.filter((key) => matchCacheKeyPattern(key, pattern));
|
|
117
113
|
};
|
|
118
114
|
const stats = async () => {
|
|
119
115
|
let expired = 0;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/strategies/memory.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\n\n/**\n * In-memory cache strategy with tag support\n * Fast but data is lost when process restarts\n */\nexport function createMemoryStrategy(options?: { defaultTtl?: number }): CacheStrategy {\n const store = new Map<string, CacheEntry>()\n const tagIndex = new Map<string, Set<string>>() // tag -> Set of keys\n const defaultTtl = options?.defaultTtl\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n function cleanupExpiredEntry(key: string, entry: CacheEntry): void {\n store.delete(key)\n // Remove from tag index\n for (const tag of entry.tags) {\n const keys = tagIndex.get(tag)\n if (keys) {\n keys.delete(key)\n if (keys.size === 0) {\n tagIndex.delete(tag)\n }\n }\n }\n }\n\n function addToTagIndex(key: string, tags: string[]): void {\n for (const tag of tags) {\n if (!tagIndex.has(tag)) {\n tagIndex.set(tag, new Set())\n }\n tagIndex.get(tag)!.add(key)\n }\n }\n\n function removeFromTagIndex(key: string, tags: string[]): void {\n for (const tag of tags) {\n const keys = tagIndex.get(tag)\n if (keys) {\n keys.delete(key)\n if (keys.size === 0) {\n tagIndex.delete(tag)\n }\n }\n }\n }\n\n
|
|
5
|
-
"mappings": "
|
|
4
|
+
"sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { matchCacheKeyPattern } from '../patterns'\n\n/**\n * In-memory cache strategy with tag support\n * Fast but data is lost when process restarts\n */\nexport function createMemoryStrategy(options?: { defaultTtl?: number }): CacheStrategy {\n const store = new Map<string, CacheEntry>()\n const tagIndex = new Map<string, Set<string>>() // tag -> Set of keys\n const defaultTtl = options?.defaultTtl\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n function cleanupExpiredEntry(key: string, entry: CacheEntry): void {\n store.delete(key)\n // Remove from tag index\n for (const tag of entry.tags) {\n const keys = tagIndex.get(tag)\n if (keys) {\n keys.delete(key)\n if (keys.size === 0) {\n tagIndex.delete(tag)\n }\n }\n }\n }\n\n function addToTagIndex(key: string, tags: string[]): void {\n for (const tag of tags) {\n if (!tagIndex.has(tag)) {\n tagIndex.set(tag, new Set())\n }\n tagIndex.get(tag)!.add(key)\n }\n }\n\n function removeFromTagIndex(key: string, tags: string[]): void {\n for (const tag of tags) {\n const keys = tagIndex.get(tag)\n if (keys) {\n keys.delete(key)\n if (keys.size === 0) {\n tagIndex.delete(tag)\n }\n }\n }\n }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const entry = store.get(key)\n if (!entry) return null\n\n if (isExpired(entry)) {\n if (options?.returnExpired) {\n return entry.value\n }\n cleanupExpiredEntry(key, entry)\n return null\n }\n\n return entry.value\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n // Remove old entry from tag index if it exists\n const oldEntry = store.get(key)\n if (oldEntry) {\n removeFromTagIndex(key, oldEntry.tags)\n }\n\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n\n const entry: CacheEntry = {\n key,\n value,\n tags,\n expiresAt,\n createdAt: Date.now(),\n }\n\n store.set(key, entry)\n addToTagIndex(key, tags)\n }\n\n const has = async (key: string): Promise<boolean> => {\n const entry = store.get(key)\n if (!entry) return false\n if (isExpired(entry)) {\n cleanupExpiredEntry(key, entry)\n return false\n }\n return true\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const entry = store.get(key)\n if (!entry) return false\n\n removeFromTagIndex(key, entry.tags)\n return store.delete(key)\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const keysToDelete = new Set<string>()\n\n // Collect all keys that have any of the specified tags\n for (const tag of tags) {\n const keys = tagIndex.get(tag)\n if (keys) {\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const size = store.size\n store.clear()\n tagIndex.clear()\n return size\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const allKeys = Array.from(store.keys())\n if (!pattern) return allKeys\n return allKeys.filter((key) => matchCacheKeyPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n let expired = 0\n for (const entry of store.values()) {\n if (isExpired(entry)) {\n expired++\n }\n }\n return { size: store.size, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n let removed = 0\n for (const [key, entry] of store.entries()) {\n if (isExpired(entry)) {\n cleanupExpiredEntry(key, entry)\n removed++\n }\n }\n return removed\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["keys", "options"]
|
|
7
7
|
}
|
package/dist/strategies/redis.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CacheDependencyUnavailableError } from "../errors.js";
|
|
2
2
|
import { getRedisUrlOrThrow } from "@open-mercato/shared/lib/redis/connection";
|
|
3
|
+
import { matchCacheKeyPattern } from "../patterns.js";
|
|
3
4
|
let redisModulePromise = null;
|
|
4
5
|
const redisRegistry = /* @__PURE__ */ new Map();
|
|
5
6
|
function resolveRequire() {
|
|
@@ -123,11 +124,6 @@ function createRedisStrategy(redisUrl, options) {
|
|
|
123
124
|
if (entry.expiresAt === null) return false;
|
|
124
125
|
return Date.now() > entry.expiresAt;
|
|
125
126
|
}
|
|
126
|
-
function matchPattern(key, pattern) {
|
|
127
|
-
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
128
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
129
|
-
return regex.test(key);
|
|
130
|
-
}
|
|
131
127
|
const get = async (key, options2) => {
|
|
132
128
|
const client = await getRedisClient();
|
|
133
129
|
const cacheKey = getCacheKey(key);
|
|
@@ -257,7 +253,7 @@ function createRedisStrategy(redisUrl, options) {
|
|
|
257
253
|
const cacheKeys = await client.keys(searchPattern);
|
|
258
254
|
const result = cacheKeys.map((key) => key.substring(keyPrefix.length));
|
|
259
255
|
if (!pattern) return result;
|
|
260
|
-
return result.filter((key) =>
|
|
256
|
+
return result.filter((key) => matchCacheKeyPattern(key, pattern));
|
|
261
257
|
};
|
|
262
258
|
const stats = async () => {
|
|
263
259
|
const client = await getRedisClient();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/strategies/redis.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\n\ntype RedisPipeline = {\n set(key: string, value: string): RedisPipeline\n setex(key: string, ttlSeconds: number, value: string): RedisPipeline\n sadd(key: string, value: string): RedisPipeline\n srem(key: string, member: string): RedisPipeline\n del(key: string): RedisPipeline\n exec(): Promise<unknown>\n}\n\ntype RedisClient = {\n get(key: string): Promise<string | null>\n set(key: string, value: string): Promise<unknown>\n setex(key: string, ttlSeconds: number, value: string): Promise<unknown>\n del(key: string): Promise<unknown>\n exists(key: string): Promise<number>\n keys(pattern: string): Promise<string[]>\n smembers(key: string): Promise<string[]>\n pipeline(): RedisPipeline\n quit(): Promise<void>\n once?(event: 'end', listener: () => void): void\n}\n\ntype RedisConstructor = new (url?: string) => RedisClient\ntype PossibleRedisModule = unknown\n\ntype RequireFn = (id: string) => unknown\n\n/**\n * Redis cache strategy with tag support\n * Persistent across process restarts, can be shared across multiple instances\n * \n * Uses Redis data structures:\n * - Hash for storing cache entries: cache:{key} -> {value, tags, expiresAt, createdAt}\n * - Sets for tag index: tag:{tag} -> Set of keys\n */\nlet redisModulePromise: Promise<PossibleRedisModule> | null = null\ntype RedisRegistryEntry = { client?: RedisClient; creating?: Promise<RedisClient>; refs: number }\nconst redisRegistry = new Map<string, RedisRegistryEntry>()\n\nfunction resolveRequire(): RequireFn | null {\n const nonWebpack = (globalThis as { __non_webpack_require__?: unknown }).__non_webpack_require__\n if (typeof nonWebpack === 'function') return nonWebpack as RequireFn\n if (typeof require === 'function') return require as RequireFn\n if (typeof module !== 'undefined' && typeof module.require === 'function') {\n return module.require.bind(module)\n }\n try {\n const maybeRequire = Function('return typeof require !== \"undefined\" ? require : undefined')()\n if (typeof maybeRequire === 'function') return maybeRequire as RequireFn\n } catch {\n // ignore\n }\n return null\n}\n\nfunction loadRedisModuleViaRequire(): PossibleRedisModule | null {\n const resolver = resolveRequire()\n if (!resolver) return null\n try {\n return resolver('ioredis') as PossibleRedisModule\n } catch {\n return null\n }\n}\n\nfunction pickRedisConstructor(mod: PossibleRedisModule): RedisConstructor | null {\n const queue: unknown[] = [mod]\n const seen = new Set<unknown>()\n while (queue.length) {\n const current = queue.shift()\n if (!current || seen.has(current)) continue\n seen.add(current)\n if (typeof current === 'function') return current as RedisConstructor\n if (typeof current === 'object') {\n queue.push((current as { default?: unknown }).default)\n queue.push((current as { Redis?: unknown }).Redis)\n queue.push((current as { module?: { exports?: unknown } }).module?.exports)\n queue.push((current as { exports?: unknown }).exports)\n }\n }\n return null\n}\n\nasync function loadRedisModule(): Promise<PossibleRedisModule> {\n if (!redisModulePromise) {\n redisModulePromise = (async () => {\n const required = loadRedisModuleViaRequire() ?? (await import('ioredis'))\n return required as PossibleRedisModule\n })().catch((error) => {\n redisModulePromise = null\n throw new CacheDependencyUnavailableError('redis', 'ioredis', error)\n })\n }\n return redisModulePromise\n}\n\nfunction retainRedisEntry(key: string): RedisRegistryEntry {\n let entry = redisRegistry.get(key)\n if (!entry) {\n entry = { refs: 0 }\n redisRegistry.set(key, entry)\n }\n entry.refs += 1\n return entry\n}\n\nasync function acquireRedisClient(key: string, entry: RedisRegistryEntry): Promise<RedisClient> {\n if (entry.client) return entry.client\n if (entry.creating) return entry.creating\n entry.creating = loadRedisModule()\n .then((mod) => {\n const ctor = pickRedisConstructor(mod)\n if (!ctor) {\n throw new CacheDependencyUnavailableError('redis', 'ioredis', new Error('No usable Redis constructor'))\n }\n const client = new ctor(key)\n entry.client = client\n entry.creating = undefined\n client.once?.('end', () => {\n if (redisRegistry.get(key) === entry && entry.refs === 0) {\n redisRegistry.delete(key)\n } else if (redisRegistry.get(key) === entry) {\n entry.client = undefined\n }\n })\n return client\n })\n .catch((error) => {\n entry.creating = undefined\n throw error\n })\n return entry.creating\n}\n\nasync function releaseRedisEntry(key: string, entry: RedisRegistryEntry): Promise<void> {\n entry.refs = Math.max(0, entry.refs - 1)\n if (entry.refs > 0) return\n redisRegistry.delete(key)\n if (entry.client) {\n try {\n await entry.client.quit()\n } catch {\n // ignore shutdown errors\n } finally {\n entry.client = undefined\n }\n }\n}\n\nexport function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number }): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const keyPrefix = 'cache:'\n const tagPrefix = 'tag:'\n const connectionUrl = redisUrl || getRedisUrlOrThrow('CACHE')\n const registryEntry = retainRedisEntry(connectionUrl)\n let redis: RedisClient | null = registryEntry.client ?? null\n\n async function getRedisClient(): Promise<RedisClient> {\n if (redis) return redis\n\n redis = await acquireRedisClient(connectionUrl, registryEntry)\n return redis\n }\n\n function getCacheKey(key: string): string {\n return `${keyPrefix}${key}`\n }\n\n function getTagKey(tag: string): string {\n return `${tagPrefix}${tag}`\n }\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n function matchPattern(key: string, pattern: string): boolean {\n const regexPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&')\n .replace(/\\*/g, '.*')\n .replace(/\\?/g, '.')\n const regex = new RegExp(`^${regexPattern}$`)\n return regex.test(key)\n }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const data = await client.get(cacheKey)\n\n if (!data) return null\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n\n if (isExpired(entry)) {\n if (options?.returnExpired) {\n return entry.value\n }\n // Clean up expired entry\n await deleteKey(key)\n return null\n }\n\n return entry.value\n } catch {\n // Invalid JSON, remove it\n await client.del(cacheKey)\n return null\n }\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Remove old entry from tag index if it exists\n const oldData = await client.get(cacheKey)\n if (oldData) {\n try {\n const oldEntry: CacheEntry = JSON.parse(oldData)\n // Remove from old tags\n const pipeline = client.pipeline()\n for (const tag of oldEntry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n await pipeline.exec()\n } catch {\n // Ignore parse errors\n }\n }\n\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n\n const entry: CacheEntry = {\n key,\n value,\n tags,\n expiresAt,\n createdAt: Date.now(),\n }\n\n const pipeline = client.pipeline()\n\n // Store the entry\n const serialized = JSON.stringify(entry)\n if (ttl) {\n pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized)\n } else {\n pipeline.set(cacheKey, serialized)\n }\n\n // Add to tag index\n for (const tag of tags) {\n pipeline.sadd(getTagKey(tag), key)\n }\n\n await pipeline.exec()\n }\n\n const has = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const exists = await client.exists(cacheKey)\n\n if (!exists) return false\n\n // Check if expired\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n await deleteKey(key)\n return false\n }\n return true\n } catch {\n return false\n }\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Get entry to remove from tag index\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n const pipeline = client.pipeline()\n\n // Remove from tag index\n for (const tag of entry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n\n // Delete the cache entry\n pipeline.del(cacheKey)\n\n await pipeline.exec()\n return true\n } catch {\n // Just delete the key if we can't parse it\n await client.del(cacheKey)\n return true\n }\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const client = await getRedisClient()\n const keysToDelete = new Set<string>()\n\n // Collect all keys that have any of the specified tags\n for (const tag of tags) {\n const tagKey = getTagKey(tag)\n const keys = await client.smembers(tagKey)\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const client = await getRedisClient()\n \n // Get all cache keys\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n const tagKeys = await client.keys(`${tagPrefix}*`)\n\n if (cacheKeys.length === 0 && tagKeys.length === 0) return 0\n\n const pipeline = client.pipeline()\n for (const key of [...cacheKeys, ...tagKeys]) {\n pipeline.del(key)\n }\n\n await pipeline.exec()\n return cacheKeys.length\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const client = await getRedisClient()\n const searchPattern = pattern \n ? `${keyPrefix}${pattern}` \n : `${keyPrefix}*`\n \n const cacheKeys = await client.keys(searchPattern)\n \n // Remove prefix from keys\n const result = cacheKeys.map((key: string) => key.substring(keyPrefix.length))\n \n if (!pattern) return result\n \n // Apply pattern matching (Redis KEYS command uses glob pattern, but we want our pattern)\n return result.filter((key: string) => matchPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let expired = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n expired++\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n return { size: cacheKeys.length, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let removed = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n const key = cacheKey.substring(keyPrefix.length)\n await deleteKey(key)\n removed++\n }\n } catch {\n // Remove invalid entries\n await client.del(cacheKey)\n removed++\n }\n }\n }\n\n return removed\n }\n\n const close = async (): Promise<void> => {\n await releaseRedisEntry(connectionUrl, registryEntry)\n redis = null\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAAS,uCAAuC;AAChD,SAAS,0BAA0B;
|
|
4
|
+
"sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { matchCacheKeyPattern } from '../patterns'\n\ntype RedisPipeline = {\n set(key: string, value: string): RedisPipeline\n setex(key: string, ttlSeconds: number, value: string): RedisPipeline\n sadd(key: string, value: string): RedisPipeline\n srem(key: string, member: string): RedisPipeline\n del(key: string): RedisPipeline\n exec(): Promise<unknown>\n}\n\ntype RedisClient = {\n get(key: string): Promise<string | null>\n set(key: string, value: string): Promise<unknown>\n setex(key: string, ttlSeconds: number, value: string): Promise<unknown>\n del(key: string): Promise<unknown>\n exists(key: string): Promise<number>\n keys(pattern: string): Promise<string[]>\n smembers(key: string): Promise<string[]>\n pipeline(): RedisPipeline\n quit(): Promise<void>\n once?(event: 'end', listener: () => void): void\n}\n\ntype RedisConstructor = new (url?: string) => RedisClient\ntype PossibleRedisModule = unknown\n\ntype RequireFn = (id: string) => unknown\n\n/**\n * Redis cache strategy with tag support\n * Persistent across process restarts, can be shared across multiple instances\n * \n * Uses Redis data structures:\n * - Hash for storing cache entries: cache:{key} -> {value, tags, expiresAt, createdAt}\n * - Sets for tag index: tag:{tag} -> Set of keys\n */\nlet redisModulePromise: Promise<PossibleRedisModule> | null = null\ntype RedisRegistryEntry = { client?: RedisClient; creating?: Promise<RedisClient>; refs: number }\nconst redisRegistry = new Map<string, RedisRegistryEntry>()\n\nfunction resolveRequire(): RequireFn | null {\n const nonWebpack = (globalThis as { __non_webpack_require__?: unknown }).__non_webpack_require__\n if (typeof nonWebpack === 'function') return nonWebpack as RequireFn\n if (typeof require === 'function') return require as RequireFn\n if (typeof module !== 'undefined' && typeof module.require === 'function') {\n return module.require.bind(module)\n }\n try {\n const maybeRequire = Function('return typeof require !== \"undefined\" ? require : undefined')()\n if (typeof maybeRequire === 'function') return maybeRequire as RequireFn\n } catch {\n // ignore\n }\n return null\n}\n\nfunction loadRedisModuleViaRequire(): PossibleRedisModule | null {\n const resolver = resolveRequire()\n if (!resolver) return null\n try {\n return resolver('ioredis') as PossibleRedisModule\n } catch {\n return null\n }\n}\n\nfunction pickRedisConstructor(mod: PossibleRedisModule): RedisConstructor | null {\n const queue: unknown[] = [mod]\n const seen = new Set<unknown>()\n while (queue.length) {\n const current = queue.shift()\n if (!current || seen.has(current)) continue\n seen.add(current)\n if (typeof current === 'function') return current as RedisConstructor\n if (typeof current === 'object') {\n queue.push((current as { default?: unknown }).default)\n queue.push((current as { Redis?: unknown }).Redis)\n queue.push((current as { module?: { exports?: unknown } }).module?.exports)\n queue.push((current as { exports?: unknown }).exports)\n }\n }\n return null\n}\n\nasync function loadRedisModule(): Promise<PossibleRedisModule> {\n if (!redisModulePromise) {\n redisModulePromise = (async () => {\n const required = loadRedisModuleViaRequire() ?? (await import('ioredis'))\n return required as PossibleRedisModule\n })().catch((error) => {\n redisModulePromise = null\n throw new CacheDependencyUnavailableError('redis', 'ioredis', error)\n })\n }\n return redisModulePromise\n}\n\nfunction retainRedisEntry(key: string): RedisRegistryEntry {\n let entry = redisRegistry.get(key)\n if (!entry) {\n entry = { refs: 0 }\n redisRegistry.set(key, entry)\n }\n entry.refs += 1\n return entry\n}\n\nasync function acquireRedisClient(key: string, entry: RedisRegistryEntry): Promise<RedisClient> {\n if (entry.client) return entry.client\n if (entry.creating) return entry.creating\n entry.creating = loadRedisModule()\n .then((mod) => {\n const ctor = pickRedisConstructor(mod)\n if (!ctor) {\n throw new CacheDependencyUnavailableError('redis', 'ioredis', new Error('No usable Redis constructor'))\n }\n const client = new ctor(key)\n entry.client = client\n entry.creating = undefined\n client.once?.('end', () => {\n if (redisRegistry.get(key) === entry && entry.refs === 0) {\n redisRegistry.delete(key)\n } else if (redisRegistry.get(key) === entry) {\n entry.client = undefined\n }\n })\n return client\n })\n .catch((error) => {\n entry.creating = undefined\n throw error\n })\n return entry.creating\n}\n\nasync function releaseRedisEntry(key: string, entry: RedisRegistryEntry): Promise<void> {\n entry.refs = Math.max(0, entry.refs - 1)\n if (entry.refs > 0) return\n redisRegistry.delete(key)\n if (entry.client) {\n try {\n await entry.client.quit()\n } catch {\n // ignore shutdown errors\n } finally {\n entry.client = undefined\n }\n }\n}\n\nexport function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number }): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const keyPrefix = 'cache:'\n const tagPrefix = 'tag:'\n const connectionUrl = redisUrl || getRedisUrlOrThrow('CACHE')\n const registryEntry = retainRedisEntry(connectionUrl)\n let redis: RedisClient | null = registryEntry.client ?? null\n\n async function getRedisClient(): Promise<RedisClient> {\n if (redis) return redis\n\n redis = await acquireRedisClient(connectionUrl, registryEntry)\n return redis\n }\n\n function getCacheKey(key: string): string {\n return `${keyPrefix}${key}`\n }\n\n function getTagKey(tag: string): string {\n return `${tagPrefix}${tag}`\n }\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const data = await client.get(cacheKey)\n\n if (!data) return null\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n\n if (isExpired(entry)) {\n if (options?.returnExpired) {\n return entry.value\n }\n // Clean up expired entry\n await deleteKey(key)\n return null\n }\n\n return entry.value\n } catch {\n // Invalid JSON, remove it\n await client.del(cacheKey)\n return null\n }\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Remove old entry from tag index if it exists\n const oldData = await client.get(cacheKey)\n if (oldData) {\n try {\n const oldEntry: CacheEntry = JSON.parse(oldData)\n // Remove from old tags\n const pipeline = client.pipeline()\n for (const tag of oldEntry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n await pipeline.exec()\n } catch {\n // Ignore parse errors\n }\n }\n\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n\n const entry: CacheEntry = {\n key,\n value,\n tags,\n expiresAt,\n createdAt: Date.now(),\n }\n\n const pipeline = client.pipeline()\n\n // Store the entry\n const serialized = JSON.stringify(entry)\n if (ttl) {\n pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized)\n } else {\n pipeline.set(cacheKey, serialized)\n }\n\n // Add to tag index\n for (const tag of tags) {\n pipeline.sadd(getTagKey(tag), key)\n }\n\n await pipeline.exec()\n }\n\n const has = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const exists = await client.exists(cacheKey)\n\n if (!exists) return false\n\n // Check if expired\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n await deleteKey(key)\n return false\n }\n return true\n } catch {\n return false\n }\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Get entry to remove from tag index\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n const pipeline = client.pipeline()\n\n // Remove from tag index\n for (const tag of entry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n\n // Delete the cache entry\n pipeline.del(cacheKey)\n\n await pipeline.exec()\n return true\n } catch {\n // Just delete the key if we can't parse it\n await client.del(cacheKey)\n return true\n }\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const client = await getRedisClient()\n const keysToDelete = new Set<string>()\n\n // Collect all keys that have any of the specified tags\n for (const tag of tags) {\n const tagKey = getTagKey(tag)\n const keys = await client.smembers(tagKey)\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const client = await getRedisClient()\n \n // Get all cache keys\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n const tagKeys = await client.keys(`${tagPrefix}*`)\n\n if (cacheKeys.length === 0 && tagKeys.length === 0) return 0\n\n const pipeline = client.pipeline()\n for (const key of [...cacheKeys, ...tagKeys]) {\n pipeline.del(key)\n }\n\n await pipeline.exec()\n return cacheKeys.length\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const client = await getRedisClient()\n const searchPattern = pattern \n ? `${keyPrefix}${pattern}` \n : `${keyPrefix}*`\n \n const cacheKeys = await client.keys(searchPattern)\n \n // Remove prefix from keys\n const result = cacheKeys.map((key: string) => key.substring(keyPrefix.length))\n \n if (!pattern) return result\n \n // Apply pattern matching (Redis KEYS command uses glob pattern, but we want our pattern)\n return result.filter((key: string) => matchCacheKeyPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let expired = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n expired++\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n return { size: cacheKeys.length, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let removed = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n const key = cacheKey.substring(keyPrefix.length)\n await deleteKey(key)\n removed++\n }\n } catch {\n // Remove invalid entries\n await client.del(cacheKey)\n removed++\n }\n }\n }\n\n return removed\n }\n\n const close = async (): Promise<void> => {\n await releaseRedisEntry(connectionUrl, registryEntry)\n redis = null\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["options", "pipeline", "keys"]
|
|
7
7
|
}
|
|
@@ -3,6 +3,7 @@ import { createRequire } from "node:module";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CacheDependencyUnavailableError } from "../errors.js";
|
|
5
5
|
import { DEFAULT_SQLITE_CACHE_PATH } from "../defaults.js";
|
|
6
|
+
import { matchCacheKeyPattern } from "../patterns.js";
|
|
6
7
|
const sqliteRequire = createRequire(path.join(process.cwd(), "package.json"));
|
|
7
8
|
function createSqliteStrategy(dbPath, options) {
|
|
8
9
|
let db = null;
|
|
@@ -45,11 +46,6 @@ function createSqliteStrategy(dbPath, options) {
|
|
|
45
46
|
if (expiresAt === null) return false;
|
|
46
47
|
return Date.now() > expiresAt;
|
|
47
48
|
}
|
|
48
|
-
function matchPattern(key, pattern) {
|
|
49
|
-
const regexPattern = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*").replace(/\?/g, ".");
|
|
50
|
-
const regex = new RegExp(`^${regexPattern}$`);
|
|
51
|
-
return regex.test(key);
|
|
52
|
-
}
|
|
53
49
|
const get = async (key, options2) => {
|
|
54
50
|
const database = await getDb();
|
|
55
51
|
const stmt = database.prepare("SELECT value, expires_at FROM cache_entries WHERE key = ?");
|
|
@@ -143,7 +139,7 @@ function createSqliteStrategy(dbPath, options) {
|
|
|
143
139
|
const rows = stmt.all();
|
|
144
140
|
const allKeys = rows.map((row) => row.key);
|
|
145
141
|
if (!pattern) return allKeys;
|
|
146
|
-
return allKeys.filter((key) =>
|
|
142
|
+
return allKeys.filter((key) => matchCacheKeyPattern(key, pattern));
|
|
147
143
|
};
|
|
148
144
|
const stats = async () => {
|
|
149
145
|
const database = await getDb();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/strategies/sqlite.ts"],
|
|
4
|
-
"sourcesContent": ["import type { CacheStrategy, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport fs from 'node:fs'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { DEFAULT_SQLITE_CACHE_PATH } from '../defaults'\n\ntype SqliteStatement<TResult = unknown> = {\n get(...args: unknown[]): TResult | undefined\n all(...args: unknown[]): TResult[]\n run(...args: unknown[]): { changes: number }\n}\n\ntype SqliteTransaction<TResult = unknown> = () => TResult\n\ntype SqliteDatabase = {\n prepare<TResult = unknown>(sql: string): SqliteStatement<TResult>\n exec(sql: string): unknown\n transaction<TResult>(fn: () => TResult): SqliteTransaction<TResult>\n close(): void\n}\n\ntype SqliteConstructor = new (file: string) => SqliteDatabase\ntype SqliteModule = SqliteConstructor | { default: SqliteConstructor }\n\nconst sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))\n\n/**\n * SQLite cache strategy with tag support\n * Persistent across process restarts, stored in a SQLite database file\n * \n * Uses two tables:\n * - cache_entries: stores cache data\n * - cache_tags: stores tag associations (many-to-many)\n */\nexport function createSqliteStrategy(dbPath?: string, options?: { defaultTtl?: number }): CacheStrategy {\n let db: SqliteDatabase | null = null\n const defaultTtl = options?.defaultTtl\n const filePath = dbPath || process.env.CACHE_SQLITE_PATH || DEFAULT_SQLITE_CACHE_PATH\n\n async function getDb(): Promise<SqliteDatabase> {\n if (db) return db\n\n try {\n const imported = sqliteRequire('better-sqlite3') as SqliteModule\n const Database = typeof imported === 'function' ? imported : imported.default\n \n // Ensure directory exists\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n db = new Database(filePath)\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS cache_entries (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at INTEGER,\n created_at INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS cache_tags (\n key TEXT NOT NULL,\n tag TEXT NOT NULL,\n PRIMARY KEY (key, tag),\n FOREIGN KEY (key) REFERENCES cache_entries(key) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_cache_tags_tag ON cache_tags(tag);\n CREATE INDEX IF NOT EXISTS idx_cache_entries_expires_at ON cache_entries(expires_at);\n `)\n\n return db\n } catch (error) {\n throw new CacheDependencyUnavailableError('sqlite', 'better-sqlite3', error)\n }\n }\n\n function isExpired(expiresAt: number | null): boolean {\n if (expiresAt === null) return false\n return Date.now() > expiresAt\n }\n\n
|
|
5
|
-
"mappings": "AACA,OAAO,QAAQ;AACf,SAAS,qBAAqB;AAC9B,OAAO,UAAU;AACjB,SAAS,uCAAuC;AAChD,SAAS,iCAAiC;
|
|
4
|
+
"sourcesContent": ["import type { CacheStrategy, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport fs from 'node:fs'\nimport { createRequire } from 'node:module'\nimport path from 'node:path'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { DEFAULT_SQLITE_CACHE_PATH } from '../defaults'\nimport { matchCacheKeyPattern } from '../patterns'\n\ntype SqliteStatement<TResult = unknown> = {\n get(...args: unknown[]): TResult | undefined\n all(...args: unknown[]): TResult[]\n run(...args: unknown[]): { changes: number }\n}\n\ntype SqliteTransaction<TResult = unknown> = () => TResult\n\ntype SqliteDatabase = {\n prepare<TResult = unknown>(sql: string): SqliteStatement<TResult>\n exec(sql: string): unknown\n transaction<TResult>(fn: () => TResult): SqliteTransaction<TResult>\n close(): void\n}\n\ntype SqliteConstructor = new (file: string) => SqliteDatabase\ntype SqliteModule = SqliteConstructor | { default: SqliteConstructor }\n\nconst sqliteRequire = createRequire(path.join(process.cwd(), 'package.json'))\n\n/**\n * SQLite cache strategy with tag support\n * Persistent across process restarts, stored in a SQLite database file\n * \n * Uses two tables:\n * - cache_entries: stores cache data\n * - cache_tags: stores tag associations (many-to-many)\n */\nexport function createSqliteStrategy(dbPath?: string, options?: { defaultTtl?: number }): CacheStrategy {\n let db: SqliteDatabase | null = null\n const defaultTtl = options?.defaultTtl\n const filePath = dbPath || process.env.CACHE_SQLITE_PATH || DEFAULT_SQLITE_CACHE_PATH\n\n async function getDb(): Promise<SqliteDatabase> {\n if (db) return db\n\n try {\n const imported = sqliteRequire('better-sqlite3') as SqliteModule\n const Database = typeof imported === 'function' ? imported : imported.default\n \n // Ensure directory exists\n const dir = path.dirname(filePath)\n if (!fs.existsSync(dir)) {\n fs.mkdirSync(dir, { recursive: true })\n }\n\n db = new Database(filePath)\n\n // Create tables\n db.exec(`\n CREATE TABLE IF NOT EXISTS cache_entries (\n key TEXT PRIMARY KEY,\n value TEXT NOT NULL,\n expires_at INTEGER,\n created_at INTEGER NOT NULL\n );\n\n CREATE TABLE IF NOT EXISTS cache_tags (\n key TEXT NOT NULL,\n tag TEXT NOT NULL,\n PRIMARY KEY (key, tag),\n FOREIGN KEY (key) REFERENCES cache_entries(key) ON DELETE CASCADE\n );\n\n CREATE INDEX IF NOT EXISTS idx_cache_tags_tag ON cache_tags(tag);\n CREATE INDEX IF NOT EXISTS idx_cache_entries_expires_at ON cache_entries(expires_at);\n `)\n\n return db\n } catch (error) {\n throw new CacheDependencyUnavailableError('sqlite', 'better-sqlite3', error)\n }\n }\n\n function isExpired(expiresAt: number | null): boolean {\n if (expiresAt === null) return false\n return Date.now() > expiresAt\n }\n\n type EntryRow = { value: string; expires_at: number | null }\n type ExpiresRow = { expires_at: number | null }\n type CountRow = { count: number }\n type KeyRow = { key: string }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const database = await getDb()\n \n const stmt = database.prepare('SELECT value, expires_at FROM cache_entries WHERE key = ?')\n const row = stmt.get(key) as EntryRow | undefined\n\n if (!row) return null\n\n try {\n const value = JSON.parse(row.value) as CacheValue\n const expiresAt = row.expires_at\n\n if (isExpired(expiresAt)) {\n if (options?.returnExpired) {\n return value\n }\n // Clean up expired entry\n await deleteKey(key)\n return null\n }\n\n return value\n } catch {\n // Invalid JSON, remove it\n await deleteKey(key)\n return null\n }\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const database = await getDb()\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n const createdAt = Date.now()\n\n const serialized = JSON.stringify(value)\n\n database.transaction(() => {\n // Delete old tags\n database.prepare('DELETE FROM cache_tags WHERE key = ?').run(key)\n\n // Insert or replace cache entry\n database.prepare(`\n INSERT OR REPLACE INTO cache_entries (key, value, expires_at, created_at)\n VALUES (?, ?, ?, ?)\n `).run(key, serialized, expiresAt, createdAt)\n\n // Insert new tags\n if (tags.length > 0) {\n const insertTag = database.prepare('INSERT INTO cache_tags (key, tag) VALUES (?, ?)')\n for (const tag of tags) {\n insertTag.run(key, tag)\n }\n }\n })()\n }\n\n const has = async (key: string): Promise<boolean> => {\n const database = await getDb()\n \n const stmt = database.prepare('SELECT expires_at FROM cache_entries WHERE key = ?')\n const row = stmt.get(key) as ExpiresRow | undefined\n\n if (!row) return false\n\n if (isExpired(row.expires_at)) {\n await deleteKey(key)\n return false\n }\n\n return true\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const database = await getDb()\n \n const result = database.transaction(() => {\n database.prepare('DELETE FROM cache_tags WHERE key = ?').run(key)\n const info = database.prepare('DELETE FROM cache_entries WHERE key = ?').run(key)\n return info.changes > 0\n })()\n\n return result\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const database = await getDb()\n \n // Get all unique keys that have any of the specified tags\n const placeholders = tags.map(() => '?').join(',')\n const stmt = database.prepare(`\n SELECT DISTINCT key FROM cache_tags WHERE tag IN (${placeholders})\n `)\n const rows = stmt.all(...tags) as KeyRow[]\n\n let deleted = 0\n for (const row of rows) {\n const success = await deleteKey(row.key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const database = await getDb()\n \n const result = database.transaction(() => {\n const countStmt = database.prepare('SELECT COUNT(*) as count FROM cache_entries')\n const count = (countStmt.get() as CountRow).count\n\n database.prepare('DELETE FROM cache_tags').run()\n database.prepare('DELETE FROM cache_entries').run()\n\n return count\n })()\n\n return result\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const database = await getDb()\n \n const stmt = database.prepare('SELECT key FROM cache_entries')\n const rows = stmt.all() as KeyRow[]\n \n const allKeys = rows.map((row) => row.key)\n \n if (!pattern) return allKeys\n \n return allKeys.filter((key: string) => matchCacheKeyPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const database = await getDb()\n \n const sizeStmt = database.prepare('SELECT COUNT(*) as count FROM cache_entries')\n const size = (sizeStmt.get() as CountRow).count\n\n const now = Date.now()\n const expiredStmt = database.prepare('SELECT COUNT(*) as count FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?')\n const expired = (expiredStmt.get(now) as CountRow).count\n\n return { size, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const database = await getDb()\n const now = Date.now()\n \n const result = database.transaction(() => {\n // Get keys to delete\n const stmt = database.prepare('SELECT key FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?')\n const rows = stmt.all(now) as KeyRow[]\n\n // Delete tags for expired keys\n for (const row of rows) {\n database.prepare('DELETE FROM cache_tags WHERE key = ?').run(row.key)\n }\n\n // Delete expired entries\n const info = database.prepare('DELETE FROM cache_entries WHERE expires_at IS NOT NULL AND expires_at < ?').run(now)\n return info.changes\n })()\n\n return result\n }\n\n const close = async (): Promise<void> => {\n if (db) {\n db.close()\n db = null\n }\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["options"]
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { matchCacheKeyPattern } from '../patterns'
|
|
2
|
+
|
|
3
|
+
describe('matchCacheKeyPattern', () => {
|
|
4
|
+
it('matches cache glob wildcards without regex execution', () => {
|
|
5
|
+
expect(matchCacheKeyPattern('user:1', 'user:*')).toBe(true)
|
|
6
|
+
expect(matchCacheKeyPattern('user:1', 'user:?')).toBe(true)
|
|
7
|
+
expect(matchCacheKeyPattern('user:100', 'user:?')).toBe(false)
|
|
8
|
+
expect(matchCacheKeyPattern('org:1', 'user:*')).toBe(false)
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('treats regex metacharacters as literal cache key characters', () => {
|
|
12
|
+
expect(matchCacheKeyPattern('tenant:(a+)+', 'tenant:(*)+')).toBe(true)
|
|
13
|
+
expect(matchCacheKeyPattern('tenant:(a+)+', 'tenant:.+')).toBe(false)
|
|
14
|
+
})
|
|
15
|
+
})
|
|
16
|
+
|
package/src/patterns.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
export function matchCacheKeyPattern(key: string, pattern: string): boolean {
|
|
2
|
+
let patternIndex = 0
|
|
3
|
+
let keyIndex = 0
|
|
4
|
+
let starIndex = -1
|
|
5
|
+
let keyRetryIndex = 0
|
|
6
|
+
|
|
7
|
+
while (keyIndex < key.length) {
|
|
8
|
+
const patternChar = pattern[patternIndex]
|
|
9
|
+
const keyChar = key[keyIndex]
|
|
10
|
+
|
|
11
|
+
if (patternChar === '*') {
|
|
12
|
+
starIndex = patternIndex
|
|
13
|
+
keyRetryIndex = keyIndex
|
|
14
|
+
patternIndex += 1
|
|
15
|
+
continue
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (patternChar === '?' || patternChar === keyChar) {
|
|
19
|
+
patternIndex += 1
|
|
20
|
+
keyIndex += 1
|
|
21
|
+
continue
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
if (starIndex !== -1) {
|
|
25
|
+
patternIndex = starIndex + 1
|
|
26
|
+
keyRetryIndex += 1
|
|
27
|
+
keyIndex = keyRetryIndex
|
|
28
|
+
continue
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
while (pattern[patternIndex] === '*') {
|
|
35
|
+
patternIndex += 1
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return patternIndex === pattern.length
|
|
39
|
+
}
|
|
40
|
+
|
package/src/service.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { createJsonFileStrategy } from './strategies/jsonfile'
|
|
|
6
6
|
import { getCurrentCacheTenant } from './tenantContext'
|
|
7
7
|
import { createHash } from 'node:crypto'
|
|
8
8
|
import { CacheDependencyUnavailableError } from './errors'
|
|
9
|
+
import { matchCacheKeyPattern } from './patterns'
|
|
9
10
|
|
|
10
11
|
function normalizeTenantKey(raw: string | null | undefined): string {
|
|
11
12
|
const value = typeof raw === 'string' ? raw.trim() : ''
|
|
@@ -83,15 +84,6 @@ function buildTagSet(tags: string[] | undefined, prefixes: TenantPrefixes, inclu
|
|
|
83
84
|
return Array.from(scoped)
|
|
84
85
|
}
|
|
85
86
|
|
|
86
|
-
function matchPattern(value: string, pattern: string): boolean {
|
|
87
|
-
const regexPattern = pattern
|
|
88
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
89
|
-
.replace(/\*/g, '.*')
|
|
90
|
-
.replace(/\?/g, '.')
|
|
91
|
-
const regex = new RegExp(`^${regexPattern}$`)
|
|
92
|
-
return regex.test(value)
|
|
93
|
-
}
|
|
94
|
-
|
|
95
87
|
function createTenantAwareWrapper(base: CacheStrategy): CacheStrategy {
|
|
96
88
|
function normalizeDeletionCount(raw: number): number {
|
|
97
89
|
if (!raw) return raw
|
|
@@ -156,7 +148,7 @@ function createTenantAwareWrapper(base: CacheStrategy): CacheStrategy {
|
|
|
156
148
|
const metadata = typeof metaValue === 'string' ? null : (isCacheMetadata(metaValue) ? metaValue : null)
|
|
157
149
|
const original = typeof metaValue === 'string' ? metaValue : metadata?.key
|
|
158
150
|
if (!original) continue
|
|
159
|
-
if (pattern && !
|
|
151
|
+
if (pattern && !matchCacheKeyPattern(original, pattern)) continue
|
|
160
152
|
originals.push(original)
|
|
161
153
|
}
|
|
162
154
|
return originals
|
|
@@ -2,6 +2,7 @@ import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, Cache
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { DEFAULT_JSON_FILE_CACHE_PATH } from '../defaults'
|
|
5
|
+
import { matchCacheKeyPattern } from '../patterns'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* JSON file cache strategy with tag support
|
|
@@ -47,15 +48,6 @@ export function createJsonFileStrategy(filePath?: string, options?: { defaultTtl
|
|
|
47
48
|
return Date.now() > entry.expiresAt
|
|
48
49
|
}
|
|
49
50
|
|
|
50
|
-
function matchPattern(key: string, pattern: string): boolean {
|
|
51
|
-
const regexPattern = pattern
|
|
52
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
53
|
-
.replace(/\*/g, '.*')
|
|
54
|
-
.replace(/\?/g, '.')
|
|
55
|
-
const regex = new RegExp(`^${regexPattern}$`)
|
|
56
|
-
return regex.test(key)
|
|
57
|
-
}
|
|
58
|
-
|
|
59
51
|
function addToTagIndex(data: StorageData, key: string, tags: string[]): void {
|
|
60
52
|
for (const tag of tags) {
|
|
61
53
|
if (!data.tagIndex[tag]) {
|
|
@@ -194,7 +186,7 @@ export function createJsonFileStrategy(filePath?: string, options?: { defaultTtl
|
|
|
194
186
|
|
|
195
187
|
if (!pattern) return allKeys
|
|
196
188
|
|
|
197
|
-
return allKeys.filter((key) =>
|
|
189
|
+
return allKeys.filter((key) => matchCacheKeyPattern(key, pattern))
|
|
198
190
|
}
|
|
199
191
|
|
|
200
192
|
const stats = async (): Promise<{ size: number; expired: number }> => {
|
package/src/strategies/memory.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'
|
|
2
|
+
import { matchCacheKeyPattern } from '../patterns'
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* In-memory cache strategy with tag support
|
|
@@ -49,15 +50,6 @@ export function createMemoryStrategy(options?: { defaultTtl?: number }): CacheSt
|
|
|
49
50
|
}
|
|
50
51
|
}
|
|
51
52
|
|
|
52
|
-
function matchPattern(key: string, pattern: string): boolean {
|
|
53
|
-
const regexPattern = pattern
|
|
54
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&') // Escape special regex chars
|
|
55
|
-
.replace(/\*/g, '.*') // * matches any characters
|
|
56
|
-
.replace(/\?/g, '.') // ? matches single character
|
|
57
|
-
const regex = new RegExp(`^${regexPattern}$`)
|
|
58
|
-
return regex.test(key)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
53
|
const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {
|
|
62
54
|
const entry = store.get(key)
|
|
63
55
|
if (!entry) return null
|
|
@@ -147,7 +139,7 @@ export function createMemoryStrategy(options?: { defaultTtl?: number }): CacheSt
|
|
|
147
139
|
const keys = async (pattern?: string): Promise<string[]> => {
|
|
148
140
|
const allKeys = Array.from(store.keys())
|
|
149
141
|
if (!pattern) return allKeys
|
|
150
|
-
return allKeys.filter((key) =>
|
|
142
|
+
return allKeys.filter((key) => matchCacheKeyPattern(key, pattern))
|
|
151
143
|
}
|
|
152
144
|
|
|
153
145
|
const stats = async (): Promise<{ size: number; expired: number }> => {
|
package/src/strategies/redis.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'
|
|
2
2
|
import { CacheDependencyUnavailableError } from '../errors'
|
|
3
3
|
import { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'
|
|
4
|
+
import { matchCacheKeyPattern } from '../patterns'
|
|
4
5
|
|
|
5
6
|
type RedisPipeline = {
|
|
6
7
|
set(key: string, value: string): RedisPipeline
|
|
@@ -179,15 +180,6 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
179
180
|
return Date.now() > entry.expiresAt
|
|
180
181
|
}
|
|
181
182
|
|
|
182
|
-
function matchPattern(key: string, pattern: string): boolean {
|
|
183
|
-
const regexPattern = pattern
|
|
184
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
185
|
-
.replace(/\*/g, '.*')
|
|
186
|
-
.replace(/\?/g, '.')
|
|
187
|
-
const regex = new RegExp(`^${regexPattern}$`)
|
|
188
|
-
return regex.test(key)
|
|
189
|
-
}
|
|
190
|
-
|
|
191
183
|
const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {
|
|
192
184
|
const client = await getRedisClient()
|
|
193
185
|
const cacheKey = getCacheKey(key)
|
|
@@ -372,7 +364,7 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
|
|
|
372
364
|
if (!pattern) return result
|
|
373
365
|
|
|
374
366
|
// Apply pattern matching (Redis KEYS command uses glob pattern, but we want our pattern)
|
|
375
|
-
return result.filter((key: string) =>
|
|
367
|
+
return result.filter((key: string) => matchCacheKeyPattern(key, pattern))
|
|
376
368
|
}
|
|
377
369
|
|
|
378
370
|
const stats = async (): Promise<{ size: number; expired: number }> => {
|
package/src/strategies/sqlite.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { createRequire } from 'node:module'
|
|
|
4
4
|
import path from 'node:path'
|
|
5
5
|
import { CacheDependencyUnavailableError } from '../errors'
|
|
6
6
|
import { DEFAULT_SQLITE_CACHE_PATH } from '../defaults'
|
|
7
|
+
import { matchCacheKeyPattern } from '../patterns'
|
|
7
8
|
|
|
8
9
|
type SqliteStatement<TResult = unknown> = {
|
|
9
10
|
get(...args: unknown[]): TResult | undefined
|
|
@@ -84,15 +85,6 @@ export function createSqliteStrategy(dbPath?: string, options?: { defaultTtl?: n
|
|
|
84
85
|
return Date.now() > expiresAt
|
|
85
86
|
}
|
|
86
87
|
|
|
87
|
-
function matchPattern(key: string, pattern: string): boolean {
|
|
88
|
-
const regexPattern = pattern
|
|
89
|
-
.replace(/[.+^${}()|[\]\\]/g, '\\$&')
|
|
90
|
-
.replace(/\*/g, '.*')
|
|
91
|
-
.replace(/\?/g, '.')
|
|
92
|
-
const regex = new RegExp(`^${regexPattern}$`)
|
|
93
|
-
return regex.test(key)
|
|
94
|
-
}
|
|
95
|
-
|
|
96
88
|
type EntryRow = { value: string; expires_at: number | null }
|
|
97
89
|
type ExpiresRow = { expires_at: number | null }
|
|
98
90
|
type CountRow = { count: number }
|
|
@@ -229,7 +221,7 @@ export function createSqliteStrategy(dbPath?: string, options?: { defaultTtl?: n
|
|
|
229
221
|
|
|
230
222
|
if (!pattern) return allKeys
|
|
231
223
|
|
|
232
|
-
return allKeys.filter((key: string) =>
|
|
224
|
+
return allKeys.filter((key: string) => matchCacheKeyPattern(key, pattern))
|
|
233
225
|
}
|
|
234
226
|
|
|
235
227
|
const stats = async (): Promise<{ size: number; expired: number }> => {
|