diff --git a/package.json b/package.json index 694df42..44dbf8e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "build": "webpack", "build-watch": "webpack --watch", "bench": "node test/spec/proxyBenchmark.js", + "bench-compare": "node test/spec/proxyBenchmark.js --compare", "test": "jasmine DUPLEX=proxy JASMINE_CONFIG_PATH=test/jasmine.json", "test-debug": "node --inspect-brk node_modules/jasmine/bin/jasmine.js DUPLEX=proxy JASMINE_CONFIG_PATH=test/jasmine.json", "version": "webpack && git add -A" diff --git a/src/jsonpatcherproxy.js b/src/jsonpatcherproxy.js index a3d9b5c..447c04a 100644 --- a/src/jsonpatcherproxy.js +++ b/src/jsonpatcherproxy.js @@ -12,6 +12,11 @@ /** Class representing a JS Object observer */ const JSONPatcherProxy = (function() { + function isObject(value) { + const type = typeof value + return value != null && (type == 'object' || type == 'function') + } + /** * Deep clones your object and returns a new object. */ @@ -34,17 +39,17 @@ const JSONPatcherProxy = (function() { JSONPatcherProxy.escapePathComponent = escapePathComponent; /** - * Walk up the parenthood tree to get the path + * Walk up the tree metadata to get the path * @param {JSONPatcherProxy} instance * @param {Object} tree the object you need to find its path */ function getPathToTree(instance, tree) { const pathComponents = []; - let parenthood = instance._parenthoodMap.get(tree); - while (parenthood && parenthood.key) { + let treeMetadata = instance._treeMetadataMap.get(tree); + while (treeMetadata && treeMetadata.keyInParent) { // because we're walking up-tree, we need to use the array as a stack - pathComponents.unshift(parenthood.key); - parenthood = instance._parenthoodMap.get(parenthood.parent); + pathComponents.unshift(treeMetadata.keyInParent); + treeMetadata = instance._treeMetadataMap.get(treeMetadata.parent); } if (pathComponents.length) { const path = pathComponents.join('/'); @@ -52,9 +57,23 @@ const JSONPatcherProxy = (function() { } return ''; } + /** + * A callback to be used as the proxy get trap callback. + * It allows to check get the tree metadata from within the tree. This allows to check if the object is proxified by this + * instance of JSONPatcherProxy, even when all you have is a reference that is someone else's proxy + * @param {JSONPatcherProxy} instance JSONPatcherProxy instance + * @param {Object} tree the affected object + * @param {String} key the effect property's name + */ + function trapForGet(instance, tree, key) { + if (key === instance._metadataSymbol) { + return instance._treeMetadataMap.get(tree); + } + return Reflect.get(tree, key); + } /** * A callback to be used as the proxy set trap callback. - * It updates parenthood map if needed, proxifies nested newly-added objects, calls default callback with the changes occurred. + * It updates tree metadata map if needed, proxifies nested newly-added objects, calls default callback with the changes occurred. * @param {JSONPatcherProxy} instance JSONPatcherProxy instance * @param {Object} tree the affected object * @param {String} key the effect property's name @@ -62,45 +81,51 @@ const JSONPatcherProxy = (function() { */ function trapForSet(instance, tree, key, newValue) { const pathToKey = getPathToTree(instance, tree) + '/' + escapePathComponent(key); - const subtreeMetadata = instance._treeMetadataMap.get(newValue); - - if (instance._treeMetadataMap.has(newValue)) { - instance._parenthoodMap.set(subtreeMetadata.originalObject, { parent: tree, key }); - } - /* - mark already proxified values as inherited. - rationale: proxy.arr.shift() - will emit - {op: replace, path: '/arr/1', value: arr_2} - {op: remove, path: '/arr/2'} - - by default, the second operation would revoke the proxy, and this renders arr revoked. - That's why we need to remember the proxies that are inherited. - */ - /* - Why do we need to check instance._isProxifyingTreeNow? - - We need to make sure we mark revocables as inherited ONLY when we're observing, - because throughout the first proxification, a sub-object is proxified and then assigned to - its parent object. This assignment of a pre-proxified object can fool us into thinking - that it's a proxified object moved around, while in fact it's the first assignment ever. - - Checking _isProxifyingTreeNow ensures this is not happening in the first proxification, - but in fact is is a proxified object moved around the tree - */ - if (subtreeMetadata && !instance._isProxifyingTreeNow) { - subtreeMetadata.inherited = true; + + if (isObject(newValue)) { + const subtreeMetadata = newValue[instance._metadataSymbol]; + if (subtreeMetadata) { + if(subtreeMetadata.parent === tree && subtreeMetadata.keyInParent === key) { + /* + This is an object that is already proxified by this instance of JSONPatcherProxy, + which is now being proxified by some external code or simply reassigned at the same place. + In this case, remain silent. + */ + return Reflect.set(tree, key, newValue); + } + subtreeMetadata.parent = tree; + subtreeMetadata.keyInParent = key; + /* + mark already proxified values as inherited. + rationale: proxy.arr.shift() + will emit + {op: replace, path: '/arr/1', value: arr_2} + {op: remove, path: '/arr/2'} + + by default, the second operation would revoke the proxy, and this renders arr revoked. + That's why we need to remember the proxies that are inherited. + */ + /* + Why do we need to check instance._isProxifyingTreeNow? + + We need to make sure we mark revocables as inherited ONLY when we're observing, + because throughout the first proxification, a sub-object is proxified and then assigned to + its parent object. This assignment of a pre-proxified object can fool us into thinking + that it's a proxified object moved around, while in fact it's the first assignment ever. + + Checking _isProxifyingTreeNow ensures this is not happening in the first proxification, + but in fact is is a proxified object moved around the tree + */ + if (!instance._isProxifyingTreeNow) { + subtreeMetadata.inherited = true; + } + } + else { + // make sure to watch it + newValue = instance._proxifyTreeRecursively(tree, newValue, key); + } } - // if the new value is an object, make sure to watch it - if ( - newValue && - typeof newValue == 'object' && - !instance._treeMetadataMap.has(newValue) - ) { - instance._parenthoodMap.set(newValue, { parent: tree, key }); - newValue = instance._proxifyTreeRecursively(tree, newValue, key); - } // let's start with this operation, and may or may not update it later const operation = { op: 'remove', @@ -118,12 +143,10 @@ const JSONPatcherProxy = (function() { // undefined array elements are JSON.stringified to `null` (operation.op = 'replace'), (operation.value = null); } - const oldSubtreeMetadata = instance._treeMetadataMap.get(tree[key]); + const oldSubtreeMetadata = tree[key][instance._metadataSymbol]; if (oldSubtreeMetadata) { - //TODO there is no test for this! - instance._parenthoodMap.delete(tree[key]); instance._disableTrapsForTreeMetadata(oldSubtreeMetadata); - instance._treeMetadataMap.delete(oldSubtreeMetadata); + instance._treeMetadataMap.delete(oldSubtreeMetadata.originalObject); //TODO there is no test for this } } } else { @@ -148,7 +171,7 @@ const JSONPatcherProxy = (function() { } /** * A callback to be used as the proxy delete trap callback. - * It updates parenthood map if needed, calls default callbacks with the changes occurred. + * It updates tree metadata map if needed, calls default callbacks with the changes occurred. * @param {JSONPatcherProxy} instance JSONPatcherProxy instance * @param {Object} tree the effected object * @param {String} key the effected property's name @@ -156,7 +179,7 @@ const JSONPatcherProxy = (function() { function trapForDeleteProperty(instance, tree, key) { if (typeof tree[key] !== 'undefined') { const pathToKey = getPathToTree(instance, tree) + '/' + escapePathComponent(key); - const subtreeMetadata = instance._treeMetadataMap.get(tree[key]); + const subtreeMetadata = tree[key][instance._metadataSymbol]; if (subtreeMetadata) { if (subtreeMetadata.inherited) { @@ -170,9 +193,8 @@ const JSONPatcherProxy = (function() { */ subtreeMetadata.inherited = false; } else { - instance._parenthoodMap.delete(subtreeMetadata.originalObject); instance._disableTrapsForTreeMetadata(subtreeMetadata); - instance._treeMetadataMap.delete(tree[key]); + instance._treeMetadataMap.delete(subtreeMetadata.originalObject); //TODO this is not tested } } const reflectionResult = Reflect.deleteProperty(tree, key); @@ -193,10 +215,13 @@ const JSONPatcherProxy = (function() { * @constructor */ function JSONPatcherProxy(root, showDetachedWarning) { + /** + * Use tree[this._metadataSymbol] to access the tree metadata when all you have is a reference to a proxified version of the tree. + */ + this._metadataSymbol = Symbol("Symbol for getting the tree metadata from external access."); this._isProxifyingTreeNow = false; this._isObserving = false; this._treeMetadataMap = new Map(); - this._parenthoodMap = new Map(); // default to true if (typeof showDetachedWarning !== 'boolean') { showDetachedWarning = true; @@ -216,26 +241,26 @@ const JSONPatcherProxy = (function() { return tree; } const handler = { + get: (...args) => trapForGet(this, ...args), set: (...args) => trapForSet(this, ...args), deleteProperty: (...args) => trapForDeleteProperty(this, ...args) }; const treeMetadata = Proxy.revocable(tree, handler); // cache the object that contains traps to disable them later. - treeMetadata.handler = handler; treeMetadata.originalObject = tree; - - /* keeping track of the object's parent and the key within the parent */ - this._parenthoodMap.set(tree, { parent, key }); + treeMetadata.handler = handler; + treeMetadata.parent = parent; + treeMetadata.keyInParent = key; /* keeping track of all the proxies to be able to revoke them later */ - this._treeMetadataMap.set(treeMetadata.proxy, treeMetadata); + this._treeMetadataMap.set(tree, treeMetadata); //the key is an UNPROXIFIED tree return treeMetadata.proxy; }; // grab tree's leaves one by one, encapsulate them into a proxy and return JSONPatcherProxy.prototype._proxifyTreeRecursively = function(parent, tree, key) { - for (let key in tree) { + for (let key in tree) { //TODO this creates a new local "key" that is different to "key" argument of the function. The name of the function argument should be changed to avoid confusion if (tree.hasOwnProperty(key)) { - if (tree[key] instanceof Object) { + if (isObject(tree[key])) { tree[key] = this._proxifyTreeRecursively( tree, tree[key], @@ -256,7 +281,7 @@ const JSONPatcherProxy = (function() { initial process; */ this.pause(); - this._isProxifyingTreeNow = true; + this._isProxifyingTreeNow = true; //TODO probably not needed, when I comment this out, all tests pass const proxifiedRoot = this._proxifyTreeRecursively( undefined, root, @@ -276,13 +301,12 @@ const JSONPatcherProxy = (function() { const message = "You're accessing an object that is detached from the observedObject tree, see https://github.com/Palindrom/JSONPatcherProxy#detached-objects"; - treeMetadata.handler.set = ( + treeMetadata.handler.get = ( parent, - key, - newValue + key ) => { console.warn(message); - return Reflect.set(parent, key, newValue); + return Reflect.get(parent, key); }; treeMetadata.handler.set = ( parent, @@ -348,6 +372,7 @@ const JSONPatcherProxy = (function() { JSONPatcherProxy.prototype.disableTraps = function() { this._treeMetadataMap.forEach(this._disableTrapsForTreeMetadata, this); }; + /** * Restores callback back to the original one provided to `observe`. */ diff --git a/test/spec/proxyBenchmark.js b/test/spec/proxyBenchmark.js index 361034b..486ee58 100644 --- a/test/spec/proxyBenchmark.js +++ b/test/spec/proxyBenchmark.js @@ -1,11 +1,23 @@ -var obj, obj2; +/* + To run with comparisons: + $ npm run bench-compare + + To run without comparisons: + $ npm run bench + */ + +let includeComparisons = true; if (typeof window === 'undefined') { const jsdom = require("jsdom"); const { JSDOM } = jsdom; - var dom = new JSDOM(); + const dom = new JSDOM(); global.window = dom.window; global.document = dom.window.document; + + if (!process.argv.includes("--compare")) { + includeComparisons = false; + } } if (typeof jsonpatch === 'undefined') { @@ -22,205 +34,211 @@ if (typeof Benchmark === 'undefined') { .benchmarkResultsToConsole; } -var suite = new Benchmark.Suite(); - -suite.add('jsonpatcherproxy generate operation', function() { - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; - - var jsonPatcherProxy = new JSONPatcherProxy(obj); - var observedObj = jsonPatcherProxy.observe(true); - - var patches = jsonPatcherProxy.generate(); - - observedObj.firstName = 'Joachim'; - observedObj.lastName = 'Wester'; - observedObj.phoneNumbers[0].number = '123'; - observedObj.phoneNumbers[1].number = '456'; -}); -suite.add('jsonpatch generate operation', function() { - var observedObj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; - var observer = jsonpatch.observe(observedObj); - - observedObj.firstName = 'Joachim'; - observedObj.lastName = 'Wester'; - observedObj.phoneNumbers[0].number = '123'; - observedObj.phoneNumbers[1].number = '456'; - - jsonpatch.generate(observer); -}); - - +const suite = new Benchmark.Suite(); +function generateSmallObjectFixture() { + return { name: 'Tesla', speed: 100 }; +} - -suite.add('jsonpatcherproxy mutation - huge object', function() { - var singleCar = { name: 'Tesla', speed: 100 }; - - var obj = { +function generateBigObjectFixture(carsSize) { + const obj = { firstName: 'Albert', lastName: 'Einstein', cars: [] }; - - for (var i = 0; i < 100; i++) { - var copy = JSONPatcherProxy.deepClone(singleCar); - var temp = copy; - for (var j = 0; j < 5; j++) { - temp.temp = JSONPatcherProxy.deepClone(singleCar); - temp = temp.temp; + for (let i = 0; i < carsSize; i++) { + let deep = generateSmallObjectFixture(); + obj.cars.push(deep); + for (let j = 0; j < 5; j++) { + deep.temp = generateSmallObjectFixture(); + deep = deep.temp; } - obj.cars.push(copy); } - var jsonPatcherProxy = new JSONPatcherProxy(obj); - var observedObj = jsonPatcherProxy.observe(true); + return obj; +} - observedObj.cars[50].name = 'Toyota' -}); +function modifyObj(obj) { + obj.firstName = 'Joachim'; + obj.lastName = 'Wester'; + obj.cars[0].speed = 123; + obj.cars[0].temp.speed = 456; +} -suite.add('jsonpatch mutation - huge object', function() { - var singleCar = { name: 'Tesla', speed: 100 }; +function reverseString(str) { + return str.split("").reverse().join(""); +} - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - cars: [] - }; +/* ============================= */ - for (var i = 0; i < 100; i++) { - var copy = JSONPatcherProxy.deepClone(singleCar); - var temp = copy; - for (var j = 0; j < 5; j++) { - temp.temp = JSONPatcherProxy.deepClone(singleCar); - temp = temp.temp; - } - obj.cars.push(copy); +{ + const suiteName = 'Observe and generate, small object'; + + if (includeComparisons) { + suite.add(`${suiteName} (noop)`, function() { + const obj = generateBigObjectFixture(1); + modifyObj(obj); + }); } - var observer = jsonpatch.observe(obj); - - obj.cars[50].name = 'Toyota'; - - jsonpatch.generate(observer); -}); + { + suite.add(`${suiteName} (JSONPatcherProxy)`, function() { + const obj = generateBigObjectFixture(1); + const jsonPatcherProxy = new JSONPatcherProxy(obj); + const observedObj = jsonPatcherProxy.observe(true); + modifyObj(observedObj); + jsonPatcherProxy.generate(); + }); + } + + if (includeComparisons) { + suite.add(`${suiteName} (fast-json-patch)`, function() { + const obj = generateBigObjectFixture(1); + const observer = jsonpatch.observe(obj); + modifyObj(obj); + jsonpatch.generate(observer); + }); + } +} -suite.add('jsonpatcherproxy generate operation - huge object', function() { - var singleCar = { name: 'Tesla', speed: 100 }; +/* ============================= */ - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - cars: [] - }; +{ + const suiteName = 'Observe and generate'; + + if (includeComparisons) { + suite.add(`${suiteName} (noop)`, function() { + const obj = generateBigObjectFixture(100); + modifyObj(obj); + }); + } - for (var i = 0; i < 100; i++) { - var copy = JSONPatcherProxy.deepClone(singleCar); - var temp = copy; - for (var j = 0; j < 5; j++) { - temp.temp = JSONPatcherProxy.deepClone(singleCar); - temp = temp.temp; - } - obj.cars.push(copy); + { + suite.add(`${suiteName} (JSONPatcherProxy)`, function() { + const obj = generateBigObjectFixture(100); + const jsonPatcherProxy = new JSONPatcherProxy(obj); + const observedObj = jsonPatcherProxy.observe(true); + modifyObj(observedObj); + jsonPatcherProxy.generate(); + }); + } + + if (includeComparisons) { + suite.add(`${suiteName} (fast-json-patch)`, function() { + const obj = generateBigObjectFixture(100); + const observer = jsonpatch.observe(obj); + modifyObj(obj); + jsonpatch.generate(observer); + }); } - var jsonPatcherProxy = new JSONPatcherProxy(obj); - var observedObj = jsonPatcherProxy.observe(true); +} - observedObj.cars.shift(); -}); +/* ============================= */ -suite.add('jsonpatch generate operation - huge object', function() { - var singleCar = { name: 'Tesla', speed: 100 }; +{ + const suiteName = 'Primitive mutation'; - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - cars: [] - }; + if (includeComparisons) { + const obj = generateBigObjectFixture(100); + + suite.add(`${suiteName} (noop)`, function() { + obj.cars[50].name = reverseString(obj.cars[50].name); + }); + } - for (var i = 0; i < 100; i++) { - var copy = JSONPatcherProxy.deepClone(singleCar); - var temp = copy; - for (var j = 0; j < 5; j++) { - temp.temp = JSONPatcherProxy.deepClone(singleCar); - temp = temp.temp; - } - obj.cars.push(copy); + { + const obj = generateBigObjectFixture(100); + const jsonPatcherProxy = new JSONPatcherProxy(obj); + const observedObj = jsonPatcherProxy.observe(true); + + suite.add(`${suiteName} (JSONPatcherProxy)`, function() { + observedObj.cars[50].name = reverseString(observedObj.cars[50].name); + jsonPatcherProxy.generate(); + }); } - var observer = jsonpatch.observe(obj); + if (includeComparisons) { + const obj = generateBigObjectFixture(100); + const observer = jsonpatch.observe(obj); + + suite.add(`${suiteName} (fast-json-patch)`, function() { + obj.cars[50].name = reverseString(obj.cars[50].name); + jsonpatch.generate(observer); + }); + } +} - obj.cars.shift(); +/* ============================= */ - jsonpatch.generate(observer); -}); +{ + const suiteName = 'Complex mutation'; -suite.add('PROXIFY big object', function() { - var singleCar = { name: 'Tesla', speed: 100 }; + if (includeComparisons) { + const obj = generateBigObjectFixture(100); + + suite.add(`${suiteName} (noop)`, function() { + const item = obj.cars.shift(); + obj.cars.push(item); + }); + } - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - cars: [] - }; - for (var i = 0; i < 50; i++) { - var copy = JSONPatcherProxy.deepClone(singleCar); - var temp = copy; - for (var j = 0; j < 5; j++) { - temp.temp = JSONPatcherProxy.deepClone(singleCar); - temp = temp.temp; - } - obj.cars.push(copy); + { + const obj = generateBigObjectFixture(100); + const jsonPatcherProxy = new JSONPatcherProxy(obj); + const observedObj = jsonPatcherProxy.observe(true); + + suite.add(`${suiteName} (JSONPatcherProxy)`, function() { + const item = observedObj.cars.shift(); + observedObj.cars.push(item); + jsonPatcherProxy.generate(); + }); } - var jsonPatcherProxy = new JSONPatcherProxy(obj); + if (includeComparisons) { + const obj = generateBigObjectFixture(100); + const observer = jsonpatch.observe(obj); + + suite.add(`${suiteName} (fast-json-patch)`, function() { + const item = obj.cars.shift(); + obj.cars.push(item); + jsonpatch.generate(observer); + }); + } +} - var observedObj = jsonPatcherProxy.observe(true); - observedObj.a = 1; -}); +/* ============================= */ -suite.add('DIRTY-OBSERVE big object', function() { - var singleCar = { name: 'Tesla', speed: 100 }; +{ + const suiteName = 'Serialization'; - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - cars: [] - }; - for (var i = 0; i < 50; i++) { - var copy = JSONPatcherProxy.deepClone(singleCar); - var temp = copy; - for (var j = 0; j < 5; j++) { - temp.temp = JSONPatcherProxy.deepClone(singleCar); - temp = temp.temp; - } - obj.cars.push(copy); + if (includeComparisons) { + const obj = generateBigObjectFixture(100); + + suite.add(`${suiteName} (noop)`, function() { + JSON.stringify(obj); + }); + } + + { + const obj = generateBigObjectFixture(100); + const jsonPatcherProxy = new JSONPatcherProxy(obj); + const observedObj = jsonPatcherProxy.observe(true); + + suite.add(`${suiteName} (JSONPatcherProxy)`, function() { + JSON.stringify(observedObj); + }); } - var observer = jsonpatch.observe(obj); - obj.a = 1; - jsonpatch.generate(observer); -}); + if (includeComparisons) { + const obj = generateBigObjectFixture(100); + const observer = jsonpatch.observe(obj); + + suite.add(`${suiteName} (fast-json-patch)`, function() { + JSON.stringify(obj); + }); + } +} + +/* ============================= */ // if we are in the browser with benchmark < 2.1.2 if (typeof benchmarkReporter !== 'undefined') { @@ -230,4 +248,4 @@ if (typeof benchmarkReporter !== 'undefined') { benchmarkResultsToConsole(suite); }); suite.run(); -} +} \ No newline at end of file diff --git a/test/spec/proxySpec.js b/test/spec/proxySpec.js index a6b877f..960ce5c 100644 --- a/test/spec/proxySpec.js +++ b/test/spec/proxySpec.js @@ -24,6 +24,21 @@ function getPatchesUsingCompare(objFactory, objChanger) { return jsonpatch.compare(mirror, JSON.parse(JSON.stringify(obj))); } +function generateDeepObjectFixture() { + return { + firstName: 'Albert', + lastName: 'Einstein', + phoneNumbers: [ + { + number: '12345' + }, + { + number: '45353' + } + ] + } +} + var customMatchers = { /** * This matcher is only needed in Chrome 28 (Chrome 28 cannot successfully compare observed objects immediately after they have been changed. Chrome 30 is unaffected) @@ -88,18 +103,7 @@ describe('proxy', function() { describe('generate', function() { it('should generate replace', function() { - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -111,18 +115,7 @@ describe('proxy', function() { var patches = jsonPatcherProxy.generate(); - var obj2 = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; + const obj2 = generateDeepObjectFixture(); jsonpatch.applyPatch(obj2, patches); /* iOS and Android */ @@ -131,7 +124,7 @@ describe('proxy', function() { expect(obj2).toReallyEqual(observedObj); }); it('should generate replace (escaped chars)', function() { - var obj = { + const obj = { '/name/first': 'Albert', '/name/last': 'Einstein', '~phone~/numbers': [ @@ -143,6 +136,7 @@ describe('proxy', function() { } ] }; + const obj2 = JSON.parse(JSON.stringify(obj)); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -152,19 +146,6 @@ describe('proxy', function() { observedObj['~phone~/numbers'][1].number = '456'; var patches = jsonPatcherProxy.generate(); - var obj2 = { - '/name/first': 'Albert', - '/name/last': 'Einstein', - '~phone~/numbers': [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; - jsonpatch.applyPatch(obj2, patches); /* iOS and Android */ @@ -212,19 +193,7 @@ describe('proxy', function() { }); it('should generate replace (double change, shallow object)', function() { - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; - + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -268,19 +237,7 @@ describe('proxy', function() { }); it('should generate replace (double change, deep object)', function() { - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; - + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -309,18 +266,55 @@ describe('proxy', function() { /* iOS and Android */ observedObj = JSONPatcherProxy.deepClone(observedObj); - expect(observedObj).toReallyEqual({ - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '123' - }, - { - number: '456' - } - ] - }); //objects should be still the same + const obj2 = generateDeepObjectFixture(); + obj2.phoneNumbers[0].number = '123'; + obj2.phoneNumbers[1].number = '456'; + expect(observedObj).toReallyEqual(obj2); //objects should be still the same + }); + + it('should generate replace (deep object, proxified)', function() { + const obj = generateDeepObjectFixture(); + const jsonPatcherProxy = new JSONPatcherProxy(obj); + let observedObj = jsonPatcherProxy.observe(true); + + //begin external proxification + observedObj = new Proxy(observedObj, {}); + observedObj.phoneNumbers = new Proxy(observedObj.phoneNumbers, {}); + observedObj.phoneNumbers[0] = new Proxy(observedObj.phoneNumbers[0], {}); + observedObj.phoneNumbers[1] = new Proxy(observedObj.phoneNumbers[1], {}); + //end external proxification + + observedObj.phoneNumbers[0].number = '123'; + + const patches = jsonPatcherProxy.generate(); + expect(patches).toReallyEqual([ + { + op: 'replace', + path: '/phoneNumbers/0/number', + value: '123' + } + ]); + }); + + it('should generate replace (deep object, proxified twice by JSONPatcherProxy)', function() { + const obj = generateDeepObjectFixture(); + const jsonPatcherProxy = new JSONPatcherProxy(obj); + let observedObj = jsonPatcherProxy.observe(true); + const jsonPatcherProxy2 = new JSONPatcherProxy(observedObj); + let observedObj2 = jsonPatcherProxy2.observe(true); + + observedObj2.phoneNumbers[0].number = '123'; + + const patches = jsonPatcherProxy.generate(); + const patches2 = jsonPatcherProxy2.generate(); + expect(patches).toReallyEqual([ + { + op: 'replace', + path: '/phoneNumbers/0/number', + value: '123' + } + ]); + expect(patches).toReallyEqual(patches2); }); it('should generate replace (changes in new array cell, primitive values)', function() { @@ -419,14 +413,7 @@ describe('proxy', function() { ]); }); it('should generate add', function() { - var obj = { - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -438,31 +425,12 @@ describe('proxy', function() { }); var patches = jsonPatcherProxy.generate(); - var obj2 = { - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - } - ] - }; - + var obj2 = generateDeepObjectFixture(); jsonpatch.applyPatch(obj2, patches); expect(obj2).toEqualInJson(observedObj); }); it('should generate remove', function() { - var obj = { - lastName: 'Einstein', - firstName: 'Albert', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '4234' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -473,34 +441,12 @@ describe('proxy', function() { var patches = jsonPatcherProxy.generate(); - var obj2 = { - lastName: 'Einstein', - firstName: 'Albert', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '4234' - } - ] - }; + const obj2 = generateDeepObjectFixture(); jsonpatch.applyPatch(obj2, patches); expect(obj2).toEqualInJson(observedObj); }); it('should generate remove and disable all traps', function() { - var obj = { - lastName: 'Einstein', - firstName: 'Albert', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '4234' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -937,18 +883,7 @@ describe('proxy', function() { describe('callback', function() { it('should generate replace', function() { var obj2; - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -959,18 +894,7 @@ describe('proxy', function() { observedObj.firstName = 'Joachim'; function objChanged(operation) { - obj2 = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; + obj2 = generateDeepObjectFixture(); jsonpatch.applyOperation(obj2, operation); /* iOS and Android */ @@ -984,18 +908,7 @@ describe('proxy', function() { var lastPatches, called = 0; - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true, function(patches) { @@ -1051,18 +964,7 @@ describe('proxy', function() { var lastPatches, called = 0; - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true); @@ -1093,18 +995,10 @@ describe('proxy', function() { } ]); //first patch should NOT be reported again here - expect(observedObj).toReallyEqual({ - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '123' - }, - { - number: '456' - } - ] - }); + const obj2 = generateDeepObjectFixture(); + obj2.phoneNumbers[0].number = '123'; + obj2.phoneNumbers[1].number = '456'; + expect(observedObj).toReallyEqual(obj2); }); describe( @@ -1200,18 +1094,7 @@ describe('proxy', function() { var lastPatches, called = 0, res; - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true, function(patches) { called++; @@ -1427,20 +1310,7 @@ describe('proxy', function() { describe('pausing and resuming', function() { it("shouldn't emit patches when paused", function() { var called = 0; - - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; - + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true, function(patches) { called++; @@ -1460,20 +1330,7 @@ describe('proxy', function() { it('Should re-start emitting patches when paused then resumed', function() { var called = 0; - - var obj = { - firstName: 'Albert', - lastName: 'Einstein', - phoneNumbers: [ - { - number: '12345' - }, - { - number: '45353' - } - ] - }; - + const obj = generateDeepObjectFixture(); var jsonPatcherProxy = new JSONPatcherProxy(obj); var observedObj = jsonPatcherProxy.observe(true, function(patches) { called++;