Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 48 additions & 7 deletions cache/ttl_cache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,20 +171,21 @@ export class TtlCache<K, V> extends Map<K, V>
);
}

const abs = options?.absoluteExpiration;
if (abs !== undefined && (!(abs >= 0) || !Number.isFinite(abs))) {
throw new RangeError(
`Cannot set entry in TtlCache: absoluteExpiration must be a finite, non-negative number: received ${abs}`,
);
}

const existing = this.#timeouts.get(key);
if (existing !== undefined) clearTimeout(existing);
super.set(key, value);
this.#timeouts.set(key, setTimeout(() => this.delete(key), ttl));

if (this.#slidingExpiration) {
this.#entryTtls!.set(key, ttl);
if (options?.absoluteExpiration !== undefined) {
const abs = options.absoluteExpiration;
if (!(abs >= 0) || !Number.isFinite(abs)) {
throw new RangeError(
`Cannot set entry in TtlCache: absoluteExpiration must be a finite, non-negative number: received ${abs}`,
);
}
if (abs !== undefined) {
this.#absoluteDeadlines!.set(key, Date.now() + abs);
} else {
this.#absoluteDeadlines!.delete(key);
Expand Down Expand Up @@ -223,6 +224,46 @@ export class TtlCache<K, V> extends Map<K, V>
return super.get(key);
}

/**
* Returns the value associated with the given key, or `undefined` if the
* key is not present, **without** resetting its TTL.
*
* This is the TTL-cache equivalent of
* {@linkcode LruCache.prototype.peek | LruCache.peek()}: a side-effect-free
* read that leaves the entry's expiration unchanged.
*
* @experimental **UNSTABLE**: New API, yet to be vetted.
*
* @param key The key to look up.
* @returns The value, or `undefined` if not present.
*
* @example Peeking at a value without resetting the sliding TTL
* ```ts
* import { TtlCache } from "@std/cache/ttl-cache";
* import { assertEquals } from "@std/assert/equals";
* import { FakeTime } from "@std/testing/time";
*
* using time = new FakeTime(0);
* const cache = new TtlCache<string, number>(100, {
* slidingExpiration: true,
* });
*
* cache.set("a", 1);
* time.now = 80;
*
* // peek does not reset the TTL
* assertEquals(cache.peek("a"), 1);
*
* // entry still expires at t=100
* time.now = 100;
* assertEquals(cache.peek("a"), undefined);
* ```
*/
peek(key: K): V | undefined {
if (!super.has(key)) return undefined;
return super.get(key);
}

/**
* Deletes the value associated with the given key.
*
Expand Down
36 changes: 36 additions & 0 deletions cache/ttl_cache_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,42 @@ Deno.test("TtlCache validates TTL", async (t) => {
});
});

Deno.test("TtlCache peek()", async (t) => {
await t.step("returns value without resetting sliding TTL", () => {
using time = new FakeTime(0);
const cache = new TtlCache<string, number>(100, {
slidingExpiration: true,
});

cache.set("a", 1);

time.now = 80;
assertEquals(cache.peek("a"), 1);

// peek did not reset the TTL, so the entry still expires at t=100
time.now = 100;
assertEquals(cache.peek("a"), undefined);
});

await t.step("returns value for non-sliding cache", () => {
using time = new FakeTime(0);
const cache = new TtlCache<string, number>(100);

cache.set("a", 1);

time.now = 50;
assertEquals(cache.peek("a"), 1);

time.now = 100;
assertEquals(cache.peek("a"), undefined);
});

await t.step("returns undefined for missing key", () => {
using cache = new TtlCache<string, number>(100);
assertEquals(cache.peek("missing"), undefined);
});
});

Deno.test("TtlCache get() returns undefined for missing key with sliding expiration", () => {
using cache = new TtlCache<string, number>(100, {
slidingExpiration: true,
Expand Down
Loading