|
1 |
| -class TrackedSet<T = unknown> implements Set<T> { |
2 |
| - private collection = createStorage(null, () => false); |
| 1 | +import type { ReactiveOptions } from './types'; |
3 | 2 |
|
4 |
| - private storages: Map<T, TrackedStorage<null>> = new Map(); |
| 3 | +import { consumeTag } from '../tracking'; |
| 4 | +import { createUpdatableTag, DIRTY_TAG } from '../validators'; |
5 | 5 |
|
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>; |
7 | 11 |
|
8 |
| - private storageFor(key: T): TrackedStorage<null> { |
9 |
| - const storages = this.storages; |
| 12 | + #storageFor(key: T): ReturnType<typeof createUpdatableTag> { |
| 13 | + const storages = this.#storages; |
10 | 14 | let storage = storages.get(key);
|
11 | 15 |
|
12 | 16 | if (storage === undefined) {
|
13 |
| - storage = createStorage(null, () => false); |
| 17 | + storage = createUpdatableTag(); |
14 | 18 | storages.set(key, storage);
|
15 | 19 | }
|
16 | 20 |
|
17 | 21 | return storage;
|
18 | 22 | }
|
19 | 23 |
|
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); |
22 | 26 |
|
23 | 27 | if (storage) {
|
24 |
| - setValue(storage, null); |
| 28 | + DIRTY_TAG(storage); |
25 | 29 | }
|
26 | 30 | }
|
27 | 31 |
|
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; |
33 | 35 | }
|
34 | 36 |
|
35 | 37 | // **** KEY GETTERS ****
|
36 | 38 | has(value: T): boolean {
|
37 |
| - getValue(this.storageFor(value)); |
| 39 | + consumeTag(this.#storageFor(value)); |
38 | 40 |
|
39 |
| - return this.vals.has(value); |
| 41 | + return this.#vals.has(value); |
40 | 42 | }
|
41 | 43 |
|
42 | 44 | // **** ALL GETTERS ****
|
43 | 45 | entries() {
|
44 |
| - getValue(this.collection); |
| 46 | + consumeTag(this.#collection); |
45 | 47 |
|
46 |
| - return this.vals.entries(); |
| 48 | + return this.#vals.entries(); |
47 | 49 | }
|
48 | 50 |
|
49 | 51 | keys() {
|
50 |
| - getValue(this.collection); |
| 52 | + consumeTag(this.#collection); |
51 | 53 |
|
52 |
| - return this.vals.keys(); |
| 54 | + return this.#vals.keys(); |
53 | 55 | }
|
54 | 56 |
|
55 | 57 | values() {
|
56 |
| - getValue(this.collection); |
| 58 | + consumeTag(this.#collection); |
57 | 59 |
|
58 |
| - return this.vals.values(); |
| 60 | + return this.#vals.values(); |
59 | 61 | }
|
60 | 62 |
|
| 63 | + // eslint-disable-next-line |
| 64 | + // @ts-ignore -- These Set types added in TS 5.5 |
61 | 65 | union<U>(other: ReadonlySetLike<U>): Set<T | U> {
|
62 |
| - getValue(this.collection); |
| 66 | + consumeTag(this.#collection); |
63 | 67 |
|
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); |
65 | 71 | }
|
66 | 72 |
|
| 73 | + // eslint-disable-next-line |
| 74 | + // @ts-ignore -- These Set types added in TS 5.5 |
67 | 75 | intersection<U>(other: ReadonlySetLike<U>): Set<T & U> {
|
68 |
| - getValue(this.collection); |
| 76 | + consumeTag(this.#collection); |
69 | 77 |
|
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); |
71 | 81 | }
|
72 | 82 |
|
| 83 | + // eslint-disable-next-line |
| 84 | + // @ts-ignore -- These Set types added in TS 5.5 |
73 | 85 | difference<U>(other: ReadonlySetLike<U>): Set<T> {
|
74 |
| - getValue(this.collection); |
| 86 | + consumeTag(this.#collection); |
75 | 87 |
|
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); |
77 | 91 | }
|
78 | 92 |
|
| 93 | + // eslint-disable-next-line |
| 94 | + // @ts-ignore -- These Set types added in TS 5.5 |
79 | 95 | symmetricDifference<U>(other: ReadonlySetLike<U>): Set<T | U> {
|
80 |
| - getValue(this.collection); |
| 96 | + consumeTag(this.#collection); |
81 | 97 |
|
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); |
83 | 101 | }
|
84 | 102 |
|
| 103 | + // eslint-disable-next-line |
| 104 | + // @ts-ignore -- These Set types added in TS 5.5 |
85 | 105 | isSubsetOf(other: ReadonlySetLike<unknown>): boolean {
|
86 |
| - getValue(this.collection); |
| 106 | + consumeTag(this.#collection); |
87 | 107 |
|
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); |
89 | 111 | }
|
90 | 112 |
|
| 113 | + // eslint-disable-next-line |
| 114 | + // @ts-ignore -- These Set types added in TS 5.5 |
91 | 115 | isSupersetOf(other: ReadonlySetLike<unknown>): boolean {
|
92 |
| - getValue(this.collection); |
| 116 | + consumeTag(this.#collection); |
93 | 117 |
|
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); |
95 | 121 | }
|
96 | 122 |
|
| 123 | + // eslint-disable-next-line |
| 124 | + // @ts-ignore -- These Set types added in TS 5.5 |
97 | 125 | isDisjointFrom(other: ReadonlySetLike<unknown>): boolean {
|
98 |
| - getValue(this.collection); |
| 126 | + consumeTag(this.#collection); |
99 | 127 |
|
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); |
101 | 131 | }
|
102 | 132 |
|
103 | 133 | forEach(fn: (value1: T, value2: T, set: Set<T>) => void): void {
|
104 |
| - getValue(this.collection); |
| 134 | + consumeTag(this.#collection); |
105 | 135 |
|
106 |
| - this.vals.forEach(fn); |
| 136 | + this.#vals.forEach(fn); |
107 | 137 | }
|
108 | 138 |
|
109 | 139 | get size(): number {
|
110 |
| - getValue(this.collection); |
| 140 | + consumeTag(this.#collection); |
111 | 141 |
|
112 |
| - return this.vals.size; |
| 142 | + return this.#vals.size; |
113 | 143 | }
|
114 | 144 |
|
115 | 145 | [Symbol.iterator]() {
|
116 |
| - getValue(this.collection); |
| 146 | + consumeTag(this.#collection); |
117 | 147 |
|
118 |
| - return this.vals[Symbol.iterator](); |
| 148 | + return this.#vals[Symbol.iterator](); |
119 | 149 | }
|
120 | 150 |
|
121 | 151 | get [Symbol.toStringTag](): string {
|
122 |
| - return this.vals[Symbol.toStringTag]; |
| 152 | + return this.#vals[Symbol.toStringTag]; |
123 | 153 | }
|
124 | 154 |
|
125 |
| - // **** KEY SETTERS **** |
126 | 155 | 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); |
129 | 180 |
|
130 |
| - this.vals.add(value); |
| 181 | + this.#vals.add(value); |
131 | 182 |
|
132 | 183 | return this;
|
133 | 184 | }
|
134 | 185 |
|
135 | 186 | delete(value: T): boolean {
|
136 |
| - this.dirtyStorageFor(value); |
137 |
| - setValue(this.collection, null); |
| 187 | + this.#dirtyStorageFor(value); |
| 188 | + DIRTY_TAG(this.#collection); |
138 | 189 |
|
139 |
| - this.storages.delete(value); |
140 |
| - return this.vals.delete(value); |
| 190 | + this.#storages.delete(value); |
| 191 | + return this.#vals.delete(value); |
141 | 192 | }
|
142 | 193 |
|
143 | 194 | // **** ALL SETTERS ****
|
144 | 195 | 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); |
147 | 198 |
|
148 |
| - this.storages.clear(); |
149 |
| - this.vals.clear(); |
| 199 | + this.#storages.clear(); |
| 200 | + this.#vals.clear(); |
150 | 201 | }
|
151 | 202 | }
|
152 | 203 |
|
153 | 204 | // So instanceof works
|
154 | 205 | Object.setPrototypeOf(TrackedSet.prototype, Set.prototype);
|
155 | 206 |
|
156 | 207 | export function trackedSet<Value = unknown>(
|
157 |
| - data?: Set<Value>, |
| 208 | + data?: Set<Value> | Iterable<Value> | null, |
158 | 209 | options?: { equals?: (a: Value, b: Value) => boolean; description?: string }
|
159 | 210 | ): Set<Value> {
|
160 | 211 | return new TrackedSet(data ?? [], {
|
|
0 commit comments