@geogirafe/lib-geoportal
Version:
GeoGirafe is a flexible application to build online geoportals.
389 lines (388 loc) • 15 kB
JavaScript
import areEqual from './equality';
import { deepClone, deepFreeze, isFunction, isIgnoredProperty, isIterator, isPrimitive, isTypeSupported, isVirtualProperty } from './tools';
export default class Brain {
constructor(initialState, callback) {
Object.defineProperty(this, "initialState", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "stateProxy", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "externalCallback", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "targetToProxy", {
enumerable: true,
configurable: true,
writable: true,
value: new WeakMap()
});
Object.defineProperty(this, "proxyToTarget", {
enumerable: true,
configurable: true,
writable: true,
value: new WeakMap()
});
Object.defineProperty(this, "proxyChildren", {
enumerable: true,
configurable: true,
writable: true,
value: new WeakMap()
});
Object.defineProperty(this, "proxyParents", {
enumerable: true,
configurable: true,
writable: true,
value: new WeakMap()
});
Object.defineProperty(this, "proxyToFullPaths", {
enumerable: true,
configurable: true,
writable: true,
value: new WeakMap()
});
Object.defineProperty(this, "delayed", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "delayedCallbacks", {
enumerable: true,
configurable: true,
writable: true,
value: new Map()
});
Object.defineProperty(this, "objectHandler", {
enumerable: true,
configurable: true,
writable: true,
value: () => ({
get: (target, prop, _receiver) => {
const proxy = this.targetToProxy.get(target);
return this.getHandler(proxy, target, prop);
},
set: (target, prop, newValue, _receiver) => {
const proxy = this.targetToProxy.get(target);
if (Array.isArray(target)) {
return this.setArrayHandler(proxy, target, prop, newValue);
}
return this.setHandler(proxy, target, prop, newValue);
}
})
});
this.initialState = initialState;
this.stateProxy = this.createProxy(this.initialState, '');
this.externalCallback = callback;
}
callback(path, oldValue, newValue, parents) {
if (!this.delayed) {
// Immediate Callback
this.externalCallback(path, oldValue, newValue, parents);
}
else {
// Remember all callback infos
const infos = this.delayedCallbacks.get(path);
if (!infos) {
this.delayedCallbacks.set(path, { oldValue: oldValue, newValue: newValue, parents: parents });
}
else {
// Just set the new value
infos.newValue = newValue;
}
}
}
createProxy(target, prop, parent) {
const proxy = new Proxy(target, this.objectHandler());
this.targetToProxy.set(target, proxy);
this.proxyToTarget.set(proxy, target);
this.proxyChildren.set(proxy, new Map());
if (parent) {
this.proxyParents.set(proxy, [parent]);
this.proxyChildren.get(parent).set(prop, proxy);
const paths = parent.__brainFullPaths.map((path) => this.getFullPath(path, prop));
this.proxyToFullPaths.set(proxy, paths);
}
else {
// Root element
this.proxyToFullPaths.set(proxy, ['']);
}
return proxy;
}
getOrCreateProxyForValue(proxy, prop, value, childPaths) {
if (isPrimitive(value)) {
// No proxy for primitives
return undefined;
}
let valueProxy = this.targetToProxy.get(value);
if (valueProxy) {
// Proxy already exists
if (valueProxy.__brainChildren.size === 0) {
// Has not children yet, it means this proxy is not used in another place yet
// This is no circular reference, we can process with the normal workflow
// Otherwise, we just keep the existing infos and do not add any new child, parent or path
proxy.__brainChildren.set(prop, valueProxy);
const fullPaths = valueProxy.__brainFullPaths;
for (const childPath of childPaths) {
if (!fullPaths.includes(childPath)) {
fullPaths.push(childPath);
}
}
// TODO REG : We have to update the child paths recursively too !!
const parents = valueProxy.__brainParents;
if (!parents.includes(proxy)) {
parents.push(proxy);
}
}
}
else {
// Create a new proxy
valueProxy = this.createProxy(value, prop, proxy);
}
return valueProxy;
}
cleanProxyForValue(proxy, prop, value, childPaths) {
if (isPrimitive(value)) {
// No proxy for primitives
return;
}
const valueProxy = this.targetToProxy.get(value);
if (!valueProxy) {
// Nothing to clean
return;
}
proxy.__brainChildren.delete(prop);
const fullPaths = valueProxy.__brainFullPaths;
for (const childPath of childPaths) {
const index = fullPaths.indexOf(childPath);
if (index >= 0) {
fullPaths.splice(index, 1);
}
}
// TODO REG : We have to update the child paths recursively too !!
const parents = valueProxy.__brainParents;
const index = parents.indexOf(proxy);
if (index >= 0) {
// TODO Remove only if another child at the same level does not have it as parent
parents.splice(index, 1);
}
}
handleGetVirtualProperties(proxy, target, prop) {
switch (prop) {
case '__brainIsProxy':
return true;
case '__brainTarget':
return target;
case '__brainParents':
return this.proxyParents.get(proxy);
case '__brainChildren':
return this.proxyChildren.get(proxy);
case '__brainFullPaths':
return this.proxyToFullPaths.get(proxy) ?? [];
}
throw new Error(`Unknown virtual property: ${prop}`);
}
handleGetIgnored(target, prop) {
const value = target[prop];
if (typeof prop === 'symbol' || prop.startsWith('_')) {
return value;
}
throw new Error(`Unknown ignored property: ${prop}`);
}
handleGetFunctions(proxy, target, func) {
return (...args) => {
// args can contain a callback for func likes filter, map, reduce, forEach, ...
if (args.length === 1 && isFunction(args[0])) {
const originalCallback = args[0];
args[0] = (...cbArgs) => {
// We want to be able to use proxies in the callback
cbArgs = cbArgs.map((arg) => this.targetToProxy.get(arg) ?? arg);
return originalCallback(...cbArgs);
};
}
else {
// If args contains some proxies, we replace them with the target objects
args = args.map((arg) => (arg.__brainIsProxy ? arg.__brainTarget : arg));
}
const oldValue = deepClone(target);
deepFreeze(oldValue);
let result = target[func](...args);
if (result instanceof Promise) {
throw new Error('Promises are not supported yet.');
}
if (Array.isArray(result)) {
// Get proxied results
result = result.map((r) => this.targetToProxy.get(r) ?? r);
}
else if (isIterator(result)) {
// Iterator
const originalIterator = result;
result = {
[Symbol.iterator]() {
return {
next: () => {
const nextVal = originalIterator.next();
if (nextVal.done) {
return nextVal;
}
return {
done: false,
// Get proxied results
value: this.targetToProxy.get(nextVal.value) ?? nextVal.value
};
}
};
}
};
}
else {
// Unique result
result = this.targetToProxy.get(result) ?? result;
}
if (!areEqual(oldValue, target)) {
for (const path of proxy.__brainFullPaths) {
this.callback(path, oldValue, proxy, proxy.__brainParents);
}
}
return result;
};
}
getHandler(proxy, target, prop) {
if (isIgnoredProperty(prop)) {
return this.handleGetIgnored(target, prop);
}
if (isVirtualProperty(prop)) {
return this.handleGetVirtualProperties(proxy, target, prop);
}
const value = target[prop];
if (isPrimitive(value)) {
// No proxy for primitives
return value;
}
if (!isTypeSupported(value)) {
throw new Error(`The type of the property ${prop} is not supported.`);
}
if (isFunction(value)) {
if (Array.isArray(target)) {
// Create proxies for all children
for (const i in target) {
const childPaths = proxy.__brainFullPaths.map((path) => this.getFullPath(path, i));
this.getOrCreateProxyForValue(proxy, i, target[i], childPaths);
}
}
return this.handleGetFunctions(proxy, target, prop);
}
const childPaths = proxy.__brainFullPaths.map((path) => this.getFullPath(path, prop));
const childProxy = this.getOrCreateProxyForValue(proxy, prop, value, childPaths);
return childProxy;
}
setHandler(proxy, target, prop, newValue) {
try {
let newValueProxy;
if (newValue?.__brainIsProxy) {
newValueProxy = newValue;
newValue = newValueProxy.__brainTarget;
}
if (isFunction(newValue)) {
// For functions, just set the property, there is nothing else to do.
target[prop] = newValue;
return true;
}
// Clean proxies
let oldValue = target[prop];
const childPaths = proxy.__brainFullPaths.map((path) => this.getFullPath(path, prop));
this.cleanProxyForValue(proxy, prop, oldValue, childPaths);
if (isIgnoredProperty(prop)) {
// Ignored => No callback and no clone of the old version
// Just set the new value
target[prop] = newValue;
return true;
}
// Create frozen clone as oldValue
oldValue = deepClone(oldValue);
deepFreeze(oldValue);
target[prop] = newValue;
if (!isTypeSupported(newValue)) {
throw new Error(`The type of the property ${prop} is not supported.`);
}
if (areEqual(oldValue, newValue)) {
// No change => No callback
return true;
}
// Call callbacks
newValueProxy = this.getOrCreateProxyForValue(proxy, prop, newValue, childPaths);
for (const childPath of childPaths) {
this.callback(childPath, oldValue, newValueProxy ?? newValue, [proxy]);
}
return true;
}
catch (error) {
console.error(`Cannot set ${prop} on ${target} : ${error}`);
return false;
}
}
setArrayHandler(proxy, target, prop, newValue) {
try {
if (!Array.isArray(target)) {
throw new Error('This method should only be called on arrays');
}
if (newValue?.__brainIsProxy) {
newValue = newValue.__brainTarget;
}
if (isFunction(Reflect.get(target, prop)) || isFunction(newValue)) {
throw new Error('Setting functions is not supported.');
}
const oldTarget = deepClone(target);
deepFreeze(oldTarget);
Reflect.set(target, prop, newValue);
if (areEqual(oldTarget, target)) {
// No change => No callback
return true;
}
// Call callbacks
const childPaths = proxy.__brainFullPaths.map((path) => this.getFullPath(path, prop));
this.getOrCreateProxyForValue(proxy, prop, newValue, childPaths);
for (const childPath of proxy.__brainFullPaths) {
this.callback(childPath, oldTarget, proxy, proxy.__brainParents);
}
return true;
}
catch (error) {
console.error(`Cannot set ${prop} on ${target} : ${error}`);
return false;
}
}
getFullPath(parentPath, prop) {
if (parentPath.trim().length > 0) {
return `${parentPath}.${prop}`;
}
return prop;
}
delay(statechanges) {
this.delayed = true;
try {
// Execute state changes
statechanges(this.stateProxy);
}
finally {
this.delayed = false;
// Call all callbacks
this.delayedCallbacks.forEach((infos, key) => {
this.externalCallback(key, infos.oldValue, infos.newValue, infos.parents);
});
this.delayedCallbacks.clear();
}
}
getState() {
return this.stateProxy;
}
}