mobx-state-tree
Version:
Opinionated, transactional, MobX powered state container
1,366 lines (1,337 loc) • 139 kB
JavaScript
import { action, computed, extendShallowObservable, extras, intercept, isComputed, isObservableArray, observable, observe, reaction, runInAction } from '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) || 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 = 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 = observable.map();
}
IdentifierCache.prototype.addNodeToCache = function (node) {
if (node.identifierAttribute) {
var identifier = node.identifier;
if (!this.cache.has(identifier)) {
this.cache.set(identifier, 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 = 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.");
}
};
No