231 lines
7.8 KiB
JavaScript
231 lines
7.8 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;
|
|
} 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 type = undefined;
|
|
let fallback = null;
|
|
let immutable = false;
|
|
let critical = false;
|
|
let virtual = false;
|
|
|
|
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();
|
|
|
|
type = value;
|
|
return pub;
|
|
},
|
|
default: value => {
|
|
checkCreated();
|
|
fallback = value === undefined ? null : value;
|
|
|
|
if(type === undefined)
|
|
type = typeof fallback;
|
|
|
|
return pub;
|
|
},
|
|
immutable: value => {
|
|
checkCreated();
|
|
immutable = value === undefined || value === true;
|
|
return pub;
|
|
},
|
|
critical: value => {
|
|
checkCreated();
|
|
critical = value === undefined || value === true;
|
|
return pub;
|
|
},
|
|
virtual: value => {
|
|
checkCreated();
|
|
virtual = value === undefined || value === true;
|
|
return pub;
|
|
},
|
|
create: () => {
|
|
checkCreated();
|
|
checkDefined();
|
|
|
|
settings.set(name, Object.freeze({
|
|
name: name,
|
|
type: type,
|
|
fallback: fallback,
|
|
immutable: immutable,
|
|
critical: critical,
|
|
}));
|
|
|
|
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;
|
|
};
|