blob: d09d9942465181e1e2c98463f0d95f30402d8546 [file] [log] [blame]
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +02001import { Mutex, MutexInterface, withTimeout } from 'async-mutex';
2
Adrià Vilanova Martínez7c941c72024-10-26 20:23:34 +02003import { getOptions } from '../../../common/options/optionsUtils';
4import { OptionCodename, OptionsValues } from '../../../common/options/optionsPrototype';
5import { OptionsConfiguration } from '../../../common/options/OptionsConfiguration';
Adrià Vilanova Martínez6ecaa0d2024-10-26 17:04:32 +02006import {
7 OptionsChangeListener,
8 OptionsProviderPort,
Adrià Vilanova Martínez7c941c72024-10-26 20:23:34 +02009} 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ínezc8628b12024-11-10 22:29:45 +010058 this.isSetUp = true;
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020059 this.listenForStorageChanges();
60 this.updateValues();
61 }
62
63 /**
64 * Sets up a listener to update the current cached configuration when there
65 * are changes to the underlying storage where options are persisted.
66 *
67 * We could try only doing this only when we're sure it has changed, but
68 * there are many factors (if the user has changed it manually, if a kill
69 * switch was activated, etc.) so we do it every time there is any change in
70 * the underlying storage.
71 */
72 private listenForStorageChanges() {
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020073 chrome.storage.onChanged.addListener((_, areaName) => {
74 if (areaName !== 'sync') return;
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020075 console.debug('[OptionsProvider] Retrieving updated options.');
76 this.updateValues();
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020077 });
78 }
79
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020080 private async updateValues() {
81 await this.mutex.runExclusive(async () => {
82 await this.nonSafeUpdateValues();
83 }, kWritePriority);
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +020084 }
85
Adrià Vilanova Martínezad696762024-05-25 19:32:36 +020086 private async nonSafeUpdateValues() {
87 const previousConfiguration = this.optionsConfiguration;
88 const currentOptionsValues = await getOptions(null);
89 this.optionsConfiguration = new OptionsConfiguration(currentOptionsValues);
90
91 this.notifyListenersIfApplicable(previousConfiguration);
92 }
93
94 private async notifyListenersIfApplicable(
95 previousOptionsConfiguration: OptionsConfiguration,
96 ) {
97 if (
98 !previousOptionsConfiguration ||
99 this.optionsConfiguration.isEqualTo(previousOptionsConfiguration)
100 ) {
101 return;
102 }
103
104 for (const listener of this.listeners) {
105 listener(previousOptionsConfiguration, this.optionsConfiguration);
106 }
107 }
Adrià Vilanova Martínezf7e86852024-05-11 14:16:38 +0200108}