webext-messenger 0.24.0 → 0.25.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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": {