webext-messenger 0.22.0 โ†’ 0.23.1

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