├── test ├── all.js ├── deref.js ├── 3-way.js ├── simple.js └── tight.js ├── package.json ├── LICENSE.APACHE2 ├── LICENSE.MIT ├── readme.markdown └── index.js /test/all.js: -------------------------------------------------------------------------------- 1 | var log = console.log 2 | log ('==============', 'SIMPLE', 'diff & patch must be consistent') 3 | require('./simple') 4 | log ('==============', 'TIGHT', 'diff & patch must be correct') 5 | require('./tight') 6 | log ('==============', '3-WAY', 'diff & patch must merge consistently') 7 | require('./3-way') 8 | log ('==============', 'DEREF', 'flatten and unflatten object to diff easily') 9 | require('./deref') 10 | log('TEST PASSED') 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Dominic Tarr (dominictarr.com)", 3 | "name": "xdiff", 4 | "description": "calc diffs of complex javascript objects.", 5 | "version": "0.2.11", 6 | "homepage": "http://github.com/dominictarr/xdiff", 7 | "repository": { 8 | "url": "git://github.com/dominictarr/xdiff.git" 9 | }, 10 | "engines": { 11 | "node": "*" 12 | }, 13 | "dependencies": { 14 | "adiff": "~0.2.4" 15 | }, 16 | "devDependencies": {}, 17 | "optionalDependencies": {} 18 | } 19 | -------------------------------------------------------------------------------- /LICENSE.APACHE2: -------------------------------------------------------------------------------- 1 | Apache License, Version 2.0 2 | 3 | Copyright (c) 2011 Dominic Tarr 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); 6 | you may not use this file except in compliance with the License. 7 | You may obtain a copy of the License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software 12 | distributed under the License is distributed on an "AS IS" BASIS, 13 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | See the License for the specific language governing permissions and 15 | limitations under the License. 16 | -------------------------------------------------------------------------------- /test/deref.js: -------------------------------------------------------------------------------- 1 | var x = require('../') 2 | var assert = require('assert') 3 | var log = console.log 4 | 5 | function assertDeref(a, e, mutate) { 6 | log('----------', mutate ? 'mutate' : 'non-mutate') 7 | log('before :', a) 8 | var deref = x.deref(a, mutate) 9 | log('deref :', deref) 10 | assert.deepEqual(deref, e) 11 | var reref = x.reref(deref, mutate) 12 | log('reref :', reref) 13 | assert.deepEqual(reref, a) 14 | 15 | if(!mutate) 16 | assertDeref(a, e, true) 17 | } 18 | 19 | function asRef (v) { 20 | return '#*=' + v 21 | } 22 | 23 | assertDeref( 24 | {a: true}, 25 | {root: {a:true}} 26 | ) 27 | var dvorak = {__id__: 'dvorak'} 28 | 29 | assertDeref( 30 | [dvorak], 31 | { 'dvorak': dvorak 32 | , root: [asRef('dvorak') ]} 33 | ) 34 | var querty = {__id__: 'querty'} 35 | assertDeref( 36 | {types: [dvorak, querty], __id__: 'layouts'}, 37 | { 'dvorak': dvorak 38 | , 'querty': querty 39 | , 'layouts': {types: [asRef('dvorak'), asRef('querty')], __id__: 'layouts'} 40 | , root: asRef('layouts')} 41 | ) 42 | 43 | //NOTE: REFERENCES ARE SUPPORTED, BUT NOT CIRCULAR REFERENCES. 44 | 45 | -------------------------------------------------------------------------------- /LICENSE.MIT: -------------------------------------------------------------------------------- 1 | The MIT License 2 | 3 | Copyright (c) 2011 Dominic Tarr 4 | 5 | Permission is hereby granted, free of charge, 6 | to any person obtaining a copy of this software and 7 | associated documentation files (the "Software"), to 8 | deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, 10 | merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom 12 | the Software is furnished to do so, 13 | subject to the following conditions: 14 | 15 | The above copyright notice and this permission notice 16 | shall be included in all copies or substantial portions of the Software. 17 | 18 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 21 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR 22 | ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 23 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 24 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 25 | -------------------------------------------------------------------------------- /test/3-way.js: -------------------------------------------------------------------------------- 1 | var x = require('..') 2 | var assert = require('assert') 3 | 4 | var log = console.log 5 | var str = JSON.stringify 6 | function assert3way(opts) { 7 | log('-------------') 8 | log('mine :', str(opts.mine)) 9 | log('old :', str(opts.old)) 10 | log('yours :', str(opts.yours)) 11 | var diff = x.diff3(opts.mine, opts.old, opts.yours) 12 | log('diff :', str(diff)) 13 | var merged = x.patch(opts.old, diff) 14 | log('merged :', str(opts.merged)) 15 | assert.deepEqual(merged, opts.merged) 16 | 17 | } 18 | 19 | //non conflicting 20 | //a simple merge 21 | 22 | assert3way({ 23 | mine : { a: 3 } 24 | , old : {} 25 | , yours : { b: 4 } 26 | , merged : { a: 3, b: 4 } 27 | }) 28 | 29 | 30 | assert3way({ 31 | mine : { a: 3, x: 2 } 32 | , old : { x: 4 } 33 | , yours : { b: 4, x: 2 } 34 | , merged : { a: 3, b: 4, x: 2 } 35 | }) 36 | 37 | //clean merge 38 | 39 | assert3way({ 40 | mine : [1, 2, 3, 4] 41 | , old : [1, 2, 3] 42 | , yours : [0, 1, 2, 3] 43 | , merged : [0, 1, 2, 3, 4] 44 | }) 45 | 46 | 47 | //clean merge 48 | 49 | assert3way({ 50 | mine : [0, 1, 2, 3] 51 | , old : [1, 2, 3] 52 | , yours : [1, 2, 3, 4] 53 | , merged : [0, 1, 2, 3, 4] 54 | }) 55 | 56 | // looks like a conflict, but not. 57 | 58 | assert3way({ 59 | mine : [1, 2, 4, 3] 60 | , old : [1, 2, 3] 61 | , yours : [1, 3] 62 | , merged : [1, 4, 3] 63 | }) 64 | 65 | assert3way({ 66 | mine : {a: {b: 1}} 67 | , old : {a: {}} 68 | , yours : {a: true} 69 | , merged : {a: {b: 1}} 70 | }) 71 | 72 | assert3way({ 73 | mine : {a: {c: 3}} 74 | , old : {a: {a: 2}} 75 | , yours : {a: {b: 1, a: 2}} 76 | , merged : {a: {c: 3, b: 1}} 77 | }) 78 | -------------------------------------------------------------------------------- /test/simple.js: -------------------------------------------------------------------------------- 1 | 2 | var a = require('assertions') 3 | var x = require('../') 4 | 5 | function assertDiffPatch(m, n) { 6 | 7 | var delta = x.diff(m, n) 8 | console.log('-------') 9 | console.log('A :', JSON.stringify(m)) 10 | console.log('B :', JSON.stringify(n)) 11 | console.log('delta :', JSON.stringify(delta)) 12 | var patched = x.patch(m, delta) 13 | console.log('patched :', JSON.stringify(patched)) 14 | a.deepEqual(patched, n) 15 | 16 | } 17 | 18 | assertDiffPatch({}, {a: 1}) 19 | assertDiffPatch({A: true}, {}) 20 | 21 | assertDiffPatch({ 22 | inner: {x: true, y: 1, z: 'DELETEME'} 23 | }, { 24 | inner: {x: false, y: 1} 25 | }) 26 | 27 | assertDiffPatch({ 28 | obj: {} 29 | }, { 30 | obj: [0] 31 | }) 32 | 33 | assertDiffPatch([1,2,3], [1, 4, 2, 3]) 34 | 35 | var b = { 36 | a: {__id__: '#0', value: 'whatever'}, 37 | b: {__id__: '#1', value: 'whenever'}, 38 | c: {__id__: '#2'} 39 | } 40 | b.c.parent = b.b 41 | 42 | assertDiffPatch({ 43 | a: {__id__: '#0'}, 44 | b: {__id__: '#1'}, 45 | c: {__id__: '#2'} 46 | }, b) 47 | 48 | 49 | assertDiffPatch([ 50 | {__id__: '#0'}, 51 | {__id__: '#1'}, 52 | {__id__: '#2'} 53 | ], [ 54 | {__id__: '#1'}, 55 | {__id__: '#2'}, 56 | {__id__: '#0'} 57 | ]) 58 | 59 | 60 | assertDiffPatch({ 61 | b:'x', 62 | a: {__id__: 'AoA'} 63 | },{ 64 | a: {__id__: 'aaa'}, 65 | b:'x' 66 | }) 67 | /* 68 | FOUND A BUG. 69 | should not ever change the __id__, 70 | so a 'set' transaction is not necessary. 71 | 72 | if it has set a ref, then it can just use a reference. 73 | need tight tests. 74 | 75 | ["UPDATE","hello","master",[{"parent":"66e6dd2fd229c1d53a4cbeb5e99b459ed1b38c35","changes":[["splice",["root","hi"],[[1,1]]]],"depth":5,"timestamp":1334883165695,"id":"6b5466a6d714b49f8ed702d1136da8f8e8927308"}]] 76 | 77 | ["UPDATE","hello","master",[{"parent":"6b5466a6d714b49f8ed702d1136da8f8e8927308","changes":[["set",["753"],{"__id__":"753"}],["set",["root","thing"],{"__id__":"753"}]],"depth":6,"timestamp":1334883206145,"id":"52d74bfe14fc2bb3ec9cf9fed8f5f006a54abc32"}]] 78 | */ 79 | -------------------------------------------------------------------------------- /test/tight.js: -------------------------------------------------------------------------------- 1 | //while simple.js just tests that the diff -> is consistant 2 | // this tests that they are correct. 3 | const 4 | SET = 'set' 5 | , SPL = 'splice' 6 | , DEL = 'del' 7 | , ROOT = 'root' 8 | 9 | function asRef(id) { 10 | return '#*='+id 11 | } 12 | 13 | var x = require('../') 14 | var assert = require('assert') 15 | var log = console.log 16 | var str = JSON.stringify 17 | var t = 0 18 | function cpy (o) { 19 | return JSON.parse(JSON.stringify(o)) 20 | } 21 | function assertDiff(a, b, d) { 22 | log('------------------') 23 | log('Before :', str(a)) 24 | log('After :', str(b)) 25 | var diff = x.diff(a, b) 26 | log('Delta :', str(diff)) 27 | assert.deepEqual(diff, d) 28 | log('ok', ++ t) 29 | if(!diff) 30 | assert.equal(diff, d) 31 | else { 32 | var patched = x.patch(cpy(a), diff) 33 | assert.deepEqual(patched, b) 34 | } 35 | } 36 | 37 | assertDiff( 38 | {}, 39 | {a: true}, 40 | [ [SET, [ROOT, 'a'], true] ] 41 | ) 42 | 43 | assertDiff( 44 | {a : true}, 45 | {a: [1, 2, 3]}, 46 | [ [SET, [ROOT, 'a'], [1, 2, 3] ] ] 47 | ) 48 | 49 | assertDiff( 50 | {a: [1, 2, 'INSERT', 3]}, 51 | {a: [1, 3]}, 52 | [ [SPL, [ROOT, 'a'], [[1, 2]] ] ] 53 | ) 54 | 55 | 56 | assertDiff( 57 | {a: [1, 2, 'INSERT', 3]}, 58 | {a: [1, 2, 'INSERT', 3], thing: {__id__: 123}}, 59 | [ [SET, ['123'], {__id__: 123}] 60 | , [SET, [ROOT, 'thing'], asRef(123) ] 61 | ] 62 | ) 63 | var thing = {__id__: '123'} 64 | var _thing = cpy(thing) 65 | assertDiff( 66 | {a: [1, 2, 'INSERT', 3], thing: thing}, 67 | {a: [1, 2, 'INSERT', 3], thing: thing, other: thing }, 68 | [ [SET, [ROOT, 'other'], asRef(123) ] 69 | ] 70 | ) 71 | 72 | //here we need to traverse the object, and update any references. 73 | // that means we will also have to traverse the object 74 | // when we apply the diff. 75 | // actually, we're already kinda doing this for the array. 76 | // oh, things might really simplify if I just traverse 77 | // the whole object, and replace the refs, then diff those. 78 | 79 | assertDiff( 80 | {hello: thing}, 81 | {hello: [thing]}, 82 | [ [SET, [ROOT, 'hello'], [asRef('123')]]] 83 | ) 84 | 85 | /* 86 | I had `a == b` at the end of equal() 87 | and equal(['X'], 'X') was evaluating to true 88 | and that was confusing adiff, which went into a infinite loop 89 | */ 90 | 91 | assertDiff( 92 | {hello: [thing, 0]}, 93 | {hello: [[ thing], 0]}, 94 | [ [SPL, [ROOT, 'hello'], [[0, 1, [asRef('123')]]]]] 95 | ) 96 | 97 | assertDiff( 98 | {hello: thing}, 99 | {hello: [null]}, 100 | [ [SET, [ROOT, 'hello'], [null]], [DEL, ['123'] ] ] 101 | ) 102 | 103 | assertDiff( 104 | {hello: [thing]}, 105 | {hello: [null]}, 106 | [ [SPL, [ROOT, 'hello'], [[0, 1 , null]]], [DEL, ['123'] ] ] 107 | ) 108 | 109 | assertDiff( 110 | {hello: thing, }, 111 | {hello: [null]}, 112 | [ [SET, [ROOT, 'hello'], [null]], [DEL, ['123'] ] ] 113 | ) 114 | 115 | /* 116 | null and undefined should be compared with == because JSON.parse(JSON.stringify(undefined)) === null 117 | 118 | */ 119 | 120 | assertDiff( 121 | [null], 122 | [undefined], 123 | undefined 124 | ) 125 | 126 | log('passed') 127 | 128 | 129 | -------------------------------------------------------------------------------- /readme.markdown: -------------------------------------------------------------------------------- 1 | # xdiff 2 | 3 | `diff`, `diff3`, `patch` (nearly) arbitary json documents. 4 | 5 | ## examples 6 | 7 | ``` js 8 | var x = require('xdiff') 9 | 10 | var a = SOMEOBJECT 11 | var b = SOMEOBJECT_AFTER_CHANGES 12 | 13 | var diff = x.diff(a, b) 14 | 15 | // can apply a diff to A to get B 16 | 17 | var patched = x.patch(a, diff) 18 | 19 | require('assert').deepEqual(a, patched) 20 | 21 | ``` 22 | 23 | ## diff, patch, diff3 24 | 25 | with `diff` you can create a diff that is applyable with `patch` 26 | you can diff nested objects, and arrays. 27 | 28 | with `diff3` you create a diff from two objects that have been edited concurrently, 29 | you need to also to pass the [concestor](http://en.wikipedia.org/wiki/Concestor). 30 | 31 | also see [adiff](https://github.com/dominictarr/adiff) which xdiff depends on to diff arrays. 32 | 33 | xdiff is compatible with [snob](https://github.com/dominictarr/snob) 34 | 35 | ### Objects 36 | 37 | ``` js 38 | var a = {a: true, c: 'deleteme'} 39 | var b = {a: true, b: false} 40 | var p = x.diff(a, b) 41 | ``` 42 | 43 | will create a diff like this: 44 | 45 | ``` js 46 | [ ['set', ['root', 'b'], false] 47 | , ['del', ['root', 'c']] ] 48 | ``` 49 | 50 | operations on nested objects are represented by thier path, 51 | unless the object has an ID. (see below) 52 | 53 | ``` js 54 | var a0 = {A: {AA: '?'}} 55 | var a1 = {A: {AA: 'aardvark'}} 56 | var p = x.diff(a, b) 57 | ``` 58 | 59 | will create diff like this: 60 | 61 | ``` js 62 | [['set', ['root', 'A', 'AA'], 'aardvark']] 63 | ``` 64 | 65 | ## Arrays 66 | 67 | ``` js 68 | var a = [1, 2 , 3] 69 | var b = [0, 1, 'hello', 3] 70 | var p = x.diff(a, b) 71 | ``` 72 | 73 | will create a diff like 74 | 75 | ``` js 76 | 77 | [ 'splice', ['root'], [ 78 | [ 1, 1, 'hello] //at index 1 delete one item and insert hello 79 | , [ 0, 0, 0] //at index 0 delete 0 items and insert `0` 80 | ] 81 | ``` 82 | 83 | ## Objects in Arrays 84 | 85 | if you give objects an ID, then xdiff will beable to track it properly, even if it's moved. 86 | even if it's concurrently changed. 87 | 88 | ``` js 89 | var a = [{__id__: '#1'}, 5, {__id__: '#2'}] 90 | var b = [5, {__id__: '#2'}, {__id__: '#1', prop: 'surprise'}] 91 | var p = x.diff(a, b) 92 | ``` 93 | 94 | will produce a diff like this 95 | 96 | ``` js 97 | [ ['set', ['#1', 'prop'], 'surprise'] //this applies the change to object #1 98 | , ['splice', ['root'], [ 99 | [ 3, 0, '#=*#1'] //this just updates the reference! 100 | , [ 0, 1] 101 | ] ] 102 | ] 103 | 104 | ``` 105 | if you don't don't use id's `xdiff` won't know that an object that has changed 106 | is actually the same object. this would cause it to reinsert a new copy of that object. 107 | 108 | id's are this is really useful when you need to do 3-way-merges to merge together concurrent changes. 109 | 110 | in a future version, xdiff will allow changing the id key. currently it uses only the `__id__` property. 111 | 112 | ## 3-way-merge: diff3 113 | 114 | three way merge takes 3 objects, `mine`, `yours` and an `old` object, which must be the [concestor](http://en.wikipedia.org/wiki/Concestor) of both mine and yours. 115 | 116 | if there are concurrent changes, xdiff will choose to use the change from `mine` 117 | 118 | in a future version, xdiff will support injectable resolve function, so that you can choose how to rosolve the merge. 119 | 120 | ## licence 121 | 122 | MIT / Apache2 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | //inject a matchRef, isRef, and a getRef function? 3 | //could use the same pattern with objects. 4 | 5 | //I don't really want to force __id__ 6 | //should be able to use anything, aslong as you 7 | 8 | 9 | function shallowEqual (a, b) { 10 | if(isObject(a) 11 | && isObject(b) 12 | && (a.__id__ == b.__id__ || a === b)) 13 | return true 14 | if(a && !b) return false 15 | return a == b 16 | } 17 | 18 | 19 | function equal (a, b) { 20 | if((a && !b) || (!a && b)) return false 21 | if(Array.isArray(a)) 22 | if(a.length != b.length) return false 23 | if(isObject(a) && isObject(b)) { 24 | if (a.__id__ == b.__id__ || a === b) 25 | return true 26 | for(var i in a) 27 | if(!equal(a[i], b[i])) return false 28 | return true 29 | } 30 | if(a == null && b == null) return true 31 | return a === b 32 | } 33 | 34 | var adiff = require('adiff')({ equal: equal }) 35 | 36 | function getPath (obj, path) { 37 | if(!Array.isArray(path)) 38 | return obj[path] 39 | for(var i in path) { 40 | obj = obj[path[i]] 41 | } 42 | return obj 43 | } 44 | 45 | function findRefs(obj, refs) { 46 | refs = refs || {} 47 | //add leaves before branches. 48 | //this will FAIL if there are circular references. 49 | 50 | if(!obj) 51 | return refs 52 | 53 | for(var k in obj) { 54 | if(obj[k] && 'object' == typeof obj[k]) 55 | findRefs(obj[k], refs) 56 | } 57 | 58 | if(obj.__id__ && !refs[obj.__id__]) 59 | refs[obj.__id__] = obj 60 | return refs 61 | } 62 | 63 | function toRef(v) { 64 | //TODO escape strings that happen to start with #*= 65 | var r 66 | if(r = isRef(v)) return '#*='+r 67 | return v 68 | } 69 | 70 | function isObject (o) { 71 | return o && 'object' == typeof o 72 | } 73 | 74 | function isRef(x) { 75 | return x ? x.__id__ : undefined 76 | } 77 | 78 | function sameRef(a, b) { 79 | return a && b && isRef(a) == isRef(b) 80 | } 81 | 82 | //traverse o, and replace every object with __id__ with a pointer. 83 | //make diffing references easy. 84 | 85 | 86 | exports.deref = function (o, mutate) { 87 | var refs = findRefs(o) 88 | var derefed = {} 89 | function deref (o, K) { 90 | if(isRef(o) && K != isRef(o)) 91 | return toRef(o) 92 | 93 | var p = mutate ? o : Array.isArray(o) ? [] : {} //will copy the tree! 94 | for (var k in o) { 95 | var r 96 | if(isRef(o[k])) p[k] = toRef(o[k]) 97 | else if(isObject(o[k])) p[k] = deref(o[k]) 98 | else p[k] = o[k] 99 | } 100 | return p 101 | } 102 | 103 | refs.root = o 104 | for (var k in refs) 105 | refs[k] = deref(refs[k], k) 106 | return refs 107 | } 108 | 109 | exports.reref = function (refs, mutate) { 110 | 111 | function fromRef(v) { 112 | //TODO escape strings that happen to start with #*= 113 | if('string' == typeof v && /^#\*=/.test(v)) return refs[v.substring(3)] 114 | return v 115 | } 116 | 117 | function reref (o) { //will MUTATE the tree 118 | if(!isObject(o)) 119 | return fromRef(o) 120 | 121 | var p = mutate ? o : Array.isArray(o) ? [] : {} //will copy the tree! 122 | for (var k in o) { 123 | if(isObject(o[k])) 124 | p[k] = reref(o[k]) 125 | else 126 | p[k] = fromRef(o[k]) 127 | } 128 | return p 129 | } 130 | //if the root is a ref. need a special case 131 | for (var k in refs) { 132 | refs[k] = reref(refs[k]) 133 | } 134 | return refs.root 135 | } 136 | 137 | exports.diff = function (a, b) { 138 | 139 | var aRefs = exports.deref(a) 140 | var bRefs = exports.deref(b) 141 | 142 | var seen = [] 143 | 144 | for (var k in aRefs) 145 | seen.push(k) 146 | 147 | function isSeen(o) { 148 | if(isRef(o)) return ~seen.indexOf(o.__id__) 149 | return true 150 | } 151 | function addSeen(o) { 152 | if(!isRef(o)) return o 153 | if(!isSeen(o)) seen.push(o.__id__) 154 | return o 155 | } 156 | 157 | // how to handle references? 158 | // this is necessary to handle objects in arrays nicely 159 | // otherwise mergeing an edit and a move is ambigous. // will need to perform a topoligical sort of the refs and diff them first, in that order. 160 | // first, apply changes to all refs, 161 | // then traverse over the root object, 162 | 163 | function _diff (a, b, path) { 164 | path = path || [] 165 | 166 | if(Array.isArray(a) && Array.isArray(b)) { 167 | var d = adiff.diff(a, b) 168 | if(d.length) delta.push(['splice', path, d]) 169 | return delta 170 | } 171 | 172 | // references to objects with ids are 173 | // changed into strings of thier id. 174 | // the string is prepended with '#*=' 175 | // to distinguish it from other strings 176 | // if you use that string in your model, 177 | // it will break. 178 | // TODO escape strings so this is safe 179 | 180 | //ah, treat root like it's a __id__ 181 | 182 | var isRoot = path.length === 1 && path[0] === 'root' 183 | 184 | for (var k in b) { 185 | // if both are nonRef objects, or are the same object, branch into them. 186 | 187 | if(isObject(a[k]) && isObject(b[k]) && sameRef(b[k], a[k])) 188 | _diff(a[k], b[k], path.concat(k)) 189 | else if(b[k] !== a[k]) 190 | delta.push(['set', path.concat(k), cpy(b[k])]) 191 | } 192 | 193 | for (var k in a) 194 | if('undefined' == typeof b[k]) 195 | delta.push(['del', path.concat(k)]) 196 | } 197 | 198 | var delta = [] 199 | _diff(aRefs, bRefs, []) 200 | 201 | if(delta.length) 202 | return cpy(delta) 203 | } 204 | 205 | exports.patch = function (a, patch) { 206 | 207 | if(!patch) throw new Error('expected patch') 208 | 209 | var refs = exports.deref(a, true) 210 | refs.root = a 211 | 212 | var methods = { 213 | set: function (key, value) { 214 | this[key] = cpy(value) // incase this was a reference, remove it. 215 | }, 216 | del: function (key) { 217 | delete this[key] 218 | }, 219 | splice: function (changes) { 220 | adiff.patch(this, changes, true) 221 | } 222 | } 223 | 224 | function pathTo(a, p) { 225 | for (var i in p) a = a[p[i]] 226 | return a 227 | } 228 | 229 | patch.forEach(function (args) { 230 | args = args.slice() 231 | var method = args.shift() 232 | var path = args.shift().slice() 233 | var key 234 | if(method != 'splice') { 235 | key = path.pop() 236 | args.unshift(key) 237 | } 238 | var obj = pathTo(refs, path) 239 | methods[method].apply(obj, args) 240 | }) 241 | 242 | return exports.reref(refs, true) 243 | } 244 | 245 | function cpy(o) { 246 | if(!o) return o 247 | return JSON.parse(JSON.stringify(o)) 248 | } 249 | 250 | exports.diff3 = function (a, o, b) { 251 | if(arguments.length == 1) 252 | o = a[1], b = a[2], a = a[0] 253 | var _a = exports.diff(o, a) || [] // if there where no changes, still merge 254 | , _b = exports.diff(o, b) || [] 255 | 256 | function cmp (a, b) { 257 | //check if a[1] > b[1] 258 | if(!b) 259 | return 1 260 | 261 | var p = a[1], q = b[1] 262 | var i = 0 263 | while (p[i] === q[i] && p[i] != null) 264 | i++ 265 | 266 | if(p[i] === q[i]) return 0 267 | return p[i] < q[i] ? -1 : 1 268 | } 269 | 270 | function isPrefix(a, b) { 271 | if(!b) return 1 272 | var p = a[1], q = b[1] 273 | var i = 0 274 | while (p[i] === q[i] && i < p.length && i < q.length) 275 | i++ 276 | if(i == p.length || i == q.length) return 0 277 | return p[i] < q[i] ? -1 : 1 278 | } 279 | 280 | //merge two lists, which must be sorted. 281 | 282 | function cmpSp (a, b) { 283 | if(a[0] == b[0]) 284 | return 0 285 | function max(k) { 286 | return k[0] + (k[1] >= 1 ? k[1] - 1 : 0) 287 | } 288 | if(max(a) < b[0] || max(b) < a[0]) 289 | return a[0] - b[0] 290 | return 0 291 | } 292 | 293 | function resolveAry(a, b) { 294 | return a 295 | } 296 | 297 | function resolve(a, b) { 298 | if(a[1].length == b[1].length) { 299 | if(a[0] == b[0]) { 300 | if(a[0] == 'splice') { 301 | var R = merge(a[2], b[2], cmpSp, resolveAry) 302 | return ['splice', a[1].slice(), R] 303 | } else if(equal(a[2], b[2])) //same change both sides. 304 | return a 305 | } 306 | } 307 | return a 308 | } 309 | 310 | function merge(a, b, cmp, resolve) { 311 | var i = a.length - 1, j = b.length - 1, r = [] 312 | while(~i && ~j) { 313 | var c = cmp(a[i], b[j]) 314 | if(c > 0) r.push(a[i--]) 315 | if(c < 0) r.push(b[j--]) 316 | if(!c) { 317 | var R = resolve(a[i], b[j]) 318 | j--, i-- 319 | r.push(R) 320 | } 321 | } 322 | //finish off the list if there are any left over 323 | while(~i) r.push(a[i--]) 324 | while(~j) r.push(b[j--]) 325 | return r 326 | } 327 | 328 | _a.sort(cmp) 329 | _b.sort(cmp) 330 | 331 | var m = merge(_a, _b, isPrefix, resolve) 332 | return m.length ? m : null 333 | } 334 | --------------------------------------------------------------------------------