webext-messenger 0.24.0 → 0.25.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.
@@ -0,0 +1 @@
1
+ export declare const events: EventTarget;
@@ -0,0 +1 @@
1
+ export const events = new EventTarget();
@@ -1,5 +1,6 @@
1
1
  export * from "./receiver.js";
2
2
  export * from "./sender.js";
3
3
  export * from "./types.js";
4
+ export * from "./events.js";
4
5
  export { getThisFrame, getTopLevelFrame } from "./thisTarget.js";
5
6
  export { toggleLogging } from "./logging.js";
@@ -2,7 +2,10 @@
2
2
  export * from "./receiver.js";
3
3
  export * from "./sender.js";
4
4
  export * from "./types.js";
5
+ export * from "./events.js";
5
6
  export { getThisFrame, getTopLevelFrame } from "./thisTarget.js";
6
7
  export { toggleLogging } from "./logging.js";
7
8
  import { initPrivateApi } from "./thisTarget.js";
9
+ // Required side effect to better track errors:
10
+ // https://github.com/pixiebrix/webext-messenger/pull/80
8
11
  initPrivateApi();
@@ -1,5 +1,5 @@
1
1
  import { type PublicMethod, type PublicMethodWithTarget, type Options, type Target, type PageTarget } from "./types.js";
2
- import { type SetReturnType } from "type-fest";
2
+ import { type Promisable, type SetReturnType } from "type-fest";
3
3
  export declare const errorTargetClosedEarly = "The target was closed before receiving a response";
4
4
  export declare const errorTabDoesntExist = "The tab doesn't exist";
5
5
  export declare const errorTabWasDiscarded = "The tab was discarded";
@@ -7,9 +7,9 @@ declare function messenger<Type extends keyof MessengerMethods, Method extends M
7
7
  isNotification: true;
8
8
  }, target: Target | PageTarget, ...args: Parameters<Method>): void;
9
9
  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;
10
- declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Target | PageTarget): PublicMethodType;
10
+ declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Promisable<Target | PageTarget>): PublicMethodType;
11
11
  declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends PublicMethodWithTarget<Method>>(type: Type): PublicMethodWithDynamicTarget;
12
- declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Target | PageTarget): PublicMethodType;
12
+ declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Promisable<Target | PageTarget>): PublicMethodType;
13
13
  declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends SetReturnType<PublicMethodWithTarget<Method>, void>>(type: Type): PublicMethodWithDynamicTarget;
14
14
  export { messenger, getMethod, getNotifier };
15
15
  export declare const backgroundTarget: PageTarget;
@@ -4,6 +4,7 @@ import { deserializeError } from "serialize-error";
4
4
  import { isObject, MessengerError, __webextMessenger } from "./shared.js";
5
5
  import { log } from "./logging.js";
6
6
  import { handlers } from "./handlers.js";
7
+ import { events } from "./events.js";
7
8
  const _errorNonExistingTarget = "Could not establish connection. Receiving end does not exist.";
8
9
  // https://github.com/mozilla/webextension-polyfill/issues/384
9
10
  const _errorTargetClosedEarly = "A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received";
@@ -16,6 +17,9 @@ function isMessengerResponse(response) {
16
17
  function attemptLog(attemptCount) {
17
18
  return attemptCount > 1 ? `(try: ${attemptCount})` : "";
18
19
  }
20
+ function wasContextInvalidated() {
21
+ return !chrome.runtime?.id;
22
+ }
19
23
  function makeMessage(type, args, target, options) {
20
24
  return {
21
25
  __webextMessenger,
@@ -26,15 +30,16 @@ function makeMessage(type, args, target, options) {
26
30
  };
27
31
  }
28
32
  // Do not turn this into an `async` function; Notifications must turn `void`
29
- function manageConnection(type, { seq, isNotification }, target, sendMessage) {
33
+ function manageConnection(type, { seq, isNotification, retry }, target, sendMessage) {
30
34
  if (!isNotification) {
31
- return manageMessage(type, target, seq, sendMessage);
35
+ return manageMessage(type, target, seq, retry ?? true, sendMessage);
32
36
  }
33
37
  void sendMessage(1).catch((error) => {
34
38
  log.debug(type, seq, "notification failed", { error });
35
39
  });
36
40
  }
37
- async function manageMessage(type, target, seq, sendMessage) {
41
+ async function manageMessage(type, target, seq, retry, sendMessage) {
42
+ // TODO: Split this up a bit because it's too long. Probably drop p-retry
38
43
  const response = await pRetry(async (attemptCount) => {
39
44
  const response = await sendMessage(attemptCount);
40
45
  if (isMessengerResponse(response)) {
@@ -61,8 +66,25 @@ async function manageMessage(type, target, seq, sendMessage) {
61
66
  }, {
62
67
  minTimeout: 100,
63
68
  factor: 1.3,
69
+ // Do not set this to undefined or Infinity, it doesn't work the same way
70
+ ...(retry ? {} : { retries: 0 }),
64
71
  maxRetryTime: 4000,
65
72
  async onFailedAttempt(error) {
73
+ events.dispatchEvent(new CustomEvent("failed-attempt", {
74
+ detail: {
75
+ type,
76
+ seq,
77
+ target,
78
+ error,
79
+ attemptCount: error.attemptNumber,
80
+ },
81
+ }));
82
+ if (wasContextInvalidated()) {
83
+ // The error matches the native context invalidated error
84
+ // *.sendMessage() might fail with a message-specific error that is less useful,
85
+ // like "Sender closed without responding"
86
+ throw new Error("Extension context invalidated.");
87
+ }
66
88
  if (error.message === _errorTargetClosedEarly) {
67
89
  throw new Error(errorTargetClosedEarly);
68
90
  }
@@ -94,6 +116,9 @@ async function manageMessage(type, target, seq, sendMessage) {
94
116
  if (error?.message === _errorNonExistingTarget) {
95
117
  throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
96
118
  }
119
+ events.dispatchEvent(new CustomEvent("attempts-exhausted", {
120
+ detail: { type, seq, target, error },
121
+ }));
97
122
  throw error;
98
123
  });
99
124
  if ("error" in response) {
@@ -141,20 +166,21 @@ function messenger(type, options, target, ...args) {
141
166
  });
142
167
  }
143
168
  function getMethod(type, target) {
144
- if (arguments.length === 1) {
169
+ if (!target) {
145
170
  return messenger.bind(undefined, type, {});
146
171
  }
147
- // @ts-expect-error `bind` types are junk
148
- return messenger.bind(undefined, type, {}, target);
172
+ return (async (...args) => messenger(type, {}, await target, ...args));
149
173
  }
150
174
  function getNotifier(type, target) {
151
175
  const options = { isNotification: true };
152
- if (arguments.length === 1) {
176
+ if (!target) {
153
177
  // @ts-expect-error `bind` types are junk
154
178
  return messenger.bind(undefined, type, options);
155
179
  }
156
- // @ts-expect-error `bind` types are junk
157
- return messenger.bind(undefined, type, options, target);
180
+ return ((...args) => {
181
+ // Async wrapper needed to use `await` while preserving a non-Promise return type
182
+ (async () => messenger(type, options, await target, ...args))();
183
+ });
158
184
  }
159
185
  export { messenger, getMethod, getNotifier };
160
186
  export const backgroundTarget = { page: "background" };
@@ -4,15 +4,7 @@ export function isObject(value) {
4
4
  return typeof value === "object" && value !== null;
5
5
  }
6
6
  export class MessengerError extends Error {
7
- constructor() {
8
- super(...arguments);
9
- Object.defineProperty(this, "name", {
10
- enumerable: true,
11
- configurable: true,
12
- writable: true,
13
- value: "MessengerError"
14
- });
15
- }
7
+ name = "MessengerError";
16
8
  }
17
9
  // @ts-expect-error Wrong `errorConstructors` types
18
10
  errorConstructors.set("MessengerError", MessengerError);
@@ -106,9 +106,7 @@ const storeTabData = once(async () => {
106
106
  }
107
107
  catch (error) {
108
108
  tabDataStatus = "error";
109
- throw new MessengerError("Tab registration failed. This page won’t be able to receive messages that require tab information",
110
- // @ts-expect-error TODO: update lib to accept Error#cause
111
- { cause: error });
109
+ throw new MessengerError("Tab registration failed. This page won’t be able to receive messages that require tab information", { cause: error });
112
110
  }
113
111
  });
114
112
  export function __getTabData() {
@@ -131,6 +129,16 @@ export async function getTopLevelFrame() {
131
129
  };
132
130
  }
133
131
  export function initPrivateApi() {
132
+ // Improve DX by informing the developer that it's being loaded the wrong way
133
+ // https://github.com/pixiebrix/webext-messenger/issues/88
134
+ if (globalThis.__webextMessenger) {
135
+ // TODO: Use Error#cause after https://bugs.chromium.org/p/chromium/issues/detail?id=1211260
136
+ console.log(globalThis.__webextMessenger.replace(/^Error/, "webext-messenger"));
137
+ console.error("webext-messenger: Duplicate execution. This is a fatal error.\nhttps://github.com/pixiebrix/webext-messenger/issues/88");
138
+ return;
139
+ }
140
+ // Use Error to capture the stack and make it easier to find the cause
141
+ globalThis.__webextMessenger = new Error("First execution").stack;
134
142
  if (isExtensionContext()) {
135
143
  // Only `runtime` pages can handle this message but I can't remove it because its listener
136
144
  // also serves the purpose of throwing a specific error when no methods have been registered.
@@ -37,6 +37,7 @@ export interface Options {
37
37
  */
38
38
  isNotification?: boolean;
39
39
  trace?: Sender[];
40
+ retry?: boolean;
40
41
  /** Automatically generated internally */
41
42
  seq?: number;
42
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.24.0",
3
+ "version": "0.25.0",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -25,29 +25,29 @@
25
25
  "watch": "tsc --watch"
26
26
  },
27
27
  "dependencies": {
28
- "p-retry": "^6.0.0",
28
+ "p-retry": "^6.2.0",
29
29
  "serialize-error": "^11.0.2",
30
- "type-fest": "^4.3.1",
31
- "webext-detect-page": "^4.1.1"
30
+ "type-fest": "^4.9.0",
31
+ "webext-detect-page": "^4.2.1"
32
32
  },
33
33
  "devDependencies": {
34
34
  "@parcel/config-webextension": "^2.6.2",
35
- "@sindresorhus/tsconfig": "^4.0.0",
36
- "@types/chrome": "^0.0.245",
37
- "@types/tape": "^5.6.1",
38
- "@types/webextension-polyfill": "^0.10.2",
35
+ "@sindresorhus/tsconfig": "^5.0.0",
36
+ "@types/chrome": "^0.0.254",
37
+ "@types/tape": "^5.6.4",
38
+ "@types/webextension-polyfill": "^0.10.7",
39
39
  "buffer": "^6.0.3",
40
- "eslint": "^8.50.0",
41
- "eslint-config-pixiebrix": "^0.27.2",
40
+ "eslint": "^8.56.0",
41
+ "eslint-config-pixiebrix": "^0.32.0",
42
42
  "events": "^3.3.0",
43
43
  "npm-run-all": "^4.1.5",
44
44
  "parcel": "^2.6.2",
45
45
  "path-browserify": "^1.0.1",
46
46
  "process": "^0.11.10",
47
47
  "stream-browserify": "^3.0.0",
48
- "tape": "^5.7.0",
49
- "typescript": "^5.2.2",
50
- "webext-content-scripts": "^2.5.5",
48
+ "tape": "^5.7.2",
49
+ "typescript": "^5.3.3",
50
+ "webext-content-scripts": "^2.6.0",
51
51
  "webextension-polyfill": "^0.10.0"
52
52
  },
53
53
  "alias": {