import defaultOptions, { type Options } from "../defaultOptions"; export type { Options }; import logger from "./logger"; import { TypedEventTarget } from "./TypedEventTarget"; import { TypedStorageArea } from "./TypedStorageArea"; const storageArea = new TypedStorageArea<{ options: Options; }>(browser.storage.sync); interface EventMap { changed: Array; } export default new (class extends TypedEventTarget { constructor() { super(); this.onStorageChanged = this.onStorageChanged.bind(this); browser.storage.onChanged.addListener(this.onStorageChanged); // Supresses sendRemoveListener closed conduit error window.addEventListener("unload", () => { browser.storage.onChanged.removeListener(this.onStorageChanged); }); } private onStorageChanged( changes: { [key: string]: browser.storage.StorageChange }, areaName: string ) { if (areaName !== "sync") { return; } if ("options" in changes) { const { oldValue, newValue } = changes.options; const changedKeys = []; for (const key of Object.keys(newValue)) { if (oldValue) { // Don't track added keys if (!(key in oldValue)) { continue; } const oldKeyValue = oldValue[key]; const newKeyValue = newValue[key]; // Equality comparison if (oldKeyValue === newKeyValue) { continue; } // Array comparison if ( oldKeyValue instanceof Array && newKeyValue instanceof Array ) { if ( oldKeyValue.length === newKeyValue.length && oldKeyValue.every( (value, index) => value === newKeyValue[index] ) ) { continue; } } } changedKeys.push(key); } this.dispatchEvent( new CustomEvent("changed", { detail: changedKeys as Array }) ); } } /** * Fetches `options` key from storage and returns it as * Options interface type. */ public async getAll(): Promise { const { options } = await storageArea.get("options"); return options; } /** * Takes Options object and sets to `options` storage key. * Returns storage promise. */ public async setAll(options: Options): Promise { return storageArea.set({ options }); } /** * Gets specific option from storage and returns it as its * type from Options interface type. */ public async get(name: T): Promise { const options = await this.getAll(); if (options.hasOwnProperty(name)) { return options[name]; } else { throw logger.error(`Failed to find option ${name} in storage.`); } } /** * Sets specific option to storage. Returns storage * promise. */ public async set( name: T, value: Options[T] ): Promise { const options = await this.getAll(); options[name] = value; return this.setAll(options); } /** * Gets existing options from storage and compares it * against defaults. Any options in defaults and not in * storage are set. Does not override any existing options. */ public async update(defaults = defaultOptions): Promise { const newOpts = await this.getAll(); // Find options not already in storage for (const [optName, optVal] of Object.entries(defaults)) { if (!newOpts.hasOwnProperty(optName)) { newOpts[optName] = optVal; } } // Update storage with default values of new options return this.setAll(newOpts); } })();