80 lines
1.8 KiB
TypeScript
80 lines
1.8 KiB
TypeScript
import { pruneMapToMaxSize } from "./map-size.js";
|
|
|
|
export type DedupeCache = {
|
|
check: (key: string | undefined | null, now?: number) => boolean;
|
|
peek: (key: string | undefined | null, now?: number) => boolean;
|
|
clear: () => void;
|
|
size: () => number;
|
|
};
|
|
|
|
type DedupeCacheOptions = {
|
|
ttlMs: number;
|
|
maxSize: number;
|
|
};
|
|
|
|
export function createDedupeCache(options: DedupeCacheOptions): DedupeCache {
|
|
const ttlMs = Math.max(0, options.ttlMs);
|
|
const maxSize = Math.max(0, Math.floor(options.maxSize));
|
|
const cache = new Map<string, number>();
|
|
|
|
const touch = (key: string, now: number) => {
|
|
cache.delete(key);
|
|
cache.set(key, now);
|
|
};
|
|
|
|
const prune = (now: number) => {
|
|
const cutoff = ttlMs > 0 ? now - ttlMs : undefined;
|
|
if (cutoff !== undefined) {
|
|
for (const [entryKey, entryTs] of cache) {
|
|
if (entryTs < cutoff) {
|
|
cache.delete(entryKey);
|
|
}
|
|
}
|
|
}
|
|
if (maxSize <= 0) {
|
|
cache.clear();
|
|
return;
|
|
}
|
|
pruneMapToMaxSize(cache, maxSize);
|
|
};
|
|
|
|
const hasUnexpired = (key: string, now: number, touchOnRead: boolean): boolean => {
|
|
const existing = cache.get(key);
|
|
if (existing === undefined) {
|
|
return false;
|
|
}
|
|
if (ttlMs > 0 && now - existing >= ttlMs) {
|
|
cache.delete(key);
|
|
return false;
|
|
}
|
|
if (touchOnRead) {
|
|
touch(key, now);
|
|
}
|
|
return true;
|
|
};
|
|
|
|
return {
|
|
check: (key, now = Date.now()) => {
|
|
if (!key) {
|
|
return false;
|
|
}
|
|
if (hasUnexpired(key, now, true)) {
|
|
return true;
|
|
}
|
|
touch(key, now);
|
|
prune(now);
|
|
return false;
|
|
},
|
|
peek: (key, now = Date.now()) => {
|
|
if (!key) {
|
|
return false;
|
|
}
|
|
return hasUnexpired(key, now, false);
|
|
},
|
|
clear: () => {
|
|
cache.clear();
|
|
},
|
|
size: () => cache.size,
|
|
};
|
|
}
|