mirror of
https://github.com/Ark0N/Codeman.git
synced 2026-10-03 14:09:42 +02:00
feat: add StaleExpirationMap utility for TTL-based cache expiration
- Automatically removes entries not accessed within TTL - Periodic cleanup with configurable interval - Optional onExpire callback for cleanup notifications - Refresh TTL on get (configurable) - Touch, peek, getAge, getRemainingTtl methods - Full iteration support - Implements Disposable interface Useful for caching ephemeral data like pending tool calls, subagent activity. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,3 +9,4 @@
|
||||
export { BufferAccumulator } from './buffer-accumulator.js';
|
||||
export { LRUMap, type LRUMapOptions } from './lru-map.js';
|
||||
export { CleanupManager, type TimerOptions } from './cleanup-manager.js';
|
||||
export { StaleExpirationMap, type StaleExpirationMapOptions } from './stale-expiration-map.js';
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
/**
|
||||
* @fileoverview Map with automatic TTL-based entry expiration.
|
||||
*
|
||||
* StaleExpirationMap automatically removes entries that haven't been accessed
|
||||
* within a configurable TTL (time-to-live). Useful for caches and tracking
|
||||
* ephemeral data that should be cleaned up after a period of inactivity.
|
||||
*
|
||||
* @module utils/stale-expiration-map
|
||||
*/
|
||||
|
||||
import type { Disposable } from '../types.js';
|
||||
|
||||
/**
|
||||
* Entry wrapper that tracks last access time.
|
||||
*/
|
||||
interface TimedEntry<V> {
|
||||
value: V;
|
||||
lastAccessedAt: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration options for StaleExpirationMap.
|
||||
*/
|
||||
export interface StaleExpirationMapOptions<K, V> {
|
||||
/** Time-to-live in milliseconds before entries expire */
|
||||
ttlMs: number;
|
||||
/** How often to run cleanup (default: ttlMs / 2) */
|
||||
cleanupIntervalMs?: number;
|
||||
/** Optional callback when an entry expires */
|
||||
onExpire?: (key: K, value: V) => void;
|
||||
/** Whether to refresh TTL on get (default: true) */
|
||||
refreshOnGet?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* A Map that automatically expires entries after a TTL.
|
||||
*
|
||||
* Entries are removed if not accessed within the TTL period.
|
||||
* Periodic cleanup runs to remove expired entries.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const cache = new StaleExpirationMap<string, object>({
|
||||
* ttlMs: 5 * 60 * 1000, // 5 minutes
|
||||
* onExpire: (key, value) => console.log(`Expired: ${key}`)
|
||||
* });
|
||||
*
|
||||
* cache.set('key1', { data: 'value' });
|
||||
* // After 5 minutes of no access, 'key1' will be automatically removed
|
||||
* ```
|
||||
*/
|
||||
export class StaleExpirationMap<K, V> implements Disposable {
|
||||
private entries = new Map<K, TimedEntry<V>>();
|
||||
private cleanupTimer: NodeJS.Timeout | null = null;
|
||||
private _isDisposed = false;
|
||||
|
||||
private readonly ttlMs: number;
|
||||
private readonly cleanupIntervalMs: number;
|
||||
private readonly onExpire?: (key: K, value: V) => void;
|
||||
private readonly refreshOnGet: boolean;
|
||||
|
||||
/**
|
||||
* Creates a new StaleExpirationMap.
|
||||
*
|
||||
* @param options - Configuration options
|
||||
*/
|
||||
constructor(options: StaleExpirationMapOptions<K, V>) {
|
||||
this.ttlMs = options.ttlMs;
|
||||
this.cleanupIntervalMs = options.cleanupIntervalMs ?? Math.floor(options.ttlMs / 2);
|
||||
this.onExpire = options.onExpire;
|
||||
this.refreshOnGet = options.refreshOnGet ?? true;
|
||||
|
||||
// Start periodic cleanup
|
||||
this.startCleanup();
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether this map has been disposed.
|
||||
*/
|
||||
get isDisposed(): boolean {
|
||||
return this._isDisposed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Number of entries in the map.
|
||||
*/
|
||||
get size(): number {
|
||||
return this.entries.size;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set a value with automatic TTL tracking.
|
||||
*
|
||||
* @param key - Key to set
|
||||
* @param value - Value to associate
|
||||
* @returns this (for chaining)
|
||||
*/
|
||||
set(key: K, value: V): this {
|
||||
if (this._isDisposed) return this;
|
||||
|
||||
const now = Date.now();
|
||||
this.entries.set(key, {
|
||||
value,
|
||||
lastAccessedAt: now,
|
||||
createdAt: now,
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a value, optionally refreshing its TTL.
|
||||
*
|
||||
* @param key - Key to look up
|
||||
* @returns Value if found and not expired, undefined otherwise
|
||||
*/
|
||||
get(key: K): V | undefined {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return undefined;
|
||||
|
||||
// Check if expired
|
||||
if (this.isExpired(entry)) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// Refresh access time if configured
|
||||
if (this.refreshOnGet) {
|
||||
entry.lastAccessedAt = Date.now();
|
||||
}
|
||||
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key exists and is not expired.
|
||||
*
|
||||
* @param key - Key to check
|
||||
* @returns True if key exists and is not expired
|
||||
*/
|
||||
has(key: K): boolean {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return false;
|
||||
|
||||
if (this.isExpired(entry)) {
|
||||
this.delete(key);
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Peek at a value without refreshing its TTL.
|
||||
*
|
||||
* @param key - Key to peek
|
||||
* @returns Value if found and not expired, undefined otherwise
|
||||
*/
|
||||
peek(key: K): V | undefined {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return undefined;
|
||||
|
||||
if (this.isExpired(entry)) {
|
||||
this.delete(key);
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return entry.value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete an entry.
|
||||
*
|
||||
* @param key - Key to delete
|
||||
* @returns True if entry existed and was deleted
|
||||
*/
|
||||
delete(key: K): boolean {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return false;
|
||||
|
||||
this.entries.delete(key);
|
||||
// Don't call onExpire for manual deletes
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all entries.
|
||||
*/
|
||||
clear(): void {
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Touch an entry to refresh its TTL without returning the value.
|
||||
*
|
||||
* @param key - Key to touch
|
||||
* @returns True if entry exists
|
||||
*/
|
||||
touch(key: K): boolean {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry || this.isExpired(entry)) return false;
|
||||
|
||||
entry.lastAccessedAt = Date.now();
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the age of an entry in milliseconds.
|
||||
*
|
||||
* @param key - Key to check
|
||||
* @returns Age in ms, or undefined if not found
|
||||
*/
|
||||
getAge(key: K): number | undefined {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return undefined;
|
||||
return Date.now() - entry.createdAt;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get remaining TTL for an entry in milliseconds.
|
||||
*
|
||||
* @param key - Key to check
|
||||
* @returns Remaining TTL in ms, or undefined if not found/expired
|
||||
*/
|
||||
getRemainingTtl(key: K): number | undefined {
|
||||
const entry = this.entries.get(key);
|
||||
if (!entry) return undefined;
|
||||
|
||||
const elapsed = Date.now() - entry.lastAccessedAt;
|
||||
const remaining = this.ttlMs - elapsed;
|
||||
return remaining > 0 ? remaining : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Iterate over all non-expired entries.
|
||||
*/
|
||||
*[Symbol.iterator](): IterableIterator<[K, V]> {
|
||||
for (const [key, entry] of this.entries) {
|
||||
if (!this.isExpired(entry)) {
|
||||
yield [key, entry.value];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all keys (non-expired).
|
||||
*/
|
||||
keys(): IterableIterator<K> {
|
||||
const self = this;
|
||||
return (function* () {
|
||||
for (const [key, entry] of self.entries) {
|
||||
if (!self.isExpired(entry)) {
|
||||
yield key;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all values (non-expired).
|
||||
*/
|
||||
values(): IterableIterator<V> {
|
||||
const self = this;
|
||||
return (function* () {
|
||||
for (const [, entry] of self.entries) {
|
||||
if (!self.isExpired(entry)) {
|
||||
yield entry.value;
|
||||
}
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
/**
|
||||
* Run cleanup immediately, removing all expired entries.
|
||||
*
|
||||
* @returns Number of entries removed
|
||||
*/
|
||||
cleanup(): number {
|
||||
let removed = 0;
|
||||
const now = Date.now();
|
||||
|
||||
for (const [key, entry] of this.entries) {
|
||||
if (now - entry.lastAccessedAt > this.ttlMs) {
|
||||
this.entries.delete(key);
|
||||
this.onExpire?.(key, entry.value);
|
||||
removed++;
|
||||
}
|
||||
}
|
||||
|
||||
return removed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispose the map, stopping cleanup timer and clearing entries.
|
||||
*/
|
||||
dispose(): void {
|
||||
if (this._isDisposed) return;
|
||||
this._isDisposed = true;
|
||||
|
||||
this.stopCleanup();
|
||||
this.entries.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an entry has expired.
|
||||
*/
|
||||
private isExpired(entry: TimedEntry<V>): boolean {
|
||||
return Date.now() - entry.lastAccessedAt > this.ttlMs;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the periodic cleanup timer.
|
||||
*/
|
||||
private startCleanup(): void {
|
||||
if (this.cleanupTimer) return;
|
||||
|
||||
this.cleanupTimer = setInterval(() => {
|
||||
if (!this._isDisposed) {
|
||||
this.cleanup();
|
||||
}
|
||||
}, this.cleanupIntervalMs);
|
||||
|
||||
// Don't prevent process exit
|
||||
this.cleanupTimer.unref();
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the periodic cleanup timer.
|
||||
*/
|
||||
private stopCleanup(): void {
|
||||
if (this.cleanupTimer) {
|
||||
clearInterval(this.cleanupTimer);
|
||||
this.cleanupTimer = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default StaleExpirationMap;
|
||||
@@ -0,0 +1,343 @@
|
||||
/**
|
||||
* Tests for StaleExpirationMap utility.
|
||||
*
|
||||
* Port: N/A (unit tests, no server)
|
||||
*/
|
||||
|
||||
import { StaleExpirationMap } from '../../src/utils/stale-expiration-map.js';
|
||||
|
||||
describe('StaleExpirationMap', () => {
|
||||
describe('basic operations', () => {
|
||||
it('should set and get values', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
map.set('b', 2);
|
||||
|
||||
expect(map.get('a')).toBe(1);
|
||||
expect(map.get('b')).toBe(2);
|
||||
expect(map.get('c')).toBeUndefined();
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should check has correctly', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
|
||||
expect(map.has('a')).toBe(true);
|
||||
expect(map.has('b')).toBe(false);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should delete values', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
|
||||
expect(map.delete('a')).toBe(true);
|
||||
expect(map.delete('a')).toBe(false);
|
||||
expect(map.has('a')).toBe(false);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should clear all values', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
map.set('b', 2);
|
||||
|
||||
map.clear();
|
||||
|
||||
expect(map.size).toBe(0);
|
||||
expect(map.has('a')).toBe(false);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should report size correctly', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
expect(map.size).toBe(0);
|
||||
|
||||
map.set('a', 1);
|
||||
expect(map.size).toBe(1);
|
||||
|
||||
map.set('b', 2);
|
||||
expect(map.size).toBe(2);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('expiration', () => {
|
||||
it('should expire entries after TTL', async () => {
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 100,
|
||||
cleanupIntervalMs: 50,
|
||||
});
|
||||
|
||||
map.set('a', 1);
|
||||
expect(map.get('a')).toBe(1);
|
||||
|
||||
// Wait for expiration
|
||||
await new Promise(r => setTimeout(r, 150));
|
||||
|
||||
expect(map.get('a')).toBeUndefined();
|
||||
expect(map.has('a')).toBe(false);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should call onExpire callback when entry expires', async () => {
|
||||
const expired: Array<[string, number]> = [];
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 100,
|
||||
cleanupIntervalMs: 50,
|
||||
onExpire: (key, value) => expired.push([key, value]),
|
||||
});
|
||||
|
||||
map.set('a', 1);
|
||||
map.set('b', 2);
|
||||
|
||||
// Wait for cleanup
|
||||
await new Promise(r => setTimeout(r, 200));
|
||||
|
||||
expect(expired).toContainEqual(['a', 1]);
|
||||
expect(expired).toContainEqual(['b', 2]);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should refresh TTL on get when refreshOnGet is true', async () => {
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 150,
|
||||
cleanupIntervalMs: 50,
|
||||
refreshOnGet: true,
|
||||
});
|
||||
|
||||
map.set('a', 1);
|
||||
|
||||
// Access at 75ms (halfway through TTL)
|
||||
await new Promise(r => setTimeout(r, 75));
|
||||
expect(map.get('a')).toBe(1); // This refreshes TTL
|
||||
|
||||
// Wait another 100ms (entry should still be valid because TTL was refreshed)
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
expect(map.get('a')).toBe(1);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should not refresh TTL on get when refreshOnGet is false', async () => {
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 100,
|
||||
cleanupIntervalMs: 50,
|
||||
refreshOnGet: false,
|
||||
});
|
||||
|
||||
map.set('a', 1);
|
||||
|
||||
// Access at 50ms
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect(map.get('a')).toBe(1); // Does NOT refresh TTL
|
||||
|
||||
// Wait another 75ms (entry should be expired)
|
||||
await new Promise(r => setTimeout(r, 75));
|
||||
expect(map.get('a')).toBeUndefined();
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('peek and touch', () => {
|
||||
it('should peek without refreshing TTL', async () => {
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 100,
|
||||
cleanupIntervalMs: 50,
|
||||
refreshOnGet: true,
|
||||
});
|
||||
|
||||
map.set('a', 1);
|
||||
|
||||
// Peek at 50ms
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
expect(map.peek('a')).toBe(1); // Does NOT refresh TTL
|
||||
|
||||
// Wait another 75ms (entry should be expired)
|
||||
await new Promise(r => setTimeout(r, 75));
|
||||
expect(map.peek('a')).toBeUndefined();
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should touch to refresh TTL', async () => {
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 150,
|
||||
cleanupIntervalMs: 50,
|
||||
});
|
||||
|
||||
map.set('a', 1);
|
||||
|
||||
// Touch at 75ms
|
||||
await new Promise(r => setTimeout(r, 75));
|
||||
expect(map.touch('a')).toBe(true);
|
||||
|
||||
// Wait another 100ms (entry should still be valid)
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
expect(map.has('a')).toBe(true);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should return false for touch on non-existent key', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
|
||||
expect(map.touch('nonexistent')).toBe(false);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('age and remaining TTL', () => {
|
||||
it('should return age of entry', async () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
|
||||
await new Promise(r => setTimeout(r, 50));
|
||||
|
||||
const age = map.getAge('a');
|
||||
expect(age).toBeDefined();
|
||||
expect(age!).toBeGreaterThanOrEqual(50);
|
||||
expect(age!).toBeLessThan(150);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should return undefined age for non-existent key', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
|
||||
expect(map.getAge('nonexistent')).toBeUndefined();
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should return remaining TTL', async () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 1000 });
|
||||
map.set('a', 1);
|
||||
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
const remaining = map.getRemainingTtl('a');
|
||||
expect(remaining).toBeDefined();
|
||||
expect(remaining!).toBeLessThanOrEqual(900);
|
||||
expect(remaining!).toBeGreaterThan(800);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('iteration', () => {
|
||||
it('should iterate over non-expired entries', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
map.set('b', 2);
|
||||
map.set('c', 3);
|
||||
|
||||
const entries = Array.from(map);
|
||||
expect(entries).toEqual([['a', 1], ['b', 2], ['c', 3]]);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should iterate over keys', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
map.set('b', 2);
|
||||
|
||||
const keys = Array.from(map.keys());
|
||||
expect(keys).toEqual(['a', 'b']);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
|
||||
it('should iterate over values', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.set('a', 1);
|
||||
map.set('b', 2);
|
||||
|
||||
const values = Array.from(map.values());
|
||||
expect(values).toEqual([1, 2]);
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('manual cleanup', () => {
|
||||
it('should remove expired entries on cleanup()', async () => {
|
||||
const expired: string[] = [];
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 50,
|
||||
cleanupIntervalMs: 10000, // Long interval so automatic cleanup doesn't run
|
||||
onExpire: (key) => expired.push(key),
|
||||
});
|
||||
|
||||
map.set('a', 1);
|
||||
map.set('b', 2);
|
||||
|
||||
// Wait for expiration
|
||||
await new Promise(r => setTimeout(r, 100));
|
||||
|
||||
// Manual cleanup
|
||||
const removed = map.cleanup();
|
||||
|
||||
expect(removed).toBe(2);
|
||||
expect(expired).toContain('a');
|
||||
expect(expired).toContain('b');
|
||||
|
||||
map.dispose();
|
||||
});
|
||||
});
|
||||
|
||||
describe('dispose', () => {
|
||||
it('should stop cleanup timer on dispose', async () => {
|
||||
let cleanupCount = 0;
|
||||
const originalClearInterval = global.clearInterval;
|
||||
let clearedIntervals = 0;
|
||||
global.clearInterval = ((id: NodeJS.Timeout) => {
|
||||
clearedIntervals++;
|
||||
originalClearInterval(id);
|
||||
}) as typeof global.clearInterval;
|
||||
|
||||
const map = new StaleExpirationMap<string, number>({
|
||||
ttlMs: 100,
|
||||
cleanupIntervalMs: 50,
|
||||
onExpire: () => cleanupCount++,
|
||||
});
|
||||
|
||||
map.dispose();
|
||||
|
||||
// Restore original
|
||||
global.clearInterval = originalClearInterval;
|
||||
|
||||
expect(clearedIntervals).toBeGreaterThan(0);
|
||||
expect(map.isDisposed).toBe(true);
|
||||
});
|
||||
|
||||
it('should be idempotent', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
|
||||
map.dispose();
|
||||
map.dispose();
|
||||
map.dispose();
|
||||
|
||||
expect(map.isDisposed).toBe(true);
|
||||
});
|
||||
|
||||
it('should not allow new entries after dispose', () => {
|
||||
const map = new StaleExpirationMap<string, number>({ ttlMs: 10000 });
|
||||
map.dispose();
|
||||
|
||||
map.set('a', 1);
|
||||
expect(map.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user