webext-messenger 0.22.0 โ†’ 0.23.1

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.
@@ -2,3 +2,4 @@ export * from "./receiver.js";
2
2
  export * from "./sender.js";
3
3
  export * from "./types.js";
4
4
  export { getThisFrame, getTopLevelFrame } from "./thisTarget.js";
5
+ export { toggleLogging } from "./shared.js";
@@ -3,5 +3,6 @@ export * from "./receiver.js";
3
3
  export * from "./sender.js";
4
4
  export * from "./types.js";
5
5
  export { getThisFrame, getTopLevelFrame } from "./thisTarget.js";
6
+ export { toggleLogging } from "./shared.js";
6
7
  import { initPrivateApi } from "./thisTarget.js";
7
8
  initPrivateApi();
@@ -1,3 +1,5 @@
1
- import { type Message } from "./types.js";
1
+ import { type Message, type MessengerMeta } from "./types.js";
2
2
  export declare function isMessengerMessage(message: unknown): message is Message;
3
3
  export declare function registerMethods(methods: Partial<MessengerMethods>): void;
4
+ /** Ensure/document that the current function was called via Messenger */
5
+ export declare function assertMessengerCall(_this: MessengerMeta): asserts _this is MessengerMeta;
@@ -1,7 +1,7 @@
1
1
  import { serializeError } from "serialize-error";
2
2
  import { getContextName } from "webext-detect-page";
3
3
  import { messenger } from "./sender.js";
4
- import { isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
4
+ import { isObject, MessengerError, log, __webextMessenger } from "./shared.js";
5
5
  import { getActionForMessage } from "./thisTarget.js";
6
6
  import { didUserRegisterMethods, handlers } from "./handlers.js";
7
7
  export function isMessengerMessage(message) {
@@ -29,16 +29,16 @@ async function handleMessage(message, sender,
29
29
  // Once messages reach this function they cannot be "ignored", they're already being handled
30
30
  action) {
31
31
  const { type, target, args, options = {} } = message;
32
- const { trace = [] } = options;
32
+ const { trace = [], seq } = options;
33
33
  trace.push(sender);
34
34
  const meta = { trace };
35
35
  let handleMessage;
36
36
  if (action === "forward") {
37
- debug(type, "๐Ÿ”€ forwarded", { sender, target });
37
+ log.debug(type, seq, "๐Ÿ”€ forwarded", { sender, target });
38
38
  handleMessage = async () => messenger(type, meta, target, ...args);
39
39
  }
40
40
  else {
41
- debug(type, "โ†˜๏ธ received in", getContextName(), {
41
+ log.debug(type, seq, "โ†˜๏ธ received in", getContextName(), {
42
42
  sender,
43
43
  args,
44
44
  wasForwarded: trace.length > 1,
@@ -59,7 +59,7 @@ action) {
59
59
  // and https://github.com/mozilla/webextension-polyfill/issues/210
60
60
  error: serializeError(error),
61
61
  }));
62
- debug(type, "โ†—๏ธ responding", response);
62
+ log.debug(type, seq, "โ†—๏ธ responding", response);
63
63
  return { ...response, __webextMessenger };
64
64
  }
65
65
  export function registerMethods(methods) {
@@ -67,8 +67,12 @@ export function registerMethods(methods) {
67
67
  if (handlers.has(type)) {
68
68
  throw new MessengerError(`Handler already set for ${type}`);
69
69
  }
70
- debug("Registered", type);
70
+ log.debug("Registered", type);
71
71
  handlers.set(type, method);
72
72
  }
73
73
  browser.runtime.onMessage.addListener(onMessageListener);
74
74
  }
75
+ /** Ensure/document that the current function was called via Messenger */
76
+ export function assertMessengerCall(_this
77
+ // eslint-disable-next-line @typescript-eslint/no-empty-function -- TypeScript already does this, it's a documentation-only call
78
+ ) { }
@@ -2,6 +2,7 @@ import { type PublicMethod, type PublicMethodWithTarget, type Options, type Targ
2
2
  import { 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
+ export declare const errorTabWasDiscarded = "The tab was discarded";
5
6
  declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type]>(type: Type, options: {
6
7
  isNotification: true;
7
8
  }, target: Target | PageTarget, ...args: Parameters<Method>): void;
@@ -1,17 +1,20 @@
1
1
  import pRetry from "p-retry";
2
2
  import { isBackground } from "webext-detect-page";
3
- import { doesTabExist } from "webext-tools";
4
3
  import { deserializeError } from "serialize-error";
5
- import { isObject, MessengerError, __webextMessenger, debug, warn, } from "./shared.js";
4
+ import { isObject, MessengerError, __webextMessenger, log } from "./shared.js";
6
5
  import { handlers } from "./handlers.js";
7
6
  const _errorNonExistingTarget = "Could not establish connection. Receiving end does not exist.";
8
7
  // https://github.com/mozilla/webextension-polyfill/issues/384
9
8
  const _errorTargetClosedEarly = "A listener indicated an asynchronous response by returning true, but the message channel closed before a response was received";
10
9
  export const errorTargetClosedEarly = "The target was closed before receiving a response";
11
10
  export const errorTabDoesntExist = "The tab doesn't exist";
11
+ export const errorTabWasDiscarded = "The tab was discarded";
12
12
  function isMessengerResponse(response) {
13
13
  return isObject(response) && response["__webextMessenger"] === true;
14
14
  }
15
+ function attemptLog(attemptCount) {
16
+ return attemptCount > 1 ? `(try: ${attemptCount})` : "";
17
+ }
15
18
  function makeMessage(type, args, target, options) {
16
19
  return {
17
20
  __webextMessenger,
@@ -22,17 +25,17 @@ function makeMessage(type, args, target, options) {
22
25
  };
23
26
  }
24
27
  // Do not turn this into an `async` function; Notifications must turn `void`
25
- function manageConnection(type, options, target, sendMessage) {
26
- if (!options.isNotification) {
27
- return manageMessage(type, target, sendMessage);
28
+ function manageConnection(type, { seq, isNotification }, target, sendMessage) {
29
+ if (!isNotification) {
30
+ return manageMessage(type, target, seq, sendMessage);
28
31
  }
29
- void sendMessage().catch((error) => {
30
- debug(type, "notification failed", { error });
32
+ void sendMessage(1).catch((error) => {
33
+ log.debug(type, seq, "notification failed", { error });
31
34
  });
32
35
  }
33
- async function manageMessage(type, target, sendMessage) {
34
- const response = await pRetry(async () => {
35
- const response = await sendMessage();
36
+ async function manageMessage(type, target, seq, sendMessage) {
37
+ const response = await pRetry(async (attemptCount) => {
38
+ const response = await sendMessage(attemptCount);
36
39
  if (isMessengerResponse(response)) {
37
40
  return response;
38
41
  }
@@ -62,23 +65,29 @@ async function manageMessage(type, target, sendMessage) {
62
65
  if (error.message === _errorTargetClosedEarly) {
63
66
  throw new Error(errorTargetClosedEarly);
64
67
  }
65
- if (
68
+ if (!(
69
+ // If NONE of these conditions is true, stop retrying
66
70
  // Don't retry sending to the background page unless it really hasn't loaded yet
67
- (target.page !== "background" && error instanceof MessengerError) ||
71
+ ((target.page !== "background" &&
72
+ error instanceof MessengerError) ||
68
73
  // Page or its content script not yet loaded
69
74
  error.message === _errorNonExistingTarget ||
70
75
  // `registerMethods` not yet loaded
71
- String(error.message).startsWith("No handlers registered in ")) {
72
- if (browser.tabs &&
73
- typeof target.tabId === "number" &&
74
- !(await doesTabExist(target.tabId))) {
76
+ String(error.message).startsWith("No handlers registered in ")))) {
77
+ throw error;
78
+ }
79
+ if (browser.tabs && typeof target.tabId === "number") {
80
+ try {
81
+ const tabInfo = await browser.tabs.get(target.tabId);
82
+ if (tabInfo.discarded) {
83
+ throw new Error(errorTabWasDiscarded);
84
+ }
85
+ }
86
+ catch {
75
87
  throw new Error(errorTabDoesntExist);
76
88
  }
77
- debug(type, "will retry. Attempt", error.attemptNumber);
78
- }
79
- else {
80
- throw error;
81
89
  }
90
+ log.debug(type, seq, "will retry. Attempt", error.attemptNumber);
82
91
  },
83
92
  }).catch((error) => {
84
93
  if (error?.message === _errorNonExistingTarget) {
@@ -87,41 +96,44 @@ async function manageMessage(type, target, sendMessage) {
87
96
  throw error;
88
97
  });
89
98
  if ("error" in response) {
90
- debug(type, "โ†˜๏ธ replied with error", response.error);
99
+ log.debug(type, seq, "โ†˜๏ธ replied with error", response.error);
91
100
  throw deserializeError(response.error);
92
101
  }
93
- debug(type, "โ†˜๏ธ replied successfully", response.value);
102
+ log.debug(type, seq, "โ†˜๏ธ replied successfully", response.value);
94
103
  return response.value;
95
104
  }
96
105
  function messenger(type, options, target, ...args) {
106
+ // Not a UID. Signal / console noise compromise. They repeat every 100 seconds
107
+ options.seq = Date.now() % 100000;
108
+ const { seq } = options;
97
109
  // Message goes to extension page
98
110
  if ("page" in target) {
99
111
  if (target.page === "background" && isBackground()) {
100
112
  const handler = handlers.get(type);
101
113
  if (handler) {
102
- warn(type, "is being handled locally");
114
+ log.warn(type, seq, "is being handled locally");
103
115
  return handler.apply({ trace: [] }, args);
104
116
  }
105
117
  throw new MessengerError("No handler registered locally for " + type);
106
118
  }
107
- const sendMessage = async () => {
108
- debug(type, "โ†—๏ธ sending message to runtime");
119
+ const sendMessage = async (attemptCount) => {
120
+ log.debug(type, seq, "โ†—๏ธ sending message to runtime", attemptLog(attemptCount));
109
121
  return browser.runtime.sendMessage(makeMessage(type, args, target, options));
110
122
  };
111
123
  return manageConnection(type, options, target, sendMessage);
112
124
  }
113
125
  // Contexts without direct Tab access must go through background
114
126
  if (!browser.tabs) {
115
- return manageConnection(type, options, target, async () => {
116
- debug(type, "โ†—๏ธ sending message to runtime");
127
+ return manageConnection(type, options, target, async (attemptCount) => {
128
+ log.debug(type, seq, "โ†—๏ธ sending message to runtime", attemptLog(attemptCount));
117
129
  return browser.runtime.sendMessage(makeMessage(type, args, target, options));
118
130
  });
119
131
  }
120
132
  // `frameId` must be specified. If missing, the message is sent to every frame
121
133
  const { tabId, frameId = 0 } = target;
122
134
  // Message tab directly
123
- return manageConnection(type, options, target, async () => {
124
- debug(type, "โ†—๏ธ sending message to tab", tabId, "frame", frameId);
135
+ return manageConnection(type, options, target, async (attemptCount) => {
136
+ log.debug(type, seq, "โ†—๏ธ sending message to tab", tabId, "frame", frameId, attemptLog(attemptCount));
125
137
  return browser.tabs.sendMessage(tabId, makeMessage(type, args, target, options), {
126
138
  frameId,
127
139
  });
@@ -10,8 +10,11 @@ export declare function isObject(value: unknown): value is Record<string, unknow
10
10
  export declare class MessengerError extends Error {
11
11
  name: string;
12
12
  }
13
- export declare const debug: (...args: any[]) => void;
14
- export declare const warn: (...args: any[]) => void;
13
+ export declare const log: {
14
+ debug: (...args: any[]) => void;
15
+ warn: (...args: any[]) => void;
16
+ };
17
+ export declare function toggleLogging(enabled: boolean): void;
15
18
  export declare function isErrorObject(error: unknown): error is ErrorObject;
16
19
  export declare function delay(milliseconds: number): Promise<void>;
17
20
  export declare function once<Callback extends (...arguments_: unknown[]) => unknown>(function_: Callback): Callback;
@@ -1,20 +1,11 @@
1
1
  import { errorConstructors } from "serialize-error";
2
- const logging = (() => {
3
- try {
4
- // @ts-expect-error it would break Webpack
5
- return process.env.WEBEXT_MESSENGER_LOGGING === "true";
6
- }
7
- catch {
8
- return false;
9
- }
10
- })();
11
- function noop() {
12
- /* */
13
- }
14
2
  export const __webextMessenger = true;
15
3
  export function isObject(value) {
16
4
  return typeof value === "object" && value !== null;
17
5
  }
6
+ function noop() {
7
+ /* */
8
+ }
18
9
  export class MessengerError extends Error {
19
10
  constructor() {
20
11
  super(...arguments);
@@ -29,8 +20,15 @@ export class MessengerError extends Error {
29
20
  // @ts-expect-error Wrong `errorConstructors` types
30
21
  errorConstructors.set("MessengerError", MessengerError);
31
22
  // .bind preserves the call location in the console
32
- export const debug = logging ? console.debug.bind(console, "Messenger:") : noop;
33
- export const warn = logging ? console.warn.bind(console, "Messenger:") : noop;
23
+ const debug = console.debug.bind(console, "Messenger:");
24
+ const warn = console.warn.bind(console, "Messenger:");
25
+ export const log = { debug, warn };
26
+ export function toggleLogging(enabled) {
27
+ log.debug = enabled ? debug : noop;
28
+ log.warn = enabled ? warn : noop;
29
+ }
30
+ // Default to "no logs"
31
+ toggleLogging(false);
34
32
  export function isErrorObject(error) {
35
33
  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a type guard function and it uses ?.
36
34
  return typeof error?.message === "string";
@@ -1,7 +1,7 @@
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, MessengerError, once } from "./shared.js";
4
+ import { log, MessengerError, once } from "./shared.js";
5
5
  /**
6
6
  * @file This file exists because `runtime.sendMessage` acts as a broadcast to
7
7
  * all open extension pages, so the receiver needs a way to figure out if the
@@ -84,7 +84,7 @@ export function getActionForMessage(from, message) {
84
84
  // Every `target` key must match `thisTarget`
85
85
  const isThisTarget = compareTargets(to, thisTarget);
86
86
  if (!isThisTarget) {
87
- debug(message.type, "๐Ÿคซ ignored due to target mismatch", {
87
+ log.debug(message.type, "๐Ÿคซ ignored due to target mismatch", {
88
88
  requestedTarget: to,
89
89
  thisTarget,
90
90
  tabDataStatus,
@@ -28,7 +28,7 @@ export type MessengerResponse = RawMessengerResponse & {
28
28
  /** Guarantees that the message was handled by this library */
29
29
  __webextMessenger: true;
30
30
  };
31
- type Arguments = any[];
31
+ type Arguments = unknown[];
32
32
  export type Method = (this: MessengerMeta, ...args: Arguments) => Promise<unknown>;
33
33
  export interface Options {
34
34
  /**
@@ -37,6 +37,8 @@ export interface Options {
37
37
  */
38
38
  isNotification?: boolean;
39
39
  trace?: Sender[];
40
+ /** Automatically generated internally */
41
+ seq?: number;
40
42
  }
41
43
  export type Message<LocalArguments extends Arguments = Arguments> = {
42
44
  type: keyof MessengerMethods;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.22.0",
3
+ "version": "0.23.1",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -10,7 +10,7 @@
10
10
  "main": "distribution/index.js",
11
11
  "scripts": {
12
12
  "build": "tsc",
13
- "demo:watch": "WEBEXT_MESSENGER_LOGGING=true parcel watch --no-cache --no-hmr",
13
+ "demo:watch": "parcel watch --no-cache --no-hmr",
14
14
  "demo:build": "parcel build --no-cache",
15
15
  "prepack": "tsc --sourceMap false",
16
16
  "test": "eslint . && tsc --noEmit",
@@ -19,30 +19,29 @@
19
19
  "watch": "tsc --watch"
20
20
  },
21
21
  "dependencies": {
22
- "p-retry": "^5.1.1",
23
- "serialize-error": "^11.0.0",
24
- "type-fest": "^3.2.0",
25
- "webext-detect-page": "^4.0.1",
26
- "webext-tools": "^1.1.0"
22
+ "p-retry": "^6.0.0",
23
+ "serialize-error": "^11.0.2",
24
+ "type-fest": "^4.3.1",
25
+ "webext-detect-page": "^4.1.1"
27
26
  },
28
27
  "devDependencies": {
29
- "@parcel/config-webextension": "^2.8.0",
30
- "@sindresorhus/tsconfig": "^3.0.1",
31
- "@types/chrome": "^0.0.203",
32
- "@types/tape": "^4.13.2",
33
- "@types/webextension-polyfill": "^0.9.0",
28
+ "@parcel/config-webextension": "^2.6.2",
29
+ "@sindresorhus/tsconfig": "^4.0.0",
30
+ "@types/chrome": "^0.0.245",
31
+ "@types/tape": "^5.6.1",
32
+ "@types/webextension-polyfill": "^0.10.2",
34
33
  "buffer": "^6.0.3",
35
- "eslint": "^8.28.0",
36
- "eslint-config-pixiebrix": "^0.20.0",
34
+ "eslint": "^8.50.0",
35
+ "eslint-config-pixiebrix": "^0.27.2",
37
36
  "events": "^3.3.0",
38
37
  "npm-run-all": "^4.1.5",
39
- "parcel": "^2.6.0",
38
+ "parcel": "^2.6.2",
40
39
  "path-browserify": "^1.0.1",
41
40
  "process": "^0.11.10",
42
41
  "stream-browserify": "^3.0.0",
43
- "tape": "^5.5.3",
44
- "typescript": "^4.9.3",
45
- "webext-content-scripts": "^1.0.2",
42
+ "tape": "^5.7.0",
43
+ "typescript": "^5.2.2",
44
+ "webext-content-scripts": "^2.5.5",
46
45
  "webextension-polyfill": "^0.10.0"
47
46
  },
48
47
  "alias": {
package/readme.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [badge-gzip]: https://img.shields.io/bundlephobia/minzip/webext-messenger.svg?label=gzipped
4
4
  [link-bundlephobia]: https://bundlephobia.com/result?p=webext-messenger
5
5
 
6
- > WIP: Browser Extension component messaging framework
6
+ > Browser Extension component messaging framework
7
7
 
8
8
  ## Install
9
9
 
@@ -17,4 +17,9 @@ import messenger from "webext-messenger";
17
17
 
18
18
  ## Context
19
19
 
20
- - [Initial considerations for this library](https://github.com/pixiebrix/webext-messenger/issues/1)
20
+ - [Initial considertions for this library](https://github.com/pixiebrix/webext-messenger/issues/1)
21
+
22
+
23
+ ## npm publishing
24
+
25
+ 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)