webext-messenger 0.14.1 → 0.15.0-3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/distribution/index.js +2 -0
- package/distribution/receiver.js +18 -9
- package/distribution/sender.d.ts +1 -1
- package/distribution/sender.js +18 -10
- package/distribution/shared.d.ts +10 -0
- package/distribution/shared.js +10 -0
- package/distribution/thisTarget.d.ts +7 -3
- package/distribution/thisTarget.js +16 -8
- package/distribution/types.d.ts +7 -6
- package/package.json +5 -3
- package/distribution/api.d.ts +0 -10
- package/distribution/api.js +0 -13
- package/distribution/namedTargets.d.ts +0 -14
- package/distribution/namedTargets.js +0 -40
package/distribution/index.js
CHANGED
package/distribution/receiver.js
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
import browser from "webextension-polyfill";
|
2
2
|
import { serializeError } from "serialize-error";
|
3
3
|
import { messenger } from "./sender.js";
|
4
|
-
import { handlers, isObject, MessengerError, debug,
|
4
|
+
import { handlers, isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
|
5
|
+
import { getContextName } from "webext-detect-page";
|
6
|
+
import { getActionForMessage, nameThisTarget } from "./thisTarget.js";
|
5
7
|
export function isMessengerMessage(message) {
|
6
8
|
return (isObject(message) &&
|
7
9
|
typeof message["type"] === "string" &&
|
@@ -14,21 +16,27 @@ function onMessageListener(message, sender) {
|
|
14
16
|
// TODO: Add test for this eventuality: ignore unrelated messages
|
15
17
|
return;
|
16
18
|
}
|
17
|
-
|
19
|
+
// Target check must be synchronous (`await` means we're handing the message)
|
20
|
+
const action = getActionForMessage(sender, message.target);
|
21
|
+
if (action === "ignore") {
|
22
|
+
return;
|
23
|
+
}
|
24
|
+
return handleMessage(message, sender, action);
|
25
|
+
}
|
26
|
+
// This function can only be called when the message *will* be handled locally.
|
27
|
+
// Returning "undefined" or throwing an error will still handle it.
|
28
|
+
async function handleMessage(message, sender, action) {
|
29
|
+
const { type, target, args, options: { trace } = {} } = message;
|
18
30
|
debug(type, "↘️ received", { sender, args });
|
19
31
|
let handleMessage;
|
20
|
-
if (
|
21
|
-
if (!browser.tabs) {
|
22
|
-
throw new MessengerError(`Message ${type} sent to wrong context, it can't be forwarded to ${JSON.stringify(target)}`);
|
23
|
-
}
|
32
|
+
if (action === "forward") {
|
24
33
|
debug(type, "🔀 forwarded", { sender, target });
|
25
|
-
handleMessage = async () => messenger(type, {}, target, ...args);
|
34
|
+
handleMessage = async () => messenger(type, { trace }, target, ...args);
|
26
35
|
}
|
27
36
|
else {
|
28
37
|
const localHandler = handlers.get(type);
|
29
38
|
if (!localHandler) {
|
30
|
-
|
31
|
-
return;
|
39
|
+
throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
|
32
40
|
}
|
33
41
|
debug(type, "➡️ will be handled here");
|
34
42
|
const meta = { trace: [sender] };
|
@@ -46,6 +54,7 @@ function onMessageListener(message, sender) {
|
|
46
54
|
});
|
47
55
|
}
|
48
56
|
export function registerMethods(methods) {
|
57
|
+
void nameThisTarget();
|
49
58
|
for (const [type, method] of Object.entries(methods)) {
|
50
59
|
if (handlers.has(type)) {
|
51
60
|
throw new MessengerError(`Handler already set for ${type}`);
|
package/distribution/sender.d.ts
CHANGED
@@ -4,7 +4,7 @@ export declare const errorNonExistingTarget = "Could not establish connection. R
|
|
4
4
|
declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type]>(type: Type, options: {
|
5
5
|
isNotification: true;
|
6
6
|
}, target: Target | PageTarget, ...args: Parameters<Method>): void;
|
7
|
-
declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], ReturnValue extends ReturnType<Method
|
7
|
+
declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], ReturnValue extends Promise<ReturnType<Method>>>(type: Type, options: Options, target: Target | PageTarget, ...args: Parameters<Method>): ReturnValue;
|
8
8
|
declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Target | PageTarget): PublicMethodType;
|
9
9
|
declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends PublicMethodWithTarget<Method>>(type: Type): PublicMethodWithDynamicTarget;
|
10
10
|
declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Target | PageTarget): PublicMethodType;
|
package/distribution/sender.js
CHANGED
@@ -7,12 +7,13 @@ export const errorNonExistingTarget = "Could not establish connection. Receiving
|
|
7
7
|
function isMessengerResponse(response) {
|
8
8
|
return isObject(response) && response["__webextMessenger"] === true;
|
9
9
|
}
|
10
|
-
function makeMessage(type, args, target) {
|
10
|
+
function makeMessage(type, args, target, options) {
|
11
11
|
return {
|
12
12
|
__webextMessenger,
|
13
13
|
type,
|
14
14
|
args,
|
15
15
|
target,
|
16
|
+
options,
|
16
17
|
};
|
17
18
|
}
|
18
19
|
// Do not turn this into an `async` function; Notifications must turn `void`
|
@@ -25,20 +26,26 @@ function manageConnection(type, options, sendMessage) {
|
|
25
26
|
});
|
26
27
|
}
|
27
28
|
async function manageMessage(type, sendMessage) {
|
28
|
-
const response = await pRetry(
|
29
|
+
const response = await pRetry(async () => {
|
30
|
+
const response = await sendMessage();
|
31
|
+
if (!isMessengerResponse(response)) {
|
32
|
+
throw new MessengerError(`No handler registered for ${type} in the receiving end`);
|
33
|
+
}
|
34
|
+
return response;
|
35
|
+
}, {
|
29
36
|
minTimeout: 100,
|
30
37
|
factor: 1.3,
|
31
38
|
maxRetryTime: 4000,
|
32
39
|
onFailedAttempt(error) {
|
33
|
-
if (
|
40
|
+
if (error instanceof MessengerError ||
|
41
|
+
String(error.message).startsWith(errorNonExistingTarget)) {
|
42
|
+
debug(type, "will retry. Attempt", error.attemptNumber);
|
43
|
+
}
|
44
|
+
else {
|
34
45
|
throw error;
|
35
46
|
}
|
36
|
-
debug(type, "will retry");
|
37
47
|
},
|
38
48
|
});
|
39
|
-
if (!isMessengerResponse(response)) {
|
40
|
-
throw new MessengerError(`No handler for ${type} was registered in the receiving end`);
|
41
|
-
}
|
42
49
|
if ("error" in response) {
|
43
50
|
debug(type, "↘️ replied with error", response.error);
|
44
51
|
throw deserializeError(response.error);
|
@@ -47,6 +54,7 @@ async function manageMessage(type, sendMessage) {
|
|
47
54
|
return response.value;
|
48
55
|
}
|
49
56
|
function messenger(type, options, target, ...args) {
|
57
|
+
// Message goes to extension page
|
50
58
|
if ("page" in target) {
|
51
59
|
if (target.page === "background" && isBackgroundPage()) {
|
52
60
|
const handler = handlers.get(type);
|
@@ -58,7 +66,7 @@ function messenger(type, options, target, ...args) {
|
|
58
66
|
}
|
59
67
|
const sendMessage = async () => {
|
60
68
|
debug(type, "↗️ sending message to runtime");
|
61
|
-
return browser.runtime.sendMessage(makeMessage(type, args));
|
69
|
+
return browser.runtime.sendMessage(makeMessage(type, args, target, options));
|
62
70
|
};
|
63
71
|
return manageConnection(type, options, sendMessage);
|
64
72
|
}
|
@@ -66,7 +74,7 @@ function messenger(type, options, target, ...args) {
|
|
66
74
|
if (!browser.tabs) {
|
67
75
|
return manageConnection(type, options, async () => {
|
68
76
|
debug(type, "↗️ sending message to runtime");
|
69
|
-
return browser.runtime.sendMessage(makeMessage(type, args, target));
|
77
|
+
return browser.runtime.sendMessage(makeMessage(type, args, target, options));
|
70
78
|
});
|
71
79
|
}
|
72
80
|
// `frameId` must be specified. If missing, the message is sent to every frame
|
@@ -74,7 +82,7 @@ function messenger(type, options, target, ...args) {
|
|
74
82
|
// Message tab directly
|
75
83
|
return manageConnection(type, options, async () => {
|
76
84
|
debug(type, "↗️ sending message to tab", tabId, "frame", frameId);
|
77
|
-
return browser.tabs.sendMessage(tabId, makeMessage(type, args), {
|
85
|
+
return browser.tabs.sendMessage(tabId, makeMessage(type, args, target, options), {
|
78
86
|
frameId,
|
79
87
|
});
|
80
88
|
});
|
package/distribution/shared.d.ts
CHANGED
@@ -1,4 +1,11 @@
|
|
1
|
+
import { JsonObject } from "type-fest";
|
1
2
|
import { Method } from "./types.js";
|
3
|
+
declare type ErrorObject = {
|
4
|
+
name?: string;
|
5
|
+
stack?: string;
|
6
|
+
message?: string;
|
7
|
+
code?: string;
|
8
|
+
} & JsonObject;
|
2
9
|
export declare const __webextMessenger = true;
|
3
10
|
export declare function isObject(value: unknown): value is Record<string, unknown>;
|
4
11
|
export declare class MessengerError extends Error {
|
@@ -7,3 +14,6 @@ export declare class MessengerError extends Error {
|
|
7
14
|
export declare const handlers: Map<string, Method>;
|
8
15
|
export declare const debug: (...args: any[]) => void;
|
9
16
|
export declare const warn: (...args: any[]) => void;
|
17
|
+
export declare function isErrorObject(error: unknown): error is ErrorObject;
|
18
|
+
export declare function delay(milliseconds: number): Promise<void>;
|
19
|
+
export {};
|
package/distribution/shared.js
CHANGED
@@ -17,3 +17,13 @@ export const handlers = new Map();
|
|
17
17
|
// .bind preserves the call location in the console
|
18
18
|
export const debug = console.debug.bind(console, "Messenger:");
|
19
19
|
export const warn = console.warn.bind(console, "Messenger:");
|
20
|
+
export function isErrorObject(error) {
|
21
|
+
var _a;
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a type guard function and it uses ?.
|
23
|
+
return typeof ((_a = error) === null || _a === void 0 ? void 0 : _a.message) === "string";
|
24
|
+
}
|
25
|
+
export async function delay(milliseconds) {
|
26
|
+
return new Promise((resolve) => {
|
27
|
+
setTimeout(resolve, milliseconds);
|
28
|
+
});
|
29
|
+
}
|
@@ -1,6 +1,10 @@
|
|
1
|
-
import {
|
2
|
-
|
3
|
-
|
1
|
+
import { MessengerMeta, Sender } from "./types.js";
|
2
|
+
interface AnyTarget {
|
3
|
+
tabId?: number | "this";
|
4
|
+
frameId?: number;
|
5
|
+
page?: string;
|
6
|
+
}
|
7
|
+
export declare function getActionForMessage(from: Sender, { ...to }: AnyTarget): "respond" | "forward" | "ignore";
|
4
8
|
export declare function nameThisTarget(): Promise<void>;
|
5
9
|
declare function __getTabData(this: MessengerMeta): AnyTarget;
|
6
10
|
declare global {
|
@@ -6,9 +6,10 @@ import { debug } from "./shared.js";
|
|
6
6
|
// This CANNOT be awaited because waiting for it means "I will handle the message."
|
7
7
|
// If a message is received before this is ready, it will just have to be ignored.
|
8
8
|
let thisTarget;
|
9
|
-
//
|
10
|
-
|
11
|
-
|
9
|
+
export function getActionForMessage(from, { ...to } // Clone object because we're editing it
|
10
|
+
) {
|
11
|
+
var _a;
|
12
|
+
if (to.page === "any") {
|
12
13
|
return "respond";
|
13
14
|
}
|
14
15
|
// Content scripts only receive messages that are meant for them. In the future
|
@@ -17,7 +18,7 @@ export function getActionForMessage(target) {
|
|
17
18
|
return "respond";
|
18
19
|
}
|
19
20
|
// We're in an extension page, but the target is not one.
|
20
|
-
if (!
|
21
|
+
if (!to.page) {
|
21
22
|
return "forward";
|
22
23
|
}
|
23
24
|
if (!thisTarget) {
|
@@ -25,18 +26,24 @@ export function getActionForMessage(target) {
|
|
25
26
|
// If this *was* the target, then probably no one else answered
|
26
27
|
return "ignore";
|
27
28
|
}
|
29
|
+
// If requests "this" tab, then set it to allow the next condition
|
30
|
+
if (to.tabId === "this" && thisTarget.tabId === ((_a = from.tab) === null || _a === void 0 ? void 0 : _a.id)) {
|
31
|
+
to.tabId = thisTarget.tabId;
|
32
|
+
}
|
28
33
|
// Every `target` key must match `thisTarget`
|
29
|
-
const isThisTarget = Object.entries(
|
34
|
+
const isThisTarget = Object.entries(to).every(
|
30
35
|
// @ts-expect-error Optional properties
|
31
36
|
([key, value]) => thisTarget[key] === value);
|
32
37
|
if (!isThisTarget) {
|
33
|
-
debug("The message’s target is",
|
38
|
+
debug("The message’s target is", to, "but this is", thisTarget);
|
34
39
|
}
|
35
40
|
return isThisTarget ? "respond" : "ignore";
|
36
41
|
}
|
42
|
+
let nameRequested = false;
|
37
43
|
export async function nameThisTarget() {
|
38
44
|
// Same as above: CS receives messages correctly
|
39
|
-
if (!thisTarget && !isContentScript()) {
|
45
|
+
if (!nameRequested && !thisTarget && !isContentScript()) {
|
46
|
+
nameRequested = true;
|
40
47
|
thisTarget = await messenger("__getTabData", {}, { page: "any" });
|
41
48
|
thisTarget.page = location.pathname;
|
42
49
|
}
|
@@ -46,8 +53,9 @@ function __getTabData() {
|
|
46
53
|
return { tabId: (_b = (_a = this.trace[0]) === null || _a === void 0 ? void 0 : _a.tab) === null || _b === void 0 ? void 0 : _b.id, frameId: (_c = this.trace[0]) === null || _c === void 0 ? void 0 : _c.frameId };
|
47
54
|
}
|
48
55
|
export function initPrivateApi() {
|
56
|
+
// Any context can handler this message
|
57
|
+
registerMethods({ __getTabData });
|
49
58
|
if (isBackgroundPage()) {
|
50
59
|
thisTarget = { page: "background" };
|
51
|
-
registerMethods({ __getTabData });
|
52
60
|
}
|
53
61
|
}
|
package/distribution/types.d.ts
CHANGED
@@ -6,13 +6,13 @@ declare global {
|
|
6
6
|
_: Method;
|
7
7
|
}
|
8
8
|
}
|
9
|
-
declare type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: Target, ...args: PreviousArguments) => TReturnValue : never;
|
9
|
+
declare type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: Target | PageTarget, ...args: PreviousArguments) => TReturnValue : never;
|
10
10
|
declare type ActuallyOmitThisParameter<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : T;
|
11
11
|
/** Removes the `this` type and ensure it's always Promised */
|
12
12
|
export declare type PublicMethod<Method extends ValueOf<MessengerMethods>> = Asyncify<ActuallyOmitThisParameter<Method>>;
|
13
13
|
export declare type PublicMethodWithTarget<Method extends ValueOf<MessengerMethods>> = WithTarget<PublicMethod<Method>>;
|
14
14
|
export interface MessengerMeta {
|
15
|
-
trace:
|
15
|
+
trace: Sender[];
|
16
16
|
}
|
17
17
|
declare type RawMessengerResponse = {
|
18
18
|
value: unknown;
|
@@ -31,15 +31,16 @@ export interface Options {
|
|
31
31
|
* @default false
|
32
32
|
*/
|
33
33
|
isNotification?: boolean;
|
34
|
+
trace?: Sender[];
|
34
35
|
}
|
35
36
|
export declare type Message<LocalArguments extends Arguments = Arguments> = {
|
36
37
|
type: keyof MessengerMethods;
|
37
38
|
args: LocalArguments;
|
38
|
-
|
39
|
-
target?: Target;
|
39
|
+
target: Target | PageTarget;
|
40
40
|
/** If the message is being sent to an intermediary receiver, also set the options */
|
41
|
-
options?:
|
41
|
+
options?: Options;
|
42
42
|
};
|
43
|
+
export declare type Sender = Runtime.MessageSender;
|
43
44
|
export declare type MessengerMessage = Message & {
|
44
45
|
/** Guarantees that a message is meant to be handled by this library */
|
45
46
|
__webextMessenger: true;
|
@@ -49,7 +50,7 @@ export interface Target {
|
|
49
50
|
frameId?: number;
|
50
51
|
}
|
51
52
|
export interface PageTarget {
|
52
|
-
tabId?: number;
|
53
|
+
tabId?: number | "this";
|
53
54
|
page: string;
|
54
55
|
}
|
55
56
|
export {};
|
package/package.json
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
{
|
2
2
|
"name": "webext-messenger",
|
3
|
-
"version": "0.
|
3
|
+
"version": "0.15.0-3",
|
4
4
|
"description": "Browser Extension component messaging framework",
|
5
5
|
"keywords": [],
|
6
6
|
"repository": "pixiebrix/webext-messenger",
|
@@ -13,7 +13,8 @@
|
|
13
13
|
"./source/sender.js": "./source/sender.ts",
|
14
14
|
"./source/receiver.js": "./source/receiver.ts",
|
15
15
|
"./source/types.js": "./source/types.ts",
|
16
|
-
"./source/shared.js": "./source/shared.ts"
|
16
|
+
"./source/shared.js": "./source/shared.ts",
|
17
|
+
"./source/thisTarget.js": "./source/thisTarget.ts"
|
17
18
|
},
|
18
19
|
"scripts": {
|
19
20
|
"build": "tsc",
|
@@ -127,7 +128,8 @@
|
|
127
128
|
"npm-run-all": "^4.1.5",
|
128
129
|
"parcel": "^2.0.1",
|
129
130
|
"tape": "^5.3.2",
|
130
|
-
"typescript": "^4.5.2"
|
131
|
+
"typescript": "^4.5.2",
|
132
|
+
"webext-content-scripts": "^0.10.1"
|
131
133
|
},
|
132
134
|
"targets": {
|
133
135
|
"main": false,
|
package/distribution/api.d.ts
DELETED
@@ -1,10 +0,0 @@
|
|
1
|
-
import { Target, MessengerMeta } from "./types";
|
2
|
-
declare function __getTabData(this: MessengerMeta): Target;
|
3
|
-
declare global {
|
4
|
-
interface MessengerMethods {
|
5
|
-
__getTabData: typeof __getTabData;
|
6
|
-
}
|
7
|
-
}
|
8
|
-
export declare const getTabData: () => Promise<Target>;
|
9
|
-
export declare function initPrivateApi(): void;
|
10
|
-
export {};
|
package/distribution/api.js
DELETED
@@ -1,13 +0,0 @@
|
|
1
|
-
import { getMethod } from "./sender";
|
2
|
-
import { registerMethods } from "./receiver";
|
3
|
-
import { isBackgroundPage } from "webext-detect-page";
|
4
|
-
function __getTabData() {
|
5
|
-
return { tabId: this.trace[0].tab.id, frameId: this.trace[0].frameId };
|
6
|
-
}
|
7
|
-
// First page to respond wins. Any context has this piece of information.
|
8
|
-
export const getTabData = getMethod("__getTabData", { page: "any" });
|
9
|
-
export function initPrivateApi() {
|
10
|
-
if (isBackgroundPage()) {
|
11
|
-
registerMethods({ __getTabData });
|
12
|
-
}
|
13
|
-
}
|
@@ -1,14 +0,0 @@
|
|
1
|
-
/// <reference types="firefox-webext-browser" />
|
2
|
-
import { NamedTarget, Target, MessengerMeta } from "./types.js";
|
3
|
-
declare global {
|
4
|
-
interface MessengerMethods {
|
5
|
-
__webextMessengerTargetRegistration: typeof _registerTarget;
|
6
|
-
}
|
7
|
-
}
|
8
|
-
export declare function resolveNamedTarget(target: NamedTarget, sender?: browser.runtime.MessageSender): Target;
|
9
|
-
export declare const targets: Map<string, Target>;
|
10
|
-
/** Register the current context so that it can be targeted with a name */
|
11
|
-
export declare const registerTarget: (name: string) => Promise<void>;
|
12
|
-
declare function _registerTarget(this: MessengerMeta, name: string): Promise<void>;
|
13
|
-
export declare function initTargets(): void;
|
14
|
-
export {};
|
@@ -1,40 +0,0 @@
|
|
1
|
-
import { isBackgroundPage } from "webext-detect-page";
|
2
|
-
import { errorNonExistingTarget, getMethod } from "./sender.js";
|
3
|
-
import { registerMethods } from "./receiver.js";
|
4
|
-
export function resolveNamedTarget(target, sender) {
|
5
|
-
var _a;
|
6
|
-
if (!isBackgroundPage()) {
|
7
|
-
throw new Error("Named targets can only be resolved in the background page");
|
8
|
-
}
|
9
|
-
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
|
10
|
-
} = target;
|
11
|
-
if (typeof tabId === "undefined") {
|
12
|
-
throw new TypeError(`${errorNonExistingTarget} The tab ID was not specified nor it was automatically determinable.`);
|
13
|
-
}
|
14
|
-
const resolvedTarget = targets.get(`${tabId}%${name}`);
|
15
|
-
if (!resolvedTarget) {
|
16
|
-
throw new Error(`${errorNonExistingTarget} Target named ${name} not registered for tab ${tabId}.`);
|
17
|
-
}
|
18
|
-
return resolvedTarget;
|
19
|
-
}
|
20
|
-
// TODO: Remove targets after tab closes to avoid "memory leaks"
|
21
|
-
export const targets = new Map();
|
22
|
-
/** Register the current context so that it can be targeted with a name */
|
23
|
-
export const registerTarget = getMethod("__webextMessengerTargetRegistration");
|
24
|
-
async function _registerTarget(name) {
|
25
|
-
const sender = this.trace[0];
|
26
|
-
const tabId = sender.tab.id;
|
27
|
-
const { frameId } = sender;
|
28
|
-
targets.set(`${tabId}%${name}`, {
|
29
|
-
tabId,
|
30
|
-
frameId,
|
31
|
-
});
|
32
|
-
console.debug(`Messenger: Target "${name}" registered for tab ${tabId}`);
|
33
|
-
}
|
34
|
-
export function initTargets() {
|
35
|
-
if (isBackgroundPage()) {
|
36
|
-
registerMethods({
|
37
|
-
__webextMessengerTargetRegistration: _registerTarget,
|
38
|
-
});
|
39
|
-
}
|
40
|
-
}
|