UNPKG

mobx-keystone

Version:

A MobX powered state management solution based on data trees with first class support for TypeScript, snapshots, patches and much more

237 lines (195 loc) 6.92 kB
import { WrapInActionOverrideContextFn } from "../action/wrapInAction" import { setDataModelAction } from "../dataModel/actions" import { BaseDataModel } from "../dataModel/BaseDataModel" import { isDataModel, isDataModelClass } from "../dataModel/utils" import { addLateInitializationFunction, failure, inDevMode, runAfterNewSymbol } from "./index" import { BaseModel } from "../model/BaseModel" import { modelInfoByClass } from "../modelShared/modelInfo" import { runAfterModelDecoratorSymbol } from "../modelShared/modelSymbols" type WrapFunction = ( data: { actionName: string | (() => string) overrideContext: WrapInActionOverrideContextFn | undefined }, fn: any ) => any const unboundMethodSymbol = Symbol("unboundMethod") const bindMethod = (method: any, instance: any) => { const unboundMethod = unboundMethodSymbol in method ? method[unboundMethodSymbol] : method const boundMethod = unboundMethod.bind(instance) // copy modelAction symbol, etc. Object.getOwnPropertySymbols(unboundMethod).forEach((s) => { boundMethod[s] = unboundMethod[s] }) boundMethod[unboundMethodSymbol] = unboundMethod return boundMethod } /** * @internal */ export function decorateWrapMethodOrField( decoratorName: string, args: any[], wrap: WrapFunction ): any { if (typeof args[1] !== "object") { // non-standard decorators const target = args[0] const propertyKey: string = args[1] const baseDescriptor: PropertyDescriptor | undefined = args[2] checkModelDecoratorTaget(decoratorName, target) checkDecoratorContext("transaction", propertyKey, false) const data = getActionNameAndContextOverride(target, propertyKey, true) const addFieldDecorator = () => { addLateInitializationFunction(target, runAfterNewSymbol, (instance) => { const method = wrap(data, instance[propertyKey]) // all of this is to make method destructuring work instance[propertyKey] = bindMethod(method, instance) }) } if (baseDescriptor) { if (baseDescriptor.get !== undefined) { throw failure(`@${decoratorName} cannot be used with getters`) } if (baseDescriptor.value) { // babel / typescript - method decorator // @action method() { } return { enumerable: false, writable: true, configurable: true, value: wrap(data, baseDescriptor.value), } } else { // babel - field decorator: @action method = () => {} addFieldDecorator() } } else { // typescript - field decorator: @action method = () => {} addFieldDecorator() } } else { // standard decorators const ctx = args[1] as ClassMethodDecoratorContext | ClassFieldDecoratorContext checkDecoratorContext(decoratorName, ctx.name, ctx.static) switch (ctx.kind) { case "method": { // @action method() { } const value = args[0] const propertyKey = ctx.name as string let inited = false ctx.addInitializer(function (this: any) { // only do one override on first initialization for the whole class if (inited) { return } inited = true const target = this checkModelDecoratorTaget(decoratorName, target) // find the deepest proto that matches the value let proto = this let nextProto = Object.getPrototypeOf(proto) while (nextProto && nextProto[propertyKey] === value) { proto = nextProto nextProto = Object.getPrototypeOf(proto) } proto[propertyKey] = wrap( getActionNameAndContextOverride(target, propertyKey, false), proto[propertyKey] ) }) break } case "field": { // @action method = () => {} const propertyKey = ctx.name as string let data: ReturnType<typeof getActionNameAndContextOverride> | undefined return function (this: any, value: any) { const instance = this if (!data) { checkModelDecoratorTaget(decoratorName, instance) data = getActionNameAndContextOverride(instance, propertyKey, false) } const method = wrap(data, value) // all of this is to make method destructuring work return bindMethod(method, instance) } } default: throw failure(`@${decoratorName} can only be used on fields or methods}`) } } } /** * @internal */ export function checkDecoratorContext( decoratorName: string, propertyKey: string | symbol, isStatic: boolean ) { if (!inDevMode) { return } if (typeof propertyKey !== "string") { throw failure(`@${decoratorName} cannot decorate symbol properties`) } if (isStatic) { throw failure(`@${decoratorName} cannot be used with static fields or methods`) } } const dataModelOverrideContext: WrapInActionOverrideContextFn = (ctx, self) => { ctx.target = self.$ } function getActionNameAndContextOverride( target: any, propertyKey: string, runLate: boolean ): { actionName: string | (() => string) overrideContext: WrapInActionOverrideContextFn | undefined } { if (isDataModelClass(target) || isDataModel(target)) { const modelClass: any = isDataModelClass(target) ? target : target.constructor let fullActionName: string const lateInit = (finalClass: any) => { const modelInfo = modelInfoByClass.get(finalClass)! fullActionName = `fn::${modelInfo.name}::${propertyKey}` setDataModelAction(fullActionName, modelInfo.class, propertyKey) } if (runLate) { addLateInitializationFunction(modelClass, runAfterModelDecoratorSymbol, lateInit) } else { lateInit(modelClass) } return { actionName: () => fullActionName, overrideContext: dataModelOverrideContext, } } else { return { actionName: propertyKey, overrideContext: undefined } } } function checkModelDecoratorTaget(decoratorName: string, target: any) { if (!inDevMode) { return } const errMessage = `@${decoratorName} must be used over model classes or instances` if (!target) { throw failure(errMessage) } // check target is a model object or extended class const isModel = target instanceof BaseModel || target === BaseModel || target.prototype instanceof BaseModel if (isModel) { return } const isDataModel = target instanceof BaseDataModel || target === BaseDataModel || target.prototype instanceof BaseDataModel if (isDataModel) { return } throw failure(errMessage) }