mami/src/mami.js/settings/settings.js

251 lines
8.5 KiB
JavaScript

#include settings/scoped.js
#include settings/virtual.js
#include settings/webstorage.js
const MamiSettings = function(storageOrPrefix, eventTarget) {
if(typeof storageOrPrefix === 'string')
storageOrPrefix = new MamiSettingsWebStorage(window.localStorage, storageOrPrefix);
else if(typeof storageOrPrefix !== 'object')
throw 'storageOrPrefix must be a prefix string or an object';
if(typeof storageOrPrefix.get !== 'function'
|| typeof storageOrPrefix.set !== 'function'
|| typeof storageOrPrefix.delete !== 'function')
throw 'required methods do not exist in storageOrPrefix object';
const storage = new MamiSettingsVirtualStorage(storageOrPrefix);
const settings = new Map;
const createUpdateEvent = (name, value, initial, silent, local) => eventTarget.create(name, {
name: name,
value: value,
initial: !!initial,
silent: !!silent,
local: !!local,
});
const dispatchUpdate = (name, value, silent, local) => eventTarget.dispatch(createUpdateEvent(name, value, false, silent, local));
const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name()}`);
const broadcastUpdate = (name, value, silent) => {
setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value, silent: !!silent }), 0);
};
const getSetting = name => {
const setting = settings.get(name);
if(setting === undefined)
throw `setting ${name} is undefined`;
return setting;
};
const getValue = setting => {
if(setting.immutable)
return setting.fallback;
const value = storage.get(setting.name);
return value === null ? setting.fallback : value;
};
const deleteValue = (setting, silent) => {
if(setting.immutable)
return;
storage.delete(setting.name);
dispatchUpdate(setting.name, setting.fallback, silent, true);
broadcastUpdate(setting.name, setting.fallback, silent);
};
const setValue = (setting, value, silent) => {
if(value !== null) {
if(value === undefined)
value = null;
else if('type' in setting) {
if(Array.isArray(setting.type)) {
if(!setting.type.includes(value))
throw `setting ${setting.name} must match an enum value`;
} else {
const type = typeof value;
let resolved = false;
if(type !== setting.type) {
if(type === 'string') {
if(setting.type === 'number') {
value = parseFloat(value);
resolved = true;
if(setting.min !== undefined && value < setting.min)
value = setting.min;
else if(setting.max !== undefined && value > setting.max)
value = setting.max;
} else if(setting.type === 'boolean') {
value = !!value;
resolved = true;
}
} else if(setting.type === 'string') {
value = value.toString();
resolved = true;
}
} else resolved = true;
if(!resolved)
throw `setting ${setting.name} must be of type ${setting.type}`;
}
}
}
if(setting.immutable)
return;
if(value === null || value === setting.fallback) {
value = setting.fallback;
storage.delete(setting.name);
} else
storage.set(setting.name, value);
dispatchUpdate(setting.name, value, silent, true);
broadcastUpdate(setting.name, value, silent);
};
broadcast.onmessage = ev => {
if(typeof ev.data !== 'object' || typeof ev.data.act !== 'string')
return;
if(ev.data.act === 'update' && typeof ev.data.name === 'string') {
dispatchUpdate(ev.data.name, ev.data.value, ev.data.silent, false);
return;
}
};
const settingBlueprint = function(name) {
if(typeof name !== 'string')
throw 'setting name must be a string';
const checkDefined = () => {
if(settings.has(name))
throw `setting ${name} has already been defined`;
};
checkDefined();
let created = false;
let virtual = false;
const setting = {
name: name,
type: undefined,
fallback: null,
immutable: false,
critical: false,
min: undefined,
max: undefined,
};
const checkCreated = () => {
if(created)
throw 'setting has already been created';
};
const pub = {
type: value => {
if(typeof value !== 'string' && !Array.isArray(value))
throw 'type must be a javascript type or array of valid string values.';
checkCreated();
setting.type = value;
return pub;
},
default: value => {
checkCreated();
setting.fallback = value === undefined ? null : value;
if(setting.type === undefined)
setting.type = typeof setting.fallback;
return pub;
},
immutable: value => {
checkCreated();
setting.immutable = value === undefined || value === true;
return pub;
},
critical: value => {
checkCreated();
setting.critical = value === undefined || value === true;
return pub;
},
min: value => {
checkCreated();
if(typeof value !== 'number')
throw 'value must be a number';
setting.min = value;
return pub;
},
max: value => {
checkCreated();
if(typeof value !== 'number')
throw 'value must be a number';
setting.max = value;
return pub;
},
virtual: value => {
checkCreated();
virtual = value === undefined || value === true;
return pub;
},
create: () => {
checkCreated();
checkDefined();
settings.set(name, Object.freeze(setting));
if(virtual)
storage.virtualise(name);
},
};
return pub;
};
const pub = {
define: name => new settingBlueprint(name),
info: name => getSetting(name),
names: () => Array.from(settings.keys()),
has: name => {
const setting = settings.get(name);
return setting !== undefined
&& !setting.immutable
&& storage.get(setting.name) !== null;
},
get: name => getValue(getSetting(name)),
set: (name, value, silent) => setValue(getSetting(name), value, silent),
delete: (name, silent) => deleteValue(getSetting(name), silent),
toggle: (name, silent) => {
const setting = getSetting(name);
if(!setting.immutable)
setValue(setting, !getValue(setting), silent);
},
touch: (name, silent) => {
const setting = getSetting(name);
dispatchUpdate(setting.name, getValue(setting), silent, true);
},
clear: (criticalOnly, prefix) => {
for(const setting of settings.values())
if((prefix === undefined || setting.name.startsWith(prefix)) && (!criticalOnly || setting.critical))
deleteValue(setting);
},
watch: (name, handler, silent) => {
const setting = getSetting(name);
eventTarget.watch(setting.name, handler);
handler(createUpdateEvent(setting.name, getValue(setting), true, silent, true));
},
unwatch: (name, handler) => {
eventTarget.unwatch(name, handler);
},
virtualise: name => storage.virtualise(getSetting(name).name),
scope: name => new MamiSettingsScoped(pub, name),
};
return pub;
};