@open-mercato/cache 0.6.5-develop.5337.1.534b781eac → 0.6.5

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
@@ -157,8 +157,11 @@ function createCacheService(options) {
157
157
  const envTtl = process.env.CACHE_TTL;
158
158
  const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : void 0;
159
159
  const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === "number" && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : void 0);
160
- const baseStrategy = createStrategyForType(strategyType, options, defaultTtl);
161
- const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl);
160
+ const envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES;
161
+ const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : void 0;
162
+ const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === "number" && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : void 0);
163
+ const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries);
164
+ const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries);
162
165
  return createTenantAwareWrapper(resilientStrategy);
163
166
  }
164
167
  class CacheService {
@@ -201,7 +204,7 @@ class CacheService {
201
204
  }
202
205
  }
203
206
  }
204
- function createStrategyForType(strategyType, options, defaultTtl) {
207
+ function createStrategyForType(strategyType, options, defaultTtl, maxEntries) {
205
208
  switch (strategyType) {
206
209
  case "redis":
207
210
  return createRedisStrategy(options?.redisUrl, { defaultTtl });
@@ -211,7 +214,7 @@ function createStrategyForType(strategyType, options, defaultTtl) {
211
214
  return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl });
212
215
  case "memory":
213
216
  default:
214
- return createMemoryStrategy({ defaultTtl });
217
+ return createMemoryStrategy({ defaultTtl, maxEntries });
215
218
  }
216
219
  }
217
220
  function describeDependencyFailure(error) {
@@ -231,14 +234,14 @@ function describeDependencyFailure(error) {
231
234
  }
232
235
  return `${error.dependency} failed to load`;
233
236
  }
234
- function withDependencyFallback(strategy, strategyType, defaultTtl) {
237
+ function withDependencyFallback(strategy, strategyType, defaultTtl, maxEntries) {
235
238
  if (strategyType === "memory") return strategy;
236
239
  let activeStrategy = strategy;
237
240
  let fallbackStrategy = null;
238
241
  let warned = false;
239
242
  const ensureFallback = (error) => {
240
243
  if (!fallbackStrategy) {
241
- fallbackStrategy = createMemoryStrategy({ defaultTtl });
244
+ fallbackStrategy = createMemoryStrategy({ defaultTtl, maxEntries });
242
245
  }
243
246
  if (!warned) {
244
247
  const dependencyMessage = error.dependency ? ` (${describeDependencyFailure(error)})` : "";
@@ -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): 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 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 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 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']\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 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\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, 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
  }
@@ -1,12 +1,36 @@
1
1
  import { matchCacheKeyPattern } from "../patterns.js";
2
+ const EXPIRED_SWEEP_WRITE_INTERVAL = 256;
3
+ const DEFAULT_MEMORY_MAX_ENTRIES = 5e4;
4
+ function normalizeMaxEntries(raw) {
5
+ if (raw === void 0) return DEFAULT_MEMORY_MAX_ENTRIES;
6
+ if (!Number.isFinite(raw) || raw <= 0) return Number.POSITIVE_INFINITY;
7
+ return Math.floor(raw);
8
+ }
2
9
  function createMemoryStrategy(options) {
3
10
  const store = /* @__PURE__ */ new Map();
4
11
  const tagIndex = /* @__PURE__ */ new Map();
5
12
  const defaultTtl = options?.defaultTtl;
13
+ const maxEntries = normalizeMaxEntries(options?.maxEntries);
14
+ let writesSinceSweep = 0;
6
15
  function isExpired(entry) {
7
16
  if (entry.expiresAt === null) return false;
8
17
  return Date.now() > entry.expiresAt;
9
18
  }
19
+ function touchKey(key, entry) {
20
+ if (maxEntries === Number.POSITIVE_INFINITY) return;
21
+ store.delete(key);
22
+ store.set(key, entry);
23
+ }
24
+ function evictIfNeeded() {
25
+ if (maxEntries === Number.POSITIVE_INFINITY) return;
26
+ while (store.size > maxEntries) {
27
+ const oldest = store.keys().next().value;
28
+ if (typeof oldest !== "string") break;
29
+ const entry = store.get(oldest);
30
+ store.delete(oldest);
31
+ if (entry) removeFromTagIndex(oldest, entry.tags);
32
+ }
33
+ }
10
34
  function cleanupExpiredEntry(key, entry) {
11
35
  store.delete(key);
12
36
  for (const tag of entry.tags) {
@@ -38,6 +62,15 @@ function createMemoryStrategy(options) {
38
62
  }
39
63
  }
40
64
  }
65
+ function sweepExpiredIfDue() {
66
+ if (++writesSinceSweep < EXPIRED_SWEEP_WRITE_INTERVAL) return;
67
+ writesSinceSweep = 0;
68
+ for (const [key, entry] of store.entries()) {
69
+ if (isExpired(entry)) {
70
+ cleanupExpiredEntry(key, entry);
71
+ }
72
+ }
73
+ }
41
74
  const get = async (key, options2) => {
42
75
  const entry = store.get(key);
43
76
  if (!entry) return null;
@@ -48,6 +81,7 @@ function createMemoryStrategy(options) {
48
81
  cleanupExpiredEntry(key, entry);
49
82
  return null;
50
83
  }
84
+ touchKey(key, entry);
51
85
  return entry.value;
52
86
  };
53
87
  const set = async (key, value, options2) => {
@@ -67,6 +101,8 @@ function createMemoryStrategy(options) {
67
101
  };
68
102
  store.set(key, entry);
69
103
  addToTagIndex(key, tags);
104
+ sweepExpiredIfDue();
105
+ evictIfNeeded();
70
106
  };
71
107
  const has = async (key) => {
72
108
  const entry = store.get(key);
@@ -143,6 +179,7 @@ function createMemoryStrategy(options) {
143
179
  };
144
180
  }
145
181
  export {
182
+ DEFAULT_MEMORY_MAX_ENTRIES,
146
183
  createMemoryStrategy
147
184
  };
148
185
  //# sourceMappingURL=memory.js.map
@@ -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'\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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",
4
+ "sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { matchCacheKeyPattern } from '../patterns'\n\nconst EXPIRED_SWEEP_WRITE_INTERVAL = 256\n\n/**\n * Default upper bound on the number of entries a memory cache retains.\n * Bounds memory for long-lived (process-wide) instances; LRU eviction drops\n * the least-recently-used entries once the cap is exceeded. Override via the\n * `maxEntries` option (or `CACHE_MEMORY_MAX_ENTRIES`); a non-positive value\n * disables the cap (unbounded \u2014 only safe for short-lived instances).\n */\nexport const DEFAULT_MEMORY_MAX_ENTRIES = 50_000\n\nfunction normalizeMaxEntries(raw?: number): number {\n if (raw === undefined) return DEFAULT_MEMORY_MAX_ENTRIES\n if (!Number.isFinite(raw) || raw <= 0) return Number.POSITIVE_INFINITY\n return Math.floor(raw)\n}\n\n/**\n * In-memory cache strategy with tag support.\n * Fast but data is lost when process restarts.\n *\n * Bounded by an LRU cap (`maxEntries`, default {@link DEFAULT_MEMORY_MAX_ENTRIES},\n * env-tunable via `CACHE_MEMORY_MAX_ENTRIES` resolved in the cache service) so a\n * process-shared instance (OM_BOOTSTRAP_CACHE, long-lived workers, memory-backed\n * CRUD list cache) cannot grow without limit on user-controllable key\n * cardinality. Recency is refreshed on read (Map re-insertion), the oldest\n * entries are evicted on write, and expired entries are reclaimed by an\n * amortized sweep every N writes \u2014 no per-instance timer, so the per-request\n * default stays leak-free (a per-instance setInterval would pin every request's\n * Maps for the process lifetime).\n */\nexport function createMemoryStrategy(options?: { defaultTtl?: number; maxEntries?: 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 const maxEntries = normalizeMaxEntries(options?.maxEntries)\n let writesSinceSweep = 0\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n // LRU bookkeeping: re-insert on read so the most-recently-used entry moves\n // to the tail (Map preserves insertion order), mirroring the rbacDefaultCache\n // precedent; evictIfNeeded then drops from the head (least-recently-used).\n function touchKey(key: string, entry: CacheEntry): void {\n if (maxEntries === Number.POSITIVE_INFINITY) return\n store.delete(key)\n store.set(key, entry)\n }\n\n function evictIfNeeded(): void {\n if (maxEntries === Number.POSITIVE_INFINITY) return\n while (store.size > maxEntries) {\n const oldest = store.keys().next().value\n if (typeof oldest !== 'string') break\n const entry = store.get(oldest)\n store.delete(oldest)\n if (entry) removeFromTagIndex(oldest, entry.tags)\n }\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 // Amortized reclamation of already-expired entries. Runs every N writes\n // instead of on a timer, keeping the no-shared-state property that makes the\n // per-request default safe. Independent of the LRU cap so expired-but-cold\n // entries are reclaimed even when the store stays under `maxEntries`.\n function sweepExpiredIfDue(): void {\n if (++writesSinceSweep < EXPIRED_SWEEP_WRITE_INTERVAL) return\n writesSinceSweep = 0\n for (const [key, entry] of store.entries()) {\n if (isExpired(entry)) {\n cleanupExpiredEntry(key, entry)\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 touchKey(key, entry)\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 sweepExpiredIfDue()\n evictIfNeeded()\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cache",
3
- "version": "0.6.5-develop.5337.1.534b781eac",
3
+ "version": "0.6.5",
4
4
  "description": "Multi-strategy cache service with tag-based invalidation support",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -58,7 +58,7 @@
58
58
  },
59
59
  "devDependencies": {
60
60
  "@types/jest": "^30.0.0",
61
- "@types/node": "^25.9.2",
61
+ "@types/node": "^25.9.3",
62
62
  "jest": "^30.4.2",
63
63
  "ts-jest": "^29.4.11"
64
64
  },
@@ -69,6 +69,5 @@
69
69
  "type": "git",
70
70
  "url": "https://github.com/open-mercato/open-mercato",
71
71
  "directory": "packages/cache"
72
- },
73
- "stableVersion": "0.6.4"
72
+ }
74
73
  }
@@ -233,6 +233,31 @@ describe('Memory Cache Strategy', () => {
233
233
  })
234
234
  })
235
235
 
236
+ describe('Amortized expired-entry sweep', () => {
237
+ it('should reclaim expired entries via the amortized sweep on writes', async () => {
238
+ jest.useFakeTimers()
239
+ jest.setSystemTime(new Date('2025-01-01T00:00:00.000Z'))
240
+ try {
241
+ const bounded = createMemoryStrategy()
242
+ for (let i = 0; i < 200; i++) {
243
+ await bounded.set(`expiring:${i}`, i, { ttl: 50 })
244
+ }
245
+ // All 200 entries are now expired but still resident (no sweep yet).
246
+ await jest.advanceTimersByTimeAsync(100)
247
+ expect((await bounded.stats()).size).toBe(200)
248
+
249
+ // Cross the sweep interval (256 cumulative writes) with fresh entries.
250
+ for (let i = 0; i < 60; i++) {
251
+ await bounded.set(`fresh:${i}`, i)
252
+ }
253
+ // The expired cohort was reclaimed; only the 60 fresh entries remain.
254
+ expect((await bounded.stats()).size).toBe(60)
255
+ } finally {
256
+ jest.useRealTimers()
257
+ }
258
+ })
259
+ })
260
+
236
261
  describe('Statistics', () => {
237
262
  beforeEach(() => {
238
263
  jest.useFakeTimers()
@@ -259,4 +284,55 @@ describe('Memory Cache Strategy', () => {
259
284
  expect(stats.expired).toBe(1)
260
285
  })
261
286
  })
287
+
288
+ describe('LRU bounding (maxEntries)', () => {
289
+ it('evicts the least-recently-used entry once the cap is exceeded', async () => {
290
+ const bounded = createMemoryStrategy({ maxEntries: 3 })
291
+ await bounded.set('a', 1)
292
+ await bounded.set('b', 2)
293
+ await bounded.set('c', 3)
294
+ // Inserting a 4th entry evicts the oldest (a)
295
+ await bounded.set('d', 4)
296
+
297
+ expect(await bounded.get('a')).toBeNull()
298
+ expect(await bounded.get('b')).toBe(2)
299
+ expect(await bounded.get('c')).toBe(3)
300
+ expect(await bounded.get('d')).toBe(4)
301
+ expect((await bounded.stats()).size).toBe(3)
302
+ })
303
+
304
+ it('keeps recently-read entries alive (reads refresh LRU position)', async () => {
305
+ const bounded = createMemoryStrategy({ maxEntries: 3 })
306
+ await bounded.set('a', 1)
307
+ await bounded.set('b', 2)
308
+ await bounded.set('c', 3)
309
+ // Touch 'a' so it becomes most-recently-used; 'b' is now the oldest
310
+ expect(await bounded.get('a')).toBe(1)
311
+ await bounded.set('d', 4)
312
+
313
+ expect(await bounded.get('a')).toBe(1)
314
+ expect(await bounded.get('b')).toBeNull()
315
+ expect(await bounded.get('c')).toBe(3)
316
+ expect(await bounded.get('d')).toBe(4)
317
+ })
318
+
319
+ it('drops evicted keys from the tag index so deleteByTags stays consistent', async () => {
320
+ const bounded = createMemoryStrategy({ maxEntries: 2 })
321
+ await bounded.set('a', 1, { tags: ['shared'] })
322
+ await bounded.set('b', 2, { tags: ['shared'] })
323
+ await bounded.set('c', 3, { tags: ['shared'] }) // evicts 'a'
324
+
325
+ const removed = await bounded.deleteByTags(['shared'])
326
+ expect(removed).toBe(2)
327
+ expect((await bounded.stats()).size).toBe(0)
328
+ })
329
+
330
+ it('treats a non-positive cap as unbounded', async () => {
331
+ const unbounded = createMemoryStrategy({ maxEntries: 0 })
332
+ for (let index = 0; index < 100; index += 1) {
333
+ await unbounded.set(`key${index}`, index)
334
+ }
335
+ expect((await unbounded.stats()).size).toBe(100)
336
+ })
337
+ })
262
338
  })
package/src/service.ts CHANGED
@@ -225,8 +225,12 @@ export function createCacheService(options?: CacheServiceOptions): CacheStrategy
225
225
  const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined
226
226
  const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)
227
227
 
228
- const baseStrategy = createStrategyForType(strategyType, options, defaultTtl)
229
- const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl)
228
+ const envMaxEntries = process.env.CACHE_MEMORY_MAX_ENTRIES
229
+ const parsedEnvMaxEntries = envMaxEntries ? Number.parseInt(envMaxEntries, 10) : undefined
230
+ const maxEntries = options?.maxEntries ?? (typeof parsedEnvMaxEntries === 'number' && Number.isFinite(parsedEnvMaxEntries) ? parsedEnvMaxEntries : undefined)
231
+
232
+ const baseStrategy = createStrategyForType(strategyType, options, defaultTtl, maxEntries)
233
+ const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl, maxEntries)
230
234
 
231
235
  return createTenantAwareWrapper(resilientStrategy)
232
236
  }
@@ -288,7 +292,7 @@ export class CacheService implements CacheStrategy {
288
292
  }
289
293
  }
290
294
 
291
- function createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number): CacheStrategy {
295
+ function createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number, maxEntries?: number): CacheStrategy {
292
296
  switch (strategyType) {
293
297
  case 'redis':
294
298
  return createRedisStrategy(options?.redisUrl, { defaultTtl })
@@ -298,7 +302,7 @@ function createStrategyForType(strategyType: CacheStrategyName, options?: CacheS
298
302
  return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })
299
303
  case 'memory':
300
304
  default:
301
- return createMemoryStrategy({ defaultTtl })
305
+ return createMemoryStrategy({ defaultTtl, maxEntries })
302
306
  }
303
307
  }
304
308
 
@@ -324,7 +328,7 @@ function describeDependencyFailure(error: CacheDependencyUnavailableError): stri
324
328
  return `${error.dependency} failed to load`
325
329
  }
326
330
 
327
- function withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStrategyName, defaultTtl?: number): CacheStrategy {
331
+ function withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStrategyName, defaultTtl?: number, maxEntries?: number): CacheStrategy {
328
332
  if (strategyType === 'memory') return strategy
329
333
 
330
334
  let activeStrategy = strategy
@@ -333,7 +337,7 @@ function withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStra
333
337
 
334
338
  const ensureFallback = (error: CacheDependencyUnavailableError) => {
335
339
  if (!fallbackStrategy) {
336
- fallbackStrategy = createMemoryStrategy({ defaultTtl })
340
+ fallbackStrategy = createMemoryStrategy({ defaultTtl, maxEntries })
337
341
  }
338
342
  if (!warned) {
339
343
  const dependencyMessage = error.dependency
@@ -1,20 +1,69 @@
1
1
  import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'
2
2
  import { matchCacheKeyPattern } from '../patterns'
3
3
 
4
+ const EXPIRED_SWEEP_WRITE_INTERVAL = 256
5
+
6
+ /**
7
+ * Default upper bound on the number of entries a memory cache retains.
8
+ * Bounds memory for long-lived (process-wide) instances; LRU eviction drops
9
+ * the least-recently-used entries once the cap is exceeded. Override via the
10
+ * `maxEntries` option (or `CACHE_MEMORY_MAX_ENTRIES`); a non-positive value
11
+ * disables the cap (unbounded — only safe for short-lived instances).
12
+ */
13
+ export const DEFAULT_MEMORY_MAX_ENTRIES = 50_000
14
+
15
+ function normalizeMaxEntries(raw?: number): number {
16
+ if (raw === undefined) return DEFAULT_MEMORY_MAX_ENTRIES
17
+ if (!Number.isFinite(raw) || raw <= 0) return Number.POSITIVE_INFINITY
18
+ return Math.floor(raw)
19
+ }
20
+
4
21
  /**
5
- * In-memory cache strategy with tag support
6
- * Fast but data is lost when process restarts
22
+ * In-memory cache strategy with tag support.
23
+ * Fast but data is lost when process restarts.
24
+ *
25
+ * Bounded by an LRU cap (`maxEntries`, default {@link DEFAULT_MEMORY_MAX_ENTRIES},
26
+ * env-tunable via `CACHE_MEMORY_MAX_ENTRIES` resolved in the cache service) so a
27
+ * process-shared instance (OM_BOOTSTRAP_CACHE, long-lived workers, memory-backed
28
+ * CRUD list cache) cannot grow without limit on user-controllable key
29
+ * cardinality. Recency is refreshed on read (Map re-insertion), the oldest
30
+ * entries are evicted on write, and expired entries are reclaimed by an
31
+ * amortized sweep every N writes — no per-instance timer, so the per-request
32
+ * default stays leak-free (a per-instance setInterval would pin every request's
33
+ * Maps for the process lifetime).
7
34
  */
8
- export function createMemoryStrategy(options?: { defaultTtl?: number }): CacheStrategy {
35
+ export function createMemoryStrategy(options?: { defaultTtl?: number; maxEntries?: number }): CacheStrategy {
9
36
  const store = new Map<string, CacheEntry>()
10
37
  const tagIndex = new Map<string, Set<string>>() // tag -> Set of keys
11
38
  const defaultTtl = options?.defaultTtl
39
+ const maxEntries = normalizeMaxEntries(options?.maxEntries)
40
+ let writesSinceSweep = 0
12
41
 
13
42
  function isExpired(entry: CacheEntry): boolean {
14
43
  if (entry.expiresAt === null) return false
15
44
  return Date.now() > entry.expiresAt
16
45
  }
17
46
 
47
+ // LRU bookkeeping: re-insert on read so the most-recently-used entry moves
48
+ // to the tail (Map preserves insertion order), mirroring the rbacDefaultCache
49
+ // precedent; evictIfNeeded then drops from the head (least-recently-used).
50
+ function touchKey(key: string, entry: CacheEntry): void {
51
+ if (maxEntries === Number.POSITIVE_INFINITY) return
52
+ store.delete(key)
53
+ store.set(key, entry)
54
+ }
55
+
56
+ function evictIfNeeded(): void {
57
+ if (maxEntries === Number.POSITIVE_INFINITY) return
58
+ while (store.size > maxEntries) {
59
+ const oldest = store.keys().next().value
60
+ if (typeof oldest !== 'string') break
61
+ const entry = store.get(oldest)
62
+ store.delete(oldest)
63
+ if (entry) removeFromTagIndex(oldest, entry.tags)
64
+ }
65
+ }
66
+
18
67
  function cleanupExpiredEntry(key: string, entry: CacheEntry): void {
19
68
  store.delete(key)
20
69
  // Remove from tag index
@@ -50,6 +99,20 @@ export function createMemoryStrategy(options?: { defaultTtl?: number }): CacheSt
50
99
  }
51
100
  }
52
101
 
102
+ // Amortized reclamation of already-expired entries. Runs every N writes
103
+ // instead of on a timer, keeping the no-shared-state property that makes the
104
+ // per-request default safe. Independent of the LRU cap so expired-but-cold
105
+ // entries are reclaimed even when the store stays under `maxEntries`.
106
+ function sweepExpiredIfDue(): void {
107
+ if (++writesSinceSweep < EXPIRED_SWEEP_WRITE_INTERVAL) return
108
+ writesSinceSweep = 0
109
+ for (const [key, entry] of store.entries()) {
110
+ if (isExpired(entry)) {
111
+ cleanupExpiredEntry(key, entry)
112
+ }
113
+ }
114
+ }
115
+
53
116
  const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {
54
117
  const entry = store.get(key)
55
118
  if (!entry) return null
@@ -62,6 +125,7 @@ export function createMemoryStrategy(options?: { defaultTtl?: number }): CacheSt
62
125
  return null
63
126
  }
64
127
 
128
+ touchKey(key, entry)
65
129
  return entry.value
66
130
  }
67
131
 
@@ -86,6 +150,8 @@ export function createMemoryStrategy(options?: { defaultTtl?: number }): CacheSt
86
150
 
87
151
  store.set(key, entry)
88
152
  addToTagIndex(key, tags)
153
+ sweepExpiredIfDue()
154
+ evictIfNeeded()
89
155
  }
90
156
 
91
157
  const has = async (key: string): Promise<boolean> => {
package/src/types.ts CHANGED
@@ -102,4 +102,12 @@ export type CacheServiceOptions = {
102
102
  sqlitePath?: string
103
103
  jsonFilePath?: string
104
104
  defaultTtl?: number
105
+ /**
106
+ * Upper bound on retained entries for the in-memory strategy (including the
107
+ * memory fallback used when a native dependency is unavailable). Bounds
108
+ * memory for process-wide instances via LRU eviction. Falls back to
109
+ * `CACHE_MEMORY_MAX_ENTRIES` then a built-in default; a non-positive value
110
+ * disables the cap.
111
+ */
112
+ maxEntries?: number
105
113
  }