proxyequal
Version:
A proxy based usage tracking and comparison
510 lines (412 loc) • 12.8 kB
JavaScript
function _extends() { _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; }; return _extends.apply(this, arguments); }
/* eslint-disable no-control-regex */
import { str as crc32_str } from "crc-32";
import { getCollectionHandlers, shouldInstrument } from "./shouldInstrument";
import { weakMemoizeArray } from "./weakMemoize";
import { EDGE, memoizedBuildTrie } from "./objectTrie";
import { addDiffer, resetDiffers } from "./differs";
import { proxyPolyfill } from './proxy-polyfill';
import { escapeKey, unescapeKey } from "./escapeKey";
var hasProxy = typeof Proxy !== 'undefined';
var ProxyConstructor = hasProxy ? Proxy : proxyPolyfill();
var spreadMarker = '!SPREAD';
var __proxyequal_scanEnd = '__proxyequal_scanEnd';
var spreadActivation = '__proxyequal_spreadActivation';
var objectKeysMarker = '!Keys';
var areSpreadGuardsEnabled = false;
var areSourceMutationsEnabled = false;
var DISABLE_ALL_PROXIES = false;
export var spreadGuardsEnabled = function spreadGuardsEnabled(flag) {
return areSpreadGuardsEnabled = flag;
};
export var sourceMutationsEnabled = function sourceMutationsEnabled(flag) {
return areSourceMutationsEnabled = flag;
};
var ProxyToState = new WeakMap();
var ProxyToFinderPrint = new WeakMap();
var KnownObjects = new WeakSet();
var isProxyfied = function isProxyfied(object) {
return object && typeof object === 'object' ? ProxyToState.has(object) : false;
};
var isKnownObject = function isKnownObject(object) {
return object && typeof object === 'object' ? KnownObjects.has(object) : false;
};
var deproxify = function deproxify(object) {
return object && typeof object === 'object' ? ProxyToState.get(object) : object || object;
};
export var deepDeproxify = function deepDeproxify(object) {
if (object && typeof object === 'object') {
var current = object;
while (ProxyToState.has(current)) {
current = ProxyToState.get(current);
}
return current;
}
return object;
};
var getProxyKey = function getProxyKey(object) {
return object && typeof object === 'object' ? ProxyToFinderPrint.get(object) : {};
};
var prepareObject = function prepareObject(state) {
if (Object.isFrozen(state)) {
// unfreeze
if (Array.isArray(state)) {
return state.slice(0);
}
if (state.constructor.name === 'Object') {
var clone = Object.assign({}, state);
Object.setPrototypeOf(clone, Object.getPrototypeOf(state));
return clone;
}
}
return state;
};
export var PROXY_REST = Symbol('PROXY_REST');
var shouldProxy = function shouldProxy(type) {
return type === 'object';
};
export function proxyfy(state, report, suffix, fingerPrint, ProxyMap, control) {
if (suffix === void 0) {
suffix = '';
}
if (!state) {
return state;
}
var alreadyProxy = isProxyfied(state);
if (!alreadyProxy) {
KnownObjects.add(state);
}
if (!alreadyProxy && !shouldInstrument(state)) {
return state;
}
var hasCollectionHandlers = !alreadyProxy && getCollectionHandlers(state);
var storedValue = ProxyMap.get(state) || {};
if (storedValue[suffix]) {
return storedValue[suffix];
}
var theBaseObject = alreadyProxy ? state : prepareObject(state);
var shouldHookOwnKeys = areSpreadGuardsEnabled && !isProxyfied(state);
var iterable = function iterable(key, iterator) {
var _ref;
var index = 0;
var next = function next() {
var nextItem = iterator.next();
var subKey = key + '.' + index;
index++;
return _extends({}, nextItem, {
get value() {
if (nextItem.done && !nextItem.value) {
return;
}
return proxyValue(subKey, subKey, nextItem.value);
}
});
};
return _ref = {}, _ref[Symbol.iterator] = function () {
return {
next: next
};
}, _ref.next = next, _ref;
};
var proxyValue = function proxyValue(key, reportValue, value) {
var thisId = report(suffix, reportValue);
var type = typeof value;
if (shouldProxy(type)) {
return proxyfy(value, control.report, thisId, fingerPrint, ProxyMap, control);
}
if (hasCollectionHandlers) {
switch (key) {
case 'get':
return function (key) {
return proxyValue(key, escapeKey(key), state.get(key));
};
case 'has':
return function (key) {
return proxyValue(key, escapeKey(key), state.has(key));
};
case 'keys':
return function () {
return state.keys();
};
case 'values':
return function () {
return iterable(key, state.values());
};
case 'entries':
return function () {
return iterable(key, state.entries());
};
case [Symbol.iterator]:
return iterable(key, state[Symbol.iterator]);
}
}
return value;
};
var hooks = {
set: function set(target, prop, value) {
var thisId = report(suffix, prop);
if (areSourceMutationsEnabled) {
state[prop] = value;
return true;
} else {
/* eslint-disable-next-line */
console.error('Source object mutations are disabled, but you tried to set', value, 'on key', thisId, 'on', state);
return false;
}
},
get: function get(target, prop) {
if (process.env.NODE_ENV !== 'production') {
if (prop === __proxyequal_scanEnd) {
report(suffix, spreadMarker, suffix);
return false;
}
}
var storedValue = state[prop];
if (DISABLE_ALL_PROXIES) {
return storedValue;
}
if (typeof prop === 'string') {
return proxyValue(prop, escapeKey(prop), storedValue);
}
return storedValue;
}
};
hooks['ownKeys'] = function () {
var keys = [].concat(Object.getOwnPropertyNames(state), Object.getOwnPropertySymbols(state));
if (!DISABLE_ALL_PROXIES) {
report(suffix, objectKeysMarker, theBaseObject);
if (shouldHookOwnKeys) {
report(suffix, spreadActivation, theBaseObject);
keys.push(__proxyequal_scanEnd);
}
}
return keys;
};
var proxy = new ProxyConstructor(theBaseObject, hooks);
storedValue[suffix] = proxy;
ProxyMap.set(state, storedValue);
ProxyToState.set(proxy, state);
ProxyToFinderPrint.set(proxy, {
suffix: suffix,
fingerPrint: fingerPrint,
report: report,
ProxyMap: ProxyMap,
control: control
});
return proxy;
}
var withProxiesDisabled = function withProxiesDisabled(fn) {
if (DISABLE_ALL_PROXIES) {
return fn();
}
DISABLE_ALL_PROXIES = true;
try {
return fn();
} finally {
DISABLE_ALL_PROXIES = false;
}
};
var collectValuables = function collectValuables(lines) {
var values = [];
for (var i = 0; i < lines.length; ++i) {
var line = lines[i];
var index = line.lastIndexOf('.');
if (index < 0 && values.indexOf(line) < 0) {
// no "." and new value
values.push(line);
continue;
}
while (index >= 0) {
line = line.slice(0, index);
if (values.indexOf(line) < 0) {
values.push(line);
index = line.lastIndexOf('.');
} else {
break; // done that
}
}
}
return lines.filter(function (line) {
return values.indexOf(line) < 0;
});
};
var memoizedCollectValuables = weakMemoizeArray(collectValuables);
var get = function get(target, path) {
var result = target;
for (var i = 1; i < path.length && result; ++i) {
var key = unescapeKey(path[i]);
if (key[0] === '!') {
if (key === objectKeysMarker) {
return Object.keys(result).map(crc32_str).reduce(function (acc, x) {
return acc ^ x;
}, 0);
}
}
result = result[key];
}
return result;
};
var proxyCompare = function proxyCompare(a, b, locations) {
DISABLE_ALL_PROXIES = true;
var ret = function () {
for (var i = 0; i < locations.length; ++i) {
var key = locations[i];
var path = key.split('.');
var la = get(a, path);
var lb = get(b, path);
if (la === lb || deepDeproxify(la) === deepDeproxify(lb)) {// nope
} else {
addDiffer([key, 'differs', la, lb]);
return false;
}
}
return true;
}();
DISABLE_ALL_PROXIES = false;
return ret;
};
var getterHelper = ['', ''];
var walkDiffer = [];
var walk = function walk(la, lb, node) {
if (la === lb || deepDeproxify(la) === deepDeproxify(lb)) {
return true;
}
if (node === EDGE) {
return false;
}
var items = Object.keys(node);
for (var i = 0; i < items.length; ++i) {
var item = items[i];
getterHelper[1] = item;
if (!walk(get(la, getterHelper), get(lb, getterHelper), node[item])) {
walkDiffer.unshift(item);
return false;
}
}
return true;
};
var proxyShallowEqual = function proxyShallowEqual(a, b, locations) {
DISABLE_ALL_PROXIES = true;
walkDiffer = [];
resetDiffers();
var ret = function () {
var root = memoizedBuildTrie(locations);
return walk(a, b, root);
}();
DISABLE_ALL_PROXIES = false;
if (!ret) {
walkDiffer.unshift('');
addDiffer([walkDiffer.join('.'), 'not equal']);
}
return ret;
};
var proxyEqual = function proxyEqual(a, b, affected) {
resetDiffers();
return proxyCompare(a, b, memoizedCollectValuables(affected));
};
var proxyState = function proxyState(state, fingerPrint, _ProxyMap) {
if (fingerPrint === void 0) {
fingerPrint = '';
}
var lastAffected = null;
var affected = [];
var affectedEqualToLast = true;
var set = new Set();
var ProxyMap = _ProxyMap || new WeakMap();
var spreadDetected = false;
var speadActiveOn = [];
var sealed = 0;
var addSpreadTest = function addSpreadTest(location) {
if (process.env.NODE_ENV !== 'production') {
Object.defineProperty(location, __proxyequal_scanEnd, {
value: 'this is secure guard',
configurable: true,
enumerable: true
});
}
};
var removeSpreadTest = function removeSpreadTest() {
if (process.env.NODE_ENV !== 'production') {
speadActiveOn.forEach(function (target) {
return Object.defineProperty(target, __proxyequal_scanEnd, {
value: 'here was spread guard',
configurable: true,
enumerable: false
});
});
speadActiveOn = [];
}
};
var onKeyUse = function onKeyUse(suffix, keyName, location) {
var key = suffix + '.' + keyName;
if (!sealed) {
if (keyName === spreadActivation) {
if (process.env.NODE_ENV !== 'production') {
addSpreadTest(location);
speadActiveOn.push(location);
}
} else if (keyName === spreadMarker) {
if (process.env.NODE_ENV !== 'production') {
spreadDetected = spreadDetected || location;
}
} else {
if (!set.has(key)) {
set.add(key);
affected.push(key);
if (lastAffected) {
var position = affected.length - 1;
if (lastAffected[position] !== affected[position]) {
affectedEqualToLast = false;
}
}
}
}
}
return key;
};
var shouldUseLastAffected = function shouldUseLastAffected() {
return lastAffected && affectedEqualToLast && lastAffected.length === affected.length;
};
var control = {
get affected() {
return shouldUseLastAffected() ? lastAffected : affected;
},
get spreadDetected() {
return spreadDetected;
},
replaceState: function replaceState(state) {
this.state = createState(state);
spreadDetected = false;
this.unseal();
sealed = 0;
return this;
},
seal: function seal() {
sealed++;
removeSpreadTest();
},
unseal: function unseal() {
sealed--;
},
reset: function reset() {
if (!shouldUseLastAffected()) {
lastAffected = affected;
}
affectedEqualToLast = true;
affected = [];
spreadDetected = false;
sealed = 0;
set.clear();
},
isKnownObject: function isKnownObject(ref) {
return ProxyMap.has(ref);
},
report: onKeyUse
};
var createState = function createState(state) {
return proxyfy(state, onKeyUse, '', fingerPrint, ProxyMap, control);
};
control.state = createState(state);
return control;
};
export { proxyEqual, proxyShallowEqual, proxyState, proxyCompare, get, deproxify, isProxyfied, isKnownObject, getProxyKey, collectValuables, withProxiesDisabled };