refactor: migrate infinite scroll feature to a new architecture

This CL introduces a new architecture for the features source code.

Bug: twpowertools:176
Change-Id: I9abc4df2fb67f9bb0c9114aaffc6916d34f1b7ff
diff --git a/src/common/architecture/features/Feature.ts b/src/common/architecture/features/Feature.ts
new file mode 100644
index 0000000..c14c338
--- /dev/null
+++ b/src/common/architecture/features/Feature.ts
@@ -0,0 +1,29 @@
+import Script, { ConcreteScript } from '../scripts/Script';
+
+export default abstract class Feature {
+  /**
+   * Internal codename used for the feature.
+   *
+   * It will be used for i18n translations, shown in log messages, etc.
+   */
+  abstract readonly codename: string;
+
+  /**
+   * Options which control the behavior of this feature.
+   */
+  abstract readonly relatedOptions: string[];
+
+  /**
+   * Uninitialized scripts which are associated with the feature.
+   */
+  abstract readonly scripts: ConcreteScript[];
+
+  private initializedScripts: Script[];
+
+  public getScripts() {
+    if (this.initializedScripts === undefined) {
+      this.initializedScripts = this.scripts.map((script) => new script());
+    }
+    return this.initializedScripts;
+  }
+}
diff --git a/src/common/architecture/scripts/Script.ts b/src/common/architecture/scripts/Script.ts
new file mode 100644
index 0000000..5cca2c4
--- /dev/null
+++ b/src/common/architecture/scripts/Script.ts
@@ -0,0 +1,66 @@
+export enum ScriptRunPhase {
+  /**
+   * Executed before any Javascript is executed.
+   */
+  Start,
+  /**
+   * Executed after the document is ready.
+   */
+  Main,
+}
+
+export enum ScriptEnvironment {
+  ContentScript,
+  InjectedScript,
+}
+
+export enum ScriptPage {
+  CommunityConsole,
+}
+
+export const ScriptRunPhaseToRunTime: Record<
+  ScriptRunPhase,
+  chrome.userScripts.RunAt
+> = {
+  [ScriptRunPhase.Start]: 'document_start',
+  [ScriptRunPhase.Main]: 'document_idle',
+};
+
+export const ScriptEnvironmentToExecutionWorld: Record<
+  ScriptEnvironment,
+  chrome.scripting.ExecutionWorld
+> = {
+  [ScriptEnvironment.ContentScript]: 'ISOLATED',
+  [ScriptEnvironment.InjectedScript]: 'MAIN',
+};
+
+export type ConcreteScript = { new (): Script };
+
+export default abstract class Script {
+  /**
+   * Priority with which the script is executed. Scripts with a lower value are
+   * executed first.
+   */
+  readonly priority: Number = 2 ** 31;
+
+  /**
+   * Page where the script should be executed.
+   */
+  abstract readonly page: ScriptPage;
+
+  /**
+   * Environment where the script should be executed.
+   */
+  abstract readonly environment: ScriptEnvironment;
+
+  /**
+   * If {@link environment} is {@link ScriptEnvironment.ContentScript}, phase of
+   * the page loading when the script should be executed.
+   */
+  abstract readonly runPhase?: ScriptRunPhase;
+
+  /**
+   * Method which contains the logic of the script.
+   */
+  abstract execute(): void;
+}
diff --git a/src/common/architecture/scripts/nodeWatcher/NodeWatcherScript.ts b/src/common/architecture/scripts/nodeWatcher/NodeWatcherScript.ts
new file mode 100644
index 0000000..2c9b91e
--- /dev/null
+++ b/src/common/architecture/scripts/nodeWatcher/NodeWatcherScript.ts
@@ -0,0 +1,29 @@
+import NodeWatcherSingleton from '../../../nodeWatcher/NodeWatcher';
+import { ConcreteNodeWatcherScriptHandler } from './handlers/NodeWatcherScriptHandler';
+import Script from '../Script';
+
+export default abstract class NodeWatcherScript<Options> extends Script {
+  public abstract handlers: Map<
+    string,
+    ConcreteNodeWatcherScriptHandler<Options>
+  >;
+
+  /**
+   * Resolves to the options when the script is executed.
+   *
+   * This is so we can defer retrieving dependencies until the script is
+   * executed, to prevent loading unnecessary things if they aren't needed
+   * after all.
+   */
+  protected abstract optionsFactory(): Options;
+
+  execute() {
+    const nodeWatcher = NodeWatcherSingleton.getInstance();
+    const options = this.optionsFactory();
+
+    for (const [key, handlerClass] of this.handlers) {
+      const handler = new handlerClass(options);
+      nodeWatcher.setHandler(key, handler);
+    }
+  }
+}
diff --git a/src/common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler.ts b/src/common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler.ts
new file mode 100644
index 0000000..22e34af
--- /dev/null
+++ b/src/common/architecture/scripts/nodeWatcher/handlers/CssSelectorNodeWatcherScriptHandler.ts
@@ -0,0 +1,27 @@
+import { NodeWatcherScriptHandler } from './NodeWatcherScriptHandler';
+import { NodeMutation, NodeMutationType } from '../../../../nodeWatcher/NodeWatcherHandler';
+
+export default abstract class CssSelectorNodeWatcherScriptHandler<
+  Options,
+> extends NodeWatcherScriptHandler<Options> {
+  readonly mutationTypesProcessed: NodeMutationType[] = [
+    NodeMutationType.InitialDiscovery,
+    NodeMutationType.NewNode,
+  ];
+
+  abstract readonly cssSelector: string;
+
+  nodeFilter(nodeMutation: NodeMutation): boolean {
+    if (
+      !this.mutationTypesProcessed.includes(nodeMutation.type) ||
+      !(nodeMutation.node instanceof Element)
+    ) {
+      return false;
+    }
+    return nodeMutation.node.matches(this.cssSelector);
+  }
+
+  get initialDiscoverySelector() {
+    return this.cssSelector;
+  }
+}
diff --git a/src/common/architecture/scripts/nodeWatcher/handlers/NodeWatcherScriptHandler.ts b/src/common/architecture/scripts/nodeWatcher/handlers/NodeWatcherScriptHandler.ts
new file mode 100644
index 0000000..5b0fd42
--- /dev/null
+++ b/src/common/architecture/scripts/nodeWatcher/handlers/NodeWatcherScriptHandler.ts
@@ -0,0 +1,14 @@
+import { NodeMutation, NodeWatcherHandler } from "../../../../nodeWatcher/NodeWatcherHandler";
+
+export abstract class NodeWatcherScriptHandler<Options>
+  implements NodeWatcherHandler
+{
+  abstract initialDiscoverySelector?: string;
+  constructor(protected options: Options) {}
+  abstract nodeFilter(nodeMutation: NodeMutation): boolean;
+  abstract onMutatedNode(nodeMutation: NodeMutation): void;
+}
+
+export type ConcreteNodeWatcherScriptHandler<Options> = {
+  new (options: Options): NodeWatcherScriptHandler<Options>;
+};