Skip to content

Commit

Permalink
Add WeakValueMap
Browse files Browse the repository at this point in the history
  • Loading branch information
jespertheend committed Oct 9, 2023
1 parent 05635b3 commit c5ceea0
Show file tree
Hide file tree
Showing 2 changed files with 182 additions and 0 deletions.
69 changes: 69 additions & 0 deletions src/util/WeakValueMap.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/**
* @template [K = unknown]
* @template {object} [V = object]
*/
export class WeakValueMap {
/**
* Similar to `WeakMap` except instead of a weakly held key, it's the value that is weakly held.
* As a result, you can use primitives as keys.
* @param {Iterable<[K, V]>} iterable
*/
constructor(iterable = []) {
for (const [key, value] of iterable) {
this.set(key, value);
}

/** @private @type {Map<K, WeakRef<V>>} */
this._map = new Map();

/** @private @type {FinalizationRegistry<K>} */
this._finalizationRegistry = new FinalizationRegistry(key => {
this._map.delete(key);
});
}

/**
* @param {K} key
* @param {V} value
*/
set(key, value) {
const currentRef = this.get(key);
if (currentRef) {
this._finalizationRegistry.unregister(currentRef);
}
this._finalizationRegistry.register(value, key, value);
this._map.set(key, new WeakRef(value));
}

/**
* @param {K} key
*/
get(key) {
const weakRef = this._map.get(key);
if (!weakRef) return;
const ref = weakRef.deref();
if (!ref) {
this._map.delete(key);
return;
}
return ref;
}

/**
* @param {K} key
*/
has(key) {
return Boolean(this.get(key));
}

/**
* @param {K} key
*/
delete(key) {
const ref = this.get(key);
if (ref) {
this._finalizationRegistry.unregister(ref);
}
this._map.delete(key);
}
}
113 changes: 113 additions & 0 deletions test/unit/src/util/WeakValueMap.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import {assertEquals, assertStrictEquals} from "std/testing/asserts.ts";
import {forceCleanup, forceCleanupAll, runWithMockWeakRef} from "../../shared/mockWeakRef.js";
import {WeakValueMap} from "../../../../src/util/WeakValueMap.js";

Deno.test({
name: "Garbage collection removes values",
fn() {
runWithMockWeakRef(() => {
const ref = Symbol("ref");

const map = new WeakValueMap();
// @ts-expect-error #773
map.set(1, ref);

assertStrictEquals(map.get(1), ref);
assertEquals(map.has(1), true);

forceCleanup(ref);

assertEquals(map.get(1), undefined);
assertEquals(map.has(1), false);
});
},
});

Deno.test({
name: "Deleting deletes values",
fn() {
runWithMockWeakRef(() => {
const ref = Symbol("ref");

const map = new WeakValueMap();
// @ts-expect-error #773
map.set(1, ref);

map.delete(1);

const result = map.get(1);
assertEquals(result, undefined);
});
},
});

Deno.test({
name: "Overwriting values does not trigger deletion when the old ref is garbage collected",
fn() {
runWithMockWeakRef(() => {
const refA = Symbol("ref A");
const refB = Symbol("ref B");

const map = new WeakValueMap();
// @ts-expect-error #773
map.set(1, refA);
// @ts-expect-error #773
map.set(1, refB);

forceCleanup(refA);

assertStrictEquals(map.get(1), refB);
});
},
});

Deno.test({
name: "Multiple values",
fn() {
runWithMockWeakRef(() => {
const refA = Symbol("ref A");
const refB = Symbol("ref B");
const refC = Symbol("ref D");

// @ts-expect-error #773
/** @type {WeakValueMap<string, symbol>} */
// @ts-expect-error #773
const map = new WeakValueMap();
map.set("A", refA);
map.set("B", refB);
map.set("C", refC);
map.set("B again", refB);

// Deleting A
assertStrictEquals(map.get("A"), refA);
assertEquals(map.has("A"), true);

map.delete("A");

assertEquals(map.get("A"), undefined);
assertEquals(map.has("A"), false);

// Deleting B
assertStrictEquals(map.get("B"), refB);
assertStrictEquals(map.get("B again"), refB);
assertEquals(map.has("B"), true);
assertEquals(map.has("B again"), true);

forceCleanup(refB);

assertEquals(map.get("B"), undefined);
assertEquals(map.get("B again"), undefined);
assertEquals(map.has("B"), false);
assertEquals(map.has("B again"), false);

// Deleting C
assertStrictEquals(map.get("C"), refC);
assertEquals(map.has("C"), true);

forceCleanupAll();

assertEquals(map.get("C"), undefined);
assertEquals(map.has("C"), false);
});
},
});

0 comments on commit c5ceea0

Please sign in to comment.