@happyvertical/cache 0.80.0 → 0.80.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/shared/types.ts","../src/shared/utils.ts","../src/index.ts"],"sourcesContent":["/**\n * Core types and interfaces for the Cache library\n */\n\n/**\n * Standardized cache entry structure (internal use)\n */\nexport interface CacheEntry<T = any> {\n /**\n * The cached value\n */\n value: T;\n\n /**\n * When this entry was created (Unix timestamp in milliseconds)\n */\n createdAt: number;\n\n /**\n * When this entry will expire (Unix timestamp in milliseconds)\n * undefined means no expiration\n */\n expiresAt?: number;\n\n /**\n * Size in bytes (for memory/disk management)\n */\n size: number;\n\n /**\n * Number of times this entry has been accessed\n */\n hits: number;\n\n /**\n * Additional metadata\n */\n metadata?: {\n compressed?: boolean;\n serialized?: boolean;\n namespace?: string;\n };\n}\n\n/**\n * Cache statistics\n */\nexport interface CacheStats {\n /**\n * Total number of cached entries\n */\n entries: number;\n\n /**\n * Total size in bytes\n */\n totalSize: number;\n\n /**\n * Cache hit count\n */\n hits: number;\n\n /**\n * Cache miss count\n */\n misses: number;\n\n /**\n * Hit rate (hits / (hits + misses))\n */\n hitRate: number;\n\n /**\n * Number of evictions (entries removed due to size/TTL)\n */\n evictions: number;\n\n /**\n * Backend-specific statistics\n */\n backend?: {\n type: 'memory' | 'file' | 'redis' | 's3';\n [key: string]: any;\n };\n}\n\n/**\n * Cache provider interface - all providers must implement this\n */\nexport interface CacheProvider {\n /**\n * Retrieves a value from the cache by key\n * @param key - The cache key\n * @returns Promise resolving to the cached value, or undefined if not found or expired\n */\n get<T = any>(key: string): Promise<T | undefined>;\n\n /**\n * Stores a value in the cache with an optional time-to-live\n * @param key - The cache key\n * @param value - The value to cache\n * @param ttl - Optional time-to-live in seconds\n * @returns Promise resolving when the value is cached\n */\n set<T = any>(key: string, value: T, ttl?: number): Promise<void>;\n\n /**\n * Checks if a key exists in the cache and is not expired\n * @param key - The cache key\n * @returns Promise resolving to true if the key exists and is valid\n */\n has(key: string): Promise<boolean>;\n\n /**\n * Removes a value from the cache\n * @param key - The cache key\n * @returns Promise resolving to true if the key was deleted, false if it didn't exist\n */\n delete(key: string): Promise<boolean>;\n\n /**\n * Clears all entries from the cache, or all entries in a namespace if specified\n * @param namespace - Optional namespace to clear\n * @returns Promise resolving when the cache is cleared\n */\n clear(namespace?: string): Promise<void>;\n\n /**\n * Gets all keys in the cache, optionally filtered by a pattern\n * @param pattern - Optional glob-style pattern to filter keys\n * @returns Promise resolving to an array of matching keys\n */\n keys(pattern?: string): Promise<string[]>;\n\n /**\n * Retrieves multiple values from the cache\n * @param keys - An array of cache keys\n * @returns Promise resolving to a map of key-value pairs\n */\n getMany<T = any>(keys: string[]): Promise<Map<string, T>>;\n\n /**\n * Stores multiple key-value pairs in the cache\n * @param entries - An array of {key, value, ttl?} objects\n * @returns Promise resolving when all values are cached\n */\n setMany<T = any>(\n entries: Array<{ key: string; value: T; ttl?: number }>,\n ): Promise<void>;\n\n /**\n * Removes multiple values from the cache\n * @param keys - An array of cache keys\n * @returns Promise resolving to the number of keys deleted\n */\n deleteMany(keys: string[]): Promise<number>;\n\n /**\n * Gets cache statistics\n * @returns Promise resolving to cache statistics\n */\n getStats(): Promise<CacheStats>;\n\n /**\n * Updates the TTL for an existing cache entry\n * @param key - The cache key\n * @param ttl - New time-to-live in seconds\n * @returns Promise resolving to true if TTL was updated, false if key doesn't exist\n */\n touch(key: string, ttl: number): Promise<boolean>;\n\n /**\n * Closes the cache connection/cleanup resources\n * @returns Promise resolving when cleanup is complete\n */\n close(): Promise<void>;\n}\n\n/**\n * Cache adapter interface (structurally identical to CacheProvider)\n */\nexport interface CacheAdapter extends CacheProvider {}\n\n/**\n * Memory cache options\n */\nexport interface MemoryOptions {\n provider: 'memory';\n namespace?: string;\n /** Default time-to-live in seconds for entries without an explicit TTL */\n defaultTTL?: number;\n /** Maximum total cache size in bytes (default: 100 MB) */\n maxSize?: number;\n /** Maximum number of entries (default: 10 000) */\n maxEntries?: number;\n evictionPolicy?: 'lru' | 'lfu' | 'fifo';\n /** Interval in milliseconds between expired-entry sweeps (default: 60 000) */\n checkPeriod?: number;\n}\n\n/**\n * File cache options\n */\nexport interface FileOptions {\n provider: 'file';\n /** Directory where cache files are stored (required) */\n cacheDir: string;\n namespace?: string;\n /** Default time-to-live in seconds for entries without an explicit TTL */\n defaultTTL?: number;\n /** Maximum total cache size in bytes (default: 500 MB) */\n maxSize?: number;\n /** Enable gzip compression for stored files (default: false) */\n compression?: boolean;\n /** File suffix for cache files (default: '.cache') */\n fileExtension?: string;\n /** Interval in milliseconds between expired-file cleanup sweeps (default: 300 000) */\n checkPeriod?: number;\n}\n\n/**\n * Redis cache options\n */\nexport interface RedisOptions {\n provider: 'redis';\n /** Redis server hostname (default: 'localhost') */\n host?: string;\n /** Redis server port (default: 6379) */\n port?: number;\n password?: string;\n /** Redis database index 0-15 (default: 0) */\n db?: number;\n namespace?: string;\n /** Alternative to namespace — used as the key prefix */\n keyPrefix?: string;\n /** Default time-to-live in seconds for entries without an explicit TTL */\n defaultTTL?: number;\n /** Enable gzip compression for values exceeding compressionThreshold (default: false) */\n enableCompression?: boolean;\n /** Minimum value size in bytes before compression applies (default: 1024) */\n compressionThreshold?: number;\n /** Socket connect timeout in milliseconds (default: 5000) */\n connectTimeout?: number;\n /** Per-command timeout in milliseconds */\n commandTimeout?: number;\n retryStrategy?: (times: number) => number | null;\n}\n\n/**\n * S3 cache options\n * Use this for CI environments where cache needs to persist between runs\n */\nexport interface S3Options {\n provider: 's3';\n /** S3 bucket name (required) */\n bucket: string;\n /** Key prefix for cache files (default: 'cache/') */\n prefix?: string;\n /** AWS region (default: from AWS_REGION env var or 'us-east-1') */\n region?: string;\n /** Optional namespace for key organization */\n namespace?: string;\n /** Default TTL in seconds */\n defaultTTL?: number;\n /** Enable gzip compression (default: true) */\n compression?: boolean;\n /** Only compress if value exceeds this size in bytes (default: 1024) */\n compressionThreshold?: number;\n}\n\n/**\n * Discriminated union of all cache adapter options\n */\nexport type CacheAdapterOptions =\n | MemoryOptions\n | FileOptions\n | RedisOptions\n | S3Options;\n\n/**\n * Base cache error class\n */\nexport class CacheError extends Error {\n constructor(\n message: string,\n public code: string,\n public provider: string,\n ) {\n super(message);\n this.name = 'CacheError';\n }\n}\n\n/**\n * Invalid cache key error\n */\nexport class CacheKeyError extends CacheError {\n constructor(\n public key: string,\n provider: string,\n ) {\n super(`Invalid cache key: ${key}`, 'INVALID_KEY', provider);\n this.name = 'CacheKeyError';\n }\n}\n\n/**\n * Cache connection error\n */\nexport class CacheConnectionError extends CacheError {\n constructor(message: string, provider: string) {\n super(message, 'CONNECTION_ERROR', provider);\n this.name = 'CacheConnectionError';\n }\n}\n\n/**\n * Cache size limit exceeded error\n */\nexport class CacheSizeError extends CacheError {\n constructor(message: string, provider: string) {\n super(message, 'SIZE_EXCEEDED', provider);\n this.name = 'CacheSizeError';\n }\n}\n\n/**\n * Cache serialization error\n */\nexport class CacheSerializationError extends CacheError {\n constructor(message: string, provider: string) {\n super(message, 'SERIALIZATION_ERROR', provider);\n this.name = 'CacheSerializationError';\n }\n}\n","/**\n * Utility functions for cache operations\n */\n\n/**\n * Validates a cache key\n * @param key - The cache key to validate\n * @returns True if the key is valid\n */\nexport function isValidKey(key: string): boolean {\n return typeof key === 'string' && key.length > 0 && key.length <= 250;\n}\n\n/**\n * Calculates the size of a value in bytes (approximate)\n * @param value - The value to measure\n * @returns Size in bytes\n */\nexport function calculateSize(value: any): number {\n try {\n const json = JSON.stringify(value);\n return new Blob([json]).size;\n } catch {\n // Fallback to rough estimate if stringify fails\n return 0;\n }\n}\n\n/**\n * Checks if a pattern matches a string (glob-style)\n * @param pattern - The glob pattern (supports * wildcard)\n * @param str - The string to test\n * @returns True if the pattern matches\n */\nexport function matchesPattern(pattern: string, str: string): boolean {\n // Convert glob pattern to regex\n const regexPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&') // Escape regex special chars\n .replace(/\\*/g, '.*') // Convert * to .*\n .replace(/\\?/g, '.'); // Convert ? to .\n\n const regex = new RegExp(`^${regexPattern}$`);\n return regex.test(str);\n}\n\n/**\n * Formats a namespace and key into a full key\n * @param namespace - Optional namespace\n * @param key - The cache key\n * @returns Formatted key with namespace prefix if provided\n */\nexport function formatKey(namespace: string | undefined, key: string): string {\n return namespace ? `${namespace}:${key}` : key;\n}\n\n/**\n * Extracts the original key from a namespaced key\n * @param namespace - Optional namespace\n * @param fullKey - The full key with namespace\n * @returns Original key without namespace\n */\nexport function extractKey(\n namespace: string | undefined,\n fullKey: string,\n): string {\n if (!namespace) {\n return fullKey;\n }\n const prefix = `${namespace}:`;\n return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;\n}\n\n/**\n * Checks if an entry has expired\n * @param expiresAt - Expiration timestamp (undefined means no expiration)\n * @returns True if the entry has expired\n */\nexport function isExpired(expiresAt: number | undefined): boolean {\n if (expiresAt === undefined) {\n return false;\n }\n return Date.now() >= expiresAt;\n}\n\n/**\n * Calculates expiration timestamp from TTL\n * @param ttl - Time-to-live in seconds (undefined means no expiration)\n * @returns Expiration timestamp in milliseconds, or undefined\n */\nexport function calculateExpiration(\n ttl: number | undefined,\n): number | undefined {\n if (ttl === undefined || ttl <= 0) {\n return undefined;\n }\n return Date.now() + ttl * 1000;\n}\n\n/**\n * Serializes a value to JSON string\n * @param value - The value to serialize\n * @returns JSON string\n * @throws Error if serialization fails\n */\nexport function serialize(value: any): string {\n try {\n return JSON.stringify(value);\n } catch (error) {\n throw new Error(\n `Failed to serialize value: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n}\n\n/**\n * Deserializes a JSON string to a value\n * @param json - The JSON string\n * @returns Deserialized value\n * @throws Error if deserialization fails\n */\nexport function deserialize<T = any>(json: string): T {\n try {\n return JSON.parse(json);\n } catch (error) {\n throw new Error(\n `Failed to deserialize value: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n}\n","/**\n * Cache package entry point\n * Provides standardized caching interface\n */\n\nimport { loadEnvConfig } from '@happyvertical/utils';\nimport type {\n CacheAdapter,\n CacheAdapterOptions,\n FileOptions,\n MemoryOptions,\n RedisOptions,\n S3Options,\n} from './shared/types';\n\n// Export all types\nexport * from './shared/types';\nexport * from './shared/utils';\n\n/**\n * Type guard for Memory cache options\n */\nfunction isMemoryOptions(\n options: CacheAdapterOptions,\n): options is MemoryOptions {\n return options.provider === 'memory';\n}\n\n/**\n * Type guard for File cache options\n */\nfunction isFileOptions(options: CacheAdapterOptions): options is FileOptions {\n return options.provider === 'file';\n}\n\n/**\n * Type guard for Redis cache options\n */\nfunction isRedisOptions(options: CacheAdapterOptions): options is RedisOptions {\n return options.provider === 'redis';\n}\n\n/**\n * Type guard for S3 cache options\n */\nfunction isS3Options(options: CacheAdapterOptions): options is S3Options {\n return options.provider === 's3';\n}\n\n/**\n * Factory function to create a cache adapter instance\n *\n * Supports environment variable configuration using the HAVE_CACHE_* pattern:\n * - HAVE_CACHE_PROVIDER → provider ('memory'|'file'|'redis'|'s3')\n * - HAVE_CACHE_NAMESPACE → namespace (string)\n * - HAVE_CACHE_DEFAULT_TTL → defaultTTL (number: seconds)\n * - HAVE_CACHE_MAX_SIZE → maxSize (number: bytes)\n * - HAVE_CACHE_MAX_ENTRIES → maxEntries (number, memory only)\n * - HAVE_CACHE_EVICTION_POLICY → evictionPolicy ('lru'|'lfu'|'fifo', memory only)\n * - HAVE_CACHE_CACHE_DIR → cacheDir (string, file only)\n * - HAVE_CACHE_COMPRESSION → compression (boolean, file/s3)\n * - HAVE_CACHE_HOST → host (string, redis only)\n * - HAVE_CACHE_PORT → port (number, redis only)\n * - HAVE_CACHE_BUCKET → bucket (string, s3 only)\n * - HAVE_CACHE_PREFIX → prefix (string, s3 only)\n * - HAVE_CACHE_REGION → region (string, s3 only)\n *\n * User-provided options always take precedence over environment variables.\n *\n * @param options - Configuration options for the cache provider\n * @returns Promise resolving to a cache adapter that implements CacheAdapter\n *\n * @example\n * ```typescript\n * // Create memory cache with explicit options\n * const memoryCache = await getCache({\n * provider: 'memory',\n * maxSize: 100 * 1024 * 1024,\n * evictionPolicy: 'lru'\n * });\n *\n * // Create memory cache with environment variables\n * // HAVE_CACHE_PROVIDER=memory\n * // HAVE_CACHE_MAX_SIZE=104857600\n * // HAVE_CACHE_EVICTION_POLICY=lru\n * const envCache = await getCache({ provider: 'memory' });\n *\n * // Create file cache\n * const fileCache = await getCache({\n * provider: 'file',\n * cacheDir: './cache',\n * compression: true\n * });\n *\n * // Create Redis cache\n * const redisCache = await getCache({\n * provider: 'redis',\n * host: 'localhost',\n * port: 6379\n * });\n *\n * // Create S3 cache (for CI persistence)\n * const s3Cache = await getCache({\n * provider: 's3',\n * bucket: 'my-cache-bucket',\n * prefix: 'cache/',\n * region: 'us-east-1'\n * });\n *\n * // Use the cache\n * await memoryCache.set('user:123', { name: 'John' });\n * const user = await memoryCache.get('user:123');\n * ```\n */\nexport async function getCache(\n options: CacheAdapterOptions,\n): Promise<CacheAdapter> {\n // Load configuration from environment variables, merging with user options\n // User options always take precedence\n // Use 'any' to work around TypeScript's discriminated union type checking\n const config = loadEnvConfig(options as any, {\n packageName: 'cache',\n schema: {\n provider: 'string',\n namespace: 'string',\n defaultTTL: 'number',\n maxSize: 'number',\n maxEntries: 'number',\n evictionPolicy: 'string',\n checkPeriod: 'number',\n cacheDir: 'string',\n compression: 'boolean',\n fileExtension: 'string',\n host: 'string',\n port: 'number',\n password: 'string',\n db: 'number',\n keyPrefix: 'string',\n enableCompression: 'boolean',\n compressionThreshold: 'number',\n connectTimeout: 'number',\n commandTimeout: 'number',\n // S3 options\n bucket: 'string',\n prefix: 'string',\n region: 'string',\n } as any,\n allowUnknown: false,\n }) as CacheAdapterOptions;\n\n if (isMemoryOptions(config)) {\n const { MemoryProvider } = await import('./providers/memory.js');\n return new MemoryProvider(config);\n }\n\n if (isFileOptions(config)) {\n const { FileProvider } = await import('./providers/file.js');\n return new FileProvider(config);\n }\n\n if (isRedisOptions(config)) {\n const { RedisProvider } = await import('./providers/redis.js');\n return new RedisProvider(config);\n }\n\n if (isS3Options(config)) {\n const { S3Provider } = await import('./providers/s3.js');\n return new S3Provider(config);\n }\n\n // This should never happen due to TypeScript's discriminated union\n throw new Error(`Unsupported provider: ${(config as any).provider}`);\n}\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../src/shared/types.ts","../src/shared/utils.ts","../src/index.ts"],"sourcesContent":["/**\n * Core types and interfaces for the Cache library\n */\n\n/**\n * Standardized cache entry structure (internal use)\n */\nexport interface CacheEntry<T = any> {\n /**\n * The cached value\n */\n value: T;\n\n /**\n * When this entry was created (Unix timestamp in milliseconds)\n */\n createdAt: number;\n\n /**\n * When this entry will expire (Unix timestamp in milliseconds)\n * undefined means no expiration\n */\n expiresAt?: number;\n\n /**\n * Size in bytes (for memory/disk management)\n */\n size: number;\n\n /**\n * Number of times this entry has been accessed\n */\n hits: number;\n\n /**\n * Additional metadata\n */\n metadata?: {\n compressed?: boolean;\n serialized?: boolean;\n namespace?: string;\n };\n}\n\n/**\n * Cache statistics\n */\nexport interface CacheStats {\n /**\n * Total number of cached entries\n */\n entries: number;\n\n /**\n * Total size in bytes\n */\n totalSize: number;\n\n /**\n * Cache hit count\n */\n hits: number;\n\n /**\n * Cache miss count\n */\n misses: number;\n\n /**\n * Hit rate (hits / (hits + misses))\n */\n hitRate: number;\n\n /**\n * Number of evictions (entries removed due to size/TTL)\n */\n evictions: number;\n\n /**\n * Backend-specific statistics\n */\n backend?: {\n type: 'memory' | 'file' | 'redis' | 's3';\n [key: string]: any;\n };\n}\n\n/**\n * Cache provider interface - all providers must implement this\n */\nexport interface CacheProvider {\n /**\n * Retrieves a value from the cache by key\n * @param key - The cache key\n * @returns Promise resolving to the cached value, or undefined if not found or expired\n */\n get<T = any>(key: string): Promise<T | undefined>;\n\n /**\n * Stores a value in the cache with an optional time-to-live\n * @param key - The cache key\n * @param value - The value to cache\n * @param ttl - Optional time-to-live in seconds\n * @returns Promise resolving when the value is cached\n */\n set<T = any>(key: string, value: T, ttl?: number): Promise<void>;\n\n /**\n * Checks if a key exists in the cache and is not expired\n * @param key - The cache key\n * @returns Promise resolving to true if the key exists and is valid\n */\n has(key: string): Promise<boolean>;\n\n /**\n * Removes a value from the cache\n * @param key - The cache key\n * @returns Promise resolving to true if the key was deleted, false if it didn't exist\n */\n delete(key: string): Promise<boolean>;\n\n /**\n * Clears all entries from the cache, or all entries in a namespace if specified\n * @param namespace - Optional namespace to clear\n * @returns Promise resolving when the cache is cleared\n */\n clear(namespace?: string): Promise<void>;\n\n /**\n * Gets all keys in the cache, optionally filtered by a pattern\n * @param pattern - Optional glob-style pattern to filter keys\n * @returns Promise resolving to an array of matching keys\n */\n keys(pattern?: string): Promise<string[]>;\n\n /**\n * Retrieves multiple values from the cache\n * @param keys - An array of cache keys\n * @returns Promise resolving to a map of key-value pairs\n */\n getMany<T = any>(keys: string[]): Promise<Map<string, T>>;\n\n /**\n * Stores multiple key-value pairs in the cache\n * @param entries - An array of {key, value, ttl?} objects\n * @returns Promise resolving when all values are cached\n */\n setMany<T = any>(\n entries: Array<{ key: string; value: T; ttl?: number }>,\n ): Promise<void>;\n\n /**\n * Removes multiple values from the cache\n * @param keys - An array of cache keys\n * @returns Promise resolving to the number of keys deleted\n */\n deleteMany(keys: string[]): Promise<number>;\n\n /**\n * Gets cache statistics\n * @returns Promise resolving to cache statistics\n */\n getStats(): Promise<CacheStats>;\n\n /**\n * Updates the TTL for an existing cache entry\n * @param key - The cache key\n * @param ttl - New time-to-live in seconds\n * @returns Promise resolving to true if TTL was updated, false if key doesn't exist\n */\n touch(key: string, ttl: number): Promise<boolean>;\n\n /**\n * Closes the cache connection/cleanup resources\n * @returns Promise resolving when cleanup is complete\n */\n close(): Promise<void>;\n}\n\n/**\n * Cache adapter interface (structurally identical to CacheProvider)\n */\nexport interface CacheAdapter extends CacheProvider {}\n\n/**\n * Memory cache options\n */\nexport interface MemoryOptions {\n provider: 'memory';\n namespace?: string;\n /** Default time-to-live in seconds for entries without an explicit TTL */\n defaultTTL?: number;\n /** Maximum total cache size in bytes (default: 100 MB) */\n maxSize?: number;\n /** Maximum number of entries (default: 10 000) */\n maxEntries?: number;\n evictionPolicy?: 'lru' | 'lfu' | 'fifo';\n /** Interval in milliseconds between expired-entry sweeps (default: 60 000) */\n checkPeriod?: number;\n}\n\n/**\n * File cache options\n */\nexport interface FileOptions {\n provider: 'file';\n /** Directory where cache files are stored (required) */\n cacheDir: string;\n namespace?: string;\n /** Default time-to-live in seconds for entries without an explicit TTL */\n defaultTTL?: number;\n /** Maximum total cache size in bytes (default: 500 MB) */\n maxSize?: number;\n /** Enable gzip compression for stored files (default: false) */\n compression?: boolean;\n /** File suffix for cache files (default: '.cache') */\n fileExtension?: string;\n /** Interval in milliseconds between expired-file cleanup sweeps (default: 300 000) */\n checkPeriod?: number;\n}\n\n/**\n * Redis cache options\n */\nexport interface RedisOptions {\n provider: 'redis';\n /** Redis server hostname (default: 'localhost') */\n host?: string;\n /** Redis server port (default: 6379) */\n port?: number;\n password?: string;\n /** Redis database index 0-15 (default: 0) */\n db?: number;\n namespace?: string;\n /** Alternative to namespace — used as the key prefix */\n keyPrefix?: string;\n /** Default time-to-live in seconds for entries without an explicit TTL */\n defaultTTL?: number;\n /** Enable gzip compression for values exceeding compressionThreshold (default: false) */\n enableCompression?: boolean;\n /** Minimum value size in bytes before compression applies (default: 1024) */\n compressionThreshold?: number;\n /** Socket connect timeout in milliseconds (default: 5000) */\n connectTimeout?: number;\n /** Per-command timeout in milliseconds */\n commandTimeout?: number;\n retryStrategy?: (times: number) => number | null;\n}\n\n/**\n * S3 cache options\n * Use this for CI environments where cache needs to persist between runs\n */\nexport interface S3Options {\n provider: 's3';\n /** S3 bucket name (required) */\n bucket: string;\n /** Key prefix for cache files (default: 'cache/') */\n prefix?: string;\n /** AWS region (default: from AWS_REGION env var or 'us-east-1') */\n region?: string;\n /** Optional namespace for key organization */\n namespace?: string;\n /** Default TTL in seconds */\n defaultTTL?: number;\n /** Enable gzip compression (default: true) */\n compression?: boolean;\n /** Only compress if value exceeds this size in bytes (default: 1024) */\n compressionThreshold?: number;\n}\n\n/**\n * Discriminated union of all cache adapter options\n */\nexport type CacheAdapterOptions =\n | MemoryOptions\n | FileOptions\n | RedisOptions\n | S3Options;\n\n/**\n * Base cache error class\n */\nexport class CacheError extends Error {\n constructor(\n message: string,\n public code: string,\n public provider: string,\n ) {\n super(message);\n this.name = 'CacheError';\n }\n}\n\n/**\n * Invalid cache key error\n */\nexport class CacheKeyError extends CacheError {\n constructor(\n public key: string,\n provider: string,\n ) {\n super(`Invalid cache key: ${key}`, 'INVALID_KEY', provider);\n this.name = 'CacheKeyError';\n }\n}\n\n/**\n * Cache connection error\n */\nexport class CacheConnectionError extends CacheError {\n constructor(message: string, provider: string) {\n super(message, 'CONNECTION_ERROR', provider);\n this.name = 'CacheConnectionError';\n }\n}\n\n/**\n * Cache size limit exceeded error\n */\nexport class CacheSizeError extends CacheError {\n constructor(message: string, provider: string) {\n super(message, 'SIZE_EXCEEDED', provider);\n this.name = 'CacheSizeError';\n }\n}\n\n/**\n * Cache serialization error\n */\nexport class CacheSerializationError extends CacheError {\n constructor(message: string, provider: string) {\n super(message, 'SERIALIZATION_ERROR', provider);\n this.name = 'CacheSerializationError';\n }\n}\n","/**\n * Utility functions for cache operations\n */\n\n/**\n * Validates a cache key\n * @param key - The cache key to validate\n * @returns True if the key is valid\n */\nexport function isValidKey(key: string): boolean {\n return typeof key === 'string' && key.length > 0 && key.length <= 250;\n}\n\n/**\n * Calculates the size of a value in bytes (approximate)\n * @param value - The value to measure\n * @returns Size in bytes\n */\nexport function calculateSize(value: any): number {\n try {\n const json = JSON.stringify(value);\n return new Blob([json]).size;\n } catch {\n // Fallback to rough estimate if stringify fails\n return 0;\n }\n}\n\n/**\n * Checks if a pattern matches a string (glob-style)\n * @param pattern - The glob pattern (supports * wildcard)\n * @param str - The string to test\n * @returns True if the pattern matches\n */\nexport function matchesPattern(pattern: string, str: string): boolean {\n // Convert glob pattern to regex\n const regexPattern = pattern\n .replace(/[.+^${}()|[\\]\\\\]/g, '\\\\$&') // Escape regex special chars\n .replace(/\\*/g, '.*') // Convert * to .*\n .replace(/\\?/g, '.'); // Convert ? to .\n\n const regex = new RegExp(`^${regexPattern}$`);\n return regex.test(str);\n}\n\n/**\n * Formats a namespace and key into a full key\n * @param namespace - Optional namespace\n * @param key - The cache key\n * @returns Formatted key with namespace prefix if provided\n */\nexport function formatKey(namespace: string | undefined, key: string): string {\n return namespace ? `${namespace}:${key}` : key;\n}\n\n/**\n * Extracts the original key from a namespaced key\n * @param namespace - Optional namespace\n * @param fullKey - The full key with namespace\n * @returns Original key without namespace\n */\nexport function extractKey(\n namespace: string | undefined,\n fullKey: string,\n): string {\n if (!namespace) {\n return fullKey;\n }\n const prefix = `${namespace}:`;\n return fullKey.startsWith(prefix) ? fullKey.slice(prefix.length) : fullKey;\n}\n\n/**\n * Checks if an entry has expired\n * @param expiresAt - Expiration timestamp (undefined means no expiration)\n * @returns True if the entry has expired\n */\nexport function isExpired(expiresAt: number | undefined): boolean {\n if (expiresAt === undefined) {\n return false;\n }\n return Date.now() >= expiresAt;\n}\n\n/**\n * Calculates expiration timestamp from TTL\n * @param ttl - Time-to-live in seconds (undefined means no expiration)\n * @returns Expiration timestamp in milliseconds, or undefined\n */\nexport function calculateExpiration(\n ttl: number | undefined,\n): number | undefined {\n if (ttl === undefined || ttl <= 0) {\n return undefined;\n }\n return Date.now() + ttl * 1000;\n}\n\n/**\n * Serializes a value to JSON string\n * @param value - The value to serialize\n * @returns JSON string\n * @throws Error if serialization fails\n */\nexport function serialize(value: any): string {\n try {\n return JSON.stringify(value);\n } catch (error) {\n throw new Error(\n `Failed to serialize value: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n}\n\n/**\n * Deserializes a JSON string to a value\n * @param json - The JSON string\n * @returns Deserialized value\n * @throws Error if deserialization fails\n */\nexport function deserialize<T = any>(json: string): T {\n try {\n return JSON.parse(json);\n } catch (error) {\n throw new Error(\n `Failed to deserialize value: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n}\n","/**\n * Cache package entry point\n * Provides standardized caching interface\n */\n\nimport { loadEnvConfig } from '@happyvertical/utils';\nimport type {\n CacheAdapter,\n CacheAdapterOptions,\n FileOptions,\n MemoryOptions,\n RedisOptions,\n S3Options,\n} from './shared/types';\n\n// Export all types\nexport * from './shared/types';\nexport * from './shared/utils';\n\n/**\n * Type guard for Memory cache options\n */\nfunction isMemoryOptions(\n options: CacheAdapterOptions,\n): options is MemoryOptions {\n return options.provider === 'memory';\n}\n\n/**\n * Type guard for File cache options\n */\nfunction isFileOptions(options: CacheAdapterOptions): options is FileOptions {\n return options.provider === 'file';\n}\n\n/**\n * Type guard for Redis cache options\n */\nfunction isRedisOptions(options: CacheAdapterOptions): options is RedisOptions {\n return options.provider === 'redis';\n}\n\n/**\n * Type guard for S3 cache options\n */\nfunction isS3Options(options: CacheAdapterOptions): options is S3Options {\n return options.provider === 's3';\n}\n\n/**\n * Factory function to create a cache adapter instance\n *\n * Supports environment variable configuration using the HAVE_CACHE_* pattern:\n * - HAVE_CACHE_PROVIDER → provider ('memory'|'file'|'redis'|'s3')\n * - HAVE_CACHE_NAMESPACE → namespace (string)\n * - HAVE_CACHE_DEFAULT_TTL → defaultTTL (number: seconds)\n * - HAVE_CACHE_MAX_SIZE → maxSize (number: bytes)\n * - HAVE_CACHE_MAX_ENTRIES → maxEntries (number, memory only)\n * - HAVE_CACHE_EVICTION_POLICY → evictionPolicy ('lru'|'lfu'|'fifo', memory only)\n * - HAVE_CACHE_CACHE_DIR → cacheDir (string, file only)\n * - HAVE_CACHE_COMPRESSION → compression (boolean, file/s3)\n * - HAVE_CACHE_HOST → host (string, redis only)\n * - HAVE_CACHE_PORT → port (number, redis only)\n * - HAVE_CACHE_BUCKET → bucket (string, s3 only)\n * - HAVE_CACHE_PREFIX → prefix (string, s3 only)\n * - HAVE_CACHE_REGION → region (string, s3 only)\n *\n * User-provided options always take precedence over environment variables.\n *\n * @param options - Configuration options for the cache provider\n * @returns Promise resolving to a cache adapter that implements CacheAdapter\n *\n * @example\n * ```typescript\n * // Create memory cache with explicit options\n * const memoryCache = await getCache({\n * provider: 'memory',\n * maxSize: 100 * 1024 * 1024,\n * evictionPolicy: 'lru'\n * });\n *\n * // Create memory cache with environment variables\n * // HAVE_CACHE_PROVIDER=memory\n * // HAVE_CACHE_MAX_SIZE=104857600\n * // HAVE_CACHE_EVICTION_POLICY=lru\n * const envCache = await getCache({ provider: 'memory' });\n *\n * // Create file cache\n * const fileCache = await getCache({\n * provider: 'file',\n * cacheDir: './cache',\n * compression: true\n * });\n *\n * // Create Redis cache\n * const redisCache = await getCache({\n * provider: 'redis',\n * host: 'localhost',\n * port: 6379\n * });\n *\n * // Create S3 cache (for CI persistence)\n * const s3Cache = await getCache({\n * provider: 's3',\n * bucket: 'my-cache-bucket',\n * prefix: 'cache/',\n * region: 'us-east-1'\n * });\n *\n * // Use the cache\n * await memoryCache.set('user:123', { name: 'John' });\n * const user = await memoryCache.get('user:123');\n * ```\n */\nexport async function getCache(\n options: CacheAdapterOptions,\n): Promise<CacheAdapter> {\n // Load configuration from environment variables, merging with user options\n // User options always take precedence\n // Use 'any' to work around TypeScript's discriminated union type checking\n const config = loadEnvConfig(options as any, {\n packageName: 'cache',\n schema: {\n provider: 'string',\n namespace: 'string',\n defaultTTL: 'number',\n maxSize: 'number',\n maxEntries: 'number',\n evictionPolicy: 'string',\n checkPeriod: 'number',\n cacheDir: 'string',\n compression: 'boolean',\n fileExtension: 'string',\n host: 'string',\n port: 'number',\n password: 'string',\n db: 'number',\n keyPrefix: 'string',\n enableCompression: 'boolean',\n compressionThreshold: 'number',\n connectTimeout: 'number',\n commandTimeout: 'number',\n // S3 options\n bucket: 'string',\n prefix: 'string',\n region: 'string',\n } as any,\n allowUnknown: false,\n }) as CacheAdapterOptions;\n\n if (isMemoryOptions(config)) {\n const { MemoryProvider } = await import('./providers/memory.js');\n return new MemoryProvider(config);\n }\n\n if (isFileOptions(config)) {\n const { FileProvider } = await import('./providers/file.js');\n return new FileProvider(config);\n }\n\n if (isRedisOptions(config)) {\n const { RedisProvider } = await import('./providers/redis.js');\n return new RedisProvider(config);\n }\n\n if (isS3Options(config)) {\n const { S3Provider } = await import('./providers/s3.js');\n return new S3Provider(config);\n }\n\n // This should never happen due to TypeScript's discriminated union\n throw new Error(`Unsupported provider: ${(config as any).provider}`);\n}\n\n/** @internal */\nexport const PACKAGE_VERSION_INITIALIZED = true;\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@happyvertical/cache",
3
- "version": "0.80.0",
3
+ "version": "0.80.2",
4
4
  "description": "Standardized caching interface supporting Memory, File, and Redis backends",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -50,14 +50,14 @@
50
50
  "@aws-sdk/client-s3": "3.1034.0",
51
51
  "@aws-sdk/credential-providers": "3.1034.0",
52
52
  "redis": "^5.12.1",
53
- "@happyvertical/utils": "0.80.0"
53
+ "@happyvertical/utils": "0.80.2"
54
54
  },
55
55
  "devDependencies": {
56
56
  "@types/node": "25.0.10",
57
- "typescript": "^5.9.3",
58
- "vite": "7.3.2",
57
+ "typescript": "5.9.3",
58
+ "vite": "8.1.4",
59
59
  "vite-plugin-dts": "4.5.4",
60
- "vitest": "^4.1.5"
60
+ "vitest": "4.1.10"
61
61
  },
62
62
  "scripts": {
63
63
  "test": "vitest --config ../../vitest.package.config.ts run",
@@ -1,450 +0,0 @@
1
- import { readFile, rm, stat, mkdir, readdir, writeFile } from "node:fs/promises";
2
- import { resolve, join } from "node:path";
3
- import { promisify } from "node:util";
4
- import { gunzip, gzip } from "node:zlib";
5
- import { isValidKey, CacheKeyError, formatKey, deserialize, isExpired, CacheError, calculateExpiration, calculateSize, extractKey, matchesPattern, serialize, CacheSerializationError, CacheSizeError } from "../index.js";
6
- const gzipAsync = promisify(gzip);
7
- const gunzipAsync = promisify(gunzip);
8
- class FileProvider {
9
- cacheDir;
10
- namespace;
11
- defaultTTL;
12
- maxSize;
13
- compression;
14
- fileExtension;
15
- checkPeriod;
16
- checkInterval;
17
- stats;
18
- constructor(options) {
19
- this.cacheDir = resolve(options.cacheDir);
20
- this.namespace = options.namespace;
21
- this.defaultTTL = options.defaultTTL;
22
- this.maxSize = options.maxSize || 500 * 1024 * 1024;
23
- this.compression = options.compression ?? false;
24
- this.fileExtension = options.fileExtension || ".cache";
25
- this.checkPeriod = options.checkPeriod || 3e5;
26
- this.stats = {
27
- hits: 0,
28
- misses: 0,
29
- evictions: 0
30
- };
31
- this.ensureCacheDir();
32
- this.startCleanup();
33
- }
34
- async get(key) {
35
- if (!isValidKey(key)) {
36
- throw new CacheKeyError(key, "file");
37
- }
38
- const fullKey = formatKey(this.namespace, key);
39
- const filePath = this.getFilePath(fullKey);
40
- try {
41
- const fileContent = await readFile(filePath);
42
- let data;
43
- if (this.compression) {
44
- data = await gunzipAsync(fileContent);
45
- } else {
46
- data = fileContent;
47
- }
48
- const entry = deserialize(data.toString("utf-8"));
49
- if (isExpired(entry.expiresAt)) {
50
- await rm(filePath, { force: true });
51
- this.stats.misses++;
52
- return void 0;
53
- }
54
- entry.hits++;
55
- this.stats.hits++;
56
- await this.writeEntry(filePath, entry);
57
- return entry.value;
58
- } catch (error) {
59
- if (error.code === "ENOENT") {
60
- this.stats.misses++;
61
- return void 0;
62
- }
63
- throw new CacheError(
64
- `Failed to read cache entry: ${error.message}`,
65
- "READ_ERROR",
66
- "file"
67
- );
68
- }
69
- }
70
- async set(key, value, ttl) {
71
- if (!isValidKey(key)) {
72
- throw new CacheKeyError(key, "file");
73
- }
74
- const fullKey = formatKey(this.namespace, key);
75
- const filePath = this.getFilePath(fullKey);
76
- const expiresAt = calculateExpiration(ttl ?? this.defaultTTL);
77
- const entry = {
78
- value,
79
- createdAt: Date.now(),
80
- expiresAt,
81
- size: calculateSize(value),
82
- hits: 0,
83
- metadata: {
84
- compressed: this.compression,
85
- namespace: this.namespace
86
- }
87
- };
88
- await this.evictIfNeeded(entry.size);
89
- await this.writeEntry(filePath, entry);
90
- }
91
- async has(key) {
92
- if (!isValidKey(key)) {
93
- throw new CacheKeyError(key, "file");
94
- }
95
- const fullKey = formatKey(this.namespace, key);
96
- const filePath = this.getFilePath(fullKey);
97
- try {
98
- const fileContent = await readFile(filePath);
99
- let data;
100
- if (this.compression) {
101
- data = await gunzipAsync(fileContent);
102
- } else {
103
- data = fileContent;
104
- }
105
- const entry = deserialize(data.toString("utf-8"));
106
- if (isExpired(entry.expiresAt)) {
107
- await rm(filePath, { force: true });
108
- return false;
109
- }
110
- return true;
111
- } catch (error) {
112
- if (error.code === "ENOENT") {
113
- return false;
114
- }
115
- throw new CacheError(
116
- `Failed to check cache entry: ${error.message}`,
117
- "CHECK_ERROR",
118
- "file"
119
- );
120
- }
121
- }
122
- async delete(key) {
123
- if (!isValidKey(key)) {
124
- throw new CacheKeyError(key, "file");
125
- }
126
- const fullKey = formatKey(this.namespace, key);
127
- const filePath = this.getFilePath(fullKey);
128
- try {
129
- await rm(filePath);
130
- return true;
131
- } catch (error) {
132
- if (error.code === "ENOENT") {
133
- return false;
134
- }
135
- throw new CacheError(
136
- `Failed to delete cache entry: ${error.message}`,
137
- "DELETE_ERROR",
138
- "file"
139
- );
140
- }
141
- }
142
- async clear(namespace) {
143
- if (namespace) {
144
- const prefix = this.sanitizeKey(`${namespace}:`);
145
- const files = await this.getAllFiles();
146
- for (const file of files) {
147
- if (file.startsWith(prefix)) {
148
- await rm(join(this.cacheDir, file), { force: true });
149
- }
150
- }
151
- } else {
152
- try {
153
- await rm(this.cacheDir, { recursive: true, force: true });
154
- await this.ensureCacheDir();
155
- this.stats.hits = 0;
156
- this.stats.misses = 0;
157
- this.stats.evictions = 0;
158
- } catch (error) {
159
- throw new CacheError(
160
- `Failed to clear cache: ${error.message}`,
161
- "CLEAR_ERROR",
162
- "file"
163
- );
164
- }
165
- }
166
- }
167
- async keys(pattern) {
168
- const files = await this.getAllFiles();
169
- const keys = [];
170
- for (const file of files) {
171
- const key = file.replace(this.fileExtension, "");
172
- const filePath = join(this.cacheDir, file);
173
- try {
174
- const fileContent = await readFile(filePath);
175
- let data;
176
- if (this.compression) {
177
- data = await gunzipAsync(fileContent);
178
- } else {
179
- data = fileContent;
180
- }
181
- const entry = deserialize(data.toString("utf-8"));
182
- if (!isExpired(entry.expiresAt)) {
183
- const desanitized = this.desanitizeKey(key);
184
- keys.push(extractKey(this.namespace, desanitized));
185
- }
186
- } catch {
187
- }
188
- }
189
- if (pattern) {
190
- return keys.filter((key) => matchesPattern(pattern, key));
191
- }
192
- return keys;
193
- }
194
- async getMany(keys) {
195
- const result = /* @__PURE__ */ new Map();
196
- for (const key of keys) {
197
- const value = await this.get(key);
198
- if (value !== void 0) {
199
- result.set(key, value);
200
- }
201
- }
202
- return result;
203
- }
204
- async setMany(entries) {
205
- for (const entry of entries) {
206
- await this.set(entry.key, entry.value, entry.ttl);
207
- }
208
- }
209
- async deleteMany(keys) {
210
- let deleted = 0;
211
- for (const key of keys) {
212
- const wasDeleted = await this.delete(key);
213
- if (wasDeleted) {
214
- deleted++;
215
- }
216
- }
217
- return deleted;
218
- }
219
- async getStats() {
220
- const files = await this.getAllFiles();
221
- let totalSize = 0;
222
- let entries = 0;
223
- for (const file of files) {
224
- const filePath = join(this.cacheDir, file);
225
- try {
226
- const stats = await stat(filePath);
227
- totalSize += stats.size;
228
- const fileContent = await readFile(filePath);
229
- let data;
230
- if (this.compression) {
231
- data = await gunzipAsync(fileContent);
232
- } else {
233
- data = fileContent;
234
- }
235
- const entry = deserialize(data.toString("utf-8"));
236
- if (!isExpired(entry.expiresAt)) {
237
- entries++;
238
- }
239
- } catch {
240
- }
241
- }
242
- const totalAccesses = this.stats.hits + this.stats.misses;
243
- const hitRate = totalAccesses > 0 ? this.stats.hits / totalAccesses : 0;
244
- return {
245
- entries,
246
- totalSize,
247
- hits: this.stats.hits,
248
- misses: this.stats.misses,
249
- hitRate,
250
- evictions: this.stats.evictions,
251
- backend: {
252
- type: "file",
253
- cacheDir: this.cacheDir,
254
- compression: this.compression,
255
- maxSize: this.maxSize
256
- }
257
- };
258
- }
259
- async touch(key, ttl) {
260
- if (!isValidKey(key)) {
261
- throw new CacheKeyError(key, "file");
262
- }
263
- const fullKey = formatKey(this.namespace, key);
264
- const filePath = this.getFilePath(fullKey);
265
- try {
266
- const fileContent = await readFile(filePath);
267
- let data;
268
- if (this.compression) {
269
- data = await gunzipAsync(fileContent);
270
- } else {
271
- data = fileContent;
272
- }
273
- const entry = deserialize(data.toString("utf-8"));
274
- if (isExpired(entry.expiresAt)) {
275
- return false;
276
- }
277
- entry.expiresAt = calculateExpiration(ttl);
278
- await this.writeEntry(filePath, entry);
279
- return true;
280
- } catch (error) {
281
- if (error.code === "ENOENT") {
282
- return false;
283
- }
284
- throw new CacheError(
285
- `Failed to touch cache entry: ${error.message}`,
286
- "TOUCH_ERROR",
287
- "file"
288
- );
289
- }
290
- }
291
- async close() {
292
- if (this.checkInterval) {
293
- clearInterval(this.checkInterval);
294
- this.checkInterval = void 0;
295
- }
296
- }
297
- /**
298
- * Ensures cache directory exists
299
- */
300
- async ensureCacheDir() {
301
- try {
302
- await mkdir(this.cacheDir, { recursive: true });
303
- } catch (error) {
304
- throw new CacheError(
305
- `Failed to create cache directory: ${error.message}`,
306
- "INIT_ERROR",
307
- "file"
308
- );
309
- }
310
- }
311
- /**
312
- * Gets the file path for a cache key
313
- */
314
- getFilePath(key) {
315
- const sanitizedKey = this.sanitizeKey(key);
316
- return join(this.cacheDir, `${sanitizedKey}${this.fileExtension}`);
317
- }
318
- /**
319
- * Sanitizes a key for use as a filename
320
- */
321
- sanitizeKey(key) {
322
- return key.replace(/[^a-zA-Z0-9_:-]/g, "_");
323
- }
324
- /**
325
- * Desanitizes a filename back to the original key
326
- */
327
- desanitizeKey(sanitized) {
328
- return sanitized;
329
- }
330
- /**
331
- * Gets all cache file names
332
- */
333
- async getAllFiles() {
334
- try {
335
- const files = await readdir(this.cacheDir);
336
- return files.filter((file) => file.endsWith(this.fileExtension));
337
- } catch (error) {
338
- if (error.code === "ENOENT") {
339
- return [];
340
- }
341
- throw new CacheError(
342
- `Failed to list cache files: ${error.message}`,
343
- "LIST_ERROR",
344
- "file"
345
- );
346
- }
347
- }
348
- /**
349
- * Writes an entry to a file
350
- */
351
- async writeEntry(filePath, entry) {
352
- try {
353
- let data;
354
- const json = serialize(entry);
355
- data = Buffer.from(json, "utf-8");
356
- if (this.compression) {
357
- data = await gzipAsync(data);
358
- }
359
- await writeFile(filePath, data);
360
- } catch (error) {
361
- throw new CacheSerializationError(
362
- `Failed to write cache entry: ${error.message}`,
363
- "file"
364
- );
365
- }
366
- }
367
- /**
368
- * Evicts files if size limit is exceeded
369
- */
370
- async evictIfNeeded(newEntrySize) {
371
- const stats = await this.getStats();
372
- if (stats.totalSize + newEntrySize > this.maxSize) {
373
- await this.evict();
374
- const updatedStats = await this.getStats();
375
- if (updatedStats.totalSize + newEntrySize > this.maxSize) {
376
- throw new CacheSizeError(
377
- `Cannot cache entry: would exceed max size of ${this.maxSize} bytes`,
378
- "file"
379
- );
380
- }
381
- }
382
- }
383
- /**
384
- * Evicts oldest files based on creation time
385
- */
386
- async evict() {
387
- const files = await this.getAllFiles();
388
- const filesWithStats = [];
389
- for (const file of files) {
390
- const filePath = join(this.cacheDir, file);
391
- try {
392
- const fileContent = await readFile(filePath);
393
- let data;
394
- if (this.compression) {
395
- data = await gunzipAsync(fileContent);
396
- } else {
397
- data = fileContent;
398
- }
399
- const entry = deserialize(data.toString("utf-8"));
400
- filesWithStats.push({ file, createdAt: entry.createdAt });
401
- } catch {
402
- }
403
- }
404
- filesWithStats.sort((a, b) => a.createdAt - b.createdAt);
405
- const toRemove = Math.max(1, Math.floor(filesWithStats.length * 0.1));
406
- for (let i = 0; i < toRemove; i++) {
407
- const filePath = join(this.cacheDir, filesWithStats[i].file);
408
- await rm(filePath, { force: true });
409
- this.stats.evictions++;
410
- }
411
- }
412
- /**
413
- * Starts background cleanup of expired files
414
- */
415
- startCleanup() {
416
- this.checkInterval = setInterval(() => {
417
- this.removeExpiredFiles();
418
- }, this.checkPeriod);
419
- if (this.checkInterval.unref) {
420
- this.checkInterval.unref();
421
- }
422
- }
423
- /**
424
- * Removes expired files
425
- */
426
- async removeExpiredFiles() {
427
- const files = await this.getAllFiles();
428
- for (const file of files) {
429
- const filePath = join(this.cacheDir, file);
430
- try {
431
- const fileContent = await readFile(filePath);
432
- let data;
433
- if (this.compression) {
434
- data = await gunzipAsync(fileContent);
435
- } else {
436
- data = fileContent;
437
- }
438
- const entry = deserialize(data.toString("utf-8"));
439
- if (isExpired(entry.expiresAt)) {
440
- await rm(filePath, { force: true });
441
- }
442
- } catch {
443
- }
444
- }
445
- }
446
- }
447
- export {
448
- FileProvider
449
- };
450
- //# sourceMappingURL=file-DyC_7WDS.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"file-DyC_7WDS.js","sources":["../../src/providers/file.ts"],"sourcesContent":["/**\n * File cache provider implementation with compression\n */\n\nimport {\n mkdir,\n readdir,\n readFile,\n rm,\n stat,\n writeFile,\n} from 'node:fs/promises';\nimport { join, resolve } from 'node:path';\nimport { promisify } from 'node:util';\nimport { gunzip, gzip } from 'node:zlib';\nimport type {\n CacheEntry,\n CacheProvider,\n CacheStats,\n FileOptions,\n} from '../shared/types';\nimport {\n CacheError,\n CacheKeyError,\n CacheSerializationError,\n CacheSizeError,\n} from '../shared/types';\nimport {\n calculateExpiration,\n calculateSize,\n deserialize,\n extractKey,\n formatKey,\n isExpired,\n isValidKey,\n matchesPattern,\n serialize,\n} from '../shared/utils';\n\nconst gzipAsync = promisify(gzip);\nconst gunzipAsync = promisify(gunzip);\n\n/**\n * File cache provider implementation\n * Stores cache entries as files with optional compression\n */\nexport class FileProvider implements CacheProvider {\n private cacheDir: string;\n private namespace?: string;\n private defaultTTL?: number;\n private maxSize: number;\n private compression: boolean;\n private fileExtension: string;\n private checkPeriod: number;\n private checkInterval?: NodeJS.Timeout;\n private stats: {\n hits: number;\n misses: number;\n evictions: number;\n };\n\n constructor(options: FileOptions) {\n this.cacheDir = resolve(options.cacheDir);\n this.namespace = options.namespace;\n this.defaultTTL = options.defaultTTL;\n this.maxSize = options.maxSize || 500 * 1024 * 1024; // 500MB default\n this.compression = options.compression ?? false;\n this.fileExtension = options.fileExtension || '.cache';\n this.checkPeriod = options.checkPeriod || 300000; // 5 minutes default\n this.stats = {\n hits: 0,\n misses: 0,\n evictions: 0,\n };\n\n // Ensure cache directory exists\n this.ensureCacheDir();\n\n // Start background cleanup\n this.startCleanup();\n }\n\n async get<T = any>(key: string): Promise<T | undefined> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'file');\n }\n\n const fullKey = formatKey(this.namespace, key);\n const filePath = this.getFilePath(fullKey);\n\n try {\n const fileContent = await readFile(filePath);\n let data: Buffer;\n\n // Decompress if needed\n if (this.compression) {\n data = await gunzipAsync(fileContent);\n } else {\n data = fileContent;\n }\n\n const entry: CacheEntry<T> = deserialize(data.toString('utf-8'));\n\n // Check if expired\n if (isExpired(entry.expiresAt)) {\n await rm(filePath, { force: true });\n this.stats.misses++;\n return undefined;\n }\n\n // Update hit count\n entry.hits++;\n this.stats.hits++;\n\n // Write back updated entry (for hit tracking)\n await this.writeEntry(filePath, entry);\n\n return entry.value;\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n this.stats.misses++;\n return undefined;\n }\n throw new CacheError(\n `Failed to read cache entry: ${error.message}`,\n 'READ_ERROR',\n 'file',\n );\n }\n }\n\n async set<T = any>(key: string, value: T, ttl?: number): Promise<void> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'file');\n }\n\n const fullKey = formatKey(this.namespace, key);\n const filePath = this.getFilePath(fullKey);\n const expiresAt = calculateExpiration(ttl ?? this.defaultTTL);\n\n const entry: CacheEntry<T> = {\n value,\n createdAt: Date.now(),\n expiresAt,\n size: calculateSize(value),\n hits: 0,\n metadata: {\n compressed: this.compression,\n namespace: this.namespace,\n },\n };\n\n // Check if we need to evict files\n await this.evictIfNeeded(entry.size);\n\n // Write entry to file\n await this.writeEntry(filePath, entry);\n }\n\n async has(key: string): Promise<boolean> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'file');\n }\n\n const fullKey = formatKey(this.namespace, key);\n const filePath = this.getFilePath(fullKey);\n\n try {\n const fileContent = await readFile(filePath);\n let data: Buffer;\n\n if (this.compression) {\n data = await gunzipAsync(fileContent);\n } else {\n data = fileContent;\n }\n\n const entry: CacheEntry = deserialize(data.toString('utf-8'));\n\n // Check if expired\n if (isExpired(entry.expiresAt)) {\n await rm(filePath, { force: true });\n return false;\n }\n\n return true;\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n return false;\n }\n throw new CacheError(\n `Failed to check cache entry: ${error.message}`,\n 'CHECK_ERROR',\n 'file',\n );\n }\n }\n\n async delete(key: string): Promise<boolean> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'file');\n }\n\n const fullKey = formatKey(this.namespace, key);\n const filePath = this.getFilePath(fullKey);\n\n try {\n await rm(filePath);\n return true;\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n return false;\n }\n throw new CacheError(\n `Failed to delete cache entry: ${error.message}`,\n 'DELETE_ERROR',\n 'file',\n );\n }\n }\n\n async clear(namespace?: string): Promise<void> {\n if (namespace) {\n // Clear specific namespace\n const prefix = this.sanitizeKey(`${namespace}:`);\n const files = await this.getAllFiles();\n\n for (const file of files) {\n if (file.startsWith(prefix)) {\n await rm(join(this.cacheDir, file), { force: true });\n }\n }\n } else {\n // Clear all cache files\n try {\n await rm(this.cacheDir, { recursive: true, force: true });\n await this.ensureCacheDir();\n this.stats.hits = 0;\n this.stats.misses = 0;\n this.stats.evictions = 0;\n } catch (error: any) {\n throw new CacheError(\n `Failed to clear cache: ${error.message}`,\n 'CLEAR_ERROR',\n 'file',\n );\n }\n }\n }\n\n async keys(pattern?: string): Promise<string[]> {\n const files = await this.getAllFiles();\n const keys: string[] = [];\n\n for (const file of files) {\n // Remove file extension\n const key = file.replace(this.fileExtension, '');\n\n // Check if file is expired\n const filePath = join(this.cacheDir, file);\n try {\n const fileContent = await readFile(filePath);\n let data: Buffer;\n\n if (this.compression) {\n data = await gunzipAsync(fileContent);\n } else {\n data = fileContent;\n }\n\n const entry: CacheEntry = deserialize(data.toString('utf-8'));\n\n if (!isExpired(entry.expiresAt)) {\n const desanitized = this.desanitizeKey(key);\n keys.push(extractKey(this.namespace, desanitized));\n }\n } catch {}\n }\n\n // Apply pattern filter if provided\n if (pattern) {\n return keys.filter((key) => matchesPattern(pattern, key));\n }\n\n return keys;\n }\n\n async getMany<T = any>(keys: string[]): Promise<Map<string, T>> {\n const result = new Map<string, T>();\n\n for (const key of keys) {\n const value = await this.get<T>(key);\n if (value !== undefined) {\n result.set(key, value);\n }\n }\n\n return result;\n }\n\n async setMany<T = any>(\n entries: Array<{ key: string; value: T; ttl?: number }>,\n ): Promise<void> {\n for (const entry of entries) {\n await this.set(entry.key, entry.value, entry.ttl);\n }\n }\n\n async deleteMany(keys: string[]): Promise<number> {\n let deleted = 0;\n\n for (const key of keys) {\n const wasDeleted = await this.delete(key);\n if (wasDeleted) {\n deleted++;\n }\n }\n\n return deleted;\n }\n\n async getStats(): Promise<CacheStats> {\n const files = await this.getAllFiles();\n let totalSize = 0;\n let entries = 0;\n\n for (const file of files) {\n const filePath = join(this.cacheDir, file);\n try {\n const stats = await stat(filePath);\n totalSize += stats.size;\n\n // Check if expired\n const fileContent = await readFile(filePath);\n let data: Buffer;\n\n if (this.compression) {\n data = await gunzipAsync(fileContent);\n } else {\n data = fileContent;\n }\n\n const entry: CacheEntry = deserialize(data.toString('utf-8'));\n\n if (!isExpired(entry.expiresAt)) {\n entries++;\n }\n } catch {}\n }\n\n const totalAccesses = this.stats.hits + this.stats.misses;\n const hitRate = totalAccesses > 0 ? this.stats.hits / totalAccesses : 0;\n\n return {\n entries,\n totalSize,\n hits: this.stats.hits,\n misses: this.stats.misses,\n hitRate,\n evictions: this.stats.evictions,\n backend: {\n type: 'file',\n cacheDir: this.cacheDir,\n compression: this.compression,\n maxSize: this.maxSize,\n },\n };\n }\n\n async touch(key: string, ttl: number): Promise<boolean> {\n if (!isValidKey(key)) {\n throw new CacheKeyError(key, 'file');\n }\n\n const fullKey = formatKey(this.namespace, key);\n const filePath = this.getFilePath(fullKey);\n\n try {\n const fileContent = await readFile(filePath);\n let data: Buffer;\n\n if (this.compression) {\n data = await gunzipAsync(fileContent);\n } else {\n data = fileContent;\n }\n\n const entry: CacheEntry = deserialize(data.toString('utf-8'));\n\n if (isExpired(entry.expiresAt)) {\n return false;\n }\n\n entry.expiresAt = calculateExpiration(ttl);\n await this.writeEntry(filePath, entry);\n\n return true;\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n return false;\n }\n throw new CacheError(\n `Failed to touch cache entry: ${error.message}`,\n 'TOUCH_ERROR',\n 'file',\n );\n }\n }\n\n async close(): Promise<void> {\n // Stop cleanup interval\n if (this.checkInterval) {\n clearInterval(this.checkInterval);\n this.checkInterval = undefined;\n }\n }\n\n /**\n * Ensures cache directory exists\n */\n private async ensureCacheDir(): Promise<void> {\n try {\n await mkdir(this.cacheDir, { recursive: true });\n } catch (error: any) {\n throw new CacheError(\n `Failed to create cache directory: ${error.message}`,\n 'INIT_ERROR',\n 'file',\n );\n }\n }\n\n /**\n * Gets the file path for a cache key\n */\n private getFilePath(key: string): string {\n const sanitizedKey = this.sanitizeKey(key);\n return join(this.cacheDir, `${sanitizedKey}${this.fileExtension}`);\n }\n\n /**\n * Sanitizes a key for use as a filename\n */\n private sanitizeKey(key: string): string {\n return key.replace(/[^a-zA-Z0-9_:-]/g, '_');\n }\n\n /**\n * Desanitizes a filename back to the original key\n */\n private desanitizeKey(sanitized: string): string {\n // This is a simple implementation - in practice, you might need\n // a more sophisticated mapping\n return sanitized;\n }\n\n /**\n * Gets all cache file names\n */\n private async getAllFiles(): Promise<string[]> {\n try {\n const files = await readdir(this.cacheDir);\n return files.filter((file) => file.endsWith(this.fileExtension));\n } catch (error: any) {\n if (error.code === 'ENOENT') {\n return [];\n }\n throw new CacheError(\n `Failed to list cache files: ${error.message}`,\n 'LIST_ERROR',\n 'file',\n );\n }\n }\n\n /**\n * Writes an entry to a file\n */\n private async writeEntry(filePath: string, entry: CacheEntry): Promise<void> {\n try {\n let data: Buffer;\n const json = serialize(entry);\n data = Buffer.from(json, 'utf-8');\n\n // Compress if enabled\n if (this.compression) {\n data = await gzipAsync(data);\n }\n\n await writeFile(filePath, data);\n } catch (error: any) {\n throw new CacheSerializationError(\n `Failed to write cache entry: ${error.message}`,\n 'file',\n );\n }\n }\n\n /**\n * Evicts files if size limit is exceeded\n */\n private async evictIfNeeded(newEntrySize: number): Promise<void> {\n const stats = await this.getStats();\n\n // Check if we need to evict\n if (stats.totalSize + newEntrySize > this.maxSize) {\n await this.evict();\n\n // Check again after eviction\n const updatedStats = await this.getStats();\n if (updatedStats.totalSize + newEntrySize > this.maxSize) {\n throw new CacheSizeError(\n `Cannot cache entry: would exceed max size of ${this.maxSize} bytes`,\n 'file',\n );\n }\n }\n }\n\n /**\n * Evicts oldest files based on creation time\n */\n private async evict(): Promise<void> {\n const files = await this.getAllFiles();\n const filesWithStats: Array<{ file: string; createdAt: number }> = [];\n\n for (const file of files) {\n const filePath = join(this.cacheDir, file);\n try {\n const fileContent = await readFile(filePath);\n let data: Buffer;\n\n if (this.compression) {\n data = await gunzipAsync(fileContent);\n } else {\n data = fileContent;\n }\n\n const entry: CacheEntry = deserialize(data.toString('utf-8'));\n filesWithStats.push({ file, createdAt: entry.createdAt });\n } catch {}\n }\n\n // Sort by creation time (oldest first)\n filesWithStats.sort((a, b) => a.createdAt - b.createdAt);\n\n // Remove oldest 10% of files\n const toRemove = Math.max(1, Math.floor(filesWithStats.length * 0.1));\n for (let i = 0; i < toRemove; i++) {\n const filePath = join(this.cacheDir, filesWithStats[i].file);\n await rm(filePath, { force: true });\n this.stats.evictions++;\n }\n }\n\n /**\n * Starts background cleanup of expired files\n */\n private startCleanup(): void {\n this.checkInterval = setInterval(() => {\n this.removeExpiredFiles();\n }, this.checkPeriod);\n\n // Don't block process exit\n if (this.checkInterval.unref) {\n this.checkInterval.unref();\n }\n }\n\n /**\n * Removes expired files\n */\n private async removeExpiredFiles(): Promise<void> {\n const files = await this.getAllFiles();\n\n for (const file of files) {\n const filePath = join(this.cacheDir, file);\n try {\n const fileContent = await readFile(filePath);\n let data: Buffer;\n\n if (this.compression) {\n data = await gunzipAsync(fileContent);\n } else {\n data = fileContent;\n }\n\n const entry: CacheEntry = deserialize(data.toString('utf-8'));\n\n if (isExpired(entry.expiresAt)) {\n await rm(filePath, { force: true });\n }\n } catch {}\n }\n }\n}\n"],"names":[],"mappings":"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"}