1+ //region Types
12export interface CacheOptions {
23 /**
34 * Enables caching
@@ -12,7 +13,9 @@ export interface CacheOptions {
1213 */
1314 logTiming ?: boolean
1415}
16+ //endregion
1517
18+ //region Cacheables
1619/**
1720 * Provides a simple in-memory cache with automatic or manual invalidation.
1821 */
@@ -27,58 +30,44 @@ export class Cacheables {
2730 this . logTiming = options ?. logTiming ?? false
2831 }
2932
30- private cache : Record <
31- string ,
32- {
33- timer ?: ReturnType < typeof setTimeout >
34- value : any
35- misses : number
36- hits : number
37- }
38- > = { }
39-
40- private clearValue ( key : string ) : void {
41- if ( this . cache [ key ] && this . cache [ key ] ?. value ) {
42- delete this . cache [ key ] ?. value
43- if ( this . log ) Logger . logInvalidatingCache ( key )
44- }
45- }
46-
47- private clearTimeout ( key : string ) : void {
48- if ( this . cache [ key ] && this . cache [ key ] ?. timer ) {
49- clearTimeout ( this . cache [ key ] ?. timer as ReturnType < typeof setTimeout > )
50- }
51- }
33+ private cacheables : Record < string , Cacheable < any > > = { }
5234
5335 /**
54- * Build a key by providing strings or numbers
36+ * Builds a key with the provided strings or numbers.
5537 * @param args
5638 */
5739 public static key ( ...args : ( string | number ) [ ] ) : string {
5840 return args . join ( ':' )
5941 }
6042
43+ /**
44+ * Deletes a cacheable.
45+ * @param key
46+ */
6147 public delete ( key : string ) : void {
62- this . clearTimeout ( key )
63- this . clearValue ( key )
48+ delete this . cacheables [ key ]
6449 }
6550
51+ /**
52+ * Clears the cache by deleting all cacheables.
53+ */
6654 public clear ( ) : void {
67- Object . keys ( this . cache ) . forEach ( this . delete . bind ( this ) )
55+ this . cacheables = { }
6856 }
6957
7058 /**
71- * Returns whether a value is present for a certain key
59+ * Returns whether a cacheable is present and valid (i.e., did not time out).
7260 */
7361 public isCached ( key : string ) : boolean {
74- return ! ! this . cache [ key ] ?. value
62+ const cacheable = this . cacheables [ key ]
63+ return ! ( ! cacheable || cacheable . timedOut )
7564 }
7665
7766 /**
7867 * Returns all the cache keys
7968 */
8069 public keys ( ) : string [ ] {
81- return Object . keys ( this . cache )
70+ return Object . keys ( this . cacheables )
8271 }
8372
8473 /**
@@ -111,9 +100,14 @@ export class Cacheables {
111100 return resource ( )
112101 }
113102
114- if ( this . log ) Logger . startLogTime ( key )
103+ // Persist log settings as this could be a race condition
104+ const { logTiming, log } = this
105+ if ( logTiming ) Logger . logTime ( key )
106+
115107 const result = await this . #cacheable( resource , key , timeout )
116- if ( this . log ) Logger . stopLogTime ( key )
108+
109+ if ( logTiming ) Logger . logTimeEnd ( key )
110+ if ( log ) Logger . logStats ( key , this . cacheables [ key ] )
117111
118112 return result
119113 }
@@ -123,78 +117,96 @@ export class Cacheables {
123117 key : string ,
124118 timeout ?: number ,
125119 ) : Promise < T > {
126- const storedResource = this . cache [ key ]
127-
128- if ( storedResource === undefined ) {
129- const value = await resource ( )
130- this . cache [ key ] = {
131- value,
132- hits : 0 ,
133- misses : 1 ,
134- timer : timeout
135- ? setTimeout ( ( ) => {
136- this . clearValue ( key )
137- } , timeout )
138- : undefined ,
139- }
140- if ( this . log ) Logger . logNewCacheable ( key )
141- return value
142- }
120+ const cacheable = this . cacheables [ key ] as Cacheable < T > | undefined
143121
144- const hasRetrievedValue = storedResource . value !== undefined
145- if ( hasRetrievedValue ) {
146- storedResource . hits += 1
147- if ( this . log ) Logger . logCacheHit ( key , storedResource . hits )
148- return storedResource . value as T
149- } else {
150- const value = await resource ( )
151- this . clearTimeout ( key )
152- if ( timeout ) {
153- storedResource . timer = setTimeout ( ( ) => {
154- this . clearValue ( key )
155- } , timeout )
156- }
157- storedResource . value = value
158- storedResource . misses += 1
159- if ( this . log ) Logger . logCacheMiss ( key , storedResource . misses )
160- return value
122+ if ( ! cacheable ) {
123+ this . cacheables [ key ] = await Cacheable . create ( resource , timeout )
124+ return this . cacheables [ key ] ?. value
161125 }
126+
127+ return await cacheable . touch ( resource , timeout )
162128 }
163129}
130+ //endregion
164131
165- class Logger {
166- static startLogTime ( key : string ) : void {
167- // eslint-disable-next-line no-console
168- console . time ( key )
132+ //region Cacheable
133+ /**
134+ * Helper class, can only be instantiated by calling its static
135+ * function `create`.
136+ */
137+ class Cacheable < T > {
138+ private timingOutAt : number | undefined
139+ public hits = 0
140+ public misses = 1
141+ public value : T
142+
143+ /**
144+ * Cacheable Factory function. The only way to get a cacheable.
145+ * @param resource
146+ * @param timeout
147+ */
148+ static async create < T > ( resource : ( ) => Promise < T > , timeout ?: number ) {
149+ const timesOutAt = timeout !== undefined ? Date . now ( ) + timeout : undefined
150+ const value = await resource ( )
151+ return new Cacheable ( value , timesOutAt )
169152 }
170153
171- static stopLogTime ( key : string ) : void {
172- // eslint-disable-next-line no-console
173- console . timeEnd ( key )
154+ private constructor ( value : T , timesOutAt ?: number ) {
155+ this . value = value
156+ this . timingOutAt = timesOutAt
174157 }
175158
176- static logDisabled ( ) : void {
177- // eslint-disable-next-line no-console
178- console . log ( 'CACHE: Caching disabled' )
159+ get timedOut ( ) : boolean {
160+ if ( this . timingOutAt === undefined ) return false
161+ return this . timingOutAt < Date . now ( )
179162 }
180163
181- static logCacheHit ( key : string , hits : number ) : void {
182- // eslint-disable-next-line no-console
183- console . log ( `CACHE HIT: "${ key } " found, hits: ${ hits } .` )
164+ /**
165+ * Get and set the value of the Cacheable.
166+ * Some tricky race are conditions going on here,
167+ * but this should behave as expected
168+ * @param resource
169+ * @param timeout
170+ */
171+ async touch ( resource : ( ) => Promise < T > , timeout ?: number ) : Promise < T > {
172+ if ( ! this . timedOut ) {
173+ this . hits += 1
174+ this . timingOutAt =
175+ timeout !== undefined ? Date . now ( ) + timeout : undefined
176+ return this . value
177+ }
178+ this . timingOutAt = timeout !== undefined ? Date . now ( ) + timeout : undefined
179+ this . value = await resource ( )
180+ this . misses += 1
181+ return this . value
184182 }
183+ }
184+ //endregion
185185
186- static logCacheMiss ( key : string , misses : number ) : void {
186+ //region Logger
187+ /**
188+ * Logger class with static logging functions.
189+ */
190+ class Logger {
191+ static logTime ( key : string ) : void {
187192 // eslint-disable-next-line no-console
188- console . log ( `CACHE MISS: " ${ key } " has no value, misses: ${ misses } .` )
193+ console . time ( key )
189194 }
190195
191- static logNewCacheable ( key : string ) : void {
196+ static logTimeEnd ( key : string ) : void {
192197 // eslint-disable-next-line no-console
193- console . log ( `CACHE MISS: " ${ key } " not in cache yet, caching.` )
198+ console . timeEnd ( key )
194199 }
195200
196- static logInvalidatingCache ( key : string ) : void {
201+ static logDisabled ( ) : void {
197202 // eslint-disable-next-line no-console
198- console . log ( `CACHE INVALIDATED: "${ key } " invalidated.` )
203+ console . log ( 'CACHE: Caching disabled' )
204+ }
205+
206+ static logStats ( key : string , cacheable : Cacheable < any > | undefined ) {
207+ if ( ! cacheable ) return
208+ const { hits, misses } = cacheable
209+ console . log ( `Cacheable "${ key } ": hits: ${ hits } , misses: ${ misses } ` )
199210 }
200211}
212+ //endregion
0 commit comments