@open-mercato/cache 0.6.6 → 0.6.7-canary.317.1.106b9d993b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/service.js CHANGED
@@ -21,10 +21,16 @@ function isCacheMetadata(value) {
21
21
  return hasValidKey && hasValidExpiresAt;
22
22
  }
23
23
  const KNOWN_STRATEGIES = ["memory", "redis", "sqlite", "jsonfile"];
24
+ const TENANT_META_KEY_PATTERN = "tenant:*:key:meta:*";
25
+ const TENANT_META_KEY_MARKER = ":key:meta:";
24
26
  function isCacheStrategyName(value) {
25
27
  if (!value) return false;
26
28
  return KNOWN_STRATEGIES.includes(value);
27
29
  }
30
+ function resolveConfiguredStrategyType(options) {
31
+ const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY) ? process.env.CACHE_STRATEGY : void 0;
32
+ return options?.strategy ?? envStrategy ?? "memory";
33
+ }
28
34
  function resolveTenantPrefixes() {
29
35
  const tenant = normalizeTenantKey(getCurrentCacheTenant());
30
36
  const base = `tenant:${tenant}:`;
@@ -116,6 +122,27 @@ function createTenantAwareWrapper(base, reportsBoundedCounters) {
116
122
  }
117
123
  return originals;
118
124
  };
125
+ const deleteByPatternsAcrossScopes = async (patterns) => {
126
+ const normalizedPatterns = Array.from(new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean)));
127
+ if (normalizedPatterns.length === 0) return 0;
128
+ const metaKeys = await base.keys(TENANT_META_KEY_PATTERN);
129
+ let deleted = 0;
130
+ for (const metadataKey of metaKeys) {
131
+ const metadataValue = await base.get(metadataKey, { returnExpired: true });
132
+ if (!metadataValue) continue;
133
+ const metadata = typeof metadataValue === "string" ? null : isCacheMetadata(metadataValue) ? metadataValue : null;
134
+ const originalKey = typeof metadataValue === "string" ? metadataValue : metadata?.key;
135
+ if (!originalKey || !normalizedPatterns.some((pattern) => matchCacheKeyPattern(originalKey, pattern))) {
136
+ continue;
137
+ }
138
+ const markerIndex = metadataKey.lastIndexOf(TENANT_META_KEY_MARKER);
139
+ if (markerIndex < 0) continue;
140
+ const primaryKey = `${metadataKey.slice(0, markerIndex)}:key:k:${metadataKey.slice(markerIndex + TENANT_META_KEY_MARKER.length)}`;
141
+ if (await base.delete(primaryKey)) deleted += 1;
142
+ await base.delete(metadataKey);
143
+ }
144
+ return deleted;
145
+ };
119
146
  const stats = async () => {
120
147
  const prefixes = resolveTenantPrefixes();
121
148
  const metaKeys = await base.keys(`${prefixes.keyPrefix}meta:*`);
@@ -152,12 +179,12 @@ function createTenantAwareWrapper(base, reportsBoundedCounters) {
152
179
  stats,
153
180
  healthcheck,
154
181
  cleanup,
155
- close
182
+ close,
183
+ deleteByPatternsAcrossScopes
156
184
  };
157
185
  }
158
- function createCacheService(options) {
159
- const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY) ? process.env.CACHE_STRATEGY : void 0;
160
- const strategyType = options?.strategy ?? envStrategy ?? "memory";
186
+ function createConfiguredTenantAwareStrategy(options) {
187
+ const strategyType = resolveConfiguredStrategyType(options);
161
188
  const envTtl = process.env.CACHE_TTL;
162
189
  const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : void 0;
163
190
  const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === "number" && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : void 0);
@@ -169,6 +196,18 @@ function createCacheService(options) {
169
196
  const reportsBoundedCounters = strategyType === "memory";
170
197
  return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters);
171
198
  }
199
+ function createCacheService(options) {
200
+ return createConfiguredTenantAwareStrategy(options);
201
+ }
202
+ async function purgeConfiguredCachePatternsAcrossTenantScopes(patterns, options) {
203
+ if (resolveConfiguredStrategyType(options) === "memory") return 0;
204
+ const cache = createConfiguredTenantAwareStrategy(options);
205
+ try {
206
+ return await cache.deleteByPatternsAcrossScopes(patterns);
207
+ } finally {
208
+ await cache.close?.();
209
+ }
210
+ }
172
211
  class CacheService {
173
212
  constructor(options) {
174
213
  this.strategy = createCacheService(options);
@@ -293,6 +332,7 @@ function withDependencyFallback(strategy, strategyType, defaultTtl, maxEntries)
293
332
  }
294
333
  export {
295
334
  CacheService,
296
- createCacheService
335
+ createCacheService,
336
+ purgeConfiguredCachePatternsAcrossTenantScopes
297
337
  };
298
338
  //# sourceMappingURL=service.js.map
@@ -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'\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, reportsBoundedCounters: boolean): 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 // size/expired stay tenant-scoped (derived from this tenant's meta keys).\n // Only memory-backed strategies track the process-global bound counters, so\n // skip the extra base.stats() round-trip for redis/sqlite/jsonfile and omit\n // the optional fields (the stats() contract marks them optional). Keyed off\n // the configured strategy: a backend degraded to the memory fallback\n // intentionally does not surface these diagnostics.\n if (!reportsBoundedCounters) {\n return { size, expired }\n }\n const { evictions, sweeps, lastSweepReclaimed } = await base.stats()\n return { size, expired, evictions, sweeps, lastSweepReclaimed }\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 const healthcheck = base.healthcheck\n ? async () => base.healthcheck!()\n : undefined\n\n return {\n get,\n set,\n has,\n delete: del,\n deleteByTags,\n clear,\n keys,\n stats,\n healthcheck,\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 envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES\n const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined\n const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)\n\n const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries)\n const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)\n const reportsBoundedCounters = strategyType === 'memory'\n\n return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters)\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<{\n size: number\n expired: number\n evictions?: number\n sweeps?: number\n lastSweepReclaimed?: number\n }> {\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, maxEntries?: 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, maxEntries })\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, maxEntries?: 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, maxEntries })\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 healthcheck: typeof strategy.healthcheck === 'function'\n ? async () => strategy.healthcheck!()\n : undefined,\n cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,\n close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,\n }\n}\n"],
5
- "mappings": "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",
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']\nconst TENANT_META_KEY_PATTERN = 'tenant:*:key:meta:*'\nconst TENANT_META_KEY_MARKER = ':key:meta:'\n\ntype TenantAwareCacheStrategy = CacheStrategy & {\n deleteByPatternsAcrossScopes(patterns: string[]): Promise<number>\n}\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 resolveConfiguredStrategyType(options?: CacheServiceOptions): CacheStrategyName {\n const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)\n ? process.env.CACHE_STRATEGY\n : undefined\n return options?.strategy ?? envStrategy ?? 'memory'\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, reportsBoundedCounters: boolean): TenantAwareCacheStrategy {\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 deleteByPatternsAcrossScopes = async (patterns: string[]) => {\n const normalizedPatterns = Array.from(new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean)))\n if (normalizedPatterns.length === 0) return 0\n\n const metaKeys = await base.keys(TENANT_META_KEY_PATTERN)\n let deleted = 0\n for (const metadataKey of metaKeys) {\n const metadataValue = await base.get(metadataKey, { returnExpired: true })\n if (!metadataValue) continue\n const metadata = typeof metadataValue === 'string'\n ? null\n : (isCacheMetadata(metadataValue) ? metadataValue : null)\n const originalKey = typeof metadataValue === 'string' ? metadataValue : metadata?.key\n if (!originalKey || !normalizedPatterns.some((pattern) => matchCacheKeyPattern(originalKey, pattern))) {\n continue\n }\n\n const markerIndex = metadataKey.lastIndexOf(TENANT_META_KEY_MARKER)\n if (markerIndex < 0) continue\n const primaryKey = `${metadataKey.slice(0, markerIndex)}:key:k:${metadataKey.slice(markerIndex + TENANT_META_KEY_MARKER.length)}`\n if (await base.delete(primaryKey)) deleted += 1\n await base.delete(metadataKey)\n }\n return deleted\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 // size/expired stay tenant-scoped (derived from this tenant's meta keys).\n // Only memory-backed strategies track the process-global bound counters, so\n // skip the extra base.stats() round-trip for redis/sqlite/jsonfile and omit\n // the optional fields (the stats() contract marks them optional). Keyed off\n // the configured strategy: a backend degraded to the memory fallback\n // intentionally does not surface these diagnostics.\n if (!reportsBoundedCounters) {\n return { size, expired }\n }\n const { evictions, sweeps, lastSweepReclaimed } = await base.stats()\n return { size, expired, evictions, sweeps, lastSweepReclaimed }\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 const healthcheck = base.healthcheck\n ? async () => base.healthcheck!()\n : undefined\n\n return {\n get,\n set,\n has,\n delete: del,\n deleteByTags,\n clear,\n keys,\n stats,\n healthcheck,\n cleanup,\n close,\n deleteByPatternsAcrossScopes,\n }\n}\n\nfunction createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): TenantAwareCacheStrategy {\n const strategyType = resolveConfiguredStrategyType(options)\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 envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES\n const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined\n const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)\n\n const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries)\n const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)\n const reportsBoundedCounters = strategyType === 'memory'\n\n return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters)\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 return createConfiguredTenantAwareStrategy(options)\n}\n\n/**\n * Delete matching logical keys across every tenant scope in the configured\n * stock cache backend without bootstrapping application DI or querying the\n * tenant database. Intended for maintenance tasks that already operate on all\n * scopes, such as post-generation structural invalidation.\n */\nexport async function purgeConfiguredCachePatternsAcrossTenantScopes(\n patterns: string[],\n options?: CacheServiceOptions,\n): Promise<number> {\n // A newly-created memory strategy cannot reach the running app's process-local\n // cache, so constructing one here would only allocate and immediately discard\n // an empty store. Persistent stock backends remain cross-process maintainable.\n if (resolveConfiguredStrategyType(options) === 'memory') return 0\n\n const cache = createConfiguredTenantAwareStrategy(options)\n try {\n return await cache.deleteByPatternsAcrossScopes(patterns)\n } finally {\n await cache.close?.()\n }\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<{\n size: number\n expired: number\n evictions?: number\n sweeps?: number\n lastSweepReclaimed?: number\n }> {\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, maxEntries?: 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, maxEntries })\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, maxEntries?: 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, maxEntries })\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 healthcheck: typeof strategy.healthcheck === 'function'\n ? async () => strategy.healthcheck!()\n : undefined,\n cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,\n close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["metaKey"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cache",
3
- "version": "0.6.6",
3
+ "version": "0.6.7-canary.317.1.106b9d993b",
4
4
  "license": "MIT",
5
5
  "description": "Multi-strategy cache service with tag-based invalidation support",
6
6
  "type": "module",
@@ -62,7 +62,7 @@
62
62
  "@types/node": "^26.0.1",
63
63
  "jest": "^30.4.2",
64
64
  "ts-jest": "^29.4.11",
65
- "typescript": "^6.0.3"
65
+ "typescript": "7.0.2"
66
66
  },
67
67
  "publishConfig": {
68
68
  "access": "public"
@@ -71,5 +71,6 @@
71
71
  "type": "git",
72
72
  "url": "https://github.com/open-mercato/open-mercato",
73
73
  "directory": "packages/cache"
74
- }
74
+ },
75
+ "stableVersion": "0.6.6"
75
76
  }
@@ -1,8 +1,13 @@
1
- import { createCacheService, CacheService } from '../service'
1
+ import {
2
+ createCacheService,
3
+ CacheService,
4
+ purgeConfiguredCachePatternsAcrossTenantScopes,
5
+ } from '../service'
2
6
  import fs from 'node:fs'
3
7
  import path from 'node:path'
4
8
  import { DEFAULT_JSON_FILE_CACHE_PATH, DEFAULT_SQLITE_CACHE_PATH } from '../defaults'
5
9
  import type { CacheStrategy } from '../types'
10
+ import { runWithCacheTenant } from '../tenantContext'
6
11
  import * as memoryStrategyModule from '../strategies/memory'
7
12
  import * as sqliteStrategyModule from '../strategies/sqlite'
8
13
 
@@ -115,6 +120,67 @@ describe('Cache Service', () => {
115
120
  })
116
121
 
117
122
  describe('Complex scenarios', () => {
123
+ it('does not allocate a throwaway cache for cross-process memory maintenance', async () => {
124
+ const createMemorySpy = jest.spyOn(memoryStrategyModule, 'createMemoryStrategy')
125
+ await expect(
126
+ purgeConfiguredCachePatternsAcrossTenantScopes(['nav:*'], { strategy: 'memory' }),
127
+ ).resolves.toBe(0)
128
+ expect(createMemorySpy).not.toHaveBeenCalled()
129
+ createMemorySpy.mockRestore()
130
+ })
131
+
132
+ it('purges matching logical keys across tenant scopes without a tenant database', async () => {
133
+ const root = fs.mkdtempSync(path.join(process.cwd(), '.cache-maintenance-'))
134
+ const jsonFilePath = path.join(root, 'cache.json')
135
+ const options = { strategy: 'jsonfile' as const, jsonFilePath }
136
+ try {
137
+ const cache = createCacheService(options)
138
+ await runWithCacheTenant(null, async () => {
139
+ await cache.set('nav:sidebar:global', 'global', { tags: ['nav'] })
140
+ })
141
+ await runWithCacheTenant('tenant-a', async () => {
142
+ await cache.set('nav:sidebar:tenant-a', 'tenant-a', { tags: ['nav'] })
143
+ await cache.set('crud|auth|GET|/api/auth/admin/nav|user-a', 'admin', { tags: ['auth'] })
144
+ await cache.set('crud|auth|GET|/api/auth/admin/naval|user-a', 'not-admin-nav')
145
+ await cache.set('unrelated:key', 'keep', { tags: ['unrelated'] })
146
+ })
147
+ await runWithCacheTenant('tenant-b', async () => {
148
+ await cache.set('crud|customer_accounts|GET|/api/customer_accounts/portal/nav|user-b', 'portal')
149
+ })
150
+ await cache.close?.()
151
+
152
+ const deleted = await purgeConfiguredCachePatternsAcrossTenantScopes(
153
+ ['nav:*', 'crud|*|*|*/admin/nav|*', 'crud|*|*|*/portal/nav|*'],
154
+ options,
155
+ )
156
+
157
+ expect(deleted).toBe(4)
158
+ const verificationCache = createCacheService(options)
159
+ await runWithCacheTenant(null, async () => {
160
+ expect(await verificationCache.get('nav:sidebar:global')).toBeNull()
161
+ })
162
+ await runWithCacheTenant('tenant-a', async () => {
163
+ expect(await verificationCache.get('nav:sidebar:tenant-a')).toBeNull()
164
+ expect(await verificationCache.get('crud|auth|GET|/api/auth/admin/nav|user-a')).toBeNull()
165
+ expect(await verificationCache.get('crud|auth|GET|/api/auth/admin/naval|user-a')).toBe('not-admin-nav')
166
+ expect(await verificationCache.get('unrelated:key')).toBe('keep')
167
+ })
168
+ await runWithCacheTenant('tenant-b', async () => {
169
+ expect(await verificationCache.get('crud|customer_accounts|GET|/api/customer_accounts/portal/nav|user-b')).toBeNull()
170
+ })
171
+ await verificationCache.close?.()
172
+
173
+ const storage = JSON.parse(fs.readFileSync(jsonFilePath, 'utf8')) as {
174
+ entries: Record<string, unknown>
175
+ tagIndex: Record<string, string[]>
176
+ }
177
+ expect(Object.keys(storage.entries)).toHaveLength(4)
178
+ expect(Object.values(storage.tagIndex).every((keys) => keys.every((key) => key in storage.entries))).toBe(true)
179
+ } finally {
180
+ fs.rmSync(root, { recursive: true, force: true })
181
+ }
182
+ })
183
+
118
184
  it('should handle concurrent operations', async () => {
119
185
  const cache = createCacheService()
120
186
 
package/src/service.ts CHANGED
@@ -41,12 +41,25 @@ function isCacheMetadata(value: CacheValue | null): value is CacheMetadata {
41
41
 
42
42
  type CacheStrategyName = NonNullable<CacheServiceOptions['strategy']>
43
43
  const KNOWN_STRATEGIES: CacheStrategyName[] = ['memory', 'redis', 'sqlite', 'jsonfile']
44
+ const TENANT_META_KEY_PATTERN = 'tenant:*:key:meta:*'
45
+ const TENANT_META_KEY_MARKER = ':key:meta:'
46
+
47
+ type TenantAwareCacheStrategy = CacheStrategy & {
48
+ deleteByPatternsAcrossScopes(patterns: string[]): Promise<number>
49
+ }
44
50
 
45
51
  function isCacheStrategyName(value: string | undefined): value is CacheStrategyName {
46
52
  if (!value) return false
47
53
  return KNOWN_STRATEGIES.includes(value as CacheStrategyName)
48
54
  }
49
55
 
56
+ function resolveConfiguredStrategyType(options?: CacheServiceOptions): CacheStrategyName {
57
+ const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)
58
+ ? process.env.CACHE_STRATEGY
59
+ : undefined
60
+ return options?.strategy ?? envStrategy ?? 'memory'
61
+ }
62
+
50
63
  function resolveTenantPrefixes(): TenantPrefixes {
51
64
  const tenant = normalizeTenantKey(getCurrentCacheTenant())
52
65
  const base = `tenant:${tenant}:`
@@ -84,7 +97,7 @@ function buildTagSet(tags: string[] | undefined, prefixes: TenantPrefixes, inclu
84
97
  return Array.from(scoped)
85
98
  }
86
99
 
87
- function createTenantAwareWrapper(base: CacheStrategy, reportsBoundedCounters: boolean): CacheStrategy {
100
+ function createTenantAwareWrapper(base: CacheStrategy, reportsBoundedCounters: boolean): TenantAwareCacheStrategy {
88
101
  function normalizeDeletionCount(raw: number): number {
89
102
  if (!raw) return raw
90
103
  if (!Number.isFinite(raw)) return raw
@@ -154,6 +167,32 @@ function createTenantAwareWrapper(base: CacheStrategy, reportsBoundedCounters: b
154
167
  return originals
155
168
  }
156
169
 
170
+ const deleteByPatternsAcrossScopes = async (patterns: string[]) => {
171
+ const normalizedPatterns = Array.from(new Set(patterns.map((pattern) => pattern.trim()).filter(Boolean)))
172
+ if (normalizedPatterns.length === 0) return 0
173
+
174
+ const metaKeys = await base.keys(TENANT_META_KEY_PATTERN)
175
+ let deleted = 0
176
+ for (const metadataKey of metaKeys) {
177
+ const metadataValue = await base.get(metadataKey, { returnExpired: true })
178
+ if (!metadataValue) continue
179
+ const metadata = typeof metadataValue === 'string'
180
+ ? null
181
+ : (isCacheMetadata(metadataValue) ? metadataValue : null)
182
+ const originalKey = typeof metadataValue === 'string' ? metadataValue : metadata?.key
183
+ if (!originalKey || !normalizedPatterns.some((pattern) => matchCacheKeyPattern(originalKey, pattern))) {
184
+ continue
185
+ }
186
+
187
+ const markerIndex = metadataKey.lastIndexOf(TENANT_META_KEY_MARKER)
188
+ if (markerIndex < 0) continue
189
+ const primaryKey = `${metadataKey.slice(0, markerIndex)}:key:k:${metadataKey.slice(markerIndex + TENANT_META_KEY_MARKER.length)}`
190
+ if (await base.delete(primaryKey)) deleted += 1
191
+ await base.delete(metadataKey)
192
+ }
193
+ return deleted
194
+ }
195
+
157
196
  const stats = async () => {
158
197
  const prefixes = resolveTenantPrefixes()
159
198
  const metaKeys = await base.keys(`${prefixes.keyPrefix}meta:*`)
@@ -206,19 +245,38 @@ function createTenantAwareWrapper(base: CacheStrategy, reportsBoundedCounters: b
206
245
  healthcheck,
207
246
  cleanup,
208
247
  close,
248
+ deleteByPatternsAcrossScopes,
209
249
  }
210
250
  }
211
251
 
252
+ function createConfiguredTenantAwareStrategy(options?: CacheServiceOptions): TenantAwareCacheStrategy {
253
+ const strategyType = resolveConfiguredStrategyType(options)
254
+
255
+ const envTtl = process.env.CACHE_TTL
256
+ const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined
257
+ const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)
258
+
259
+ const envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES
260
+ const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined
261
+ const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)
262
+
263
+ const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries)
264
+ const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)
265
+ const reportsBoundedCounters = strategyType === 'memory'
266
+
267
+ return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters)
268
+ }
269
+
212
270
  /**
213
271
  * Cache service that provides a unified interface to different cache strategies
214
- *
272
+ *
215
273
  * Configuration via environment variables:
216
274
  * - CACHE_STRATEGY: 'memory' | 'redis' | 'sqlite' | 'jsonfile' (default: 'memory')
217
275
  * - CACHE_TTL: Default TTL in milliseconds (optional)
218
276
  * - CACHE_REDIS_URL: Redis connection URL (for redis strategy)
219
277
  * - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)
220
278
  * - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)
221
- *
279
+ *
222
280
  * @example
223
281
  * const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })
224
282
  * await cache.set('user:123', { name: 'John' }, { tags: ['users', 'user:123'] })
@@ -226,24 +284,30 @@ function createTenantAwareWrapper(base: CacheStrategy, reportsBoundedCounters: b
226
284
  * await cache.deleteByTags(['users']) // Invalidate all user-related cache
227
285
  */
228
286
  export function createCacheService(options?: CacheServiceOptions): CacheStrategy {
229
- const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)
230
- ? process.env.CACHE_STRATEGY
231
- : undefined
232
- const strategyType: CacheStrategyName = options?.strategy ?? envStrategy ?? 'memory'
233
-
234
- const envTtl = process.env.CACHE_TTL
235
- const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined
236
- const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)
237
-
238
- const envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES
239
- const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined
240
- const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)
241
-
242
- const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries)
243
- const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)
244
- const reportsBoundedCounters = strategyType === 'memory'
287
+ return createConfiguredTenantAwareStrategy(options)
288
+ }
245
289
 
246
- return createTenantAwareWrapper(resilientStrategy, reportsBoundedCounters)
290
+ /**
291
+ * Delete matching logical keys across every tenant scope in the configured
292
+ * stock cache backend without bootstrapping application DI or querying the
293
+ * tenant database. Intended for maintenance tasks that already operate on all
294
+ * scopes, such as post-generation structural invalidation.
295
+ */
296
+ export async function purgeConfiguredCachePatternsAcrossTenantScopes(
297
+ patterns: string[],
298
+ options?: CacheServiceOptions,
299
+ ): Promise<number> {
300
+ // A newly-created memory strategy cannot reach the running app's process-local
301
+ // cache, so constructing one here would only allocate and immediately discard
302
+ // an empty store. Persistent stock backends remain cross-process maintainable.
303
+ if (resolveConfiguredStrategyType(options) === 'memory') return 0
304
+
305
+ const cache = createConfiguredTenantAwareStrategy(options)
306
+ try {
307
+ return await cache.deleteByPatternsAcrossScopes(patterns)
308
+ } finally {
309
+ await cache.close?.()
310
+ }
247
311
  }
248
312
 
249
313
  /**