diff --git a/devNotes/tests/diffProxyTest.js b/devNotes/tests/diffProxyTest.js new file mode 100644 index 0000000000000000000000000000000000000000..39fa429ac39725a13974370ca00c00c9da7789f9 --- /dev/null +++ b/devNotes/tests/diffProxyTest.js @@ -0,0 +1,82 @@ +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); +} diff --git a/devTools/types/FC/util.d.ts b/devTools/types/FC/util.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..5dd8757958ccef4ca9823b5f3c38adaa4c9c415c --- /dev/null +++ b/devTools/types/FC/util.d.ts @@ -0,0 +1,16 @@ +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> + } +} diff --git a/js/diff.js b/js/diff.js new file mode 100644 index 0000000000000000000000000000000000000000..b5c00efc77c80ed15a9a3cdd2767b00f3141d2a6 --- /dev/null +++ b/js/diff.js @@ -0,0 +1,189 @@ +/** + * 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, + }; +})();