@@ -146,4 +146,253 @@ describe('TTLCache', () => {
146146 clearIntervalSpy . mockRestore ( ) ;
147147 } ) ;
148148 } ) ;
149+
150+ describe ( 'has()' , ( ) => {
151+ it ( 'returns true for a valid key' , ( ) => {
152+ const cache = new TTLCache < string > ( ) ;
153+ cache . set ( 'user' , 'octocat' , 60_000 ) ;
154+ expect ( cache . has ( 'user' ) ) . toBe ( true ) ;
155+ cache . destroy ( ) ;
156+ } ) ;
157+
158+ it ( 'returns false for a missing key' , ( ) => {
159+ const cache = new TTLCache < string > ( ) ;
160+ expect ( cache . has ( 'missing' ) ) . toBe ( false ) ;
161+ cache . destroy ( ) ;
162+ } ) ;
163+
164+ it ( 'returns false for an expired key' , ( ) => {
165+ vi . useFakeTimers ( ) ;
166+ const cache = new TTLCache < string > ( ) ;
167+ cache . set ( 'user' , 'octocat' , 1_000 ) ;
168+ vi . advanceTimersByTime ( 2_000 ) ;
169+ expect ( cache . has ( 'user' ) ) . toBe ( false ) ;
170+ cache . destroy ( ) ;
171+ } ) ;
172+ } ) ;
173+
174+ describe ( 'delete()' , ( ) => {
175+ it ( 'removes an existing key and returns true' , ( ) => {
176+ const cache = new TTLCache < string > ( ) ;
177+ cache . set ( 'user' , 'octocat' , 60_000 ) ;
178+ expect ( cache . delete ( 'user' ) ) . toBe ( true ) ;
179+ expect ( cache . get ( 'user' ) ) . toBeNull ( ) ;
180+ cache . destroy ( ) ;
181+ } ) ;
182+
183+ it ( 'returns false when deleting a missing key' , ( ) => {
184+ const cache = new TTLCache < string > ( ) ;
185+ expect ( cache . delete ( 'missing' ) ) . toBe ( false ) ;
186+ cache . destroy ( ) ;
187+ } ) ;
188+
189+ it ( 'still removes an expired key from store but returns true (key exists)' , ( ) => {
190+ vi . useFakeTimers ( ) ;
191+ const cache = new TTLCache < string > ( ) ;
192+ cache . set ( 'user' , 'octocat' , 1_000 ) ;
193+ vi . advanceTimersByTime ( 2_000 ) ;
194+ // Key still exists in store even though expired, so delete returns true
195+ expect ( cache . delete ( 'user' ) ) . toBe ( true ) ;
196+ cache . destroy ( ) ;
197+ } ) ;
198+ } ) ;
199+
200+ describe ( 'TTL expiry behavior' , ( ) => {
201+ it ( 'returns value before TTL expiry' , ( ) => {
202+ vi . useFakeTimers ( ) ;
203+ const cache = new TTLCache < string > ( ) ;
204+ cache . set ( 'user' , 'octocat' , 5_000 ) ;
205+
206+ // Check at 1 second (before expiry at 5 seconds)
207+ vi . advanceTimersByTime ( 1_000 ) ;
208+ expect ( cache . get ( 'user' ) ) . toBe ( 'octocat' ) ;
209+
210+ // Check at 4 seconds (still before expiry)
211+ vi . advanceTimersByTime ( 3_000 ) ;
212+ expect ( cache . get ( 'user' ) ) . toBe ( 'octocat' ) ;
213+
214+ cache . destroy ( ) ;
215+ } ) ;
216+
217+ it ( 'returns value at exactly TTL time (not yet expired)' , ( ) => {
218+ vi . useFakeTimers ( ) ;
219+ const cache = new TTLCache < string > ( ) ;
220+ cache . set ( 'user' , 'octocat' , 5_000 ) ;
221+
222+ // Advance exactly to TTL expiry time
223+ // At this point Date.now() === expiresAt, so > check fails and value is returned
224+ vi . advanceTimersByTime ( 5_000 ) ;
225+ expect ( cache . get ( 'user' ) ) . toBe ( 'octocat' ) ;
226+
227+ cache . destroy ( ) ;
228+ } ) ;
229+
230+ it ( 'returns null after passing TTL expiry' , ( ) => {
231+ vi . useFakeTimers ( ) ;
232+ const cache = new TTLCache < string > ( ) ;
233+ cache . set ( 'user' , 'octocat' , 5_000 ) ;
234+
235+ // Advance just past TTL expiry time
236+ vi . advanceTimersByTime ( 5_001 ) ;
237+ expect ( cache . get ( 'user' ) ) . toBeNull ( ) ;
238+
239+ cache . destroy ( ) ;
240+ } ) ;
241+ } ) ;
242+
243+ describe ( 'overwriting keys resets TTL' , ( ) => {
244+ it ( 'resets TTL when overwriting an existing key' , ( ) => {
245+ vi . useFakeTimers ( ) ;
246+ const cache = new TTLCache < string > ( ) ;
247+ cache . set ( 'user' , 'octocat' , 5_000 ) ;
248+
249+ // Advance to 3 seconds (before expiry)
250+ vi . advanceTimersByTime ( 3_000 ) ;
251+
252+ // Overwrite the key with a new 5-second TTL
253+ cache . set ( 'user' , 'new-octocat' , 5_000 ) ;
254+
255+ // Advance another 3 seconds (total 6 seconds, but only 3 since last set)
256+ vi . advanceTimersByTime ( 3_000 ) ;
257+
258+ // Should still be available because TTL was reset
259+ expect ( cache . get ( 'user' ) ) . toBe ( 'new-octocat' ) ;
260+
261+ cache . destroy ( ) ;
262+ } ) ;
263+
264+ it ( 'expires after new TTL when overwritten' , ( ) => {
265+ vi . useFakeTimers ( ) ;
266+ const cache = new TTLCache < string > ( ) ;
267+ cache . set ( 'user' , 'octocat' , 5_000 ) ;
268+
269+ // Advance to 3 seconds
270+ vi . advanceTimersByTime ( 3_000 ) ;
271+
272+ // Overwrite with new 2-second TTL
273+ cache . set ( 'user' , 'new-octocat' , 2_000 ) ;
274+
275+ // Advance another 3 seconds (total 6 from start, 3 from new set)
276+ vi . advanceTimersByTime ( 3_000 ) ;
277+
278+ // Should be expired because new TTL (2s) has passed
279+ expect ( cache . get ( 'user' ) ) . toBeNull ( ) ;
280+
281+ cache . destroy ( ) ;
282+ } ) ;
283+ } ) ;
284+
285+ describe ( 'storing different data types' , ( ) => {
286+ it ( 'stores and retrieves string values' , ( ) => {
287+ const cache = new TTLCache < string > ( ) ;
288+ cache . set ( 'msg' , 'hello world' , 60_000 ) ;
289+ expect ( cache . get ( 'msg' ) ) . toBe ( 'hello world' ) ;
290+ cache . destroy ( ) ;
291+ } ) ;
292+
293+ it ( 'stores and retrieves number values' , ( ) => {
294+ const cache = new TTLCache < number > ( ) ;
295+ cache . set ( 'count' , 42 , 60_000 ) ;
296+ expect ( cache . get ( 'count' ) ) . toBe ( 42 ) ;
297+ cache . destroy ( ) ;
298+ } ) ;
299+
300+ it ( 'stores and retrieves boolean values' , ( ) => {
301+ const cache = new TTLCache < boolean > ( ) ;
302+ cache . set ( 'flag' , true , 60_000 ) ;
303+ expect ( cache . get ( 'flag' ) ) . toBe ( true ) ;
304+ cache . destroy ( ) ;
305+ } ) ;
306+
307+ it ( 'stores and retrieves object values' , ( ) => {
308+ const cache = new TTLCache < { name : string ; age : number } > ( ) ;
309+ const user = { name : 'Alice' , age : 30 } ;
310+ cache . set ( 'user' , user , 60_000 ) ;
311+ expect ( cache . get ( 'user' ) ) . toEqual ( user ) ;
312+ cache . destroy ( ) ;
313+ } ) ;
314+
315+ it ( 'stores and retrieves array values' , ( ) => {
316+ const cache = new TTLCache < string [ ] > ( ) ;
317+ const tags = [ 'javascript' , 'typescript' , 'vitest' ] ;
318+ cache . set ( 'tags' , tags , 60_000 ) ;
319+ expect ( cache . get ( 'tags' ) ) . toEqual ( tags ) ;
320+ cache . destroy ( ) ;
321+ } ) ;
322+
323+ it ( 'stores and retrieves nested object values' , ( ) => {
324+ const cache = new TTLCache < {
325+ user : { id : number ; name : string } ;
326+ metadata : { created : string } ;
327+ } > ( ) ;
328+ const data = {
329+ user : { id : 1 , name : 'Bob' } ,
330+ metadata : { created : '2024-01-01' } ,
331+ } ;
332+ cache . set ( 'data' , data , 60_000 ) ;
333+ expect ( cache . get ( 'data' ) ) . toEqual ( data ) ;
334+ cache . destroy ( ) ;
335+ } ) ;
336+
337+ it ( 'stores null values in different type context' , ( ) => {
338+ const cache = new TTLCache < string | null > ( ) ;
339+ cache . set ( 'nullable' , null , 60_000 ) ;
340+ expect ( cache . get ( 'nullable' ) ) . toBeNull ( ) ;
341+ cache . destroy ( ) ;
342+ } ) ;
343+ } ) ;
344+
345+ describe ( 'edge cases and error handling' , ( ) => {
346+ it ( 'throws RangeError when ttlMs is 0 or negative' , ( ) => {
347+ const cache = new TTLCache < string > ( ) ;
348+ expect ( ( ) => cache . set ( 'key' , 'value' , 0 ) ) . toThrow ( RangeError ) ;
349+ expect ( ( ) => cache . set ( 'key' , 'value' , - 1 ) ) . toThrow ( RangeError ) ;
350+ cache . destroy ( ) ;
351+ } ) ;
352+
353+ it ( 'handles rapid get/set/delete cycles' , ( ) => {
354+ const cache = new TTLCache < number > ( ) ;
355+ for ( let i = 0 ; i < 100 ; i ++ ) {
356+ cache . set ( `key-${ i } ` , i , 60_000 ) ;
357+ }
358+ for ( let i = 0 ; i < 100 ; i ++ ) {
359+ expect ( cache . get ( `key-${ i } ` ) ) . toBe ( i ) ;
360+ }
361+ for ( let i = 0 ; i < 100 ; i ++ ) {
362+ cache . delete ( `key-${ i } ` ) ;
363+ }
364+ for ( let i = 0 ; i < 100 ; i ++ ) {
365+ expect ( cache . get ( `key-${ i } ` ) ) . toBeNull ( ) ;
366+ }
367+ cache . destroy ( ) ;
368+ } ) ;
369+
370+ it ( 'handles very short TTL values (1ms)' , ( ) => {
371+ vi . useFakeTimers ( ) ;
372+ const cache = new TTLCache < string > ( ) ;
373+ cache . set ( 'short' , 'lived' , 1 ) ;
374+ // Immediately at creation time, should exist
375+ expect ( cache . get ( 'short' ) ) . toBe ( 'lived' ) ;
376+ // Advance 1ms
377+ vi . advanceTimersByTime ( 1 ) ;
378+ // Now it should be expired or at boundary
379+ // (depends on exact timing, but get() should handle it gracefully)
380+ const result = cache . get ( 'short' ) ;
381+ expect ( [ null , 'lived' ] ) . toContain ( result ) ;
382+ cache . destroy ( ) ;
383+ } ) ;
384+
385+ it ( 'multiple clear operations work correctly' , ( ) => {
386+ const cache = new TTLCache < string > ( ) ;
387+ cache . set ( 'a' , 'x' , 60_000 ) ;
388+ cache . clear ( ) ;
389+ expect ( cache . size ( ) ) . toBe ( 0 ) ;
390+
391+ cache . set ( 'b' , 'y' , 60_000 ) ;
392+ cache . clear ( ) ;
393+ expect ( cache . size ( ) ) . toBe ( 0 ) ;
394+
395+ cache . destroy ( ) ;
396+ } ) ;
397+ } ) ;
149398} ) ;
0 commit comments