webext-messenger 0.34.0 → 0.36.0
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/receiver.js +6 -5
- package/distribution/sender.js +96 -69
- package/package.json +10 -11
package/distribution/receiver.js
CHANGED
|
@@ -42,7 +42,7 @@ function onMessageListener(message, sender, sendResponse) {
|
|
|
42
42
|
return;
|
|
43
43
|
}
|
|
44
44
|
const { type, target, args, options = {} } = message;
|
|
45
|
-
const { trace = [], seq } = options;
|
|
45
|
+
const { trace = [], seq, retry } = options;
|
|
46
46
|
if (action === "forward") {
|
|
47
47
|
log.debug(type, seq, "🔀 forwarded", { sender, target });
|
|
48
48
|
}
|
|
@@ -57,7 +57,7 @@ function onMessageListener(message, sender, sendResponse) {
|
|
|
57
57
|
(async () => {
|
|
58
58
|
try {
|
|
59
59
|
trace.push(sender);
|
|
60
|
-
const value = await prepareResponse(message, action, { trace });
|
|
60
|
+
const value = await prepareResponse(message, action, { trace, retry });
|
|
61
61
|
log.debug(type, seq, "↗️ responding", { value });
|
|
62
62
|
sendResponse({ __webextMessenger, value });
|
|
63
63
|
}
|
|
@@ -87,17 +87,18 @@ function onMessageExternalListener(message, sender, sendResponse) {
|
|
|
87
87
|
});
|
|
88
88
|
}
|
|
89
89
|
/** Generates the value or error to return to the sender; does not include further messaging logic */
|
|
90
|
-
async function prepareResponse(message, action,
|
|
90
|
+
async function prepareResponse(message, action, options) {
|
|
91
91
|
const { type, target, args } = message;
|
|
92
92
|
if (action === "forward") {
|
|
93
|
-
return messenger(type,
|
|
93
|
+
return messenger(type, options, target, ...args);
|
|
94
94
|
}
|
|
95
95
|
const localHandler = handlers.get(type);
|
|
96
96
|
if (localHandler) {
|
|
97
97
|
if ("extensionId" in target && !externalMethods.has(type)) {
|
|
98
98
|
throw new MessengerError(`The ${type} handler is registered in ${getContextName()} for internal use only`);
|
|
99
99
|
}
|
|
100
|
-
|
|
100
|
+
const { trace = [] } = options;
|
|
101
|
+
return localHandler.apply({ trace }, args);
|
|
101
102
|
}
|
|
102
103
|
if (didUserRegisterMethods()) {
|
|
103
104
|
throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
|
package/distribution/sender.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import pRetry from "p-retry";
|
|
2
1
|
import { isBackground, isExtensionContext } from "webext-detect";
|
|
3
2
|
import { deserializeError } from "serialize-error";
|
|
4
3
|
import { isObject, MessengerError, ExtensionNotFoundError, __webextMessenger, } from "./shared.js";
|
|
@@ -20,6 +19,28 @@ function attemptLog(attemptCount) {
|
|
|
20
19
|
function wasContextInvalidated() {
|
|
21
20
|
return !chrome.runtime?.id;
|
|
22
21
|
}
|
|
22
|
+
function getErrorMessage(error) {
|
|
23
|
+
if (error && typeof error === "object" && "message" in error) {
|
|
24
|
+
return String(error.message);
|
|
25
|
+
}
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
function shouldRetryError(error, target) {
|
|
29
|
+
const message = getErrorMessage(error);
|
|
30
|
+
// Don't retry sending to the background page unless it really hasn't loaded yet
|
|
31
|
+
if (target.page !== "background" && error instanceof MessengerError) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
// Page or its content script not yet loaded
|
|
35
|
+
if (message === _errorNonExistingTarget) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// `registerMethods` not yet loaded
|
|
39
|
+
if (message?.startsWith("No handlers registered in ")) {
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
23
44
|
function makeMessage(type, args, target, options) {
|
|
24
45
|
return {
|
|
25
46
|
__webextMessenger,
|
|
@@ -39,102 +60,108 @@ function manageConnection(type, { seq, isNotification, retry }, target, sendMess
|
|
|
39
60
|
});
|
|
40
61
|
}
|
|
41
62
|
async function manageMessage(type, target, seq, retry, sendMessage) {
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
63
|
+
const startTime = Date.now();
|
|
64
|
+
const maxRetryTime = 4000;
|
|
65
|
+
const minTimeout = 100;
|
|
66
|
+
const factor = 1.3;
|
|
67
|
+
// Safety cap to avoid infinite loops, generally stops at 11 with current setting
|
|
68
|
+
// MUST BE UPDATED if maxRetryTime, minTimeout or factor are changed
|
|
69
|
+
const maxRetryCount = 15;
|
|
70
|
+
let attemptCount = 0;
|
|
71
|
+
let currentTimeout = minTimeout;
|
|
72
|
+
while (attemptCount < maxRetryCount) {
|
|
73
|
+
attemptCount++;
|
|
74
|
+
try {
|
|
75
|
+
// eslint-disable-next-line no-await-in-loop -- Necessary for retry logic
|
|
76
|
+
const response = await sendMessage(attemptCount);
|
|
77
|
+
if (isMessengerResponse(response)) {
|
|
78
|
+
if ("error" in response) {
|
|
79
|
+
log.debug(type, seq, "↘️ replied with error", response.error);
|
|
80
|
+
throw deserializeError(response.error);
|
|
81
|
+
}
|
|
82
|
+
log.debug(type, seq, "↘️ replied successfully", response.value);
|
|
83
|
+
return response.value;
|
|
60
84
|
}
|
|
61
|
-
|
|
85
|
+
// If no one answers, `response` will be `undefined`
|
|
86
|
+
// If the target does not have any `onMessage` listener at all, it will throw
|
|
87
|
+
// Possible:
|
|
88
|
+
// - Any target exists and has onMessage handler, but never handled the message
|
|
89
|
+
// - Extension page exists and has Messenger, but never handled the message (Messenger in Runtime ignores messages when the target isn't found)
|
|
90
|
+
// Not possible:
|
|
91
|
+
// - Tab exists and has Messenger, but never handled the message (Messenger in CS always handles messages)
|
|
92
|
+
// - Any target exists, but Messenger didn't have the specific Type handler (The receiving Messenger will throw an error)
|
|
93
|
+
// - No targets exist (the browser immediately throws "Could not establish connection. Receiving end does not exist.")
|
|
94
|
+
if (response === undefined) {
|
|
95
|
+
if ("page" in target) {
|
|
96
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
|
97
|
+
}
|
|
98
|
+
throw new MessengerError(`Messenger was not available in the target ${JSON.stringify(target)} for ${type}`);
|
|
99
|
+
}
|
|
100
|
+
// Possible:
|
|
101
|
+
// - Non-Messenger handler responded
|
|
102
|
+
throw new MessengerError(`Conflict: The message ${type} was handled by a third-party listener`);
|
|
62
103
|
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
throw new MessengerError(`Conflict: The message ${type} was handled by a third-party listener`);
|
|
66
|
-
}, {
|
|
67
|
-
minTimeout: 100,
|
|
68
|
-
factor: 1.3,
|
|
69
|
-
// Do not set this to undefined or Infinity, it doesn't work the same way
|
|
70
|
-
...(retry ? {} : { retries: 0 }),
|
|
71
|
-
maxRetryTime: 4000,
|
|
72
|
-
async onFailedAttempt(error) {
|
|
104
|
+
catch (error) {
|
|
105
|
+
const errorMessage = getErrorMessage(error);
|
|
73
106
|
events.dispatchEvent(new CustomEvent("failed-attempt", {
|
|
74
107
|
detail: {
|
|
75
108
|
type,
|
|
76
109
|
seq,
|
|
77
110
|
target,
|
|
78
111
|
error,
|
|
79
|
-
attemptCount
|
|
112
|
+
attemptCount,
|
|
80
113
|
},
|
|
81
114
|
}));
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
// The extension is not available and it will not be. Do not retry.
|
|
115
|
+
// Check for non-retryable errors
|
|
116
|
+
if ("extensionId" in target && errorMessage === _errorNonExistingTarget) {
|
|
85
117
|
throw new ExtensionNotFoundError(errorExtensionNotFound.replace("$ID", target.extensionId));
|
|
86
118
|
}
|
|
87
119
|
if (isExtensionContext() && wasContextInvalidated()) {
|
|
88
|
-
// The error matches the native context invalidated error
|
|
89
|
-
// *.sendMessage() might fail with a message-specific error that is less useful,
|
|
90
|
-
// like "Sender closed without responding"
|
|
91
120
|
throw new Error("Extension context invalidated.");
|
|
92
121
|
}
|
|
93
|
-
if (
|
|
122
|
+
if (errorMessage === _errorTargetClosedEarly) {
|
|
94
123
|
throw new Error(errorTargetClosedEarly);
|
|
95
124
|
}
|
|
96
|
-
if (!(
|
|
97
|
-
// If NONE of these conditions is true, stop retrying
|
|
98
|
-
// Don't retry sending to the background page unless it really hasn't loaded yet
|
|
99
|
-
((target.page !== "background" &&
|
|
100
|
-
error instanceof MessengerError) ||
|
|
101
|
-
// Page or its content script not yet loaded
|
|
102
|
-
error.message === _errorNonExistingTarget ||
|
|
103
|
-
// `registerMethods` not yet loaded
|
|
104
|
-
String(error.message).startsWith("No handlers registered in ")))) {
|
|
125
|
+
if (!shouldRetryError(error, target)) {
|
|
105
126
|
throw error;
|
|
106
127
|
}
|
|
128
|
+
// Check if tab is still valid
|
|
107
129
|
if (chrome.tabs && typeof target.tabId === "number") {
|
|
130
|
+
let tabInfo;
|
|
108
131
|
try {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
throw new Error(errorTabWasDiscarded);
|
|
112
|
-
}
|
|
132
|
+
// eslint-disable-next-line no-await-in-loop -- Necessary to check tab status during retry
|
|
133
|
+
tabInfo = await chrome.tabs.get(target.tabId);
|
|
113
134
|
}
|
|
114
135
|
catch {
|
|
115
136
|
throw new Error(errorTabDoesntExist);
|
|
116
137
|
}
|
|
138
|
+
if (tabInfo.discarded) {
|
|
139
|
+
throw new Error(errorTabWasDiscarded);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
// Check if we should stop retrying
|
|
143
|
+
const elapsedTime = Date.now() - startTime;
|
|
144
|
+
if (!retry || elapsedTime >= maxRetryTime) {
|
|
145
|
+
if (errorMessage === _errorNonExistingTarget) {
|
|
146
|
+
throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
|
|
147
|
+
}
|
|
148
|
+
events.dispatchEvent(new CustomEvent("attempts-exhausted", {
|
|
149
|
+
detail: { type, seq, target, error },
|
|
150
|
+
}));
|
|
151
|
+
throw error;
|
|
117
152
|
}
|
|
118
|
-
log.debug(type, seq, "will retry
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
153
|
+
log.debug(type, seq, "will retry", attemptLog(attemptCount));
|
|
154
|
+
// Wait before retrying with exponential backoff
|
|
155
|
+
const waitTime = currentTimeout;
|
|
156
|
+
// eslint-disable-next-line no-await-in-loop -- Necessary for retry delay
|
|
157
|
+
await new Promise((resolve) => {
|
|
158
|
+
setTimeout(resolve, waitTime);
|
|
159
|
+
});
|
|
160
|
+
currentTimeout = Math.floor(currentTimeout * factor);
|
|
126
161
|
}
|
|
127
|
-
events.dispatchEvent(new CustomEvent("attempts-exhausted", {
|
|
128
|
-
detail: { type, seq, target, error },
|
|
129
|
-
}));
|
|
130
|
-
throw error;
|
|
131
|
-
});
|
|
132
|
-
if ("error" in response) {
|
|
133
|
-
log.debug(type, seq, "↘️ replied with error", response.error);
|
|
134
|
-
throw deserializeError(response.error);
|
|
135
162
|
}
|
|
136
|
-
|
|
137
|
-
|
|
163
|
+
// If you reach this, refer to note above `maxRetryCount` definition
|
|
164
|
+
throw new MessengerError("Exceeded maximum retry attempts. This suggests a low `maxRetryCount`");
|
|
138
165
|
}
|
|
139
166
|
// Not a UID nor a truly global sequence. Signal / console noise compromise.
|
|
140
167
|
// The time part is a pseudo-random number between 0 and 99 that helps visually
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webext-messenger",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.36.0",
|
|
4
4
|
"description": "Browser Extension component messaging framework",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"repository": "pixiebrix/webext-messenger",
|
|
@@ -27,32 +27,31 @@
|
|
|
27
27
|
"watch": "tsc --watch"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"one-event": "^4.
|
|
31
|
-
"
|
|
32
|
-
"
|
|
33
|
-
"type-fest": "^5.0.1",
|
|
30
|
+
"one-event": "^4.4.0",
|
|
31
|
+
"serialize-error": "^13.0.1",
|
|
32
|
+
"type-fest": "^5.7.0",
|
|
34
33
|
"webext-detect": "^5.3.2"
|
|
35
34
|
},
|
|
36
35
|
"@parcel/resolver-default": {
|
|
37
36
|
"packageExports": true
|
|
38
37
|
},
|
|
39
38
|
"devDependencies": {
|
|
40
|
-
"@parcel/config-webextension": "^2.
|
|
41
|
-
"@sindresorhus/tsconfig": "^8.0
|
|
42
|
-
"@types/chrome": "^0.1
|
|
39
|
+
"@parcel/config-webextension": "^2.16.3",
|
|
40
|
+
"@sindresorhus/tsconfig": "^8.1.0",
|
|
41
|
+
"@types/chrome": "^0.2.1",
|
|
43
42
|
"@types/tape": "^5.8.1",
|
|
44
43
|
"buffer": "^6.0.3",
|
|
45
44
|
"eslint": "^8.57.0",
|
|
46
45
|
"eslint-config-pixiebrix": "^0.41.1",
|
|
47
46
|
"events": "^3.3.0",
|
|
48
47
|
"npm-run-all": "^4.1.5",
|
|
49
|
-
"parcel": "^2.
|
|
48
|
+
"parcel": "^2.16.0",
|
|
50
49
|
"path-browserify": "^1.0.1",
|
|
51
50
|
"process": "^0.11.10",
|
|
52
51
|
"stream-browserify": "^3.0.0",
|
|
53
|
-
"tape": "^5.
|
|
52
|
+
"tape": "^5.10.2",
|
|
54
53
|
"typescript": "^5.9.3",
|
|
55
|
-
"vitest": "^
|
|
54
|
+
"vitest": "^4.1.9"
|
|
56
55
|
},
|
|
57
56
|
"targets": {
|
|
58
57
|
"main": false,
|