UNPKG

mobx-state-tree

Version:

Opinionated, transactional, MobX powered state container

1,370 lines (1,339 loc) 140 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var mobx = require('mobx'); /*! ***************************************************************************** Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 THIS CODE IS PROVIDED ON AN *AS IS* BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING WITHOUT LIMITATION ANY IMPLIED WARRANTIES OR CONDITIONS OF TITLE, FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABLITY OR NON-INFRINGEMENT. See the Apache Version 2.0 License for specific language governing permissions and limitations under the License. ***************************************************************************** */ /* global Reflect, Promise */ var extendStatics = Object.setPrototypeOf || ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; function __extends(d, b) { extendStatics(d, b); function __() { this.constructor = d; } d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); } function __decorate(decorators, target, key, desc) { var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d; if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc); else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r; return c > 3 && r && Object.defineProperty(target, key, r), r; } var EMPTY_ARRAY = Object.freeze([]); var EMPTY_OBJECT = Object.freeze({}); function fail(message) { if (message === void 0) { message = "Illegal state"; } throw new Error("[mobx-state-tree] " + message); } function identity(_) { return _; } function noop() { } function isArray(val) { return !!(Array.isArray(val) || mobx.isObservableArray(val)); } function asArray(val) { if (!val) return EMPTY_ARRAY; if (isArray(val)) return val; return [val]; } function extend(a) { var b = []; for (var _i = 1; _i < arguments.length; _i++) { b[_i - 1] = arguments[_i]; } for (var i = 0; i < b.length; i++) { var current = b[i]; for (var key in current) a[key] = current[key]; } return a; } function isPlainObject(value) { if (value === null || typeof value !== "object") return false; var proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } function isMutable(value) { return (value !== null && typeof value === "object" && !(value instanceof Date) && !(value instanceof RegExp)); } function isPrimitive(value) { if (value === null || value === undefined) return true; if (typeof value === "string" || typeof value === "number" || typeof value === "boolean" || value instanceof Date) return true; return false; } function freeze(value) { return isPrimitive(value) ? value : Object.freeze(value); } function deepFreeze(value) { freeze(value); if (isPlainObject(value)) { Object.keys(value).forEach(function (propKey) { if (!isPrimitive(value[propKey]) && !Object.isFrozen(value[propKey])) { deepFreeze(value[propKey]); } }); } return value; } function isSerializable(value) { return typeof value !== "function"; } function addHiddenFinalProp(object, propName, value) { Object.defineProperty(object, propName, { enumerable: false, writable: false, configurable: true, value: value }); } function addReadOnlyProp(object, propName, value) { Object.defineProperty(object, propName, { enumerable: true, writable: false, configurable: true, value: value }); } function remove(collection, item) { var idx = collection.indexOf(item); if (idx !== -1) collection.splice(idx, 1); } function registerEventHandler(handlers, handler) { handlers.push(handler); return function () { remove(handlers, handler); }; } function argsToArray(args) { var res = new Array(args.length); for (var i = 0; i < args.length; i++) res[i] = args[i]; return res; } // https://tools.ietf.org/html/rfc6902 // http://jsonpatch.com/ function splitPatch(patch) { if (!("oldValue" in patch)) fail("Patches without `oldValue` field cannot be inversed"); return [stripPatch(patch), invertPatch(patch)]; } function stripPatch(patch) { // strips `oldvalue` information from the patch, so that it becomes a patch conform the json-patch spec // this removes the ability to undo the patch switch (patch.op) { case "add": return { op: "add", path: patch.path, value: patch.value }; case "remove": return { op: "remove", path: patch.path }; case "replace": return { op: "replace", path: patch.path, value: patch.value }; } } function invertPatch(patch) { switch (patch.op) { case "add": return { op: "remove", path: patch.path }; case "remove": return { op: "add", path: patch.path, value: patch.oldValue }; case "replace": return { op: "replace", path: patch.path, value: patch.oldValue }; } } /** * escape slashes and backslashes * http://tools.ietf.org/html/rfc6901 */ function escapeJsonPath(str) { return str.replace(/~/g, "~1").replace(/\//g, "~0"); } /** * unescape slashes and backslashes */ function unescapeJsonPath(str) { return str.replace(/~0/g, "/").replace(/~1/g, "~"); } function joinJsonPath(path) { // `/` refers to property with an empty name, while `` refers to root itself! if (path.length === 0) return ""; return "/" + path.map(escapeJsonPath).join("/"); } function splitJsonPath(path) { // `/` refers to property with an empty name, while `` refers to root itself! var parts = path.split("/").map(unescapeJsonPath); // path '/a/b/c' -> a b c // path '../../b/c -> .. .. b c return parts[0] === "" ? parts.slice(1) : parts; } var TypeFlags; (function (TypeFlags) { TypeFlags[TypeFlags["String"] = 1] = "String"; TypeFlags[TypeFlags["Number"] = 2] = "Number"; TypeFlags[TypeFlags["Boolean"] = 4] = "Boolean"; TypeFlags[TypeFlags["Date"] = 8] = "Date"; TypeFlags[TypeFlags["Literal"] = 16] = "Literal"; TypeFlags[TypeFlags["Array"] = 32] = "Array"; TypeFlags[TypeFlags["Map"] = 64] = "Map"; TypeFlags[TypeFlags["Object"] = 128] = "Object"; TypeFlags[TypeFlags["Frozen"] = 256] = "Frozen"; TypeFlags[TypeFlags["Optional"] = 512] = "Optional"; TypeFlags[TypeFlags["Reference"] = 1024] = "Reference"; TypeFlags[TypeFlags["Identifier"] = 2048] = "Identifier"; TypeFlags[TypeFlags["Late"] = 4096] = "Late"; TypeFlags[TypeFlags["Refinement"] = 8192] = "Refinement"; TypeFlags[TypeFlags["Union"] = 16384] = "Union"; TypeFlags[TypeFlags["Null"] = 32768] = "Null"; TypeFlags[TypeFlags["Undefined"] = 65536] = "Undefined"; })(TypeFlags || (TypeFlags = {})); function isType(value) { return typeof value === "object" && value && value.isType === true; } function isPrimitiveType(type) { return (isType(type) && (type.flags & (TypeFlags.String | TypeFlags.Number | TypeFlags.Boolean | TypeFlags.Date)) > 0); } function isReferenceType(type) { return (type.flags & TypeFlags.Reference) > 0; } /** * Returns the _actual_ type of the given tree node. (Or throws) * * @export * @param {IStateTreeNode} object * @returns {IType<S, T>} */ function getType(object) { return getStateTreeNode(object).type; } /** * Returns the _declared_ type of the given sub property of an object, array or map. * * @example * const Box = types.model({ x: 0, y: 0 }) * const box = Box.create() * * console.log(getChildType(box, "x").name) // 'number' * * @export * @param {IStateTreeNode} object * @param {string} child * @returns {IType<any, any>} */ function getChildType(object, child) { return getStateTreeNode(object).getChildType(child); } /** * Registers a function that will be invoked for each mutation that is applied to the provided model instance, or to any of its children. * See [patches](https://github.com/mobxjs/mobx-state-tree#patches) for more details. onPatch events are emitted immediately and will not await the end of a transaction. * Patches can be used to deep observe a model tree. * * @export * @param {Object} target the model instance from which to receive patches * @param {(patch: IJsonPatch, reversePatch) => void} callback the callback that is invoked for each patch. The reversePatch is a patch that would actually undo the emitted patch * @param {includeOldValue} boolean if oldValue is included in the patches, they can be inverted. However patches will become much bigger and might not be suitable for efficient transport * @returns {IDisposer} function to remove the listener */ function onPatch(target, callback) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof callback !== "function") fail("expected second argument to be a function, got " + callback + " instead"); } return getStateTreeNode(target).onPatch(callback); } /** * Registers a function that is invoked whenever a new snapshot for the given model instance is available. * The listener will only be fire at the and of the current MobX (trans)action. * See [snapshots](https://github.com/mobxjs/mobx-state-tree#snapshots) for more details. * * @export * @param {Object} target * @param {(snapshot: any) => void} callback * @returns {IDisposer} */ function onSnapshot(target, callback) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof callback !== "function") fail("expected second argument to be a function, got " + callback + " instead"); } return getStateTreeNode(target).onSnapshot(callback); } /** * Applies a JSON-patch to the given model instance or bails out if the patch couldn't be applied * See [patches](https://github.com/mobxjs/mobx-state-tree#patches) for more details. * * Can apply a single past, or an array of patches. * * @export * @param {Object} target * @param {IJsonPatch} patch * @returns */ function applyPatch(target, patch) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof patch !== "object") fail("expected second argument to be an object or array, got " + patch + " instead"); } getStateTreeNode(target).applyPatches(asArray(patch)); } /** * Small abstraction around `onPatch` and `applyPatch`, attaches a patch listener to a tree and records all the patches. * Returns an recorder object with the following signature: * * @example * export interface IPatchRecorder { * // the recorded patches * patches: IJsonPatch[] * // the inverse of the recorded patches * inversePatches: IJsonPatch[] * // stop recording patches * stop(target?: IStateTreeNode): any * // resume recording patches * resume() * // apply all the recorded patches on the given target (the original subject if omitted) * replay(target?: IStateTreeNode): any * // reverse apply the recorded patches on the given target (the original subject if omitted) * // stops the recorder if not already stopped * undo(): void * } * * @export * @param {IStateTreeNode} subject * @returns {IPatchRecorder} */ function recordPatches(subject) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(subject)) fail("expected first argument to be a mobx-state-tree node, got " + subject + " instead"); } var disposer = null; function resume() { if (disposer) return; disposer = onPatch(subject, function (patch, inversePatch) { recorder.rawPatches.push([patch, inversePatch]); }); } var recorder = { rawPatches: [], get patches() { return this.rawPatches.map(function (_a) { var a = _a[0]; return a; }); }, get inversePatches() { return this.rawPatches.map(function (_a) { var _ = _a[0], b = _a[1]; return b; }); }, stop: function () { if (disposer) disposer(); disposer = null; }, resume: resume, replay: function (target) { applyPatch(target || subject, recorder.patches); }, undo: function (target) { applyPatch(target || subject, recorder.inversePatches.slice().reverse()); } }; resume(); return recorder; } /** * The inverse of `unprotect` * * @export * @param {IStateTreeNode} target * */ function protect(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } var node = getStateTreeNode(target); if (!node.isRoot) fail("`protect` can only be invoked on root nodes"); node.isProtectionEnabled = true; } /** * By default it is not allowed to directly modify a model. Models can only be modified through actions. * However, in some cases you don't care about the advantages (like replayability, traceability, etc) this yields. * For example because you are building a PoC or don't have any middleware attached to your tree. * * In that case you can disable this protection by calling `unprotect` on the root of your tree. * * @example * const Todo = types.model({ * done: false, * toggle() { * this.done = !this.done * } * }) * * const todo = new Todo() * todo.done = true // OK * protect(todo) * todo.done = false // throws! * todo.toggle() // OK */ function unprotect(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } var node = getStateTreeNode(target); if (!node.isRoot) fail("`unprotect` can only be invoked on root nodes"); node.isProtectionEnabled = false; } /** * Returns true if the object is in protected mode, @see protect */ function isProtected(target) { return getStateTreeNode(target).isProtected; } /** * Applies a snapshot to a given model instances. Patch and snapshot listeners will be invoked as usual. * * @export * @param {Object} target * @param {Object} snapshot * @returns */ function applySnapshot(target, snapshot) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } return getStateTreeNode(target).applySnapshot(snapshot); } /** * Calculates a snapshot from the given model instance. The snapshot will always reflect the latest state but use * structural sharing where possible. Doesn't require MobX transactions to be completed. * * @export * @param {Object} target * @returns {*} */ function getSnapshot(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } return getStateTreeNode(target).snapshot; } /** * Given a model instance, returns `true` if the object has a parent, that is, is part of another object, map or array * * @export * @param {Object} target * @param {number} depth = 1, how far should we look upward? * @returns {boolean} */ function hasParent(target, depth) { if (depth === void 0) { depth = 1; } // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof depth !== "number") fail("expected second argument to be a number, got " + depth + " instead"); if (depth < 0) fail("Invalid depth: " + depth + ", should be >= 1"); } var parent = getStateTreeNode(target).parent; while (parent) { if (--depth === 0) return true; parent = parent.parent; } return false; } /** * Returns the immediate parent of this object, or null. * * Note that the immediate parent can be either an object, map or array, and * doesn't necessarily refer to the parent model * * @export * @param {Object} target * @param {number} depth = 1, how far should we look upward? * @returns {*} */ function getParent(target, depth) { if (depth === void 0) { depth = 1; } // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof depth !== "number") fail("expected second argument to be a number, got " + depth + " instead"); if (depth < 0) fail("Invalid depth: " + depth + ", should be >= 1"); } var d = depth; var parent = getStateTreeNode(target).parent; while (parent) { if (--d === 0) return parent.storedValue; parent = parent.parent; } return fail("Failed to find the parent of " + getStateTreeNode(target) + " at depth " + depth); } /** * Given an object in a model tree, returns the root object of that tree * * @export * @param {Object} target * @returns {*} */ function getRoot(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } return getStateTreeNode(target).root.storedValue; } /** * Returns the path of the given object in the model tree * * @export * @param {Object} target * @returns {string} */ function getPath(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } return getStateTreeNode(target).path; } /** * Returns the path of the given object as unescaped string array * * @export * @param {Object} target * @returns {string[]} */ function getPathParts(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } return splitJsonPath(getStateTreeNode(target).path); } /** * Returns true if the given object is the root of a model tree * * @export * @param {Object} target * @returns {boolean} */ function isRoot(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } return getStateTreeNode(target).isRoot; } /** * Resolves a path relatively to a given object. * Returns undefined if no value can be found. * * @export * @param {Object} target * @param {string} path - escaped json path * @returns {*} */ function resolvePath(target, path) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof path !== "string") fail("expected second argument to be a number, got " + path + " instead"); } var node = getStateTreeNode(target).resolve(path); return node ? node.value : undefined; } /** * Resolves a model instance given a root target, the type and the identifier you are searching for. * Returns undefined if no value can be found. * * @export * @param {IType<any, any>} type * @param {IStateTreeNode} target * @param {(string | number)} identifier * @returns {*} */ function resolveIdentifier(type, target, identifier) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isType(type)) fail("expected first argument to be a mobx-state-tree type, got " + type + " instead"); if (!isStateTreeNode(target)) fail("expected second argument to be a mobx-state-tree node, got " + target + " instead"); if (!(typeof identifier === "string" || typeof identifier === "number")) fail("expected third argument to be a string or number, got " + identifier + " instead"); } var node = getStateTreeNode(target).root.identifierCache.resolve(type, "" + identifier); return node ? node.value : undefined; } /** * * * @export * @param {Object} target * @param {string} path * @returns {*} */ function tryResolve(target, path) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof path !== "string") fail("expected second argument to be a string, got " + path + " instead"); } var node = getStateTreeNode(target).resolve(path, false); if (node === undefined) return undefined; return node ? node.value : undefined; } /** * Given two state tree nodes that are part of the same tree, * returns the shortest jsonpath needed to navigate from the one to the other * * @export * @param {IStateTreeNode} base * @param {IStateTreeNode} target * @returns {string} */ function getRelativePath(base, target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected second argument to be a mobx-state-tree node, got " + target + " instead"); if (!isStateTreeNode(base)) fail("expected first argument to be a mobx-state-tree node, got " + base + " instead"); } return getStateTreeNode(base).getRelativePathTo(getStateTreeNode(target)); } /** * Returns a deep copy of the given state tree node as new tree. * Short hand for `snapshot(x) = getType(x).create(getSnapshot(x))` * * _Tip: clone will create a literal copy, including the same identifiers. To modify identifiers etc during cloning, don't use clone but take a snapshot of the tree, modify it, and create new instance_ * * @export * @template T * @param {T} source * @param {boolean | any} keepEnvironment indicates whether the clone should inherit the same environment (`true`, the default), or not have an environment (`false`). If an object is passed in as second argument, that will act as the environment for the cloned tree. * @returns {T} */ function clone(source, keepEnvironment) { if (keepEnvironment === void 0) { keepEnvironment = true; } // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(source)) fail("expected first argument to be a mobx-state-tree node, got " + source + " instead"); } var node = getStateTreeNode(source); return node.type.create(node.snapshot, keepEnvironment === true ? node.root._environment : keepEnvironment === false ? undefined : keepEnvironment); // it's an object or something else } /** * Removes a model element from the state tree, and let it live on as a new state tree */ function detach(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } getStateTreeNode(target).detach(); return target; } /** * Removes a model element from the state tree, and mark it as end-of-life; the element should not be used anymore */ function destroy(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } var node = getStateTreeNode(target); if (node.isRoot) node.die(); else node.parent.removeChild(node.subpath); } /** * Returns true if the given state tree node is not killed yet. * This means that the node is still a part of a tree, and that `destroy` * has not been called. If a node is not alive anymore, the only thing one can do with it * is requesting it's last path and snapshot * * @export * @param {IStateTreeNode} target * @returns {boolean} */ function isAlive(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } return getStateTreeNode(target).isAlive; } /** * Use this utility to register a function that should be called whenever the * targeted state tree node is destroyed. This is a useful alternative to managing * cleanup methods yourself using the `beforeDestroy` hook. * * @example * const Todo = types.model({ * title: types.string * }, { * afterCreate() { * const autoSaveDisposer = reaction( * () => getSnapshot(this), * snapshot => sendSnapshotToServerSomehow(snapshot) * ) * // stop sending updates to server if this * // instance is destroyed * addDisposer(this, autoSaveDisposer) * } * }) * * @export * @param {IStateTreeNode} target * @param {() => void} disposer */ function addDisposer(target, disposer) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof disposer !== "function") fail("expected second argument to be a function, got " + disposer + " instead"); } getStateTreeNode(target).addDisposer(disposer); } /** * Returns the environment of the current state tree. For more info on environments, * see [Dependency injection](https://github.com/mobxjs/mobx-state-tree#dependency-injection) * * Returns an empty environment if the tree wasn't initialized with an environment * * @export * @param {IStateTreeNode} target * @returns {*} */ function getEnv(target) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); } var node = getStateTreeNode(target); var env = node.root._environment; if (!!!env) return EMPTY_OBJECT; return env; } /** * Performs a depth first walk through a tree */ function walk(target, processor) { // check all arguments if (process.env.NODE_ENV !== "production") { if (!isStateTreeNode(target)) fail("expected first argument to be a mobx-state-tree node, got " + target + " instead"); if (typeof processor !== "function") fail("expected second argument to be a function, got " + processor + " instead"); } var node = getStateTreeNode(target); // tslint:disable-next-line:no_unused-variable node.getChildren().forEach(function (child) { if (isStateTreeNode(child.storedValue)) walk(child.storedValue, processor); }); processor(node.storedValue); } var nextActionId = 1; var currentActionContext = null; function getNextActionId() { return nextActionId++; } function runWithActionContext(context, fn) { var node = getStateTreeNode(context.context); var baseIsRunningAction = node._isRunningAction; var prevContext = currentActionContext; node.assertAlive(); node._isRunningAction = true; currentActionContext = context; try { return runMiddleWares(node, context, fn); } finally { currentActionContext = prevContext; node._isRunningAction = baseIsRunningAction; } } function getActionContext() { if (!currentActionContext) return fail("Not running an action!"); return currentActionContext; } function createActionInvoker(target, name, fn) { var wrappedFn = mobx.action(name, fn); wrappedFn.$mst_middleware = fn.$mst_middleware; return function () { var id = getNextActionId(); return runWithActionContext({ type: "action", name: name, id: id, args: argsToArray(arguments), context: target, tree: getRoot(target), rootId: currentActionContext ? currentActionContext.rootId : id, parentId: currentActionContext ? currentActionContext.id : 0 }, wrappedFn); }; } /** * Middleware can be used to intercept any action is invoked on the subtree where it is attached. * If a tree is protected (by default), this means that any mutation of the tree will pass through your middleware. * * For more details, see the [middleware docs](docs/middleware.md) * * @export * @param {IStateTreeNode} target * @param {(action: IRawActionCall, next: (call: IRawActionCall) => any) => any} middleware * @returns {IDisposer} */ function addMiddleware(target, middleware) { var node = getStateTreeNode(target); if (process.env.NODE_ENV !== "production") { if (!node.isProtectionEnabled) console.warn("It is recommended to protect the state tree before attaching action middleware, as otherwise it cannot be guaranteed that all changes are passed through middleware. See `protect`"); } return node.addMiddleWare(middleware); } /** * Binds middleware to a specific action * * @example * type.actions(self => { * function takeA____() { * self.toilet.donate() * self.wipe() * self.wipe() * self.toilet.flush() * } * return { * takeA____: decorate(atomic, takeA____) * } * }) * * @export * @template T * @param {IMiddlewareHandler} middleware * @param Function} fn * @returns the original function */ function decorate(middleware, fn) { if (fn.$mst_middleware) fn.$mst_middleware.push(middleware); else fn.$mst_middleware = [middleware]; return fn; } function collectMiddlewareHandlers(node, baseCall, fn) { var handlers = fn.$mst_middleware || []; var n = node; // Find all middlewares. Optimization: cache this? while (n) { handlers = handlers.concat(n.middlewares); n = n.parent; } return handlers; } function runMiddleWares(node, baseCall, originalFn) { var handlers = collectMiddlewareHandlers(node, baseCall, originalFn); // Short circuit if (!handlers.length) return originalFn.apply(null, baseCall.args); function runNextMiddleware(call) { var handler = handlers.shift(); // Optimization: counter instead of shift is probably faster if (handler) return handler(call, runNextMiddleware); else return originalFn.apply(null, baseCall.args); } return runNextMiddleware(baseCall); } var IdentifierCache = /** @class */ (function () { function IdentifierCache() { this.cache = mobx.observable.map(); } IdentifierCache.prototype.addNodeToCache = function (node) { if (node.identifierAttribute) { var identifier = node.identifier; if (!this.cache.has(identifier)) { this.cache.set(identifier, mobx.observable.shallowArray()); } var set = this.cache.get(identifier); if (set.indexOf(node) !== -1) fail("Already registered"); set.push(node); } return this; }; IdentifierCache.prototype.mergeCache = function (node) { var _this = this; node.identifierCache.cache.values().forEach(function (nodes) { return nodes.forEach(function (child) { _this.addNodeToCache(child); }); }); }; IdentifierCache.prototype.notifyDied = function (node) { if (node.identifierAttribute) { var set = this.cache.get(node.identifier); if (set) set.remove(node); } }; IdentifierCache.prototype.splitCache = function (node) { var res = new IdentifierCache(); var basePath = node.path; this.cache.values().forEach(function (nodes) { for (var i = nodes.length - 1; i >= 0; i--) { if (nodes[i].path.indexOf(basePath) === 0) { res.addNodeToCache(nodes[i]); nodes.splice(i, 1); } } }); return res; }; IdentifierCache.prototype.resolve = function (type, identifier) { var set = this.cache.get(identifier); if (!set) return null; var matches = set.filter(function (candidate) { return type.isAssignableFrom(candidate.type); }); switch (matches.length) { case 0: return null; case 1: return matches[0]; default: return fail("Cannot resolve a reference to type '" + type.name + "' with id: '" + identifier + "' unambigously, there are multiple candidates: " + matches .map(function (n) { return n.path; }) .join(", ")); } }; return IdentifierCache; }()); var nextNodeId = 1; var Node = /** @class */ (function () { function Node(type, parent, subpath, environment, initialValue, createNewInstance, finalizeNewInstance) { if (createNewInstance === void 0) { createNewInstance = identity; } if (finalizeNewInstance === void 0) { finalizeNewInstance = noop; } var _this = this; // optimization: these fields make MST memory expensive for primitives. Most can be initialized lazily, or with EMPTY_ARRAY on prototype this.nodeId = ++nextNodeId; this._parent = null; this.subpath = ""; this.isProtectionEnabled = true; this.identifierAttribute = undefined; // not to be modified directly, only through model initialization this._environment = undefined; this._isRunningAction = false; // only relevant for root this._autoUnbox = true; // unboxing is disabled when reading child nodes this._isAlive = true; // optimization: use binary flags for all these switches this._isDetaching = false; this.middlewares = []; this.snapshotSubscribers = []; // TODO: split patches in two; patch and reversePatch this.patchSubscribers = []; this.disposers = []; this.type = type; this._parent = parent; this.subpath = subpath; this._environment = environment; this.unbox = this.unbox.bind(this); this.storedValue = createNewInstance(initialValue); var canAttachTreeNode = canAttachNode(this.storedValue); // Optimization: this does not need to be done per instance // if some pieces from createActionInvoker are extracted this.applyPatches = createActionInvoker(this.storedValue, "@APPLY_PATCHES", function (patches) { patches.forEach(function (patch) { var parts = splitJsonPath(patch.path); var node = _this.resolvePath(parts.slice(0, -1)); node.applyPatchLocally(parts[parts.length - 1], patch); }); }).bind(this.storedValue); this.applySnapshot = createActionInvoker(this.storedValue, "@APPLY_SNAPSHOT", function (snapshot) { // if the snapshot is the same as the current one, avoid performing a reconcile if (snapshot === _this.snapshot) return; // else, apply it by calling the type logic return _this.type.applySnapshot(_this, snapshot); }).bind(this.storedValue); if (!parent) this.identifierCache = new IdentifierCache(); if (canAttachTreeNode) addHiddenFinalProp(this.storedValue, "$treenode", this); var sawException = true; try { if (canAttachTreeNode) addHiddenFinalProp(this.storedValue, "toJSON", toJSON); this._isRunningAction = true; finalizeNewInstance(this, initialValue); this._isRunningAction = false; if (parent) parent.root.identifierCache.addNodeToCache(this); else this.identifierCache.addNodeToCache(this); this.fireHook("afterCreate"); if (parent) this.fireHook("afterAttach"); sawException = false; } finally { if (sawException) { // short-cut to die the instance, to avoid the snapshot computed starting to throw... this._isAlive = false; } } // optimization: don't keep the snapshot by default alive with a reaction by default // in prod mode. This saves lot of GC overhead (important for e.g. React Native) // if the feature is not actively used // downside; no structural sharing if getSnapshot is called incidently var snapshotDisposer = mobx.reaction(function () { return _this.snapshot; }, function (snapshot) { _this.emitSnapshot(snapshot); }); snapshotDisposer.onError(function (e) { throw e; }); this.addDisposer(snapshotDisposer); } Object.defineProperty(Node.prototype, "identifier", { get: function () { return this.identifierAttribute ? this.storedValue[this.identifierAttribute] : null; }, enumerable: true, configurable: true }); Object.defineProperty(Node.prototype, "path", { /* * Returnes (escaped) path representation as string */ get: function () { if (!this.parent) return ""; return this.parent.path + "/" + escapeJsonPath(this.subpath); }, enumerable: true, configurable: true }); Object.defineProperty(Node.prototype, "isRoot", { get: function () { return this.parent === null; }, enumerable: true, configurable: true }); Object.defineProperty(Node.prototype, "parent", { get: function () { return this._parent; }, enumerable: true, configurable: true }); Object.defineProperty(Node.prototype, "root", { // TODO: make computed get: function () { // future optimization: store root ref in the node and maintain it var p, r = this; while ((p = r.parent)) r = p; return r; }, enumerable: true, configurable: true }); // TODO: lift logic outside this file Node.prototype.getRelativePathTo = function (target) { // PRE condition target is (a child of) base! if (this.root !== target.root) fail("Cannot calculate relative path: objects '" + this + "' and '" + target + "' are not part of the same object tree"); var baseParts = splitJsonPath(this.path); var targetParts = splitJsonPath(target.path); var common = 0; for (; common < baseParts.length; common++) { if (baseParts[common] !== targetParts[common]) break; } // TODO: assert that no targetParts paths are "..", "." or ""! return (baseParts.slice(common).map(function (_) { return ".."; }).join("/") + joinJsonPath(targetParts.slice(common))); }; Node.prototype.resolve = function (path, failIfResolveFails) { if (failIfResolveFails === void 0) { failIfResolveFails = true; } return this.resolvePath(splitJsonPath(path), failIfResolveFails); }; Node.prototype.resolvePath = function (pathParts, failIfResolveFails) { if (failIfResolveFails === void 0) { failIfResolveFails = true; } // counter part of getRelativePath // note that `../` is not part of the JSON pointer spec, which is actually a prefix format // in json pointer: "" = current, "/a", attribute a, "/" is attribute "" etc... // so we treat leading ../ apart... var current = this; for (var i = 0; i < pathParts.length; i++) { if (pathParts[i] === "") current = current.root; else if (pathParts[i] === ".." // '/bla' or 'a//b' splits to empty strings ) current = current.parent; else if (pathParts[i] === "." || pathParts[i] === "") continue; else if (current) { current = current.getChildNode(pathParts[i]); continue; } if (!current) { if (failIfResolveFails) return fail("Could not resolve '" + pathParts[i] + "' in '" + joinJsonPath(pathParts.slice(0, i - 1)) + "', path of the patch does not resolve"); else return undefined; } } return current; }; Object.defineProperty(Node.prototype, "value", { get: function () { if (!this._isAlive) return undefined; return this.type.getValue(this); }, enumerable: true, configurable: true }); Object.defineProperty(Node.prototype, "isAlive", { get: function () { return this._isAlive; }, enumerable: true, configurable: true }); Node.prototype.die = function () { if (this._isDetaching) return; if (isStateTreeNode(this.storedValue)) { walk(this.storedValue, function (child) { return getStateTreeNode(child).aboutToDie(); }); walk(this.storedValue, function (child) { return getStateTreeNode(child).finalizeDeath(); }); } }; Node.prototype.aboutToDie = function () { this.disposers.splice(0).forEach(function (f) { return f(); }); this.fireHook("beforeDestroy"); }; Node.prototype.finalizeDeath = function () { // invariant: not called directly but from "die" this.root.identifierCache.notifyDied(this); var self = this; var oldPath = this.path; addReadOnlyProp(this, "snapshot", this.snapshot); // kill the computed prop and just store the last snapshot this.patchSubscribers.splice(0); this.snapshotSubscribers.splice(0); this.patchSubscribers.splice(0); this._isAlive = false; this._parent = null; this.subpath = ""; // This is quite a hack, once interceptable objects / arrays / maps are extracted from mobx, // we could express this in a much nicer way // TODO: should be possible to obtain id's still... Object.defineProperty(this.storedValue, "$mobx", { get: function () { fail("This object has died and is no longer part of a state tree. It cannot be used anymore. The object (of type '" + self .type .name + "') used to live at '" + oldPath + "'. It is possible to access the last snapshot of this object using 'getSnapshot', or to create a fresh copy using 'clone'. If you want to remove an object from the tree without killing it, use 'detach' instead."); } }); }; Node.prototype.assertAlive = function () { if (!this._isAlive) fail(this + " cannot be used anymore as it has died; it has been removed from a state tree. If you want to remove an element from a tree and let it live on, use 'detach' or 'clone' the value"); }; Object.defineProperty(Node.prototype, "snapshot", { get: function () { if (!this._isAlive) return undefined; // advantage of using computed for a snapshot is that nicely respects transactions etc. // Optimization: only freeze on dev builds return freeze(this.type.getSnapshot(this)); }, enumerable: true, configurable: true }); Node.prototype.onSnapshot = function (onChange) { return registerEventHandler(this.snapshotSubscribers, onChange); }; Node.prototype.emitSnapshot = function (snapshot) { this.snapshotSubscribers.forEach(function (f) { return f(snapshot); }); }; Node.prototype.applyPatchLocally = function (subpath, patch) { this.assertWritable(); this.type.applyPatchLocally(this, subpath, patch); }; Node.prototype.onPatch = function (handler) { return registerEventHandler(this.patchSubscribers, handler); }; Node.prototype.emitPatch = function (basePatch, source) { if (this.patchSubscribers.length) { var localizedPatch = extend({}, basePatch, { path: source.path.substr(this.path.length) + "/" + basePatch.path // calculate the relative path of the patch }); var _a = splitPatch(localizedPatch), patch_1 = _a[0], reversePatch_1 = _a[1]; this.patchSubscribers.forEach(function (f) { return f(patch_1, reversePatch_1); }); } if (this.parent) this.parent.emitPatch(basePatch, source); }; Node.prototype.setParent = function (newParent, subpath) { if (subpath === void 0) { subpath = null; } if (this.parent === newParent && this.subpath === subpath) return; if (newParent) { if (this._parent && newParent !== this._parent) { fail("A node cannot exists twice in the state tree. Failed to add " + this + " to path '" + newParent.path + "/" + subpath + "'."); } if (!this._parent && newParent.root === this) { fail("A state tree is not allowed to contain itself. Cannot assign " + this + " to path '" + newParent.path + "/" + subpath + "'"); } if (!this._parent && !!this._environment && this._environment !== newParent._environment) { fail("A state tree cannot be made part of another state tree as long as their environments are different."); } } if (this.parent && !newParent) { this.die(); } else { this.subpath = subpath || ""; if (newParent && newParent !== this._parent) { newParent.root.identifierCache.mergeCache(this); this._parent = newParent; this.fireHook("afterAttach"); } } }; Node.prototype.addDisposer = function (disposer) { this.disposers.unshift(disposer); }; Node.prototype.isRunningAction = function () { if (this._isRunningAction) return true; if (this.isRoot) return false; return this.parent.isRunningAction(); }; Node.prototype.addMiddleWare = function (handler) { return registerEventHandler(this.middlewares, handler); }; Node.prototype.getChildNode = function (subpath) { this.assertAlive(); this._autoUnbox = false; var res = this.type.getChildNode(this, subpath); this._autoUnbox = true; return res; }; Node.prototype.getChildren = function () { this.assertAlive(); this._autoUnbox = false; var res = this.type.getChildren(this); this._autoUnbox = true; return res; }; Node.prototype.getChildType = function (key) { return this.type.getChildType(key); }; Object.defineProperty(Node.prototype, "isProtected", { get: function () { return this.root.isProtectionEnabled; }, enumerable: true, configurable: true }); Node.prototype.assertWritable = function () { this.assertAlive(); if (!this.isRunningAction() && this.isProtected) { fail("Cannot modify '" + this + "', the object is protected and can only be modified by using an action."); } }; Node.prototype.removeChild = f