Create mainWorldContentScriptBridge

These classes are the base of a communication bridge between the main
world and content scripts, which has been extracted from the main world
options watcher.

This will be used by the main world i18n component.

Bug: twpowertools:157
Change-Id: I08553b05648ad8453203ab08ecf01e824af15fea
diff --git a/src/common/mainWorldContentScriptBridge/Client.js b/src/common/mainWorldContentScriptBridge/Client.js
new file mode 100644
index 0000000..70e567b
--- /dev/null
+++ b/src/common/mainWorldContentScriptBridge/Client.js
@@ -0,0 +1,50 @@
+export const kDefaultTimeout = 10 * 1000;  // 10 seconds
+
+export default class MainWorldContentScriptBridgeClient {
+  constructor(CSTarget, MWTarget, timeout) {
+    if (!CSTarget || !MWTarget) {
+      throw new Error(
+          `[MWOptionsWatcherClient] CSTarget and MWTarget are compulsory.`);
+    }
+
+    this.CSTarget = CSTarget;
+    this.MWTarget = MWTarget;
+    this.timeout = timeout ?? kDefaultTimeout;
+  }
+
+  _sendRequestWithoutCallback(action, request, uuid) {
+    if (!uuid) uuid = self.crypto.randomUUID();
+    const data = {
+      target: this.CSTarget,
+      uuid,
+      action,
+      request,
+    };
+    window.postMessage(data, '*');
+  }
+
+  _sendRequest(action, request) {
+    return new Promise((res, rej) => {
+      const uuid = self.crypto.randomUUID();
+
+      let timeoutId;
+      let listener = e => {
+        if (e.source !== window || e.data?.target !== this.MWTarget ||
+            e.data?.uuid !== uuid)
+          return;
+
+        window.removeEventListener('message', listener);
+        clearTimeout(timeoutId);
+        res(e.data?.response);
+      };
+      window.addEventListener('message', listener);
+
+      timeoutId = setTimeout(() => {
+        window.removeEventListener('message', listener);
+        rej(new Error('Timed out while waiting response.'));
+      }, this.timeout);
+
+      this._sendRequestWithoutCallback(action, request, uuid);
+    });
+  }
+}
diff --git a/src/common/mainWorldContentScriptBridge/Server.js b/src/common/mainWorldContentScriptBridge/Server.js
new file mode 100644
index 0000000..321a068
--- /dev/null
+++ b/src/common/mainWorldContentScriptBridge/Server.js
@@ -0,0 +1,37 @@
+export default class MainWorldContentScriptBridgeServer {
+  constructor(CSTarget, MWTarget) {
+    if (!CSTarget || !MWTarget) {
+      throw new Error(
+          `[MWOptionsWatcherServer] CSTarget and MWTarget are compulsory.`);
+    }
+
+    this.CSTarget = CSTarget;
+    this.MWTarget = MWTarget;
+    this.handler = () => {};
+  }
+
+  // Handler should be an action of the form (uuid, action, request) => {...}.
+  setUpHandler(handler) {
+    this.handler = handler;
+    window.addEventListener('message', e => this.#handleMessage(e));
+  }
+
+  #handleMessage(e) {
+    const uuid = e.data?.uuid;
+    if (e.source !== window || e.data?.target !== this.CSTarget || !uuid)
+      return;
+
+    const action = e.data?.action;
+    const request = e.data?.request;
+    return this.handler(uuid, action, request);
+  }
+
+  _respond(uuid, response) {
+    const data = {
+      target: this.MWTarget,
+      uuid,
+      response,
+    };
+    window.postMessage(data, window.origin);
+  }
+}
diff --git a/src/common/mainWorldOptionsWatcher/Client.js b/src/common/mainWorldOptionsWatcher/Client.js
index 3e5a9f9..a37b394 100644
--- a/src/common/mainWorldOptionsWatcher/Client.js
+++ b/src/common/mainWorldOptionsWatcher/Client.js
@@ -1,76 +1,35 @@
-export const kDefaultTimeout = 10 * 1000;  // 10 seconds
+import MainWorldContentScriptBridgeClient from '../mainWorldContentScriptBridge/Client.js';
 
 // Main World OptionsWatcher client (used in scripts injected into the Main
 // World (MW) to get the options).
-export default class MWOptionsWatcherClient {
+export default class MWOptionsWatcherClient extends
+    MainWorldContentScriptBridgeClient {
   constructor(options, CSTarget, MWTarget, timeout) {
-    if (!CSTarget || !MWTarget)
-      throw new Error(
-          `[MWOptionsWatcherClient] CSTarget and MWTarget are compulsory.`);
-
-    this.CSTarget = CSTarget;
-    this.MWTarget = MWTarget;
-    this.timeout = timeout ?? kDefaultTimeout;
+    super(CSTarget, MWTarget, timeout);
     this.#setUp(options);
   }
 
   #setUp(options) {
-    this.#sendRequestWithoutCallback('setUp', {options});
+    this._sendRequestWithoutCallback('setUp', {options});
   }
 
   async getOption(option) {
     if (!option) return null;
-    return this.#sendRequest('getOption', {option});
+    return this._sendRequest('getOption', {option});
   }
 
   async getOptions(options) {
     if (!options || options?.length === 0) return [];
-    return this.#sendRequest('getOptions', {options});
+    return this._sendRequest('getOptions', {options});
   }
 
   async isEnabled(option) {
     if (!option) return null;
-    return this.#sendRequest('isEnabled', {option});
+    return this._sendRequest('isEnabled', {option});
   }
 
   async areEnabled(options) {
     if (!options || options?.length === 0) return [];
-    return this.#sendRequest('areEnabled', {options});
-  }
-
-  #sendRequestWithoutCallback(action, request, uuid) {
-    if (!uuid) uuid = self.crypto.randomUUID();
-    const data = {
-      target: this.CSTarget,
-      uuid,
-      action,
-      request,
-    };
-    window.postMessage(data, '*');
-  }
-
-  #sendRequest(action, request) {
-    return new Promise((res, rej) => {
-      const uuid = self.crypto.randomUUID();
-
-      let timeoutId;
-      let listener = e => {
-        if (e.source !== window || e.data?.target !== this.MWTarget ||
-            e.data?.uuid !== uuid)
-          return;
-
-        window.removeEventListener('message', listener);
-        clearTimeout(timeoutId);
-        res(e.data?.response);
-      };
-      window.addEventListener('message', listener);
-
-      timeoutId = setTimeout(() => {
-        window.removeEventListener('message', listener);
-        rej(new Error('Timed out while waiting response.'));
-      }, this.timeout);
-
-      this.#sendRequestWithoutCallback(action, request, uuid);
-    });
+    return this._sendRequest('areEnabled', {options});
   }
 }
diff --git a/src/common/mainWorldOptionsWatcher/Server.js b/src/common/mainWorldOptionsWatcher/Server.js
index 0d30aac..353c63d 100644
--- a/src/common/mainWorldOptionsWatcher/Server.js
+++ b/src/common/mainWorldOptionsWatcher/Server.js
@@ -1,46 +1,38 @@
 import OptionsWatcher from '../../common/optionsWatcher.js';
+import MainWorldContentScriptBridgeServer from '../mainWorldContentScriptBridge/Server.js';
 
 // Main World OptionsWatcher server (used in content scripts to be able to serve
 // the options to Main World (MW) scripts).
-export default class MWOptionsWatcherServer {
+export default class MWOptionsWatcherServer extends
+    MainWorldContentScriptBridgeServer {
   constructor(CSTarget, MWTarget) {
-    if (!CSTarget || !MWTarget)
-      throw new Error(
-          `[MWOptionsWatcherServer] CSTarget and MWTarget are compulsory.`);
-
+    super(CSTarget, MWTarget);
     this.optionsWatcher = null;
-    this.CSTarget = CSTarget;
-    this.MWTarget = MWTarget;
-
-    window.addEventListener('message', e => this.handleMessage(e));
+    this.setUpHandler(this.handleMessage);
   }
 
-  handleMessage(e) {
-    const uuid = e.data?.uuid;
-    if (e.source !== window || e.data?.target !== this.CSTarget || !uuid)
-      return;
-
-    if (e.data?.action === 'setUp') {
-      this.optionsWatcher = new OptionsWatcher(e.data?.request?.options);
+  handleMessage(uuid, action, request) {
+    if (action === 'setUp') {
+      this.optionsWatcher = new OptionsWatcher(request?.options);
       return;
     }
 
     if (!this.optionsWatcher) {
       console.warn(`[MWOptionsWatcherServer] Action '${
-          e.data?.action}' called before setting up options watcher.`);
+          action}' called before setting up options watcher.`);
       return;
     }
 
-    switch (e.data?.action) {
+    switch (action) {
       case 'getOption':
-        this.optionsWatcher.getOption(e.data?.request?.option).then(value => {
-          this.respond(uuid, value);
+        this.optionsWatcher.getOption(request?.option).then(value => {
+          this._respond(uuid, value);
         });
         return;
 
       case 'getOptions':
         var promises = [];
-        var options = e.data?.request?.options ?? [];
+        var options = request?.options ?? [];
         for (const option of options) {
           promises.push(this.optionsWatcher.getOption(option));
         }
@@ -49,19 +41,19 @@
           for (let i = 0; i < responses.length; i++) {
             response[options[i]] = responses[i];
           }
-          this.respond(uuid, response);
+          this._respond(uuid, response);
         });
         return;
 
       case 'isEnabled':
-        this.optionsWatcher.isEnabled(e.data?.request?.option).then(value => {
-          this.respond(uuid, value);
+        this.optionsWatcher.isEnabled(request?.option).then(value => {
+          this._respond(uuid, value);
         });
         return;
 
       case 'areEnabled':
         var promises = [];
-        var options = e.data?.request?.options ?? [];
+        var options = request?.options ?? [];
         for (const option of options) {
           promises.push(this.optionsWatcher.isEnabled(option));
         }
@@ -70,22 +62,13 @@
           for (let i = 0; i < responses.length; i++) {
             response[options[i]] = responses[i];
           }
-          this.respond(uuid, response);
+          this._respond(uuid, response);
         });
         return;
 
       default:
-        console.error(`[MWOptionsWatcherServer] Invalid action received (${
-            e.data?.action})`);
+        console.error(
+            `[MWOptionsWatcherServer] Invalid action received (${action})`);
     }
   }
-
-  respond(uuid, response) {
-    const data = {
-      target: this.MWTarget,
-      uuid,
-      response,
-    };
-    window.postMessage(data, window.origin);
-  }
 }