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

229 lines
7.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) => eventTarget.create(name, {
name: name,
value: value,
initial: !!initial,
});
const dispatchUpdate = (name, value) => eventTarget.dispatch(createUpdateEvent(name, value));
const broadcast = new BroadcastChannel(`${MAMI_MAIN_JS}:settings:${storage.name()}`);
const broadcastUpdate = (name, value) => {
setTimeout(() => broadcast.postMessage({ act: 'update', name: name, value: value }), 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 => {
if(setting.immutable)
return;
storage.delete(setting.name);
dispatchUpdate(setting.name, setting.fallback);
broadcastUpdate(setting.name, setting.fallback);
};
const setValue = (setting, value) => {
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);
broadcastUpdate(setting.name, value);
};
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);
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) => setValue(getSetting(name), value),
delete: name => deleteValue(getSetting(name)),
toggle: name => {
const setting = getSetting(name);
if(!setting.immutable)
setValue(setting, !getValue(setting));
},
touch: name => {
const setting = getSetting(name);
dispatchUpdate(setting.name, getValue(setting));
},
clear: (criticalOnly, prefix) => {
for(const setting of settings.values())
if((prefix === undefined || setting.name.startsWith(prefix)) && (!criticalOnly || setting.critical))
deleteValue(setting);
},
watch: (name, handler) => {
const setting = getSetting(name);
eventTarget.watch(setting.name, handler);
handler(createUpdateEvent(setting.name, getValue(setting), true));
},
unwatch: (name, handler) => {
eventTarget.unwatch(name, handler);
},
virtualise: name => storage.virtualise(getSetting(name).name),
scope: name => new MamiSettingsScoped(pub, name),
};
return pub;
};