internal/OptionsModel.js

/**
 * Provides the data for the settings.
 *
 * @public
 * @module AutomaticSettings
 */

/**
 * This callback is called to retrieve the default value if it is not saved already.
 *
 * The callback is called with the option name in the same way
 * browser.storage.sync.get is called.
 * However, in contrast to browsers built-in functions, it will
 * never be called with an object or so, but only with a string.
 * This means only one option is retrieved at a time.
 *
 * @callback defaultOptionGetterCallback
 * @param {string} option
 * @return {Object}
 */
let defaultOptionGetter;

/**
 * Remembers options groups with each option to be able to aggreegate them, later.
 *
 * @private
 * @var {Object}
 */
let rememberedOptions;

/**
 * Resets all remembered options.
 *
 * It just cleans the whole {@link rememberedOptions}.
 *
 * @protected
 * @function
 * @returns {void}
 */
export function resetRememberedOptions() {
    rememberedOptions = {};
}

/**
 * Returns the whole option group based on the cached data.
 *
 * @protected
 * @param {string} optionGroupName
 * @function
 * @returns {void}
 */
export function getOptionGroup(optionGroupName) {
    let optionValue;

    // if options are cached/saved use them to prevent them from getting lost
    if (optionGroupName in rememberedOptions) {
        optionValue = rememberedOptions[optionGroupName];
    } else {
        // otherwise just init empty array
        optionValue = {};
    }

    return optionValue;
}

/**
 * Returns the sync option or falls back to the default option.
 *
 * @private
 * @param  {string} option string ob object ID
 * @param  {string|null} optionGroup optiom group, if it is used
 * @param  {Object|undefined} optionValues pre-loaded object values
 * @returns {Object|undefined}
 */
function getOptionGroupOrOption(option, optionGroup, optionValues) {
    let optionValue;

    // as value is present, get value from settings array
    if (optionGroup === null) {
        optionValue = optionValues[option];
    } else {
        const allOptionsInGroup = optionValues[optionGroup];
        optionValue = optionValues[optionGroup][option];

        // save options if needed
        if (optionGroup in rememberedOptions) {
            rememberedOptions[optionGroup] = allOptionsInGroup;
        }
    }

    return optionValue;
}

/**
 * Returns the option value if you give it a pre-fetched object of options.
 *
 * It basically handles the complexity behind option groups and automatically
 * fetches the default options, if needed.
 *
 * @protected
 * @param  {string} option string ob object ID
 * @param  {string|null} optionGroup optiom group, if it is used
 * @param  {Object|undefined} optionValues pre-loaded object values
 * @returns {Object|null}
 */
export function getOptionValueFromRequestResults(option, optionGroup, optionValues) {
    let optionValue;

    // need to start with true, to allow "concatenation"
    let shouldQueryDefault = true;

    if (optionGroup === null) {
        // do not get default, if it has already been passed directly
        shouldQueryDefault = shouldQueryDefault && !(option in optionValues);
    } else {
        shouldQueryDefault = shouldQueryDefault && (
            // do not get default, if it a group is loaded and the group has already been passed
            !(optionGroup in optionValues) ||
            // and the loaded values actually contains the value we want
            !(option in optionValues[optionGroup])
        );
    }

    // get default value if value is not passed
    if (shouldQueryDefault) {
        if (optionGroup === null) {
            optionValue = getDefaultOption(option);
        } else {
            optionValue = getDefaultOption(optionGroup)[option];
        }

        console.info("got default value for applying option", option, ":", optionValue);

        // if still no default value, try to use HTML defaults, i.e. do not set option
        if (optionValue === undefined) {
            return null;
        }
    } else {
        optionValue = getOptionGroupOrOption(option, optionGroup, optionValues);
    }

    return optionValue;
}

/**
 * Returns whether the default option provider is provided, so we can get some defaults.
 *
 * @private
 * @returns {boolean}
 */
function canGetDefaults() {
    return defaultOptionGetter !== null;
}

/**
 * Sets the callback for getting the default options.
 *
 * See {@link defaultOptionGetterCallback} for how the callback needs to
 * behave.
 * You need to call this function before the main init function of
 * AutomaticSettings. However, if you do not want to specify defaults
 * in JS, but just in HTML, you can pass "null" to this and it will not
 * try to request defaults.
 * Pass "undefined" to it to unset it.
 *
 * @public
 * @param {defaultOptionGetterCallback|null} defaultOptionCallback
 * @returns {void}
 */
export function setDefaultOptionProvider(defaultOptionCallback) {
    defaultOptionGetter = defaultOptionCallback;
}

/**
 * The actual function providing the default options.
 *
 * Returns "undefined" if default option provider is disabled.
 *
 * @package
 * @param {string} option
 * @returns {Object|undefined}
 */
export function getDefaultOption(option) {
    // just check if it is ready
    verifyItIsReady();

    if (!canGetDefaults()) {
        return undefined;
    }

    return defaultOptionGetter(option);
}

/**
 * Returns the sync option or falls back to the default option.
 *
 * @package
 * @param {string} option
 * @returns {Object|undefined}
 */
export async function getOption(option) {
    const optionValue = await browser.storage.sync.get(option);

    if (!(option in optionValue)) {
        return getDefaultOption(option);
    }

    return optionValue[option];
}

/**
 * Returns whether the module is ready yet.
 *
 * Usually throws if a bad error is found.
 *
 * @package
 * @returns {boolean}
 * @throws {Error}
 */
export function verifyItIsReady() {
    if (defaultOptionGetter === undefined) {
        throw new Error("Default option provider is not set. You need to call .setDefaultOptionProvider() before .init() to set it.");
    }

    return true;
}