import type { RpcServer } from "./RpcServer" import { isRpcResponseMessage, RPC_CALL } from "./common" import type { AsyncFunction, RpcCallMessage, RpcResponseMessage, ReplaceRpcInfo } from "./common" export type { AsyncFunction } /** * Error thrown when the remote function could not be found/executed. */ export class RpcError extends Error { constructor(message: string) { super(message) this.name = this.constructor.name } } /** * Error thrown when the remote function threw an error. */ export class RemoteError extends Error { constructor(message: string) { super(message) this.name = this.constructor.name } } /** * If the special symbol `injectRpcInfo` is passed as the first argument to a proxy function, this * argument will be replaced on the executing side by an `RpcInfo` object. */ export const injectRpcInfo = Symbol('RpcInfo') export interface RpcOptions { /** * The id of the tab whose content script is the remote side. Leave undefined * to call the background script (from a content script). */ tabId?: number, } /** * RpcFunction equals the function F, except for two tweaks: * - In the parameters, any RpcInfo is swapped for rpcInfoSymbol. * - In the return type, nested promises become one promise. */ export type RpcFunction = ( ...args: ReplaceRpcInfo, typeof injectRpcInfo> ) => Promise>> type FunctionsOf = R['functions'] /** * Define a ‘connection’ for remote procedure calls. * @param options.tabId - The id of the tab whose content script is the remote side. Leave undefined * to call functions of the background script (from a content script). */ export class RpcClient { tabId: number | undefined constructor(options: RpcOptions = {}) { this.tabId = options.tabId; } /** * Create a proxy function that invokes the specified remote function. * @param funcName - Name of the function as registered on the remote side. * @param options.tabId - Overrides the `tabId` passed to `RpcClient`. * @returns The proxy function. */ func>( funcName: FunctionName, { tabId = this.tabId, }: RpcOptions = {}, ): RpcFunction[FunctionName]> { type RemoteFunction = FunctionsOf[FunctionName] const otherSide = (tabId !== undefined) ? "the tab's content script" : 'the background script' const f: RpcFunction = async function (...args): ReturnType> { const message: RpcCallMessage = { __WEBEXTENSION_RPC_MESSAGE__: RPC_CALL, funcName, args, addRpcInfoAsArgument: false, } if (args.includes(injectRpcInfo)) { const argIndex = args.indexOf(injectRpcInfo) message.addRpcInfoAsArgument = argIndex message.args[argIndex] = null } // Try send the message and await the response. let response: RpcResponseMessage try { response = (tabId !== undefined && tabId !== null) ? await browser.tabs.sendMessage(tabId, message) : await browser.runtime.sendMessage(message) } catch (err) {} // Check if we got an error or no response. if (response === undefined) { throw new RpcError( `Got no response when trying to call '${funcName}'. ` + `Did you enable RPC in ${otherSide}?` ) } // Check if it was *our* listener that responded. if (!isRpcResponseMessage(response)) { throw new RpcError( `RPC got a response from an interfering listener while calling '${funcName}' in ` + `${otherSide}` ) } // If we could not invoke the function on the other side, throw an error. if ('rpcError' in response) { throw new RpcError(response.rpcError) } // Return the value or throw the error we received from the other side. if ('errorMessage' in response) { throw new RemoteError(response.errorMessage) } else { return response.returnValue } } // Give it a name, could be helpful in debugging Object.defineProperty(f, 'name', { value: `${funcName}_RPC` }) return f } }