// A Remote Procedure Call abstraction around the message passing available to
// WebExtension scripts. Usable to call a function in the background script from
// a tab's content script, or vice versa.
//
// The calling side always gets a Promise of the return value. The executing
// (remote) function can be an async function (= it returns a Promise), whose
// completion then will then be waited for.

// Example use:
//
// === background.js ===
// function myFunc(arg) {
//     return arg*2
// }
// makeRemotelyCallable({myFunc})
//
// === content_script.js ===
// const myRemoteFunc = remoteFunction('myFunc')
// myRemoteFunc(21).then(result => { ... result is 42! ... })


import mapValues from 'lodash/fp/mapValues'


// Our secret tokens to recognise our messages
const RPC_CALL = '__RPC_CALL__'
const RPC_RESPONSE = '__RPC_RESPONSE__'


// === Initiating side ===

// Create a proxy function that invokes the specified remote function.
// Arguments
// - funcName (required): name of the function as registered on the remote side.
// - options (optional): {
//       tabId: The id of the tab whose content script is the remote side.
//              Leave undefined to call the background script (from a tab).
//   }
export function remoteFunction(funcName, {tabId} = {}) {
    const otherSide = (tabId !== undefined)
        ? "the tab's content script"
        : 'the background script'

    const f = async function (...args) {
        const message = {
            [RPC_CALL]: RPC_CALL,
            funcName,
            args,
        }

        // Try send the message and await the response.
        let response
        try {
            response = (tabId !== undefined)
                ? await browser.tabs.sendMessage(tabId, message)
                : await browser.runtime.sendMessage(message)
        } catch (err) {
            throw new Error(
                `Got no response when trying to call '${funcName}'. `
                + `Did you enable RPC in ${otherSide}?`
            )
        }

        // Check if it was *our* listener that responded.
        if (!response || response[RPC_RESPONSE] !== RPC_RESPONSE) {
            throw new Error(`RPC got a response from an interfering listener.`)
        }

        // Return the value or throw the error we received from the other side.
        if (response.error) {
            throw new Error(response.error)
        } else {
            return response.returnValue
        }
    }

    // Give it a name, could be helpful in debugging
    Object.defineProperty(f, 'name', { value: `${funcName}_RPC` })
    return f
}


// === Executing side ===

const noSuchFunctionError = 'Received RPC for unknown function: '

const remotelyCallableFunctions = {}

function incomingRPCListener(message, sender) {
    if (message && message[RPC_CALL] === RPC_CALL) {
        const funcName = message.funcName
        const args = message.hasOwnProperty('args') ? message.args : []
        const func = remotelyCallableFunctions[funcName]
        if (func === undefined) {
            console.error(noSuchFunctionError, funcName)
            return {
                error: `No such function registered for RPC: ${funcName}`,
            }
        }
        const extraArg = {
            tab: sender.tab,
        }
        const value = func(extraArg, ...args)
        return Promise.resolve(value).then(
            value => ({
                returnValue: value,
                [RPC_RESPONSE]: RPC_RESPONSE,
            })
        )
    }
}

// A bit of global state to ensure we only attach the event listener once.
let enabled = false

// Register a function to allow remote scripts to call it.
// Arguments:
// - functions (required):
//     An object with a {functionName: function} mapping.
//     Each function will be callable with the given name.
// - options (optional): {
//       insertExtraArg:
//           If truthy, each executed function also receives, as its first
//           argument before the arguments it was invoked with, an object with
//           the details of the tab that sent the message.
//   }

export function makeRemotelyCallable(functions, {insertExtraArg = false} = {}) {
    // Every function is passed an extra argument with sender information,
    // so remove this from the call if this was not desired.
    if (!insertExtraArg) {
        // Replace each func with...
        const wrapFunctions = mapValues(func =>
            // ...a function that calls func, but hides the inserted argument.
            (extraArg, ...args) => func(...args)
        )
        functions = wrapFunctions(functions)
    }

    // Add the functions to our global repetoir.
    Object.assign(remotelyCallableFunctions, functions)

    // Enable the listener if needed.
    if (!enabled) {
        browser.runtime.onMessage.addListener(incomingRPCListener)
        enabled = true
    }
}

@@ -3181,16 +3181,62 @@
 Name%7D%60,%0A
+                %5BRPC_RESPONSE%5D: RPC_RESPONSE,%0A
         
@@ -3233,32 +3233,32 @@
 ,%0A            %7D%0A
-
         %7D%0A      
@@ -3313,32 +3313,33 @@
 ,%0A        %7D%0A
+%0A
         
 const value 
@@ -3330,15 +3330,95 @@
     
-const v
+// Run the function, and await its value if it returns a promise.%0A        const returnV
 alue
@@ -3475,17 +3475,23 @@
 resolve(
-v
+returnV
 alue).th
@@ -3497,22 +3497,15 @@
 hen(
-%0A            v
+returnV
 alue
@@ -3519,28 +3519,24 @@
             
-    
 returnValue:
@@ -3538,21 +3538,10 @@
 alue
-: value,%0A    
+,%0A
     
@@ -3590,23 +3590,101 @@
     
-    %7D)%0A        
+%7D)).catch(error =%3E (%7B%0A            error,%0A            %5BRPC_RESPONSE%5D: RPC_RESPONSE,%0A        %7D)
 )%0A  

Fix RPC in Firefox.