Skip to content

Commit 1cd63ca

Browse files
feat(cache/unstable): add peek() to TtlCache (#7070)
1 parent 008db8d commit 1cd63ca

2 files changed

Lines changed: 84 additions & 7 deletions

File tree

cache/ttl_cache.ts

Lines changed: 48 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -171,20 +171,21 @@ export class TtlCache<K, V> extends Map<K, V>
171171
);
172172
}
173173

174+
const abs = options?.absoluteExpiration;
175+
if (abs !== undefined && (!(abs >= 0) || !Number.isFinite(abs))) {
176+
throw new RangeError(
177+
`Cannot set entry in TtlCache: absoluteExpiration must be a finite, non-negative number: received ${abs}`,
178+
);
179+
}
180+
174181
const existing = this.#timeouts.get(key);
175182
if (existing !== undefined) clearTimeout(existing);
176183
super.set(key, value);
177184
this.#timeouts.set(key, setTimeout(() => this.delete(key), ttl));
178185

179186
if (this.#slidingExpiration) {
180187
this.#entryTtls!.set(key, ttl);
181-
if (options?.absoluteExpiration !== undefined) {
182-
const abs = options.absoluteExpiration;
183-
if (!(abs >= 0) || !Number.isFinite(abs)) {
184-
throw new RangeError(
185-
`Cannot set entry in TtlCache: absoluteExpiration must be a finite, non-negative number: received ${abs}`,
186-
);
187-
}
188+
if (abs !== undefined) {
188189
this.#absoluteDeadlines!.set(key, Date.now() + abs);
189190
} else {
190191
this.#absoluteDeadlines!.delete(key);
@@ -223,6 +224,46 @@ export class TtlCache<K, V> extends Map<K, V>
223224
return super.get(key);
224225
}
225226

227+
/**
228+
* Returns the value associated with the given key, or `undefined` if the
229+
* key is not present, **without** resetting its TTL.
230+
*
231+
* This is the TTL-cache equivalent of
232+
* {@linkcode LruCache.prototype.peek | LruCache.peek()}: a side-effect-free
233+
* read that leaves the entry's expiration unchanged.
234+
*
235+
* @experimental **UNSTABLE**: New API, yet to be vetted.
236+
*
237+
* @param key The key to look up.
238+
* @returns The value, or `undefined` if not present.
239+
*
240+
* @example Peeking at a value without resetting the sliding TTL
241+
* ```ts
242+
* import { TtlCache } from "@std/cache/ttl-cache";
243+
* import { assertEquals } from "@std/assert/equals";
244+
* import { FakeTime } from "@std/testing/time";
245+
*
246+
* using time = new FakeTime(0);
247+
* const cache = new TtlCache<string, number>(100, {
248+
* slidingExpiration: true,
249+
* });
250+
*
251+
* cache.set("a", 1);
252+
* time.now = 80;
253+
*
254+
* // peek does not reset the TTL
255+
* assertEquals(cache.peek("a"), 1);
256+
*
257+
* // entry still expires at t=100
258+
* time.now = 100;
259+
* assertEquals(cache.peek("a"), undefined);
260+
* ```
261+
*/
262+
peek(key: K): V | undefined {
263+
if (!super.has(key)) return undefined;
264+
return super.get(key);
265+
}
266+
226267
/**
227268
* Deletes the value associated with the given key.
228269
*

cache/ttl_cache_test.ts

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,42 @@ Deno.test("TtlCache validates TTL", async (t) => {
265265
});
266266
});
267267

268+
Deno.test("TtlCache peek()", async (t) => {
269+
await t.step("returns value without resetting sliding TTL", () => {
270+
using time = new FakeTime(0);
271+
const cache = new TtlCache<string, number>(100, {
272+
slidingExpiration: true,
273+
});
274+
275+
cache.set("a", 1);
276+
277+
time.now = 80;
278+
assertEquals(cache.peek("a"), 1);
279+
280+
// peek did not reset the TTL, so the entry still expires at t=100
281+
time.now = 100;
282+
assertEquals(cache.peek("a"), undefined);
283+
});
284+
285+
await t.step("returns value for non-sliding cache", () => {
286+
using time = new FakeTime(0);
287+
const cache = new TtlCache<string, number>(100);
288+
289+
cache.set("a", 1);
290+
291+
time.now = 50;
292+
assertEquals(cache.peek("a"), 1);
293+
294+
time.now = 100;
295+
assertEquals(cache.peek("a"), undefined);
296+
});
297+
298+
await t.step("returns undefined for missing key", () => {
299+
using cache = new TtlCache<string, number>(100);
300+
assertEquals(cache.peek("missing"), undefined);
301+
});
302+
});
303+
268304
Deno.test("TtlCache get() returns undefined for missing key with sliding expiration", () => {
269305
using cache = new TtlCache<string, number>(100, {
270306
slidingExpiration: true,

0 commit comments

Comments
 (0)