refactor(infinite-scroll): migrate to the new DI architecture

Bug: twpowertools:226

Change-Id: I5f4156204327cc66e976821134cef667d57f7233
diff --git a/src/infrastructure/services/options/OptionsProvider.adapter.ts b/src/infrastructure/services/options/OptionsProvider.adapter.ts
new file mode 100644
index 0000000..e5570fe
--- /dev/null
+++ b/src/infrastructure/services/options/OptionsProvider.adapter.ts
@@ -0,0 +1,107 @@
+import { Mutex, MutexInterface, withTimeout } from 'async-mutex';
+
+import { getOptions } from '../../../common/options/optionsUtils';
+import { OptionCodename, OptionsValues } from '../../../common/options/optionsPrototype';
+import { OptionsConfiguration } from '../../../common/options/OptionsConfiguration';
+import {
+  OptionsChangeListener,
+  OptionsProviderPort,
+} from '../../../services/options/OptionsProvider';
+
+// Prioritize reads before writes.
+const kReadPriority = 10;
+const kWritePriority = 0;
+
+/**
+ * Class which provides option values and a way to listen to option changes.
+ */
+export default class OptionsProviderAdapter implements OptionsProviderPort {
+  private optionsConfiguration: OptionsConfiguration;
+  private mutex: MutexInterface = withTimeout(new Mutex(), 60 * 1000);
+  private listeners: Set<OptionsChangeListener> = new Set();
+  private isSetUp = false;
+
+  async getOptionValue<O extends OptionCodename>(
+    option: O,
+  ): Promise<OptionsValues[O]> {
+    this.setUp();
+    return this.mutex.runExclusive(
+      () => this.optionsConfiguration.getOptionValue(option),
+      kReadPriority,
+    );
+  }
+
+  async isEnabled(option: OptionCodename): Promise<boolean> {
+    this.setUp();
+    return this.mutex.runExclusive(
+      () => this.optionsConfiguration.isEnabled(option),
+      kReadPriority,
+    );
+  }
+
+  async getOptionsValues(): Promise<OptionsValues> {
+    this.setUp();
+    return this.mutex.runExclusive(
+      () => this.optionsConfiguration.optionsValues,
+      kReadPriority,
+    );
+  }
+
+  addListener(listener: OptionsChangeListener) {
+    this.setUp();
+    this.listeners.add(listener);
+  }
+
+  private setUp() {
+    if (this.isSetUp) return;
+
+    this.listenForStorageChanges();
+    this.updateValues();
+  }
+
+  /**
+   * Sets up a listener to update the current cached configuration when there
+   * are changes to the underlying storage where options are persisted.
+   *
+   * We could try only doing this only when we're sure it has changed, but
+   * there are many factors (if the user has changed it manually, if a kill
+   * switch was activated, etc.) so we do it every time there is any change in
+   * the underlying storage.
+   */
+  private listenForStorageChanges() {
+    chrome.storage.onChanged.addListener((_, areaName) => {
+      if (areaName !== 'sync') return;
+      console.debug('[OptionsProvider] Retrieving updated options.');
+      this.updateValues();
+    });
+  }
+
+  private async updateValues() {
+    await this.mutex.runExclusive(async () => {
+      await this.nonSafeUpdateValues();
+    }, kWritePriority);
+  }
+
+  private async nonSafeUpdateValues() {
+    const previousConfiguration = this.optionsConfiguration;
+    const currentOptionsValues = await getOptions(null);
+    this.optionsConfiguration = new OptionsConfiguration(currentOptionsValues);
+
+    this.notifyListenersIfApplicable(previousConfiguration);
+  }
+
+  private async notifyListenersIfApplicable(
+    previousOptionsConfiguration: OptionsConfiguration,
+  ) {
+    if (
+      !previousOptionsConfiguration ||
+      this.optionsConfiguration.isEqualTo(previousOptionsConfiguration)
+    ) {
+      return;
+    }
+
+    for (const listener of this.listeners) {
+      listener(previousOptionsConfiguration, this.optionsConfiguration);
+    }
+  }
+}