@open-mercato/cache 0.6.5-develop.5116.1.f0af9e5080 → 0.6.5-develop.5155.1.148d10a46d

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
@@ -136,6 +136,7 @@ function createTenantAwareWrapper(base) {
136
136
  };
137
137
  const cleanup = base.cleanup ? async () => normalizeDeletionCount(await base.cleanup()) : void 0;
138
138
  const close = base.close ? async () => base.close() : void 0;
139
+ const healthcheck = base.healthcheck ? async () => base.healthcheck() : void 0;
139
140
  return {
140
141
  get,
141
142
  set,
@@ -145,6 +146,7 @@ function createTenantAwareWrapper(base) {
145
146
  clear,
146
147
  keys,
147
148
  stats,
149
+ healthcheck,
148
150
  cleanup,
149
151
  close
150
152
  };
@@ -276,6 +278,7 @@ function withDependencyFallback(strategy, strategyType, defaultTtl) {
276
278
  clear: wrapMethod("clear"),
277
279
  keys: wrapMethod("keys"),
278
280
  stats: wrapMethod("stats"),
281
+ healthcheck: typeof strategy.healthcheck === "function" ? async () => strategy.healthcheck() : void 0,
279
282
  cleanup: typeof strategy.cleanup === "function" ? wrapMethod("cleanup") : void 0,
280
283
  close: typeof strategy.close === "function" ? wrapMethod("close") : void 0
281
284
  };
@@ -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\n return {\n get,\n set,\n has,\n delete: del,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n\n/**\n * Cache service that provides a unified interface to different cache strategies\n * \n * Configuration via environment variables:\n * - CACHE_STRATEGY: 'memory' | 'redis' | 'sqlite' | 'jsonfile' (default: 'memory')\n * - CACHE_TTL: Default TTL in milliseconds (optional)\n * - CACHE_REDIS_URL: Redis connection URL (for redis strategy)\n * - CACHE_SQLITE_PATH: SQLite database file path (for sqlite strategy)\n * - CACHE_JSON_FILE_PATH: JSON file path (for jsonfile strategy)\n * \n * @example\n * const cache = createCacheService({ strategy: 'memory', defaultTtl: 60000 })\n * await cache.set('user:123', { name: 'John' }, { tags: ['users', 'user:123'] })\n * const user = await cache.get('user:123')\n * await cache.deleteByTags(['users']) // Invalidate all user-related cache\n */\nexport function createCacheService(options?: CacheServiceOptions): CacheStrategy {\n const envStrategy = isCacheStrategyName(process.env.CACHE_STRATEGY)\n ? process.env.CACHE_STRATEGY\n : undefined\n const strategyType: CacheStrategyName = options?.strategy ?? envStrategy ?? 'memory'\n\n const envTtl = process.env.CACHE_TTL\n const parsedEnvTtl = envTtl ? Number.parseInt(envTtl, 10) : undefined\n const defaultTtl = options?.defaultTtl ?? (typeof parsedEnvTtl === 'number' && Number.isFinite(parsedEnvTtl) ? parsedEnvTtl : undefined)\n\n const baseStrategy = createStrategyForType(strategyType, options, defaultTtl)\n const resilientStrategy = withDependencyFallback(baseStrategy, strategyType, defaultTtl)\n\n return createTenantAwareWrapper(resilientStrategy)\n}\n\n/**\n * CacheService class wrapper for DI integration\n * Provides the same interface as the functional API but as a class\n */\nexport class CacheService implements CacheStrategy {\n private strategy: CacheStrategy\n\n constructor(options?: CacheServiceOptions) {\n this.strategy = createCacheService(options)\n }\n\n async get(key: string, options?: CacheGetOptions): Promise<CacheValue | null> {\n return this.strategy.get(key, options)\n }\n\n async set(key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> {\n return this.strategy.set(key, value, options)\n }\n\n async has(key: string): Promise<boolean> {\n return this.strategy.has(key)\n }\n\n async delete(key: string): Promise<boolean> {\n return this.strategy.delete(key)\n }\n\n async deleteByTags(tags: string[]): Promise<number> {\n return this.strategy.deleteByTags(tags)\n }\n\n async clear(): Promise<number> {\n return this.strategy.clear()\n }\n\n async keys(pattern?: string): Promise<string[]> {\n return this.strategy.keys(pattern)\n }\n\n async stats(): Promise<{ size: number; expired: number }> {\n return this.strategy.stats()\n }\n\n async cleanup(): Promise<number> {\n if (this.strategy.cleanup) {\n return this.strategy.cleanup()\n }\n return 0\n }\n\n async close(): Promise<void> {\n if (this.strategy.close) {\n return this.strategy.close()\n }\n }\n}\n\nfunction createStrategyForType(strategyType: CacheStrategyName, options?: CacheServiceOptions, defaultTtl?: number): CacheStrategy {\n switch (strategyType) {\n case 'redis':\n return createRedisStrategy(options?.redisUrl, { defaultTtl })\n case 'sqlite':\n return createSqliteStrategy(options?.sqlitePath, { defaultTtl })\n case 'jsonfile':\n return createJsonFileStrategy(options?.jsonFilePath, { defaultTtl })\n case 'memory':\n default:\n return createMemoryStrategy({ defaultTtl })\n }\n}\n\nfunction describeDependencyFailure(error: CacheDependencyUnavailableError): string {\n const originalError = error.originalError\n if (!(originalError instanceof Error)) {\n return `missing dependency: ${error.dependency}`\n }\n\n const message = originalError.message.trim()\n if (message.length === 0) {\n return `missing dependency: ${error.dependency}`\n }\n\n if (message.includes('compiled against a different Node.js version') || message.includes('NODE_MODULE_VERSION')) {\n return `${error.dependency} native module needs rebuild for the current Node.js version`\n }\n\n if (message.includes('Cannot find module')) {\n return `missing dependency: ${error.dependency}`\n }\n\n return `${error.dependency} failed to load`\n}\n\nfunction withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStrategyName, defaultTtl?: number): CacheStrategy {\n if (strategyType === 'memory') return strategy\n\n let activeStrategy = strategy\n let fallbackStrategy: CacheStrategy | null = null\n let warned = false\n\n const ensureFallback = (error: CacheDependencyUnavailableError) => {\n if (!fallbackStrategy) {\n fallbackStrategy = createMemoryStrategy({ defaultTtl })\n }\n if (!warned) {\n const dependencyMessage = error.dependency\n ? ` (${describeDependencyFailure(error)})`\n : ''\n console.warn(`[cache] ${error.strategy} strategy unavailable${dependencyMessage}. Falling back to memory strategy.`)\n warned = true\n }\n activeStrategy = fallbackStrategy\n }\n\n const wrapMethod = <K extends keyof CacheStrategy>(method: K): CacheStrategy[K] => {\n const handler = async (...args: Parameters<NonNullable<CacheStrategy[K]>>) => {\n const fn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n\n try {\n return await fn(...args)\n } catch (error) {\n if (error instanceof CacheDependencyUnavailableError) {\n ensureFallback(error)\n const fallbackFn = activeStrategy[method] as ((...methodArgs: Parameters<NonNullable<CacheStrategy[K]>>) => ReturnType<NonNullable<CacheStrategy[K]>>) | undefined\n if (!fallbackFn) {\n return undefined as Awaited<ReturnType<NonNullable<CacheStrategy[K]>>>\n }\n return fallbackFn(...args)\n }\n throw error\n }\n }\n\n return handler as CacheStrategy[K]\n }\n\n return {\n get: wrapMethod('get'),\n set: wrapMethod('set'),\n has: wrapMethod('has'),\n delete: wrapMethod('delete'),\n deleteByTags: wrapMethod('deleteByTags'),\n clear: wrapMethod('clear'),\n keys: wrapMethod('keys'),\n stats: wrapMethod('stats'),\n cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,\n close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,\n }\n}\n"],
5
- "mappings": "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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 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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",
6
6
  "names": ["metaKey"]
7
7
  }
@@ -299,6 +299,10 @@ function createRedisStrategy(redisUrl, options) {
299
299
  await releaseRedisEntry(connectionUrl, registryEntry);
300
300
  redis = null;
301
301
  };
302
+ const healthcheck = async () => {
303
+ const client = await getRedisClient();
304
+ await client.ping();
305
+ };
302
306
  return {
303
307
  get,
304
308
  set,
@@ -308,6 +312,7 @@ function createRedisStrategy(redisUrl, options) {
308
312
  clear,
309
313
  keys,
310
314
  stats,
315
+ healthcheck,
311
316
  cleanup,
312
317
  close
313
318
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../src/strategies/redis.ts"],
4
- "sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { matchCacheKeyPattern } from '../patterns'\n\ntype RedisPipeline = {\n set(key: string, value: string): RedisPipeline\n setex(key: string, ttlSeconds: number, value: string): RedisPipeline\n sadd(key: string, value: string): RedisPipeline\n srem(key: string, member: string): RedisPipeline\n del(key: string): RedisPipeline\n exec(): Promise<unknown>\n}\n\ntype RedisClient = {\n get(key: string): Promise<string | null>\n set(key: string, value: string): Promise<unknown>\n setex(key: string, ttlSeconds: number, value: string): Promise<unknown>\n del(key: string): Promise<unknown>\n exists(key: string): Promise<number>\n keys(pattern: string): Promise<string[]>\n smembers(key: string): Promise<string[]>\n pipeline(): RedisPipeline\n quit(): Promise<void>\n once?(event: 'end', listener: () => void): void\n}\n\ntype RedisConstructor = new (url?: string) => RedisClient\ntype PossibleRedisModule = unknown\n\ntype RequireFn = (id: string) => unknown\n\n/**\n * Redis cache strategy with tag support\n * Persistent across process restarts, can be shared across multiple instances\n * \n * Uses Redis data structures:\n * - Hash for storing cache entries: cache:{key} -> {value, tags, expiresAt, createdAt}\n * - Sets for tag index: tag:{tag} -> Set of keys\n */\nlet redisModulePromise: Promise<PossibleRedisModule> | null = null\ntype RedisRegistryEntry = { client?: RedisClient; creating?: Promise<RedisClient>; refs: number }\nconst redisRegistry = new Map<string, RedisRegistryEntry>()\n\nfunction resolveRequire(): RequireFn | null {\n const nonWebpack = (globalThis as { __non_webpack_require__?: unknown }).__non_webpack_require__\n if (typeof nonWebpack === 'function') return nonWebpack as RequireFn\n if (typeof require === 'function') return require as RequireFn\n if (typeof module !== 'undefined' && typeof module.require === 'function') {\n return module.require.bind(module)\n }\n try {\n const maybeRequire = Function('return typeof require !== \"undefined\" ? require : undefined')()\n if (typeof maybeRequire === 'function') return maybeRequire as RequireFn\n } catch {\n // ignore\n }\n return null\n}\n\nfunction loadRedisModuleViaRequire(): PossibleRedisModule | null {\n const resolver = resolveRequire()\n if (!resolver) return null\n try {\n return resolver('ioredis') as PossibleRedisModule\n } catch {\n return null\n }\n}\n\nfunction pickRedisConstructor(mod: PossibleRedisModule): RedisConstructor | null {\n const queue: unknown[] = [mod]\n const seen = new Set<unknown>()\n while (queue.length) {\n const current = queue.shift()\n if (!current || seen.has(current)) continue\n seen.add(current)\n if (typeof current === 'function') return current as RedisConstructor\n if (typeof current === 'object') {\n queue.push((current as { default?: unknown }).default)\n queue.push((current as { Redis?: unknown }).Redis)\n queue.push((current as { module?: { exports?: unknown } }).module?.exports)\n queue.push((current as { exports?: unknown }).exports)\n }\n }\n return null\n}\n\nasync function loadRedisModule(): Promise<PossibleRedisModule> {\n if (!redisModulePromise) {\n redisModulePromise = (async () => {\n const required = loadRedisModuleViaRequire() ?? (await import('ioredis'))\n return required as PossibleRedisModule\n })().catch((error) => {\n redisModulePromise = null\n throw new CacheDependencyUnavailableError('redis', 'ioredis', error)\n })\n }\n return redisModulePromise\n}\n\nfunction retainRedisEntry(key: string): RedisRegistryEntry {\n let entry = redisRegistry.get(key)\n if (!entry) {\n entry = { refs: 0 }\n redisRegistry.set(key, entry)\n }\n entry.refs += 1\n return entry\n}\n\nasync function acquireRedisClient(key: string, entry: RedisRegistryEntry): Promise<RedisClient> {\n if (entry.client) return entry.client\n if (entry.creating) return entry.creating\n entry.creating = loadRedisModule()\n .then((mod) => {\n const ctor = pickRedisConstructor(mod)\n if (!ctor) {\n throw new CacheDependencyUnavailableError('redis', 'ioredis', new Error('No usable Redis constructor'))\n }\n const client = new ctor(key)\n entry.client = client\n entry.creating = undefined\n client.once?.('end', () => {\n if (redisRegistry.get(key) === entry && entry.refs === 0) {\n redisRegistry.delete(key)\n } else if (redisRegistry.get(key) === entry) {\n entry.client = undefined\n }\n })\n return client\n })\n .catch((error) => {\n entry.creating = undefined\n throw error\n })\n return entry.creating\n}\n\nasync function releaseRedisEntry(key: string, entry: RedisRegistryEntry): Promise<void> {\n entry.refs = Math.max(0, entry.refs - 1)\n if (entry.refs > 0) return\n redisRegistry.delete(key)\n if (entry.client) {\n try {\n await entry.client.quit()\n } catch {\n // ignore shutdown errors\n } finally {\n entry.client = undefined\n }\n }\n}\n\nexport function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number }): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const keyPrefix = 'cache:'\n const tagPrefix = 'tag:'\n const connectionUrl = redisUrl || getRedisUrlOrThrow('CACHE')\n const registryEntry = retainRedisEntry(connectionUrl)\n let redis: RedisClient | null = registryEntry.client ?? null\n\n async function getRedisClient(): Promise<RedisClient> {\n if (redis) return redis\n\n redis = await acquireRedisClient(connectionUrl, registryEntry)\n return redis\n }\n\n function getCacheKey(key: string): string {\n return `${keyPrefix}${key}`\n }\n\n function getTagKey(tag: string): string {\n return `${tagPrefix}${tag}`\n }\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const data = await client.get(cacheKey)\n\n if (!data) return null\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n\n if (isExpired(entry)) {\n if (options?.returnExpired) {\n return entry.value\n }\n // Clean up expired entry\n await deleteKey(key)\n return null\n }\n\n return entry.value\n } catch {\n // Invalid JSON, remove it\n await client.del(cacheKey)\n return null\n }\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Remove old entry from tag index if it exists\n const oldData = await client.get(cacheKey)\n if (oldData) {\n try {\n const oldEntry: CacheEntry = JSON.parse(oldData)\n // Remove from old tags\n const pipeline = client.pipeline()\n for (const tag of oldEntry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n await pipeline.exec()\n } catch {\n // Ignore parse errors\n }\n }\n\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n\n const entry: CacheEntry = {\n key,\n value,\n tags,\n expiresAt,\n createdAt: Date.now(),\n }\n\n const pipeline = client.pipeline()\n\n // Store the entry\n const serialized = JSON.stringify(entry)\n if (ttl) {\n pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized)\n } else {\n pipeline.set(cacheKey, serialized)\n }\n\n // Add to tag index\n for (const tag of tags) {\n pipeline.sadd(getTagKey(tag), key)\n }\n\n await pipeline.exec()\n }\n\n const has = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const exists = await client.exists(cacheKey)\n\n if (!exists) return false\n\n // Check if expired\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n await deleteKey(key)\n return false\n }\n return true\n } catch {\n return false\n }\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Get entry to remove from tag index\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n const pipeline = client.pipeline()\n\n // Remove from tag index\n for (const tag of entry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n\n // Delete the cache entry\n pipeline.del(cacheKey)\n\n await pipeline.exec()\n return true\n } catch {\n // Just delete the key if we can't parse it\n await client.del(cacheKey)\n return true\n }\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const client = await getRedisClient()\n const keysToDelete = new Set<string>()\n\n // Collect all keys that have any of the specified tags\n for (const tag of tags) {\n const tagKey = getTagKey(tag)\n const keys = await client.smembers(tagKey)\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const client = await getRedisClient()\n \n // Get all cache keys\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n const tagKeys = await client.keys(`${tagPrefix}*`)\n\n if (cacheKeys.length === 0 && tagKeys.length === 0) return 0\n\n const pipeline = client.pipeline()\n for (const key of [...cacheKeys, ...tagKeys]) {\n pipeline.del(key)\n }\n\n await pipeline.exec()\n return cacheKeys.length\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const client = await getRedisClient()\n const searchPattern = pattern \n ? `${keyPrefix}${pattern}` \n : `${keyPrefix}*`\n \n const cacheKeys = await client.keys(searchPattern)\n \n // Remove prefix from keys\n const result = cacheKeys.map((key: string) => key.substring(keyPrefix.length))\n \n if (!pattern) return result\n \n // Apply pattern matching (Redis KEYS command uses glob pattern, but we want our pattern)\n return result.filter((key: string) => matchCacheKeyPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let expired = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n expired++\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n return { size: cacheKeys.length, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let removed = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n const key = cacheKey.substring(keyPrefix.length)\n await deleteKey(key)\n removed++\n }\n } catch {\n // Remove invalid entries\n await client.del(cacheKey)\n removed++\n }\n }\n }\n\n return removed\n }\n\n const close = async (): Promise<void> => {\n await releaseRedisEntry(connectionUrl, registryEntry)\n redis = null\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n cleanup,\n close,\n }\n}\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import type { CacheStrategy, CacheEntry, CacheGetOptions, CacheSetOptions, CacheValue } from '../types'\nimport { CacheDependencyUnavailableError } from '../errors'\nimport { getRedisUrlOrThrow } from '@open-mercato/shared/lib/redis/connection'\nimport { matchCacheKeyPattern } from '../patterns'\n\ntype RedisPipeline = {\n set(key: string, value: string): RedisPipeline\n setex(key: string, ttlSeconds: number, value: string): RedisPipeline\n sadd(key: string, value: string): RedisPipeline\n srem(key: string, member: string): RedisPipeline\n del(key: string): RedisPipeline\n exec(): Promise<unknown>\n}\n\ntype RedisClient = {\n ping(): Promise<string>\n get(key: string): Promise<string | null>\n set(key: string, value: string): Promise<unknown>\n setex(key: string, ttlSeconds: number, value: string): Promise<unknown>\n del(key: string): Promise<unknown>\n exists(key: string): Promise<number>\n keys(pattern: string): Promise<string[]>\n smembers(key: string): Promise<string[]>\n pipeline(): RedisPipeline\n quit(): Promise<void>\n once?(event: 'end', listener: () => void): void\n}\n\ntype RedisConstructor = new (url?: string) => RedisClient\ntype PossibleRedisModule = unknown\n\ntype RequireFn = (id: string) => unknown\n\n/**\n * Redis cache strategy with tag support\n * Persistent across process restarts, can be shared across multiple instances\n * \n * Uses Redis data structures:\n * - Hash for storing cache entries: cache:{key} -> {value, tags, expiresAt, createdAt}\n * - Sets for tag index: tag:{tag} -> Set of keys\n */\nlet redisModulePromise: Promise<PossibleRedisModule> | null = null\ntype RedisRegistryEntry = { client?: RedisClient; creating?: Promise<RedisClient>; refs: number }\nconst redisRegistry = new Map<string, RedisRegistryEntry>()\n\nfunction resolveRequire(): RequireFn | null {\n const nonWebpack = (globalThis as { __non_webpack_require__?: unknown }).__non_webpack_require__\n if (typeof nonWebpack === 'function') return nonWebpack as RequireFn\n if (typeof require === 'function') return require as RequireFn\n if (typeof module !== 'undefined' && typeof module.require === 'function') {\n return module.require.bind(module)\n }\n try {\n const maybeRequire = Function('return typeof require !== \"undefined\" ? require : undefined')()\n if (typeof maybeRequire === 'function') return maybeRequire as RequireFn\n } catch {\n // ignore\n }\n return null\n}\n\nfunction loadRedisModuleViaRequire(): PossibleRedisModule | null {\n const resolver = resolveRequire()\n if (!resolver) return null\n try {\n return resolver('ioredis') as PossibleRedisModule\n } catch {\n return null\n }\n}\n\nfunction pickRedisConstructor(mod: PossibleRedisModule): RedisConstructor | null {\n const queue: unknown[] = [mod]\n const seen = new Set<unknown>()\n while (queue.length) {\n const current = queue.shift()\n if (!current || seen.has(current)) continue\n seen.add(current)\n if (typeof current === 'function') return current as RedisConstructor\n if (typeof current === 'object') {\n queue.push((current as { default?: unknown }).default)\n queue.push((current as { Redis?: unknown }).Redis)\n queue.push((current as { module?: { exports?: unknown } }).module?.exports)\n queue.push((current as { exports?: unknown }).exports)\n }\n }\n return null\n}\n\nasync function loadRedisModule(): Promise<PossibleRedisModule> {\n if (!redisModulePromise) {\n redisModulePromise = (async () => {\n const required = loadRedisModuleViaRequire() ?? (await import('ioredis'))\n return required as PossibleRedisModule\n })().catch((error) => {\n redisModulePromise = null\n throw new CacheDependencyUnavailableError('redis', 'ioredis', error)\n })\n }\n return redisModulePromise\n}\n\nfunction retainRedisEntry(key: string): RedisRegistryEntry {\n let entry = redisRegistry.get(key)\n if (!entry) {\n entry = { refs: 0 }\n redisRegistry.set(key, entry)\n }\n entry.refs += 1\n return entry\n}\n\nasync function acquireRedisClient(key: string, entry: RedisRegistryEntry): Promise<RedisClient> {\n if (entry.client) return entry.client\n if (entry.creating) return entry.creating\n entry.creating = loadRedisModule()\n .then((mod) => {\n const ctor = pickRedisConstructor(mod)\n if (!ctor) {\n throw new CacheDependencyUnavailableError('redis', 'ioredis', new Error('No usable Redis constructor'))\n }\n const client = new ctor(key)\n entry.client = client\n entry.creating = undefined\n client.once?.('end', () => {\n if (redisRegistry.get(key) === entry && entry.refs === 0) {\n redisRegistry.delete(key)\n } else if (redisRegistry.get(key) === entry) {\n entry.client = undefined\n }\n })\n return client\n })\n .catch((error) => {\n entry.creating = undefined\n throw error\n })\n return entry.creating\n}\n\nasync function releaseRedisEntry(key: string, entry: RedisRegistryEntry): Promise<void> {\n entry.refs = Math.max(0, entry.refs - 1)\n if (entry.refs > 0) return\n redisRegistry.delete(key)\n if (entry.client) {\n try {\n await entry.client.quit()\n } catch {\n // ignore shutdown errors\n } finally {\n entry.client = undefined\n }\n }\n}\n\nexport function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?: number }): CacheStrategy {\n const defaultTtl = options?.defaultTtl\n const keyPrefix = 'cache:'\n const tagPrefix = 'tag:'\n const connectionUrl = redisUrl || getRedisUrlOrThrow('CACHE')\n const registryEntry = retainRedisEntry(connectionUrl)\n let redis: RedisClient | null = registryEntry.client ?? null\n\n async function getRedisClient(): Promise<RedisClient> {\n if (redis) return redis\n\n redis = await acquireRedisClient(connectionUrl, registryEntry)\n return redis\n }\n\n function getCacheKey(key: string): string {\n return `${keyPrefix}${key}`\n }\n\n function getTagKey(tag: string): string {\n return `${tagPrefix}${tag}`\n }\n\n function isExpired(entry: CacheEntry): boolean {\n if (entry.expiresAt === null) return false\n return Date.now() > entry.expiresAt\n }\n\n const get = async (key: string, options?: CacheGetOptions): Promise<CacheValue | null> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const data = await client.get(cacheKey)\n\n if (!data) return null\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n\n if (isExpired(entry)) {\n if (options?.returnExpired) {\n return entry.value\n }\n // Clean up expired entry\n await deleteKey(key)\n return null\n }\n\n return entry.value\n } catch {\n // Invalid JSON, remove it\n await client.del(cacheKey)\n return null\n }\n }\n\n const set = async (key: string, value: CacheValue, options?: CacheSetOptions): Promise<void> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Remove old entry from tag index if it exists\n const oldData = await client.get(cacheKey)\n if (oldData) {\n try {\n const oldEntry: CacheEntry = JSON.parse(oldData)\n // Remove from old tags\n const pipeline = client.pipeline()\n for (const tag of oldEntry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n await pipeline.exec()\n } catch {\n // Ignore parse errors\n }\n }\n\n const ttl = options?.ttl ?? defaultTtl\n const tags = options?.tags || []\n const expiresAt = ttl ? Date.now() + ttl : null\n\n const entry: CacheEntry = {\n key,\n value,\n tags,\n expiresAt,\n createdAt: Date.now(),\n }\n\n const pipeline = client.pipeline()\n\n // Store the entry\n const serialized = JSON.stringify(entry)\n if (ttl) {\n pipeline.setex(cacheKey, Math.ceil(ttl / 1000), serialized)\n } else {\n pipeline.set(cacheKey, serialized)\n }\n\n // Add to tag index\n for (const tag of tags) {\n pipeline.sadd(getTagKey(tag), key)\n }\n\n await pipeline.exec()\n }\n\n const has = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n const exists = await client.exists(cacheKey)\n\n if (!exists) return false\n\n // Check if expired\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n await deleteKey(key)\n return false\n }\n return true\n } catch {\n return false\n }\n }\n\n const deleteKey = async (key: string): Promise<boolean> => {\n const client = await getRedisClient()\n const cacheKey = getCacheKey(key)\n\n // Get entry to remove from tag index\n const data = await client.get(cacheKey)\n if (!data) return false\n\n try {\n const entry: CacheEntry = JSON.parse(data)\n const pipeline = client.pipeline()\n\n // Remove from tag index\n for (const tag of entry.tags) {\n pipeline.srem(getTagKey(tag), key)\n }\n\n // Delete the cache entry\n pipeline.del(cacheKey)\n\n await pipeline.exec()\n return true\n } catch {\n // Just delete the key if we can't parse it\n await client.del(cacheKey)\n return true\n }\n }\n\n const deleteByTags = async (tags: string[]): Promise<number> => {\n const client = await getRedisClient()\n const keysToDelete = new Set<string>()\n\n // Collect all keys that have any of the specified tags\n for (const tag of tags) {\n const tagKey = getTagKey(tag)\n const keys = await client.smembers(tagKey)\n for (const key of keys) {\n keysToDelete.add(key)\n }\n }\n\n // Delete all collected keys\n let deleted = 0\n for (const key of keysToDelete) {\n const success = await deleteKey(key)\n if (success) deleted++\n }\n\n return deleted\n }\n\n const clear = async (): Promise<number> => {\n const client = await getRedisClient()\n \n // Get all cache keys\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n const tagKeys = await client.keys(`${tagPrefix}*`)\n\n if (cacheKeys.length === 0 && tagKeys.length === 0) return 0\n\n const pipeline = client.pipeline()\n for (const key of [...cacheKeys, ...tagKeys]) {\n pipeline.del(key)\n }\n\n await pipeline.exec()\n return cacheKeys.length\n }\n\n const keys = async (pattern?: string): Promise<string[]> => {\n const client = await getRedisClient()\n const searchPattern = pattern \n ? `${keyPrefix}${pattern}` \n : `${keyPrefix}*`\n \n const cacheKeys = await client.keys(searchPattern)\n \n // Remove prefix from keys\n const result = cacheKeys.map((key: string) => key.substring(keyPrefix.length))\n \n if (!pattern) return result\n \n // Apply pattern matching (Redis KEYS command uses glob pattern, but we want our pattern)\n return result.filter((key: string) => matchCacheKeyPattern(key, pattern))\n }\n\n const stats = async (): Promise<{ size: number; expired: number }> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let expired = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n expired++\n }\n } catch {\n // Ignore parse errors\n }\n }\n }\n\n return { size: cacheKeys.length, expired }\n }\n\n const cleanup = async (): Promise<number> => {\n const client = await getRedisClient()\n const cacheKeys = await client.keys(`${keyPrefix}*`)\n \n let removed = 0\n for (const cacheKey of cacheKeys) {\n const data = await client.get(cacheKey)\n if (data) {\n try {\n const entry: CacheEntry = JSON.parse(data)\n if (isExpired(entry)) {\n const key = cacheKey.substring(keyPrefix.length)\n await deleteKey(key)\n removed++\n }\n } catch {\n // Remove invalid entries\n await client.del(cacheKey)\n removed++\n }\n }\n }\n\n return removed\n }\n\n const close = async (): Promise<void> => {\n await releaseRedisEntry(connectionUrl, registryEntry)\n redis = null\n }\n\n const healthcheck = async (): Promise<void> => {\n const client = await getRedisClient()\n await client.ping()\n }\n\n return {\n get,\n set,\n has,\n delete: deleteKey,\n deleteByTags,\n clear,\n keys,\n stats,\n healthcheck,\n cleanup,\n close,\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["options", "pipeline", "keys"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/cache",
3
- "version": "0.6.5-develop.5116.1.f0af9e5080",
3
+ "version": "0.6.5-develop.5155.1.148d10a46d",
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.1",
61
+ "@types/node": "^25.9.2",
62
62
  "jest": "^30.4.2",
63
63
  "ts-jest": "^29.4.11"
64
64
  },
package/src/service.ts CHANGED
@@ -180,6 +180,9 @@ function createTenantAwareWrapper(base: CacheStrategy): CacheStrategy {
180
180
  const close = base.close
181
181
  ? async () => base.close!()
182
182
  : undefined
183
+ const healthcheck = base.healthcheck
184
+ ? async () => base.healthcheck!()
185
+ : undefined
183
186
 
184
187
  return {
185
188
  get,
@@ -190,6 +193,7 @@ function createTenantAwareWrapper(base: CacheStrategy): CacheStrategy {
190
193
  clear,
191
194
  keys,
192
195
  stats,
196
+ healthcheck,
193
197
  cleanup,
194
198
  close,
195
199
  }
@@ -375,6 +379,9 @@ function withDependencyFallback(strategy: CacheStrategy, strategyType: CacheStra
375
379
  clear: wrapMethod('clear'),
376
380
  keys: wrapMethod('keys'),
377
381
  stats: wrapMethod('stats'),
382
+ healthcheck: typeof strategy.healthcheck === 'function'
383
+ ? async () => strategy.healthcheck!()
384
+ : undefined,
378
385
  cleanup: typeof strategy.cleanup === 'function' ? wrapMethod('cleanup') : undefined,
379
386
  close: typeof strategy.close === 'function' ? wrapMethod('close') : undefined,
380
387
  }
@@ -13,6 +13,7 @@ type RedisPipeline = {
13
13
  }
14
14
 
15
15
  type RedisClient = {
16
+ ping(): Promise<string>
16
17
  get(key: string): Promise<string | null>
17
18
  set(key: string, value: string): Promise<unknown>
18
19
  setex(key: string, ttlSeconds: number, value: string): Promise<unknown>
@@ -420,6 +421,11 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
420
421
  redis = null
421
422
  }
422
423
 
424
+ const healthcheck = async (): Promise<void> => {
425
+ const client = await getRedisClient()
426
+ await client.ping()
427
+ }
428
+
423
429
  return {
424
430
  get,
425
431
  set,
@@ -429,6 +435,7 @@ export function createRedisStrategy(redisUrl?: string, options?: { defaultTtl?:
429
435
  clear,
430
436
  keys,
431
437
  stats,
438
+ healthcheck,
432
439
  cleanup,
433
440
  close,
434
441
  }
package/src/types.ts CHANGED
@@ -79,6 +79,11 @@ export type CacheStrategy = {
79
79
  expired: number // Number of expired entries
80
80
  }>
81
81
 
82
+ /**
83
+ * Verify the configured backend is reachable without activating a fallback.
84
+ */
85
+ healthcheck?(): Promise<void>
86
+
82
87
  /**
83
88
  * Clean up expired entries (optional, some strategies may auto-cleanup)
84
89
  * @returns Number of entries removed