Skip to content

Commit ffb7111

Browse files
authored
Merge pull request #48 from aichatwatch/overrideSimplecacheFolder
adding support for overriding simplecache folder
2 parents 40d1793 + 8b26f33 commit ffb7111

1 file changed

Lines changed: 97 additions & 13 deletions

File tree

src/utils/simple-cache.ts

Lines changed: 97 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,29 @@ import { USER_CACHE_DIR } from '../config/user-paths.js';
44
import { logger } from './compact-logger.js';
55
import { 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+
730
interface 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
*/
1658
export 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

Comments
 (0)