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.
@@ -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, meta) {
90
+ async function prepareResponse(message, action, options) {
91
91
  const { type, target, args } = message;
92
92
  if (action === "forward") {
93
- return messenger(type, meta, target, ...args);
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
- return localHandler.apply(meta, args);
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()}`);
@@ -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
- // TODO: Split this up a bit because it's too long. Probably drop p-retry
43
- const response = await pRetry(async (attemptCount) => {
44
- const response = await sendMessage(attemptCount);
45
- if (isMessengerResponse(response)) {
46
- return response;
47
- }
48
- // If no one answers, `response` will be `undefined`
49
- // If the target does not have any `onMessage` listener at all, it will throw
50
- // Possible:
51
- // - Any target exists and has onMessage handler, but never handled the message
52
- // - Extension page exists and has Messenger, but never handled the message (Messenger in Runtime ignores messages when the target isn't found)
53
- // Not possible:
54
- // - Tab exists and has Messenger, but never handled the message (Messenger in CS always handles messages)
55
- // - Any target exists, but Messenger didn't have the specific Type handler (The receiving Messenger will throw an error)
56
- // - No targets exist (the browser immediately throws "Could not establish connection. Receiving end does not exist.")
57
- if (response === undefined) {
58
- if ("page" in target) {
59
- throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
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
- throw new MessengerError(`Messenger was not available in the target ${JSON.stringify(target)} for ${type}`);
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
- // Possible:
64
- // - Non-Messenger handler responded
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: error.attemptNumber,
112
+ attemptCount,
80
113
  },
81
114
  }));
82
- if ("extensionId" in target &&
83
- error.message === _errorNonExistingTarget) {
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 (error.message === _errorTargetClosedEarly) {
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
- const tabInfo = await chrome.tabs.get(target.tabId);
110
- if (tabInfo.discarded) {
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. Attempt", error.attemptNumber);
119
- },
120
- }).catch((error) => {
121
- if (error &&
122
- typeof error === "object" &&
123
- "message" in error &&
124
- error?.message === _errorNonExistingTarget) {
125
- throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
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
- log.debug(type, seq, "↘️ replied successfully", response.value);
137
- return response.value;
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.34.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.3.0",
31
- "p-retry": "^6.2.1",
32
- "serialize-error": "^12.0.0",
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.15.4",
41
- "@sindresorhus/tsconfig": "^8.0.1",
42
- "@types/chrome": "^0.1.16",
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.15.4",
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.9.0",
52
+ "tape": "^5.10.2",
54
53
  "typescript": "^5.9.3",
55
- "vitest": "^3.2.4"
54
+ "vitest": "^4.1.9"
56
55
  },
57
56
  "targets": {
58
57
  "main": false,