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

196 lines (162 loc) 5.08 kB
import { action, createAtom, IAtom } from "mobx" import { fastGetParentPath, ParentPath } from "../parent/path" import { invalidateCachedToSnapshotProcessorResult } from "../types/TypeChecker" import { isPrimitive } from "../utils" import { PrimitiveValue } from "../utils/types" /** * @internal */ export type SnapshotTransformFn = (sn: unknown) => unknown interface SnapshotData { untransformed: any readonly transformFn: SnapshotTransformFn | undefined transformed: any atom: IAtom | undefined } const snapshots = new WeakMap<object, SnapshotData>() // true if it has been accessed publicly and therefore should be cloned // rather than modified in place const frozenState = new WeakMap<object, boolean>() /** * @internal */ export function getInternalSnapshot<T extends object>( value: T ): Readonly<SnapshotData> | undefined { return snapshots.get(value) } interface InternalSnapshotParent { parentSnapshot: SnapshotData | undefined parentPath: ParentPath<any> } function getInternalSnapshotParent( sn: Readonly<SnapshotData> | undefined, parentPath: ParentPath<any> | undefined ): InternalSnapshotParent | undefined { if (!(parentPath && sn)) { return undefined } const parentSn = getInternalSnapshot(parentPath.parent) if (!parentSn) { return undefined } return { parentSnapshot: parentSn, parentPath: parentPath, } } /** * @internal */ export const unsetInternalSnapshot = action("unsetInternalSnapshot", (value: any) => { const oldSn = getInternalSnapshot(value) if (oldSn) { snapshots.delete(value) oldSn.atom?.reportChanged() } }) /** * @internal */ export const setNewInternalSnapshot = action( "setNewInternalSnapshot", <T extends object>( value: any, untransformed: T, transformFn: SnapshotTransformFn | undefined, markAsFrozen = false ): void => { const transformed: any = transformFn ? transformFn(untransformed) : untransformed const sn: SnapshotData = { untransformed, transformFn, transformed, atom: undefined, // will be created when first observed } frozenState.set(untransformed, markAsFrozen) if (transformed !== undefined && transformed !== untransformed) { frozenState.set(transformed, markAsFrozen) } snapshots.set(value, sn) sn.atom?.reportChanged() } ) type MutateInternalSnapshotFn<T> = (prevSn: T) => void /** * @internal */ export const updateInternalSnapshot = action( "updateInternalSnapshot", <T extends object>(value: any, mutate: MutateInternalSnapshotFn<T>): void => { const sn = getInternalSnapshot(value)! as SnapshotData let untransformed = sn.untransformed const snFrozen = frozenState.get(untransformed)! if (snFrozen) { if (Array.isArray(untransformed)) { untransformed = untransformed.slice() } else { untransformed = Object.assign({}, untransformed) } } else { // the processor cached result is no longer valid since we will // mutate the object invalidateCachedToSnapshotProcessorResult(untransformed) } mutate(untransformed) sn.untransformed = untransformed sn.transformed = sn.transformFn ? sn.transformFn(untransformed) : untransformed frozenState.set(sn.untransformed, false) if (sn.transformed !== undefined) { frozenState.set(sn.transformed, false) } sn.atom?.reportChanged() // also update parent(s) snapshot(s) if needed const parent = getInternalSnapshotParent(sn, fastGetParentPath(value, false)) if (parent) { const { parentSnapshot, parentPath } = parent // might be false in the cases where the parent has not yet been created if (parentSnapshot) { const path = parentPath.path // patches for parent changes should not be emitted updateInternalSnapshot(parentPath.parent, (objOrArray: any) => { objOrArray[path] = sn.transformed }) } } } ) /** * @internal */ export function reportInternalSnapshotObserved(sn: SnapshotData) { if (!sn.atom) { sn.atom = createAtom("snapshot") } sn.atom.reportObserved() } /** * @internal */ export function freezeInternalSnapshot<T extends PrimitiveValue | object>(data: T): T { if (isPrimitive(data)) { return data } // this might be undefined if the data comes from example from transforms const isFrozen = frozenState.get(data) if (isFrozen === undefined || isFrozen) { // already frozen or an external data (e.g. from a transform) return data } if (Array.isArray(data)) { for (let i = 0; i < data.length; i++) { freezeInternalSnapshot(data[i]) } } else { const keys = Object.keys(data) for (let i = 0; i < keys.length; i++) { freezeInternalSnapshot((data as any)[keys[i]]) } } frozenState.set(data, true) return data }