webext-messenger 0.14.1 → 0.15.0-3

Sign up to get free protection for your applications and to get access to all the features.
@@ -6,3 +6,5 @@
6
6
  export * from "./receiver.js";
7
7
  export * from "./sender.js";
8
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(sender, 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}`);
@@ -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
+ }
@@ -1,6 +1,10 @@
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";
1
+ import { MessengerMeta, Sender } from "./types.js";
2
+ interface AnyTarget {
3
+ tabId?: number | "this";
4
+ frameId?: number;
5
+ page?: string;
6
+ }
7
+ export declare function getActionForMessage(from: Sender, { ...to }: AnyTarget): "respond" | "forward" | "ignore";
4
8
  export declare function nameThisTarget(): Promise<void>;
5
9
  declare function __getTabData(this: MessengerMeta): AnyTarget;
6
10
  declare global {
@@ -6,9 +6,10 @@ import { debug } from "./shared.js";
6
6
  // This CANNOT be awaited because waiting for it means "I will handle the message."
7
7
  // If a message is received before this is ready, it will just have to be ignored.
8
8
  let thisTarget;
9
- //
10
- export function getActionForMessage(target) {
11
- if (target.page === "any") {
9
+ export function getActionForMessage(from, { ...to } // Clone object because we're editing it
10
+ ) {
11
+ var _a;
12
+ if (to.page === "any") {
12
13
  return "respond";
13
14
  }
14
15
  // Content scripts only receive messages that are meant for them. In the future
@@ -17,7 +18,7 @@ export function getActionForMessage(target) {
17
18
  return "respond";
18
19
  }
19
20
  // We're in an extension page, but the target is not one.
20
- if (!("page" in target)) {
21
+ if (!to.page) {
21
22
  return "forward";
22
23
  }
23
24
  if (!thisTarget) {
@@ -25,18 +26,24 @@ export function getActionForMessage(target) {
25
26
  // If this *was* the target, then probably no one else answered
26
27
  return "ignore";
27
28
  }
29
+ // If requests "this" tab, then set it to allow the next condition
30
+ if (to.tabId === "this" && thisTarget.tabId === ((_a = from.tab) === null || _a === void 0 ? void 0 : _a.id)) {
31
+ to.tabId = thisTarget.tabId;
32
+ }
28
33
  // Every `target` key must match `thisTarget`
29
- const isThisTarget = Object.entries(target).every(
34
+ const isThisTarget = Object.entries(to).every(
30
35
  // @ts-expect-error Optional properties
31
36
  ([key, value]) => thisTarget[key] === value);
32
37
  if (!isThisTarget) {
33
- debug("The message’s target is", target, "but this is", thisTarget);
38
+ debug("The message’s target is", to, "but this is", thisTarget);
34
39
  }
35
40
  return isThisTarget ? "respond" : "ignore";
36
41
  }
42
+ let nameRequested = false;
37
43
  export async function nameThisTarget() {
38
44
  // Same as above: CS receives messages correctly
39
- if (!thisTarget && !isContentScript()) {
45
+ if (!nameRequested && !thisTarget && !isContentScript()) {
46
+ nameRequested = true;
40
47
  thisTarget = await messenger("__getTabData", {}, { page: "any" });
41
48
  thisTarget.page = location.pathname;
42
49
  }
@@ -46,8 +53,9 @@ function __getTabData() {
46
53
  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
54
  }
48
55
  export function initPrivateApi() {
56
+ // Any context can handler this message
57
+ registerMethods({ __getTabData });
49
58
  if (isBackgroundPage()) {
50
59
  thisTarget = { page: "background" };
51
- registerMethods({ __getTabData });
52
60
  }
53
61
  }
@@ -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;
@@ -49,7 +50,7 @@ export interface Target {
49
50
  frameId?: number;
50
51
  }
51
52
  export interface PageTarget {
52
- tabId?: number;
53
+ tabId?: number | "this";
53
54
  page: string;
54
55
  }
55
56
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "webext-messenger",
3
- "version": "0.14.1",
3
+ "version": "0.15.0-3",
4
4
  "description": "Browser Extension component messaging framework",
5
5
  "keywords": [],
6
6
  "repository": "pixiebrix/webext-messenger",
@@ -13,7 +13,8 @@
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",
@@ -127,7 +128,8 @@
127
128
  "npm-run-all": "^4.1.5",
128
129
  "parcel": "^2.0.1",
129
130
  "tape": "^5.3.2",
130
- "typescript": "^4.5.2"
131
+ "typescript": "^4.5.2",
132
+ "webext-content-scripts": "^0.10.1"
131
133
  },
132
134
  "targets": {
133
135
  "main": false,
@@ -1,10 +0,0 @@
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 {};
@@ -1,13 +0,0 @@
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,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
- }