1+ import { initLogger , type Logger } from '../utils/logger.ts'
2+
3+ interface CacheEntry < T > {
4+ data : T
5+ expiresAt : number
6+ }
7+
8+ interface CacheOptions {
9+ ttlDays ?: number
10+ }
11+
12+ export class CacheService {
13+ private logger : Logger
14+ private cache : Map < string , CacheEntry < any > >
15+
16+ constructor ( ) {
17+ this . logger = initLogger ( )
18+ this . cache = new Map ( )
19+ }
20+
21+ /**
22+ * Fetches data with caching. If cached data exists and hasn't expired, returns it.
23+ * Otherwise, fetches fresh data using the provided fetcher function.
24+ */
25+ async fetchWithCache < T > (
26+ key : string ,
27+ fetcher : ( ) => Promise < T > ,
28+ options : CacheOptions = { }
29+ ) : Promise < T > {
30+ const { ttlDays = 7 } = options
31+ const now = Date . now ( )
32+
33+ // Check if we have valid cached data
34+ const cached = this . cache . get ( key )
35+ if ( cached && cached . expiresAt > now ) {
36+ this . logger . info ( { msg : `Cache hit for key: ${ key } ` } )
37+ return cached . data
38+ }
39+
40+ // Cache miss or expired - fetch fresh data
41+ this . logger . info ( { msg : `Cache miss for key: ${ key } , fetching fresh data...` } )
42+ try {
43+ const data = await fetcher ( )
44+
45+ // Calculate expiration time
46+ const expiresAt = now + ( ttlDays * 24 * 60 * 60 * 1000 )
47+
48+ // Store in cache
49+ this . cache . set ( key , { data, expiresAt } )
50+
51+ this . logger . info ( {
52+ msg : `Cached data for key: ${ key } ` ,
53+ expiresAt : new Date ( expiresAt ) . toISOString ( ) ,
54+ ttlDays
55+ } )
56+
57+ return data
58+ } catch ( error ) {
59+ this . logger . error ( { err : error , msg : `Failed to fetch data for key: ${ key } ` } )
60+ throw error
61+ }
62+ }
63+
64+ /**
65+ * Convenience method for fetching HTTP resources with caching
66+ */
67+ async fetchHttpWithCache (
68+ url : string ,
69+ options : CacheOptions & { responseType ?: 'json' | 'text' } = { }
70+ ) : Promise < any > {
71+ const { responseType = 'json' , ...cacheOptions } = options
72+
73+ return this . fetchWithCache (
74+ url ,
75+ async ( ) => {
76+ this . logger . info ( { msg : `Fetching HTTP resource: ${ url } ` } )
77+ const response = await fetch ( url )
78+
79+ if ( ! response . ok ) {
80+ throw new Error ( `HTTP error! status: ${ response . status } ${ response . statusText } ` )
81+ }
82+
83+ const data = responseType === 'json' ? await response . json ( ) : await response . text ( )
84+ this . logger . info ( { msg : `Successfully fetched HTTP resource: ${ url } ` } )
85+ return data
86+ } ,
87+ cacheOptions
88+ )
89+ }
90+
91+ /**
92+ * Clear a specific cache entry
93+ */
94+ clearCache ( key : string ) : void {
95+ this . cache . delete ( key )
96+ this . logger . info ( { msg : `Cleared cache for key: ${ key } ` } )
97+ }
98+
99+ /**
100+ * Clear all expired cache entries
101+ */
102+ clearExpiredCache ( ) : void {
103+ const now = Date . now ( )
104+ let clearedCount = 0
105+
106+ for ( const [ key , entry ] of this . cache . entries ( ) ) {
107+ if ( entry . expiresAt <= now ) {
108+ this . cache . delete ( key )
109+ clearedCount ++
110+ }
111+ }
112+
113+ if ( clearedCount > 0 ) {
114+ this . logger . info ( { msg : `Cleared ${ clearedCount } expired cache entries` } )
115+ }
116+ }
117+
118+ /**
119+ * Get cache statistics
120+ */
121+ getCacheStats ( ) : { size : number ; keys : string [ ] } {
122+ return {
123+ size : this . cache . size ,
124+ keys : Array . from ( this . cache . keys ( ) )
125+ }
126+ }
127+ }
0 commit comments