import { get, writable, type Writable } from 'svelte/store'; import type { LayerTreeType } from '$lib/data/layers'; /** What the settings were saved as, by key: JSON strings (see `Engine.openStorage`). */ export type StoredSettings = Record; /** Where the settings are kept. */ export type SettingsStorage = { /** Keeps the JSON of a setting. */ save(key: string, json: string): void; /** Forgets a setting. */ remove(key: string): void; }; let storage: SettingsStorage | undefined; /** Sets where the settings are kept: until then, changes are not saved. */ export function useSettingsStorage(newStorage: SettingsStorage | undefined) { storage = newStorage; } /** Reads the saved value of a setting, `undefined` if there is none or if it cannot be read. */ export function storedValue(stored: StoredSettings, key: string): { value: unknown } | undefined { if (!Object.hasOwn(stored, key)) { return undefined; } try { return { value: JSON.parse(stored[key]) }; } catch { return undefined; } } /** Keeps a value; `undefined` is not something JSON can hold, it means no value. */ function save(key: string, value: unknown) { const json = JSON.stringify(value); if (json === undefined) { storage?.remove(key); } else { storage?.save(key, json); } } /** * Makes a saved value one that the setting can hold: what was saved can be anything (an old * format, a value that is not allowed anymore), the validator checks it. */ export type Validator = (value: unknown) => V; /** * The validator of a setting: a boolean or a number can do without one, its saved value is then * checked against the type of its initial value. The other settings say what they can hold: a * string is often one of a few, an object has a shape. */ type ValidatorArgument = [V] extends [boolean] | [number] ? [validator?: Validator] : [validator: Validator]; /** Keeps a saved value that has the type of the initial one (a boolean or a number). */ function sameTypeAs(initial: V): Validator { return (value) => (typeof value === typeof initial ? (value as V) : initial); } /** * A setting: a store whose value is kept when it changes. `S` is what the store holds, which is * `V` or, for a setting that has no value until it is read, `V | undefined`. */ abstract class SettingBase { protected _key: string; protected _value: Writable; protected _validator: Validator; protected constructor(key: string, initial: S, validator: Validator) { this._key = key; this._value = writable(initial); this._validator = validator; } /** The saved value, validated, `undefined` if there is none. */ protected saved(stored: StoredSettings): { value: V } | undefined { const saved = storedValue(stored, this._key); return saved && { value: this._validator(saved.value) }; } /** Takes the saved value, if there is one. */ abstract connect(stored: StoredSettings): void; subscribe(run: (value: S) => void, invalidate?: (value?: S) => void) { return this._value.subscribe(run, invalidate); } /** Sets the value and keeps it. An object is always kept: it may have been changed in place. */ set(value: V) { if (typeof value === 'object' || value !== get(this._value)) { this._value.set(value as S); save(this._key, value); } } update(callback: (value: any) => any) { this.set(callback(get(this._value))); } } export class Setting extends SettingBase { constructor(key: string, initial: V, ...[validator]: ValidatorArgument) { super(key, initial, validator ?? sameTypeAs(initial)); } connect(stored: StoredSettings) { const saved = this.saved(stored); if (saved) { this._value.set(saved.value); } } } /** A setting that has no value until it is connected: then the saved one, or the initial one. */ export class SettingInitOnFirstRead extends SettingBase { private _initial: V; constructor(key: string, initial: V, ...[validator]: ValidatorArgument) { super(key, undefined, validator ?? sameTypeAs(initial)); this._initial = initial; } connect(stored: StoredSettings) { this._value.set(this.saved(stored)?.value ?? this._initial); } /** Takes the initial value and keeps it, for a setting that was never saved. */ initialize() { this.set(this._initial); } } /** Whether a value is one of the allowed ones. */ function oneOf(allowed: V[]) { const dict = new Set(allowed); return (value: unknown): value is V => dict.has(value); } export function getValueValidator(allowed: V[], fallback: V): Validator { const isAllowed = oneOf(allowed); return (value) => (isAllowed(value) ? value : fallback); } export function isString(value: unknown): value is string { return typeof value === 'string'; } export function isNumber(value: unknown): value is number { return typeof value === 'number'; } /** Keeps the items of a saved list that are what it holds, in their order; nothing if it is not a list. */ export function getListValidator(isItem: (value: unknown) => value is V): Validator { return (value) => (Array.isArray(value) ? value.filter(isItem) : []); } /** Keeps the allowed values of a list, in their order; nothing if what was saved is not a list. */ export function getArrayValidator(allowed: V[]): Validator { return getListValidator(oneOf(allowed)); } /** Keeps the entries of a saved record that are what it holds; `fallback` if it is not a record. */ export function getRecordValidator( isEntry: (value: unknown) => value is V, fallback: Record ): Validator> { return (value) => isRecord(value) ? Object.fromEntries( Object.entries(value).filter((entry): entry is [string, V] => isEntry(entry[1])) ) : fallback; } export function getLayerValidator( allowed: Record, fallback: string ): Validator { return (layer) => typeof layer === 'string' && (Object.hasOwn(allowed, layer) || layer.startsWith('custom-') || layer.startsWith('extension-')) ? layer : fallback; } /** Whether a value is a record, which can hold the layers of a tree: an object, not null or a list. */ function isRecord(value: unknown): value is Record { return typeof value === 'object' && value !== null && !Array.isArray(value); } /** * The saved tree `t`, with the layers of `allowed`: the choices saved for them are kept, the new * ones have their default, and of the others only the custom and extension layers are kept. */ export function filterLayerTree(t: unknown, allowed: LayerTreeType | undefined): LayerTreeType { const tree = isRecord(t) ? t : {}; const filtered: LayerTreeType = {}; if (allowed) { Object.entries(allowed).forEach(([key, value]) => { const saved = Object.hasOwn(tree, key) ? tree[key] : undefined; if (typeof value === 'boolean') { filtered[key] = typeof saved === 'boolean' ? saved : value; } else { filtered[key] = filterLayerTree(saved, value); } }); } Object.entries(tree).forEach(([key, value]) => { if (!Object.hasOwn(filtered, key)) { if (typeof value === 'boolean') { if (key.startsWith('custom-') || key.startsWith('extension-')) { filtered[key] = value; } } else if (isRecord(value)) { filtered[key] = filterLayerTree(value, undefined); } } }); return filtered; } export function getLayerTreeValidator(allowed: LayerTreeType): Validator { return (value) => filterLayerTree(value, allowed); }