webext-messenger 0.14.0 → 0.15.0-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,10 @@
1
+ import { Target, MessengerMeta } from "./types";
2
+ declare function __getTabData(this: MessengerMeta): Target;
3
+ declare global {
4
+ interface MessengerMethods {
5
+ __getTabData: typeof __getTabData;
6
+ }
7
+ }
8
+ export declare const getTabData: () => Promise<Target>;
9
+ export declare function initPrivateApi(): void;
10
+ export {};
@@ -0,0 +1,13 @@
1
+ import { getMethod } from "./sender";
2
+ import { registerMethods } from "./receiver";
3
+ import { isBackgroundPage } from "webext-detect-page";
4
+ function __getTabData() {
5
+ return { tabId: this.trace[0].tab.id, frameId: this.trace[0].frameId };
6
+ }
7
+ // First page to respond wins. Any context has this piece of information.
8
+ export const getTabData = getMethod("__getTabData", { page: "any" });
9
+ export function initPrivateApi() {
10
+ if (isBackgroundPage()) {
11
+ registerMethods({ __getTabData });
12
+ }
13
+ }
@@ -1,3 +1,3 @@
1
- export { registerMethods } from "./receiver.js";
2
- export { messenger, getMethod, getNotifier, backgroundTarget, } from "./sender.js";
3
- export { MessengerMeta, Target } from "./types.js";
1
+ export * from "./receiver.js";
2
+ export * from "./sender.js";
3
+ export * from "./types.js";
@@ -1,2 +1,5 @@
1
- export { registerMethods } from "./receiver.js";
2
- export { messenger, getMethod, getNotifier, backgroundTarget, } from "./sender.js";
1
+ export * from "./receiver.js";
2
+ export * from "./sender.js";
3
+ export * from "./types.js";
4
+ import { initPrivateApi } from "./thisTarget";
5
+ initPrivateApi();
@@ -1,7 +1,9 @@
1
1
  import browser from "webextension-polyfill";
2
2
  import { serializeError } from "serialize-error";
3
3
  import { messenger } from "./sender.js";
4
- import { handlers, isObject, MessengerError, debug, warn, __webextMessenger, } from "./shared.js";
4
+ import { handlers, isObject, MessengerError, debug, __webextMessenger, } from "./shared.js";
5
+ import { getContextName } from "webext-detect-page";
6
+ import { getActionForMessage, nameThisTarget } from "./thisTarget";
5
7
  export function isMessengerMessage(message) {
6
8
  return (isObject(message) &&
7
9
  typeof message["type"] === "string" &&
@@ -14,21 +16,27 @@ function onMessageListener(message, sender) {
14
16
  // TODO: Add test for this eventuality: ignore unrelated messages
15
17
  return;
16
18
  }
17
- const { type, target, args } = message;
19
+ // Target check must be synchronous (`await` means we're handing the message)
20
+ const action = getActionForMessage(message.target);
21
+ if (action === "ignore") {
22
+ return;
23
+ }
24
+ return handleMessage(message, sender, action);
25
+ }
26
+ // This function can only be called when the message *will* be handled locally.
27
+ // Returning "undefined" or throwing an error will still handle it.
28
+ async function handleMessage(message, sender, action) {
29
+ const { type, target, args, options: { trace } = {} } = message;
18
30
  debug(type, "↘️ received", { sender, args });
19
31
  let handleMessage;
20
- if (target) {
21
- if (!browser.tabs) {
22
- throw new MessengerError(`Message ${type} sent to wrong context, it can't be forwarded to ${JSON.stringify(target)}`);
23
- }
32
+ if (action === "forward") {
24
33
  debug(type, "🔀 forwarded", { sender, target });
25
- handleMessage = async () => messenger(type, {}, target, ...args);
34
+ handleMessage = async () => messenger(type, { trace }, target, ...args);
26
35
  }
27
36
  else {
28
37
  const localHandler = handlers.get(type);
29
38
  if (!localHandler) {
30
- warn(type, "⚠️ ignored, can't be handled here");
31
- return;
39
+ throw new MessengerError(`No handler registered for ${type} in ${getContextName()}`);
32
40
  }
33
41
  debug(type, "➡️ will be handled here");
34
42
  const meta = { trace: [sender] };
@@ -46,6 +54,7 @@ function onMessageListener(message, sender) {
46
54
  });
47
55
  }
48
56
  export function registerMethods(methods) {
57
+ void nameThisTarget();
49
58
  for (const [type, method] of Object.entries(methods)) {
50
59
  if (handlers.has(type)) {
51
60
  throw new MessengerError(`Handler already set for ${type}`);
@@ -53,10 +62,5 @@ export function registerMethods(methods) {
53
62
  console.debug("Messenger: Registered", type);
54
63
  handlers.set(type, method);
55
64
  }
56
- if ("browser" in globalThis) {
57
- browser.runtime.onMessage.addListener(onMessageListener);
58
- }
59
- else {
60
- throw new Error("`webext-messenger` requires `webextension");
61
- }
65
+ browser.runtime.onMessage.addListener(onMessageListener);
62
66
  }
@@ -4,7 +4,7 @@ export declare const errorNonExistingTarget = "Could not establish connection. R
4
4
  declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type]>(type: Type, options: {
5
5
  isNotification: true;
6
6
  }, target: Target | PageTarget, ...args: Parameters<Method>): void;
7
- declare function messenger<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], ReturnValue extends ReturnType<Method>>(type: Type, options: Options, target: Target | PageTarget, ...args: Parameters<Method>): ReturnValue;
7
+ 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;
8
8
  declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends PublicMethod<Method>>(type: Type, target: Target | PageTarget): PublicMethodType;
9
9
  declare function getMethod<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodWithDynamicTarget extends PublicMethodWithTarget<Method>>(type: Type): PublicMethodWithDynamicTarget;
10
10
  declare function getNotifier<Type extends keyof MessengerMethods, Method extends MessengerMethods[Type], PublicMethodType extends SetReturnType<PublicMethod<Method>, void>>(type: Type, target: Target | PageTarget): PublicMethodType;
@@ -7,12 +7,13 @@ export const errorNonExistingTarget = "Could not establish connection. Receiving
7
7
  function isMessengerResponse(response) {
8
8
  return isObject(response) && response["__webextMessenger"] === true;
9
9
  }
10
- function makeMessage(type, args, target) {
10
+ function makeMessage(type, args, target, options) {
11
11
  return {
12
12
  __webextMessenger,
13
13
  type,
14
14
  args,
15
15
  target,
16
+ options,
16
17
  };
17
18
  }
18
19
  // Do not turn this into an `async` function; Notifications must turn `void`
@@ -30,14 +31,14 @@ async function manageMessage(type, sendMessage) {
30
31
  factor: 1.3,
31
32
  maxRetryTime: 4000,
32
33
  onFailedAttempt(error) {
33
- if (!String(error === null || error === void 0 ? void 0 : error.message).startsWith(errorNonExistingTarget)) {
34
+ if (!String(error.message).startsWith(errorNonExistingTarget)) {
34
35
  throw error;
35
36
  }
36
- debug(type, "will retry");
37
+ debug(type, "will retry. Attempt", error.attemptNumber);
37
38
  },
38
39
  });
39
40
  if (!isMessengerResponse(response)) {
40
- throw new MessengerError(`No handler for ${type} was registered in the receiving end`);
41
+ throw new MessengerError(`No handler registered for ${type} in the receiving end`);
41
42
  }
42
43
  if ("error" in response) {
43
44
  debug(type, "↘️ replied with error", response.error);
@@ -47,6 +48,7 @@ async function manageMessage(type, sendMessage) {
47
48
  return response.value;
48
49
  }
49
50
  function messenger(type, options, target, ...args) {
51
+ // Message goes to extension page
50
52
  if ("page" in target) {
51
53
  if (target.page === "background" && isBackgroundPage()) {
52
54
  const handler = handlers.get(type);
@@ -58,7 +60,7 @@ function messenger(type, options, target, ...args) {
58
60
  }
59
61
  const sendMessage = async () => {
60
62
  debug(type, "↗️ sending message to runtime");
61
- return browser.runtime.sendMessage(makeMessage(type, args));
63
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
62
64
  };
63
65
  return manageConnection(type, options, sendMessage);
64
66
  }
@@ -66,7 +68,7 @@ function messenger(type, options, target, ...args) {
66
68
  if (!browser.tabs) {
67
69
  return manageConnection(type, options, async () => {
68
70
  debug(type, "↗️ sending message to runtime");
69
- return browser.runtime.sendMessage(makeMessage(type, args, target));
71
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
70
72
  });
71
73
  }
72
74
  // `frameId` must be specified. If missing, the message is sent to every frame
@@ -74,7 +76,7 @@ function messenger(type, options, target, ...args) {
74
76
  // Message tab directly
75
77
  return manageConnection(type, options, async () => {
76
78
  debug(type, "↗️ sending message to tab", tabId, "frame", frameId);
77
- return browser.tabs.sendMessage(tabId, makeMessage(type, args), {
79
+ return browser.tabs.sendMessage(tabId, makeMessage(type, args, target, options), {
78
80
  frameId,
79
81
  });
80
82
  });
@@ -0,0 +1,12 @@
1
+ import { Target, PageTarget, MessengerMeta } from "./types.js";
2
+ declare type AnyTarget = Partial<Target & PageTarget>;
3
+ export declare function getActionForMessage(target: AnyTarget): "respond" | "forward" | "ignore";
4
+ export declare function nameThisTarget(): Promise<void>;
5
+ declare function __getTabData(this: MessengerMeta): AnyTarget;
6
+ declare global {
7
+ interface MessengerMethods {
8
+ __getTabData: typeof __getTabData;
9
+ }
10
+ }
11
+ export declare function initPrivateApi(): void;
12
+ export {};
@@ -0,0 +1,53 @@
1
+ import { isBackgroundPage, isContentScript } from "webext-detect-page";
2
+ import { messenger } from "./index";
3
+ import { registerMethods } from "./receiver.js";
4
+ import { debug } from "./shared.js";
5
+ // Soft warning: Race conditions are possible.
6
+ // This CANNOT be awaited because waiting for it means "I will handle the message."
7
+ // If a message is received before this is ready, it will just have to be ignored.
8
+ let thisTarget;
9
+ //
10
+ export function getActionForMessage(target) {
11
+ if (target.page === "any") {
12
+ return "respond";
13
+ }
14
+ // Content scripts only receive messages that are meant for them. In the future
15
+ // they'll also forward them, but that still means they need to be handled here.
16
+ if (isContentScript()) {
17
+ return "respond";
18
+ }
19
+ // We're in an extension page, but the target is not one.
20
+ if (!("page" in target)) {
21
+ return "forward";
22
+ }
23
+ if (!thisTarget) {
24
+ console.warn("A message was received before this context was ready");
25
+ // If this *was* the target, then probably no one else answered
26
+ return "ignore";
27
+ }
28
+ // Every `target` key must match `thisTarget`
29
+ const isThisTarget = Object.entries(target).every(
30
+ // @ts-expect-error Optional properties
31
+ ([key, value]) => thisTarget[key] === value);
32
+ if (!isThisTarget) {
33
+ debug("The message’s target is", target, "but this is", thisTarget);
34
+ }
35
+ return isThisTarget ? "respond" : "ignore";
36
+ }
37
+ export async function nameThisTarget() {
38
+ // Same as above: CS receives messages correctly
39
+ if (!thisTarget && !isContentScript()) {
40
+ thisTarget = await messenger("__getTabData", {}, { page: "any" });
41
+ thisTarget.page = location.pathname;
42
+ }
43
+ }
44
+ function __getTabData() {
45
+ var _a, _b, _c;
46
+ return { tabId: (_b = (_a = this.trace[0]) === null || _a === void 0 ? void 0 : _a.tab) === null || _b === void 0 ? void 0 : _b.id, frameId: (_c = this.trace[0]) === null || _c === void 0 ? void 0 : _c.frameId };
47
+ }
48
+ export function initPrivateApi() {
49
+ if (isBackgroundPage()) {
50
+ thisTarget = { page: "background" };
51
+ registerMethods({ __getTabData });
52
+ }
53
+ }
@@ -6,13 +6,13 @@ declare global {
6
6
  _: Method;
7
7
  }
8
8
  }
9
- declare type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: Target, ...args: PreviousArguments) => TReturnValue : never;
9
+ declare type WithTarget<Method> = Method extends (...args: infer PreviousArguments) => infer TReturnValue ? (target: Target | PageTarget, ...args: PreviousArguments) => TReturnValue : never;
10
10
  declare type ActuallyOmitThisParameter<T> = T extends (...args: infer A) => infer R ? (...args: A) => R : T;
11
11
  /** Removes the `this` type and ensure it's always Promised */
12
12
  export declare type PublicMethod<Method extends ValueOf<MessengerMethods>> = Asyncify<ActuallyOmitThisParameter<Method>>;
13
13
  export declare type PublicMethodWithTarget<Method extends ValueOf<MessengerMethods>> = WithTarget<PublicMethod<Method>>;
14
14
  export interface MessengerMeta {
15
- trace: Runtime.MessageSender[];
15
+ trace: Sender[];
16
16
  }
17
17
  declare type RawMessengerResponse = {
18
18
  value: unknown;
@@ -31,15 +31,16 @@ export interface Options {
31
31
  * @default false
32
32
  */
33
33
  isNotification?: boolean;
34
+ trace?: Sender[];
34
35
  }
35
36
  export declare type Message<LocalArguments extends Arguments = Arguments> = {
36
37
  type: keyof MessengerMethods;
37
38
  args: LocalArguments;
38
- /** If the message is being sent to an intermediary receiver, also set the target */
39
- target?: Target;
39
+ target: Target | PageTarget;
40
40
  /** If the message is being sent to an intermediary receiver, also set the options */
41
- options?: Target;
41
+ options?: Options;
42
42
  };
43
+ export declare type Sender = Runtime.MessageSender;
43
44
  export declare type MessengerMessage = Message & {
44
45
  /** Guarantees that a message is meant to be handled by this library */
45
46
  __webextMessenger: true;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.14.0",
3
+ "version": "0.15.0-0",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -17,8 +17,8 @@
17
17
  },
18
18
  "scripts": {
19
19
  "build": "tsc",
20
- "demo:watch": "parcel watch --no-cache --no-hmr --detailed-report 0",
21
- "demo:build": "parcel build --no-cache --detailed-report 0",
20
+ "demo:watch": "parcel watch --no-cache --no-hmr",
21
+ "demo:build": "parcel build --no-cache",
22
22
  "prepack": "tsc --sourceMap false",
23
23
  "test": "eslint . && tsc --noEmit",
24
24
  "lint": "eslint .",
@@ -84,7 +84,7 @@
84
84
  "p-retry": "^4.6.1",
85
85
  "serialize-error": "^8.1.0",
86
86
  "type-fest": "^2.5.1",
87
- "webext-detect-page": "^3.0.2",
87
+ "webext-detect-page": "^3.1.0",
88
88
  "webextension-polyfill": "^0.8.0"
89
89
  },
90
90
  "devDependencies": {
@@ -104,6 +104,7 @@
104
104
  "npm-run-all": "^4.1.5",
105
105
  "parcel": "^2.0.0",
106
106
  "typescript": "^4.4.4",
107
+ "webext-content-scripts": "^0.10.1",
107
108
  "xo": "^0.45.0"
108
109
  },
109
110
  "targets": {