From d1ef8cc95a481a5097ad6c98e7b62324e9a1a54c Mon Sep 17 00:00:00 2001 From: "Alex A. Naanou" Date: Mon, 23 Jul 2018 00:17:35 +0300 Subject: [PATCH] finalized the patch architecture, still need to revize the format + some refactoring... Signed-off-by: Alex A. Naanou --- diff.js | 276 ++++++++++++++++++++++++++++++++++---------------------- 1 file changed, 168 insertions(+), 108 deletions(-) diff --git a/diff.js b/diff.js index 8a88992..f99fb50 100644 --- a/diff.js +++ b/diff.js @@ -6,6 +6,14 @@ ((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define) (function(require){ var module={} // make module AMD/node compatible... /*********************************************************************/ + +var ANY = {type: 'ANY_PLACEHOLDER'} +var NONE = {type: 'NONE_PLACEHOLDER'} +var EMPTY = {type: 'EMPTY_PLACEHOLDER'} + + + +//--------------------------------------------------------------------- // Helpers... // zip(array, array, ...) @@ -212,6 +220,9 @@ var proxy = function(path, func){ // // A: , // B: , +// +// // optional payload data... +// ... // } // // // A and B are arrays... @@ -288,6 +299,9 @@ var proxy = function(path, func){ // // or null then it means that the item // // does not exist in the corresponding // // array... +// // NOTE: if both of the array items are arrays +// // it means that we are splicing array +// // sections instead of array elements... // path: [, ...], // // // values in A and B... @@ -300,6 +314,10 @@ var proxy = function(path, func){ // // EMPTY - the slot exists but it is empty (set/delete) // A: | EMPTY | NONE, // B: | EMPTY | NONE, +// +// // used if we are splicing array sections to indicate section +// // lengths, useful when splicing sparse sections... +// length: [a, b], // }, // ... // ] @@ -311,9 +329,12 @@ var proxy = function(path, func){ // like the type information which is not needed for patching but // may be useful for a more thorough compatibility check. var Types = { + __cache: null, + // Object-level utilities... clone: function(){ var res = Object.create(this) + res.__cache = null res.handlers = new Map(this.handlers.entries()) return res }, @@ -570,13 +591,13 @@ var Types = { } // builtin types... - if(DIFF_TYPES.has(A) || DIFF_TYPES.has(B)){ + if(this.DIFF_TYPES.has(A) || this.DIFF_TYPES.has(B)){ return this.handle('Basic', {}, diff, A, B, options) } // cache... - cache = cache || new Map() + cache = this.__cache = cache || this.__cache || new Map() var diff = cache.diff = cache.diff || function(a, b){ var l2 = cache.get(a) || new Map() var d = l2.get(b) || that.diff(a, b, options, cache) @@ -610,15 +631,44 @@ var Types = { : res }, + // Deep-compare A and B... + // + cmp: function(A, B, options){ + return this.diff(A, B, options) == null }, + // Patch (update) obj via diff... // + // XXX would need to let the type handlers handle themselves a-la .handle(..) patch: function(diff, obj, options){ - // XXX approach: - // - check - // - flatten - // - run patch - // - might be a good idea to enable handlers to handle - // their own updates... + var NONE = diff.placeholders.NONE + var EMPTY = diff.placeholders.EMPTY + var options = diff.options + + this.walk(diff.diff, function(change){ + // replace the object itself... + if(change.path.length == 0){ + return change.B + } + + var type = change.type || Object + + var target = change.path + .slice(0, -1) + .reduce(function(res, e){ + return res[e]}, obj) + var key = change.path[change.path.length-1] + + // XXX this needs to be able to replace the target... + this.getHandler(type).patch.call(this, target, key, change, options) + }) + return obj + }, + + // Reverse diff... + // + // XXX should we do this or reverse patch / undo-patch??? + reverse: function(diff){ + // XXX }, // Check if diff is applicable to obj... @@ -626,6 +676,7 @@ var Types = { check: function(diff, obj, options){ // XXX }, + } @@ -715,6 +766,8 @@ var Types = { // NOTE: a basic type is one that returns a specific non-'object' // typeof... // i.e. when typeof(x) != 'object' +// NOTE: this does not need a .patch(..) method because it is not a +// container... Types.set('Basic', { priority: 50, @@ -727,13 +780,10 @@ Types.set('Basic', { || (obj.B = B) }, walk: function(diff, func, path){ - var change = { + var change = Object.assign({ path: path, - } - 'A' in diff - && (change.A = diff.A) - 'B' in diff - && (change.B = diff.B) + }, diff) + delete change.type return func(change) }, }) @@ -765,6 +815,24 @@ Types.set(Object, { return that.walk(v, func, p) }) }, + // XXX add object compatibility checks... + patch: function(obj, key, change){ + // object attr... + if(typeof(key) == typeof('str')){ + if(this.cmp(change.B, EMPTY)){ + delete obj[key] + + } else { + obj[key] = change.B + } + + // array item... + // XXX should this make this decision??? + } else { + return this.getHandler(Array).patch.call(this, obj, key, change) + } + return obj + }, // part handlers... attributes: function(diff, A, B, options, filter){ @@ -855,6 +923,62 @@ Types.set(Array, { }) : []) }, + // XXX add object compatibility checks... + patch: function(obj, key, change){ + var i = key instanceof Array ? key[0] : key + var j = key instanceof Array ? key[1] : key + + // sub-array manipulation... + if(i instanceof Array){ + i = i[0] + j = j[0] + + // XXX check compatibility... + + obj.splice(j, + 'A' in change ? + change.A.length + : change.length[0], + ...('B' in change ? + change.B + : new Array(change.length[1]))) + + // item manipulation... + } else { + if(i == null){ + // XXX this will mess up the indexing for the rest of + // item removals... + obj.splice(j, 0, change.B) + + } else if(j == null){ + // obj explicitly empty... + if('B' in change && this.cmp(change.B, EMPTY)){ + delete obj[i] + + // splice out obj... + } else if(!('B' in change) || this.cmp(change.B, NONE)){ + // NOTE: this does not affect the later elements + // indexing as it essentially shifts the + // indexes to their obj state for next + // changes... + obj.splice(i, 1) + + // XXX + } else { + // XXX + console.log('!!!!!!!!!!') + } + + } else if(i == j){ + obj[j] = change.B + + } else { + obj[j] = change.B + } + } + + return obj + }, // part handlers... items: function(diff, A, B, options){ @@ -877,6 +1001,24 @@ Types.set(Array, { var a = gap[0][1] var b = gap[1][1] + // split into two: a common-length section and tails of + // 0 and l lengths... + var l = Math.min(a.length, b.length) + var ta = a.slice(l) + var tb = b.slice(l) + // tail sections... + // XXX hack??? + // XXX should we use a different type/sub-type??? + var tail = { type: 'Basic', } + ta.filter(() => true).length > 0 + && (tail.A = ta) + tb.filter(() => true).length > 0 + && (tail.B = tb) + tail.length = [ta.length, tb.length] + + a = a.slice(0, l) + b = b.slice(0, l) + return zip( function(n, elems){ return [ @@ -903,8 +1045,17 @@ Types.set(Array, { options), ] }, a, b) - .filter(function(e){ + // clear matching stuff... + .filter(function(e){ return e[2] != null}) + // splice array sub-sections... + .concat(ta.length + tb.length > 0 ? + [[ + [i+l], + [j+l], + tail, + ]] + : []) }) .reduce(function(res, e){ return res.concat(e) }, []) @@ -957,6 +1108,7 @@ Types.set('Text', { }, // XXX + // XXX add object compatibility checks... patch: function(change, obj){ // XXX @@ -977,7 +1129,7 @@ Types.set('Text', { var cmp = module.cmp = function(A, B){ - return Types.diff(A, B) == null ? true : false } + return Types.clone().cmp(A, B) } // Diff interface function... @@ -1098,98 +1250,6 @@ function(diff, obj, options, types){ -// XXX would need to let the type handlers handle themselves a-la .handle(..) -// XXX Problems: -// _patch(diff(i = [1,2], [2,1]), i) -// -> [2,2] -var _patch = function(diff, obj){ - var NONE = diff.placeholders.NONE - var EMPTY = diff.placeholders.EMPTY - var options = diff.options - - // XXX also check what is overwritten... - // XXX need to correctly check EMPTY/NONE... - var checkTypeMatch = function(change, target, key){ - if('A' in change - && !(cmp(change.A, EMPTY) ? - !(key in target) - : cmp(target[key], change.A))){ - console.warn('Patch: Mismatching values at:', change.path, - 'expected:', change.A, - 'got:', target[key]) - return false - } - return true - } - - Types.walk(diff.diff, function(change){ - // replace the object itself... - if(change.path.length == 0){ - return change.B - } - - var type = change.type || 'item' - - var target = change.path - .slice(0, -1) - .reduce(function(res, e){ - return res[e]}, obj) - var key = change.path[change.path.length-1] - - if(type == 'item'){ - // object attr... - if(typeof(key) == typeof('str')){ - if(cmp(change.B, EMPTY)){ - delete target[key] - - } else { - checkTypeMatch(change, target, key) - - target[key] = change.B - } - - // array item... - } else { - var i = key instanceof Array ? key[0] : key - var j = key instanceof Array ? key[1] : key - - // XXX check A... - - if(i == null){ - target.splice(j, 0, change.B) - - } else if(j == null){ - // target explicitly empty... - if('B' in change && cmp(change.B, EMPTY)){ - delete target[i] - - // splice out target... - } else if(!('B' in change) || cmp(change.B, NONE)){ - target.splice(i, 1) - - // XXX - } else { - // XXX - console.log('!!!!!!!!!!') - } - - } else if(i == j){ - target[j] = change.B - - } else { - target[j] = change.B - } - } - - // custom types... - } else { - // XXX revise... - obj = this.getHandler(type).patch.call(this, change, obj) - } - - }) - return obj -}