@@ -4,6 +4,29 @@ import { USER_CACHE_DIR } from '../config/user-paths.js';
44import { logger } from './compact-logger.js' ;
55import { writeFileAtomic } from './misc-utils.js' ;
66
7+ /**
8+ * Logger interface for optional external logging
9+ */
10+ export interface ISimpleCacheLogger {
11+ debug : ( msg : string ) => void ;
12+ warn : ( msg : string ) => void ;
13+ error : ( msg : string ) => void ;
14+ }
15+
16+ /**
17+ * Configuration options for SimpleCache
18+ */
19+ export interface SimpleCacheOptions {
20+ /** Name of the cache (used for file naming) */
21+ cacheName : string ;
22+ /** Directory to store cache files (defaults to system cache dir) */
23+ cacheDir ?: string ;
24+ /** Maximum age of cache entries in seconds (optional TTL) */
25+ maxAgeSeconds ?: number ;
26+ /** Optional logger for debug/warn/error messages */
27+ logger ?: ISimpleCacheLogger ;
28+ }
29+
730interface CacheEntry {
831 value : string ;
932 timestamp : number ; // milliseconds since epoch
@@ -12,28 +35,68 @@ interface CacheEntry {
1235/**
1336 * Simple key-value cache with file persistence and optional TTL support
1437 * Can be used for any caching needs in the application
38+ *
39+ * @example
40+ * // Basic usage (backward compatible)
41+ * const cache = new SimpleCache('my-cache');
42+ *
43+ * @example
44+ * // With custom directory and TTL
45+ * const cache = new SimpleCache({
46+ * cacheName: 'my-cache',
47+ * cacheDir: './data/cache',
48+ * maxAgeSeconds: 3600
49+ * });
50+ *
51+ * @example
52+ * // With custom logger
53+ * const cache = new SimpleCache({
54+ * cacheName: 'my-cache',
55+ * logger: myCustomLogger
56+ * });
1557 */
1658export class SimpleCache {
1759 private cache : Map < string , CacheEntry > ;
1860 private cachePath : string ;
1961 private cacheName : string ;
62+ private cacheDir : string ;
2063 private isDirty : boolean = false ;
2164 private maxAgeSeconds ?: number ;
65+ private logger ?: ISimpleCacheLogger ;
66+
67+ /**
68+ * Create a new SimpleCache instance
69+ * @param options - Cache configuration (or cacheName string for backward compatibility)
70+ * @param maxAgeSeconds - Optional TTL in seconds (only used with legacy string constructor)
71+ */
72+ constructor ( options : SimpleCacheOptions | string , maxAgeSeconds ?: number ) {
73+ // Support backward compatible constructor: new SimpleCache('name', ttl)
74+ if ( typeof options === 'string' ) {
75+ this . cacheName = options ;
76+ this . cacheDir = USER_CACHE_DIR ;
77+ this . maxAgeSeconds = maxAgeSeconds ;
78+ this . logger = logger ; // Use default logger for backward compat
79+ } else {
80+ this . cacheName = options . cacheName ;
81+ this . cacheDir = options . cacheDir || USER_CACHE_DIR ;
82+ this . maxAgeSeconds = options . maxAgeSeconds ;
83+ this . logger = options . logger ;
84+ }
2285
23- constructor ( cacheName : string , maxAgeSeconds ?: number ) {
24- this . cacheName = cacheName ;
25- this . cachePath = path . join ( USER_CACHE_DIR , `${ cacheName } .txt` ) ;
86+ this . cachePath = path . join ( this . cacheDir , `${ this . cacheName } .txt` ) ;
2687 this . cache = new Map ( ) ;
27- this . maxAgeSeconds = maxAgeSeconds ;
2888 }
2989
3090 /**
3191 * Load cache from disk
92+ * Automatically creates the cache directory if it doesn't exist.
93+ * Skips expired entries based on maxAgeSeconds.
94+ * Backward compatible with old cache format (without timestamps).
3295 */
3396 async load ( ) : Promise < void > {
3497 try {
3598 // Ensure cache directory exists
36- await fs . mkdir ( USER_CACHE_DIR , { recursive : true } ) ;
99+ await fs . mkdir ( this . cacheDir , { recursive : true } ) ;
37100
38101 // Try to read cache file
39102 const content = await fs . readFile ( this . cachePath , 'utf-8' ) ;
@@ -84,17 +147,17 @@ export class SimpleCache {
84147 }
85148
86149 if ( validEntries > 0 ) {
87- logger . debug ( `Loaded ${ validEntries } entries from ${ this . cacheName } cache` ) ;
150+ this . logger ? .debug ( `Loaded ${ validEntries } entries from ${ this . cacheName } cache` ) ;
88151 }
89152 if ( invalidEntries > 0 ) {
90- logger . warn ( `Skipped ${ invalidEntries } invalid entries in ${ this . cacheName } cache` ) ;
153+ this . logger ? .warn ( `Skipped ${ invalidEntries } invalid entries in ${ this . cacheName } cache` ) ;
91154 }
92155
93156 } catch ( error : any ) {
94157 if ( error . code === 'ENOENT' ) {
95- logger . debug ( `No existing ${ this . cacheName } cache found, starting fresh` ) ;
158+ this . logger ? .debug ( `No existing ${ this . cacheName } cache found, starting fresh` ) ;
96159 } else {
97- logger . warn ( `Failed to load ${ this . cacheName } cache: ${ error . message } ` ) ;
160+ this . logger ? .warn ( `Failed to load ${ this . cacheName } cache: ${ error . message } ` ) ;
98161 }
99162 // Start with empty cache on any error
100163 this . cache . clear ( ) ;
@@ -103,6 +166,9 @@ export class SimpleCache {
103166
104167 /**
105168 * Save cache to disk (only if dirty)
169+ * Uses atomic write to prevent corruption.
170+ * Only writes if the cache has been modified (dirty flag optimization).
171+ * Automatically creates the cache directory if it doesn't exist.
106172 */
107173 async save ( ) : Promise < void > {
108174 if ( ! this . isDirty ) {
@@ -111,7 +177,7 @@ export class SimpleCache {
111177
112178 try {
113179 // Ensure cache directory exists
114- await fs . mkdir ( USER_CACHE_DIR , { recursive : true } ) ;
180+ await fs . mkdir ( this . cacheDir , { recursive : true } ) ;
115181
116182 // Build cache content
117183 const lines : string [ ] = [ ] ;
@@ -126,16 +192,19 @@ export class SimpleCache {
126192 await writeFileAtomic ( this . cachePath , lines . join ( '\n' ) , { encoding : 'utf-8' } ) ;
127193 this . isDirty = false ;
128194
129- logger . debug ( `Saved ${ this . cache . size } entries to ${ this . cacheName } cache` ) ;
195+ this . logger ? .debug ( `Saved ${ this . cache . size } entries to ${ this . cacheName } cache` ) ;
130196
131197 } catch ( error : any ) {
132- logger . error ( `Failed to save ${ this . cacheName } cache: ${ error . message } ` ) ;
198+ this . logger ? .error ( `Failed to save ${ this . cacheName } cache: ${ error . message } ` ) ;
133199 // Don't throw - caching should not break the main flow
134200 }
135201 }
136202
137203 /**
138204 * Get value from cache
205+ * Automatically checks expiration and removes expired entries.
206+ * @param key - Cache key
207+ * @returns Cached value or undefined if not found or expired
139208 */
140209 get ( key : string ) : string | undefined {
141210 const entry = this . cache . get ( key ) ;
@@ -156,6 +225,10 @@ export class SimpleCache {
156225
157226 /**
158227 * Set value in cache
228+ * Marks cache as dirty if the value is new or changed.
229+ * Automatically sets timestamp for TTL tracking.
230+ * @param key - Cache key
231+ * @param value - Value to cache (must be a string)
159232 */
160233 set ( key : string , value : string ) : void {
161234 const newEntry = { value, timestamp : Date . now ( ) } ;
@@ -170,6 +243,9 @@ export class SimpleCache {
170243
171244 /**
172245 * Check if key exists in cache
246+ * Respects TTL - returns false for expired entries.
247+ * @param key - Cache key to check
248+ * @returns true if key exists and is not expired, false otherwise
173249 */
174250 has ( key : string ) : boolean {
175251 // Use get() to check expiration
@@ -178,6 +254,9 @@ export class SimpleCache {
178254
179255 /**
180256 * Delete a key from cache
257+ * Marks cache as dirty for persistence.
258+ * @param key - Cache key to delete
259+ * @returns true if key was deleted, false if key didn't exist
181260 */
182261 delete ( key : string ) : boolean {
183262 const result = this . cache . delete ( key ) ;
@@ -189,6 +268,7 @@ export class SimpleCache {
189268
190269 /**
191270 * Clear all cache entries
271+ * Marks cache as dirty for persistence.
192272 */
193273 clear ( ) : void {
194274 if ( this . cache . size > 0 ) {
@@ -199,27 +279,31 @@ export class SimpleCache {
199279
200280 /**
201281 * Get number of cached entries
282+ * @returns Number of entries currently in cache
202283 */
203284 size ( ) : number {
204285 return this . cache . size ;
205286 }
206287
207288 /**
208- * Get all keys
289+ * Get all keys in cache
290+ * @returns Array of all cache keys
209291 */
210292 keys ( ) : string [ ] {
211293 return Array . from ( this . cache . keys ( ) ) ;
212294 }
213295
214296 /**
215297 * Get all entries as array of [key, value] pairs
298+ * @returns Array of [key, value] tuples
216299 */
217300 entries ( ) : Array < [ string , string ] > {
218301 return Array . from ( this . cache . entries ( ) ) . map ( ( [ key , entry ] ) => [ key , entry . value ] ) ;
219302 }
220303
221304 /**
222305 * Get cache statistics
306+ * @returns Object containing cache size, file path, and dirty flag
223307 */
224308 getStats ( ) : { size : number ; path : string ; isDirty : boolean } {
225309 return {
0 commit comments