Skip to content

Commit

Permalink
Implement code review changes
Browse files Browse the repository at this point in the history
  • Loading branch information
lionel-rowe committed Aug 20, 2024
1 parent c1845f3 commit 52c7efc
Show file tree
Hide file tree
Showing 2 changed files with 61 additions and 71 deletions.
56 changes: 18 additions & 38 deletions cache/ttl_cache.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,18 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import type { MemoizationCache } from "./memoize.ts";
import { delay } from "@std/async/delay";

/**
* Time-to-live cache.
*
* Automatically removes entries once the configured amount of time elapses. If
* the values themselves are promises, the countdown starts from when they
* resolve.
* Automatically removes entries once the configured amount of time elapses.
*
* @typeParam K The type of the cache keys.
* @typeParam V The type of the cache values.
*
* @example Usage
* ```ts
* import { TtlCache } from "@std/cache";
* import { assertEquals } from "@std/assert";
* import { TtlCache } from "@std/cache/ttl-cache";
* import { assertEquals } from "@std/assert/equals";
* import { delay } from "@std/async/delay";
*
* const cache = new TtlCache<string, number>(1000);
Expand All @@ -28,69 +25,52 @@ import { delay } from "@std/async/delay";
*/
export class TtlCache<K, V> extends Map<K, V>
implements MemoizationCache<K, V> {
/**
* The default time-to-live in milliseconds
*
* @example Usage
* ```ts
* import { TtlCache } from "@std/cache";
* import { assertEquals } from "@std/assert";
* const cache = new TtlCache(1000);
* assertEquals(cache.defaultTtl, 1000);
* ```
*/
defaultTtl: number;
#defaultTtl: number;

/**
* Construct a new `TtlCache`.
* @param defaultTtl The default time-to-live in milliseconds
*/
constructor(defaultTtl: number) {
super();
this.defaultTtl = defaultTtl;
this.#defaultTtl = defaultTtl;
}

#deleters = new Map<K, { promise: Promise<void>; ac: AbortController }>();
#abortControllers = new Map<K, AbortController>();

/**
* Set a value in the cache.
*
* @param key The cache key
* @param value The value to set
* @param customTtl A custom time-to-live. If supplied, overrides the cache's default TTL for this entry.
* @param ttl A custom time-to-live. If supplied, overrides the cache's default TTL for this entry.
* @returns `this` for chaining.
*
* @example Usage
* ```ts
* import { TtlCache } from "@std/cache";
* import { assertEquals } from "@std/assert";
* import { TtlCache } from "@std/cache/ttl-cache";
* import { assertEquals } from "@std/assert/equals";
*
* const cache = new TtlCache<string, number>(1000);
*
* cache.set("a", 1);
* assertEquals(cache.get("a"), 1);
* ```
*/
override set(key: K, value: V, customTtl?: number): this {
override set(key: K, value: V, ttl: number = this.#defaultTtl): this {
super.set(key, value);

this.#deleters.get(key)?.ac.abort();
this.#abortControllers.get(key)?.abort();
const ac = new AbortController();

this.#deleters.set(key, {
promise: (async () => {
if (value instanceof Promise) {
await value;
}

await delay(customTtl ?? this.defaultTtl);
setTimeout(() => {
if (!ac.signal.aborted) {
super.delete(key);
}
this.#abortControllers.delete(key);
}, ttl);

if (!ac.signal.aborted) {
super.delete(key);
}
})(),
ac,
});
this.#abortControllers.set(key, ac);

return this;
}
Expand Down
76 changes: 43 additions & 33 deletions cache/ttl_cache_test.ts
Original file line number Diff line number Diff line change
@@ -1,60 +1,70 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assertEquals } from "@std/assert";
import { TtlCache } from "./ttl_cache.ts";
import { delay } from "@std/async/delay";

Deno.test("TtlCache deletes entries after they expire", async (t) => {
await t.step("when values are sync", async () => {
const cache = new TtlCache(10);
import { assertEquals } from "@std/assert";
import { FakeTime } from "@std/testing/time";

cache.set(1, "!");
cache.set(2, "!");
const UNSET = Symbol("UNSET");

assertEquals(cache.size, 2);
function assertEntries<K extends unknown, V extends unknown>(
cache: TtlCache<K, V>,
entries: [key: K, value: V | typeof UNSET][],
) {
const filteredEntries = entries.filter(([, value]) => value !== UNSET);
const filteredKeys = filteredEntries.map(([key]) => key);
const filteredValues = filteredEntries.map(([, value]) => value);

await delay(5);
assertEquals(cache.size, filteredEntries.length);

cache.set(1, "!");
assertEquals([...cache.entries()], filteredEntries);
assertEquals([...cache.keys()], filteredKeys);
assertEquals([...cache.values()], filteredValues);

await delay(5);
for (const [key, value] of entries) {
assertEquals(cache.has(key), value !== UNSET);
assertEquals(cache.get(key), value === UNSET ? undefined : value);
}
}

assertEquals(cache.size, 1);
Deno.test("TtlCache deletes entries after they expire", async (t) => {
await t.step("default TTL (passed in constructor)", async () => {
using time = new FakeTime(0);

await delay(8);
const cache = new TtlCache<number, string>(10);

assertEquals(cache.size, 0);
});
cache.set(1, "one");
cache.set(2, "two");

await t.step("when values are promises", async () => {
const cache = new TtlCache(10);
assertEntries(cache, [[1, "one"], [2, "two"]]);

cache.set(1, delay(10));
await time.tickAsync(5);

assertEquals(cache.size, 1);
cache.set(1, "one");

await delay(15);
await time.tickAsync(5);

assertEquals(cache.size, 1);
assertEntries(cache, [[1, "one"], [2, UNSET]]);

await delay(10);
await time.tickAsync(8);

assertEquals(cache.size, 0);
assertEntries(cache, [[1, UNSET], [2, UNSET]]);
});

await t.step("custom TTL", async () => {
const cache = new TtlCache(10);
await t.step("custom TTL (passed in `set`)", async () => {
using time = new FakeTime(0);

const cache = new TtlCache<number, string>(10);

cache.set(1, "!");
cache.set(2, "!", 3);
cache.set(1, "one");
cache.set(2, "two", 3);

assertEquals(cache.size, 2);
assertEntries(cache, [[1, "one"], [2, "two"]]);

await delay(5);
await time.tickAsync(5);

assertEquals(cache.size, 1);
assertEntries(cache, [[1, "one"], [2, UNSET]]);

await delay(5);
await time.tickAsync(5);

assertEquals(cache.size, 0);
assertEntries(cache, [[1, UNSET], [2, UNSET]]);
});
});

0 comments on commit 52c7efc

Please sign in to comment.