blob: 94a43de5bac0208776744d2c3c2348a9d7a4f14d [file] [log] [blame]
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +02001import { Mutex, MutexInterface, withTimeout } from 'async-mutex';
2
3import { getOptions } from './optionsUtils';
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +02004import { OptionCodename, OptionsValues } from './optionsPrototype';
5import { OptionsConfiguration } from './OptionsConfiguration';
Adrià Vilanova Martínez6ecaa0d2024-10-26 17:04:32 +02006import {
7 OptionsChangeListener,
8 OptionsProviderPort,
9} from '../../services/options/OptionsProvider';
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020010
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020011// Prioritize reads before writes.
12const kReadPriority = 10;
13const kWritePriority = 0;
14
15/**
16 * Class which provides option values and a way to listen to option changes.
17 */
Adrià Vilanova Martínez6ecaa0d2024-10-26 17:04:32 +020018export default class OptionsProviderAdapter implements OptionsProviderPort {
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020019 private optionsConfiguration: OptionsConfiguration;
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020020 private mutex: MutexInterface = withTimeout(new Mutex(), 60 * 1000);
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020021 private listeners: Set<OptionsChangeListener> = new Set();
Adrià Vilanova Martínez6ecaa0d2024-10-26 17:04:32 +020022 private isSetUp = false;
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020023
Adrià Vilanova Martínez6ecaa0d2024-10-26 17:04:32 +020024 async getOptionValue<O extends OptionCodename>(
25 option: O,
26 ): Promise<OptionsValues[O]> {
27 this.setUp();
28 return this.mutex.runExclusive(
29 () => this.optionsConfiguration.getOptionValue(option),
30 kReadPriority,
31 );
32 }
33
34 async isEnabled(option: OptionCodename): Promise<boolean> {
35 this.setUp();
36 return this.mutex.runExclusive(
37 () => this.optionsConfiguration.isEnabled(option),
38 kReadPriority,
39 );
40 }
41
42 async getOptionsValues(): Promise<OptionsValues> {
43 this.setUp();
44 return this.mutex.runExclusive(
45 () => this.optionsConfiguration.optionsValues,
46 kReadPriority,
47 );
48 }
49
50 addListener(listener: OptionsChangeListener) {
51 this.setUp();
52 this.listeners.add(listener);
53 }
54
55 private setUp() {
56 if (this.isSetUp) return;
57
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020058 this.listenForStorageChanges();
59 this.updateValues();
60 }
61
62 /**
63 * Sets up a listener to update the current cached configuration when there
64 * are changes to the underlying storage where options are persisted.
65 *
66 * We could try only doing this only when we're sure it has changed, but
67 * there are many factors (if the user has changed it manually, if a kill
68 * switch was activated, etc.) so we do it every time there is any change in
69 * the underlying storage.
70 */
71 private listenForStorageChanges() {
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020072 chrome.storage.onChanged.addListener((_, areaName) => {
73 if (areaName !== 'sync') return;
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020074 console.debug('[OptionsProvider] Retrieving updated options.');
75 this.updateValues();
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020076 });
77 }
78
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020079 private async updateValues() {
80 await this.mutex.runExclusive(async () => {
81 await this.nonSafeUpdateValues();
82 }, kWritePriority);
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020083 }
84
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020085 private async nonSafeUpdateValues() {
86 const previousConfiguration = this.optionsConfiguration;
87 const currentOptionsValues = await getOptions(null);
88 this.optionsConfiguration = new OptionsConfiguration(currentOptionsValues);
89
90 this.notifyListenersIfApplicable(previousConfiguration);
91 }
92
93 private async notifyListenersIfApplicable(
94 previousOptionsConfiguration: OptionsConfiguration,
95 ) {
96 if (
97 !previousOptionsConfiguration ||
98 this.optionsConfiguration.isEqualTo(previousOptionsConfiguration)
99 ) {
100 return;
101 }
102
103 for (const listener of this.listeners) {
104 listener(previousOptionsConfiguration, this.optionsConfiguration);
105 }
106 }
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +0200107}