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/features/Features.ts b/src/features/Features.ts
new file mode 100644
index 0000000..9b3eaea
--- /dev/null
+++ b/src/features/Features.ts
@@ -0,0 +1,47 @@
+import Feature from '../common/architecture/features/Feature';
+import {
+  ScriptEnvironment,
+  ScriptPage,
+  ScriptRunPhase,
+} from '../common/architecture/scripts/Script';
+import InfiniteScrollFeature from './infiniteScroll/infiniteScroll.feature';
+
+export type ConcreteFeatureClass = { new (): Feature };
+
+export interface Context {
+  page: ScriptPage;
+  environment: ScriptEnvironment;
+  runPhase: ScriptRunPhase;
+}
+
+export default class Features {
+  private features: ConcreteFeatureClass[] = [InfiniteScrollFeature];
+  private initializedFeatures: Feature[];
+
+  runScripts(context: Context) {
+    const scripts = this.getScripts(context).sort((a, b) =>
+      a.priority < b.priority ? -1 : 1,
+    );
+    for (const script of scripts) {
+      script.execute();
+    }
+  }
+
+  getScripts(context: Context) {
+    const features = this.getFeatures();
+    const allScripts = features.map((feature) => feature.getScripts()).flat(1);
+    return allScripts.filter(
+      (script) =>
+        script.page === context.page &&
+        script.environment === context.environment &&
+        script.runPhase === context.runPhase,
+    );
+  }
+
+  private getFeatures() {
+    if (this.initializedFeatures === undefined) {
+      this.initializedFeatures = this.features.map((feature) => new feature());
+    }
+    return this.initializedFeatures;
+  }
+}