Skip to content

Commit b6fed5f

Browse files
committed
feat: add InvertedWeakMap
1 parent 466124e commit b6fed5f

File tree

4 files changed

+271
-1
lines changed

4 files changed

+271
-1
lines changed

inverted_weak_map.test.ts

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
import { assert, assertEquals, assertFalse } from "testing/asserts.ts";
2+
import { InvertedWeakMap } from "./inverted_weak_map.ts";
3+
4+
function* iterate<T>(items: T[]): IterableIterator<T> {
5+
for (const item of items) {
6+
yield item;
7+
}
8+
}
9+
10+
Deno.test("InvertedWeakMap, constructor", () => {
11+
assertEquals(new InvertedWeakMap().size, 0);
12+
assertEquals(new InvertedWeakMap(undefined).size, 0);
13+
assertEquals(new InvertedWeakMap(null).size, 0);
14+
15+
assertEquals(new InvertedWeakMap([]).size, 0);
16+
assertEquals(new InvertedWeakMap(iterate([])).size, 0);
17+
18+
assertEquals(new InvertedWeakMap([[1, {}]]).size, 1);
19+
assertEquals(new InvertedWeakMap(iterate([[1, {}] as const])).size, 1);
20+
});
21+
22+
Deno.test("InvertedWeakMap, comparison Map, WeakMap", () => {
23+
const map = new Map();
24+
const iwmap = new InvertedWeakMap();
25+
26+
assertEquals(map.size, 0);
27+
assertEquals(iwmap.size, 0);
28+
29+
const obj1 = {};
30+
const obj2 = {};
31+
32+
assertEquals(map.get("1"), undefined);
33+
assertEquals(iwmap.get("1"), undefined);
34+
35+
assertEquals(map.set("1", obj1), map);
36+
assertEquals(iwmap.set("1", obj1), iwmap);
37+
38+
assertEquals(map.get("1"), obj1);
39+
assertEquals(iwmap.get("1"), obj1);
40+
41+
assertEquals(map.size, 1);
42+
assertEquals(iwmap.size, 1);
43+
44+
// add the same object again
45+
assertEquals(map.set("1", obj2), map);
46+
assertEquals(iwmap.set("1", obj2), iwmap);
47+
48+
assertEquals(map.get("1"), obj2);
49+
assertEquals(iwmap.get("1"), obj2);
50+
51+
assertEquals(map.size, 1);
52+
assertEquals(iwmap.size, 1);
53+
54+
assertEquals(map.set(2, obj1), map);
55+
assertEquals(iwmap.set(2, obj1), iwmap);
56+
57+
assertEquals(map.size, 2);
58+
assertEquals(iwmap.size, 2);
59+
60+
assert(map.has(2));
61+
assert(iwmap.has(2));
62+
63+
// delete the object
64+
assert(map.delete("1"));
65+
assert(iwmap.delete("1"));
66+
67+
assertEquals(map.size, 1);
68+
assertEquals(iwmap.size, 1);
69+
70+
assertFalse(map.delete("1"));
71+
assertFalse(iwmap.delete("1"));
72+
73+
assertEquals(map.size, 1);
74+
assertEquals(iwmap.size, 1);
75+
76+
assertFalse(map.has("1"));
77+
assertFalse(iwmap.has("1"));
78+
79+
assertEquals(map.get("1"), undefined);
80+
assertEquals(iwmap.get("1"), undefined);
81+
82+
assertFalse(map.clear());
83+
assertFalse(iwmap.clear());
84+
85+
assertEquals(map.size, 0);
86+
assertEquals(iwmap.size, 0);
87+
88+
assertEquals(map.toString(), "[object Map]");
89+
assertEquals(iwmap.toString(), "[object InvertedWeakMap]");
90+
});
91+
92+
Deno.test("InvertedWeakMap, iterable", () => {
93+
const tuples: [number | string | boolean, Record<string, unknown>][] = [
94+
[1, {}],
95+
["2", {}],
96+
[
97+
false,
98+
{},
99+
],
100+
];
101+
102+
const maps = [
103+
new Map(tuples),
104+
new InvertedWeakMap(tuples),
105+
];
106+
107+
for (const map of maps) {
108+
assertEquals(map.size, 3);
109+
110+
{
111+
let i = 0;
112+
map.forEach(function (this: unknown, k, v, s) {
113+
assert(this === undefined);
114+
assert(k === tuples[i][1]);
115+
assert(v === tuples[i][0]);
116+
assert(s === map);
117+
i++;
118+
});
119+
assertFalse(i === 0);
120+
}
121+
{
122+
let i = 0;
123+
map.forEach(function (this: unknown, k, v, s) {
124+
assert(this === ":D");
125+
assert(k === tuples[i][1]);
126+
assert(v === tuples[i][0]);
127+
assert(s === map);
128+
i++;
129+
}, ":D");
130+
assertFalse(i === 0);
131+
}
132+
{
133+
let i = 0;
134+
for (const [k, v] of map) {
135+
assert(k === tuples[i][0]);
136+
assert(v === tuples[i++][1]);
137+
}
138+
assertFalse(i === 0);
139+
}
140+
{
141+
let i = 0;
142+
for (const k of map.keys()) {
143+
assert(k === tuples[i++][0]);
144+
}
145+
assertFalse(i === 0);
146+
}
147+
{
148+
let i = 0;
149+
for (const v of map.values()) {
150+
assert(v === tuples[i++][1]);
151+
}
152+
assertFalse(i === 0);
153+
}
154+
{
155+
let i = 0;
156+
for (const [k, v] of map.entries()) {
157+
assert(k === tuples[i][0]);
158+
assert(v === tuples[i++][1]);
159+
}
160+
assertFalse(i === 0);
161+
}
162+
}
163+
});
164+
165+
Deno.test("InvertedWeakMap, garbage collect", async () => {
166+
let removedCount = 0;
167+
let insertedCount = 0;
168+
const register = new FinalizationRegistry(() => {
169+
removedCount++;
170+
});
171+
172+
const map = new InvertedWeakMap();
173+
for (let i = 0; removedCount < 100; i++) {
174+
await new Promise((resolve) => setTimeout(resolve, 16));
175+
const data = {};
176+
map.set(i, data);
177+
register.register(data, i);
178+
insertedCount++;
179+
}
180+
181+
assertEquals(insertedCount - removedCount, map.size);
182+
assertEquals([...map].length, map.size);
183+
});

inverted_weak_map.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
// deno-lint-ignore-file ban-types
2+
3+
export class InvertedWeakMap<K, V extends object> implements Map<K, V> {
4+
#map = new Map<K, WeakRef<V>>();
5+
#registry = new FinalizationRegistry<K>(this.#map.delete.bind(this.#map));
6+
7+
constructor(entries?: readonly (readonly [K, V])[] | null);
8+
constructor(iterable: Iterable<readonly [K, V]>);
9+
constructor(iterable?: Iterable<readonly [K, V]> | null) {
10+
for (const [key, value] of iterable ?? []) {
11+
this.set(key, value);
12+
}
13+
}
14+
15+
get size(): number {
16+
return this.#map.size;
17+
}
18+
19+
clear(): void {
20+
for (const key of this.#map.keys()) {
21+
this.delete(key);
22+
}
23+
}
24+
25+
delete(key: K): boolean {
26+
const ref = this.#map.get(key);
27+
if (ref) {
28+
this.#map.delete(key);
29+
this.#registry.unregister(ref);
30+
return true;
31+
}
32+
return false;
33+
}
34+
35+
get(key: K): V | undefined {
36+
return this.#map.get(key)?.deref();
37+
}
38+
39+
has(key: K): boolean {
40+
return this.#map.has(key);
41+
}
42+
43+
set(key: K, value: V): this {
44+
const ref = new WeakRef(value);
45+
this.#map.set(key, ref);
46+
this.#registry.register(value, key, ref);
47+
return this;
48+
}
49+
50+
get [Symbol.toStringTag]() {
51+
return "InvertedWeakMap";
52+
}
53+
54+
forEach(
55+
callbackfn: (value: V, key: K, map: Map<K, V>) => void,
56+
thisArg?: unknown,
57+
): void {
58+
for (const [key, value] of this[Symbol.iterator]()) {
59+
callbackfn.call(thisArg, value, key, this);
60+
}
61+
}
62+
63+
*[Symbol.iterator](): IterableIterator<[K, V]> {
64+
for (const [key, ref] of this.#map) {
65+
yield [key, ref.deref()!];
66+
}
67+
}
68+
69+
entries(): IterableIterator<[K, V]> {
70+
return this[Symbol.iterator]();
71+
}
72+
73+
keys(): IterableIterator<K> {
74+
return this.#map.keys();
75+
}
76+
77+
*values(): IterableIterator<V> {
78+
for (const ref of this.#map.values()) {
79+
yield ref.deref()!;
80+
}
81+
}
82+
}

iterable_weak_map.test.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ Deno.test("IterableWeakMap, comparison Map, WeakMap", () => {
111111
});
112112

113113
Deno.test("IterableWeakMap, iterable", () => {
114-
const tuples: [{}, number][] = [[{}, 1], [{}, 2], [{}, 3]];
114+
const tuples: [Record<string, unknown>, number][] = [[{}, 1], [{}, 2], [
115+
{},
116+
3,
117+
]];
115118

116119
const maps = [
117120
new Map(tuples),

mod.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,4 @@
11
export { IterableWeakSet } from "./iterable_weak_set.ts";
22
export { IterableWeakMap } from "./iterable_weak_map.ts";
3+
4+
export { InvertedWeakMap } from "./inverted_weak_map.ts";

0 commit comments

Comments
 (0)