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

184 lines (157 loc) 5.18 kB
import { action } from "mobx" import { isDataModel } from "../dataModel/utils" import { isModelAutoTypeCheckingEnabled } from "../globalConfig/globalConfig" import { getObjectChildren } from "../parent/coreObjectChildren" import { fastGetParent, ParentPath } from "../parent/path" import { setParent } from "../parent/setParent" import { unsetInternalSnapshot } from "../snapshot/internal" import type { AnyStandardType, TypeToData } from "../types/schemas" import { typeCheck } from "../types/typeCheck" import { failure, inDevMode, isMap, isObject, isPrimitive, isSet } from "../utils" import { isTweakedObject, tweakedObjects } from "./core" import { registerDefaultTweakers } from "./registerDefaultTweakers" /** * Turns an object (array, plain object) into a tree node, * which then can accept calls to `getParent`, `getSnapshot`, etc. * If a tree node is passed it will return the passed argument directly. * Additionally this method will use the type passed to check the value * conforms to the type when model auto type checking is enabled. * * @param type Type checker. * @param value Object to turn into a tree node. * @returns The object as a tree node. */ export function toTreeNode<TType extends AnyStandardType, V extends TypeToData<TType>>( type: TType, value: V ): V /** * Turns an object (array, plain object) into a tree node, * which then can accept calls to `getParent`, `getSnapshot`, etc. * If a tree node is passed it will return the passed argument directly. * * @param value Object to turn into a tree node. * @returns The object as a tree node. */ export function toTreeNode<T extends object>(value: T): T // base export function toTreeNode(arg1: any, arg2?: any): any { let value: object let type: AnyStandardType | undefined let hasType: boolean if (arguments.length === 1) { hasType = false value = arg1 } else { type = arg1 hasType = true value = arg2 } if (!isObject(value)) { throw failure("only objects can be turned into tree nodes") } if (hasType && isModelAutoTypeCheckingEnabled()) { const errors = typeCheck(type, value) if (errors) { errors.throw() } } if (!isTweakedObject(value, true)) { return tweak(value, undefined) } return value } /** * @internal */ export type Tweaker<T> = (value: T, parentPath: ParentPath<any> | undefined) => T | undefined const tweakers: { priority: number; tweaker: Tweaker<any> }[] = [] /** * @internal */ export function registerTweaker<T>(priority: number, tweaker: Tweaker<T>): void { tweakers.push({ priority, tweaker }) tweakers.sort((a, b) => a.priority - b.priority) } function internalTweak<T>(value: T, parentPath: ParentPath<any> | undefined): T { if (isPrimitive(value)) { return value } // already tweaked if (isTweakedObject(value, true)) { value = setParent( value, parentPath, false, // indexChangeAllowed false, // isDataObject true // cloneIfApplicable ) return value } // unsupported (must go before plain object tweaker) if (isDataModel(value)) { throw failure( "data models are not directly supported. you may insert the data in the tree instead ('$' property)." ) } registerDefaultTweakers() const tweakersLen = tweakers.length for (let i = 0; i < tweakersLen; i++) { const { tweaker } = tweakers[i] const tweakedVal = tweaker(value, parentPath) if (tweakedVal !== undefined) { return tweakedVal } } // unsupported if (isMap(value)) { throw failure("maps are not directly supported. consider using 'ObjectMap' / 'asMap' instead.") } // unsupported if (isSet(value)) { throw failure("sets are not directly supported. consider using 'ArraySet' / 'asSet' instead.") } throw failure( `tweak can only work over models, observable objects/arrays, or primitives, but got ${value} instead` ) } /** * @internal */ export const tweak = action("tweak", internalTweak) /** * @internal */ export function tryUntweak(value: any): (() => void) | undefined { if (isPrimitive(value)) { return undefined } if (inDevMode) { if (!fastGetParent(value, false)) { throw failure("assertion failed: object cannot be untweaked if it does not have a parent") } } const untweaker = tweakedObjects.get(value) if (!untweaker) { return undefined } // pre-untweaking, untweak children first // we have to make a copy since it will be changed const children = Array.from(getObjectChildren(value).values()) for (let i = 0; i < children.length; i++) { setParent( children[i], // value undefined, // parentPath false, // indexChangeAllowed false, // isDataObject // no need to clone if unsetting the parent false // cloneIfApplicable ) } return () => { // post-untweaking, call the untweaker untweaker() tweakedObjects.delete(value) unsetInternalSnapshot(value) } }