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

141 lines (120 loc) 4.09 kB
import { action, isAction } from "mobx" import { fastGetParentPath } from "../parent/path" import type { PathElement } from "../parent/pathTypes" import { assertTweakedObject } from "../tweaker/core" import { assertIsFunction, deleteFromArray } from "../utils" import type { Patch } from "./Patch" /** * @ignore * @internal */ export class InternalPatchRecorder { patches!: Patch[] invPatches!: Patch[] record(patches: Patch[], invPatches: Patch[]) { this.patches = patches this.invPatches = invPatches } emit(obj: object) { emitPatch(obj, this.patches, this.invPatches, true) } } /** * A function that gets called when a patch is emitted. */ export type OnPatchesListener = (patches: Patch[], inversePatches: Patch[]) => void /** * A function that gets called when a global patch is emitted. */ export type OnGlobalPatchesListener = ( target: object, patches: Patch[], inversePatches: Patch[] ) => void /** * Disposer function to stop listening to patches. */ export type OnPatchesDisposer = () => void const patchListeners = new WeakMap<object, OnPatchesListener[]>() const globalPatchListeners: OnGlobalPatchesListener[] = [] /** * Adds a listener that will be called every time a patch is generated for the tree of the given target object. * * @param subtreeRoot Subtree root object of the patch listener. * @param listener The listener function that will be called everytime a patch is generated for the object or its children. * @returns A disposer to stop listening to patches. */ export function onPatches(subtreeRoot: object, listener: OnPatchesListener): OnPatchesDisposer { assertTweakedObject(subtreeRoot, "subtreeRoot") assertIsFunction(listener, "listener") if (!isAction(listener)) { listener = action(listener.name || "onPatchesListener", listener) } let listenersForObject = patchListeners.get(subtreeRoot) if (!listenersForObject) { listenersForObject = [] patchListeners.set(subtreeRoot, listenersForObject) } listenersForObject.push(listener) return () => { deleteFromArray(listenersForObject!, listener) } } /** * Adds a listener that will be called every time a patch is generated anywhere. * Usually prefer using `onPatches`. * * @param listener The listener function that will be called everytime a patch is generated anywhere. * @returns A disposer to stop listening to patches. */ export function onGlobalPatches(listener: OnGlobalPatchesListener): OnPatchesDisposer { assertIsFunction(listener, "listener") if (!isAction(listener)) { listener = action(listener.name || "onGlobalPatchesListener", listener) } globalPatchListeners.push(listener) return () => { deleteFromArray(globalPatchListeners, listener) } } function emitPatch( obj: object, patches: Patch[], inversePatches: Patch[], emitGlobally: boolean ): void { if (patches.length <= 0 && inversePatches.length <= 0) { return } // first emit global listeners if (emitGlobally) { for (let i = 0; i < globalPatchListeners.length; i++) { const listener = globalPatchListeners[i] listener(obj, patches, inversePatches) } } // then per subtree listeners const listenersForObject = patchListeners.get(obj) if (listenersForObject) { for (let i = 0; i < listenersForObject.length; i++) { const listener = listenersForObject[i] listener(patches, inversePatches) } } // and also emit subtree listeners all the way to the root const parentPath = fastGetParentPath(obj) if (parentPath) { // tweak patches so they include the child path const childPath = parentPath.path const newPatches = patches.map((p) => addPathToPatch(p, childPath)) const newInversePatches = inversePatches.map((p) => addPathToPatch(p, childPath)) // false to avoid emitting global patches again for the same change emitPatch(parentPath.parent, newPatches, newInversePatches, false) } } function addPathToPatch(patch: Patch, path: PathElement): Patch { return { ...patch, path: [path, ...patch.path], } }