Skip to content

Commit 26a0fe6

Browse files
committed
Add TTLMap tests
1 parent 1aabf39 commit 26a0fe6

File tree

3 files changed

+133
-5
lines changed

3 files changed

+133
-5
lines changed

backend/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"migration": "pnpm run _node src/storage/main.ts",
1111
"typecheck": "tsc",
1212
"lint": "eslint",
13-
"test": "node --import=./registerSWC.js --test src/**/*.test.ts",
13+
"test": "node --import=./registerSWC.js --enable-source-maps --test src/**/*.test.ts",
1414
"_node": "node --env-file=.env --disable-proto=throw --disallow-code-generation-from-strings --experimental-transform-types"
1515
},
1616
"dependencies": {

backend/src/common/ttlMap.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { MINUTE, SECOND } from "#common/time.ts";
2+
import { TTLMap } from "#common/ttlMap.ts";
3+
import assert from "node:assert";
4+
import test, { suite } from "node:test";
5+
6+
let time = 0;
7+
8+
function mockTime<T>(callback: () => T): T {
9+
// reset mocked value
10+
time = 0;
11+
12+
const oldDateNow = Date.now;
13+
Date.now = () => time;
14+
15+
try {
16+
return callback();
17+
} finally {
18+
Date.now = oldDateNow;
19+
}
20+
}
21+
22+
function assertConsistentData<V>(map: TTLMap<string, V>, data: Record<string, V>): void {
23+
assert.equal(map.size, Object.keys(data).length, "map size == data size");
24+
assert.deepEqual([...map.keys()], Object.keys(data), "map keys == data keys");
25+
assert.deepEqual([...map.values()], Object.values(data), "map values == data values");
26+
assert.deepEqual([...map.entries()], Object.entries(data), "map entries == data entries");
27+
28+
const fromForEach: [string, V][] = [];
29+
map.forEach((value, key) => fromForEach.push([key, value]));
30+
assert.deepEqual(fromForEach, Object.entries(data), "forEach entries == data entries");
31+
32+
const fromForOf: [string, V][] = [];
33+
34+
for (const [key, value] of map)
35+
fromForOf.push([key, value]);
36+
37+
for (const key in data)
38+
assert.ok(map.has(key), `map.has('${key}')`);
39+
40+
assert.deepEqual(fromForOf, Object.entries(data), "for of entries == data entries");
41+
}
42+
43+
function assertInternalData<V>(map: TTLMap<string, V>, data: Record<string, V>): void {
44+
assert.deepEqual(
45+
[...map["_map"].entries()].map(([key, [value]]) => [key, value]),
46+
Object.entries(data)
47+
);
48+
}
49+
50+
suite("TTLMap", () => {
51+
test("instant expiry", () => mockTime(() => {
52+
const map: TTLMap<string, number> = new TTLMap(0);
53+
54+
map.set("a", 1);
55+
map.set("b", 2);
56+
map.set("c", 3);
57+
58+
assertConsistentData(map, {});
59+
assertInternalData(map, { a: 1, b: 2, c: 3 });
60+
61+
map.cleanup();
62+
assertConsistentData(map, {});
63+
assertInternalData(map, {});
64+
}));
65+
66+
test("staggered expiry", () => mockTime(() => {
67+
const map: TTLMap<string, number> = new TTLMap(MINUTE);
68+
69+
map.set("a", 1);
70+
assertConsistentData(map, { a: 1 });
71+
assertInternalData(map, { a: 1 });
72+
time += 20 * SECOND;
73+
74+
map.set("b", 2);
75+
assertConsistentData(map, { a: 1, b: 2 });
76+
assertInternalData(map, { a: 1, b: 2 });
77+
time += 20 * SECOND;
78+
79+
map.set("c", 3);
80+
assertConsistentData(map, { a: 1, b: 2, c: 3 });
81+
assertInternalData(map, { a: 1, b: 2, c: 3 });
82+
time += 20 * SECOND;
83+
84+
map.set("d", 4);
85+
assertConsistentData(map, { b: 2, c: 3, d: 4 });
86+
assertInternalData(map, { a: 1, b: 2, c: 3, d: 4 });
87+
time += 20 * SECOND;
88+
89+
assertConsistentData(map, { c: 3, d: 4 });
90+
assertInternalData(map, { a: 1, b: 2, c: 3, d: 4 });
91+
map.cleanup();
92+
assertInternalData(map, { c: 3, d: 4 });
93+
94+
time += 1 * MINUTE;
95+
assertConsistentData(map, {});
96+
assertInternalData(map, { c: 3, d: 4 });
97+
map.cleanup();
98+
assertConsistentData(map, {});
99+
assertInternalData(map, {});
100+
}));
101+
102+
test("has", () => mockTime(() => {
103+
const map: TTLMap<string, null> = new TTLMap(0);
104+
map.set("a", null);
105+
assert.ok(!map.has("a"), "!map.has('a')");
106+
}));
107+
108+
test("precision", () => mockTime(() => {
109+
const map: TTLMap<string, number> = new TTLMap(SECOND);
110+
111+
map.set("a", 0);
112+
time += 1;
113+
map.set("b", 1);
114+
time += SECOND - 2;
115+
116+
assertConsistentData(map, { a: 0, b: 1 });
117+
time += 1;
118+
119+
assertConsistentData(map, { b: 1 });
120+
time += 1;
121+
122+
assertConsistentData(map, {});
123+
}));
124+
});

backend/src/common/ttlMap.ts

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export class TTLMap<K, V> {
2323
}
2424

2525
private _isExpired(now: number, date: number): boolean {
26-
return (now - date) > this._ttl;
26+
return (now - date) >= this._ttl;
2727
}
2828

2929
get size(): number {
@@ -70,7 +70,7 @@ export class TTLMap<K, V> {
7070
return false;
7171

7272
const [_, date] = this._map.get(key)!;
73-
return this._isExpired(Date.now(), date);
73+
return !this._isExpired(Date.now(), date);
7474
}
7575

7676
set(key: K, value: V): this {
@@ -86,8 +86,12 @@ export class TTLMap<K, V> {
8686
yield [key, value];
8787
}
8888

89-
keys(): IterableIterator<K> {
90-
return this._map.keys();
89+
*keys(): IterableIterator<K> {
90+
const now = Date.now();
91+
92+
for (const [key, [_, date]] of this._map)
93+
if (!this._isExpired(now, date))
94+
yield key;
9195
}
9296

9397
*values(): IterableIterator<V> {

0 commit comments

Comments
 (0)