finalized the patch architecture, still need to revize the format + some refactoring...

Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
Alex A. Naanou 2018-07-23 00:17:35 +03:00
parent 1fd00a2f72
commit d1ef8cc95a

274
diff.js
View File

@ -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: <value>,
// B: <value>,
//
// // 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: [<key>, ...],
//
// // 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: <value> | EMPTY | NONE,
// B: <value> | 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)
// 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
}