forked from rendajs/Renda
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
05635b3
commit c5ceea0
Showing
2 changed files
with
182 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}); | ||
}, | ||
}); |