@@ -30,9 +30,20 @@ export interface TtlCacheSetOptions {
3030 * @experimental **UNSTABLE**: New API, yet to be vetted.
3131 */
3232export interface TtlCacheOptions < K , V > {
33+ /**
34+ * Maximum number of entries the cache may hold. When a new entry would
35+ * exceed this limit, the entry that was
36+ * {@linkcode TtlCache.prototype.set | set()} least recently is evicted
37+ * before the new one is added. Must be a positive integer when provided.
38+ */
39+ maxSize ?: number ;
3340 /**
3441 * Callback invoked when an entry is removed, whether by TTL expiry,
35- * manual deletion, or clearing the cache.
42+ * capacity eviction, manual deletion, or clearing the cache. The entry is
43+ * already removed from the cache when this callback fires. Overwriting an
44+ * existing key via {@linkcode TtlCache.prototype.set | set()} does **not**
45+ * trigger this callback. The cache is not re-entrant during this callback:
46+ * calling `set`, `delete`, or `clear` will throw.
3647 */
3748 onEject ?: ( ejectedKey : K , ejectedValue : V ) => void ;
3849 /**
@@ -93,7 +104,9 @@ export interface TtlCacheOptions<K, V> {
93104export class TtlCache < K , V > extends Map < K , V >
94105 implements MemoizationCache < K , V > {
95106 #defaultTtl: number ;
107+ #maxSize: number ;
96108 #timeouts = new Map < K , number > ( ) ;
109+ #ejecting = false ;
97110 #eject?: ( ( ejectedKey : K , ejectedValue : V ) => void ) | undefined ;
98111 #slidingExpiration: boolean ;
99112 #entryTtls?: Map < K , number > ;
@@ -119,7 +132,14 @@ export class TtlCache<K, V> extends Map<K, V>
119132 `Cannot create TtlCache: defaultTtl must be a finite, non-negative number: received ${ defaultTtl } ` ,
120133 ) ;
121134 }
135+ const maxSize = options ?. maxSize ;
136+ if ( maxSize !== undefined && ( ! Number . isInteger ( maxSize ) || maxSize < 1 ) ) {
137+ throw new RangeError (
138+ `Cannot create TtlCache: maxSize must be a positive integer: received ${ maxSize } ` ,
139+ ) ;
140+ }
122141 this . #defaultTtl = defaultTtl ;
142+ this . #maxSize = maxSize ?? Infinity ;
123143 this . #eject = options ?. onEject ;
124144 this . #slidingExpiration = options ?. slidingExpiration ?? false ;
125145 if ( this . #slidingExpiration) {
@@ -128,6 +148,27 @@ export class TtlCache<K, V> extends Map<K, V>
128148 }
129149 }
130150
151+ /**
152+ * The maximum number of entries the cache may hold, or `Infinity` if no
153+ * limit was set.
154+ *
155+ * @experimental **UNSTABLE**: New API, yet to be vetted.
156+ *
157+ * @returns The maximum number of entries in the cache.
158+ *
159+ * @example Usage
160+ * ```ts
161+ * import { TtlCache } from "@std/cache/ttl-cache";
162+ * import { assertEquals } from "@std/assert/equals";
163+ *
164+ * const cache = new TtlCache<string, number>(1000, { maxSize: 5 });
165+ * assertEquals(cache.maxSize, 5);
166+ * ```
167+ */
168+ get maxSize ( ) : number {
169+ return this . #maxSize;
170+ }
171+
131172 /**
132173 * Set a value in the cache.
133174 *
@@ -158,6 +199,11 @@ export class TtlCache<K, V> extends Map<K, V>
158199 value : V ,
159200 options ?: TtlCacheSetOptions ,
160201 ) : this {
202+ if ( this . #ejecting) {
203+ throw new TypeError (
204+ "Cannot set entry in TtlCache: cache is not re-entrant during onEject callbacks" ,
205+ ) ;
206+ }
161207 if ( options ?. absoluteExpiration !== undefined && ! this . #slidingExpiration) {
162208 throw new TypeError (
163209 "Cannot set entry in TtlCache: absoluteExpiration requires slidingExpiration to be enabled" ,
@@ -178,8 +224,14 @@ export class TtlCache<K, V> extends Map<K, V>
178224 ) ;
179225 }
180226
227+ const isNew = ! super . has ( key ) ;
228+ if ( isNew && this . size >= this . #maxSize) {
229+ this . delete ( super . keys ( ) . next ( ) . value as K ) ;
230+ }
231+
181232 const existing = this . #timeouts. get ( key ) ;
182233 if ( existing !== undefined ) clearTimeout ( existing ) ;
234+ super . delete ( key ) ;
183235 super . set ( key , value ) ;
184236 this . #timeouts. set ( key , setTimeout ( ( ) => this . delete ( key ) , ttl ) ) ;
185237
@@ -285,6 +337,11 @@ export class TtlCache<K, V> extends Map<K, V>
285337 * ```
286338 */
287339 override delete ( key : K ) : boolean {
340+ if ( this . #ejecting) {
341+ throw new TypeError (
342+ "Cannot delete entry in TtlCache: cache is not re-entrant during onEject callbacks" ,
343+ ) ;
344+ }
288345 const value = super . get ( key ) ;
289346 const existed = super . delete ( key ) ;
290347 if ( ! existed ) return false ;
@@ -294,7 +351,14 @@ export class TtlCache<K, V> extends Map<K, V>
294351 this . #timeouts. delete ( key ) ;
295352 this . #entryTtls?. delete ( key ) ;
296353 this . #absoluteDeadlines?. delete ( key ) ;
297- this . #eject?.( key , value ! ) ;
354+ if ( this . #eject) {
355+ this . #ejecting = true ;
356+ try {
357+ this . #eject( key , value ! ) ;
358+ } finally {
359+ this . #ejecting = false ;
360+ }
361+ }
298362 return true ;
299363 }
300364
@@ -317,6 +381,11 @@ export class TtlCache<K, V> extends Map<K, V>
317381 * ```
318382 */
319383 override clear ( ) : void {
384+ if ( this . #ejecting) {
385+ throw new TypeError (
386+ "Cannot clear TtlCache: cache is not re-entrant during onEject callbacks" ,
387+ ) ;
388+ }
320389 for ( const timeout of this . #timeouts. values ( ) ) {
321390 clearTimeout ( timeout ) ;
322391 }
@@ -325,13 +394,19 @@ export class TtlCache<K, V> extends Map<K, V>
325394 this . #absoluteDeadlines?. clear ( ) ;
326395 const entries = [ ...super . entries ( ) ] ;
327396 super . clear ( ) ;
397+ if ( ! this . #eject) return ;
398+ this . #ejecting = true ;
328399 let error : unknown ;
329- for ( const [ key , value ] of entries ) {
330- try {
331- this . #eject?.( key , value ) ;
332- } catch ( e ) {
333- error ??= e ;
400+ try {
401+ for ( const [ key , value ] of entries ) {
402+ try {
403+ this . #eject( key , value ) ;
404+ } catch ( e ) {
405+ error ??= e ;
406+ }
334407 }
408+ } finally {
409+ this . #ejecting = false ;
335410 }
336411 if ( error !== undefined ) throw error ;
337412 }
@@ -361,9 +436,7 @@ export class TtlCache<K, V> extends Map<K, V>
361436 }
362437
363438 #resetTtl( key : K ) : void {
364- const ttl = this . #entryTtls! . get ( key ) ;
365- if ( ttl === undefined ) return ;
366-
439+ const ttl = this . #entryTtls! . get ( key ) ! ;
367440 const deadline = this . #absoluteDeadlines! . get ( key ) ;
368441 const effectiveTtl = deadline !== undefined
369442 ? Math . min ( ttl , Math . max ( 0 , deadline - Date . now ( ) ) )
0 commit comments