1 | import {
|
2 | CreateObservableOptions,
|
3 | getAnnotationFromOptions,
|
4 | propagateChanged,
|
5 | isAnnotation,
|
6 | $mobx,
|
7 | Atom,
|
8 | Annotation,
|
9 | ComputedValue,
|
10 | IAtom,
|
11 | IComputedValueOptions,
|
12 | IEnhancer,
|
13 | IInterceptable,
|
14 | IListenable,
|
15 | Lambda,
|
16 | ObservableValue,
|
17 | addHiddenProp,
|
18 | createInstanceofPredicate,
|
19 | endBatch,
|
20 | getNextId,
|
21 | hasInterceptors,
|
22 | hasListeners,
|
23 | interceptChange,
|
24 | isObject,
|
25 | isPlainObject,
|
26 | isSpyEnabled,
|
27 | notifyListeners,
|
28 | referenceEnhancer,
|
29 | registerInterceptor,
|
30 | registerListener,
|
31 | spyReportEnd,
|
32 | spyReportStart,
|
33 | startBatch,
|
34 | stringifyKey,
|
35 | globalState,
|
36 | ADD,
|
37 | UPDATE,
|
38 | die,
|
39 | hasProp,
|
40 | getDescriptor,
|
41 | storedAnnotationsSymbol,
|
42 | ownKeys,
|
43 | isOverride,
|
44 | defineProperty,
|
45 | autoAnnotation,
|
46 | getAdministration,
|
47 | getDebugName,
|
48 | objectPrototype,
|
49 | MakeResult
|
50 | } from "../internal"
|
51 |
|
52 | const descriptorCache = Object.create(null)
|
53 |
|
54 | export type IObjectDidChange<T = any> = {
|
55 | observableKind: "object"
|
56 | name: PropertyKey
|
57 | object: T
|
58 | debugObjectName: string
|
59 | } & (
|
60 | | {
|
61 | type: "add"
|
62 | newValue: any
|
63 | }
|
64 | | {
|
65 | type: "update"
|
66 | oldValue: any
|
67 | newValue: any
|
68 | }
|
69 | | {
|
70 | type: "remove"
|
71 | oldValue: any
|
72 | }
|
73 | )
|
74 |
|
75 | export type IObjectWillChange<T = any> =
|
76 | | {
|
77 | object: T
|
78 | type: "update" | "add"
|
79 | name: PropertyKey
|
80 | newValue: any
|
81 | }
|
82 | | {
|
83 | object: T
|
84 | type: "remove"
|
85 | name: PropertyKey
|
86 | }
|
87 |
|
88 | const REMOVE = "remove"
|
89 |
|
90 | export class ObservableObjectAdministration
|
91 | implements IInterceptable<IObjectWillChange>, IListenable {
|
92 | keysAtom_: IAtom
|
93 | changeListeners_
|
94 | interceptors_
|
95 | proxy_: any
|
96 | isPlainObject_: boolean
|
97 | appliedAnnotations_?: object
|
98 | private pendingKeys_: undefined | Map<PropertyKey, ObservableValue<boolean>>
|
99 |
|
100 | constructor(
|
101 | public target_: any,
|
102 | public values_ = new Map<PropertyKey, ObservableValue<any> | ComputedValue<any>>(),
|
103 | public name_: string,
|
104 |
|
105 | public defaultAnnotation_: Annotation = autoAnnotation
|
106 | ) {
|
107 | this.keysAtom_ = new Atom(__DEV__ ? `${this.name_}.keys` : "ObservableObject.keys")
|
108 |
|
109 | this.isPlainObject_ = isPlainObject(this.target_)
|
110 | if (__DEV__ && !isAnnotation(this.defaultAnnotation_)) {
|
111 | die(`defaultAnnotation must be valid annotation`)
|
112 | }
|
113 | if (__DEV__) {
|
114 |
|
115 | this.appliedAnnotations_ = {}
|
116 | }
|
117 | }
|
118 |
|
119 | getObservablePropValue_(key: PropertyKey): any {
|
120 | return this.values_.get(key)!.get()
|
121 | }
|
122 |
|
123 | setObservablePropValue_(key: PropertyKey, newValue): boolean | null {
|
124 | const observable = this.values_.get(key)
|
125 | if (observable instanceof ComputedValue) {
|
126 | observable.set(newValue)
|
127 | return true
|
128 | }
|
129 |
|
130 |
|
131 | if (hasInterceptors(this)) {
|
132 | const change = interceptChange<IObjectWillChange>(this, {
|
133 | type: UPDATE,
|
134 | object: this.proxy_ || this.target_,
|
135 | name: key,
|
136 | newValue
|
137 | })
|
138 | if (!change) return null
|
139 | newValue = (change as any).newValue
|
140 | }
|
141 | newValue = (observable as any).prepareNewValue_(newValue)
|
142 |
|
143 |
|
144 | if (newValue !== globalState.UNCHANGED) {
|
145 | const notify = hasListeners(this)
|
146 | const notifySpy = __DEV__ && isSpyEnabled()
|
147 | const change: IObjectDidChange | null =
|
148 | notify || notifySpy
|
149 | ? {
|
150 | type: UPDATE,
|
151 | observableKind: "object",
|
152 | debugObjectName: this.name_,
|
153 | object: this.proxy_ || this.target_,
|
154 | oldValue: (observable as any).value_,
|
155 | name: key,
|
156 | newValue
|
157 | }
|
158 | : null
|
159 |
|
160 | if (__DEV__ && notifySpy) spyReportStart(change!)
|
161 | ;(observable as ObservableValue<any>).setNewValue_(newValue)
|
162 | if (notify) notifyListeners(this, change)
|
163 | if (__DEV__ && notifySpy) spyReportEnd()
|
164 | }
|
165 | return true
|
166 | }
|
167 |
|
168 | get_(key: PropertyKey): any {
|
169 | if (globalState.trackingDerivation && !hasProp(this.target_, key)) {
|
170 |
|
171 | this.has_(key)
|
172 | }
|
173 | return this.target_[key]
|
174 | }
|
175 |
|
176 | |
177 |
|
178 |
|
179 |
|
180 |
|
181 |
|
182 |
|
183 | set_(key: PropertyKey, value: any, proxyTrap: boolean = false): boolean | null {
|
184 |
|
185 | if (hasProp(this.target_, key)) {
|
186 |
|
187 | if (this.values_.has(key)) {
|
188 |
|
189 | return this.setObservablePropValue_(key, value)
|
190 | } else if (proxyTrap) {
|
191 |
|
192 | return Reflect.set(this.target_, key, value)
|
193 | } else {
|
194 |
|
195 | this.target_[key] = value
|
196 | return true
|
197 | }
|
198 | } else {
|
199 |
|
200 | return this.extend_(
|
201 | key,
|
202 | { value, enumerable: true, writable: true, configurable: true },
|
203 | this.defaultAnnotation_,
|
204 | proxyTrap
|
205 | )
|
206 | }
|
207 | }
|
208 |
|
209 |
|
210 | has_(key: PropertyKey): boolean {
|
211 | if (!globalState.trackingDerivation) {
|
212 |
|
213 | return key in this.target_
|
214 | }
|
215 | this.pendingKeys_ ||= new Map()
|
216 | let entry = this.pendingKeys_.get(key)
|
217 | if (!entry) {
|
218 | entry = new ObservableValue(
|
219 | key in this.target_,
|
220 | referenceEnhancer,
|
221 | __DEV__ ? `${this.name_}.${stringifyKey(key)}?` : "ObservableObject.key?",
|
222 | false
|
223 | )
|
224 | this.pendingKeys_.set(key, entry)
|
225 | }
|
226 | return entry.get()
|
227 | }
|
228 |
|
229 | |
230 |
|
231 |
|
232 |
|
233 | make_(key: PropertyKey, annotation: Annotation | boolean): void {
|
234 | if (annotation === true) {
|
235 | annotation = this.defaultAnnotation_
|
236 | }
|
237 | if (annotation === false) {
|
238 | return
|
239 | }
|
240 | assertAnnotable(this, annotation, key)
|
241 | if (!(key in this.target_)) {
|
242 |
|
243 |
|
244 |
|
245 |
|
246 |
|
247 | if (this.target_[storedAnnotationsSymbol]?.[key]) {
|
248 | return
|
249 | } else {
|
250 | die(1, annotation.annotationType_, `${this.name_}.${key.toString()}`)
|
251 | }
|
252 | }
|
253 | let source = this.target_
|
254 | while (source && source !== objectPrototype) {
|
255 | const descriptor = getDescriptor(source, key)
|
256 | if (descriptor) {
|
257 | const outcome = annotation.make_(this, key, descriptor, source)
|
258 | if (outcome === MakeResult.Cancel) return
|
259 | if (outcome === MakeResult.Break) break
|
260 | }
|
261 | source = Object.getPrototypeOf(source)
|
262 | }
|
263 | recordAnnotationApplied(this, annotation, key)
|
264 | }
|
265 |
|
266 | |
267 |
|
268 |
|
269 |
|
270 |
|
271 |
|
272 |
|
273 | extend_(
|
274 | key: PropertyKey,
|
275 | descriptor: PropertyDescriptor,
|
276 | annotation: Annotation | boolean,
|
277 | proxyTrap: boolean = false
|
278 | ): boolean | null {
|
279 | if (annotation === true) {
|
280 | annotation = this.defaultAnnotation_
|
281 | }
|
282 | if (annotation === false) {
|
283 | return this.defineProperty_(key, descriptor, proxyTrap)
|
284 | }
|
285 | assertAnnotable(this, annotation, key)
|
286 | const outcome = annotation.extend_(this, key, descriptor, proxyTrap)
|
287 | if (outcome) {
|
288 | recordAnnotationApplied(this, annotation, key)
|
289 | }
|
290 | return outcome
|
291 | }
|
292 |
|
293 | |
294 |
|
295 |
|
296 |
|
297 |
|
298 |
|
299 | defineProperty_(
|
300 | key: PropertyKey,
|
301 | descriptor: PropertyDescriptor,
|
302 | proxyTrap: boolean = false
|
303 | ): boolean | null {
|
304 | try {
|
305 | startBatch()
|
306 |
|
307 |
|
308 | const deleteOutcome = this.delete_(key)
|
309 | if (!deleteOutcome) {
|
310 |
|
311 | return deleteOutcome
|
312 | }
|
313 |
|
314 |
|
315 | if (hasInterceptors(this)) {
|
316 | const change = interceptChange<IObjectWillChange>(this, {
|
317 | object: this.proxy_ || this.target_,
|
318 | name: key,
|
319 | type: ADD,
|
320 | newValue: descriptor.value
|
321 | })
|
322 | if (!change) return null
|
323 | const { newValue } = change as any
|
324 | if (descriptor.value !== newValue) {
|
325 | descriptor = {
|
326 | ...descriptor,
|
327 | value: newValue
|
328 | }
|
329 | }
|
330 | }
|
331 |
|
332 |
|
333 | if (proxyTrap) {
|
334 | if (!Reflect.defineProperty(this.target_, key, descriptor)) {
|
335 | return false
|
336 | }
|
337 | } else {
|
338 | defineProperty(this.target_, key, descriptor)
|
339 | }
|
340 |
|
341 |
|
342 | this.notifyPropertyAddition_(key, descriptor.value)
|
343 | } finally {
|
344 | endBatch()
|
345 | }
|
346 | return true
|
347 | }
|
348 |
|
349 |
|
350 | defineObservableProperty_(
|
351 | key: PropertyKey,
|
352 | value: any,
|
353 | enhancer: IEnhancer<any>,
|
354 | proxyTrap: boolean = false
|
355 | ): boolean | null {
|
356 | try {
|
357 | startBatch()
|
358 |
|
359 |
|
360 | const deleteOutcome = this.delete_(key)
|
361 | if (!deleteOutcome) {
|
362 |
|
363 | return deleteOutcome
|
364 | }
|
365 |
|
366 |
|
367 | if (hasInterceptors(this)) {
|
368 | const change = interceptChange<IObjectWillChange>(this, {
|
369 | object: this.proxy_ || this.target_,
|
370 | name: key,
|
371 | type: ADD,
|
372 | newValue: value
|
373 | })
|
374 | if (!change) return null
|
375 | value = (change as any).newValue
|
376 | }
|
377 |
|
378 | const cachedDescriptor = getCachedObservablePropDescriptor(key)
|
379 | const descriptor = {
|
380 | configurable: globalState.safeDescriptors ? this.isPlainObject_ : true,
|
381 | enumerable: true,
|
382 | get: cachedDescriptor.get,
|
383 | set: cachedDescriptor.set
|
384 | }
|
385 |
|
386 |
|
387 | if (proxyTrap) {
|
388 | if (!Reflect.defineProperty(this.target_, key, descriptor)) {
|
389 | return false
|
390 | }
|
391 | } else {
|
392 | defineProperty(this.target_, key, descriptor)
|
393 | }
|
394 |
|
395 | const observable = new ObservableValue(
|
396 | value,
|
397 | enhancer,
|
398 | __DEV__ ? `${this.name_}.${key.toString()}` : "ObservableObject.key",
|
399 | false
|
400 | )
|
401 |
|
402 | this.values_.set(key, observable)
|
403 |
|
404 |
|
405 | this.notifyPropertyAddition_(key, observable.value_)
|
406 | } finally {
|
407 | endBatch()
|
408 | }
|
409 | return true
|
410 | }
|
411 |
|
412 |
|
413 | defineComputedProperty_(
|
414 | key: PropertyKey,
|
415 | options: IComputedValueOptions<any>,
|
416 | proxyTrap: boolean = false
|
417 | ): boolean | null {
|
418 | try {
|
419 | startBatch()
|
420 |
|
421 |
|
422 | const deleteOutcome = this.delete_(key)
|
423 | if (!deleteOutcome) {
|
424 |
|
425 | return deleteOutcome
|
426 | }
|
427 |
|
428 |
|
429 | if (hasInterceptors(this)) {
|
430 | const change = interceptChange<IObjectWillChange>(this, {
|
431 | object: this.proxy_ || this.target_,
|
432 | name: key,
|
433 | type: ADD,
|
434 | newValue: undefined
|
435 | })
|
436 | if (!change) return null
|
437 | }
|
438 | options.name ||= __DEV__ ? `${this.name_}.${key.toString()}` : "ObservableObject.key"
|
439 | options.context = this.proxy_ || this.target_
|
440 | const cachedDescriptor = getCachedObservablePropDescriptor(key)
|
441 | const descriptor = {
|
442 | configurable: globalState.safeDescriptors ? this.isPlainObject_ : true,
|
443 | enumerable: false,
|
444 | get: cachedDescriptor.get,
|
445 | set: cachedDescriptor.set
|
446 | }
|
447 |
|
448 |
|
449 | if (proxyTrap) {
|
450 | if (!Reflect.defineProperty(this.target_, key, descriptor)) {
|
451 | return false
|
452 | }
|
453 | } else {
|
454 | defineProperty(this.target_, key, descriptor)
|
455 | }
|
456 |
|
457 | this.values_.set(key, new ComputedValue(options))
|
458 |
|
459 |
|
460 | this.notifyPropertyAddition_(key, undefined)
|
461 | } finally {
|
462 | endBatch()
|
463 | }
|
464 | return true
|
465 | }
|
466 |
|
467 | |
468 |
|
469 |
|
470 |
|
471 |
|
472 |
|
473 | delete_(key: PropertyKey, proxyTrap: boolean = false): boolean | null {
|
474 |
|
475 | if (!hasProp(this.target_, key)) {
|
476 | return true
|
477 | }
|
478 |
|
479 |
|
480 | if (hasInterceptors(this)) {
|
481 | const change = interceptChange<IObjectWillChange>(this, {
|
482 | object: this.proxy_ || this.target_,
|
483 | name: key,
|
484 | type: REMOVE
|
485 | })
|
486 |
|
487 | if (!change) return null
|
488 | }
|
489 |
|
490 |
|
491 | try {
|
492 | startBatch()
|
493 | const notify = hasListeners(this)
|
494 | const notifySpy = __DEV__ && isSpyEnabled()
|
495 | const observable = this.values_.get(key)
|
496 |
|
497 | let value = undefined
|
498 |
|
499 | if (!observable && (notify || notifySpy)) {
|
500 | value = getDescriptor(this.target_, key)?.value
|
501 | }
|
502 |
|
503 | if (proxyTrap) {
|
504 | if (!Reflect.deleteProperty(this.target_, key)) {
|
505 | return false
|
506 | }
|
507 | } else {
|
508 | delete this.target_[key]
|
509 | }
|
510 |
|
511 | if (__DEV__) {
|
512 | delete this.appliedAnnotations_![key]
|
513 | }
|
514 |
|
515 | if (observable) {
|
516 | this.values_.delete(key)
|
517 |
|
518 | if (observable instanceof ObservableValue) {
|
519 | value = observable.value_
|
520 | }
|
521 |
|
522 | propagateChanged(observable)
|
523 | }
|
524 |
|
525 | this.keysAtom_.reportChanged()
|
526 |
|
527 |
|
528 |
|
529 | this.pendingKeys_?.get(key)?.set(key in this.target_)
|
530 |
|
531 |
|
532 | if (notify || notifySpy) {
|
533 | const change: IObjectDidChange = {
|
534 | type: REMOVE,
|
535 | observableKind: "object",
|
536 | object: this.proxy_ || this.target_,
|
537 | debugObjectName: this.name_,
|
538 | oldValue: value,
|
539 | name: key
|
540 | }
|
541 | if (__DEV__ && notifySpy) spyReportStart(change!)
|
542 | if (notify) notifyListeners(this, change)
|
543 | if (__DEV__ && notifySpy) spyReportEnd()
|
544 | }
|
545 | } finally {
|
546 | endBatch()
|
547 | }
|
548 | return true
|
549 | }
|
550 |
|
551 | |
552 |
|
553 |
|
554 |
|
555 |
|
556 | observe_(callback: (changes: IObjectDidChange) => void, fireImmediately?: boolean): Lambda {
|
557 | if (__DEV__ && fireImmediately === true)
|
558 | die("`observe` doesn't support the fire immediately property for observable objects.")
|
559 | return registerListener(this, callback)
|
560 | }
|
561 |
|
562 | intercept_(handler): Lambda {
|
563 | return registerInterceptor(this, handler)
|
564 | }
|
565 |
|
566 | notifyPropertyAddition_(key: PropertyKey, value: any) {
|
567 | const notify = hasListeners(this)
|
568 | const notifySpy = __DEV__ && isSpyEnabled()
|
569 | if (notify || notifySpy) {
|
570 | const change: IObjectDidChange | null =
|
571 | notify || notifySpy
|
572 | ? ({
|
573 | type: ADD,
|
574 | observableKind: "object",
|
575 | debugObjectName: this.name_,
|
576 | object: this.proxy_ || this.target_,
|
577 | name: key,
|
578 | newValue: value
|
579 | } as const)
|
580 | : null
|
581 |
|
582 | if (__DEV__ && notifySpy) spyReportStart(change!)
|
583 | if (notify) notifyListeners(this, change)
|
584 | if (__DEV__ && notifySpy) spyReportEnd()
|
585 | }
|
586 |
|
587 | this.pendingKeys_?.get(key)?.set(true)
|
588 |
|
589 |
|
590 | this.keysAtom_.reportChanged()
|
591 | }
|
592 |
|
593 | ownKeys_(): PropertyKey[] {
|
594 | this.keysAtom_.reportObserved()
|
595 | return ownKeys(this.target_)
|
596 | }
|
597 |
|
598 | keys_(): PropertyKey[] {
|
599 |
|
600 |
|
601 |
|
602 |
|
603 |
|
604 |
|
605 | this.keysAtom_.reportObserved()
|
606 | return Object.keys(this.target_)
|
607 | }
|
608 | }
|
609 |
|
610 | export interface IIsObservableObject {
|
611 | $mobx: ObservableObjectAdministration
|
612 | }
|
613 |
|
614 | export function asObservableObject(
|
615 | target: any,
|
616 | options?: CreateObservableOptions
|
617 | ): IIsObservableObject {
|
618 | if (__DEV__ && options && isObservableObject(target)) {
|
619 | die(`Options can't be provided for already observable objects.`)
|
620 | }
|
621 |
|
622 | if (hasProp(target, $mobx)) {
|
623 | if (__DEV__ && !(getAdministration(target) instanceof ObservableObjectAdministration)) {
|
624 | die(
|
625 | `Cannot convert '${getDebugName(target)}' into observable object:` +
|
626 | `\nThe target is already observable of different type.` +
|
627 | `\nExtending builtins is not supported.`
|
628 | )
|
629 | }
|
630 | return target
|
631 | }
|
632 |
|
633 | if (__DEV__ && !Object.isExtensible(target))
|
634 | die("Cannot make the designated object observable; it is not extensible")
|
635 |
|
636 | const name =
|
637 | options?.name ??
|
638 | (__DEV__
|
639 | ? `${
|
640 | isPlainObject(target) ? "ObservableObject" : target.constructor.name
|
641 | }@${getNextId()}`
|
642 | : "ObservableObject")
|
643 |
|
644 | const adm = new ObservableObjectAdministration(
|
645 | target,
|
646 | new Map(),
|
647 | String(name),
|
648 | getAnnotationFromOptions(options)
|
649 | )
|
650 |
|
651 | addHiddenProp(target, $mobx, adm)
|
652 |
|
653 | return target
|
654 | }
|
655 |
|
656 | const isObservableObjectAdministration = createInstanceofPredicate(
|
657 | "ObservableObjectAdministration",
|
658 | ObservableObjectAdministration
|
659 | )
|
660 |
|
661 | function getCachedObservablePropDescriptor(key) {
|
662 | return (
|
663 | descriptorCache[key] ||
|
664 | (descriptorCache[key] = {
|
665 | get() {
|
666 | return this[$mobx].getObservablePropValue_(key)
|
667 | },
|
668 | set(value) {
|
669 | return this[$mobx].setObservablePropValue_(key, value)
|
670 | }
|
671 | })
|
672 | )
|
673 | }
|
674 |
|
675 | export function isObservableObject(thing: any): boolean {
|
676 | if (isObject(thing)) {
|
677 | return isObservableObjectAdministration((thing as any)[$mobx])
|
678 | }
|
679 | return false
|
680 | }
|
681 |
|
682 | export function recordAnnotationApplied(
|
683 | adm: ObservableObjectAdministration,
|
684 | annotation: Annotation,
|
685 | key: PropertyKey
|
686 | ) {
|
687 | if (__DEV__) {
|
688 | adm.appliedAnnotations_![key] = annotation
|
689 | }
|
690 |
|
691 | delete adm.target_[storedAnnotationsSymbol]?.[key]
|
692 | }
|
693 |
|
694 | function assertAnnotable(
|
695 | adm: ObservableObjectAdministration,
|
696 | annotation: Annotation,
|
697 | key: PropertyKey
|
698 | ) {
|
699 |
|
700 | if (__DEV__ && !isAnnotation(annotation)) {
|
701 | die(`Cannot annotate '${adm.name_}.${key.toString()}': Invalid annotation.`)
|
702 | }
|
703 |
|
704 | |
705 |
|
706 |
|
707 |
|
708 |
|
709 |
|
710 |
|
711 |
|
712 |
|
713 |
|
714 |
|
715 |
|
716 |
|
717 |
|
718 |
|
719 |
|
720 |
|
721 |
|
722 |
|
723 |
|
724 |
|
725 |
|
726 |
|
727 |
|
728 |
|
729 |
|
730 |
|
731 |
|
732 |
|
733 |
|
734 |
|
735 |
|
736 | if (__DEV__ && !isOverride(annotation) && hasProp(adm.appliedAnnotations_!, key)) {
|
737 | const fieldName = `${adm.name_}.${key.toString()}`
|
738 | const currentAnnotationType = adm.appliedAnnotations_![key].annotationType_
|
739 | const requestedAnnotationType = annotation.annotationType_
|
740 | die(
|
741 | `Cannot apply '${requestedAnnotationType}' to '${fieldName}':` +
|
742 | `\nThe field is already annotated with '${currentAnnotationType}'.` +
|
743 | `\nRe-annotating fields is not allowed.` +
|
744 | `\nUse 'override' annotation for methods overriden by subclass.`
|
745 | )
|
746 | }
|
747 | }
|