webext-messenger 0.20.0 → 0.21.0
Sign up to get free protection for your applications and to get access to all the features.
- package/distribution/index.js +1 -5
- package/distribution/receiver.js +8 -9
- package/distribution/sender.js +27 -5
- package/distribution/shared.js +3 -0
- package/distribution/thisTarget.d.ts +2 -3
- package/distribution/thisTarget.js +65 -21
- package/distribution/types.d.ts +3 -1
- package/package.json +1 -1
- package/readme.md +4 -0
package/distribution/index.js
CHANGED
@@ -1,8 +1,4 @@
|
|
1
|
-
// Imports must use the .js extension because
|
2
|
-
// This works in TS even if the .js doesn't exist, but it breaks Parcel (the tests builder)
|
3
|
-
// For this reason, there's an `alias` field in package.json to redirect these imports.
|
4
|
-
// If you see "@parcel/resolver-default: Cannot load file './yourNewFile.js'" you need to add it to the `alias` list
|
5
|
-
// 🥲
|
1
|
+
// Imports must use the .js extension because ESM requires it and TS refuses to rewrite .ts to .js
|
6
2
|
export * from "./receiver.js";
|
7
3
|
export * from "./sender.js";
|
8
4
|
export * from "./types.js";
|
package/distribution/receiver.js
CHANGED
@@ -1,8 +1,8 @@
|
|
1
1
|
import { serializeError } from "serialize-error";
|
2
|
-
import { getContextName
|
2
|
+
import { getContextName } from "webext-detect-page";
|
3
3
|
import { messenger } from "./sender.js";
|
4
4
|
import { isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
|
5
|
-
import { getActionForMessage
|
5
|
+
import { getActionForMessage } from "./thisTarget.js";
|
6
6
|
import { didUserRegisterMethods, handlers } from "./handlers.js";
|
7
7
|
export function isMessengerMessage(message) {
|
8
8
|
return (isObject(message) &&
|
@@ -17,7 +17,7 @@ function onMessageListener(message, sender) {
|
|
17
17
|
return;
|
18
18
|
}
|
19
19
|
// Target check must be synchronous (`await` means we're handling the message)
|
20
|
-
const action = getActionForMessage(sender, message
|
20
|
+
const action = getActionForMessage(sender, message);
|
21
21
|
if (action === "ignore") {
|
22
22
|
return;
|
23
23
|
}
|
@@ -43,11 +43,13 @@ action) {
|
|
43
43
|
args,
|
44
44
|
wasForwarded: trace.length > 1,
|
45
45
|
});
|
46
|
-
if (!didUserRegisterMethods()) {
|
47
|
-
throw new MessengerError(`No handlers registered in ${getContextName()}`);
|
48
|
-
}
|
49
46
|
const localHandler = handlers.get(type);
|
50
47
|
if (!localHandler) {
|
48
|
+
if (!didUserRegisterMethods()) {
|
49
|
+
// TODO: Test the handling of __getTabData in contexts that have no registered methods
|
50
|
+
// https://github.com/pixiebrix/webext-messenger/pull/82
|
51
|
+
throw new MessengerError(`No handlers registered in ${getContextName()}`);
|
52
|
+
}
|
51
53
|
throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
|
52
54
|
}
|
53
55
|
handleMessage = async () => localHandler.apply(meta, args);
|
@@ -61,9 +63,6 @@ action) {
|
|
61
63
|
return { ...response, __webextMessenger };
|
62
64
|
}
|
63
65
|
export function registerMethods(methods) {
|
64
|
-
if (!isBackground()) {
|
65
|
-
void nameThisTarget();
|
66
|
-
}
|
67
66
|
for (const [type, method] of Object.entries(methods)) {
|
68
67
|
if (handlers.has(type)) {
|
69
68
|
throw new MessengerError(`Handler already set for ${type}`);
|
package/distribution/sender.js
CHANGED
@@ -33,10 +33,27 @@ function manageConnection(type, options, target, sendMessage) {
|
|
33
33
|
async function manageMessage(type, target, sendMessage) {
|
34
34
|
const response = await pRetry(async () => {
|
35
35
|
const response = await sendMessage();
|
36
|
-
if (
|
37
|
-
|
36
|
+
if (isMessengerResponse(response)) {
|
37
|
+
return response;
|
38
38
|
}
|
39
|
-
|
39
|
+
// If no one answers, `response` will be `undefined`
|
40
|
+
// If the target does not have any `onMessage` listener at all, it will throw
|
41
|
+
// Possible:
|
42
|
+
// - Any target exists and has onMessage handler, but never handled the message
|
43
|
+
// - Extension page exists and has Messenger, but never handled the message (Messenger in Runtime ignores messages when the target isn't found)
|
44
|
+
// Not possible:
|
45
|
+
// - Tab exists and has Messenger, but never handled the message (Messenger in CS always handles messages)
|
46
|
+
// - Any target exists, but Messenger didn't have the specific Type handler (The receiving Messenger will throw an error)
|
47
|
+
// - No targets exist (the browser immediately throws "Could not establish connection. Receiving end does not exist.")
|
48
|
+
if (response === undefined) {
|
49
|
+
if ("page" in target) {
|
50
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
51
|
+
}
|
52
|
+
throw new MessengerError(`Messenger was not available in the target ${JSON.stringify(target)} for ${type}`);
|
53
|
+
}
|
54
|
+
// Possible:
|
55
|
+
// - Non-Messenger handler responded
|
56
|
+
throw new MessengerError(`Conflict: The message ${type} was handled by a third-party listener`);
|
40
57
|
}, {
|
41
58
|
minTimeout: 100,
|
42
59
|
factor: 1.3,
|
@@ -49,7 +66,7 @@ async function manageMessage(type, target, sendMessage) {
|
|
49
66
|
// Don't retry sending to the background page unless it really hasn't loaded yet
|
50
67
|
(target.page !== "background" && error instanceof MessengerError) ||
|
51
68
|
// Page or its content script not yet loaded
|
52
|
-
|
69
|
+
error.message === _errorNonExistingTarget ||
|
53
70
|
// `registerMethods` not yet loaded
|
54
71
|
String(error.message).startsWith("No handlers registered in ")) {
|
55
72
|
if (browser.tabs &&
|
@@ -63,6 +80,11 @@ async function manageMessage(type, target, sendMessage) {
|
|
63
80
|
throw error;
|
64
81
|
}
|
65
82
|
},
|
83
|
+
}).catch((error) => {
|
84
|
+
if (error?.message === _errorNonExistingTarget) {
|
85
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
86
|
+
}
|
87
|
+
throw error;
|
66
88
|
});
|
67
89
|
if ("error" in response) {
|
68
90
|
debug(type, "↘️ replied with error", response.error);
|
@@ -80,7 +102,7 @@ function messenger(type, options, target, ...args) {
|
|
80
102
|
warn(type, "is being handled locally");
|
81
103
|
return handler.apply({ trace: [] }, args);
|
82
104
|
}
|
83
|
-
throw new MessengerError("No handler registered for " + type);
|
105
|
+
throw new MessengerError("No handler registered locally for " + type);
|
84
106
|
}
|
85
107
|
const sendMessage = async () => {
|
86
108
|
debug(type, "↗️ sending message to runtime");
|
package/distribution/shared.js
CHANGED
@@ -1,3 +1,4 @@
|
|
1
|
+
import { errorConstructors } from "serialize-error";
|
1
2
|
const logging = (() => {
|
2
3
|
try {
|
3
4
|
// @ts-expect-error it would break Webpack
|
@@ -25,6 +26,8 @@ export class MessengerError extends Error {
|
|
25
26
|
});
|
26
27
|
}
|
27
28
|
}
|
29
|
+
// @ts-expect-error Wrong `errorConstructors` types
|
30
|
+
errorConstructors.set("MessengerError", MessengerError);
|
28
31
|
// .bind preserves the call location in the console
|
29
32
|
export const debug = logging ? console.debug.bind(console, "Messenger:") : noop;
|
30
33
|
export const warn = logging ? console.warn.bind(console, "Messenger:") : noop;
|
@@ -1,5 +1,4 @@
|
|
1
|
-
import { AnyTarget, MessengerMeta, Sender } from "./types.js";
|
2
|
-
export declare function getActionForMessage(from: Sender,
|
3
|
-
export declare function nameThisTarget(): Promise<void>;
|
1
|
+
import { AnyTarget, Message, MessengerMeta, Sender } from "./types.js";
|
2
|
+
export declare function getActionForMessage(from: Sender, message: Message): "respond" | "forward" | "ignore";
|
4
3
|
export declare function __getTabData(this: MessengerMeta): AnyTarget;
|
5
4
|
export declare function initPrivateApi(): void;
|
@@ -1,11 +1,44 @@
|
|
1
1
|
import { isBackground, isContentScript, isExtensionContext, } from "webext-detect-page";
|
2
2
|
import { messenger } from "./sender.js";
|
3
3
|
import { registerMethods } from "./receiver.js";
|
4
|
-
import { debug } from "./shared.js";
|
4
|
+
import { debug, MessengerError } from "./shared.js";
|
5
|
+
/**
|
6
|
+
* @file This file exists because `runtime.sendMessage` acts as a broadcast to
|
7
|
+
* all open extension pages, so the receiver needs a way to figure out if the
|
8
|
+
* message was intended for them.
|
9
|
+
*
|
10
|
+
* If the requested target only includes a `page` (URL), then it can be determined
|
11
|
+
* immediately. If the target also specifies a tab, like `{tabId: 1, page: '/sidebar.html'}`,
|
12
|
+
* then the receiving target needs to fetch the tab information via `__getTabData`.
|
13
|
+
*
|
14
|
+
* `__getTabData` is called automatically when `webext-messenger` is imported in
|
15
|
+
* a context that requires this logic (most extension:// pages).
|
16
|
+
*
|
17
|
+
* If a broadcast message with `tabId` target is received before `__getTabData` is "received",
|
18
|
+
* the message will be ignored and it can be retried. If `__getTabData` somehow fails,
|
19
|
+
* the target will forever ignore any messages that require the `tabId`. In that case,
|
20
|
+
* an error would be thrown once and will be visible in the console, uncaught.
|
21
|
+
*
|
22
|
+
* Content scripts do not use this logic at all at the moment because they're
|
23
|
+
* always targeted via `tabId/frameId` combo and `tabs.sendMessage`.
|
24
|
+
*/
|
5
25
|
// Soft warning: Race conditions are possible.
|
6
26
|
// This CANNOT be awaited because waiting for it means "I will handle the message."
|
7
27
|
// If a message is received before this is ready, it will just have to be ignored.
|
8
|
-
|
28
|
+
const thisTarget = isBackground()
|
29
|
+
? { page: "background" }
|
30
|
+
: {
|
31
|
+
get page() {
|
32
|
+
return location.pathname + location.search;
|
33
|
+
},
|
34
|
+
};
|
35
|
+
let tabDataStatus =
|
36
|
+
// The background page doesn't have a tab
|
37
|
+
isBackground() ||
|
38
|
+
// Content scripts don't use named targets yet
|
39
|
+
isContentScript()
|
40
|
+
? "not-needed"
|
41
|
+
: "needed";
|
9
42
|
function compareTargets(to, thisTarget) {
|
10
43
|
for (const [key, value] of Object.entries(to)) {
|
11
44
|
if (thisTarget[key] === value) {
|
@@ -27,8 +60,10 @@ function compareTargets(to, thisTarget) {
|
|
27
60
|
}
|
28
61
|
return true;
|
29
62
|
}
|
30
|
-
|
31
|
-
) {
|
63
|
+
// TODO: Test this in Jest, outside the browser
|
64
|
+
export function getActionForMessage(from, message) {
|
65
|
+
// Clone object because we're editing it
|
66
|
+
const to = { ...message.target };
|
32
67
|
if (to.page === "any") {
|
33
68
|
return "respond";
|
34
69
|
}
|
@@ -41,11 +76,6 @@ export function getActionForMessage(from, { ...to } // Clone object because we'r
|
|
41
76
|
if (!to.page) {
|
42
77
|
return "forward";
|
43
78
|
}
|
44
|
-
if (!thisTarget) {
|
45
|
-
console.warn("A message was received before this context was ready");
|
46
|
-
// If this *was* the target, then probably no one else answered
|
47
|
-
return "ignore";
|
48
|
-
}
|
49
79
|
// Set "this" tab to the current tabId
|
50
80
|
if (to.tabId === "this" && thisTarget.tabId === from.tab?.id) {
|
51
81
|
to.tabId = thisTarget.tabId;
|
@@ -53,28 +83,42 @@ export function getActionForMessage(from, { ...to } // Clone object because we'r
|
|
53
83
|
// Every `target` key must match `thisTarget`
|
54
84
|
const isThisTarget = compareTargets(to, thisTarget);
|
55
85
|
if (!isThisTarget) {
|
56
|
-
debug("
|
86
|
+
debug(message.type, "🤫 ignored due to target mismatch", {
|
87
|
+
requestedTarget: to,
|
88
|
+
thisTarget,
|
89
|
+
tabDataStatus,
|
90
|
+
});
|
57
91
|
}
|
58
92
|
return isThisTarget ? "respond" : "ignore";
|
59
93
|
}
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
thisTarget
|
94
|
+
async function storeTabData() {
|
95
|
+
if (tabDataStatus !== "needed") {
|
96
|
+
return;
|
97
|
+
}
|
98
|
+
try {
|
99
|
+
tabDataStatus = "pending";
|
100
|
+
Object.assign(thisTarget, {
|
101
|
+
...(await messenger("__getTabData", {}, { page: "any" })),
|
102
|
+
});
|
103
|
+
tabDataStatus = "received";
|
104
|
+
}
|
105
|
+
catch (error) {
|
106
|
+
tabDataStatus = "error";
|
107
|
+
throw new MessengerError("Tab registration failed. This page won’t be able to receive messages that require tab information",
|
108
|
+
// @ts-expect-error TODO: update lib to accept Error#cause
|
109
|
+
{ cause: error });
|
67
110
|
}
|
68
111
|
}
|
69
112
|
export function __getTabData() {
|
70
113
|
return { tabId: this.trace[0]?.tab?.id, frameId: this.trace[0]?.frameId };
|
71
114
|
}
|
72
115
|
export function initPrivateApi() {
|
73
|
-
if (isBackground()) {
|
74
|
-
thisTarget = { page: "background" };
|
75
|
-
}
|
76
116
|
if (isExtensionContext()) {
|
77
|
-
//
|
117
|
+
// Only `runtime` pages can handle this message but I can't remove it because its listener
|
118
|
+
// also serves the purpose of throwing a specific error when no methods have been registered.
|
119
|
+
// https://github.com/pixiebrix/webext-messenger/pull/80
|
78
120
|
registerMethods({ __getTabData });
|
121
|
+
// `getTabInformation` includes per-context exclusion logic
|
122
|
+
void storeTabData();
|
79
123
|
}
|
80
124
|
}
|
package/distribution/types.d.ts
CHANGED
@@ -40,7 +40,9 @@ export declare type Message<LocalArguments extends Arguments = Arguments> = {
|
|
40
40
|
/** If the message is being sent to an intermediary receiver, also set the options */
|
41
41
|
options?: Options;
|
42
42
|
};
|
43
|
-
export declare type Sender = Runtime.MessageSender
|
43
|
+
export declare type Sender = Runtime.MessageSender & {
|
44
|
+
origin?: string;
|
45
|
+
};
|
44
46
|
export declare type MessengerMessage = Message & {
|
45
47
|
/** Guarantees that a message is meant to be handled by this library */
|
46
48
|
__webextMessenger: true;
|
package/package.json
CHANGED
package/readme.md
CHANGED
@@ -18,3 +18,7 @@ import messenger from "webext-messenger";
|
|
18
18
|
## Context
|
19
19
|
|
20
20
|
- [Initial considerations for this library](https://github.com/pixiebrix/webext-messenger/issues/1)
|
21
|
+
|
22
|
+
## npm publishing
|
23
|
+
|
24
|
+
Collaborators can publish a new version of what's on main [via "workflow_dispatch"](https://github.blog/changelog/2020-07-06-github-actions-manual-triggers-with-workflow_dispatch/) under [Actions » Publish](https://github.com/pixiebrix/webext-messenger/actions/workflows/npm-publish.yml)
|