Skip to content

Commit 417fba5

Browse files
committed
feat: add WeakValueMultiKeyMap and WeakValueMap
1 parent 47eb25a commit 417fba5

File tree

8 files changed

+678
-1
lines changed

8 files changed

+678
-1
lines changed

README.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,62 @@ console.log(map.get([provider1, "key2"])); // 3
298298
console.log([...map.keys()]); // [[{name: "1"}, "key1"], [{name: "2"}, "key1"], [{name: "1"}, "key2"]])
299299
```
300300

301+
### `WeakValueMultiKeyMap`
302+
`WeakValueMultiKeyMap` is a utility class that works like a [`MultiKeyMap`](#multikeymap), but doesn't keep strong references to the values.
303+
304+
When a value is garbage collected, it is automatically removed from the map.
305+
306+
```typescript
307+
import {WeakValueMultiKeyMap} from "lifecycle-utils";
308+
309+
type Provider = {name: string};
310+
311+
const map = new WeakValueMultiKeyMap<[type: string, name: string], Provider>();
312+
313+
{
314+
const provider1: Provider = {name: "1"};
315+
map.set(["type1", "key1"], provider1);
316+
317+
console.log(map.has(["type1", "key1"])); // true
318+
console.log(map.get(["type1", "key1"])); // {name: "1"}
319+
console.log(map.size); // 1
320+
}
321+
322+
await new Promise(resolve => setTimeout(resolve, 1000 * 60 * 10)); // wait for the runtime to run garbage collection
323+
324+
console.log(map.has(["type1", "key1"])); // false
325+
console.log(map.get(["type1", "key1"])); // undefined
326+
console.log(map.size); // 0
327+
```
328+
329+
### `WeakValueMap`
330+
`WeakValueMap` is a utility class that works like a `Map`, but doesn't keep strong references to the values.
331+
332+
When a value is garbage collected, it is automatically removed from the map.
333+
334+
```typescript
335+
import {WeakValueMap} from "lifecycle-utils";
336+
337+
type Provider = {name: string};
338+
339+
const map = new WeakValueMap<string, Provider>();
340+
341+
{
342+
const provider1: Provider = {name: "1"};
343+
map.set("provider1", provider1);
344+
345+
console.log(map.has("provider1")); // true
346+
console.log(map.get("provider1")); // {name: "1"}
347+
console.log(map.size); // 1
348+
}
349+
350+
await new Promise(resolve => setTimeout(resolve, 1000 * 60 * 10)); // wait for the runtime to run garbage collection
351+
352+
console.log(map.has("provider1")); // false
353+
console.log(map.get("provider1")); // undefined
354+
console.log(map.size); // 0
355+
```
356+
301357
### `LongTimeout`
302358
A timeout that can be set to a delay longer than the maximum timeout delay supported by a regular `setTimeout`.
303359

src/MultiKeyMap.ts

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,13 @@ export class MultiKeyMap<const Key extends readonly any[], const V> {
77
/** @internal */ public readonly _map: InternalMap<Key> = new Map();
88
/** @internal */ private readonly _keys = new Map<Key, V>();
99

10-
public constructor(entries?: readonly (readonly [key: Key, value: V])[] | MultiKeyMap<Key, V> | null) {
10+
public constructor(
11+
entries?:
12+
readonly (readonly [key: Key, value: V])[] |
13+
MultiKeyMap<Key, V> |
14+
ReadonlyMultiKeyMap<Key, V> |
15+
null
16+
) {
1117
if (entries != null) {
1218
for (const [key, value] of entries)
1319
this.set(key, value);

src/WeakValueMap.ts

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
/**
2+
* A utility class that works like a `Map`,
3+
* but does not keep strong references to the values (allowing them to be garbage collected).
4+
*
5+
* When a value is garbage collected, it is automatically removed from the map.
6+
*/
7+
export class WeakValueMap<const Key, const V extends object> {
8+
/** @internal */ private readonly _map = new Map<Key, InternalWeakValue<V>>();
9+
10+
public constructor(
11+
entries?: readonly (readonly [key: Key, value: V])[] |
12+
Map<Key, V> |
13+
ReadonlyMap<Key, V> |
14+
WeakValueMap<Key, V> |
15+
ReadonlyWeakValueMap<Key, V> |
16+
null
17+
) {
18+
if (entries != null) {
19+
for (const [key, value] of entries)
20+
this.set(key, value);
21+
}
22+
}
23+
24+
/**
25+
* Add or update a value for a given key.
26+
*
27+
* Time complexity: O(1), given that the length of the key is constant.
28+
*/
29+
public set(key: Readonly<Key>, value: V): this {
30+
const currentWeakValue = this._map.get(key);
31+
if (currentWeakValue != null) {
32+
const currentValue = currentWeakValue.ref.deref();
33+
34+
if (currentValue != null)
35+
currentWeakValue.tracker.unregister(currentValue);
36+
}
37+
38+
const weakValue: InternalWeakValue<V> = {
39+
ref: new WeakRef(value),
40+
tracker: null as any // will be set below
41+
};
42+
weakValue.tracker = new FinalizationRegistry<Readonly<Key>>(this._finalize.bind(this, weakValue));
43+
weakValue.tracker.register(value, key);
44+
45+
this._map.set(key, weakValue);
46+
return this;
47+
}
48+
49+
/**
50+
* Get a value for a given key.
51+
*
52+
* Time complexity: O(1), given that the length of the key is constant.
53+
*/
54+
public get(key: Readonly<Key>): V | undefined {
55+
const weakValue = this._map.get(key);
56+
if (weakValue == null)
57+
return undefined;
58+
59+
const value = weakValue.ref.deref();
60+
/* c8 ignore start */
61+
if (value == null) {
62+
this._map.delete(key);
63+
return undefined;
64+
} /* c8 ignore stop */
65+
66+
return value;
67+
}
68+
69+
/**
70+
* Check if a value exists for a given key.
71+
*
72+
* Time complexity: O(1), given that the length of the key is constant.
73+
*/
74+
public has(key: Readonly<Key>): boolean {
75+
return this.get(key) != null;
76+
}
77+
78+
/**
79+
* Delete the value for a given key.
80+
*
81+
* Time complexity: O(1), given that the length of the key is constant.
82+
*/
83+
public delete(key: Readonly<Key>): boolean {
84+
const weakValue = this._map.get(key);
85+
if (weakValue == null)
86+
return false;
87+
88+
const value = weakValue.ref.deref();
89+
if (value != null)
90+
weakValue.tracker.unregister(value);
91+
92+
this._map.delete(key);
93+
return true;
94+
}
95+
96+
/**
97+
* Clear all values from the map.
98+
*/
99+
public clear(): void {
100+
for (const [, weakValue] of this._map.entries()) {
101+
const value = weakValue.ref.deref();
102+
if (value != null)
103+
weakValue.tracker.unregister(value);
104+
}
105+
this._map.clear();
106+
}
107+
108+
/**
109+
* Get the number of entries in the map.
110+
*/
111+
public get size(): number {
112+
return this._map.size;
113+
}
114+
115+
/**
116+
* Get an iterator for all entries in the map.
117+
*/
118+
public *entries(): Generator<[key: Key, value: V]> {
119+
for (const [key, weakValue] of this._map.entries()) {
120+
const value = weakValue.ref.deref();
121+
if (value != null)
122+
yield [key, value];
123+
}
124+
}
125+
126+
/**
127+
* Get an iterator for all keys in the map.
128+
*/
129+
public *keys(): Generator<Key> {
130+
for (const [key] of this.entries())
131+
yield key;
132+
}
133+
134+
/**
135+
* Get an iterator for all values in the map.
136+
*/
137+
public *values(): Generator<V> {
138+
for (const [, value] of this.entries())
139+
yield value;
140+
}
141+
142+
/**
143+
* Call a function for each entry in the map.
144+
*/
145+
public forEach(callbackfn: (value: V, key: Key, map: this) => void, thisArg?: any): void {
146+
for (const [key, value] of this.entries()) {
147+
if (thisArg !== undefined)
148+
callbackfn.call(thisArg, value, key, this);
149+
else
150+
callbackfn.call(this, value, key, this);
151+
}
152+
}
153+
154+
public [Symbol.iterator](): Generator<[key: Key, value: V]> {
155+
return this.entries();
156+
}
157+
158+
/** @internal */
159+
private _finalize(value: InternalWeakValue<V>, key: Readonly<Key>) {
160+
const weakValue = this._map.get(key);
161+
if (weakValue === value)
162+
this._map.delete(key);
163+
}
164+
}
165+
166+
export type ReadonlyWeakValueMap<Key, V extends object> = Omit<WeakValueMap<Key, V>, "set" | "delete" | "clear">;
167+
168+
type InternalWeakValue<T extends object> = {
169+
ref: WeakRef<T>,
170+
tracker: FinalizationRegistry<any>
171+
};

0 commit comments

Comments
 (0)