Last active
December 28, 2015 09:49
-
-
Save warpech/7482194 to your computer and use it in GitHub Desktop.
JSON Patch duplex - ignore locally added properties
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// json-patch-duplex.js 0.3.6 with enhancement to ignore locally added properties | |
// (c) 2013 Joachim Wester | |
// MIT license | |
var jsonpatch; | |
(function (jsonpatch) { | |
var objOps = { | |
add: function (obj, key) { | |
obj[key] = this.value; | |
return true; | |
}, | |
remove: function (obj, key) { | |
delete obj[key]; | |
return true; | |
}, | |
replace: function (obj, key) { | |
obj[key] = this.value; | |
return true; | |
}, | |
move: function (obj, key, tree) { | |
var temp = { op: "_get", path: this.from }; | |
apply(tree, [temp]); | |
apply(tree, [ | |
{ op: "remove", path: this.from } | |
]); | |
apply(tree, [ | |
{ op: "add", path: this.path, value: temp.value } | |
]); | |
return true; | |
}, | |
copy: function (obj, key, tree) { | |
var temp = { op: "_get", path: this.from }; | |
apply(tree, [temp]); | |
apply(tree, [ | |
{ op: "add", path: this.path, value: temp.value } | |
]); | |
return true; | |
}, | |
test: function (obj, key) { | |
return (JSON.stringify(obj[key]) === JSON.stringify(this.value)); | |
}, | |
_get: function (obj, key) { | |
this.value = obj[key]; | |
} | |
}; | |
var arrOps = { | |
add: function (arr, i) { | |
arr.splice(i, 0, this.value); | |
return true; | |
}, | |
remove: function (arr, i) { | |
arr.splice(i, 1); | |
return true; | |
}, | |
replace: function (arr, i) { | |
arr[i] = this.value; | |
return true; | |
}, | |
move: objOps.move, | |
copy: objOps.copy, | |
test: objOps.test, | |
_get: objOps._get | |
}; | |
var observeOps = { | |
add: function (patches, path) { | |
var patch = { | |
op: "add", | |
path: path + escapePathComponent(this.name), | |
value: this.object[this.name] | |
}; | |
patches.push(patch); | |
}, | |
'delete': function (patches, path) { | |
var patch = { | |
op: "remove", | |
path: path + escapePathComponent(this.name) | |
}; | |
patches.push(patch); | |
}, | |
update: function (patches, path) { | |
var patch = { | |
op: "replace", | |
path: path + escapePathComponent(this.name), | |
value: this.object[this.name] | |
}; | |
patches.push(patch); | |
} | |
}; | |
function escapePathComponent(str) { | |
if (str.indexOf('/') === -1 && str.indexOf('~') === -1) | |
return str; | |
return str.replace(/~/g, '~0').replace(/\//g, '~1'); | |
} | |
function _getPathRecursive(root, obj) { | |
var found; | |
for (var key in root) { | |
if (root.hasOwnProperty(key)) { | |
if (root[key] === obj) { | |
return escapePathComponent(key) + '/'; | |
} else if (typeof root[key] === 'object') { | |
found = _getPathRecursive(root[key], obj); | |
if (found != '') { | |
return escapePathComponent(key) + '/' + found; | |
} | |
} | |
} | |
} | |
return ''; | |
} | |
function getPath(root, obj) { | |
if (root === obj) { | |
return '/'; | |
} | |
var path = _getPathRecursive(root, obj); | |
if (path === '') { | |
throw new Error("Object not found in root"); | |
} | |
return '/' + path; | |
} | |
var beforeDict = []; | |
jsonpatch.intervals; | |
var Mirror = (function () { | |
function Mirror(obj) { | |
this.observers = []; | |
this.obj = obj; | |
} | |
return Mirror; | |
})(); | |
var ObserverInfo = (function () { | |
function ObserverInfo(callback, observer) { | |
this.callback = callback; | |
this.observer = observer; | |
} | |
return ObserverInfo; | |
})(); | |
function getMirror(obj) { | |
for (var i = 0, ilen = beforeDict.length; i < ilen; i++) { | |
if (beforeDict[i].obj === obj) { | |
return beforeDict[i]; | |
} | |
} | |
} | |
function getObserverFromMirror(mirror, callback) { | |
for (var j = 0, jlen = mirror.observers.length; j < jlen; j++) { | |
if (mirror.observers[j].callback === callback) { | |
return mirror.observers[j].observer; | |
} | |
} | |
} | |
function removeObserverFromMirror(mirror, observer) { | |
for (var j = 0, jlen = mirror.observers.length; j < jlen; j++) { | |
if (mirror.observers[j].observer === observer) { | |
mirror.observers.splice(j, 1); | |
return; | |
} | |
} | |
} | |
function unobserve(root, observer) { | |
generate(observer); | |
if (Object.observe) { | |
_unobserve(observer, root); | |
} else { | |
clearTimeout(observer.next); | |
} | |
var mirror = getMirror(root); | |
removeObserverFromMirror(mirror, observer); | |
} | |
jsonpatch.unobserve = unobserve; | |
var privateProperties = new WeakMap(); | |
function observe(obj, callback) { | |
var patches = []; | |
var root = obj; | |
var observer; | |
var mirror = getMirror(obj); | |
if (!mirror) { | |
mirror = new Mirror(obj); | |
beforeDict.push(mirror); | |
} else { | |
observer = getObserverFromMirror(mirror, callback); | |
} | |
if (observer) { | |
return observer; | |
} | |
if (Object.observe) { | |
observer = function (arr) { | |
//This "refresh" is needed to begin observing new object properties | |
_unobserve(observer, obj); | |
_observe(observer, obj); | |
var a = 0, alen = arr.length; | |
while (a < alen) { | |
if (!(arr[a].name === 'length' && _isArray(arr[a].object)) && !(arr[a].name === '__Jasmine_been_here_before__')) { | |
var type = arr[a].type; | |
switch (type) { | |
case 'new': | |
type = 'add'; | |
break; | |
case 'deleted': | |
type = 'delete'; | |
break; | |
case 'updated': | |
type = 'update'; | |
break; | |
} | |
var privates = privateProperties.get(arr[a].object); | |
if (!privates) { | |
privates = {}; | |
privateProperties.set(arr[a].object, privates); | |
} | |
if (type === 'add') { | |
privates[arr[a].name] = true; | |
} | |
if (privates[arr[a].name]) { | |
return; | |
} | |
observeOps[type].call(arr[a], patches, getPath(root, arr[a].object)); | |
} | |
a++; | |
} | |
if (patches) { | |
if (callback) { | |
callback(patches); | |
} | |
} | |
observer.patches = patches; | |
patches = []; | |
}; | |
} else { | |
observer = {}; | |
mirror.value = JSON.parse(JSON.stringify(obj)); | |
if (callback) { | |
//callbacks.push(callback); this has no purpose | |
observer.callback = callback; | |
observer.next = null; | |
var intervals = this.intervals || [100, 1000, 10000, 60000]; | |
var currentInterval = 0; | |
var dirtyCheck = function () { | |
generate(observer); | |
}; | |
var fastCheck = function () { | |
clearTimeout(observer.next); | |
observer.next = setTimeout(function () { | |
dirtyCheck(); | |
currentInterval = 0; | |
observer.next = setTimeout(slowCheck, intervals[currentInterval++]); | |
}, 0); | |
}; | |
var slowCheck = function () { | |
dirtyCheck(); | |
if (currentInterval == intervals.length) | |
currentInterval = intervals.length - 1; | |
observer.next = setTimeout(slowCheck, intervals[currentInterval++]); | |
}; | |
if (typeof window !== 'undefined') { | |
if (window.addEventListener) { | |
window.addEventListener('mousedown', fastCheck); | |
window.addEventListener('mouseup', fastCheck); | |
window.addEventListener('keydown', fastCheck); | |
} else { | |
window.attachEvent('onmousedown', fastCheck); | |
window.attachEvent('onmouseup', fastCheck); | |
window.attachEvent('onkeydown', fastCheck); | |
} | |
} | |
observer.next = setTimeout(slowCheck, intervals[currentInterval++]); | |
} | |
} | |
observer.patches = patches; | |
observer.object = obj; | |
mirror.observers.push(new ObserverInfo(callback, observer)); | |
return _observe(observer, obj); | |
} | |
jsonpatch.observe = observe; | |
/// Listen to changes on an object tree, accumulate patches | |
function _observe(observer, obj) { | |
if (Object.observe) { | |
Object.observe(obj, observer); | |
for (var key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
var v = obj[key]; | |
if (v && typeof (v) === "object") { | |
_observe(observer, v); | |
} | |
} | |
} | |
} | |
return observer; | |
} | |
function _unobserve(observer, obj) { | |
if (Object.observe) { | |
Object.unobserve(obj, observer); | |
for (var key in obj) { | |
if (obj.hasOwnProperty(key)) { | |
var v = obj[key]; | |
if (v && typeof (v) === "object") { | |
_unobserve(observer, v); | |
} | |
} | |
} | |
} | |
return observer; | |
} | |
function generate(observer) { | |
if (Object.observe) { | |
Object.deliverChangeRecords(observer); | |
} else { | |
var mirror; | |
for (var i = 0, ilen = beforeDict.length; i < ilen; i++) { | |
if (beforeDict[i].obj === observer.object) { | |
mirror = beforeDict[i]; | |
break; | |
} | |
} | |
_generate(mirror.value, observer.object, observer.patches, ""); | |
} | |
var temp = observer.patches; | |
if (temp.length > 0) { | |
observer.patches = []; | |
if (observer.callback) { | |
observer.callback(temp); | |
} | |
} | |
return temp; | |
} | |
jsonpatch.generate = generate; | |
var _objectKeys; | |
if (Object.keys) { | |
_objectKeys = Object.keys; | |
} else { | |
_objectKeys = function (obj) { | |
var keys = []; | |
for (var o in obj) { | |
if (obj.hasOwnProperty(o)) { | |
keys.push(o); | |
} | |
} | |
return keys; | |
}; | |
} | |
// Dirty check if obj is different from mirror, generate patches and update mirror | |
function _generate(mirror, obj, patches, path) { | |
var newKeys = _objectKeys(obj); | |
var oldKeys = _objectKeys(mirror); | |
var changed = false; | |
var deleted = false; | |
for (var t = oldKeys.length - 1; t >= 0; t--) { | |
var key = oldKeys[t]; | |
var oldVal = mirror[key]; | |
if (obj.hasOwnProperty(key)) { | |
var newVal = obj[key]; | |
if (oldVal instanceof Object) { | |
_generate(oldVal, newVal, patches, path + "/" + escapePathComponent(key)); | |
} else { | |
if (oldVal != newVal) { | |
changed = true; | |
patches.push({ op: "replace", path: path + "/" + escapePathComponent(key), value: newVal }); | |
mirror[key] = newVal; | |
} | |
} | |
} else { | |
patches.push({ op: "remove", path: path + "/" + escapePathComponent(key) }); | |
delete mirror[key]; | |
deleted = true; | |
} | |
} | |
if (!deleted && newKeys.length == oldKeys.length) { | |
return; | |
} | |
for (var t = 0; t < newKeys.length; t++) { | |
var key = newKeys[t]; | |
if (!mirror.hasOwnProperty(key)) { | |
patches.push({ op: "add", path: path + "/" + escapePathComponent(key), value: obj[key] }); | |
mirror[key] = JSON.parse(JSON.stringify(obj[key])); | |
} | |
} | |
} | |
var _isArray; | |
if (Array.isArray) { | |
_isArray = Array.isArray; | |
} else { | |
_isArray = function (obj) { | |
return obj.push && typeof obj.length === 'number'; | |
}; | |
} | |
/// Apply a json-patch operation on an object tree | |
function apply(tree, patches) { | |
var result = false, p = 0, plen = patches.length, patch; | |
while (p < plen) { | |
patch = patches[p]; | |
// Find the object | |
var keys = patch.path.split('/'); | |
var obj = tree; | |
var t = 1; | |
var len = keys.length; | |
while (true) { | |
if (_isArray(obj)) { | |
var index = parseInt(keys[t], 10); | |
t++; | |
if (t >= len) { | |
result = arrOps[patch.op].call(patch, obj, index, tree); | |
break; | |
} | |
obj = obj[index]; | |
} else { | |
var key = keys[t]; | |
if (key.indexOf('~') != -1) | |
key = key.replace(/~1/g, '/').replace(/~0/g, '~'); | |
t++; | |
if (t >= len) { | |
result = objOps[patch.op].call(patch, obj, key, tree); | |
break; | |
} | |
obj = obj[key]; | |
} | |
} | |
p++; | |
} | |
return result; | |
} | |
jsonpatch.apply = apply; | |
})(jsonpatch || (jsonpatch = {})); | |
if (typeof exports !== "undefined") { | |
exports.apply = jsonpatch.apply; | |
exports.observe = jsonpatch.observe; | |
exports.unobserve = jsonpatch.unobserve; | |
exports.generate = jsonpatch.generate; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment