mirror of
https://github.com/flynx/diff.js.git
synced 2025-10-29 02:50:10 +00:00
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:
parent
1fd00a2f72
commit
d1ef8cc95a
276
diff.js
276
diff.js
@ -6,6 +6,14 @@
|
|||||||
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
|
((typeof define)[0]=='u'?function(f){module.exports=f(require)}:define)
|
||||||
(function(require){ var module={} // make module AMD/node compatible...
|
(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...
|
// Helpers...
|
||||||
|
|
||||||
// zip(array, array, ...)
|
// zip(array, array, ...)
|
||||||
@ -212,6 +220,9 @@ var proxy = function(path, func){
|
|||||||
//
|
//
|
||||||
// A: <value>,
|
// A: <value>,
|
||||||
// B: <value>,
|
// B: <value>,
|
||||||
|
//
|
||||||
|
// // optional payload data...
|
||||||
|
// ...
|
||||||
// }
|
// }
|
||||||
//
|
//
|
||||||
// // A and B are arrays...
|
// // A and B are arrays...
|
||||||
@ -288,6 +299,9 @@ var proxy = function(path, func){
|
|||||||
// // or null then it means that the item
|
// // or null then it means that the item
|
||||||
// // does not exist in the corresponding
|
// // does not exist in the corresponding
|
||||||
// // array...
|
// // array...
|
||||||
|
// // NOTE: if both of the array items are arrays
|
||||||
|
// // it means that we are splicing array
|
||||||
|
// // sections instead of array elements...
|
||||||
// path: [<key>, ...],
|
// path: [<key>, ...],
|
||||||
//
|
//
|
||||||
// // values in A and B...
|
// // values in A and B...
|
||||||
@ -300,6 +314,10 @@ var proxy = function(path, func){
|
|||||||
// // EMPTY - the slot exists but it is empty (set/delete)
|
// // EMPTY - the slot exists but it is empty (set/delete)
|
||||||
// A: <value> | EMPTY | NONE,
|
// A: <value> | EMPTY | NONE,
|
||||||
// B: <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
|
// like the type information which is not needed for patching but
|
||||||
// may be useful for a more thorough compatibility check.
|
// may be useful for a more thorough compatibility check.
|
||||||
var Types = {
|
var Types = {
|
||||||
|
__cache: null,
|
||||||
|
|
||||||
// Object-level utilities...
|
// Object-level utilities...
|
||||||
clone: function(){
|
clone: function(){
|
||||||
var res = Object.create(this)
|
var res = Object.create(this)
|
||||||
|
res.__cache = null
|
||||||
res.handlers = new Map(this.handlers.entries())
|
res.handlers = new Map(this.handlers.entries())
|
||||||
return res
|
return res
|
||||||
},
|
},
|
||||||
@ -570,13 +591,13 @@ var Types = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// builtin 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)
|
return this.handle('Basic', {}, diff, A, B, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// cache...
|
// cache...
|
||||||
cache = cache || new Map()
|
cache = this.__cache = cache || this.__cache || new Map()
|
||||||
var diff = cache.diff = cache.diff || function(a, b){
|
var diff = cache.diff = cache.diff || function(a, b){
|
||||||
var l2 = cache.get(a) || new Map()
|
var l2 = cache.get(a) || new Map()
|
||||||
var d = l2.get(b) || that.diff(a, b, options, cache)
|
var d = l2.get(b) || that.diff(a, b, options, cache)
|
||||||
@ -610,15 +631,44 @@ var Types = {
|
|||||||
: res
|
: res
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// Deep-compare A and B...
|
||||||
|
//
|
||||||
|
cmp: function(A, B, options){
|
||||||
|
return this.diff(A, B, options) == null },
|
||||||
|
|
||||||
// Patch (update) obj via diff...
|
// Patch (update) obj via diff...
|
||||||
//
|
//
|
||||||
|
// XXX would need to let the type handlers handle themselves a-la .handle(..)
|
||||||
patch: function(diff, obj, options){
|
patch: function(diff, obj, options){
|
||||||
// XXX approach:
|
var NONE = diff.placeholders.NONE
|
||||||
// - check
|
var EMPTY = diff.placeholders.EMPTY
|
||||||
// - flatten
|
var options = diff.options
|
||||||
// - run patch
|
|
||||||
// - might be a good idea to enable handlers to handle
|
this.walk(diff.diff, function(change){
|
||||||
// their own updates...
|
// 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...
|
// Check if diff is applicable to obj...
|
||||||
@ -626,6 +676,7 @@ var Types = {
|
|||||||
check: function(diff, obj, options){
|
check: function(diff, obj, options){
|
||||||
// XXX
|
// XXX
|
||||||
},
|
},
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -715,6 +766,8 @@ var Types = {
|
|||||||
// NOTE: a basic type is one that returns a specific non-'object'
|
// NOTE: a basic type is one that returns a specific non-'object'
|
||||||
// typeof...
|
// typeof...
|
||||||
// i.e. when typeof(x) != 'object'
|
// i.e. when typeof(x) != 'object'
|
||||||
|
// NOTE: this does not need a .patch(..) method because it is not a
|
||||||
|
// container...
|
||||||
Types.set('Basic', {
|
Types.set('Basic', {
|
||||||
priority: 50,
|
priority: 50,
|
||||||
|
|
||||||
@ -727,13 +780,10 @@ Types.set('Basic', {
|
|||||||
|| (obj.B = B)
|
|| (obj.B = B)
|
||||||
},
|
},
|
||||||
walk: function(diff, func, path){
|
walk: function(diff, func, path){
|
||||||
var change = {
|
var change = Object.assign({
|
||||||
path: path,
|
path: path,
|
||||||
}
|
}, diff)
|
||||||
'A' in diff
|
delete change.type
|
||||||
&& (change.A = diff.A)
|
|
||||||
'B' in diff
|
|
||||||
&& (change.B = diff.B)
|
|
||||||
return func(change)
|
return func(change)
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
@ -765,6 +815,24 @@ Types.set(Object, {
|
|||||||
return that.walk(v, func, p)
|
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...
|
// part handlers...
|
||||||
attributes: function(diff, A, B, options, filter){
|
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...
|
// part handlers...
|
||||||
items: function(diff, A, B, options){
|
items: function(diff, A, B, options){
|
||||||
@ -877,6 +1001,24 @@ Types.set(Array, {
|
|||||||
var a = gap[0][1]
|
var a = gap[0][1]
|
||||||
var b = gap[1][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(
|
return zip(
|
||||||
function(n, elems){
|
function(n, elems){
|
||||||
return [
|
return [
|
||||||
@ -903,8 +1045,17 @@ Types.set(Array, {
|
|||||||
options),
|
options),
|
||||||
] },
|
] },
|
||||||
a, b)
|
a, b)
|
||||||
.filter(function(e){
|
// clear matching stuff...
|
||||||
|
.filter(function(e){
|
||||||
return e[2] != null})
|
return e[2] != null})
|
||||||
|
// splice array sub-sections...
|
||||||
|
.concat(ta.length + tb.length > 0 ?
|
||||||
|
[[
|
||||||
|
[i+l],
|
||||||
|
[j+l],
|
||||||
|
tail,
|
||||||
|
]]
|
||||||
|
: [])
|
||||||
})
|
})
|
||||||
.reduce(function(res, e){
|
.reduce(function(res, e){
|
||||||
return res.concat(e) }, [])
|
return res.concat(e) }, [])
|
||||||
@ -957,6 +1108,7 @@ Types.set('Text', {
|
|||||||
},
|
},
|
||||||
|
|
||||||
// XXX
|
// XXX
|
||||||
|
// XXX add object compatibility checks...
|
||||||
patch: function(change, obj){
|
patch: function(change, obj){
|
||||||
// XXX
|
// XXX
|
||||||
|
|
||||||
@ -977,7 +1129,7 @@ Types.set('Text', {
|
|||||||
var cmp =
|
var cmp =
|
||||||
module.cmp =
|
module.cmp =
|
||||||
function(A, B){
|
function(A, B){
|
||||||
return Types.diff(A, B) == null ? true : false }
|
return Types.clone().cmp(A, B) }
|
||||||
|
|
||||||
|
|
||||||
// Diff interface function...
|
// 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
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user