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

136 lines (119 loc) 3.58 kB
import { isObservable, toJS } from "mobx" import { getGlobalConfig } from "../globalConfig" import type { SnapshotInOfFrozen } from "../snapshot" import { tweak } from "../tweaker/tweak" import { failure, inDevMode, isPlainObject, isPrimitive } from "../utils" /** * Should freeze and plain json checks be done when creating the frozen object? */ export enum FrozenCheckMode { /** Only when in dev mode */ DevModeOnly = "devModeOnly", /** Always */ On = "on", /** Never */ Off = "off", } /** * @ignore */ export const frozenKey = "$frozen" /** * A class that contains frozen data. * Use `frozen` to create an instance of this class. * * @template T Data type. */ export class Frozen<T> { /** * Frozen data, deeply immutable. */ readonly data: T /** * Creates an instance of Frozen. * Do not use directly, use `frozen` instead. * * @param dataToFreeze * @param checkMode */ constructor(dataToFreeze: T, checkMode: FrozenCheckMode = FrozenCheckMode.DevModeOnly) { if (isObservable(dataToFreeze)) { dataToFreeze = toJS(dataToFreeze) } const check = checkMode === FrozenCheckMode.On || (inDevMode && checkMode === FrozenCheckMode.DevModeOnly) if (check) { checkDataIsSerializableAndFreeze(dataToFreeze) } this.data = dataToFreeze if (check) { Object.freeze(this.data) } tweak(this, undefined) } } /** * Marks some data as frozen. Frozen data becomes immutable (at least in dev mode), and is not enhanced * with capabilities such as getting the parent of the objects (except for the root object), it is not * made deeply observable (though the root object is observable by reference), etc. * On the other hand, this means it will be much faster to create/access. Use this for big data pieces * that are unlikely to change unless all of them change (for example lists of points for a polygon, etc). * * Note that data passed to frozen must be serializable to JSON, this is: * - primitive, plain object, or array * - without cycles * * @param data * @param checkMode */ export function frozen<T>( data: T, checkMode: FrozenCheckMode = FrozenCheckMode.DevModeOnly ): Frozen<T> { return new Frozen<T>(data, checkMode) } function checkDataIsSerializableAndFreeze(data: any) { // TODO: detect cycles and throw if present? // primitives are ok if (isPrimitive(data)) { return } if (Array.isArray(data)) { const arrLen = data.length for (let i = 0; i < arrLen; i++) { const v = data[i] if (v === undefined && !getGlobalConfig().allowUndefinedArrayElements) { throw failure( "undefined is not supported inside arrays since it is not serializable in JSON, consider using null instead" ) } checkDataIsSerializableAndFreeze(v) } Object.freeze(data) return } if (isPlainObject(data)) { const dataKeys = Object.keys(data) const dataKeysLen = dataKeys.length for (let i = 0; i < dataKeysLen; i++) { const k = dataKeys[i] const v = data[k] checkDataIsSerializableAndFreeze(k) checkDataIsSerializableAndFreeze(v) } Object.freeze(data) return } throw failure(`frozen data must be plainly serializable to JSON, but ${data} is not`) } /** * @internal * * Checks if an snapshot is an snapshot for a frozen data. * * @param snapshot * @returns */ export function isFrozenSnapshot(snapshot: unknown): snapshot is SnapshotInOfFrozen<Frozen<any>> { return isPlainObject(snapshot) && frozenKey in snapshot }