Skip to content

Commit e40cce4

Browse files
committed
Implement trackedSet
1 parent b1f59c7 commit e40cce4

File tree

4 files changed

+123
-69
lines changed

4 files changed

+123
-69
lines changed

packages/@glimmer/validator/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ if (Reflect.has(globalThis, GLIMMER_VALIDATOR_REGISTRATION)) {
99
Reflect.set(globalThis, GLIMMER_VALIDATOR_REGISTRATION, true);
1010

1111
export { trackedArray } from './lib/collections/array';
12+
export { trackedSet } from './lib/collections/set';
1213
export { trackedWeakMap } from './lib/collections/weak-map';
1314
export { trackedWeakSet } from './lib/collections/weak-set';
1415
export { debug } from './lib/debug';

packages/@glimmer/validator/lib/collections/set.ts

Lines changed: 108 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,160 +1,211 @@
1-
class TrackedSet<T = unknown> implements Set<T> {
2-
private collection = createStorage(null, () => false);
1+
import type { ReactiveOptions } from './types';
32

4-
private storages: Map<T, TrackedStorage<null>> = new Map();
3+
import { consumeTag } from '../tracking';
4+
import { createUpdatableTag, DIRTY_TAG } from '../validators';
55

6-
private vals: Set<T>;
6+
class TrackedSet<T extends WeakKey> implements Set<T> {
7+
#options: ReactiveOptions<T>;
8+
#collection = createUpdatableTag();
9+
#storages = new Map<T, ReturnType<typeof createUpdatableTag>>();
10+
#vals: Set<T>;
711

8-
private storageFor(key: T): TrackedStorage<null> {
9-
const storages = this.storages;
12+
#storageFor(key: T): ReturnType<typeof createUpdatableTag> {
13+
const storages = this.#storages;
1014
let storage = storages.get(key);
1115

1216
if (storage === undefined) {
13-
storage = createStorage(null, () => false);
17+
storage = createUpdatableTag();
1418
storages.set(key, storage);
1519
}
1620

1721
return storage;
1822
}
1923

20-
private dirtyStorageFor(key: T): void {
21-
const storage = this.storages.get(key);
24+
#dirtyStorageFor(key: T): void {
25+
const storage = this.#storages.get(key);
2226

2327
if (storage) {
24-
setValue(storage, null);
28+
DIRTY_TAG(storage);
2529
}
2630
}
2731

28-
constructor();
29-
constructor(values: readonly T[] | null);
30-
constructor(iterable: Iterable<T>);
31-
constructor(existing?: readonly T[] | Iterable<T> | null) {
32-
this.vals = new Set(existing);
32+
constructor(existing?: readonly T[] | Iterable<T> | null, options: ReactiveOptions<T>) {
33+
this.#vals = new Set(existing);
34+
this.#options = options;
3335
}
3436

3537
// **** KEY GETTERS ****
3638
has(value: T): boolean {
37-
getValue(this.storageFor(value));
39+
consumeTag(this.#storageFor(value));
3840

39-
return this.vals.has(value);
41+
return this.#vals.has(value);
4042
}
4143

4244
// **** ALL GETTERS ****
4345
entries() {
44-
getValue(this.collection);
46+
consumeTag(this.#collection);
4547

46-
return this.vals.entries();
48+
return this.#vals.entries();
4749
}
4850

4951
keys() {
50-
getValue(this.collection);
52+
consumeTag(this.#collection);
5153

52-
return this.vals.keys();
54+
return this.#vals.keys();
5355
}
5456

5557
values() {
56-
getValue(this.collection);
58+
consumeTag(this.#collection);
5759

58-
return this.vals.values();
60+
return this.#vals.values();
5961
}
6062

63+
// eslint-disable-next-line
64+
// @ts-ignore -- These Set types added in TS 5.5
6165
union<U>(other: ReadonlySetLike<U>): Set<T | U> {
62-
getValue(this.collection);
66+
consumeTag(this.#collection);
6367

64-
return this.vals.union(other);
68+
// eslint-disable-next-line
69+
// @ts-ignore -- These Set types added in TS 5.5
70+
return this.#vals.union(other);
6571
}
6672

73+
// eslint-disable-next-line
74+
// @ts-ignore -- These Set types added in TS 5.5
6775
intersection<U>(other: ReadonlySetLike<U>): Set<T & U> {
68-
getValue(this.collection);
76+
consumeTag(this.#collection);
6977

70-
return this.vals.intersection(other);
78+
// eslint-disable-next-line
79+
// @ts-ignore -- These Set types added in TS 5.5
80+
return this.#vals.intersection(other);
7181
}
7282

83+
// eslint-disable-next-line
84+
// @ts-ignore -- These Set types added in TS 5.5
7385
difference<U>(other: ReadonlySetLike<U>): Set<T> {
74-
getValue(this.collection);
86+
consumeTag(this.#collection);
7587

76-
return this.vals.difference(other);
88+
// eslint-disable-next-line
89+
// @ts-ignore -- These Set types added in TS 5.5
90+
return this.#vals.difference(other);
7791
}
7892

93+
// eslint-disable-next-line
94+
// @ts-ignore -- These Set types added in TS 5.5
7995
symmetricDifference<U>(other: ReadonlySetLike<U>): Set<T | U> {
80-
getValue(this.collection);
96+
consumeTag(this.#collection);
8197

82-
return this.vals.symmetricDifference(other);
98+
// eslint-disable-next-line
99+
// @ts-ignore -- These Set types added in TS 5.5
100+
return this.#vals.symmetricDifference(other);
83101
}
84102

103+
// eslint-disable-next-line
104+
// @ts-ignore -- These Set types added in TS 5.5
85105
isSubsetOf(other: ReadonlySetLike<unknown>): boolean {
86-
getValue(this.collection);
106+
consumeTag(this.#collection);
87107

88-
return this.vals.isSubsetOf(other);
108+
// eslint-disable-next-line
109+
// @ts-ignore -- These Set types added in TS 5.5
110+
return this.#vals.isSubsetOf(other);
89111
}
90112

113+
// eslint-disable-next-line
114+
// @ts-ignore -- These Set types added in TS 5.5
91115
isSupersetOf(other: ReadonlySetLike<unknown>): boolean {
92-
getValue(this.collection);
116+
consumeTag(this.#collection);
93117

94-
return this.vals.isSupersetOf(other);
118+
// eslint-disable-next-line
119+
// @ts-ignore -- These Set types added in TS 5.5
120+
return this.#vals.isSupersetOf(other);
95121
}
96122

123+
// eslint-disable-next-line
124+
// @ts-ignore -- These Set types added in TS 5.5
97125
isDisjointFrom(other: ReadonlySetLike<unknown>): boolean {
98-
getValue(this.collection);
126+
consumeTag(this.#collection);
99127

100-
return this.vals.isDisjointFrom(other);
128+
// eslint-disable-next-line
129+
// @ts-ignore -- These Set types added in TS 5.5
130+
return this.#vals.isDisjointFrom(other);
101131
}
102132

103133
forEach(fn: (value1: T, value2: T, set: Set<T>) => void): void {
104-
getValue(this.collection);
134+
consumeTag(this.#collection);
105135

106-
this.vals.forEach(fn);
136+
this.#vals.forEach(fn);
107137
}
108138

109139
get size(): number {
110-
getValue(this.collection);
140+
consumeTag(this.#collection);
111141

112-
return this.vals.size;
142+
return this.#vals.size;
113143
}
114144

115145
[Symbol.iterator]() {
116-
getValue(this.collection);
146+
consumeTag(this.#collection);
117147

118-
return this.vals[Symbol.iterator]();
148+
return this.#vals[Symbol.iterator]();
119149
}
120150

121151
get [Symbol.toStringTag](): string {
122-
return this.vals[Symbol.toStringTag];
152+
return this.#vals[Symbol.toStringTag];
123153
}
124154

125-
// **** KEY SETTERS ****
126155
add(value: T): this {
127-
this.dirtyStorageFor(value);
128-
setValue(this.collection, null);
156+
/**
157+
* In a WeakSet, there is no `.get()`, but if there was,
158+
* we could assume it's the same value as what we passed.
159+
*
160+
* So for a WeakSet, if we try to add something that already exists
161+
* we no-op.
162+
*
163+
* WeakSet already does this internally for us,
164+
* but we want the ability for the reactive behavior to reflect the same behavior.
165+
*
166+
* i.e.: doing weakSet.add(value) should never dirty with the defaults
167+
* if the `value` is already in the weakSet
168+
*/
169+
if (this.#vals.has(value)) {
170+
/**
171+
* This looks a little silly, where a always will === b,
172+
* but see the note above.
173+
*/
174+
let isUnchanged = this.#options.equals(value, value);
175+
if (isUnchanged) return this;
176+
}
177+
178+
this.#dirtyStorageFor(value);
179+
DIRTY_TAG(this.#collection);
129180

130-
this.vals.add(value);
181+
this.#vals.add(value);
131182

132183
return this;
133184
}
134185

135186
delete(value: T): boolean {
136-
this.dirtyStorageFor(value);
137-
setValue(this.collection, null);
187+
this.#dirtyStorageFor(value);
188+
DIRTY_TAG(this.#collection);
138189

139-
this.storages.delete(value);
140-
return this.vals.delete(value);
190+
this.#storages.delete(value);
191+
return this.#vals.delete(value);
141192
}
142193

143194
// **** ALL SETTERS ****
144195
clear(): void {
145-
this.storages.forEach((s) => setValue(s, null));
146-
setValue(this.collection, null);
196+
this.#storages.forEach((s) => DIRTY_TAG(s));
197+
DIRTY_TAG(this.#collection);
147198

148-
this.storages.clear();
149-
this.vals.clear();
199+
this.#storages.clear();
200+
this.#vals.clear();
150201
}
151202
}
152203

153204
// So instanceof works
154205
Object.setPrototypeOf(TrackedSet.prototype, Set.prototype);
155206

156207
export function trackedSet<Value = unknown>(
157-
data?: Set<Value>,
208+
data?: Set<Value> | Iterable<Value> | null,
158209
options?: { equals?: (a: Value, b: Value) => boolean; description?: string }
159210
): Set<Value> {
160211
return new TrackedSet(data ?? [], {
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export interface ReactiveOptions<Value> {
2+
equals: (a: Value, b: Value) => boolean;
3+
description: string | undefined;
4+
}

packages/@glimmer/validator/lib/collections/weak-map.ts

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,8 @@
1+
import type { ReactiveOptions } from './types';
2+
13
import { consumeTag } from '../tracking';
24
import { createUpdatableTag, DIRTY_TAG } from '../validators';
35

4-
interface ReactiveOptions<Value> {
5-
equals: (a: Value, b: Value) => boolean;
6-
description: string | undefined;
7-
}
8-
96
class TrackedWeakMap<K extends WeakKey = object, V = unknown> implements WeakMap<K, V> {
107
#options: ReactiveOptions<V>;
118
#storages = new WeakMap<K, ReturnType<typeof createUpdatableTag>>();
@@ -29,14 +26,15 @@ class TrackedWeakMap<K extends WeakKey = object, V = unknown> implements WeakMap
2926
}
3027
}
3128

32-
constructor(iterable: Iterable<readonly [K, V]> | readonly [K, V][] | null, options: ReactiveOptions<V>);
3329
constructor(
34-
existing?: readonly [K, V][] | Iterable<readonly [K, V]> | null,
30+
existing: [K, V][] | Iterable<readonly [K, V]> | WeakMap<K, V>,
3531
options: ReactiveOptions<V>
3632
) {
37-
// TypeScript doesn't correctly resolve the overloads for calling the `Map`
38-
// constructor for the no-value constructor. This resolves that.
39-
this.#vals = existing ? new WeakMap(existing) : new WeakMap();
33+
/**
34+
* SAFETY: note that wehn passing in an existing weak map, we can't
35+
* clone it as it is not iterable and not a supported type of structuredClone
36+
*/
37+
this.#vals = existing instanceof WeakMap ? existing : new WeakMap<K, V>(existing);
4038
this.#options = options;
4139
}
4240

@@ -86,10 +84,10 @@ class TrackedWeakMap<K extends WeakKey = object, V = unknown> implements WeakMap
8684
Object.setPrototypeOf(TrackedWeakMap.prototype, WeakMap.prototype);
8785

8886
export function trackedWeakMap<Key extends WeakKey, Value = unknown>(
89-
data?: WeakMap<Key, Value>,
87+
data?: WeakMap<Key, Value> | [Key, Value][] | Iterable<readonly [Key, Value]> | null,
9088
options?: { equals?: (a: Value, b: Value) => boolean; description?: string }
9189
): WeakMap<Key, Value> {
92-
return new TrackedWeakMap(data ?? [], {
90+
return new TrackedWeakMap<Key, Value>(data ?? [], {
9391
equals: options?.equals ?? Object.is,
9492
description: options?.description,
9593
});

0 commit comments

Comments
 (0)