UNPKG

mobx

Version:

Simple, scalable state management.

288 lines (277 loc) 9.74 kB
import { asObservableObject, addHiddenProp, action, autoAction, isAction, computed, observable, AnnotationsMap, Annotation, getEnhancerFromAnnotation, endBatch, startBatch, CreateObservableOptions, ObservableObjectAdministration, applyDecorators, isObservableProp, getDescriptor, isPlainObject, isObservableObject, isFunction, die, ACTION, ACTION_BOUND, AUTOACTION, AUTOACTION_BOUND, COMPUTED, COMPUTED_STRUCT, OBSERVABLE, OBSERVABLE_REF, OBSERVABLE_SHALLOW, OBSERVABLE_STRUCT, getOwnPropertyDescriptors, defineProperty, ownKeys, objectPrototype, hasProp, FLOW, flow, isGenerator, isFlow } from "../internal" const CACHED_ANNOTATIONS = Symbol("mobx-cached-annotations") function makeAction(target, key, name, fn, asAutoAction) { addHiddenProp(target, key, asAutoAction ? autoAction(name || key, fn) : action(name || key, fn)) } function getInferredAnnotation( desc: PropertyDescriptor, defaultAnnotation: Annotation | undefined, autoBind: boolean ): Annotation | boolean { if (desc.get) return computed if (desc.set) return false // ignore pure setters // if already wrapped in action, don't do that another time, but assume it is already set up properly if (isFunction(desc.value)) return isGenerator(desc.value) ? flow : isAction(desc.value) ? false : autoBind ? autoAction.bound : autoAction // if (!desc.configurable || !desc.writable) return false return defaultAnnotation ?? observable.deep } function getDescriptorInChain(target: Object, prop: PropertyKey): [PropertyDescriptor, Object] { let current = target while (current && current !== objectPrototype) { // Optimization: cache meta data, especially for members from prototypes? const desc = getDescriptor(current, prop) if (desc) { return [desc, current] } current = Object.getPrototypeOf(current) } die(1, prop) } export function makeProperty( adm: ObservableObjectAdministration, owner: Object, key: PropertyKey, descriptor: PropertyDescriptor, annotation: Annotation | boolean, forceCopy: boolean, // extend observable will copy even unannotated properties autoBind: boolean ): void { const { target_: target } = adm const defaultAnnotation: Annotation | undefined = observable // ideally grap this from adm's defaultEnahncer instead! const originAnnotation = annotation if (annotation === true) { annotation = getInferredAnnotation(descriptor, defaultAnnotation, autoBind) } if (annotation === false) { if (forceCopy) { defineProperty(target, key, descriptor) } return } if (!annotation || annotation === true || !annotation.annotationType_) { return die(2, key) } const type = annotation.annotationType_ switch (type) { case AUTOACTION: case ACTION: { const fn = descriptor.value if (!isFunction(fn)) die(3, key) if (owner !== target && !forceCopy) { if (!isAction(owner[key])) makeAction(owner, key, annotation.arg_, fn, type === AUTOACTION) } else { makeAction(target, key, annotation.arg_, fn, type === AUTOACTION) } break } case AUTOACTION_BOUND: case ACTION_BOUND: { const fn = descriptor.value if (!isFunction(fn)) die(3, key) makeAction( target, key, annotation.arg_, fn.bind(adm.proxy_ || target), type === AUTOACTION_BOUND ) break } case FLOW: { if (owner !== target && !forceCopy) { if (!isFlow(owner[key])) addHiddenProp(owner, key, flow(descriptor.value!)) } else { addHiddenProp(target, key, flow(descriptor.value)) } break } case COMPUTED: case COMPUTED_STRUCT: { if (!descriptor.get) die(4, key) adm.addComputedProp_(target, key, { get: descriptor.get, set: descriptor.set, compareStructural: annotation.annotationType_ === COMPUTED_STRUCT, ...annotation.arg_ }) break } case OBSERVABLE: case OBSERVABLE_REF: case OBSERVABLE_SHALLOW: case OBSERVABLE_STRUCT: { if (__DEV__ && isObservableProp(target, key as any)) die( `Cannot decorate '${key.toString()}': the property is already decorated as observable.` ) if (__DEV__ && !("value" in descriptor)) die( `Cannot decorate '${key.toString()}': observable cannot be used on setter / getter properties.` ) // if the originAnnotation was true, preferred the adm's default enhancer over the inferred one const enhancer = originAnnotation === true ? adm.defaultEnhancer_ : getEnhancerFromAnnotation(annotation) adm.addObservableProp_(key, descriptor.value, enhancer) break } default: if (__DEV__) die( `invalid decorator '${ annotation.annotationType_ ?? annotation }' for '${key.toString()}'` ) } } // Hack based on https://github.com/Microsoft/TypeScript/issues/14829#issuecomment-322267089 // We need this, because otherwise, AdditionalKeys is going to be inferred to be any // set of superfluous keys. But, we rather want to get a compile error unless AdditionalKeys is // _explicity_ passed as generic argument // Fixes: https://github.com/mobxjs/mobx/issues/2325#issuecomment-691070022 type NoInfer<T> = [T][T extends any ? 0 : never] export function makeObservable<T, AdditionalKeys extends PropertyKey = never>( target: T, annotations?: AnnotationsMap<T, NoInfer<AdditionalKeys>>, options?: CreateObservableOptions ): T { const autoBind = !!options?.autoBind const adm = asObservableObject( target, options?.name, getEnhancerFromAnnotation(options?.defaultDecorator) ) startBatch() try { if (!annotations) { const didDecorate = applyDecorators(target) if (__DEV__ && !didDecorate) die( `No annotations were passed to makeObservable, but no decorator members have been found either` ) return target } const make = key => { let annotation = annotations[key] const [desc, owner] = getDescriptorInChain(target, key) makeProperty(adm, owner, key, desc, annotation, false, autoBind) } ownKeys(annotations).forEach(make) } finally { endBatch() } return target } export function makeAutoObservable<T extends Object, AdditionalKeys extends PropertyKey = never>( target: T, overrides?: AnnotationsMap<T, NoInfer<AdditionalKeys>>, options?: CreateObservableOptions ): T { const proto = Object.getPrototypeOf(target) const isPlain = proto == null || proto === objectPrototype if (__DEV__) { if (!isPlain && !isPlainObject(proto)) die(`'makeAutoObservable' can only be used for classes that don't have a superclass`) if (isObservableObject(target)) die(`makeAutoObservable can only be used on objects not already made observable`) } let annotations: AnnotationsMap<any, any> if (!isPlain && hasProp(proto, CACHED_ANNOTATIONS)) { // shortcut, reuse inferred annotations for this type from the previous time annotations = proto[CACHED_ANNOTATIONS] as any } else { annotations = { ...overrides } extractAnnotationsFromObject(target, annotations, options) if (!isPlain) { extractAnnotationsFromProto(proto, annotations, options) addHiddenProp(proto, CACHED_ANNOTATIONS, annotations) } } makeObservable(target, annotations as any, options) return target } function extractAnnotationsFromObject( target, collector: AnnotationsMap<any, any>, options: CreateObservableOptions | undefined ) { const autoBind = !!options?.autoBind const defaultAnnotation: Annotation = options?.deep === undefined ? options?.defaultDecorator ?? observable.deep : options?.deep ? observable.deep : observable.ref Object.entries(getOwnPropertyDescriptors(target)).forEach(([key, descriptor]) => { if (key in collector || key === "constructor") return collector[key] = getInferredAnnotation(descriptor, defaultAnnotation, autoBind) }) } function extractAnnotationsFromProto( proto: any, collector: AnnotationsMap<any, any>, options?: CreateObservableOptions ) { Object.entries(getOwnPropertyDescriptors(proto)).forEach(([key, prop]) => { if (key in collector || key === "constructor") return if (prop.get) { collector[key as any] = computed } else if (isFunction(prop.value)) { collector[key as any] = isGenerator(prop.value) ? flow : options?.autoBind ? autoAction.bound : autoAction } }) }