mirror of
https://github.com/flynx/features.js.git
synced 2025-10-29 10:20:09 +00:00
more cleanup...
Signed-off-by: Alex A. Naanou <alex.nanou@gmail.com>
This commit is contained in:
parent
270bec2a52
commit
6a240ef261
484
features.js
484
features.js
@ -271,486 +271,6 @@ var FeatureSetProto = {
|
||||
&& that[e] instanceof Feature })
|
||||
},
|
||||
|
||||
// Build list of features...
|
||||
//
|
||||
// Build list of all features for an empty object...
|
||||
// .buildFeatureList()
|
||||
// .buildFeatureList({})
|
||||
// .buildFeatureList({}, '*')
|
||||
// -> data
|
||||
//
|
||||
// Build a list of features for a specific root feature and object...
|
||||
// .buildFeatureList(object, feature)
|
||||
// -> data
|
||||
//
|
||||
// Build a list of features for a specific set of root features and object...
|
||||
// .buildFeatureList(object, [feature, ..])
|
||||
// -> data
|
||||
// NOTE: to disable a feature and all of it's dependants prefix
|
||||
// it's tag with '-' in the list.
|
||||
// e.g. including 'some-feature' will include the feature
|
||||
// and its dependants while '-some-feature' will remove
|
||||
// it and it's dependants.
|
||||
//
|
||||
//
|
||||
// This will build from user input a loadable list of features taking
|
||||
// into account feature dependencies, priorities and suggestions.
|
||||
//
|
||||
// Roughly this is done in this order starting with the given features:
|
||||
// - include all dependencies (recursively)
|
||||
// - include all suggested features (recursively)
|
||||
// - sort features by priority
|
||||
// - sort features by dependency
|
||||
// - check for feature applicability
|
||||
// - remove non-applicable features and all dependants (recursively)
|
||||
// - remove disabled features and all dependants (recursively)
|
||||
// - check and resolve exclusivity conflicts (XXX needs revision)
|
||||
// - check for missing features and dependencies
|
||||
//
|
||||
//
|
||||
// Return format:
|
||||
// {
|
||||
// // list of input features...
|
||||
// input: [ .. ],
|
||||
//
|
||||
// // features in correct load order...
|
||||
// features: [ .. ],
|
||||
//
|
||||
// // features disabled explicitly and their dependants...
|
||||
// disabled: [ .. ],
|
||||
// // unapplicable features and their dependants...
|
||||
// unapplicable: [ .. ],
|
||||
//
|
||||
// // features removed due to exclusivity conflict...
|
||||
// excluded: [ .. ],
|
||||
//
|
||||
// missing: {
|
||||
// // features explicitly given by user but missing...
|
||||
// USER: [ .. ],
|
||||
// // missing <feature> dependencies...
|
||||
// <feature>: [ .. ],
|
||||
// ...
|
||||
// },
|
||||
// conflicts: {
|
||||
// XXX
|
||||
// },
|
||||
// }
|
||||
//
|
||||
//
|
||||
// NOTE: obj (action set) here is used only for applicability testing...
|
||||
// NOTE: some feature applicability checks (.isApplicable(..)) may
|
||||
// require a real action set, thus for correct operation one
|
||||
// should be provided.
|
||||
// NOTE: all feature sorting is done maintaining relative feature order
|
||||
// when possible...
|
||||
// NOTE: meta-features are not included in the list as they do not
|
||||
// need to be setup.
|
||||
// ...this is because they are not Feature objects.
|
||||
//
|
||||
// XXX should meta-features be MetaFeature objects???
|
||||
// XXX not sure about handling excluded features (see inside)...
|
||||
// XXX add dependency loops to .conflicts...
|
||||
// XXX might be a good idea to check dependency loops on feature
|
||||
// construction, too... (???)
|
||||
/*
|
||||
buildFeatureList: function(obj, lst){
|
||||
var that = this
|
||||
obj = obj || {}
|
||||
|
||||
lst = (lst == null || lst == '*') ? this.features : lst
|
||||
lst = lst.constructor !== Array ? [lst] : lst
|
||||
|
||||
var input = lst.slice()
|
||||
var disabled = []
|
||||
var excluded = []
|
||||
var unapplicable = []
|
||||
var missing = {}
|
||||
var conflicts = {}
|
||||
|
||||
var exclusive = {}
|
||||
|
||||
|
||||
// reverse dependency cache...
|
||||
var dependants = {}
|
||||
|
||||
// build dependency list...
|
||||
var _buildDepList = function(n, seen){
|
||||
seen = seen || []
|
||||
return seen.indexOf(n) >= 0 ? []
|
||||
: seen.push(n) && dependants[n] ? []
|
||||
.concat.apply(
|
||||
dependants[n],
|
||||
dependants[n]
|
||||
.map(function(n){ return _buildDepList(n, seen) }))
|
||||
: []
|
||||
}
|
||||
|
||||
|
||||
// missing stage 1: check if all user included features exist...
|
||||
// NOTE: we'll ignore missing disabled features too...
|
||||
lst.forEach(function(n){
|
||||
if(!that[n] && n[0] != '-'){
|
||||
var m = missing['USER'] = missing['USER'] || []
|
||||
m.push(n)
|
||||
}
|
||||
})
|
||||
|
||||
// include all dependencies...
|
||||
//
|
||||
// NOTE: this should never fall into an infinite loop as we do
|
||||
// not include feature already seen...
|
||||
// ...unless there is an infinite number of features, but
|
||||
// I'll try to avoid that big a feature creep.
|
||||
// XXX should we check for dependency loops here???
|
||||
// ...this would have been simple if this was a recursion
|
||||
// (just check if cur is in path), but here it is not
|
||||
// trivial...
|
||||
for(var i=0; i < lst.length; i++){
|
||||
var k = lst[i]
|
||||
|
||||
// skip disabled or missing features....
|
||||
if(k[0] == '-' || !that[k]){
|
||||
continue
|
||||
}
|
||||
|
||||
var deps = that[k].depends || []
|
||||
var refs = that[k].suggested || []
|
||||
var excl = that[k].exclusive || []
|
||||
|
||||
deps.forEach(function(n){
|
||||
// expand lst with dependencies....
|
||||
lst.indexOf(n) < 0 && lst.push(n)
|
||||
|
||||
// build reverse dependency index...
|
||||
var d = dependants[n] = dependants[n] || []
|
||||
d.indexOf(k) < 0 && d.push(k)
|
||||
})
|
||||
|
||||
// expand lst with suggenstions....
|
||||
refs.forEach(function(n){
|
||||
lst.indexOf(n) < 0 && lst.push(n)
|
||||
})
|
||||
|
||||
// build exclusive table...
|
||||
excl.forEach(function(n){
|
||||
var l = exclusive[n] = exclusive[n] || []
|
||||
l.indexOf(k) < 0 && l.push(k)
|
||||
})
|
||||
}
|
||||
|
||||
// sort features by priority or position...
|
||||
lst = lst
|
||||
// remove undefined and non-features...
|
||||
.filter(function(n){
|
||||
// feature disabled -> record and skip...
|
||||
if(n[0] == '-'){
|
||||
disabled.push(n.slice(1))
|
||||
return false
|
||||
}
|
||||
var f = that[n]
|
||||
// feature not defined or is not a feature...
|
||||
if(f == null || !(f instanceof Feature)){
|
||||
return false
|
||||
}
|
||||
// check applicability...
|
||||
if(f.isApplicable && !f.isApplicable.call(that, obj)){
|
||||
unapplicable.push(n)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
// remove disabled...
|
||||
.filter(function(e){ return disabled.indexOf(e) < 0 })
|
||||
// build the sort table: [ <priority>, <index>, <elem> ]
|
||||
.map(function(e, i){ return [ that[e].getPriority(), i, e ] })
|
||||
// sort by priority then index...
|
||||
// NOTE: JS compares lists as strings so we have to compare
|
||||
// the list manually...
|
||||
.sort(function(a, b){ return a[0] - b[0] || a[1] - b[1] })
|
||||
// cleanup -- drop the sort table...
|
||||
.map(function(e){ return e[2] })
|
||||
|
||||
// remove dependants on not applicable and on disabled...
|
||||
var _unapplicable = unapplicable.slice()
|
||||
var _disabled = disabled.slice()
|
||||
// build the full lists of features to remove...
|
||||
_unapplicable
|
||||
.forEach(function(n){ _unapplicable = _unapplicable.concat(_buildDepList(n)) })
|
||||
_disabled
|
||||
.forEach(function(n){ _disabled = _disabled.concat(_buildDepList(n)) })
|
||||
// clear...
|
||||
// NOTE: in case of intersection disabled has priority...
|
||||
lst = lst
|
||||
.filter(function(n){
|
||||
return _disabled.indexOf(n) >= 0 ?
|
||||
disabled.push(n) && false
|
||||
: _unapplicable.indexOf(n) >= 0 ?
|
||||
unapplicable.push(n) && false
|
||||
: true })
|
||||
|
||||
// missing stage 2: dependencies...
|
||||
lst.forEach(function(k){
|
||||
(that[k].depends || []).forEach(function(d){
|
||||
// NOTE: we do not need to check disabled or unapplicable
|
||||
// here as if the feature depended on dropped feature
|
||||
// it would have been already dropped too...
|
||||
// NOTE: we skip exclusive tags as they will get replaced
|
||||
// with actual feature tags later...
|
||||
// if a tag is exclusive then at least one feature
|
||||
// with it is present...
|
||||
if(!that[d] && !exclusive[d] && d[0] != '-'){
|
||||
var m = missing[k] = missing[k] || []
|
||||
m.push(d)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// check exclusive -> excluded...
|
||||
//
|
||||
// NOTE: this is the right spot for this, just after priority
|
||||
// sorting and clearing but before dependency sorting.
|
||||
//
|
||||
// XXX do we need to clear dependencies pulled by excluded features???
|
||||
// ways to go:
|
||||
// - drop excluded and continue (current state)
|
||||
// - disable excluded, add to original input and rebuild
|
||||
// - err and let the user decide
|
||||
var _exclusive = []
|
||||
lst = lst.filter(function(n){
|
||||
var e = that[n]
|
||||
|
||||
// keep non-exclusive stuff...
|
||||
if(!e || e.exclusive == null){
|
||||
return true
|
||||
}
|
||||
|
||||
// count the number of exclusive features already present...
|
||||
var res = e.exclusive
|
||||
.filter(function(n){
|
||||
if(_exclusive.indexOf(n) < 0){
|
||||
_exclusive.push(n)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
})
|
||||
.length == 0
|
||||
|
||||
!res
|
||||
&& excluded.push(n)
|
||||
// warn the user...
|
||||
// XXX not sure if this is the right place for this...
|
||||
&& console.warn(
|
||||
'Excluding unaplicable:', n, '(reccomended to exclude manually)')
|
||||
|
||||
return res
|
||||
})
|
||||
|
||||
// sort by dependency...
|
||||
var l = lst.length
|
||||
// get maximum possible length...
|
||||
// ...the worst case length appears to be (for full reversal):
|
||||
// S(2*(n-1) + 1)
|
||||
// S = n => n > 0 ? 2*(n-1)+1 + S(n-1) : 0
|
||||
// S = n => n > 0 ? 2*n-1 + S(n-1) : 0
|
||||
//
|
||||
// 2 * S(n) - n
|
||||
// S = n => n > 0 ? n + S(n-1) : 0
|
||||
// f = n => 2 * S(n) - n
|
||||
//
|
||||
// N^2 + C
|
||||
// S = n => n * n
|
||||
//
|
||||
// NOTE: this is the brute force way to check if we have a
|
||||
// dependency loop, need something faster...
|
||||
//
|
||||
// XXX is O(n^2) good enough worst case here?
|
||||
// ...at this point I think it is acceptable as we'll not
|
||||
// expect dependency graphs too saturated, and the average
|
||||
// complexity is far better...
|
||||
var max = l * l
|
||||
|
||||
var moved = []
|
||||
var chain = []
|
||||
var chains = []
|
||||
|
||||
for(var i=0; i < lst.length; i++){
|
||||
var k = lst[i]
|
||||
var depends = (that[k].depends || []).slice()
|
||||
|
||||
// replace dependencies that are exclusive tags...
|
||||
Object.keys(exclusive).forEach(function(e){
|
||||
var i = depends.indexOf(e)
|
||||
i >= 0
|
||||
&& exclusive[e].forEach(function(f){
|
||||
if(lst.indexOf(f) >= 0){
|
||||
//console.log('EXCL->DEP', e, f)
|
||||
depends[i] = f
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
// list of dependencies to move...
|
||||
var move = []
|
||||
|
||||
lst
|
||||
.slice(0, i)
|
||||
.forEach(function(n, j){
|
||||
// if n is a dependency of k, prepare to move...
|
||||
if(depends.indexOf(n) >= 0){
|
||||
delete lst[j]
|
||||
move.push(n)
|
||||
}
|
||||
})
|
||||
|
||||
// move the dependencies after k...
|
||||
// NOTE: this will keep the order within the dependencies...
|
||||
if(move.length > 0){
|
||||
lst.splice.apply(lst, [i+1, 0].concat(move))
|
||||
|
||||
// unseen feature -> new chain...
|
||||
if(moved.indexOf(k) < 0){
|
||||
chain = []
|
||||
}
|
||||
|
||||
moved.push(k)
|
||||
|
||||
// chain completed -> check and store...
|
||||
if(chain.indexOf(k) >= 0){
|
||||
var c = JSON.stringify(chain)
|
||||
// repeating chain -> loop or order conflict detected...
|
||||
if(chains.indexOf(c) >= 0){
|
||||
// format the graph...
|
||||
// XXX this catches strange things and not only loops...
|
||||
// ...at this point I can't repeat this error, see
|
||||
// ImageGrid.Viewer in nwjs load console output...
|
||||
var graph = []
|
||||
chain.forEach(function(k){
|
||||
graph = graph
|
||||
.concat((that[k].depends || [])
|
||||
.filter(function(e){
|
||||
return chain.indexOf(e) >= 0 })
|
||||
.map(function(e){
|
||||
return `${k} \t-> ${e}` })) })
|
||||
console.error('Feature cyclic dependency or order conflict:\n\t'
|
||||
+ graph.join('\n\t'))
|
||||
|
||||
// XXX should we give up completely (break) here
|
||||
// or move on and find new loops???
|
||||
break
|
||||
}
|
||||
chains.push(c)
|
||||
|
||||
// add item to chain...
|
||||
} else {
|
||||
chain.push(k)
|
||||
}
|
||||
}
|
||||
|
||||
// check for cyclic dependencies...
|
||||
// XXX loop signs:
|
||||
// - the tail length stops changing -- we stop progressing to list end
|
||||
// - the loop is packed
|
||||
// - each element includes a set of dependencies
|
||||
// - this set is of the same length when at a specific element
|
||||
// - we only shift the same set of N elements over N iterations
|
||||
// - ...
|
||||
if(lst.length >= max){
|
||||
// XXX get the actual cycle...
|
||||
console.error('Feature cyclic dependency:', chain)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// cleanup after sort...
|
||||
lst = lst
|
||||
// remove undefined and non-features...
|
||||
.filter(function(e){
|
||||
return that[e] != null && that[e] instanceof Feature })
|
||||
.reverse()
|
||||
|
||||
|
||||
return {
|
||||
input: input,
|
||||
|
||||
features: lst,
|
||||
|
||||
disabled: disabled,
|
||||
unapplicable: unapplicable,
|
||||
excluded: excluded,
|
||||
|
||||
missing: missing,
|
||||
conflicts: conflicts,
|
||||
}
|
||||
},
|
||||
setup: function(obj, lst){
|
||||
// if no explicit object is given, just the list...
|
||||
if(lst == null){
|
||||
lst = obj
|
||||
obj = null
|
||||
}
|
||||
|
||||
obj = obj || (this.__actions__ || actions.Actions)()
|
||||
|
||||
lst = lst.constructor !== Array ? [lst] : lst
|
||||
var features = this.buildFeatureList(obj, lst)
|
||||
lst = features.features
|
||||
|
||||
// check for conflicts...
|
||||
if(Object.keys(features.conflicts).length != 0
|
||||
|| Object.keys(features.missing).length != 0){
|
||||
var m = features.missing
|
||||
var c = features.conflicts
|
||||
|
||||
// build a report...
|
||||
var report = []
|
||||
|
||||
// missing deps...
|
||||
Object.keys(m).forEach(function(k){
|
||||
report.push(k + ': requires following missing features:\n'
|
||||
+' ' + m[k].join(', '))
|
||||
})
|
||||
report.push('\n')
|
||||
|
||||
// conflicts...
|
||||
Object.keys(c).forEach(function(k){
|
||||
report.push(k + ': must setup after:\n ' + c[k].join(', '))
|
||||
})
|
||||
|
||||
// break...
|
||||
throw 'Feature dependency error:\n ' + report.join('\n ')
|
||||
}
|
||||
|
||||
// report excluded features...
|
||||
if(this.__verbose__ && features.excluded.length > 0){
|
||||
console.warn('Excluded features due to exclusivity conflict:',
|
||||
features.excluded.join(', '))
|
||||
}
|
||||
|
||||
// report unapplicable features...
|
||||
if(this.__verbose__ && features.unapplicable.length > 0){
|
||||
console.log('Features not applicable in current context:',
|
||||
features.unapplicable.join(', '))
|
||||
}
|
||||
|
||||
// do the setup...
|
||||
var that = this
|
||||
var setup = FeatureProto.setup
|
||||
lst.forEach(function(n){
|
||||
// setup...
|
||||
if(that[n] != null){
|
||||
this.__verbose__ && console.log('Setting up feature:', n)
|
||||
setup.call(that[n], obj)
|
||||
}
|
||||
})
|
||||
|
||||
// XXX should we extend this if it already was in the object???
|
||||
obj.features = features
|
||||
|
||||
return obj
|
||||
},
|
||||
//*/
|
||||
|
||||
|
||||
// Build list of features in load order...
|
||||
//
|
||||
// .buildFeatureList()
|
||||
@ -1111,7 +631,9 @@ var FeatureSetProto = {
|
||||
var excluded = []
|
||||
Object.keys(conflicts)
|
||||
.forEach(function(group){
|
||||
// XXX is this how we decide which feature to keep???
|
||||
// XXX BUG: for some reason this does not behave
|
||||
// deterministically and in some cases the order
|
||||
// of the list is not stable...
|
||||
excluded = excluded.concat(conflicts[group].slice(1))})
|
||||
disabled = disabled.concat(excluded)
|
||||
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "ig-features",
|
||||
"version": "3.1.2",
|
||||
"version": "3.1.3",
|
||||
"description": "",
|
||||
"main": "features.js",
|
||||
"scripts": {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user