2017-11-13 01:23:39 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
|
|
// ============================================================================
|
|
|
|
// Settings Contexts
|
|
|
|
// ============================================================================
|
|
|
|
|
|
|
|
import {EventEmitter} from 'utilities/events';
|
2019-06-20 15:15:54 -04:00
|
|
|
import {has, get as getter, array_equals, set_equals, map_equals} from 'utilities/object';
|
2017-11-13 01:23:39 -05:00
|
|
|
|
2018-03-22 22:39:27 -04:00
|
|
|
import * as DEFINITIONS from './types';
|
|
|
|
|
2019-04-30 15:18:29 -04:00
|
|
|
/**
|
|
|
|
* Perform a basic check of a setting's requirements to see if they changed.
|
|
|
|
* @param {Object} definition
|
|
|
|
* @param {Map} cache
|
|
|
|
* @param {Map} old_cache
|
|
|
|
* @returns Whether or not they changed.
|
|
|
|
*/
|
|
|
|
function compare_requirements(definition, cache, old_cache) {
|
|
|
|
if ( ! definition || ! Array.isArray(definition.requires) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
for(const req of definition.requires) {
|
|
|
|
const old_value = old_cache.get(req),
|
|
|
|
new_value = cache.get(req);
|
|
|
|
|
|
|
|
if ( typeof old_value !== typeof new_value )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
if ( Array.isArray(old_value) && Array.isArray(new_value) ) {
|
|
|
|
if ( ! array_equals(new_value, old_value) )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} else if ( new_value instanceof Set && old_value instanceof Set ) {
|
|
|
|
if ( ! set_equals(new_value, old_value) )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} else if( new_value instanceof Map && old_value instanceof Map ) {
|
|
|
|
if ( ! map_equals(new_value, old_value) )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
} else if ( new_value !== old_value )
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
/**
|
|
|
|
* The SettingsContext class provides a context through which to read
|
|
|
|
* settings values in addition to emitting events when settings values
|
|
|
|
* are changed.
|
|
|
|
* @extends EventEmitter
|
|
|
|
*/
|
|
|
|
export default class SettingsContext extends EventEmitter {
|
|
|
|
constructor(manager, context) {
|
|
|
|
super();
|
|
|
|
|
|
|
|
if ( manager instanceof SettingsContext ) {
|
|
|
|
this.parent = manager;
|
|
|
|
this.manager = manager.manager;
|
|
|
|
|
|
|
|
this.parent.on('context_changed', this._rebuildContext, this);
|
|
|
|
|
|
|
|
} else {
|
|
|
|
this.parent = null;
|
|
|
|
this.manager = manager;
|
|
|
|
}
|
|
|
|
|
|
|
|
this.manager.__contexts.push(this);
|
|
|
|
this._context = context || {};
|
|
|
|
|
2019-04-30 15:18:29 -04:00
|
|
|
/*this._context_objects = new Set;
|
|
|
|
if ( context )
|
|
|
|
this._updateContext(context, undefined, undefined, new Set);*/
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
this.__cache = new Map;
|
|
|
|
this.__meta = new Map;
|
|
|
|
this.__profiles = [];
|
|
|
|
this.order = [];
|
|
|
|
|
|
|
|
this._rebuildContext();
|
|
|
|
}
|
|
|
|
|
|
|
|
destroy() {
|
|
|
|
if ( this.parent )
|
|
|
|
this.parent.off('context_changed', this._rebuildContext, this);
|
|
|
|
|
|
|
|
for(const profile of this.__profiles)
|
|
|
|
profile.off('changed', this._onChanged, this);
|
|
|
|
|
|
|
|
const contexts = this.manager.__contexts,
|
|
|
|
idx = contexts.indexOf(this);
|
|
|
|
|
|
|
|
if ( idx !== -1 )
|
|
|
|
contexts.splice(idx, 1);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
// State Construction
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
_rebuildContext() {
|
|
|
|
this.__context = this.parent ?
|
2019-12-31 19:32:55 -05:00
|
|
|
Object.assign({}, this.parent.__context || this.parent._context, this._context) :
|
2017-11-13 01:23:39 -05:00
|
|
|
this._context;
|
|
|
|
|
|
|
|
// Make sure we re-build the cache. Dependency hell.
|
|
|
|
if ( ! this.selectProfiles() )
|
|
|
|
this.rebuildCache();
|
|
|
|
|
|
|
|
this.emit('context_changed');
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
selectProfiles() {
|
|
|
|
const new_profiles = [],
|
|
|
|
order = this.order = [];
|
2019-10-09 16:02:25 -04:00
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
for(const profile of this.manager.__profiles)
|
2019-10-09 16:02:25 -04:00
|
|
|
if ( profile.toggled && profile.matches(this.__context) ) {
|
2017-11-13 01:23:39 -05:00
|
|
|
new_profiles.push(profile);
|
|
|
|
order.push(profile.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( array_equals(this.__profiles, new_profiles) )
|
|
|
|
return false;
|
|
|
|
|
|
|
|
const changed_ids = new Set;
|
|
|
|
|
|
|
|
for(const profile of this.__profiles)
|
|
|
|
if ( ! new_profiles.includes(profile) ) {
|
|
|
|
profile.off('changed', this._onChanged, this);
|
|
|
|
changed_ids.add(profile.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
for(const profile of new_profiles)
|
|
|
|
if ( ! this.__profiles.includes(profile) ) {
|
|
|
|
profile.on('changed', this._onChanged, this);
|
|
|
|
changed_ids.add(profile.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.__profiles = new_profiles;
|
|
|
|
this.emit('profiles_changed');
|
|
|
|
this.rebuildCache(changed_ids);
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
rebuildCache() {
|
|
|
|
const old_cache = this.__cache,
|
|
|
|
old_meta = this.__meta,
|
|
|
|
meta = this.__meta = new Map;
|
|
|
|
|
|
|
|
this.__cache = new Map;
|
|
|
|
|
|
|
|
// TODO: Limit the values we recalculate to ones affected by the change
|
|
|
|
// that happened to the profiles. This is harder because of setting
|
|
|
|
// dependencies.
|
|
|
|
|
|
|
|
for(const [key, old_value] of old_cache) {
|
|
|
|
const new_value = this.get(key),
|
|
|
|
new_m = meta.get(key),
|
|
|
|
old_m = old_meta.get(key),
|
|
|
|
new_uses = new_m ? new_m.uses : null,
|
|
|
|
old_uses = old_m ? old_m.uses : null;
|
|
|
|
|
2019-04-30 15:18:29 -04:00
|
|
|
const definition = this.manager.definitions.get(key);
|
|
|
|
let changed = false;
|
|
|
|
|
|
|
|
if ( definition && definition.equals ) {
|
|
|
|
if ( definition.equals === 'requirements' )
|
|
|
|
changed = compare_requirements(definition, this.__cache, old_cache);
|
|
|
|
else if ( typeof definition.equals === 'function' )
|
|
|
|
changed = ! definition.equals(new_value, old_value, this.__cache, old_cache);
|
|
|
|
}
|
|
|
|
|
|
|
|
else if ( Array.isArray(new_value) && Array.isArray(old_value) )
|
|
|
|
changed = ! array_equals(new_value, old_value);
|
|
|
|
|
|
|
|
else if ( new_value instanceof Set && old_value instanceof Set )
|
|
|
|
changed = ! set_equals(new_value, old_value);
|
|
|
|
|
|
|
|
else if ( new_value instanceof Map && old_value instanceof Map )
|
|
|
|
changed = ! map_equals(new_value, old_value);
|
|
|
|
|
|
|
|
else if ( new_value !== old_value )
|
|
|
|
changed = true;
|
|
|
|
|
|
|
|
if ( changed ) {
|
2017-11-13 01:23:39 -05:00
|
|
|
this.emit('changed', key, new_value, old_value);
|
|
|
|
this.emit(`changed:${key}`, new_value, old_value);
|
|
|
|
}
|
|
|
|
|
2018-03-22 22:39:27 -04:00
|
|
|
if ( ! array_equals(new_uses, old_uses) ) {
|
2017-11-13 01:23:39 -05:00
|
|
|
this.emit('uses_changed', key, new_uses, old_uses);
|
|
|
|
this.emit(`uses_changed:${key}`, new_uses, old_uses);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
// Context Control
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
context(context) {
|
|
|
|
return new SettingsContext(this, context);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateContext(context) {
|
|
|
|
let changed = false;
|
|
|
|
|
|
|
|
for(const key in context)
|
|
|
|
if ( has(context, key) && context[key] !== this._context[key] ) {
|
|
|
|
this._context[key] = context[key];
|
|
|
|
changed = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( changed )
|
|
|
|
this._rebuildContext();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-04-30 15:18:29 -04:00
|
|
|
/*_updateContext(context, prefix, keys, changed) {
|
|
|
|
if ( ! context || typeof context !== 'object' )
|
|
|
|
return;
|
|
|
|
|
|
|
|
if ( ! keys )
|
|
|
|
keys = Object.keys(context);
|
|
|
|
|
|
|
|
if ( prefix && this._context_objects.has(prefix) ) {
|
|
|
|
for(const key of Object.keys(this._context) )
|
|
|
|
if ( key.startsWith(prefix) ) {
|
|
|
|
const partial_key = key.substr(prefix.length);
|
|
|
|
if ( ! keys.includes(partial_key) )
|
|
|
|
keys.push(partial_key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( prefix )
|
|
|
|
this._context_objects.add(prefix);
|
|
|
|
|
|
|
|
for(const key of keys) {
|
|
|
|
const full_key = prefix ? `${prefix}${key}` : key,
|
|
|
|
pref = `${full_key}.`,
|
|
|
|
old_value = this._context[full_key],
|
|
|
|
val = context[key];
|
|
|
|
|
|
|
|
const keys = val && (typeof val === 'object') && Object.keys(val);
|
|
|
|
if ( keys && keys.length ) {
|
|
|
|
this._updateContext(val, pref, keys, changed);
|
|
|
|
|
|
|
|
} else if ( this._context_objects.has(pref) ) {
|
|
|
|
this._updateContext({}, pref, [], changed);
|
|
|
|
this._context_objects.delete(pref);
|
|
|
|
|
|
|
|
if ( (val || old_value) && val !== old_value ) {
|
|
|
|
this._context[full_key] = val;
|
|
|
|
changed.add(full_key);
|
|
|
|
}
|
|
|
|
|
|
|
|
} else if ( val !== old_value ) {
|
|
|
|
this._context[full_key] = val;
|
|
|
|
changed.add(full_key);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
updateContext(context) {
|
|
|
|
if ( ! context || typeof context !== 'object' )
|
|
|
|
return;
|
|
|
|
|
|
|
|
const changed = new Set;
|
|
|
|
this._updateContext(context, undefined, undefined, changed);
|
|
|
|
|
|
|
|
if ( changed.size )
|
|
|
|
this._rebuildContext();
|
|
|
|
}*/
|
|
|
|
|
|
|
|
|
2017-11-13 01:23:39 -05:00
|
|
|
setContext(context) {
|
2019-04-30 15:18:29 -04:00
|
|
|
this._context_objects = new Set;
|
|
|
|
this._context = {};
|
|
|
|
this.updateContext(context);
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
// Data Access
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
_onChanged(key) {
|
|
|
|
this._update(key, key, []);
|
|
|
|
}
|
|
|
|
|
|
|
|
_update(key, initial, visited) {
|
|
|
|
if ( ! this.__cache.has(key) )
|
|
|
|
return;
|
|
|
|
|
|
|
|
else if ( visited.includes(key) )
|
|
|
|
throw new Error(`cyclic dependent chain when updating setting "${initial}"`);
|
|
|
|
|
|
|
|
visited.push(key);
|
|
|
|
|
|
|
|
const old_value = this.__cache.get(key),
|
|
|
|
old_meta = this.__meta.get(key),
|
|
|
|
new_value = this._get(key, key, []),
|
|
|
|
new_meta = this.__meta.get(key),
|
|
|
|
|
|
|
|
old_uses = old_meta ? old_meta.uses : null,
|
|
|
|
new_uses = new_meta ? new_meta.uses : null;
|
|
|
|
|
2018-03-22 22:39:27 -04:00
|
|
|
if ( ! array_equals(new_uses, old_uses) ) {
|
2017-11-13 01:23:39 -05:00
|
|
|
this.emit('uses_changed', key, new_uses, old_uses);
|
|
|
|
this.emit(`uses_changed:${key}`, new_uses, old_uses);
|
|
|
|
}
|
|
|
|
|
|
|
|
if ( old_value === new_value )
|
|
|
|
return;
|
|
|
|
|
|
|
|
this.emit('changed', key, new_value, old_value);
|
|
|
|
this.emit(`changed:${key}`, new_value, old_value);
|
|
|
|
|
|
|
|
const definition = this.manager.definitions.get(key);
|
|
|
|
if ( definition && definition.required_by )
|
|
|
|
for(const req_key of definition.required_by)
|
|
|
|
if ( ! req_key.startsWith('context.') )
|
|
|
|
this._update(req_key, initial, Array.from(visited));
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
_get(key, initial, visited) {
|
|
|
|
if ( visited.includes(key) )
|
|
|
|
throw new Error(`cyclic dependency when resolving setting "${initial}"`);
|
|
|
|
|
|
|
|
visited.push(key);
|
|
|
|
|
|
|
|
const definition = this.manager.definitions.get(key),
|
2018-03-22 22:39:27 -04:00
|
|
|
raw_type = definition && definition.type,
|
|
|
|
type = raw_type ? DEFINITIONS[raw_type] : DEFINITIONS.basic;
|
|
|
|
|
|
|
|
if ( ! type )
|
|
|
|
throw new Error(`non-existent setting type "${raw_type}"`);
|
|
|
|
|
|
|
|
const raw_value = this._getRaw(key, type),
|
2017-11-13 01:23:39 -05:00
|
|
|
meta = {
|
2018-03-22 22:39:27 -04:00
|
|
|
uses: raw_value ? raw_value[1] : null
|
2017-11-13 01:23:39 -05:00
|
|
|
};
|
|
|
|
|
|
|
|
let value = raw_value ? raw_value[0] : undefined;
|
|
|
|
|
|
|
|
if ( definition ) {
|
|
|
|
if ( Array.isArray(definition) )
|
|
|
|
throw new Error(`non-existent setting "${key}" required when resolving setting "${initial}"`);
|
|
|
|
|
|
|
|
if ( meta.uses === null ) {
|
|
|
|
const def_default = definition.default;
|
|
|
|
if ( typeof def_default === 'function' )
|
|
|
|
value = def_default(this);
|
|
|
|
else
|
|
|
|
value = def_default;
|
2018-03-30 19:54:26 -04:00
|
|
|
|
|
|
|
if ( type.default )
|
2018-06-27 14:13:59 -04:00
|
|
|
value = type.default(value, definition, this.manager.log);
|
2017-11-13 01:23:39 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( definition.requires )
|
|
|
|
for(const req_key of definition.requires)
|
|
|
|
if ( ! req_key.startsWith('context.') && ! this.__cache.has(req_key) )
|
|
|
|
this._get(req_key, initial, Array.from(visited));
|
|
|
|
|
|
|
|
if ( definition.process )
|
|
|
|
value = definition.process(this, value, meta);
|
|
|
|
}
|
|
|
|
|
|
|
|
this.__cache.set(key, value);
|
|
|
|
this.__meta.set(key, meta);
|
|
|
|
return value;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-03-22 22:39:27 -04:00
|
|
|
*profiles() {
|
2017-11-13 01:23:39 -05:00
|
|
|
for(const profile of this.__profiles)
|
2018-03-22 22:39:27 -04:00
|
|
|
yield profile;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2019-10-09 16:02:25 -04:00
|
|
|
hasProfile(profile) {
|
|
|
|
if ( typeof profile === 'number' )
|
|
|
|
for(const prof of this.__profiles)
|
|
|
|
if ( prof.id === profile )
|
|
|
|
return true;
|
|
|
|
|
|
|
|
return this.__profiles.includes(profile);
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2018-03-22 22:39:27 -04:00
|
|
|
_getRaw(key, type) {
|
|
|
|
if ( ! type )
|
2018-06-27 14:13:59 -04:00
|
|
|
throw new Error(`non-existent type for ${key}`)
|
2018-03-22 22:39:27 -04:00
|
|
|
|
2020-08-13 14:00:47 -04:00
|
|
|
return type.get(key, this.profiles(), this.manager.definitions.get(key), this.manager.log, this);
|
2018-03-22 22:39:27 -04:00
|
|
|
}
|
2018-04-02 03:30:22 -04:00
|
|
|
/* for(const profile of this.__profiles)
|
2017-11-13 01:23:39 -05:00
|
|
|
if ( profile.has(key) )
|
|
|
|
return [profile.get(key), profile]
|
2018-03-22 22:39:27 -04:00
|
|
|
}*/
|
2017-11-13 01:23:39 -05:00
|
|
|
|
|
|
|
|
|
|
|
// ========================================================================
|
|
|
|
// Data Access
|
|
|
|
// ========================================================================
|
|
|
|
|
|
|
|
update(key) {
|
|
|
|
this._update(key, key, []);
|
|
|
|
}
|
|
|
|
|
|
|
|
get(key) {
|
|
|
|
if ( key.startsWith('context.') )
|
2019-04-30 15:18:29 -04:00
|
|
|
//return this.__context[key.slice(8)];
|
2017-11-13 01:23:39 -05:00
|
|
|
return getter(key.slice(8), this.__context);
|
|
|
|
|
|
|
|
if ( this.__cache.has(key) )
|
|
|
|
return this.__cache.get(key);
|
|
|
|
|
|
|
|
return this._get(key, key, []);
|
|
|
|
}
|
|
|
|
|
|
|
|
uses(key) {
|
|
|
|
if ( key.startsWith('context.') )
|
|
|
|
return null;
|
|
|
|
|
|
|
|
if ( ! this.__meta.has(key) )
|
|
|
|
this._get(key, key, []);
|
|
|
|
|
|
|
|
return this.__meta.get(key).uses;
|
|
|
|
}
|
|
|
|
}
|