Files
fx_cast/extension/src/lib/options.ts
2026-03-02 20:59:22 +00:00

146 lines
4.3 KiB
TypeScript

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<keyof Options>;
}
export default new (class extends TypedEventTarget<EventMap> {
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<keyof Options>
})
);
}
}
/**
* Fetches `options` key from storage and returns it as
* Options interface type.
*/
public async getAll(): Promise<Options> {
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<void> {
return storageArea.set({ options });
}
/**
* Gets specific option from storage and returns it as its
* type from Options interface type.
*/
public async get<T extends keyof Options>(name: T): Promise<Options[T]> {
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<T extends keyof Options>(
name: T,
value: Options[T]
): Promise<void> {
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<void> {
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);
}
})();