Skip to content

Commit 1163aa5

Browse files
committed
IterableWeakMap
1 parent 05f8468 commit 1163aa5

File tree

1 file changed

+87
-2
lines changed

1 file changed

+87
-2
lines changed

packages/stack-shared/src/utils/maps.tsx

Lines changed: 87 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,93 @@
11
import { Result } from "./results";
22

3+
export class WeakRefIfAvailable<T extends object> {
4+
private readonly _ref: { deref: () => T | undefined };
5+
6+
constructor(value: T) {
7+
if (typeof WeakRef === "undefined") {
8+
this._ref = { deref: () => value };
9+
} else {
10+
this._ref = new WeakRef<T>(value);
11+
}
12+
}
13+
14+
deref(): T | undefined {
15+
return this._ref.deref();
16+
}
17+
}
18+
19+
20+
/**
21+
* A WeakMap-like object that can be iterated over.
22+
*
23+
* Note that it relies on WeakRef, and always falls back to the regular Map behavior in browsers that don't support it.
24+
*/
25+
export class IterableWeakMap<K extends object, V> {
26+
private readonly _weakMap: WeakMap<K & WeakKey, { value: V, keyRef: WeakRefIfAvailable<K & WeakKey> }>;
27+
private readonly _keyRefs: Set<WeakRefIfAvailable<K & WeakKey>>;
28+
29+
constructor(entries?: readonly (readonly [K, V])[] | null) {
30+
const mappedEntries = entries?.map((e) => [e[0], { value: e[1], keyRef: new WeakRefIfAvailable(e[0]) }] as const);
31+
this._weakMap = new WeakMap(mappedEntries ?? []);
32+
this._keyRefs = new Set(mappedEntries?.map((e) => e[1].keyRef) ?? []);
33+
}
34+
35+
get(key: K): V | undefined {
36+
return this._weakMap.get(key)?.value;
37+
}
38+
39+
set(key: K, value: V): this {
40+
const existing = this._weakMap.get(key);
41+
const updated = { value, keyRef: existing?.keyRef ?? new WeakRefIfAvailable(key) };
42+
this._weakMap.set(key, updated);
43+
this._keyRefs.add(updated.keyRef);
44+
return this;
45+
}
46+
47+
delete(key: K): boolean {
48+
const res = this._weakMap.get(key);
49+
if (res) {
50+
this._weakMap.delete(key);
51+
this._keyRefs.delete(res.keyRef);
52+
return true;
53+
}
54+
return false;
55+
}
56+
57+
has(key: K): boolean {
58+
return this._weakMap.has(key) && this._keyRefs.has(this._weakMap.get(key)!.keyRef);
59+
}
60+
61+
*[Symbol.iterator](): IterableIterator<[K, V]> {
62+
for (const keyRef of this._keyRefs) {
63+
const key = keyRef.deref();
64+
const existing = key ? this._weakMap.get(key) : undefined;
65+
if (!key) {
66+
// This can happen if the key was GCed. Remove it so the next iteration is faster.
67+
this._keyRefs.delete(keyRef);
68+
} else if (existing) {
69+
yield [key, existing.value];
70+
}
71+
}
72+
}
73+
74+
[Symbol.toStringTag] = "IterableWeakMap";
75+
}
76+
77+
/**
78+
* A map that is a IterableWeakMap for object keys and a regular Map for primitive keys. Also provides iteration over both
79+
* object and primitive keys.
80+
*
81+
* Note that, just like IterableWeakMap, it doesn't support primitive keys.
82+
*/
383
export class MaybeWeakMap<K, V> {
484
private readonly _primitiveMap: Map<K, V>;
5-
private readonly _weakMap: WeakMap<K & WeakKey, V>;
85+
private readonly _weakMap: IterableWeakMap<K & WeakKey, V>;
686

787
constructor(entries?: readonly (readonly [K, V])[] | null) {
888
const entriesArray = [...entries ?? []];
989
this._primitiveMap = new Map(entriesArray.filter((e) => !this._isAllowedInWeakMap(e[0])));
10-
this._weakMap = new WeakMap(entriesArray.filter((e): e is [K & WeakKey, V] => this._isAllowedInWeakMap(e[0])));
90+
this._weakMap = new IterableWeakMap(entriesArray.filter((e): e is [K & WeakKey, V] => this._isAllowedInWeakMap(e[0])));
1191
}
1292

1393
private _isAllowedInWeakMap(key: K): key is K & WeakKey {
@@ -47,6 +127,11 @@ export class MaybeWeakMap<K, V> {
47127
}
48128
}
49129

130+
*[Symbol.iterator](): IterableIterator<[K, V]> {
131+
yield* this._primitiveMap;
132+
yield* this._weakMap;
133+
}
134+
50135
[Symbol.toStringTag] = "MaybeWeakMap";
51136
}
52137

0 commit comments

Comments
 (0)