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

444 lines (398 loc) 8.82 kB
import * as mobx from "mobx" import { IObservableArray, isObservableArray, isObservableMap, isObservableObject, isObservableSet, ObservableMap, ObservableSet, } from "mobx" import { PrimitiveValue } from "./types" /** * A mobx-keystone error. */ export class MobxKeystoneError extends Error { constructor(msg: string) { super(msg) // Set the prototype explicitly. Object.setPrototypeOf(this, MobxKeystoneError.prototype) } } /** * @ignore * @internal */ export function failure(msg: string) { return new MobxKeystoneError(msg) } const writableHiddenPropDescriptor: PropertyDescriptor = { enumerable: false, writable: true, configurable: false, value: undefined, } /** * @ignore * @internal */ export function addHiddenProp(object: any, propName: PropertyKey, value: any, writable = true) { if (writable) { Object.defineProperty(object, propName, writableHiddenPropDescriptor) object[propName] = value } else { Object.defineProperty(object, propName, { enumerable: false, writable, configurable: true, value, }) } } /** * @ignore * @internal */ export function makePropReadonly<T>(object: T, propName: keyof T, enumerable: boolean) { const propDesc = Object.getOwnPropertyDescriptor(object, propName) if (propDesc) { propDesc.enumerable = enumerable if (propDesc.get) { delete propDesc.set } else { propDesc.writable = false } Object.defineProperty(object, propName, propDesc) } } /** * @ignore * @internal */ export function isPlainObject(value: any): value is Object { if (!isObject(value)) return false const proto = Object.getPrototypeOf(value) return proto === Object.prototype || proto === null } /** * @ignore * @internal */ export function isObject(value: any): value is Object { return value !== null && typeof value === "object" } /** * @ignore * @internal */ export function isPrimitive(value: any): value is PrimitiveValue { switch (typeof value) { case "number": case "string": case "boolean": case "undefined": case "bigint": return true } return value === null } /** * @ignore * @internal */ export function debugFreeze(value: object) { if (inDevMode()) { Object.freeze(value) } } /** * @ignore * @internal */ export function deleteFromArray<T>(array: T[], value: T): boolean { let index = array.indexOf(value) if (index >= 0) { array.splice(index, 1) return true } return false } /** * @ignore * @internal */ export function isMap(val: any): val is Map<any, any> | ObservableMap { return val instanceof Map || isObservableMap(val) } /** * @ignore * @internal */ export function isSet(val: any): val is Set<any> | ObservableSet { return val instanceof Set || isObservableSet(val) } /** * @ignore * @internal */ export function isArray(val: any): val is any[] | IObservableArray { return Array.isArray(val) || isObservableArray(val) } /** * @ignore * @internal */ export function inDevMode(): boolean { return process.env.NODE_ENV !== "production" } /** * @ignore * @internal */ export function assertIsObject(value: unknown, argName: string): asserts value is object { if (!isObject(value)) { throw failure(`${argName} must be an object`) } } /** * @ignore * @internal */ export function assertIsPlainObject(value: unknown, argName: string): asserts value is object { if (!isPlainObject(value)) { throw failure(`${argName} must be a plain object`) } } /** * @ignore * @internal */ export function assertIsObservableObject(value: unknown, argName: string): asserts value is object { if (!isObservableObject(value)) { throw failure(`${argName} must be an observable object`) } } /** * @ignore * @internal */ export function assertIsObservableArray( value: unknown, argName: string ): asserts value is IObservableArray { if (!isObservableArray(value)) { throw failure(`${argName} must be an observable array`) } } /** * @ignore * @internal */ export function assertIsMap(value: unknown, argName: string): asserts value is Map<any, any> { if (!isMap(value)) { throw failure(`${argName} must be a map`) } } /** * @ignore * @internal */ export function assertIsSet(value: unknown, argName: string): asserts value is Set<any> { if (!isSet(value)) { throw failure(`${argName} must be a set`) } } /** * @ignore * @internal */ export function assertIsFunction(value: unknown, argName: string): asserts value is Function { if (typeof value !== "function") { throw failure(`${argName} must be a function`) } } /** * @ignore * @internal */ export function assertIsPrimitive( value: unknown, argName: string ): asserts value is PrimitiveValue { if (!isPrimitive(value)) { throw failure(`${argName} must be a primitive`) } } /** * @ignore * @internal */ export function assertIsString(value: unknown, argName: string): asserts value is string { if (typeof value !== "string") { throw failure(`${argName} must be a string`) } } /** * @ignore * @internal */ export interface DecorateMethodOrFieldData { target: any propertyKey: string baseDescriptor?: PropertyDescriptor & { initializer?: () => any } } /** * @ignore * @internal */ export const runAfterNewSymbol = Symbol("runAfterNew") type WrapFunction = (data: DecorateMethodOrFieldData, fn: any) => any type LateInitializationFunctionsArray = ((instance: any) => void)[] /** * @ignore * @internal */ export function addLateInitializationFunction( target: any, symbol: symbol, fn: (instance: any) => void ) { let array: LateInitializationFunctionsArray = target[symbol] if (!array) { array = [] addHiddenProp(target, symbol, array) } array.push(fn) } /** * @ignore * @internal */ export function decorateWrapMethodOrField( decoratorName: string, data: DecorateMethodOrFieldData, wrap: WrapFunction ): any { const { target, propertyKey, baseDescriptor } = data const addFieldDecorator = () => { addLateInitializationFunction(target, runAfterNewSymbol, (instance) => { instance[propertyKey] = wrap(data, instance[propertyKey]) }) } 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 addFieldDecorator() } } /** * @ignore * @internal */ export function runLateInitializationFunctions(target: any, symbol: symbol): void { const fns: LateInitializationFunctionsArray | undefined = target[symbol] if (fns) { for (const fn of fns) { fn(target) } } } const warningsAlreadyDisplayed = new Set<string>() /** * @ignore * @internal */ export function logWarning(type: "warn" | "error", msg: string, uniqueKey?: string): void { if (uniqueKey) { if (warningsAlreadyDisplayed.has(uniqueKey)) { return } warningsAlreadyDisplayed.add(uniqueKey) } msg = "[mobx-keystone] " + msg switch (type) { case "warn": console.warn(msg) break case "error": console.error(msg) break default: throw failure(`unknown log type - ${type}`) } } const notMemoized = Symbol("notMemoized") /** * @ignore * @internal */ export function lateVal<TF extends (...args: any[]) => any>(getter: TF): TF { let memoized: TF | typeof notMemoized = notMemoized const fn = (...args: any[]): any => { if (memoized === notMemoized) { memoized = getter(...args) } return memoized } return fn as TF } /** * @ignore * @internal */ export function lazy<V>(valueGen: () => V): () => V { let inited = false let val: V | undefined return (): V => { if (!inited) { val = valueGen() inited = true } return val! } } /** * @ignore * @internal */ export const mobx6 = { // eslint-disable-next-line no-useless-concat makeObservable: (mobx as any)[ // just to ensure import * is kept properly String.fromCharCode("l".charCodeAt(0) + 1) + "akeObservable" ] as typeof mobx["makeObservable"], } /** * @ignore * @internal */ export function propNameToSetterName(propName: string): string { return `set${propName[0].toUpperCase()}${propName.slice(1)}` } /** * @ignore * @internal */ export function getMobxVersion(): number { if (mobx6.makeObservable!) { return 6 } else { return 5 } }