webext-messenger 0.20.1 → 0.21.0-0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,8 +1,8 @@
1
1
  import { serializeError } from "serialize-error";
2
- import { getContextName, isBackground } from "webext-detect-page";
2
+ import { getContextName } from "webext-detect-page";
3
3
  import { messenger } from "./sender.js";
4
4
  import { isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
5
- import { getActionForMessage, nameThisTarget } from "./thisTarget.js";
5
+ import { getActionForMessage } from "./thisTarget.js";
6
6
  import { didUserRegisterMethods, handlers } from "./handlers.js";
7
7
  export function isMessengerMessage(message) {
8
8
  return (isObject(message) &&
@@ -17,7 +17,7 @@ function onMessageListener(message, sender) {
17
17
  return;
18
18
  }
19
19
  // Target check must be synchronous (`await` means we're handling the message)
20
- const action = getActionForMessage(sender, message.target);
20
+ const action = getActionForMessage(sender, message);
21
21
  if (action === "ignore") {
22
22
  return;
23
23
  }
@@ -63,9 +63,6 @@ action) {
63
63
  return { ...response, __webextMessenger };
64
64
  }
65
65
  export function registerMethods(methods) {
66
- if (!isBackground()) {
67
- void nameThisTarget();
68
- }
69
66
  for (const [type, method] of Object.entries(methods)) {
70
67
  if (handlers.has(type)) {
71
68
  throw new MessengerError(`Handler already set for ${type}`);
@@ -33,10 +33,27 @@ function manageConnection(type, options, target, sendMessage) {
33
33
  async function manageMessage(type, target, sendMessage) {
34
34
  const response = await pRetry(async () => {
35
35
  const response = await sendMessage();
36
- if (!isMessengerResponse(response)) {
37
- throw new MessengerError(`No handler registered for ${type} in the receiving end`);
36
+ if (isMessengerResponse(response)) {
37
+ return response;
38
38
  }
39
- return response;
39
+ // If no one answers, `response` will be `undefined`
40
+ // If the target does not have any `onMessage` listener at all, it will throw
41
+ // Possible:
42
+ // - Any target exists and has onMessage handler, but never handled the message
43
+ // - Extension page exists and has Messenger, but never handled the message (Messenger in Runtime ignores messages when the target isn't found)
44
+ // Not possible:
45
+ // - Tab exists and has Messenger, but never handled the message (Messenger in CS always handles messages)
46
+ // - Any target exists, but Messenger didn't have the specific Type handler (The receiving Messenger will throw an error)
47
+ // - No targets exist (the browser immediately throws "Could not establish connection. Receiving end does not exist.")
48
+ if (response === undefined) {
49
+ if ("page" in target) {
50
+ throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
51
+ }
52
+ throw new MessengerError(`Messenger was not available in the target ${JSON.stringify(target)} for ${type}`);
53
+ }
54
+ // Possible:
55
+ // - Non-Messenger handler responded
56
+ throw new MessengerError(`Conflict: The message ${type} was handled by a third-party listener`);
40
57
  }, {
41
58
  minTimeout: 100,
42
59
  factor: 1.3,
@@ -49,7 +66,7 @@ async function manageMessage(type, target, sendMessage) {
49
66
  // Don't retry sending to the background page unless it really hasn't loaded yet
50
67
  (target.page !== "background" && error instanceof MessengerError) ||
51
68
  // Page or its content script not yet loaded
52
- String(error.message).startsWith(_errorNonExistingTarget) ||
69
+ error.message === _errorNonExistingTarget ||
53
70
  // `registerMethods` not yet loaded
54
71
  String(error.message).startsWith("No handlers registered in ")) {
55
72
  if (browser.tabs &&
@@ -63,6 +80,11 @@ async function manageMessage(type, target, sendMessage) {
63
80
  throw error;
64
81
  }
65
82
  },
83
+ }).catch((error) => {
84
+ if (error?.message === _errorNonExistingTarget) {
85
+ throw new MessengerError(`The target ${JSON.stringify(target)} for ${type} was not found`);
86
+ }
87
+ throw error;
66
88
  });
67
89
  if ("error" in response) {
68
90
  debug(type, "↘️ replied with error", response.error);
@@ -80,7 +102,7 @@ function messenger(type, options, target, ...args) {
80
102
  warn(type, "is being handled locally");
81
103
  return handler.apply({ trace: [] }, args);
82
104
  }
83
- throw new MessengerError("No handler registered for " + type);
105
+ throw new MessengerError("No handler registered locally for " + type);
84
106
  }
85
107
  const sendMessage = async () => {
86
108
  debug(type, "↗️ sending message to runtime");
@@ -1,3 +1,4 @@
1
+ import { errorConstructors } from "serialize-error";
1
2
  const logging = (() => {
2
3
  try {
3
4
  // @ts-expect-error it would break Webpack
@@ -25,6 +26,8 @@ export class MessengerError extends Error {
25
26
  });
26
27
  }
27
28
  }
29
+ // @ts-expect-error Wrong `errorConstructors` types
30
+ errorConstructors.set("MessengerError", MessengerError);
28
31
  // .bind preserves the call location in the console
29
32
  export const debug = logging ? console.debug.bind(console, "Messenger:") : noop;
30
33
  export const warn = logging ? console.warn.bind(console, "Messenger:") : noop;
@@ -1,5 +1,4 @@
1
- import { AnyTarget, MessengerMeta, Sender } from "./types.js";
2
- export declare function getActionForMessage(from: Sender, { ...to }: AnyTarget): "respond" | "forward" | "ignore";
3
- export declare function nameThisTarget(): Promise<void>;
1
+ import { AnyTarget, Message, MessengerMeta, Sender } from "./types.js";
2
+ export declare function getActionForMessage(from: Sender, message: Message): "respond" | "forward" | "ignore";
4
3
  export declare function __getTabData(this: MessengerMeta): AnyTarget;
5
4
  export declare function initPrivateApi(): void;
@@ -1,13 +1,44 @@
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 } from "./shared.js";
4
+ import { debug, MessengerError } from "./shared.js";
5
+ /**
6
+ * @file This file exists because `runtime.sendMessage` acts as a broadcast to
7
+ * all open extension pages, so the receiver needs a way to figure out if the
8
+ * message was intended for them.
9
+ *
10
+ * If the requested target only includes a `page` (URL), then it can be determined
11
+ * immediately. If the target also specifies a tab, like `{tabId: 1, page: '/sidebar.html'}`,
12
+ * then the receiving target needs to fetch the tab information via `__getTabData`.
13
+ *
14
+ * `__getTabData` is called automatically when `webext-messenger` is imported in
15
+ * a context that requires this logic (most extension:// pages).
16
+ *
17
+ * If a broadcast message with `tabId` target is received before `__getTabData` is "received",
18
+ * the message will be ignored and it can be retried. If `__getTabData` somehow fails,
19
+ * the target will forever ignore any messages that require the `tabId`. In that case,
20
+ * an error would be thrown once and will be visible in the console, uncaught.
21
+ *
22
+ * Content scripts do not use this logic at all at the moment because they're
23
+ * always targeted via `tabId/frameId` combo and `tabs.sendMessage`.
24
+ */
5
25
  // Soft warning: Race conditions are possible.
6
26
  // This CANNOT be awaited because waiting for it means "I will handle the message."
7
27
  // If a message is received before this is ready, it will just have to be ignored.
8
- let thisTarget = isBackground()
28
+ const thisTarget = isBackground()
9
29
  ? { page: "background" }
10
- : undefined;
30
+ : {
31
+ get page() {
32
+ return location.pathname + location.search;
33
+ },
34
+ };
35
+ let tabDataStatus =
36
+ // The background page doesn't have a tab
37
+ isBackground() ||
38
+ // Content scripts don't use named targets yet
39
+ isContentScript()
40
+ ? "not-needed"
41
+ : "needed";
11
42
  function compareTargets(to, thisTarget) {
12
43
  for (const [key, value] of Object.entries(to)) {
13
44
  if (thisTarget[key] === value) {
@@ -29,8 +60,10 @@ function compareTargets(to, thisTarget) {
29
60
  }
30
61
  return true;
31
62
  }
32
- export function getActionForMessage(from, { ...to } // Clone object because we're editing it
33
- ) {
63
+ // TODO: Test this in Jest, outside the browser
64
+ export function getActionForMessage(from, message) {
65
+ // Clone object because we're editing it
66
+ const to = { ...message.target };
34
67
  if (to.page === "any") {
35
68
  return "respond";
36
69
  }
@@ -43,11 +76,6 @@ export function getActionForMessage(from, { ...to } // Clone object because we'r
43
76
  if (!to.page) {
44
77
  return "forward";
45
78
  }
46
- if (!thisTarget) {
47
- console.warn("A message was received before this context was ready");
48
- // If this *was* the target, then probably no one else answered
49
- return "ignore";
50
- }
51
79
  // Set "this" tab to the current tabId
52
80
  if (to.tabId === "this" && thisTarget.tabId === from.tab?.id) {
53
81
  to.tabId = thisTarget.tabId;
@@ -55,17 +83,30 @@ export function getActionForMessage(from, { ...to } // Clone object because we'r
55
83
  // Every `target` key must match `thisTarget`
56
84
  const isThisTarget = compareTargets(to, thisTarget);
57
85
  if (!isThisTarget) {
58
- debug("The message’s target is", to, "but this is", thisTarget);
86
+ debug(message.type, "🤫 ignored due to target mismatch", {
87
+ requestedTarget: to,
88
+ thisTarget,
89
+ tabDataStatus,
90
+ });
59
91
  }
60
92
  return isThisTarget ? "respond" : "ignore";
61
93
  }
62
- let nameRequested = false;
63
- export async function nameThisTarget() {
64
- // Same as above: CS receives messages correctly
65
- if (!nameRequested && !thisTarget && !isContentScript()) {
66
- nameRequested = true;
67
- thisTarget = await messenger("__getTabData", {}, { page: "any" });
68
- thisTarget.page = location.pathname + location.search;
94
+ async function storeTabData() {
95
+ if (tabDataStatus !== "needed") {
96
+ return;
97
+ }
98
+ try {
99
+ tabDataStatus = "pending";
100
+ Object.assign(thisTarget, {
101
+ ...(await messenger("__getTabData", {}, { page: "any" })),
102
+ });
103
+ tabDataStatus = "received";
104
+ }
105
+ catch (error) {
106
+ tabDataStatus = "error";
107
+ throw new MessengerError("Tab registration failed. This page won’t be able to receive messages that require tab information",
108
+ // @ts-expect-error TODO: update lib to accept Error#cause
109
+ { cause: error });
69
110
  }
70
111
  }
71
112
  export function __getTabData() {
@@ -77,5 +118,7 @@ export function initPrivateApi() {
77
118
  // also serves the purpose of throwing a specific error when no methods have been registered.
78
119
  // https://github.com/pixiebrix/webext-messenger/pull/80
79
120
  registerMethods({ __getTabData });
121
+ // `getTabInformation` includes per-context exclusion logic
122
+ void storeTabData();
80
123
  }
81
124
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.20.1",
3
+ "version": "0.21.0-0",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",