Source: settings/store.js

import { DEFAULT_SETTINGS, SETTINGS_VERSION, diffFromDefaults, migrate, sanitizeSettings } from './schema.js';
import { resolveAdapter } from './storage.js';
import { noop } from '../io/general.js';

/**
 * Settings store: validated settings, persisted through an adapter.
 *
 *   const store = createSettingsStore({ persist: 'cookie' });
 *   store.get();                         // full settings object
 *   store.set({ units: 'metric' });      // validate, save, notify
 *   store.subscribe((settings) => …);    // returns an unsubscribe function
 *   store.reset();                       // back to defaults and clear storage
 *
 * @param {object} [options]
 * @param {boolean|string|object} [options.persist='auto']  see resolveAdapter()
 * @param {string} [options.key='sharpjs:settings']        storage key / cookie name
 * @param {object} [options.defaults]                      host defaults (under saved settings)
 * @param {object} [options.overrides]                     host values (over saved settings)
 */
export function createSettingsStore({ persist = 'auto', key = 'sharpjs:settings', defaults, overrides } = {}) {
  const adapter = resolveAdapter(persist, key);
  const base = sanitizeSettings(defaults, DEFAULT_SETTINGS);
  let settings = sanitizeSettings(overrides, base);
  const listeners = new Set();

  const notify = () => {
    for (const fn of listeners) {
      try {
        fn(settings);
      } catch (err) {
        console.error('SharpJS settings listener failed', err);
      }
    }
  };

  const persistNow = () => {
    try {
      const result = adapter.save({ v: SETTINGS_VERSION, settings: diffFromDefaults(settings) });
      if (result && typeof result.catch === 'function') result.catch((err) => console.warn('SharpJS: could not save settings', err));
    } catch (err) {
      console.warn('SharpJS: could not save settings', err);
    }
  };

  const applyLoaded = (payload) => {
    const saved = migrate(payload);
    settings = sanitizeSettings(overrides, sanitizeSettings(saved, base));
  };

  let ready;
  try {
    const loaded = adapter.load();
    if (loaded && typeof loaded.then === 'function') {
      ready = loaded.then(
        (payload) => {
          applyLoaded(payload);
          notify();
          return settings;
        },
        (err) => {
          console.warn('SharpJS: could not load settings', err);
          return settings;
        },
      );
    } else {
      applyLoaded(loaded);
      ready = Promise.resolve(settings);
    }
  } catch (err) {
    console.warn('SharpJS: could not load settings', err);
    ready = Promise.resolve(settings);
  }

  return {
    /** Resolves once an async adapter has loaded (immediately for browser storage). */
    ready,
    /** Which storage is in use: "localStorage", "cookie", "memory" or a custom adapter's kind. */
    get storage() {
      return adapter.kind || 'custom';
    },
    get() {
      return JSON.parse(JSON.stringify(settings));
    },
    /**
     * @param {object} partial
     * @param {{ save?: boolean, silent?: boolean }} [opts]
     * @returns {boolean} whether anything changed
     */
    set(partial, { save = true, silent = false } = {}) {
      const next = sanitizeSettings(partial, settings);
      if (JSON.stringify(next) === JSON.stringify(settings)) return false;
      settings = next;
      if (save) persistNow();
      if (!silent) notify();
      return true;
    },
    reset() {
      settings = sanitizeSettings(overrides, base);
      try {
        const result = adapter.clear ? adapter.clear() : adapter.save({ v: SETTINGS_VERSION, settings: {} });
        if (result && typeof result.catch === 'function') result.catch(noop);
      } catch {
        /* ignore */
      }
      notify();
      return this.get();
    },
    subscribe(fn) {
      listeners.add(fn);
      return () => listeners.delete(fn);
    },
  };
}