refactor(response-modifier): extract XHR-specific code from the class

The ResponseModifier class had code specific for XMLHttpRequest. Since
in a follow-up commit we will add support for proxying fetch as well,
this commit extracts the XMLHttpRequest-specific code to the XHRProxy
class.

Bug: twpowertools:229
Change-Id: I1b3653ee20f09ec762aafbac2583a96790a27f45
diff --git a/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts b/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
index 464a7fa..c5d5311 100644
--- a/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
+++ b/src/presentation/standaloneScripts/mainWorldServers/MWOptionsWatcherServerScript.script.ts
@@ -1,6 +1,6 @@
 import Script, { ScriptEnvironment, ScriptPage, ScriptRunPhase } from "../../../common/architecture/scripts/Script"
 import MWOptionsWatcherServer from "../../../common/mainWorldOptionsWatcher/Server"
-import { kCSTarget, kMWTarget } from "../../../xhrInterceptor/responseModifiers"
+import { kCSTarget, kMWTarget } from "../../../xhrInterceptor/ResponseModifier"
 
 export default class MWOptionsWatcherServerScript extends Script {
   // The server should be available as soon as possible, since e.g. the
diff --git a/src/xhrInterceptor/ResponseModifier.ts b/src/xhrInterceptor/ResponseModifier.ts
new file mode 100644
index 0000000..c2b3a34
--- /dev/null
+++ b/src/xhrInterceptor/ResponseModifier.ts
@@ -0,0 +1,107 @@
+import MWOptionsWatcherClient from '../common/mainWorldOptionsWatcher/Client.js';
+import { OptionCodename } from '../common/options/optionsPrototype.js';
+import { ProtobufObject } from '../common/protojs.types.js';
+
+import createMessageRemoveParentRef from './responseModifiers/createMessageRemoveParentRef';
+import flattenThread from './responseModifiers/flattenThread';
+import loadMoreThread from './responseModifiers/loadMoreThread';
+import { Modifier } from './responseModifiers/types';
+
+export const responseModifiers = [
+  loadMoreThread,
+  flattenThread,
+  createMessageRemoveParentRef,
+] as Modifier[];
+
+// Content script target
+export const kCSTarget = 'TWPT-XHRInterceptorOptionsWatcher-CS';
+// Main world (AKA regular web page) target
+export const kMWTarget = 'TWPT-XHRInterceptorOptionsWatcher-MW';
+
+export default class ResponseModifier {
+  private optionsWatcher: MWOptionsWatcherClient;
+
+  constructor() {
+    this.optionsWatcher = new MWOptionsWatcherClient(
+      Array.from(this.watchingFeatures(responseModifiers)),
+      kCSTarget,
+      kMWTarget,
+    );
+  }
+
+  private watchingFeatures(modifiers: Modifier[]): Set<OptionCodename> {
+    const union = new Set<OptionCodename>();
+    for (const m of modifiers) {
+      if (!m.featureGated) continue;
+      for (const feature of m.features) union.add(feature);
+    }
+    return union;
+  }
+
+  private async getMatchingModifiers(requestUrl: string) {
+    // First filter modifiers which match the request URL regex.
+    const urlModifiers = responseModifiers.filter((modifier) =>
+      requestUrl.match(modifier.urlRegex),
+    );
+
+    // Now filter modifiers which require a certain feature to be enabled
+    // (feature-gated modifiers).
+    const featuresAreEnabled = await this.optionsWatcher.areEnabled(
+      Array.from(this.watchingFeatures(urlModifiers)),
+    );
+
+    // #!if !production
+    if (Object.keys(featuresAreEnabled).length > 0) {
+      console.debug(
+        '[XHR Interceptor - Response Modifier] Requested features',
+        featuresAreEnabled,
+        'for request',
+        requestUrl,
+      );
+    }
+    // #!endif
+
+    return urlModifiers.filter((modifier) => {
+      return !modifier.featureGated || modifier.isEnabled(featuresAreEnabled);
+    });
+  }
+
+  async intercept(interception: InterceptedResponse): Promise<Result> {
+    const matchingModifiers = await this.getMatchingModifiers(interception.url);
+
+    // If we didn't find any matching modifiers, return the response right away.
+    if (matchingModifiers.length === 0) return { wasModified: false };
+
+    // Otherwise, apply the modifiers sequentially and set the new response.
+    let json = interception.originalResponse;
+    for (const modifier of matchingModifiers) {
+      json = await modifier.interceptor(json, interception.url);
+    }
+    return {
+      wasModified: true,
+      modifiedResponse: json,
+    };
+  }
+}
+
+/**
+ * Represents an intercepted response.
+ */
+export interface InterceptedResponse {
+  /**
+   * URL of the original request.
+   */
+  url: string;
+
+  /**
+   * Object with the response as intercepted without any modification.
+   */
+  originalResponse: ProtobufObject;
+}
+
+export type Result =
+  | { wasModified: false }
+  | {
+      wasModified: true;
+      modifiedResponse: ProtobufObject;
+    };
diff --git a/src/xhrInterceptor/XHRProxy.js b/src/xhrInterceptor/XHRProxy.js
index 95529d8..14d2aae 100644
--- a/src/xhrInterceptor/XHRProxy.js
+++ b/src/xhrInterceptor/XHRProxy.js
@@ -1,7 +1,7 @@
 import {waitFor} from 'poll-until-promise';
 
 import {correctArrayKeys} from '../common/protojs';
-import ResponseModifier from '../xhrInterceptor/responseModifiers/index.js';
+import ResponseModifier from '../xhrInterceptor/ResponseModifier';
 import * as utils from '../xhrInterceptor/utils.js';
 
 const kSpecialEvents = ['load', 'loadend'];
@@ -144,26 +144,39 @@
     window.XMLHttpRequest.prototype.$proxySpecialEvents = function() {
       const proxyInstance = this;
       kSpecialEvents.forEach(eventName => {
-        this.xhr.addEventListener(eventName, function() {
-          let interceptedPromise;
+        this.xhr.addEventListener(eventName, async function() {
           if (eventName === 'load') {
-            interceptedPromise =
-                XHRProxyInstance.responseModifier.intercept(proxyInstance)
-                    .then(() => {
-                      proxyInstance.$responseIntercepted = true;
-                    });
+            const initialResponse = utils.getResponseJSON({
+              responseType: proxyInstance.xhr.responseType,
+              response: proxyInstance.xhr.response,
+              $TWPTRequestURL: proxyInstance.$TWPTRequestURL,
+              $isArrayProto: proxyInstance.$isArrayProto,
+            });
+
+            const result = await XHRProxyInstance.responseModifier.intercept({
+              url: proxyInstance.$TWPTRequestURL,
+              originalResponse: initialResponse,
+            });
+
+            if (result.wasModified) {
+              proxyInstance.$newResponse = utils.convertJSONToResponse(
+                  proxyInstance, result.modifiedResponse);
+              proxyInstance.$newResponseText = utils.convertJSONToResponseText(
+                  proxyInstance, result.modifiedResponse);
+            }
+
+            proxyInstance.$responseModified = result.wasModified;
+            proxyInstance.$responseIntercepted = true;
           } else {
-            interceptedPromise = waitFor(() => {
+            await waitFor(() => {
               if (proxyInstance.$responseIntercepted) return Promise.resolve();
               return Promise.reject();
             }, kCheckInterceptionOptions);
           }
 
-          interceptedPromise.then(() => {
-            for (const e of proxyInstance.specialHandlers[eventName]) {
-              e[1](arguments);
-            }
-          });
+          for (const e of proxyInstance.specialHandlers[eventName]) {
+            e[1](arguments);
+          }
         });
       });
     };
diff --git a/src/xhrInterceptor/responseModifiers/index.js b/src/xhrInterceptor/responseModifiers/index.js
deleted file mode 100644
index b822153..0000000
--- a/src/xhrInterceptor/responseModifiers/index.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import MWOptionsWatcherClient from '../../common/mainWorldOptionsWatcher/Client.js';
-import {convertJSONToResponse, convertJSONToResponseText, getResponseJSON} from '../utils.js';
-
-import createMessageRemoveParentRef from './createMessageRemoveParentRef';
-import flattenThread from './flattenThread';
-import loadMoreThread from './loadMoreThread';
-
-export const responseModifiers = [
-  loadMoreThread,
-  flattenThread,
-  createMessageRemoveParentRef,
-];
-
-// Content script target
-export const kCSTarget = 'TWPT-XHRInterceptorOptionsWatcher-CS';
-// Main world (AKA regular web page) target
-export const kMWTarget = 'TWPT-XHRInterceptorOptionsWatcher-MW';
-
-export default class ResponseModifier {
-  constructor() {
-    this.optionsWatcher = new MWOptionsWatcherClient(
-        Array.from(this.watchingFeatures()), kCSTarget, kMWTarget);
-  }
-
-  watchingFeatures(modifiers) {
-    if (!modifiers) modifiers = responseModifiers;
-
-    const union = new Set();
-    for (const m of modifiers) {
-      if (!m.featureGated) continue;
-      for (const feature of m.features) union.add(feature);
-    }
-    return union;
-  }
-
-  async #getMatchingModifiers(request) {
-    // First filter modifiers which match the request URL regex.
-    const urlModifiers = responseModifiers.filter(
-        modifier => request.$TWPTRequestURL.match(modifier.urlRegex));
-
-    // Now filter modifiers which require a certain feature to be enabled
-    // (feature-gated modifiers).
-    const featuresAreEnabled = await this.optionsWatcher.areEnabled(
-        Array.from(this.watchingFeatures(urlModifiers)));
-
-    // #!if !production
-    if (Object.keys(featuresAreEnabled).length > 0) {
-      console.debug(
-          '[XHR Interceptor - Response Modifier] Requested features',
-          featuresAreEnabled, 'for request', request.$TWPTRequestURL);
-    }
-    // #!endif
-
-    return urlModifiers.filter(modifier => {
-      return !modifier.featureGated || modifier.isEnabled(featuresAreEnabled);
-    });
-  }
-
-  async intercept(request) {
-    const matchingModifiers = await this.#getMatchingModifiers(request);
-
-    // If we didn't find any matching modifiers, return the response right away.
-    if (matchingModifiers.length === 0) return request.xhr.response;
-
-    // Otherwise, apply the modifiers sequentially and set the new response.
-    let json = getResponseJSON({
-      responseType: request.xhr.responseType,
-      response: request.xhr.response,
-      $TWPTRequestURL: request.$TWPTRequestURL,
-      $isArrayProto: request.$isArrayProto,
-    });
-    for (const modifier of matchingModifiers) {
-      json = await modifier.interceptor(request, json);
-    }
-    request.$newResponse = convertJSONToResponse(request, json);
-    request.$newResponseText = convertJSONToResponseText(request, json);
-    request.$responseModified = true;
-  }
-}