Prompt
Implement a TypeScript LRU cache with per-entry TTL behind a single class: get/set/delete, a configurable max size that evicts least-recently-used entries, lazy expiry on read plus an explicit prune(), and no external dependencies. Include a short usage example. Return complete, runnable code.
Responses with Independent SoTA check: Kimi K3 vs Fable 5 vs GPT-5.6 Sol on a coding task (HN 48999291) (1 rows)
kimi-k3
moonshotai/kimi-k3
172.07sSlowest by 7x$0.110Priciest by 2x
Here's a complete implementation using a Map (which preserves insertion order) to get O(1) LRU operations with no dependencies:
/**
* LRU cache with per-entry TTL. No external dependencies.
*
* Run directly: npx ts-node lru-cache.ts
* Or compile: npx tsc --target es2020 lru-cache.ts && node lru-cache.js
*/
export interface LRUCacheOptions {
/** Maximum number of entries. The least-recently-used entry is evicted when exceeded. */
maxSize: number;
/** Default TTL in milliseconds for entries set without an explicit ttl. Omit for no expiry. */
ttl?: number;
}
interface CacheEntry<V> {
value: V;
/** Timestamp (ms since epoch) at which the entry expires. Infinity = never. */
expiresAt: number;
}
export class LRUCache<K, V> {
private readonly maxSize: number;
private readonly defaultTtl?: number;
// Map iteration order = insertion order: least-recently-used first.
private readonly cache = new Map<K, CacheEntry<V>>();
constructor(options: LRUCacheOptions) {
if (!Number.isInteger(options.maxSize) || options.maxSize < 1) {
throw new Error("maxSize must be a positive integer");
}
this.maxSize = options.maxSize;
this.defaultTtl = options.ttl;
}
/** Number of stored entries (may include expired entries not yet pruned/read). */
get size(): number {
return this.cache.size;
}
/** Returns the value and marks the key as most-recently-used; undefined if missing or expired. */
get(key: K): V | undefined {
const entry = this.cache.get(key);
if (entry === undefined) return undefined;
if (this.isExpired(entry)) {
this.cache.delete(key); // lazy expiry on read
return undefined;
}
// Re-insert at the end to mark as most-recently-used.
this.cache.delete(key);
this.cache.set(key, entry);
return entry.value;
}
/**
* Inserts or updates an entry. `ttl` (ms) overrides the default TTL for this
* entry; pass Infinity for an entry that never expires (ttl <= 0 expires
* immediately). Returns `this` for chaining.
*/
set(key: K, value: V, ttl?: number): this {
const effectiveTtl = ttl ?? this.defaultTtl;
const expiresAt =
effectiveTtl === undefined || !Number.isFinite(effectiveTtl)
? Infinity
: Date.now() + effectiveTtl;
// Delete first so an existing key's position is refreshed.
this.cache.delete(key);
this.cache.set(key, { value, expiresAt });
// Evict least-recently-used entries from the front of the Map.
while (this.cache.size > this.maxSize) {
const oldest = this.cache.keys().next();
if (oldest.done) break; // unreachable, but satisfies the compiler
this.cache.delete(oldest.value);
}
return this;
}
/** Removes the entry for `key`; returns true if it existed. */
delete(key: K): boolean {
return this.cache.delete(key);
}
/** True if a non-expired entry exists (does not affect recency). */
has(key: K): boolean {
const entry = this.cache.get(key);
if (entry === undefined) return false;
if (this.isExpired(entry)) {
this.cache.delete(key);
return false;
}
return true;
}
/** Removes all expired entries; returns the number removed. */
prune(): number {
const now = Date.now();
let removed = 0;
for (const [key, entry] of this.cache) {
if (entry.expiresAt <= now) {
this.cache.delete(key);
removed++;
}
}
return removed;
}
/** Removes all entries. */
clear(): void {
this.cache.clear();
}
private isExpired(entry: CacheEntry<V>): boolean {
return entry.expiresAt <= Date.now();
}
}
// ---------------------------------------------------------------------------
// Usage example (remove when using LRUCache as a library)
// ---------------------------------------------------------------------------
function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function main(): Promise<void> {
// --- LRU eviction by max size ---
const lru = new LRUCache<string, number>({ maxSize: 3 });
lru.set("a", 1).set("b", 2).set("c", 3);
lru.get("a"); // touch "a" → "b" is now least-recently-used
lru.set("d", 4); // exceeds maxSize → evicts "b"
console.log(lru.get("b")); // undefined (evicted)
console.log(lru.get("a")); // 1
console.log(lru.size); // 3
// --- Per-entry TTL ---
const cache = new LRUCache<string, number>({ maxSize: 100, ttl: 1_000 });
cache.set("session", 42, 100); // per-entry TTL overrides the default
cache.set("pinned", 7, Infinity); // never expires
await sleep(150);
console.log(cache.get("session")); // undefined — lazily expired on read
console.log(cache.get("pinned")); // 7
// --- Explicit prune (expired entries nobody reads still occupy space) ---
cache.set("ghost1", 1, 50);
cache.set("ghost2", 2, 50);
await sleep(60);
console.log(cache.size); // 3 (pinned + two expired ghosts)
console.log(cache.prune()); // 2 — expired entries removed
console.log(cache.size); // 1
// --- delete / has ---
cache.set("temp", 9);
cache.delete("temp");
console.log(cache.has("temp")); // false
}
main();
Design notes:
- LRU via
Mapordering: JavaScriptMaps iterate in insertion order. Everyget/setre-inserts the key at the end, so the first key is always the least-recently-used — giving O(1) get/set/evict with no extra bookkeeping. - Expiry semantics: an entry expires when
Date.now() >= setTime + ttl. Expired entries are removed lazily onget/has, or eagerly viaprune()(O(n)) — useful to call on a timer if many entries expire without being read. - TTL precedence: per-entry
ttlinset()> constructor defaultttl> no expiry.Infinityexplicitly pins an entry. - Eviction policy: when
maxSizeis exceeded, the strictly least-recently-used entry is evicted, regardless of whether some other entry is expired-but-unread (callprune()first if you prefer otherwise).
claude-fable-5
anthropic/claude-fable-5
22.24sFastest by 7x$0.107
LRU Cache with Per-Entry TTL
interface CacheEntry<V> {
value: V;
/** Absolute expiry timestamp in ms, or Infinity for no TTL. */
expiresAt: number;
}
interface LRUCacheOptions {
/** Maximum number of entries before LRU eviction kicks in. */
maxSize: number;
/** Default TTL in milliseconds applied when set() gets no explicit TTL. */
defaultTTL?: number;
}
class LRUCache<K, V> {
private readonly maxSize: number;
private readonly defaultTTL: number;
// Map preserves insertion order; we treat the *last* entry as most recently used.
private readonly store = new Map<K, CacheEntry<V>>();
constructor(options: LRUCacheOptions) {
if (!Number.isInteger(options.maxSize) || options.maxSize <= 0) {
throw new RangeError("maxSize must be a positive integer");
}
this.maxSize = options.maxSize;
this.defaultTTL = options.defaultTTL ?? Infinity;
}
/**
* Returns the value if present and not expired, otherwise undefined.
* Expired entries are removed lazily. A hit refreshes LRU recency.
*/
get(key: K): V | undefined {
const entry = this.store.get(key);
if (entry === undefined) return undefined;
if (this.isExpired(entry)) {
this.store.delete(key); // lazy expiry
return undefined;
}
// Refresh recency: move to the end of the Map.
this.store.delete(key);
this.store.set(key, entry);
return entry.value;
}
/**
* Inserts or updates a key. Optional ttl (ms) overrides the default.
* Evicts the least-recently-used entry if capacity is exceeded.
*/
set(key: K, value: V, ttl?: number): this {
const effectiveTTL = ttl ?? this.defaultTTL;
const expiresAt =
effectiveTTL === Infinity ? Infinity : Date.now() + effectiveTTL;
// Delete first so re-insertion moves the key to most-recent position.
this.store.delete(key);
this.store.set(key, { value, expiresAt });
if (this.store.size > this.maxSize) {
// Oldest entry = first key in Map iteration order.
const oldestKey = this.store.keys().next().value as K;
this.store.delete(oldestKey);
}
return this;
}
/** Removes a key. Returns true if it existed. */
delete(key: K): boolean {
return this.store.delete(key);
}
/** True if the key exists and is not expired (does not refresh recency). */
has(key: K): boolean {
const entry = this.store.get(key);
if (entry === undefined) return false;
if (this.isExpired(entry)) {
this.store.delete(key);
return false;
}
return true;
}
/** Eagerly removes all expired entries. Returns the number removed. */
prune(): number {
const now = Date.now();
let removed = 0;
for (const [key, entry] of this.store) {
if (entry.expiresAt <= now) {
this.store.delete(key);
removed++;
}
}
return removed;
}
/** Removes everything. */
clear(): void {
this.store.clear();
}
/** Current entry count (may include not-yet-pruned expired entries). */
get size(): number {
return this.store.size;
}
private isExpired(entry: CacheEntry<V>): boolean {
return entry.expiresAt <= Date.now();
}
}
// ---------------------------------------------------------------------------
// Usage example
// ---------------------------------------------------------------------------
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function main() {
const cache = new LRUCache<string, number>({ maxSize: 3, defaultTTL: 1000 });
cache.set("a", 1);
cache.set("b", 2);
cache.set("c", 3);
console.log(cache.get("a")); // 1 (also makes "a" most recently used)
cache.set("d", 4); // capacity exceeded -> evicts "b" (least recently used)
console.log(cache.get("b")); // undefined (evicted)
console.log(cache.get("d")); // 4
cache.set("short", 99, 100); // per-entry TTL of 100ms overrides default
await sleep(150);
console.log(cache.get("short")); // undefined (lazily expired on read)
cache.set("x", 10, 50);
await sleep(100);
console.log("size before prune:", cache.size); // may include expired "x"
console.log("pruned:", cache.prune()); // removes expired entries
console.log("size after prune:", cache.size);
cache.delete("a");
console.log(cache.has("a")); // false
}
main();
Design notes
- LRU via
Mapordering — JavaScriptMapiterates in insertion order, so recency is tracked by deleting and re-inserting a key on everyget/sethit. The least-recently-used entry is always the first key, making eviction O(1). - Lazy expiry — TTLs are stored as absolute timestamps;
getandhascheck expiry on access and delete stale entries without any timers.prune()sweeps everything eagerly when you want deterministic memory reclamation. - Per-entry TTL —
set(key, value, ttl)overrides the constructor'sdefaultTTL; omitting both means the entry never expires (Infinity). - No dependencies — runs with just
ts-node/tscor can be pasted into any TS project (target ES2015+).
gpt-5.6-sol
openai/gpt-5.6-sol
29.11s$0.053Cheapest by 2x
export class LRUCache<K, V> {
private readonly entries = new Map<
K,
{ value: V; expiresAt: number | null }
>();
constructor(private readonly maxSize: number) {
if (!Number.isInteger(maxSize) || maxSize <= 0) {
throw new RangeError("maxSize must be a positive integer");
}
}
/**
* Returns a value and marks it as recently used.
* Expired entries are removed and return undefined.
*/
get(key: K): V | undefined {
const entry = this.entries.get(key);
if (entry === undefined) {
return undefined;
}
if (entry.expiresAt !== null && entry.expiresAt <= Date.now()) {
this.entries.delete(key);
return undefined;
}
// Map preserves insertion order. Reinsert to mark as most recently used.
this.entries.delete(key);
this.entries.set(key, entry);
return entry.value;
}
/**
* Adds or updates an entry.
* Omit ttlMs, or use Infinity, for an entry that does not expire.
*/
set(key: K, value: V, ttlMs?: number): this {
if (ttlMs !== undefined && Number.isNaN(ttlMs)) {
throw new RangeError("ttlMs must be a number");
}
this.entries.delete(key);
// A non-positive TTL means the entry is already expired.
if (ttlMs !== undefined && ttlMs <= 0) {
return this;
}
const expiresAt =
ttlMs === undefined || ttlMs === Infinity
? null
: Date.now() + ttlMs;
// Remove stale entries before evicting a live LRU entry.
if (this.entries.size >= this.maxSize) {
this.prune();
}
while (this.entries.size >= this.maxSize) {
const oldest = this.entries.keys().next();
if (oldest.done) {
break;
}
this.entries.delete(oldest.value);
}
this.entries.set(key, { value, expiresAt });
return this;
}
delete(key: K): boolean {
return this.entries.delete(key);
}
/**
* Removes all expired entries and returns the number removed.
*/
prune(): number {
const now = Date.now();
let removed = 0;
for (const [key, entry] of this.entries) {
if (entry.expiresAt !== null && entry.expiresAt <= now) {
this.entries.delete(key);
removed++;
}
}
return removed;
}
}
// Usage example
const cache = new LRUCache<string, number>(2);
cache.set("a", 1, 5_000);
cache.set("b", 2); // No expiry
console.log(cache.get("a")); // 1; "a" is now most recently used
cache.set("c", 3); // Evicts "b", the least recently used entry
console.log(cache.get("b")); // undefined
console.log(cache.get("c")); // 3
cache.set("short-lived", 4, 10);
setTimeout(() => {
console.log(cache.get("short-lived")); // undefined; lazily removed
console.log(cache.prune()); // Removes any other expired entries
}, 20);
Discover if you're paying too much for an AI model that's too slow.