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

173 lines (154 loc) 5.75 kB
import { runInAction } from "mobx" import { applyAction } from "../../action/applyAction" import { frozenKey } from "../../frozen/Frozen" import { getModelIdPropertyName } from "../../model/getModelMetadata" import { isModel } from "../../model/utils" import { resolvePath } from "../../parent/path" import type { WritablePath } from "../../parent/pathTypes" import { applyPatches } from "../../patch/applyPatches" import { onPatches } from "../../patch/emitPatch" import type { Patch } from "../../patch/Patch" import { assertTweakedObject } from "../../tweaker/core" import { failure, isObject } from "../../utils" import { deserializeActionCall, SerializedActionCall } from "./actionSerialization" /** * Serialized action call with model ID overrides. * Can be generated with `applySerializedActionAndTrackNewModelIds`. * To be applied with `applySerializedActionAndSyncNewModelIds`. */ export interface SerializedActionCallWithModelIdOverrides extends SerializedActionCall { /** * Model Id overrides to be applied at the end of applying the action. */ readonly modelIdOverrides: ReadonlyArray<Patch> } /** * Applies (runs) a serialized action over a target object. * In this mode newly generated / modified model IDs will be tracked * so they can be later synchronized when applying it on another machine * via `applySerializedActionAndSyncNewModelIds`. * This means this method is usually used on the server side. * * If you intend to apply non-serialized actions check `applyAction` instead. * * @param subtreeRoot Subtree root target object to run the action over. * @param call The serialized action, usually as coming from the server/client. * @returns The return value of the action, if any, plus a new serialized action * with model overrides. */ export function applySerializedActionAndTrackNewModelIds<TRet = any>( subtreeRoot: object, call: SerializedActionCall ): { returnValue: TRet serializedActionCall: SerializedActionCallWithModelIdOverrides } { if (!call.serialized) { throw failure("cannot apply a non-serialized action call, use 'applyAction' instead") } assertTweakedObject(subtreeRoot, "subtreeRoot") const deserializedCall = deserializeActionCall(call, subtreeRoot) const modelIdOverrides: Patch[] = [] // set a patch listener to track changes to model ids const patchDisposer = onPatches(subtreeRoot, (patches) => { scanPatchesForModelIdChanges(subtreeRoot, modelIdOverrides, patches) }) try { const returnValue = applyAction(subtreeRoot, deserializedCall) return { returnValue, serializedActionCall: { ...call, modelIdOverrides, }, } } finally { patchDisposer() } } function scanPatchesForModelIdChanges(root: object, modelIdOverrides: Patch[], patches: Patch[]) { const len = patches.length for (let i = 0; i < len; i++) { const patch = patches[i] if (patch.op === "replace" || patch.op === "add") { deepScanValueForModelIdChanges( root, modelIdOverrides, patch.value, patch.path as WritablePath ) } } } function deepScanValueForModelIdChanges( root: object, modelIdOverrides: Patch[], value: any, path: WritablePath ) { if (path.length > 0 && typeof value === "string") { // ensure the parent is an actual model const parent = resolvePath(root, path.slice(0, path.length - 1)).value if (isModel(parent)) { const propertyName = path[path.length - 1] if (propertyName === getModelIdPropertyName(parent.constructor as any)) { // found one modelIdOverrides.push({ op: "replace", path: path.slice(), value: value, }) } } } else if (Array.isArray(value)) { const len = value.length for (let i = 0; i < len; i++) { path.push(i) deepScanValueForModelIdChanges(root, modelIdOverrides, value[i], path) path.pop() } } else if (isObject(value)) { // skip frozen values if (!value[frozenKey]) { const keys = Object.keys(value) const len = keys.length for (let i = 0; i < len; i++) { const propName = keys[i] const propValue = value[propName] path.push(propName) deepScanValueForModelIdChanges(root, modelIdOverrides, propValue, path) path.pop() } } } } /** * Applies (runs) a serialized action over a target object. * In this mode newly generated / modified model IDs previously tracked * by `applySerializedActionAndTrackNewModelIds` will be synchronized after * the action is applied. * This means this method is usually used on the client side. * * If you intend to apply non-serialized actions check `applyAction` instead. * * @param subtreeRoot Subtree root target object to run the action over. * @param call The serialized action, usually as coming from the server/client. * @returns The return value of the action, if any. */ export function applySerializedActionAndSyncNewModelIds<TRet = any>( subtreeRoot: object, call: SerializedActionCallWithModelIdOverrides ): TRet { if (!call.serialized) { throw failure("cannot apply a non-serialized action call, use 'applyAction' instead") } assertTweakedObject(subtreeRoot, "subtreeRoot") const deserializedCall = deserializeActionCall(call, subtreeRoot) let returnValue: any runInAction(() => { returnValue = applyAction(subtreeRoot, deserializedCall) // apply model id overrides applyPatches(subtreeRoot, call.modelIdOverrides) }) return returnValue }