Remote Procedure Call implementation for WebExtensions, to easily call functions across content scripts and background script.
Browse Source

Make RpcServer/RpcClient classes

master
Gerben 2 years ago
parent
commit
6fda4e2ce6
10 changed files with 355 additions and 261 deletions
  1. +26
    -14
      Readme.md
  2. +7
    -7
      example/background-script.ts
  3. +6
    -8
      example/content-script.ts
  4. +15
    -0
      package.json
  5. +138
    -0
      src/RpcClient.ts
  6. +75
    -0
      src/RpcServer.ts
  7. +55
    -0
      src/common.ts
  8. +2
    -215
      src/index.ts
  9. +30
    -14
      test/RpcClient.ts
  10. +1
    -3
      tsconfig.json

+ 26
- 14
Readme.md View File

@@ -9,16 +9,16 @@ background script from a tab’s content script, or vice versa.


In `background.js`: In `background.js`:


import { makeRemotelyCallable } from 'webextension-rpc'
import { RpcServer } from 'webextension-rpc'
async function myFunc(arg) { async function myFunc(arg) {
return arg * 2 return arg * 2
} }
makeRemotelyCallable({ myFunc })
new RpcServer({ myFunc })


In `content_script.js`: In `content_script.js`:


import { remoteFunction } from 'webextension-rpc'
const myRemoteFunc = remoteFunction('myFunc')
import { RpcClient } from 'webextension-rpc'
const myRemoteFunc = new RpcClient().func('myFunc')
await myRemoteFunc(21) // 42! await myRemoteFunc(21) // 42!


Note that the remote function always returns a `Promise`, which resolves with the remote function’s Note that the remote function always returns a `Promise`, which resolves with the remote function’s
@@ -39,30 +39,42 @@ This module is published [on npm](https://www.npmjs.com/package/webextension-rpc


Run `npm install webextension-rpc` or equivalent, and in your code import what you need, e.g.: Run `npm install webextension-rpc` or equivalent, and in your code import what you need, e.g.:


import { makeRemotelyCallable } from 'webextension-rpc'

Or copy its `lib/index.js` and import from that if you prefer (this module has no dependencies).
import { RpcClient, RpcServer } from 'webextension-rpc'




## API ## API


### `remoteFunction(functionName, { tabId })`
### RpcClient

#### `new RpcClient(options?)` (constructor)

Instantiate the RpcClient.

Arguments:
- `options` (object, optional):
- `options.tabId` (number): The id of the tab whose content script is the remote side. Leave undefined or
null to invoke functions in the background script (from a content script).

#### `func(functionName, options?)`


Create a proxy function that invokes the specified remote function. Create a proxy function that invokes the specified remote function.


Arguments:
- `functionName` (string, required): name of the function as registered on the remote side. - `functionName` (string, required): name of the function as registered on the remote side.
- `options` (object, optional):
- `tabId` (number): The id of the tab whose content script is the remote side. Leave undefined
to call the background script (from a content script).
- `options` (object, optional): override any options passed to the constructor.


### RpcServer


### `makeRemotelyCallable(functions, { insertExtraArg })`
#### `new RpcServer(functions)` (constructor)


Register one or more functions to enable remote scripts to call them. Arguments:
Register one or more functions to enable remote scripts to call them.

Arguments:


- `functions` (object, required): An object with a `{ functionName: function }` mapping. Each - `functions` (object, required): An object with a `{ functionName: function }` mapping. Each
function will be remotely callable using the given name. function will be remotely callable using the given name.



### `injectRpcInfo` ### `injectRpcInfo`


If the special symbol `injectRpcInfo` is passed as the first argument to a proxy function, this If the special symbol `injectRpcInfo` is passed as the first argument to a proxy function, this


+ 7
- 7
example/background-script.ts View File

@@ -1,19 +1,19 @@
import { remoteFunction, makeRemotelyCallable } from 'webextension-rpc';
import { RpcClient, RpcServer } from 'webextension-rpc';
import type { RpcInfo } from 'webextension-rpc'; import type { RpcInfo } from 'webextension-rpc';
// Only import the *types* of the remote script’s functions. // Only import the *types* of the remote script’s functions.
import type { contentScriptRemoteFunctions } from './content-script';
import type { contentScriptRpcServer } from './content-script';


// From background to content script. // From background to content script.
const setColour = remoteFunction<typeof contentScriptRemoteFunctions.setColour>(
'setColour',
{ tabId: 123 },
);
const contentScriptRpc = new RpcClient<typeof contentScriptRpcServer>({ tabId: 123 });
const setColour = contentScriptRpc.func('setColour');
await setColour('blue'); await setColour('blue');


// From content to background script. // From content to background script.
export const backgroundScriptRemoteFunctions = makeRemotelyCallable({
const backgroundScriptRpcServer = new RpcServer({
async duplicateTab(rpcInfo: RpcInfo, active: boolean) { async duplicateTab(rpcInfo: RpcInfo, active: boolean) {
const newTab = await browser.tabs.duplicate(rpcInfo.tab.id, { active }); const newTab = await browser.tabs.duplicate(rpcInfo.tab.id, { active });
return newTab.id; return newTab.id;
}, },
async timesTwo(x: number) { return 2 * x },
}); });
export type { backgroundScriptRpcServer }

+ 6
- 8
example/content-script.ts View File

@@ -1,22 +1,20 @@
import { import {
remoteFunction,
makeRemotelyCallable,
RpcClient,
RpcServer,
injectRpcInfo, injectRpcInfo,
} from 'webextension-rpc'; } from 'webextension-rpc';
// Only import the *types* of the remote script’s functions. // Only import the *types* of the remote script’s functions.
import type { backgroundScriptRemoteFunctions } from './background-script';
import type { backgroundScriptRpcServer } from './background-script';


// From background to content script. // From background to content script.
export const contentScriptRemoteFunctions = makeRemotelyCallable({
const contentScriptRpcServer = new RpcServer({
async setColour(colour: string) { async setColour(colour: string) {
document.body.style.backgroundColor = colour; document.body.style.backgroundColor = colour;
}, },
}); });
export type { contentScriptRpcServer }


// From content to background script. // From content to background script.
const duplicateMe =
remoteFunction<typeof backgroundScriptRemoteFunctions.duplicateTab>(
'duplicateTab',
);
const duplicateMe = new RpcClient<typeof backgroundScriptRpcServer>().func('duplicateTab');
// The injectRpcInfo placeholder will be replaced by actual info. // The injectRpcInfo placeholder will be replaced by actual info.
const newTabId = await duplicateMe(injectRpcInfo, true); const newTabId = await duplicateMe(injectRpcInfo, true);

+ 15
- 0
package.json View File

@@ -4,6 +4,21 @@
"description": "Remote Procedure Call implementation for WebExtensions, to easily call functions across content scripts and background script.", "description": "Remote Procedure Call implementation for WebExtensions, to easily call functions across content scripts and background script.",
"main": "./lib/index.js", "main": "./lib/index.js",
"types": "./lib/index.d.ts", "types": "./lib/index.d.ts",
"exports": {
".": {
"import": "./lib/index.js",
"types": "./lib/index.d.ts"
},
"./RpcClient": {
"import": "./lib/RpcClient.js",
"types": "./lib/RpcClient.d.ts"
},
"./RpcServer": {
"import": "./lib/RpcServer.js",
"types": "./lib/RpcServer.d.ts"
}
},
"files": ["lib"],
"scripts": { "scripts": {
"prepare": "tsc", "prepare": "tsc",
"test": "ava" "test": "ava"


+ 138
- 0
src/RpcClient.ts View File

@@ -0,0 +1,138 @@
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<F> 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<F extends AsyncFunction> = (
...args: ReplaceRpcInfo<Parameters<F>, typeof injectRpcInfo>
) => Promise<Awaited<ReturnType<F>>>

type FunctionsOf<R extends RpcServer> = 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<TheRpcServer extends RpcServer> {
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<FunctionName extends string & keyof FunctionsOf<TheRpcServer>>(
funcName: FunctionName,
{
tabId = this.tabId,
}: RpcOptions = {},
): RpcFunction<FunctionsOf<TheRpcServer>[FunctionName]> {
type RemoteFunction = FunctionsOf<TheRpcServer>[FunctionName]

const otherSide = (tabId !== undefined)
? "the tab's content script"
: 'the background script'

const f: RpcFunction<RemoteFunction> = async function (...args): ReturnType<RpcFunction<RemoteFunction>> {
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<RemoteFunction>
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
}
}

+ 75
- 0
src/RpcServer.ts View File

@@ -0,0 +1,75 @@
import { isRpcCallMessage, RPC_RESPONSE } from './common'
import type { AsyncFunction, RpcCallMessage, RpcResponseMessage } from './common'

export type { AsyncFunction }

/**
* 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 interface RpcInfo {
tab: browser.tabs.Tab,
}

/**
* Register one or more functions to enable remote scripts to call them.
*
* @param functions - A `{ functionName: function }` mapping. Each function will be remotely
* callable using the given name.
*/
export class RpcServer<Fs extends Record<string, AsyncFunction> = Record<string, AsyncFunction>> {
constructor(
public readonly functions: Fs,
) {
browser.runtime.onMessage.addListener(this.incomingRPCListener.bind(this))
}

// TODO Avoid conflict if there are multiple listeners.
private incomingRPCListener(
message: any,
sender: browser.runtime.MessageSender,
): undefined | Promise<RpcResponseMessage> {
if (!isRpcCallMessage(message)) return

// TODO Support extension popups and other pages, not just background script & tabs.
// Each page gets the message, so we may need to name each endpoint.
// Then here we should return if the message was not for us.

return this.executeRpc(message, sender)
}

private async executeRpc(message: RpcCallMessage, sender: browser.runtime.MessageSender): Promise<RpcResponseMessage> {
const funcName = message.funcName
const func = this.functions[funcName]
if (func === undefined) {
console.error(`Received RPC for unknown function: ${funcName}`)
return {
rpcError: `No such function registered for RPC: ${funcName}`,
__WEBEXTENSION_RPC_MESSAGE__: RPC_RESPONSE,
}
}

const args = message.args

if (message.addRpcInfoAsArgument !== false) {
const rpcInfo: RpcInfo = {
tab: sender.tab,
}
args[message.addRpcInfoAsArgument] = rpcInfo
}

// Run the function, return the result.
try {
const returnValue = await func(...args)
return {
returnValue,
__WEBEXTENSION_RPC_MESSAGE__: RPC_RESPONSE,
}
} catch (error) {
return {
errorMessage: error.message,
__WEBEXTENSION_RPC_MESSAGE__: RPC_RESPONSE,
}
}
}
}

+ 55
- 0
src/common.ts View File

@@ -0,0 +1,55 @@
import type { RpcInfo } from './RpcServer'

export type AsyncFunction = Function & ((...args: any[]) => Promise<any>)

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

export interface RpcMessage {
__WEBEXTENSION_RPC_MESSAGE__: typeof RPC_CALL | typeof RPC_RESPONSE,
funcName: string,
}

export interface RpcCallMessage<F extends AsyncFunction = AsyncFunction> extends RpcMessage {
__WEBEXTENSION_RPC_MESSAGE__: typeof RPC_CALL,
funcName: F['name'],
args: ReplaceRpcInfo<Parameters<F>, null>,
addRpcInfoAsArgument: number | false,
}

export type RpcResponseMessage<F extends AsyncFunction = AsyncFunction> =
| RpcResponseResolve<F>
| RpcResponseReject
| RpcResponseRpcError

interface RpcResponseMessage_base {
__WEBEXTENSION_RPC_MESSAGE__: typeof RPC_RESPONSE,
}

interface RpcResponseResolve<F extends AsyncFunction> extends RpcResponseMessage_base {
returnValue: ReturnType<F>,
}

interface RpcResponseReject extends RpcResponseMessage_base {
errorMessage: string,
}

interface RpcResponseRpcError extends RpcResponseMessage_base {
rpcError: string,
}

export function isRpcCallMessage(message: any): message is RpcCallMessage {
return !!(message && message['__WEBEXTENSION_RPC_MESSAGE__'] === RPC_CALL)
}

export function isRpcResponseMessage(message: any): message is RpcResponseMessage {
return !!(message && message['__WEBEXTENSION_RPC_MESSAGE__'] === RPC_RESPONSE)
}

type Tail<T extends any[]> = T extends [infer _Head, ...infer Tail] ? Tail : [];

export type ReplaceRpcInfo<Params extends Parameters<AsyncFunction>, Replacement extends any> =
Params extends [RpcInfo, ...any]
? [Replacement, ...Tail<Params>]
: Params

+ 2
- 215
src/index.ts View File

@@ -1,215 +1,2 @@
// Our secret tokens to recognise our messages
const RPC_CALL = '__RPC_CALL__'
const RPC_RESPONSE = '__RPC_RESPONSE__'

export class RpcError extends Error {
constructor(message) {
super(message)
this.name = this.constructor.name
}
}

export class RemoteError extends Error {
constructor(message) {
super(message)
this.name = this.constructor.name
}
}

export const injectRpcInfo = Symbol('RpcInfo')

export interface RpcInfo {
tab: browser.tabs.Tab,
}

type AsyncFunction = Function & ((...args: any[]) => Promise<any>)

interface RpcMessage {
__WEBEXTENSION_RPC_MESSAGE__: typeof RPC_CALL | typeof RPC_RESPONSE,
funcName: string,
}

interface RpcCallMessage<F extends AsyncFunction = AsyncFunction> extends RpcMessage {
__WEBEXTENSION_RPC_MESSAGE__: typeof RPC_CALL,
funcName: F['name'],
args: ParametersWithRpcInfo<F>,
addRpcInfoAsArgument: number | false,
}

type RpcResponseMessage<F extends AsyncFunction = AsyncFunction> =
| RpcResponseResolve<F>
| RpcResponseReject
| RpcResponseRpcError

interface RpcResponseMessage_base {
__WEBEXTENSION_RPC_MESSAGE__: typeof RPC_RESPONSE,
}

interface RpcResponseResolve<F extends AsyncFunction> extends RpcResponseMessage_base {
returnValue: ReturnType<F>,
}

interface RpcResponseReject extends RpcResponseMessage_base {
errorMessage: string,
}

interface RpcResponseRpcError extends RpcResponseMessage_base {
rpcError: string,
}

function isRpcCallMessage(message: any): message is RpcCallMessage {
return !!(message && message['__WEBEXTENSION_RPC_MESSAGE__'] === RPC_CALL)
}

function isRpcResponseMessage(message: any): message is RpcResponseMessage {
return !!(message && message['__WEBEXTENSION_RPC_MESSAGE__'] === RPC_RESPONSE)
}

type Tail<T extends any[]> = T extends [infer _Head, ...infer Tail] ? Tail : [];

type ParametersWithRpcInfo<F extends AsyncFunction> = Parameters<F> extends [RpcInfo, ...any]
? [typeof injectRpcInfo, ...Tail<Parameters<F>>]
: Parameters<F>

// I thought the type below should allow putting the RpcInfo argument at any position, but it does not work.
// type ParametersWithRpcInfo<F extends AsyncFunction> = Array<any> & {
// [N in keyof Parameters<F>]: Parameters<F>[N] extends RpcInfo
// ? typeof injectRpcInfo
// : Parameters<F>[N]
// }

/**
* RpcFunction<F> 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<F extends AsyncFunction> = (...args: ParametersWithRpcInfo<F>) => Promise<Awaited<ReturnType<F>>>


// === Initiating side ===

/**
* 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 - The id of the tab whose content script is the remote side. Leave undefined
to call the background script (from a content script).
* @returns The proxy function.
*/
export function remoteFunction<F extends AsyncFunction>(
funcName: string,
{ tabId }: { tabId?: number } = {},
): RpcFunction<F> {
const otherSide = (tabId !== undefined)
? "the tab's content script"
: 'the background script'

const f: RpcFunction<F> = async function (...args): Promise<Awaited<ReturnType<F>>> {
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<F>
try {
response = (tabId !== undefined)
? 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
}


// === Executing side ===


/**
* Register one or more functions to enable remote scripts to call them.
*
* @param functions - A `{ functionName: function }` mapping. Each function will be remotely
* callable using the given name.
* @returns The passed `functions` object.
*/
export function makeRemotelyCallable<Fs extends Record<string, AsyncFunction>>(
functions: Fs,
): typeof functions {
browser.runtime.onMessage.addListener(incomingRPCListener)
return functions

async function incomingRPCListener(message: any, sender: browser.runtime.MessageSender): Promise<RpcResponseMessage> {
if (!isRpcCallMessage(message)) return

const funcName = message.funcName
const func = functions[funcName]
if (func === undefined) {
console.error(`Received RPC for unknown function: ${funcName}`)
return {
rpcError: `No such function registered for RPC: ${funcName}`,
__WEBEXTENSION_RPC_MESSAGE__: RPC_RESPONSE,
}
}

const args = message.args

if (message.addRpcInfoAsArgument !== false) {
const rpcInfo: RpcInfo = {
tab: sender.tab,
}
args[message.addRpcInfoAsArgument] = rpcInfo
}

// Run the function, return the result.
try {
const returnValue = await func(...args)
return {
returnValue,
__WEBEXTENSION_RPC_MESSAGE__: RPC_RESPONSE,
}
} catch (error) {
return {
errorMessage: error.message,
__WEBEXTENSION_RPC_MESSAGE__: RPC_RESPONSE,
}
}
}
}
export * from './RpcClient'
export * from './RpcServer'

test/index.ts → test/RpcClient.ts View File

@@ -1,7 +1,7 @@
import test from 'ava' import test from 'ava'
import sinon from 'ts-sinon' import sinon from 'ts-sinon'


import { remoteFunction, RpcError, RemoteError, injectRpcInfo } from '../src/index'
import { RpcClient, RpcError, RemoteError, injectRpcInfo } from '../src/RpcClient'


function mockBrowser() { function mockBrowser() {
return { return {
@@ -23,13 +23,13 @@ test.beforeEach(() => {
}) })


test.serial('should create a function', t => { test.serial('should create a function', t => {
const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
t.is(remoteFunc.name, 'remoteFunc_RPC') t.is(remoteFunc.name, 'remoteFunc_RPC')
t.is(typeof remoteFunc, 'function') t.is(typeof remoteFunc, 'function')
}) })


test.serial('should throw an error when unable to sendMessage', async t => { test.serial('should throw an error when unable to sendMessage', async t => {
const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
browser.tabs.sendMessage = async () => { throw new Error() } browser.tabs.sendMessage = async () => { throw new Error() }
await t.throwsAsync(remoteFunc, { await t.throwsAsync(remoteFunc, {
instanceOf: RpcError, instanceOf: RpcError,
@@ -38,7 +38,7 @@ test.serial('should throw an error when unable to sendMessage', async t => {
}) })


test.serial('should call the browser.tabs function when tabId is given', async t => { test.serial('should call the browser.tabs function when tabId is given', async t => {
const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
try { try {
await remoteFunc() await remoteFunc()
} catch (e) {} } catch (e) {}
@@ -47,7 +47,25 @@ test.serial('should call the browser.tabs function when tabId is given', async t
}) })


test.serial('should call the browser.runtime function when tabId is undefined', async t => { test.serial('should call the browser.runtime function when tabId is undefined', async t => {
const remoteFunc = remoteFunction('remoteFunc')
const remoteFunc = new RpcClient().func('remoteFunc')
try {
await remoteFunc()
} catch (e) {}
t.true(browser.tabs.sendMessage.notCalled)
t.true(browser.runtime.sendMessage.calledOnce)
})

test.serial('should call the browser.tabs function when tabId is overridden', async t => {
const remoteFunc = new RpcClient().func('remoteFunc', { tabId: 123 })
try {
await remoteFunc()
} catch (e) {}
t.true(browser.tabs.sendMessage.calledOnce)
t.true(browser.runtime.sendMessage.notCalled)
})

test.serial('should call the browser.runtime function when tabId is overridden as null', async t => {
const remoteFunc = new RpcClient({ tabId: 123 }).func('remoteFunc', { tabId: null })
try { try {
await remoteFunc() await remoteFunc()
} catch (e) {} } catch (e) {}
@@ -56,7 +74,7 @@ test.serial('should call the browser.runtime function when tabId is undefined',
}) })


test.serial('should send the call message correctly', async t => { test.serial('should send the call message correctly', async t => {
const remoteFunc = remoteFunction('remoteFunc')
const remoteFunc = new RpcClient().func('remoteFunc')
try { try {
await remoteFunc('a', 'b', 'c', 'd') await remoteFunc('a', 'b', 'c', 'd')
} catch {} } catch {}
@@ -70,7 +88,7 @@ test.serial('should send the call message correctly', async t => {
}) })


test.serial('should handle the RpcInfoSymbol', async t => { test.serial('should handle the RpcInfoSymbol', async t => {
const remoteFunc = remoteFunction('remoteFunc')
const remoteFunc = new RpcClient().func('remoteFunc')
try { try {
await remoteFunc('a', 'b', injectRpcInfo, 'd') await remoteFunc('a', 'b', injectRpcInfo, 'd')
} catch {} } catch {}
@@ -78,14 +96,14 @@ test.serial('should handle the RpcInfoSymbol', async t => {
t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{ t.deepEqual(browser.runtime.sendMessage.lastCall.args, [{
__WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__', __WEBEXTENSION_RPC_MESSAGE__: '__RPC_CALL__',
funcName: 'remoteFunc', funcName: 'remoteFunc',
args: ['a', 'b', injectRpcInfo, 'd'],
args: ['a', 'b', null, 'd'],
addRpcInfoAsArgument: 2, addRpcInfoAsArgument: 2,
}]) }])
}) })


test.serial('should throw an "interfering listener" error if response is unrecognised', async t => { test.serial('should throw an "interfering listener" error if response is unrecognised', async t => {
browser.tabs.sendMessage = async () => 'some unexpected return value' browser.tabs.sendMessage = async () => 'some unexpected return value'
const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
await t.throwsAsync(remoteFunc, { await t.throwsAsync(remoteFunc, {
instanceOf: RpcError, instanceOf: RpcError,
message: /RPC got a response from an interfering listener/, message: /RPC got a response from an interfering listener/,
@@ -96,7 +114,7 @@ test.serial('should throw a "no response" error if response is undefined', async
// It seems we can get back undefined when the tab is closed before the response is sent. // It seems we can get back undefined when the tab is closed before the response is sent.
// In such cases 'no response' seems a better error message than 'interfering listener'. // In such cases 'no response' seems a better error message than 'interfering listener'.
browser.tabs.sendMessage = async () => undefined browser.tabs.sendMessage = async () => undefined
const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
await t.throwsAsync(remoteFunc, { await t.throwsAsync(remoteFunc, {
instanceOf: RpcError, instanceOf: RpcError,
message: /Got no response/, message: /Got no response/,
@@ -108,7 +126,7 @@ test.serial('should throw RemoteError if the response contains an error message'
__WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__', __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__',
errorMessage: 'Remote function error', errorMessage: 'Remote function error',
}) })
const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
await t.throwsAsync(remoteFunc, { await t.throwsAsync(remoteFunc, {
instanceOf: RemoteError, instanceOf: RemoteError,
message: 'Remote function error', message: 'Remote function error',
@@ -120,8 +138,6 @@ test.serial('should return the value contained in the response', async t => {
__WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__', __WEBEXTENSION_RPC_MESSAGE__: '__RPC_RESPONSE__',
returnValue: 'Remote function return value', returnValue: 'Remote function return value',
}) })
const remoteFunc = remoteFunction('remoteFunc', { tabId: 1 })
const remoteFunc = new RpcClient({ tabId: 1 }).func('remoteFunc')
t.is(await remoteFunc(), 'Remote function return value') t.is(await remoteFunc(), 'Remote function return value')
}) })

// TODO Test behaviour of executing side.

+ 1
- 3
tsconfig.json View File

@@ -4,9 +4,7 @@
"declaration": true, "declaration": true,
"outDir": "lib" "outDir": "lib"
}, },
"files": [
"src/index.ts"
],
"include": ["src"],
"ts-node": { "ts-node": {
"transpileOnly": true "transpileOnly": true
} }


Loading…
Cancel
Save