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

246 lines (208 loc) 8.12 kB
import { HookAction } from "../action/hookActions" import { wrapModelMethodInActionIfNeeded } from "../action/wrapInAction" import type { AnyDataModel } from "../dataModel/BaseDataModel" import { isDataModelClass } from "../dataModel/utils" import { getGlobalConfig } from "../globalConfig" import type { AnyModel } from "../model/BaseModel" import { modelTypeKey } from "../model/metadata" import { isModelClass } from "../model/utils" import { ModelClass, modelInitializedSymbol } from "../modelShared/BaseModelShared" import { modelInfoByClass, modelInfoByName } from "../modelShared/modelInfo" import { modelUnwrappedClassSymbol, runAfterModelDecoratorSymbol, } from "../modelShared/modelSymbols" import { failure, getMobxVersion, logWarning, mobx6, runAfterNewSymbol, runBeforeOnInitSymbol, runLateInitializationFunctions, } from "../utils" import { AnyFunction } from "../utils/AnyFunction" /** * Decorator that marks this class (which MUST inherit from the `Model` or `DataModel` abstract classes) * as a model. * * @param name Unique name for the model type. Note that this name must be unique for your whole * application, so it is usually a good idea to use some prefix unique to your application domain. */ export const model = (name: string) => <MC extends ModelClass<AnyModel | AnyDataModel>>(clazz: MC, ...args: any[]): MC => { const ctx = typeof args[1] === "object" ? (args[1] as ClassDecoratorContext) : undefined return internalModel(name, clazz, ctx?.addInitializer) as any } const afterClassInitializationData = new WeakMap< ModelClass<AnyModel | AnyDataModel>, { needsMakeObservable: boolean | undefined type: "class" | "data" } >() const runAfterClassInitialization = ( target: ModelClass<AnyModel | AnyDataModel>, instance: any ) => { runLateInitializationFunctions(instance, runAfterNewSymbol) const tag = afterClassInitializationData.get(target)! // compatibility with mobx 6 if (tag.needsMakeObservable) { // we know it can be done and shouldn't fail mobx6.makeObservable(instance) } else if (tag.needsMakeObservable === undefined) { if (getMobxVersion() >= 6) { try { mobx6.makeObservable(instance) tag.needsMakeObservable = true } catch (e) { const err = e as Error if ( err.message !== "[MobX] No annotations were passed to makeObservable, but no decorator members have been found either" && err.message !== "[MobX] No annotations were passed to makeObservable, but no decorated members have been found either" ) { throw err } // sadly we need to use this hack since the PR to do this the proper way // was rejected on the mobx side tag.needsMakeObservable = false } } else { tag.needsMakeObservable = false } } // the object is ready instance[modelInitializedSymbol] = true runLateInitializationFunctions(instance, runBeforeOnInitSymbol) if (tag.type === "class" && instance.onInit) { wrapModelMethodInActionIfNeeded(instance, "onInit", HookAction.OnInit) instance.onInit() } if (tag.type === "data" && instance.onLazyInit) { wrapModelMethodInActionIfNeeded(instance, "onLazyInit", HookAction.OnLazyInit) instance.onLazyInit() } } const proxyClassHandler: ProxyHandler<ModelClass<AnyModel | AnyDataModel>> = { construct(clazz, args) { const instance = new (clazz as any)(...args) runAfterClassInitialization(clazz, instance) return instance }, } const internalModel = <MC extends ModelClass<AnyModel | AnyDataModel>>( name: string, clazz: MC, addInitializer: ((initializer: (this: any) => void) => void) | undefined ): MC | void => { const type = isModelClass(clazz) ? "class" : isDataModelClass(clazz) ? "data" : undefined if (!type) { throw failure(`clazz must be a class that extends from Model/DataModel`) } if (modelInfoByName[name]) { if (getGlobalConfig().showDuplicateModelNameWarnings) { logWarning( "warn", `a model with name "${name}" already exists (if you are using hot-reloading you may safely ignore this warning)`, `duplicateModelName - ${name}` ) } } if (modelUnwrappedClassSymbol in clazz && clazz[modelUnwrappedClassSymbol] === clazz) { throw failure("a class already decorated with `@model` cannot be re-decorated") } clazz.toString = () => `class ${clazz.name}#${name}` if (type === "class") { ;(clazz as any)[modelTypeKey] = name } // track if we fail so we only try it once per class afterClassInitializationData.set(clazz, { needsMakeObservable: undefined, type }) if (addInitializer) { // standard decorator API, avoid proxies addInitializer(function (this: any) { runAfterClassInitialization(clazz, this) }) const modelInfo = { name, class: clazz, } modelInfoByName[name] = modelInfo modelInfoByClass.set(clazz, modelInfo) runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol) return undefined // use same class } else { // non-standard decorator API, use proxies // trick so plain new works const proxyClass = new Proxy<MC>(clazz, proxyClassHandler) // set or else it points to the undecorated class proxyClass.prototype.constructor = proxyClass ;(proxyClass as any)[modelUnwrappedClassSymbol] = clazz const modelInfo = { name, class: proxyClass, } modelInfoByName[name] = modelInfo modelInfoByClass.set(proxyClass, modelInfo) modelInfoByClass.set(clazz, modelInfo) runLateInitializationFunctions(clazz, runAfterModelDecoratorSymbol) return proxyClass } } // basically taken from TS function tsDecorate(decorators: any, target: any, key: any, desc: any) { const c = arguments.length let r = c < 3 ? target : desc === null ? (desc = Object.getOwnPropertyDescriptor(target, key)) : desc let d: any if (typeof Reflect === "object" && typeof (Reflect as any).decorate === "function") { r = (Reflect as any).decorate(decorators, target, key, desc) } else { for ( // biome-ignore lint/correctness/noInnerDeclarations: minified file var i = decorators.length - 1; i >= 0; i-- ) { if ((d = decorators[i])) { r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r } } } // biome-ignore lint/complexity/noCommaOperator: minified file return c > 3 && r && Object.defineProperty(target, key, r), r } /** * Marks a class (which MUST inherit from the `Model` abstract class) * as a model and decorates some of its methods/properties. * * @param name Unique name for the model type. Note that this name must be unique for your whole * application, so it is usually a good idea to use some prefix unique to your application domain. * If you don't want to assign a name yet (e.g. for a base model) pass `undefined`. * @param clazz Model class. * @param decorators Decorators. */ export function decoratedModel<M, MC extends abstract new (...ags: any) => M>( name: string | undefined, clazz: MC, decorators: { [k in keyof M]?: AnyFunction | ReadonlyArray<AnyFunction> } ): MC { // decorate class members for (const [k, decorator] of Object.entries(decorators)) { const prototypeValueDesc = Object.getOwnPropertyDescriptor(clazz.prototype, k) // TS seems to send null for methods in the prototype // (which we substitute for the descriptor to avoid a double look-up) and void 0 (undefined) for props tsDecorate( Array.isArray(decorator) ? decorator : [decorator], clazz.prototype, k, prototypeValueDesc ? prototypeValueDesc : void 0 ) } return (name ? model(name)(clazz as unknown as ModelClass<AnyModel>) : clazz) as MC }