Skip to content

Commit 963ef63

Browse files
authored
feat: introduces ExpirableMap (#794)
* feat: introduces ExpirableMap * prunes on every get * supports source iterator in constructor
1 parent c115480 commit 963ef63

File tree

3 files changed

+202
-0
lines changed

3 files changed

+202
-0
lines changed

docs/generated/changelog.html

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,10 @@ <h2>Version x.x.x</h2>
3333
<li>new Verify export on ed25519 because why not</li>
3434
</ul>
3535
<li>Adds support for Uint8Arrays in Principal.from()</li>
36+
<li>
37+
feat: introduces ExpirableMap, a utility class that will return values up until a
38+
configured expiry
39+
</li>
3640
<li>
3741
chore: increases size limit for agent-js to allow for Ed25519 support for node key
3842
signature verification
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import { ExpirableMap } from './expirableMap';
2+
3+
jest.useFakeTimers();
4+
describe('ExpirableMap', () => {
5+
it('should return undefined if the key is not present', () => {
6+
const map = new ExpirableMap();
7+
expect(map.get('key')).toBeUndefined();
8+
});
9+
it('should return a key if one has been recently set', () => {
10+
const map = new ExpirableMap();
11+
map.set('key', 'value');
12+
expect(map.get('key')).toBe('value');
13+
});
14+
it('should return undefined if the key has expired', () => {
15+
const map = new ExpirableMap({ expirationTime: 10 });
16+
map.set('key', 'value');
17+
jest.advanceTimersByTime(11);
18+
expect(map.get('key')).toBeUndefined();
19+
});
20+
it('should support iterable operations', () => {
21+
const map = new ExpirableMap({
22+
source: [
23+
['key1', 8],
24+
['key2', 1234],
25+
],
26+
});
27+
28+
expect(Array.from(map)).toStrictEqual([
29+
['key1', 8],
30+
['key2', 1234],
31+
]);
32+
33+
for (const [key, value] of map) {
34+
expect(map.get(key)).toBe(value);
35+
}
36+
});
37+
});
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
export type ExpirableMapOptions<K, V> = {
2+
source?: Iterable<[K, V]>;
3+
expirationTime?: number;
4+
};
5+
6+
/**
7+
* A map that expires entries after a given time.
8+
* Defaults to 10 minutes.
9+
*/
10+
export class ExpirableMap<K, V> implements Map<K, V> {
11+
// Internals
12+
#inner: Map<K, { value: V; timestamp: number }>;
13+
#expirationTime: number;
14+
15+
[Symbol.iterator]: () => IterableIterator<[K, V]> = this.entries.bind(this);
16+
[Symbol.toStringTag] = 'ExpirableMap';
17+
18+
/**
19+
* Create a new ExpirableMap.
20+
* @param {ExpirableMapOptions<any, any>} options - options for the map.
21+
* @param {Iterable<[any, any]>} options.source - an optional source of entries to initialize the map with.
22+
* @param {number} options.expirationTime - the time in milliseconds after which entries will expire.
23+
*/
24+
constructor(options: ExpirableMapOptions<K, V> = {}) {
25+
const { source = [], expirationTime = 10 * 60 * 1000 } = options;
26+
const currentTime = Date.now();
27+
this.#inner = new Map(
28+
[...source].map(([key, value]) => [key, { value, timestamp: currentTime }]),
29+
);
30+
this.#expirationTime = expirationTime;
31+
}
32+
33+
/**
34+
* Prune removes all expired entries.
35+
*/
36+
prune() {
37+
const currentTime = Date.now();
38+
for (const [key, entry] of this.#inner.entries()) {
39+
if (currentTime - entry.timestamp > this.#expirationTime) {
40+
this.#inner.delete(key);
41+
}
42+
}
43+
return this;
44+
}
45+
46+
// Implementing the Map interface
47+
48+
/**
49+
* Set the value for the given key. Prunes expired entries.
50+
* @param key for the entry
51+
* @param value of the entry
52+
* @returns this
53+
*/
54+
set(key: K, value: V) {
55+
this.prune();
56+
const entry = {
57+
value,
58+
timestamp: Date.now(),
59+
};
60+
this.#inner.set(key, entry);
61+
62+
return this;
63+
}
64+
65+
/**
66+
* Get the value associated with the key, if it exists and has not expired.
67+
* @param key K
68+
* @returns the value associated with the key, or undefined if the key is not present or has expired.
69+
*/
70+
get(key: K) {
71+
const entry = this.#inner.get(key);
72+
if (entry === undefined) {
73+
return undefined;
74+
}
75+
if (Date.now() - entry.timestamp > this.#expirationTime) {
76+
this.#inner.delete(key);
77+
return undefined;
78+
}
79+
return entry.value;
80+
}
81+
82+
/**
83+
* Clear all entries.
84+
*/
85+
clear() {
86+
this.#inner.clear();
87+
}
88+
89+
/**
90+
* Entries returns the entries of the map, without the expiration time.
91+
* @returns an iterator over the entries of the map.
92+
*/
93+
entries(): IterableIterator<[K, V]> {
94+
const iterator = this.#inner.entries();
95+
const generator = function* () {
96+
for (const [key, value] of iterator) {
97+
yield [key, value.value] as [K, V];
98+
}
99+
};
100+
return generator();
101+
}
102+
103+
/**
104+
* Values returns the values of the map, without the expiration time.
105+
* @returns an iterator over the values of the map.
106+
*/
107+
values(): IterableIterator<V> {
108+
const iterator = this.#inner.values();
109+
const generator = function* () {
110+
for (const value of iterator) {
111+
yield value.value;
112+
}
113+
};
114+
return generator();
115+
}
116+
117+
/**
118+
* Keys returns the keys of the map
119+
* @returns an iterator over the keys of the map.
120+
*/
121+
keys(): IterableIterator<K> {
122+
return this.#inner.keys();
123+
}
124+
125+
/**
126+
* forEach calls the callbackfn on each entry of the map.
127+
* @param callbackfn to call on each entry
128+
* @param thisArg to use as this when calling the callbackfn
129+
*/
130+
forEach(callbackfn: (value: V, key: K, map: Map<K, V>) => void, thisArg?: ExpirableMap<K, V>) {
131+
for (const [key, value] of this.#inner.entries()) {
132+
callbackfn.call(thisArg, value.value, key, this);
133+
}
134+
}
135+
136+
/**
137+
* has returns true if the key exists and has not expired.
138+
* @param key K
139+
* @returns true if the key exists and has not expired.
140+
*/
141+
has(key: K): boolean {
142+
return this.#inner.has(key);
143+
}
144+
145+
/**
146+
* delete the entry for the given key.
147+
* @param key K
148+
* @returns true if the key existed and has been deleted.
149+
*/
150+
delete(key: K) {
151+
return this.#inner.delete(key);
152+
}
153+
154+
/**
155+
* get size of the map.
156+
* @returns the size of the map.
157+
*/
158+
get size() {
159+
return this.#inner.size;
160+
}
161+
}

0 commit comments

Comments
 (0)