webext-messenger 0.14.0 → 0.15.0-2

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.
@@ -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,10 @@
1
- export { registerMethods } from "./receiver.js";
2
- export { messenger, getMethod, getNotifier, backgroundTarget, } from "./sender.js";
1
+ // Imports must use the .js extension because of ESM requires it and TS refuses to rewrite .ts to .js
2
+ // This works in TS even if the .js doesn't exist, but it breaks Parcel (the tests builder)
3
+ // For this reason, there's an `alias` field in package.json to redirect these imports.
4
+ // If you see "@parcel/resolver-default: Cannot load file './yourNewFile.js'" you need to add it to the `alias` list
5
+ // 🥲
6
+ export * from "./receiver.js";
7
+ export * from "./sender.js";
8
+ export * from "./types.js";
9
+ import { initPrivateApi } from "./thisTarget.js";
10
+ 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.js";
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`
@@ -25,20 +26,26 @@ function manageConnection(type, options, sendMessage) {
25
26
  });
26
27
  }
27
28
  async function manageMessage(type, sendMessage) {
28
- const response = await pRetry(sendMessage, {
29
+ const response = await pRetry(async () => {
30
+ const response = await sendMessage();
31
+ if (!isMessengerResponse(response)) {
32
+ throw new MessengerError(`No handler registered for ${type} in the receiving end`);
33
+ }
34
+ return response;
35
+ }, {
29
36
  minTimeout: 100,
30
37
  factor: 1.3,
31
38
  maxRetryTime: 4000,
32
39
  onFailedAttempt(error) {
33
- if (!String(error === null || error === void 0 ? void 0 : error.message).startsWith(errorNonExistingTarget)) {
40
+ if (error instanceof MessengerError ||
41
+ String(error.message).startsWith(errorNonExistingTarget)) {
42
+ debug(type, "will retry. Attempt", error.attemptNumber);
43
+ }
44
+ else {
34
45
  throw error;
35
46
  }
36
- debug(type, "will retry");
37
47
  },
38
48
  });
39
- if (!isMessengerResponse(response)) {
40
- throw new MessengerError(`No handler for ${type} was registered in the receiving end`);
41
- }
42
49
  if ("error" in response) {
43
50
  debug(type, "↘️ replied with error", response.error);
44
51
  throw deserializeError(response.error);
@@ -47,6 +54,7 @@ async function manageMessage(type, sendMessage) {
47
54
  return response.value;
48
55
  }
49
56
  function messenger(type, options, target, ...args) {
57
+ // Message goes to extension page
50
58
  if ("page" in target) {
51
59
  if (target.page === "background" && isBackgroundPage()) {
52
60
  const handler = handlers.get(type);
@@ -58,7 +66,7 @@ function messenger(type, options, target, ...args) {
58
66
  }
59
67
  const sendMessage = async () => {
60
68
  debug(type, "↗️ sending message to runtime");
61
- return browser.runtime.sendMessage(makeMessage(type, args));
69
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
62
70
  };
63
71
  return manageConnection(type, options, sendMessage);
64
72
  }
@@ -66,7 +74,7 @@ function messenger(type, options, target, ...args) {
66
74
  if (!browser.tabs) {
67
75
  return manageConnection(type, options, async () => {
68
76
  debug(type, "↗️ sending message to runtime");
69
- return browser.runtime.sendMessage(makeMessage(type, args, target));
77
+ return browser.runtime.sendMessage(makeMessage(type, args, target, options));
70
78
  });
71
79
  }
72
80
  // `frameId` must be specified. If missing, the message is sent to every frame
@@ -74,7 +82,7 @@ function messenger(type, options, target, ...args) {
74
82
  // Message tab directly
75
83
  return manageConnection(type, options, async () => {
76
84
  debug(type, "↗️ sending message to tab", tabId, "frame", frameId);
77
- return browser.tabs.sendMessage(tabId, makeMessage(type, args), {
85
+ return browser.tabs.sendMessage(tabId, makeMessage(type, args, target, options), {
78
86
  frameId,
79
87
  });
80
88
  });
@@ -1,4 +1,11 @@
1
+ import { JsonObject } from "type-fest";
1
2
  import { Method } from "./types.js";
3
+ declare type ErrorObject = {
4
+ name?: string;
5
+ stack?: string;
6
+ message?: string;
7
+ code?: string;
8
+ } & JsonObject;
2
9
  export declare const __webextMessenger = true;
3
10
  export declare function isObject(value: unknown): value is Record<string, unknown>;
4
11
  export declare class MessengerError extends Error {
@@ -7,3 +14,6 @@ export declare class MessengerError extends Error {
7
14
  export declare const handlers: Map<string, Method>;
8
15
  export declare const debug: (...args: any[]) => void;
9
16
  export declare const warn: (...args: any[]) => void;
17
+ export declare function isErrorObject(error: unknown): error is ErrorObject;
18
+ export declare function delay(milliseconds: number): Promise<void>;
19
+ export {};
@@ -17,3 +17,13 @@ export const handlers = new Map();
17
17
  // .bind preserves the call location in the console
18
18
  export const debug = console.debug.bind(console, "Messenger:");
19
19
  export const warn = console.warn.bind(console, "Messenger:");
20
+ export function isErrorObject(error) {
21
+ var _a;
22
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- This is a type guard function and it uses ?.
23
+ return typeof ((_a = error) === null || _a === void 0 ? void 0 : _a.message) === "string";
24
+ }
25
+ export async function delay(milliseconds) {
26
+ return new Promise((resolve) => {
27
+ setTimeout(resolve, milliseconds);
28
+ });
29
+ }
@@ -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 "./sender.js";
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-2",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -13,12 +13,13 @@
13
13
  "./source/sender.js": "./source/sender.ts",
14
14
  "./source/receiver.js": "./source/receiver.ts",
15
15
  "./source/types.js": "./source/types.ts",
16
- "./source/shared.js": "./source/shared.ts"
16
+ "./source/shared.js": "./source/shared.ts",
17
+ "./source/thisTarget.js": "./source/thisTarget.ts"
17
18
  },
18
19
  "scripts": {
19
20
  "build": "tsc",
20
- "demo:watch": "parcel watch --no-cache --no-hmr --detailed-report 0",
21
- "demo:build": "parcel build --no-cache --detailed-report 0",
21
+ "demo:watch": "parcel watch --no-cache --no-hmr",
22
+ "demo:build": "parcel build --no-cache",
22
23
  "prepack": "tsc --sourceMap false",
23
24
  "test": "eslint . && tsc --noEmit",
24
25
  "lint": "eslint .",
@@ -45,6 +46,21 @@
45
46
  "plugin:unicorn/recommended"
46
47
  ],
47
48
  "rules": {
49
+ "no-restricted-imports": [
50
+ "error",
51
+ {
52
+ "paths": [
53
+ {
54
+ "name": "./index",
55
+ "message": "The index file is only used to re-export internal files. Use direct imports instead."
56
+ }
57
+ ]
58
+ }
59
+ ],
60
+ "import/extensions": [
61
+ "error",
62
+ "always"
63
+ ],
48
64
  "import/no-unresolved": "off",
49
65
  "unicorn/filename-case": [
50
66
  "error",
@@ -77,34 +93,43 @@
77
93
  "@typescript-eslint/no-explicit-any": "off",
78
94
  "@typescript-eslint/no-unsafe-member-access": "off"
79
95
  }
96
+ },
97
+ {
98
+ "files": [
99
+ "source/test/**/*"
100
+ ],
101
+ "rules": {
102
+ "import/extensions": "off"
103
+ }
80
104
  }
81
105
  ]
82
106
  },
83
107
  "dependencies": {
84
- "p-retry": "^4.6.1",
85
- "serialize-error": "^8.1.0",
86
- "type-fest": "^2.5.1",
87
- "webext-detect-page": "^3.0.2",
108
+ "p-retry": "^5.0.0",
109
+ "serialize-error": "^9.0.0",
110
+ "type-fest": "^2.6.0",
111
+ "webext-detect-page": "^3.1.0",
88
112
  "webextension-polyfill": "^0.8.0"
89
113
  },
90
114
  "devDependencies": {
91
- "@parcel/config-webextension": "^2.0.0",
115
+ "@parcel/config-webextension": "^2.0.1",
92
116
  "@sindresorhus/tsconfig": "^2.0.0",
93
- "@types/chrome": "^0.0.159",
94
- "@types/webextension-polyfill": "^0.8.0",
95
- "@typescript-eslint/eslint-plugin": "^5.1.0",
96
- "@typescript-eslint/parser": "^5.1.0",
97
- "eslint": "^8.1.0",
117
+ "@types/chrome": "^0.0.164",
118
+ "@types/tape": "^4.13.2",
119
+ "@types/webextension-polyfill": "^0.8.2",
120
+ "@typescript-eslint/eslint-plugin": "^5.4.0",
121
+ "@typescript-eslint/parser": "^5.4.0",
122
+ "eslint": "^8.3.0",
98
123
  "eslint-config-prettier": "^8.3.0",
99
124
  "eslint-config-xo": "^0.39.0",
100
- "eslint-config-xo-typescript": "^0.45.2",
101
- "eslint-plugin-import": "^2.25.2",
102
- "eslint-plugin-unicorn": "^37.0.1",
103
- "fresh-tape": "^5.3.1",
125
+ "eslint-config-xo-typescript": "^0.47.1",
126
+ "eslint-plugin-import": "^2.25.3",
127
+ "eslint-plugin-unicorn": "^39.0.0",
104
128
  "npm-run-all": "^4.1.5",
105
- "parcel": "^2.0.0",
106
- "typescript": "^4.4.4",
107
- "xo": "^0.45.0"
129
+ "parcel": "^2.0.1",
130
+ "tape": "^5.3.2",
131
+ "typescript": "^4.5.2",
132
+ "webext-content-scripts": "^0.10.1"
108
133
  },
109
134
  "targets": {
110
135
  "main": false,
@@ -1,14 +0,0 @@
1
- /// <reference types="firefox-webext-browser" />
2
- import { NamedTarget, Target, MessengerMeta } from "./types.js";
3
- declare global {
4
- interface MessengerMethods {
5
- __webextMessengerTargetRegistration: typeof _registerTarget;
6
- }
7
- }
8
- export declare function resolveNamedTarget(target: NamedTarget, sender?: browser.runtime.MessageSender): Target;
9
- export declare const targets: Map<string, Target>;
10
- /** Register the current context so that it can be targeted with a name */
11
- export declare const registerTarget: (name: string) => Promise<void>;
12
- declare function _registerTarget(this: MessengerMeta, name: string): Promise<void>;
13
- export declare function initTargets(): void;
14
- export {};
@@ -1,40 +0,0 @@
1
- import { isBackgroundPage } from "webext-detect-page";
2
- import { errorNonExistingTarget, getMethod } from "./sender.js";
3
- import { registerMethods } from "./receiver.js";
4
- export function resolveNamedTarget(target, sender) {
5
- var _a;
6
- if (!isBackgroundPage()) {
7
- throw new Error("Named targets can only be resolved in the background page");
8
- }
9
- const { name, tabId = (_a = sender === null || sender === void 0 ? void 0 : sender.tab) === null || _a === void 0 ? void 0 : _a.id, // If not specified, try to use the sender’s
10
- } = target;
11
- if (typeof tabId === "undefined") {
12
- throw new TypeError(`${errorNonExistingTarget} The tab ID was not specified nor it was automatically determinable.`);
13
- }
14
- const resolvedTarget = targets.get(`${tabId}%${name}`);
15
- if (!resolvedTarget) {
16
- throw new Error(`${errorNonExistingTarget} Target named ${name} not registered for tab ${tabId}.`);
17
- }
18
- return resolvedTarget;
19
- }
20
- // TODO: Remove targets after tab closes to avoid "memory leaks"
21
- export const targets = new Map();
22
- /** Register the current context so that it can be targeted with a name */
23
- export const registerTarget = getMethod("__webextMessengerTargetRegistration");
24
- async function _registerTarget(name) {
25
- const sender = this.trace[0];
26
- const tabId = sender.tab.id;
27
- const { frameId } = sender;
28
- targets.set(`${tabId}%${name}`, {
29
- tabId,
30
- frameId,
31
- });
32
- console.debug(`Messenger: Target "${name}" registered for tab ${tabId}`);
33
- }
34
- export function initTargets() {
35
- if (isBackgroundPage()) {
36
- registerMethods({
37
- __webextMessengerTargetRegistration: _registerTarget,
38
- });
39
- }
40
- }