diff --git a/docs/generated/changelog.html b/docs/generated/changelog.html
index 4191ba4c5..d67a34196 100644
--- a/docs/generated/changelog.html
+++ b/docs/generated/changelog.html
@@ -33,6 +33,10 @@
Version x.x.x
new Verify export on ed25519 because why not
Adds support for Uint8Arrays in Principal.from()
+
+ feat: introduces ExpirableMap, a utility class that will return values up until a
+ configured expiry
+
chore: increases size limit for agent-js to allow for Ed25519 support for node key
signature verification
diff --git a/packages/agent/src/utils/expirableMap.test.ts b/packages/agent/src/utils/expirableMap.test.ts
new file mode 100644
index 000000000..9bd0b2e12
--- /dev/null
+++ b/packages/agent/src/utils/expirableMap.test.ts
@@ -0,0 +1,37 @@
+import { ExpirableMap } from './expirableMap';
+
+jest.useFakeTimers();
+describe('ExpirableMap', () => {
+ it('should return undefined if the key is not present', () => {
+ const map = new ExpirableMap();
+ expect(map.get('key')).toBeUndefined();
+ });
+ it('should return a key if one has been recently set', () => {
+ const map = new ExpirableMap();
+ map.set('key', 'value');
+ expect(map.get('key')).toBe('value');
+ });
+ it('should return undefined if the key has expired', () => {
+ const map = new ExpirableMap({ expirationTime: 10 });
+ map.set('key', 'value');
+ jest.advanceTimersByTime(11);
+ expect(map.get('key')).toBeUndefined();
+ });
+ it('should support iterable operations', () => {
+ const map = new ExpirableMap({
+ source: [
+ ['key1', 8],
+ ['key2', 1234],
+ ],
+ });
+
+ expect(Array.from(map)).toStrictEqual([
+ ['key1', 8],
+ ['key2', 1234],
+ ]);
+
+ for (const [key, value] of map) {
+ expect(map.get(key)).toBe(value);
+ }
+ });
+});
diff --git a/packages/agent/src/utils/expirableMap.ts b/packages/agent/src/utils/expirableMap.ts
new file mode 100644
index 000000000..39657484c
--- /dev/null
+++ b/packages/agent/src/utils/expirableMap.ts
@@ -0,0 +1,161 @@
+export type ExpirableMapOptions = {
+ source?: Iterable<[K, V]>;
+ expirationTime?: number;
+};
+
+/**
+ * A map that expires entries after a given time.
+ * Defaults to 10 minutes.
+ */
+export class ExpirableMap implements Map {
+ // Internals
+ #inner: Map;
+ #expirationTime: number;
+
+ [Symbol.iterator]: () => IterableIterator<[K, V]> = this.entries.bind(this);
+ [Symbol.toStringTag] = 'ExpirableMap';
+
+ /**
+ * Create a new ExpirableMap.
+ * @param {ExpirableMapOptions} options - options for the map.
+ * @param {Iterable<[any, any]>} options.source - an optional source of entries to initialize the map with.
+ * @param {number} options.expirationTime - the time in milliseconds after which entries will expire.
+ */
+ constructor(options: ExpirableMapOptions = {}) {
+ const { source = [], expirationTime = 10 * 60 * 1000 } = options;
+ const currentTime = Date.now();
+ this.#inner = new Map(
+ [...source].map(([key, value]) => [key, { value, timestamp: currentTime }]),
+ );
+ this.#expirationTime = expirationTime;
+ }
+
+ /**
+ * Prune removes all expired entries.
+ */
+ prune() {
+ const currentTime = Date.now();
+ for (const [key, entry] of this.#inner.entries()) {
+ if (currentTime - entry.timestamp > this.#expirationTime) {
+ this.#inner.delete(key);
+ }
+ }
+ return this;
+ }
+
+ // Implementing the Map interface
+
+ /**
+ * Set the value for the given key. Prunes expired entries.
+ * @param key for the entry
+ * @param value of the entry
+ * @returns this
+ */
+ set(key: K, value: V) {
+ this.prune();
+ const entry = {
+ value,
+ timestamp: Date.now(),
+ };
+ this.#inner.set(key, entry);
+
+ return this;
+ }
+
+ /**
+ * Get the value associated with the key, if it exists and has not expired.
+ * @param key K
+ * @returns the value associated with the key, or undefined if the key is not present or has expired.
+ */
+ get(key: K) {
+ const entry = this.#inner.get(key);
+ if (entry === undefined) {
+ return undefined;
+ }
+ if (Date.now() - entry.timestamp > this.#expirationTime) {
+ this.#inner.delete(key);
+ return undefined;
+ }
+ return entry.value;
+ }
+
+ /**
+ * Clear all entries.
+ */
+ clear() {
+ this.#inner.clear();
+ }
+
+ /**
+ * Entries returns the entries of the map, without the expiration time.
+ * @returns an iterator over the entries of the map.
+ */
+ entries(): IterableIterator<[K, V]> {
+ const iterator = this.#inner.entries();
+ const generator = function* () {
+ for (const [key, value] of iterator) {
+ yield [key, value.value] as [K, V];
+ }
+ };
+ return generator();
+ }
+
+ /**
+ * Values returns the values of the map, without the expiration time.
+ * @returns an iterator over the values of the map.
+ */
+ values(): IterableIterator {
+ const iterator = this.#inner.values();
+ const generator = function* () {
+ for (const value of iterator) {
+ yield value.value;
+ }
+ };
+ return generator();
+ }
+
+ /**
+ * Keys returns the keys of the map
+ * @returns an iterator over the keys of the map.
+ */
+ keys(): IterableIterator {
+ return this.#inner.keys();
+ }
+
+ /**
+ * forEach calls the callbackfn on each entry of the map.
+ * @param callbackfn to call on each entry
+ * @param thisArg to use as this when calling the callbackfn
+ */
+ forEach(callbackfn: (value: V, key: K, map: Map) => void, thisArg?: ExpirableMap) {
+ for (const [key, value] of this.#inner.entries()) {
+ callbackfn.call(thisArg, value.value, key, this);
+ }
+ }
+
+ /**
+ * has returns true if the key exists and has not expired.
+ * @param key K
+ * @returns true if the key exists and has not expired.
+ */
+ has(key: K): boolean {
+ return this.#inner.has(key);
+ }
+
+ /**
+ * delete the entry for the given key.
+ * @param key K
+ * @returns true if the key existed and has been deleted.
+ */
+ delete(key: K) {
+ return this.#inner.delete(key);
+ }
+
+ /**
+ * get size of the map.
+ * @returns the size of the map.
+ */
+ get size() {
+ return this.#inner.size;
+ }
+}