Skip to content
Snippets Groups Projects
Commit 494c186f authored by Arkerthan's avatar Arkerthan
Browse files

Add diff proxy

parent a1215f92
No related branches found
No related tags found
1 merge request!10062object diff system
function tests() {
const getProxy = App.Utils.Diff.getProxy;
function orig() {
return {
a: 1,
b: [1, 2],
c: {a: 1}
};
}
function log(name, p) {
console.log(name);
const original = p.diffOriginal;
const diff = p.diffChange;
console.log("Original:", _.cloneDeep(original));
console.log("Diff: ", diff);
App.Utils.Diff.applyDiff(original, diff);
console.log("Apply: ", original);
}
log("Proxy", getProxy(orig()));
let o = getProxy(orig());
o.a = 2;
log(1, o);
o = getProxy(orig());
delete o.a;
log(2, o);
o = getProxy(orig());
o.c.a = 2;
log(3, o);
o = getProxy(orig());
delete o.c.a;
log(4, o);
o = getProxy(orig());
delete o.c;
log(5, o);
o = getProxy(orig());
o.b[1] = 5;
log(6, o);
o = getProxy(orig());
o.b.push(5);
log(7, o);
o = getProxy(orig());
o.b.push(5);
console.log("EXPECT: 5, IS: ", o.b[2]);
log(8, o);
o = getProxy(orig());
console.log("POP 1:", o.b.pop());
log(9, o);
o = getProxy(orig());
o.d = 7;
log(10, o);
o = getProxy(orig());
o.d = {a: 5};
console.log("Expect 5:", o.d.a);
log(11, o);
o = getProxy(orig());
o.d = {a: [5]};
o.d.a.unshift(9);
log(12, o);
let slaveDummy = getProxy({eye: new App.Entity.EyeState()});
eyeSurgery(slaveDummy, "left", "remove");
log(20, slaveDummy);
slaveDummy = getProxy({eye: new App.Entity.EyeState()});
eyeSurgery(slaveDummy, "both", "remove");
log(20, slaveDummy);
}
declare namespace FC {
namespace Util {
interface DiffBase<T> {
/**
* The original object
*/
diffOriginal: T
/**
* The changes applied to the object
*/
diffChange: Partial<T>
}
type DiffRecorder<T> = T & DiffBase<T>
}
}
/**
* The diff proxy records all changes to the original object without changing the original object while emulating the
* changes for access from outside. The resulting diff object can then be applied on the original. The resulting object
* is the same as if the changes were applied directly to the original object.
*/
App.Utils.Diff = (function() {
const deletedSymbol = Symbol("deleted property");
/**
* @template {object} T
* @param {T} base
* @returns {FC.Util.DiffRecorder<T>}
*/
function getProxy(base) {
const diff = {};
/**
* @typedef {Array<string|symbol>} path
* Can only point to objects or arrays, never to primitives
*/
/**
* @param {path} path
* @returns {any}
*/
function localDiff(path) {
let value = diff;
for (const key of path) {
if (key in value) {
value = value[key];
} else {
return {};
}
}
return value;
}
/**
* @param {path} path
* @param {string|symbol} prop
* @param {any} value
*/
function setDiff(path, prop, value) {
let localDiff = diff;
/**
* @type {object}
*/
let original = base;
let originalLost = false; // True, if the original object does not have this path
for (const key of path) {
if (key in original) {
original = original[key];
} else {
originalLost = true;
}
if (!(key in localDiff)) {
if (originalLost) {
// Should not happen
throw new TypeError("Neither original nor diff have this property: " + path);
}
if (_.isArray(original)) {
// clone arrays to make sure push, pop, etc. operations don't mess anything up later
// Deep copy in case array entries get modified later
localDiff[key] = _.cloneDeep(original);
} else if (_.isObject(original)) {
localDiff[key] = {};
}
}
localDiff = localDiff[key];
}
localDiff[prop] = value;
}
/**
* @template {object} T
* @param {T} target
* @param {path} path
* @returns {FC.Util.DiffRecorder<T>|T}
*/
function makeProxy(target, path) {
if (target == null) { // intentionally ==
return target;
}
if (_.isArray(target)) {
return new Proxy(target, {
get: function(o, prop) {
if (prop === "diffOriginal") {
return o;
} else if (prop === "diffChange") {
return localDiff(path);
}
const value = prop in localDiff(path) ? localDiff(path)[prop] : o[prop];
if (typeof value === "function") {
if (prop in localDiff(path)) {
return value.bind(localDiff(path));
}
if (["push", "pop", "shift", "unshift", "splice", "fill", "reverse"].includes(prop)) {
// Deep copy in case array entries get modified later
const diffArray = _.cloneDeep(o);
setDiff(path.slice(0, -1), path.last(), diffArray);
return value.bind(diffArray);
}
return value.bind(o);
}
if (value === deletedSymbol) {
return undefined;
}
return makeProxy(value, [...path, prop]);
},
set: function(o, prop, value) {
setDiff(path, prop, value);
return true;
},
deleteProperty: function(o, prop) {
setDiff(path, prop, deletedSymbol);
return true;
}
});
}
if (_.isObject(target)) {
return new Proxy(target, {
get: function(o, prop) {
if (prop === "diffOriginal") {
return o;
} else if (prop === "diffChange") {
return localDiff(path);
}
if (prop in localDiff(path)) {
if (localDiff(path)[prop] === deletedSymbol) {
return undefined;
}
return makeProxy(localDiff(path)[prop], [...path, prop]);
}
return makeProxy(o[prop], [...path, prop]);
},
set: function(o, prop, value) {
setDiff(path, prop, value);
return true;
},
deleteProperty: function(o, prop) {
setDiff(path, prop, deletedSymbol);
return true;
}
});
}
return target;
}
// base is an object, therefore makeProxy() returns FC.Util.DiffRecorder
// @ts-ignore
return makeProxy(base, []);
}
/**
* @template {object} T
* @param {T} base
* @param {Partial<T>} diff
*/
function applyDiff(base, diff) {
for (const key in diff) {
const value = diff[key];
// @ts-ignore
if (value === deletedSymbol) {
delete base[key];
} else if (!_.isObject(value)) {
base[key] = value;
} else if (_.isArray(value)) {
base[key] = value;
} else {
if (base[key] !== undefined) {
applyDiff(base[key], value);
} else {
base[key] = value;
}
}
}
}
return {
getProxy: getProxy,
applyDiff: applyDiff,
};
})();
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment