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
text/typescript
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
}