UNPKG

mobx-keystone-mindreframer

Version:

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

127 lines (103 loc) 3.79 kB
import { dataTypeSymbol, ModelClass } from "../modelShared/BaseModelShared" import { modelInfoByClass } from "../modelShared/modelInfo" import { getSnapshot } from "../snapshot/getSnapshot" import { toTreeNode } from "../tweaker/tweak" import { typesDataModelData } from "../typeChecking/dataModelData" import { typeCheck } from "../typeChecking/typeCheck" import type { TypeCheckError } from "../typeChecking/TypeCheckError" import { failure, isObject } from "../utils" import { getOrCreate } from "../utils/mapUtils" import type { DataModelConstructorOptions } from "./DataModelConstructorOptions" import { internalNewDataModel } from "./newDataModel" import { setBaseDataModel } from "./_BaseDataModel" const dataModelInstanceCache = new WeakMap<ModelClass<AnyDataModel>, WeakMap<any, AnyDataModel>>() /** * Base abstract class for data models. Use `DataModel` instead when extending. * * Never override the constructor, use `onLazyInit` instead. * * @typeparam Data Props data type. */ export abstract class BaseDataModel<Data extends { [k: string]: any }> { // just to make typing work properly [dataTypeSymbol]: Data /** * Called after the instance is created when there's the first call to `fn(M, data)`. */ protected onLazyInit?(): void /** * Data part of the model, which is observable and will be serialized in snapshots. * Use it if one of the data properties matches one of the model properties/functions. * This also allows access to the backed values of transformed properties. */ readonly $!: Data /** * Performs a type check over the model instance. * For this to work a data type has to be declared as part of the model properties. * * @returns A `TypeCheckError` or `null` if there is no error. */ typeCheck(): TypeCheckError | null { const type = typesDataModelData<this>(this.constructor as any) return typeCheck(type, this.$ as any) } /** * Creates an instance of a data model. */ constructor(data: Data) { if (!isObject(data)) { throw failure("data models can only work over data objects") } const tweakedData = toTreeNode(data) const { modelClass: _modelClass }: DataModelConstructorOptions = arguments[1] as any const modelClass = _modelClass! const instancesForModelClass = getOrCreate( dataModelInstanceCache, modelClass, () => new WeakMap() ) const instance = instancesForModelClass.get(tweakedData) if (instance) { return instance } instancesForModelClass.set(tweakedData, this) Object.setPrototypeOf(this, modelClass.prototype) const self = this as any // delete unnecessary props delete self[dataTypeSymbol] internalNewDataModel(this, tweakedData as any, { modelClass, }) } toString(options?: { withData?: boolean }) { const finalOptions = { withData: true, ...options, } const modelInfo = modelInfoByClass.get(this.constructor as any) const firstPart = `${this.constructor.name}#${modelInfo!.name}` return finalOptions.withData ? `[${firstPart} ${JSON.stringify(getSnapshot(this))}]` : `[${firstPart}]` } } setBaseDataModel(BaseDataModel) /** * @ignore */ export type BaseDataModelKeys = keyof AnyDataModel | "onLazyInit" // these props will never be hoisted to this /** * @internal */ export const baseDataModelPropNames = new Set<BaseDataModelKeys>(["onLazyInit", "$", "typeCheck"]) /** * Any kind of data model instance. */ export interface AnyDataModel extends BaseDataModel<any> {} /** * A data model class declaration, made of a base model and the model interface. */ export type DataModelClassDeclaration<BaseModelClass, ModelInterface> = BaseModelClass & { (...args: any[]): ModelInterface }