webext-typed-messages
Version:
Better WebExtension sendMessage API
175 lines (164 loc) • 4.33 kB
text/typescript
/**
* Definitions for your custom runtime messages.
*
* Should be a map of message types to message handler functions.
*
* @example
*
* ```ts
* declare module "webext-typed-messages" {
* interface RuntimeMessages {
* hello: (name: string) => string;
* }
* }
* ```
*/
export interface RuntimeMessages {}
/**
* Definitions for your custom tab messages.
*
* Should be a map of message types to message handler functions.
*
* @example
*
* ```ts
* declare module "webext-typed-messages" {
* interface TabMessages {
* add: (a: number, b: number) => Promise<number>;
* }
* }
* ```
*/
export interface TabMessages {}
/**
* Parameters for a message handler.
*
* @template T - Message handler map
* @template M - Message type
*/
type messageParams<T extends {}, M extends keyof T> = T[M] extends (
...params: infer P
) => any
? P
: never;
/**
* Return type of a message handler.
*
* @template T - Message handler map
* @template M - Message type
*/
type messageResult<T extends {}, M extends keyof T> = T[M] extends (
...params: any[]
) => infer R
? Awaited<R>
: undefined;
/**
* Sends a runtime message to your registered handlers.
*
* Call {@link addRuntimeMessageListener} to register your handlers.
*
* @see {@link chrome.runtime.sendMessage}
*/
export function sendRuntimeMessage<M extends keyof RuntimeMessages>(
type: M,
...params: messageParams<RuntimeMessages, M>
): Promise<messageResult<RuntimeMessages, M> | undefined> {
return new Promise((resolve, reject) => {
chrome.runtime.sendMessage({ type, params }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
});
});
}
/**
* Sends a message to the content script in the specified tab.
*
* The content script should call {@link addTabMessageListener} to receive the message.
*
* @see {@link chrome.tabs.sendMessage}
*/
export function sendTabMessage<M extends keyof TabMessages>(
tabId: number,
type: M,
...params: messageParams<TabMessages, M>
): Promise<messageResult<TabMessages, M> | undefined> {
return new Promise((resolve, reject) => {
chrome.tabs.sendMessage(tabId, { type, params }, (response) => {
if (chrome.runtime.lastError) {
reject(new Error(chrome.runtime.lastError.message));
} else {
resolve(response);
}
});
});
}
/**
* Object of functions for handling messages of each type.
*
* @template T - Message handler map
*/
type MessageListenerMap<T extends {}> = {
[M in keyof T]: (
this: void,
...params: messageParams<T, M>
) => messageResult<T, M> | Promise<messageResult<T, M>>;
};
/**
* Registers listeners for runtime messages.
*
* Should be called in the background script.
*
* @see {@link chrome.runtime.onMessage}
*/
export function addRuntimeMessageListener(
listenerMap: MessageListenerMap<RuntimeMessages>
) {
const messageListener = getMessageListener(listenerMap);
chrome.runtime.onMessage.addListener(messageListener);
return function removeMessageListener() {
chrome.runtime.onMessage.removeListener(messageListener);
};
}
/**
* Registers listeners for tab messages.
*
* Should be called in the content script.
*
* @see {@link chrome.runtime.onMessage}
*/
export function addTabMessageListener(
listenerMap: MessageListenerMap<TabMessages>
) {
const messageListener = getMessageListener(listenerMap);
chrome.runtime.onMessage.addListener(messageListener);
return function removeMessageListener() {
chrome.runtime.onMessage.removeListener(messageListener);
};
}
function getMessageListener<T extends {}>(listenerMap: MessageListenerMap<T>) {
return function messageListener(
message: any,
sender: chrome.runtime.MessageSender,
sendResponse: (response: any) => void
) {
handleMessage(listenerMap, message, sendResponse);
return true;
};
}
async function handleMessage(
listenerMap: Record<string, (...params: any[]) => any>,
message: any,
sendResponse: (response: any) => void
) {
const listener = listenerMap[message.type];
try {
const result = await listener?.(...message.params);
sendResponse(result);
} catch (error) {
sendResponse(undefined);
throw error;
}
}