#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; };