BrowserCommunication.js

/**
 * Communicates between different browser parts.
 *
 * @public
 * @module BrowserCommunication
 * @requires data/BrowserCommunicationTypes
 */
import { COMMUNICATION_MESSAGE_TYPE } from "../data/BrowserCommunicationTypes.js";

const callbacks = {};

/**
 * The callback that can be used to send a response.
 *
 * It is just the default browser callback that is used and documented here.
 * However, it is strongly discouraged to return "true", as this is hard to
 * handle with multiple registered callbacks.
 *
 * @callback sendResponseCallback
 * @param {string} message the response message, "may be any JSON-ifiable object"
 * @return {Promise}
 * @see {@link https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/Runtime/onMessage#Parameters}
 */

/**
 * This is the listener that is called when a message with that type has arrived.
 *
 * It is just the default browser callback that is used and documented here.
 * However, it is strongly discouraged to return "true", as this is hard to
 * handle with multiple registered callbacks.
 *
 * @callback listenerCallback
 * @param {Object} request JSON-ifiable object of the message
 * @param {Object} sender the runtime.MessageSender, see {@link https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender}
 * @param {sendResponseCallback} sendResponse
 * @returns {Promise}
 * @see {@link https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/Runtime/onMessage#Parameters}
 */

/**
 * Throws an error, if the message type is not known/known.
 *
 * @private
 * @param {COMMUNICATION_MESSAGE_TYPE} messageType type of message
 * @returns {void}
 * @throws {Error}
 */
function checkMessageTypeVadility(messageType) {
    if (messageType === undefined) {
        throw new Error("message type is undefined");
    }

    if (Object.values(COMMUNICATION_MESSAGE_TYPE).includes(messageType)) {
        return; // all right
    }

    throw new Error(`message type ${messageType} is not valid/known`);
}

/**
 * Handles messages received by other parts.
 *
 * @private
 * @param {Object} request JSON-ifiable object of the message
 * @param {Object} sender the runtime.MessageSender, see {@link https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/runtime/MessageSender}
 * @param {sendResponseCallback} sendResponse
 * @returns {Promise|true}
 * @see {@link https://developer.mozilla.org/docs/Mozilla/Add-ons/WebExtensions/API/Runtime/onMessage#Parameters}
 */
function handleMessages(request, sender, sendResponse) {
    console.info("Got message", request, "from", sender);

    const messageType = request.type;
    checkMessageTypeVadility(messageType);

    if (!(messageType in callbacks) || callbacks[messageType].length === 0) {
        console.warn(`No callbacks for message type "${messageType}" registered.`);
        return Promise.resolve();
    }

    // call all callbacks and keep return values
    const promises = [];
    let gotTrueAsReturn = false;
    for (const callback of callbacks[messageType]) {
        const returnValue = callback(request, sender, sendResponse);

        // notice if return value is just "true"
        if (returnValue === true) {
            gotTrueAsReturn = true;
            continue;
        }

        // return value should be a Promise
        promises.push(returnValue);
    }

    // handle returning
    if (gotTrueAsReturn) {
        if (callbacks[messageType].length !== 1) {
            // if it was not the only callback, then show a real error
            console.error(`At least one callback for message type "${messageType}" returned the legacy value "true".
            As you have registered ${callbacks[messageType].length} listeners this may lead to errors.`);
        } else {
            // show warning as this behaviour is discouraged
            console.warn(`At least one callback for message type "${messageType}" returned the legacy value "true". Please return a Promise instead.`);
        }

        return true;
    }

    return Promise.all(promises);
}

/**
 * Add a listener for a specific type.
 *
 * You can add multiple listeners, but may *NOT* preserve the order.
 * Actually it does call them in reverse as it uses a stack (LIFO) internally.
 *
 * @public
 * @param {COMMUNICATION_MESSAGE_TYPE} messageType type of message to receive
 * @param {listenerCallback} callback
 * @returns {void}
 */
export function addListener(messageType, callback) {
    checkMessageTypeVadility(messageType);

    if (!(messageType in callbacks)) {
        callbacks[messageType] = [];
    }
    callbacks[messageType].push(callback);
}

/**
 * Init context menu module.
 *
 * Adds menu elements.
 *
 * @public
 * @returns {void}
 */
export function init() {
    browser.runtime.onMessage.addListener(handleMessages);
}

// automatically init's itself
init();