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