UNPKG

15.6 kBPlain TextView Raw
1import {
2 $mobx,
3 IEnhancer,
4 IInterceptable,
5 IInterceptor,
6 IListenable,
7 Lambda,
8 ObservableValue,
9 checkIfStateModificationsAreAllowed,
10 createAtom,
11 createInstanceofPredicate,
12 deepEnhancer,
13 getNextId,
14 getPlainObjectKeys,
15 hasInterceptors,
16 hasListeners,
17 interceptChange,
18 isES6Map,
19 isPlainObject,
20 isSpyEnabled,
21 makeIterable,
22 notifyListeners,
23 referenceEnhancer,
24 registerInterceptor,
25 registerListener,
26 spyReportEnd,
27 spyReportStart,
28 stringifyKey,
29 transaction,
30 untracked,
31 onBecomeUnobserved,
32 globalState,
33 die,
34 isFunction,
35 UPDATE,
36 IAtom
37} from "../internal"
38
39export interface IKeyValueMap<V = any> {
40 [key: string]: V
41}
42
43export type IMapEntry<K = any, V = any> = [K, V]
44export type IMapEntries<K = any, V = any> = IMapEntry<K, V>[]
45
46export type IMapDidChange<K = any, V = any> = { observableKind: "map"; debugObjectName: string } & (
47 | {
48 object: ObservableMap<K, V>
49 name: K // actual the key or index, but this is based on the ancient .observe proposal for consistency
50 type: "update"
51 newValue: V
52 oldValue: V
53 }
54 | {
55 object: ObservableMap<K, V>
56 name: K
57 type: "add"
58 newValue: V
59 }
60 | {
61 object: ObservableMap<K, V>
62 name: K
63 type: "delete"
64 oldValue: V
65 }
66)
67
68export interface IMapWillChange<K = any, V = any> {
69 object: ObservableMap<K, V>
70 type: "update" | "add" | "delete"
71 name: K
72 newValue?: V
73}
74
75const ObservableMapMarker = {}
76
77export const ADD = "add"
78export const DELETE = "delete"
79
80export type IObservableMapInitialValues<K = any, V = any> =
81 | IMapEntries<K, V>
82 | IKeyValueMap<V>
83 | Map<K, V>
84
85// just extend Map? See also https://gist.github.com/nestharus/13b4d74f2ef4a2f4357dbd3fc23c1e54
86// But: https://github.com/mobxjs/mobx/issues/1556
87export class ObservableMap<K = any, V = any>
88 implements Map<K, V>, IInterceptable<IMapWillChange<K, V>>, IListenable {
89 [$mobx] = ObservableMapMarker
90 data_: Map<K, ObservableValue<V>>
91 hasMap_: Map<K, ObservableValue<boolean>> // hasMap, not hashMap >-).
92 keysAtom_: IAtom
93 interceptors_
94 changeListeners_
95 dehancer: any
96
97 constructor(
98 initialData?: IObservableMapInitialValues<K, V>,
99 public enhancer_: IEnhancer<V> = deepEnhancer,
100 public name_ = __DEV__ ? "ObservableMap@" + getNextId() : "ObservableMap"
101 ) {
102 if (!isFunction(Map)) {
103 die(18)
104 }
105 this.keysAtom_ = createAtom(__DEV__ ? `${this.name_}.keys()` : "ObservableMap.keys()")
106 this.data_ = new Map()
107 this.hasMap_ = new Map()
108 this.merge(initialData)
109 }
110
111 private has_(key: K): boolean {
112 return this.data_.has(key)
113 }
114
115 has(key: K): boolean {
116 if (!globalState.trackingDerivation) return this.has_(key)
117
118 let entry = this.hasMap_.get(key)
119 if (!entry) {
120 const newEntry = (entry = new ObservableValue(
121 this.has_(key),
122 referenceEnhancer,
123 __DEV__ ? `${this.name_}.${stringifyKey(key)}?` : "ObservableMap.key?",
124 false
125 ))
126 this.hasMap_.set(key, newEntry)
127 onBecomeUnobserved(newEntry, () => this.hasMap_.delete(key))
128 }
129
130 return entry.get()
131 }
132
133 set(key: K, value: V) {
134 const hasKey = this.has_(key)
135 if (hasInterceptors(this)) {
136 const change = interceptChange<IMapWillChange<K, V>>(this, {
137 type: hasKey ? UPDATE : ADD,
138 object: this,
139 newValue: value,
140 name: key
141 })
142 if (!change) return this
143 value = change.newValue!
144 }
145 if (hasKey) {
146 this.updateValue_(key, value)
147 } else {
148 this.addValue_(key, value)
149 }
150 return this
151 }
152
153 delete(key: K): boolean {
154 checkIfStateModificationsAreAllowed(this.keysAtom_)
155 if (hasInterceptors(this)) {
156 const change = interceptChange<IMapWillChange<K, V>>(this, {
157 type: DELETE,
158 object: this,
159 name: key
160 })
161 if (!change) return false
162 }
163 if (this.has_(key)) {
164 const notifySpy = isSpyEnabled()
165 const notify = hasListeners(this)
166 const change: IMapDidChange<K, V> | null =
167 notify || notifySpy
168 ? {
169 observableKind: "map",
170 debugObjectName: this.name_,
171 type: DELETE,
172 object: this,
173 oldValue: (<any>this.data_.get(key)).value_,
174 name: key
175 }
176 : null
177
178 if (__DEV__ && notifySpy) spyReportStart(change!)
179 transaction(() => {
180 this.keysAtom_.reportChanged()
181 this.hasMap_.get(key)?.setNewValue_(false)
182 const observable = this.data_.get(key)!
183 observable.setNewValue_(undefined as any)
184 this.data_.delete(key)
185 })
186 if (notify) notifyListeners(this, change)
187 if (__DEV__ && notifySpy) spyReportEnd()
188 return true
189 }
190 return false
191 }
192
193 private updateValue_(key: K, newValue: V | undefined) {
194 const observable = this.data_.get(key)!
195 newValue = (observable as any).prepareNewValue_(newValue) as V
196 if (newValue !== globalState.UNCHANGED) {
197 const notifySpy = isSpyEnabled()
198 const notify = hasListeners(this)
199 const change: IMapDidChange<K, V> | null =
200 notify || notifySpy
201 ? {
202 observableKind: "map",
203 debugObjectName: this.name_,
204 type: UPDATE,
205 object: this,
206 oldValue: (observable as any).value_,
207 name: key,
208 newValue
209 }
210 : null
211 if (__DEV__ && notifySpy) spyReportStart(change!)
212 observable.setNewValue_(newValue as V)
213 if (notify) notifyListeners(this, change)
214 if (__DEV__ && notifySpy) spyReportEnd()
215 }
216 }
217
218 private addValue_(key: K, newValue: V) {
219 checkIfStateModificationsAreAllowed(this.keysAtom_)
220 transaction(() => {
221 const observable = new ObservableValue(
222 newValue,
223 this.enhancer_,
224 __DEV__ ? `${this.name_}.${stringifyKey(key)}` : "ObservableMap.key",
225 false
226 )
227 this.data_.set(key, observable)
228 newValue = (observable as any).value_ // value might have been changed
229 this.hasMap_.get(key)?.setNewValue_(true)
230 this.keysAtom_.reportChanged()
231 })
232 const notifySpy = isSpyEnabled()
233 const notify = hasListeners(this)
234 const change: IMapDidChange<K, V> | null =
235 notify || notifySpy
236 ? {
237 observableKind: "map",
238 debugObjectName: this.name_,
239 type: ADD,
240 object: this,
241 name: key,
242 newValue
243 }
244 : null
245 if (__DEV__ && notifySpy) spyReportStart(change!)
246 if (notify) notifyListeners(this, change)
247 if (__DEV__ && notifySpy) spyReportEnd()
248 }
249
250 get(key: K): V | undefined {
251 if (this.has(key)) return this.dehanceValue_(this.data_.get(key)!.get())
252 return this.dehanceValue_(undefined)
253 }
254
255 private dehanceValue_<X extends V | undefined>(value: X): X {
256 if (this.dehancer !== undefined) {
257 return this.dehancer(value)
258 }
259 return value
260 }
261
262 keys(): IterableIterator<K> {
263 this.keysAtom_.reportObserved()
264 return this.data_.keys()
265 }
266
267 values(): IterableIterator<V> {
268 const self = this
269 const keys = this.keys()
270 return makeIterable({
271 next() {
272 const { done, value } = keys.next()
273 return {
274 done,
275 value: done ? (undefined as any) : self.get(value)
276 }
277 }
278 })
279 }
280
281 entries(): IterableIterator<IMapEntry<K, V>> {
282 const self = this
283 const keys = this.keys()
284 return makeIterable({
285 next() {
286 const { done, value } = keys.next()
287 return {
288 done,
289 value: done ? (undefined as any) : ([value, self.get(value)!] as [K, V])
290 }
291 }
292 })
293 }
294
295 [Symbol.iterator]() {
296 return this.entries()
297 }
298
299 forEach(callback: (value: V, key: K, object: Map<K, V>) => void, thisArg?) {
300 for (const [key, value] of this) callback.call(thisArg, value, key, this)
301 }
302
303 /** Merge another object into this object, returns this. */
304 merge(other: ObservableMap<K, V> | IKeyValueMap<V> | any): ObservableMap<K, V> {
305 if (isObservableMap(other)) {
306 other = new Map(other)
307 }
308 transaction(() => {
309 if (isPlainObject(other))
310 getPlainObjectKeys(other).forEach((key: any) =>
311 this.set((key as any) as K, other[key])
312 )
313 else if (Array.isArray(other)) other.forEach(([key, value]) => this.set(key, value))
314 else if (isES6Map(other)) {
315 if (other.constructor !== Map) die(19, other)
316 other.forEach((value, key) => this.set(key, value))
317 } else if (other !== null && other !== undefined) die(20, other)
318 })
319 return this
320 }
321
322 clear() {
323 transaction(() => {
324 untracked(() => {
325 for (const key of this.keys()) this.delete(key)
326 })
327 })
328 }
329
330 replace(values: ObservableMap<K, V> | IKeyValueMap<V> | any): ObservableMap<K, V> {
331 // Implementation requirements:
332 // - respect ordering of replacement map
333 // - allow interceptors to run and potentially prevent individual operations
334 // - don't recreate observables that already exist in original map (so we don't destroy existing subscriptions)
335 // - don't _keysAtom.reportChanged if the keys of resulting map are indentical (order matters!)
336 // - note that result map may differ from replacement map due to the interceptors
337 transaction(() => {
338 // Convert to map so we can do quick key lookups
339 const replacementMap = convertToMap(values)
340 const orderedData = new Map()
341 // Used for optimization
342 let keysReportChangedCalled = false
343 // Delete keys that don't exist in replacement map
344 // if the key deletion is prevented by interceptor
345 // add entry at the beginning of the result map
346 for (const key of this.data_.keys()) {
347 // Concurrently iterating/deleting keys
348 // iterator should handle this correctly
349 if (!replacementMap.has(key)) {
350 const deleted = this.delete(key)
351 // Was the key removed?
352 if (deleted) {
353 // _keysAtom.reportChanged() was already called
354 keysReportChangedCalled = true
355 } else {
356 // Delete prevented by interceptor
357 const value = this.data_.get(key)
358 orderedData.set(key, value)
359 }
360 }
361 }
362 // Merge entries
363 for (const [key, value] of replacementMap.entries()) {
364 // We will want to know whether a new key is added
365 const keyExisted = this.data_.has(key)
366 // Add or update value
367 this.set(key, value)
368 // The addition could have been prevent by interceptor
369 if (this.data_.has(key)) {
370 // The update could have been prevented by interceptor
371 // and also we want to preserve existing values
372 // so use value from _data map (instead of replacement map)
373 const value = this.data_.get(key)
374 orderedData.set(key, value)
375 // Was a new key added?
376 if (!keyExisted) {
377 // _keysAtom.reportChanged() was already called
378 keysReportChangedCalled = true
379 }
380 }
381 }
382 // Check for possible key order change
383 if (!keysReportChangedCalled) {
384 if (this.data_.size !== orderedData.size) {
385 // If size differs, keys are definitely modified
386 this.keysAtom_.reportChanged()
387 } else {
388 const iter1 = this.data_.keys()
389 const iter2 = orderedData.keys()
390 let next1 = iter1.next()
391 let next2 = iter2.next()
392 while (!next1.done) {
393 if (next1.value !== next2.value) {
394 this.keysAtom_.reportChanged()
395 break
396 }
397 next1 = iter1.next()
398 next2 = iter2.next()
399 }
400 }
401 }
402 // Use correctly ordered map
403 this.data_ = orderedData
404 })
405 return this
406 }
407
408 get size(): number {
409 this.keysAtom_.reportObserved()
410 return this.data_.size
411 }
412
413 toString(): string {
414 return "[object ObservableMap]"
415 }
416
417 toJSON(): [K, V][] {
418 return Array.from(this)
419 }
420
421 get [Symbol.toStringTag]() {
422 return "Map"
423 }
424
425 /**
426 * Observes this object. Triggers for the events 'add', 'update' and 'delete'.
427 * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/observe
428 * for callback details
429 */
430 observe_(listener: (changes: IMapDidChange<K, V>) => void, fireImmediately?: boolean): Lambda {
431 if (__DEV__ && fireImmediately === true)
432 die("`observe` doesn't support fireImmediately=true in combination with maps.")
433 return registerListener(this, listener)
434 }
435
436 intercept_(handler: IInterceptor<IMapWillChange<K, V>>): Lambda {
437 return registerInterceptor(this, handler)
438 }
439}
440
441// eslint-disable-next-line
442export var isObservableMap = createInstanceofPredicate("ObservableMap", ObservableMap) as (
443 thing: any
444) => thing is ObservableMap<any, any>
445
446function convertToMap(dataStructure: any): Map<any, any> {
447 if (isES6Map(dataStructure) || isObservableMap(dataStructure)) {
448 return dataStructure
449 } else if (Array.isArray(dataStructure)) {
450 return new Map(dataStructure)
451 } else if (isPlainObject(dataStructure)) {
452 const map = new Map()
453 for (const key in dataStructure) {
454 map.set(key, dataStructure[key])
455 }
456 return map
457 } else {
458 return die(21, dataStructure)
459 }
460}