UNPKG

7.87 kBPlain TextView Raw
1import {
2 $mobx,
3 createAtom,
4 deepEnhancer,
5 getNextId,
6 IEnhancer,
7 isSpyEnabled,
8 hasListeners,
9 IListenable,
10 registerListener,
11 Lambda,
12 spyReportStart,
13 notifyListeners,
14 spyReportEnd,
15 createInstanceofPredicate,
16 hasInterceptors,
17 interceptChange,
18 IInterceptable,
19 IInterceptor,
20 registerInterceptor,
21 checkIfStateModificationsAreAllowed,
22 untracked,
23 makeIterable,
24 transaction,
25 isES6Set,
26 IAtom,
27 DELETE,
28 ADD,
29 die,
30 isFunction
31} from "../internal"
32
33const ObservableSetMarker = {}
34
35export type IObservableSetInitialValues<T> = Set<T> | readonly T[]
36
37export type ISetDidChange<T = any> =
38 | {
39 object: ObservableSet<T>
40 observableKind: "set"
41 debugObjectName: string
42 type: "add"
43 newValue: T
44 }
45 | {
46 object: ObservableSet<T>
47 observableKind: "set"
48 debugObjectName: string
49 type: "delete"
50 oldValue: T
51 }
52
53export type ISetWillChange<T = any> =
54 | {
55 type: "delete"
56 object: ObservableSet<T>
57 oldValue: T
58 }
59 | {
60 type: "add"
61 object: ObservableSet<T>
62 newValue: T
63 }
64
65export class ObservableSet<T = any> implements Set<T>, IInterceptable<ISetWillChange>, IListenable {
66 [$mobx] = ObservableSetMarker
67 private data_: Set<any> = new Set()
68 private atom_: IAtom
69 changeListeners_
70 interceptors_
71 dehancer: any
72 enhancer_: (newV: any, oldV: any | undefined) => any
73
74 constructor(
75 initialData?: IObservableSetInitialValues<T>,
76 enhancer: IEnhancer<T> = deepEnhancer,
77 public name_ = __DEV__ ? "ObservableSet@" + getNextId() : "ObservableSet"
78 ) {
79 if (!isFunction(Set)) {
80 die(22)
81 }
82 this.atom_ = createAtom(this.name_)
83 this.enhancer_ = (newV, oldV) => enhancer(newV, oldV, name_)
84 if (initialData) {
85 this.replace(initialData)
86 }
87 }
88
89 private dehanceValue_<X extends T | undefined>(value: X): X {
90 if (this.dehancer !== undefined) {
91 return this.dehancer(value)
92 }
93 return value
94 }
95
96 clear() {
97 transaction(() => {
98 untracked(() => {
99 for (const value of this.data_.values()) this.delete(value)
100 })
101 })
102 }
103
104 forEach(callbackFn: (value: T, value2: T, set: Set<T>) => void, thisArg?: any) {
105 for (const value of this) {
106 callbackFn.call(thisArg, value, value, this)
107 }
108 }
109
110 get size() {
111 this.atom_.reportObserved()
112 return this.data_.size
113 }
114
115 add(value: T) {
116 checkIfStateModificationsAreAllowed(this.atom_)
117 if (hasInterceptors(this)) {
118 const change = interceptChange<ISetWillChange<T>>(this, {
119 type: ADD,
120 object: this,
121 newValue: value
122 })
123 if (!change) return this
124 // ideally, value = change.value would be done here, so that values can be
125 // changed by interceptor. Same applies for other Set and Map api's.
126 }
127 if (!this.has(value)) {
128 transaction(() => {
129 this.data_.add(this.enhancer_(value, undefined))
130 this.atom_.reportChanged()
131 })
132 const notifySpy = __DEV__ && isSpyEnabled()
133 const notify = hasListeners(this)
134 const change =
135 notify || notifySpy
136 ? <ISetDidChange<T>>{
137 observableKind: "set",
138 debugObjectName: this.name_,
139 type: ADD,
140 object: this,
141 newValue: value
142 }
143 : null
144 if (notifySpy && __DEV__) spyReportStart(change!)
145 if (notify) notifyListeners(this, change)
146 if (notifySpy && __DEV__) spyReportEnd()
147 }
148
149 return this
150 }
151
152 delete(value: any) {
153 if (hasInterceptors(this)) {
154 const change = interceptChange<ISetWillChange<T>>(this, {
155 type: DELETE,
156 object: this,
157 oldValue: value
158 })
159 if (!change) return false
160 }
161 if (this.has(value)) {
162 const notifySpy = __DEV__ && isSpyEnabled()
163 const notify = hasListeners(this)
164 const change =
165 notify || notifySpy
166 ? <ISetDidChange<T>>{
167 observableKind: "set",
168 debugObjectName: this.name_,
169 type: DELETE,
170 object: this,
171 oldValue: value
172 }
173 : null
174
175 if (notifySpy && __DEV__) spyReportStart(change!)
176 transaction(() => {
177 this.atom_.reportChanged()
178 this.data_.delete(value)
179 })
180 if (notify) notifyListeners(this, change)
181 if (notifySpy && __DEV__) spyReportEnd()
182 return true
183 }
184 return false
185 }
186
187 has(value: any) {
188 this.atom_.reportObserved()
189 return this.data_.has(this.dehanceValue_(value))
190 }
191
192 entries() {
193 let nextIndex = 0
194 const keys = Array.from(this.keys())
195 const values = Array.from(this.values())
196 return makeIterable<[T, T]>({
197 next() {
198 const index = nextIndex
199 nextIndex += 1
200 return index < values.length
201 ? { value: [keys[index], values[index]], done: false }
202 : { done: true }
203 }
204 } as any)
205 }
206
207 keys(): IterableIterator<T> {
208 return this.values()
209 }
210
211 values(): IterableIterator<T> {
212 this.atom_.reportObserved()
213 const self = this
214 let nextIndex = 0
215 const observableValues = Array.from(this.data_.values())
216 return makeIterable<T>({
217 next() {
218 return nextIndex < observableValues.length
219 ? { value: self.dehanceValue_(observableValues[nextIndex++]), done: false }
220 : { done: true }
221 }
222 } as any)
223 }
224
225 replace(other: ObservableSet<T> | IObservableSetInitialValues<T>): ObservableSet<T> {
226 if (isObservableSet(other)) {
227 other = new Set(other)
228 }
229
230 transaction(() => {
231 if (Array.isArray(other)) {
232 this.clear()
233 other.forEach(value => this.add(value))
234 } else if (isES6Set(other)) {
235 this.clear()
236 other.forEach(value => this.add(value))
237 } else if (other !== null && other !== undefined) {
238 die("Cannot initialize set from " + other)
239 }
240 })
241
242 return this
243 }
244 observe_(listener: (changes: ISetDidChange<T>) => void, fireImmediately?: boolean): Lambda {
245 // ... 'fireImmediately' could also be true?
246 if (__DEV__ && fireImmediately === true)
247 die("`observe` doesn't support fireImmediately=true in combination with sets.")
248 return registerListener(this, listener)
249 }
250
251 intercept_(handler: IInterceptor<ISetWillChange<T>>): Lambda {
252 return registerInterceptor(this, handler)
253 }
254
255 toJSON(): T[] {
256 return Array.from(this)
257 }
258
259 toString(): string {
260 return "[object ObservableSet]"
261 }
262
263 [Symbol.iterator]() {
264 return this.values()
265 }
266
267 get [Symbol.toStringTag]() {
268 return "Set"
269 }
270}
271
272// eslint-disable-next-line
273export var isObservableSet = createInstanceofPredicate("ObservableSet", ObservableSet) as (
274 thing: any
275) => thing is ObservableSet<any>