webext-messenger
Advanced tools
Comparing version 0.13.0-1 to 0.13.0-2
@@ -1,58 +0,4 @@ | ||
/// <reference types="firefox-webext-browser" /> | ||
import { Asyncify, SetReturnType, ValueOf } from "type-fest"; | ||
declare global { | ||
interface MessengerMethods { | ||
_: Method; | ||
__webextMessengerTargetRegistration: typeof _registerTarget; | ||
} | ||
} | ||
declare type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: Target | NamedTarget, ...args: PreviousArguments) => TReturnValue : never; | ||
declare type ActuallyOmitThisParameter<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : T; | ||
/** Removes the `this` type and ensure it's always Promised */ | ||
declare type PublicMethod<Method extends ValueOf<MessengerMethods>> = Asyncify<ActuallyOmitThisParameter<Method>>; | ||
declare type PublicMethodWithTarget<Method extends ValueOf<MessengerMethods>> = WithTarget<PublicMethod<Method>>; | ||
export interface MessengerMeta { | ||
trace: browser.runtime.MessageSender[]; | ||
} | ||
declare type Arguments = any[]; | ||
declare type Method = (this: MessengerMeta, ...args: Arguments) => Promise<unknown>; | ||
export declare class MessengerError extends Error { | ||
name: string; | ||
} | ||
export interface Target { | ||
tabId: number; | ||
frameId?: number; | ||
} | ||
export interface NamedTarget { | ||
/** If the id is missing, it will use the sender’s tabId instead */ | ||
tabId?: number; | ||
name: string; | ||
} | ||
interface Options { | ||
/** | ||
* "Notifications" won't await the response, return values, attempt retries, nor throw errors | ||
* @default false | ||
*/ | ||
isNotification?: boolean; | ||
} | ||
/** | ||
* Replicates the original method, including its types. | ||
* To be called in the sender’s end. | ||
*/ | ||
declare function getContentScriptMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethod extends PublicMethodWithTarget<Method>>(type: Type, options: { | ||
isNotification: true; | ||
}): SetReturnType<PublicMethod, void>; | ||
declare function getContentScriptMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethod extends PublicMethodWithTarget<Method>>(type: Type, options?: Options): PublicMethod; | ||
/** | ||
* Replicates the original method, including its types. | ||
* To be called in the sender’s end. | ||
*/ | ||
declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, options: { | ||
isNotification: true; | ||
}): SetReturnType<PublicMethodType, void>; | ||
declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, options?: Options): PublicMethodType; | ||
declare function registerMethods(methods: Partial<MessengerMethods>): void; | ||
/** Register the current context so that it can be targeted with a name */ | ||
declare const registerTarget: (name: string) => Promise<void>; | ||
declare function _registerTarget(this: MessengerMeta, name: string): void; | ||
export { getMethod, getContentScriptMethod, registerMethods, registerTarget }; | ||
export { registerMethods } from "./receiver"; | ||
export { getContentScriptMethod, getMethod } from "./sender"; | ||
export { MessengerMeta, NamedTarget, Target } from "./types"; | ||
export { registerTarget } from "./namedTargets"; |
@@ -1,189 +0,5 @@ | ||
import pRetry from "p-retry"; | ||
import { deserializeError, serializeError } from "serialize-error"; | ||
import { isBackgroundPage } from "webext-detect-page"; | ||
const errorNonExistingTarget = "Could not establish connection. Receiving end does not exist."; | ||
export class MessengerError extends Error { | ||
constructor() { | ||
super(...arguments); | ||
Object.defineProperty(this, "name", { | ||
enumerable: true, | ||
configurable: true, | ||
writable: true, | ||
value: "MessengerError" | ||
}); | ||
} | ||
} | ||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Private key | ||
const __webext_messenger__ = true; | ||
function isObject(value) { | ||
return typeof value === "object" && value !== null; | ||
} | ||
function isMessengerMessage(message) { | ||
return (isObject(message) && | ||
typeof message["type"] === "string" && | ||
message["__webext_messenger__"] === true && | ||
Array.isArray(message["args"])); | ||
} | ||
function isMessengerResponse(response) { | ||
return isObject(response) && response["__webext_messenger__"] === true; | ||
} | ||
const handlers = new Map(); | ||
async function handleCall(message, sender, call) { | ||
console.debug(`Messenger:`, message.type, message.args, "from", { sender }); | ||
// The handler could actually be a synchronous function | ||
const response = await Promise.resolve(call).then((value) => ({ value }), (error) => ({ | ||
// Errors must be serialized because the stacktraces are currently lost on Chrome and | ||
// https://github.com/mozilla/webextension-polyfill/issues/210 | ||
error: serializeError(error), | ||
})); | ||
console.debug(`Messenger:`, message.type, "responds", response); | ||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Private key | ||
return { ...response, __webext_messenger__ }; | ||
} | ||
async function handleMessage(message, sender) { | ||
if (message.target) { | ||
if (!isBackgroundPage()) { | ||
console.warn("Messenger:", message.type, "received but ignored; Wrong context"); | ||
return; | ||
} | ||
const resolvedTarget = "name" in message.target | ||
? resolveNamedTarget(message.target, sender.trace[0]) | ||
: message.target; | ||
const publicMethod = getContentScriptMethod(message.type); | ||
return handleCall(message, sender, publicMethod(resolvedTarget, ...message.args)); | ||
} | ||
const handler = handlers.get(message.type); | ||
if (handler) { | ||
return handleCall(message, sender, handler.apply(sender, message.args)); | ||
} | ||
// More context in https://github.com/pixiebrix/webext-messenger/issues/45 | ||
console.warn("Messenger:", message.type, "received but ignored; No handlers were registered here"); | ||
} | ||
// Do not turn this into an `async` function; Notifications must turn `void` | ||
function manageConnection(type, options, sendMessage) { | ||
if (!options.isNotification) { | ||
return manageMessage(type, sendMessage); | ||
} | ||
void sendMessage().catch((error) => { | ||
console.debug("Messenger:", type, "notification failed", { error }); | ||
}); | ||
} | ||
async function manageMessage(type, sendMessage) { | ||
const response = await pRetry(sendMessage, { | ||
minTimeout: 100, | ||
factor: 1.3, | ||
maxRetryTime: 4000, | ||
onFailedAttempt(error) { | ||
if (!String(error === null || error === void 0 ? void 0 : error.message).startsWith(errorNonExistingTarget)) { | ||
throw error; | ||
} | ||
console.debug("Messenger:", type, "will retry"); | ||
}, | ||
}); | ||
if (!isMessengerResponse(response)) { | ||
throw new MessengerError(`No handler for ${type} was registered in the receiving end`); | ||
} | ||
if ("error" in response) { | ||
throw deserializeError(response.error); | ||
} | ||
return response.value; | ||
} | ||
// MUST NOT be `async` or Promise-returning-only | ||
function onMessageListener(message, sender) { | ||
if (isMessengerMessage(message)) { | ||
return handleMessage(message, { trace: [sender] }); | ||
} | ||
// TODO: Add test for this eventuality: ignore unrelated messages | ||
} | ||
function makeMessage(type, args, target) { | ||
return { | ||
// eslint-disable-next-line @typescript-eslint/naming-convention -- Private key | ||
__webext_messenger__, | ||
type, | ||
args, | ||
target, | ||
}; | ||
} | ||
function getContentScriptMethod(type, options = {}) { | ||
const publicMethod = (target, ...args) => { | ||
// Named targets and contexts without direct Tab access must go through background, unless we're already in it | ||
if (!browser.tabs || ("name" in target && !isBackgroundPage())) { | ||
return manageConnection(type, options, async () => browser.runtime.sendMessage(makeMessage(type, args, target))); | ||
} | ||
const resolvedTarget = "name" in target ? resolveNamedTarget(target) : target; | ||
// `frameId` must be specified. If missing, the message is sent to every frame | ||
const { tabId, frameId = 0 } = resolvedTarget; | ||
// Message tab directly | ||
return manageConnection(type, options, async () => browser.tabs.sendMessage(tabId, makeMessage(type, args), { frameId })); | ||
}; | ||
return publicMethod; | ||
} | ||
function getMethod(type, options = {}) { | ||
const publicMethod = (...args) => { | ||
if (isBackgroundPage()) { | ||
const handler = handlers.get(type); | ||
if (handler) { | ||
console.warn("Messenger:", type, "is being handled locally"); | ||
return handler.apply({ trace: [] }, args); | ||
} | ||
throw new MessengerError("No handler registered for " + type); | ||
} | ||
const sendMessage = async () => browser.runtime.sendMessage(makeMessage(type, args)); | ||
return manageConnection(type, options, sendMessage); | ||
}; | ||
return publicMethod; | ||
} | ||
function registerMethods(methods) { | ||
for (const [type, method] of Object.entries(methods)) { | ||
if (handlers.has(type)) { | ||
throw new MessengerError(`Handler already set for ${type}`); | ||
} | ||
console.debug(`Messenger: Registered`, type); | ||
handlers.set(type, method); | ||
} | ||
// Use "chrome" because the polyfill might not be available when `_registerTarget` is registered | ||
if ("browser" in globalThis) { | ||
browser.runtime.onMessage.addListener(onMessageListener); | ||
} | ||
else { | ||
console.error("Messenger: webextension-polyfill was not loaded in time, this might cause a runtime error later"); | ||
// @ts-expect-error Temporary workaround until I drop the webextension-polyfill dependency | ||
chrome.runtime.onMessage.addListener(onMessageListener); | ||
} | ||
} | ||
// TODO: Remove targets after tab closes to avoid "memory leaks" | ||
const targets = new Map(); | ||
/** Register the current context so that it can be targeted with a name */ | ||
const registerTarget = getMethod("__webextMessengerTargetRegistration"); | ||
function _registerTarget(name) { | ||
const sender = this.trace[0]; | ||
const tabId = sender.tab.id; | ||
const { frameId } = sender; | ||
targets.set(`${tabId}%${name}`, { | ||
tabId, | ||
frameId, | ||
}); | ||
console.debug(`Messenger: Target "${name}" registered for tab ${tabId}`); | ||
} | ||
function resolveNamedTarget(target, sender) { | ||
var _a; | ||
if (!isBackgroundPage()) { | ||
throw new Error("Named targets can only be resolved in the background page"); | ||
} | ||
const { name, tabId = (_a = sender === null || sender === void 0 ? void 0 : sender.tab) === null || _a === void 0 ? void 0 : _a.id, // If not specified, try to use the sender’s | ||
} = target; | ||
if (typeof tabId === "undefined") { | ||
throw new TypeError(`${errorNonExistingTarget} The tab ID was not specified nor it was automatically determinable.`); | ||
} | ||
const resolvedTarget = targets.get(`${tabId}%${name}`); | ||
if (!resolvedTarget) { | ||
throw new Error(`${errorNonExistingTarget} Target named ${name} not registered for tab ${tabId}.`); | ||
} | ||
return resolvedTarget; | ||
} | ||
if (isBackgroundPage()) { | ||
registerMethods({ | ||
__webextMessengerTargetRegistration: _registerTarget, | ||
}); | ||
} | ||
export { getMethod, getContentScriptMethod, registerMethods, registerTarget }; | ||
export { registerMethods } from "./receiver"; | ||
export { getContentScriptMethod, getMethod } from "./sender"; | ||
export { registerTarget } from "./namedTargets"; | ||
import { initTargets } from "./namedTargets"; | ||
initTargets(); |
{ | ||
"name": "webext-messenger", | ||
"version": "0.13.0-1", | ||
"version": "0.13.0-2", | ||
"description": "Browser Extension component messaging framework", | ||
@@ -11,10 +11,6 @@ "keywords": [], | ||
"main": "distribution/index.js", | ||
"files": [ | ||
"distribution/index.js", | ||
"distribution/index.d.ts" | ||
], | ||
"scripts": { | ||
"build": "tsc", | ||
"demo:watch": "parcel watch test/demo-extension/manifest.json --dist-dir test/dist --no-cache --no-hmr --detailed-report 0", | ||
"demo:build": "parcel build test/demo-extension/manifest.json --dist-dir test/dist --no-cache --detailed-report 0 --no-optimize", | ||
"demo:watch": "parcel watch --no-cache --no-hmr --detailed-report 0", | ||
"demo:build": "parcel build --no-cache --detailed-report 0", | ||
"prepack": "tsc --sourceMap false", | ||
@@ -110,6 +106,12 @@ "test": "eslint . && tsc --noEmit", | ||
"targets": { | ||
"main": false | ||
"main": false, | ||
"default": { | ||
"source": "source/test/manifest.json", | ||
"sourceMap": { | ||
"inline": true | ||
} | ||
} | ||
}, | ||
"webExt": { | ||
"sourceDir": "test/dist", | ||
"sourceDir": "dist", | ||
"run": { | ||
@@ -116,0 +118,0 @@ "startUrl": [ |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
18520
17
312
1