// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
/**
* @fileoverview Class to handle accessing/storing/caching prefs data.
*/
import {TestImportManager} from './testing/test_import_manager.js';
type PrefObject = chrome.settingsPrivate.PrefObject;
export class Settings {
private listeners_: Record<string, Array<(prefValue: any) => void>> = {};
private prefs_: Record<string, PrefObject>|null = null;
static instance?: Settings;
/**
* @param keys The settings keys the extension cares about.
*/
constructor(keys: string[]) {
keys.forEach(key => this.listeners_[key] = []);
}
/**
* @param keys The settings keys the extension cares about.
*/
static async init(keys: string[]): Promise<void> {
if (Settings.instance) {
throw new Error(
'Settings.init() should be called at most once in each ' +
'browser context.');
}
Settings.instance = new Settings(keys);
await Settings.instance.initialFetch_();
// Add prefs changed listener after initialFetch_() so we don't get updates
// before we've fetched initially.
// TODO(b/314203187): Not null asserted, check these to make sure this is
// correct.
chrome.settingsPrivate.onPrefsChanged.addListener(
(updates: PrefObject[]) => Settings.instance!.update_(updates));
}
/**
* Adds a callback to listen to changes to one or more preferences.
* The callback will be called immediately if there is a value set.
* @param keys The settings keys being listened to.
* @param listener The callback when the value changes.
*/
static addListener(keys: string|string[], listener: (prefValue: any) => void):
void {
if (typeof keys === 'string') {
keys = [keys];
}
for (const key of keys) {
// TODO(b/314203187): Not null asserted, check these to make sure this is
// correct.
Settings.instance!.addListener_(key, listener);
}
}
static get(key: string): any {
// TODO(b/314203187): Not nulls asserted, check these to make sure this is
// correct.
Settings.instance!.validate_(key);
return Settings.instance!.prefs_![key].value;
}
static set(key: string, value: any): void {
// TODO(b/314203187): Not nulls asserted, check these to make sure this is
// correct.
Settings.instance!.validate_(key);
const oldValue = Settings.instance!.prefs_![key].value;
chrome.settingsPrivate.setPref(key, value);
Settings.instance!.prefs_![key].value = value;
if (oldValue !== value) {
Settings.instance!.listeners_[key].forEach(listener => listener(value));
}
}
// ============ Private methods ============
/**
* @param key The settings key being listened to.
* @param listener The callback when the value changes.
* @private
*/
private addListener_(key: string, listener: (prefValue: any) => void): void {
this.validate_(key);
this.listeners_[key].push(listener);
// TODO(b/314203187): Not nulls asserted, check these to make sure this is
// correct.
if (this.prefs_![key] !== null) {
listener(this.prefs_![key].value);
}
}
private async initialFetch_(): Promise<void> {
const prefs: PrefObject[] = await new Promise(
resolve => chrome.settingsPrivate.getAllPrefs(resolve));
const trackedPrefs = prefs!.filter(pref => this.isTracked_(pref.key));
this.prefs_ =
Object.fromEntries(trackedPrefs.map(pref => [pref.key, pref]));
}
private isTracked_(key: string): boolean {
// Because we assign to this.prefs_ in initialFetch_(), use listeners_ as
// the official source of truth on what keys are in scope.
return key in this.listeners_;
}
private update_(updates: PrefObject[]): void {
for (const pref of updates) {
if (!this.isTracked_(pref.key)) {
continue;
}
// TODO(b/314203187): Not null asserted, check these to make sure this is
// correct.
const oldValue = this.prefs_![pref.key].value;
if (oldValue === pref.value) {
continue;
}
// TODO(b/314203187): Not null asserted, check these to make sure this is
// correct.
this.prefs_![pref.key] = pref;
this.listeners_[pref.key].forEach(listener => listener(pref.value));
}
}
private validate_(key: string): void {
if (this.prefs_ === null) {
throw new Error('Cannot access Settings until init() has resolved.');
}
if (!this.isTracked_(key)) {
throw new Error('Prefs key "' + key + '" is not being tracked.');
}
if (!this.prefs_[key]) {
throw new Error('Settings missing pref with key: ' + key);
}
}
}
/** @private {Settings} */
Settings.instance;
TestImportManager.exportForTesting(Settings);