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,
+	};
+})();