proxy-state-tree
Version:
An implementation of the Mobx/Vue state tracking approach, for library authors
318 lines (317 loc) • 12.7 kB
JavaScript
const ENVIRONMENT = (() => {
let env;
try {
env = process.env.NODE_ENV;
}
catch {
env = 'development';
}
return env;
})();
export const IS_PROXY = Symbol('IS_PROXY');
export const PATH = Symbol('PATH');
export const VALUE = Symbol('VALUE');
export const PROXY_TREE = Symbol('PROXY_TREE');
const isPlainObject = (value) => {
return (String(value) === '[object Object]' && value.constructor.name === 'Object');
};
const arrayMutations = new Set([
'push',
'shift',
'pop',
'unshift',
'splice',
'reverse',
'sort',
'copyWithin',
]);
const getValue = (proxyOrValue) => proxyOrValue && proxyOrValue[IS_PROXY] ? proxyOrValue[VALUE] : proxyOrValue;
const isClass = (value) => typeof value === 'object' &&
value !== null &&
!Array.isArray(value) &&
value.constructor.name !== 'Object' &&
Object.isExtensible(value);
const shouldProxy = (value) => {
return (value !== undefined &&
(!isClass(value) ||
(isClass(value) &&
!(value instanceof Date) &&
!(value instanceof Map) &&
!(value instanceof Set))));
};
export class Proxifier {
tree;
CACHED_PROXY = Symbol('CACHED_PROXY');
delimiter;
ssr;
constructor(tree) {
this.tree = tree;
this.delimiter = tree.root.options.delimiter;
this.ssr = Boolean(tree.root.options.ssr);
}
concat(path, prop) {
return path ? path + this.delimiter + prop : prop;
}
ensureMutationTrackingIsEnabled(path) {
if (ENVIRONMENT === 'production')
return;
if (this.tree.root.options.devmode && !this.tree.canMutate()) {
throw new Error(`proxy-state-tree - You are mutating the path "${path}", but it is not allowed. The following could have happened:
- The mutation is explicitly being blocked
- You are passing state to a 3rd party tool trying to manipulate the state
- You are running asynchronous code and forgot to "await" its execution
`);
}
}
isDefaultProxifier() {
return this.tree.proxifier === this.tree.root.proxifier;
}
ensureValueDosntExistInStateTreeElsewhere(value) {
if (ENVIRONMENT === 'production')
return;
if (value && value[IS_PROXY] === true) {
throw new Error(`proxy-state-tree - You are trying to insert a value that already exists in the state tree on path "${value[PATH]}"`);
}
return value;
}
trackPath(path) {
if (!this.tree.canTrack()) {
return;
}
if (this.isDefaultProxifier()) {
const trackStateTree = this.tree.root.currentTree;
if (!trackStateTree) {
return;
}
trackStateTree.addTrackingPath(path);
}
else {
;
this.tree.addTrackingPath(path);
}
}
// With tracking trees we want to ensure that we are always
// on the currently tracked tree. This ensures when we access
// a tracking proxy that is not part of the current tracking tree (pass as prop)
// we move the ownership to the current tracker
getTrackingTree() {
if (this.tree.root.currentTree && this.isDefaultProxifier()) {
return this.tree.root.currentTree;
}
if (!this.tree.canTrack()) {
return null;
}
if (this.tree.canTrack()) {
return this.tree;
}
return null;
}
getMutationTree() {
return this.tree.root.mutationTree || this.tree;
}
isProxyCached(value, path) {
return (value[this.CACHED_PROXY] &&
String(value[this.CACHED_PROXY][PATH]) === String(path));
}
createArrayProxy(value, path) {
if (!this.ssr && this.isProxyCached(value, path)) {
return value[this.CACHED_PROXY];
}
const proxifier = this;
const proxy = new Proxy(value, {
get(target, prop) {
if (prop === IS_PROXY)
return true;
if (prop === PATH)
return path;
if (prop === VALUE)
return value;
if (prop === 'indexOf') {
return (searchTerm, offset) => value.indexOf(getValue(searchTerm), getValue(offset));
}
if (prop === 'length' ||
(typeof target[prop] === 'function' &&
!arrayMutations.has(String(prop))) ||
typeof prop === 'symbol') {
return target[prop];
}
const trackingTree = proxifier.getTrackingTree();
const nestedPath = proxifier.concat(path, prop);
const currentTree = trackingTree || proxifier.tree;
trackingTree && trackingTree.proxifier.trackPath(nestedPath);
currentTree.trackPathListeners.forEach((cb) => cb(nestedPath));
const method = String(prop);
if (arrayMutations.has(method)) {
/* @__PURE__ */ proxifier.ensureMutationTrackingIsEnabled(nestedPath);
return (...args) => {
const mutationTree = proxifier.getMutationTree();
let result;
if (ENVIRONMENT === 'production') {
result = target[prop](...args);
}
else {
result = target[prop](...args.map((arg) =>
/* @__PURE__ */ proxifier.ensureValueDosntExistInStateTreeElsewhere(arg)));
}
mutationTree.addMutation({
method,
path,
delimiter: proxifier.delimiter,
args,
hasChangedValue: true,
});
return result;
};
}
if (shouldProxy(target[prop])) {
return proxifier.proxify(target[prop], nestedPath);
}
return target[prop];
},
set(target, prop, value) {
const nestedPath = proxifier.concat(path, prop);
/* @__PURE__ */ proxifier.ensureMutationTrackingIsEnabled(nestedPath);
/* @__PURE__ */ proxifier.ensureValueDosntExistInStateTreeElsewhere(value);
const mutationTree = proxifier.getMutationTree();
const result = Reflect.set(target, prop, value);
mutationTree.addMutation({
method: 'set',
path: nestedPath,
args: [value],
delimiter: proxifier.delimiter,
hasChangedValue: true,
});
return result;
},
});
if (!this.ssr) {
Object.defineProperty(value, this.CACHED_PROXY, {
value: proxy,
configurable: true,
});
}
return proxy;
}
createObjectProxy(object, path) {
if (!this.ssr && this.isProxyCached(object, path)) {
return object[this.CACHED_PROXY];
}
const proxifier = this;
const proxy = new Proxy(object, {
get(target, prop) {
if (prop === IS_PROXY)
return true;
if (prop === PATH)
return path;
if (prop === VALUE)
return object;
if (prop === PROXY_TREE)
return proxifier.tree;
if (typeof prop === 'symbol' || prop in Object.prototype)
return target[prop];
const descriptor = Object.getOwnPropertyDescriptor(target, prop) ||
(Object.getPrototypeOf(target) &&
Object.getOwnPropertyDescriptor(Object.getPrototypeOf(target), prop));
if (descriptor && 'get' in descriptor) {
const value = descriptor.get.call(proxy);
if (proxifier.tree.root.options.devmode &&
proxifier.tree.root.options.onGetter) {
proxifier.tree.root.options.onGetter(proxifier.concat(path, prop), value);
}
return value;
}
const trackingTree = proxifier.getTrackingTree();
const targetValue = target[prop];
const nestedPath = proxifier.concat(path, prop);
const currentTree = trackingTree || proxifier.tree;
if (typeof targetValue === 'function') {
if (proxifier.tree.root.options.onGetFunction) {
return proxifier.tree.root.options.onGetFunction(trackingTree || proxifier.tree, nestedPath, target, prop);
}
return isClass(target)
? targetValue
: targetValue.call(target, proxifier.tree, nestedPath);
}
else {
currentTree.trackPathListeners.forEach((cb) => cb(nestedPath));
trackingTree && trackingTree.proxifier.trackPath(nestedPath);
}
if (shouldProxy(targetValue)) {
return proxifier.proxify(targetValue, nestedPath);
}
return targetValue;
},
set(target, prop, value) {
const nestedPath = proxifier.concat(path, prop);
/* @__PURE__ */ proxifier.ensureMutationTrackingIsEnabled(nestedPath);
/* @__PURE__ */ proxifier.ensureValueDosntExistInStateTreeElsewhere(value);
let objectChangePath;
if (!(prop in target)) {
objectChangePath = path;
}
const mutationTree = proxifier.getMutationTree();
const existingValue = target[prop];
if (typeof value === 'function' &&
proxifier.tree.root.options.onSetFunction) {
value = proxifier.tree.root.options.onSetFunction(proxifier.getTrackingTree() || proxifier.tree, nestedPath, target, prop, value);
}
const hasChangedValue = existingValue !== value;
const result = Reflect.set(target, prop, value);
mutationTree.addMutation({
method: 'set',
path: nestedPath,
args: [value],
delimiter: proxifier.delimiter,
hasChangedValue,
}, objectChangePath);
return result;
},
deleteProperty(target, prop) {
const nestedPath = proxifier.concat(path, prop);
/* @__PURE__ */ proxifier.ensureMutationTrackingIsEnabled(nestedPath);
let objectChangePath;
if (prop in target) {
objectChangePath = path;
}
const mutationTree = proxifier.getMutationTree();
delete target[prop];
mutationTree.addMutation({
method: 'unset',
path: nestedPath,
args: [],
delimiter: proxifier.delimiter,
hasChangedValue: true,
}, objectChangePath);
return true;
},
});
if (!this.ssr) {
Object.defineProperty(object, this.CACHED_PROXY, {
value: proxy,
configurable: true,
});
}
return proxy;
}
proxify(value, path) {
if (value) {
const isUnmatchingProxy = value[IS_PROXY] &&
(String(value[PATH]) !== String(path) ||
value[VALUE][this.CACHED_PROXY] !== value);
if (isUnmatchingProxy) {
return this.proxify(value[VALUE], path);
}
else if (value[IS_PROXY]) {
return value;
}
else if (Array.isArray(value)) {
return this.createArrayProxy(value, path);
}
else if (isPlainObject(value) || isClass(value)) {
return this.createObjectProxy(value, path);
}
}
return value;
}
}
//# sourceMappingURL=Proxyfier.js.map