1 | import {
|
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 |
|
39 | export interface IKeyValueMap<V = any> {
|
40 | [key: string]: V
|
41 | }
|
42 |
|
43 | export type IMapEntry<K = any, V = any> = [K, V]
|
44 | export type IMapEntries<K = any, V = any> = IMapEntry<K, V>[]
|
45 |
|
46 | export type IMapDidChange<K = any, V = any> = { observableKind: "map"; debugObjectName: string } & (
|
47 | | {
|
48 | object: ObservableMap<K, V>
|
49 | name: K
|
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 |
|
68 | export interface IMapWillChange<K = any, V = any> {
|
69 | object: ObservableMap<K, V>
|
70 | type: "update" | "add" | "delete"
|
71 | name: K
|
72 | newValue?: V
|
73 | }
|
74 |
|
75 | const ObservableMapMarker = {}
|
76 |
|
77 | export const ADD = "add"
|
78 | export const DELETE = "delete"
|
79 |
|
80 | export type IObservableMapInitialValues<K = any, V = any> =
|
81 | | IMapEntries<K, V>
|
82 | | IKeyValueMap<V>
|
83 | | Map<K, V>
|
84 |
|
85 |
|
86 |
|
87 | export 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>>
|
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_
|
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 |
|
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 |
|
332 |
|
333 |
|
334 |
|
335 |
|
336 |
|
337 | transaction(() => {
|
338 |
|
339 | const replacementMap = convertToMap(values)
|
340 | const orderedData = new Map()
|
341 |
|
342 | let keysReportChangedCalled = false
|
343 |
|
344 |
|
345 |
|
346 | for (const key of this.data_.keys()) {
|
347 |
|
348 |
|
349 | if (!replacementMap.has(key)) {
|
350 | const deleted = this.delete(key)
|
351 |
|
352 | if (deleted) {
|
353 |
|
354 | keysReportChangedCalled = true
|
355 | } else {
|
356 |
|
357 | const value = this.data_.get(key)
|
358 | orderedData.set(key, value)
|
359 | }
|
360 | }
|
361 | }
|
362 |
|
363 | for (const [key, value] of replacementMap.entries()) {
|
364 |
|
365 | const keyExisted = this.data_.has(key)
|
366 |
|
367 | this.set(key, value)
|
368 |
|
369 | if (this.data_.has(key)) {
|
370 |
|
371 |
|
372 |
|
373 | const value = this.data_.get(key)
|
374 | orderedData.set(key, value)
|
375 |
|
376 | if (!keyExisted) {
|
377 |
|
378 | keysReportChangedCalled = true
|
379 | }
|
380 | }
|
381 | }
|
382 |
|
383 | if (!keysReportChangedCalled) {
|
384 | if (this.data_.size !== orderedData.size) {
|
385 |
|
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 |
|
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 |
|
427 |
|
428 |
|
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 |
|
442 | export var isObservableMap = createInstanceofPredicate("ObservableMap", ObservableMap) as (
|
443 | thing: any
|
444 | ) => thing is ObservableMap<any, any>
|
445 |
|
446 | function 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 | }
|