npm package: @jam.dev/extension-messaging
Description: Communication patterns for browser extension development
Author: Jam
Latest version: 1.0.3

# @jam.dev/extension-messaging

This package provides a set of common communication patterns for building browser extensions.

Right now, this package supports the `chrome` API as the underlying transport layer. Additional APIs/browser vendors will be added over time.

## Overview
Building a browser extension requires communication between multiple system components. The browser-provided messaging APIs are a great foundation, but quickly require adding additional application logic to ensure your messages are being received and processed by the right component. 

This package provides two classes: 
 - `EventBus` for event general broadcasting
 - `MessageBroker` for request/response style communication between two extension components

### Quick usage

#### Event Bus
```ts
import {EventBus, Component} from "@jam.dev/extension-messaging";

type MyEvents = {
  Hello: {world: boolean};
};

const bus = new EventBus<MyEvents>({ component: Component.Main });

await bus.emit({
  name: "Hello",
  data: { world: true },
});

// some other component can listen
bus.on("Hello", (payload) => {
  console.log(payload.data) // {world: true}
});
```

#### Message Broker
```ts
import {MessageBroker, Component} from "@jam.dev/extension-messaging";

type MyMessages = {
  Ping: {
    request: boolean;
    response: boolean;
  };
};

const broker = new MessageBroker<MyMessages>({ 
  component: Component.ContentScript,
  // in case you have multiple scripts injected, this 
  // gives the ability to target individual scripts
  context: "my-script-context",
});

const response = await broker.send({
  name: "Ping",
  data: true,
  target: Component.Main,
});

// On the `Component.Main` instance:
broker.on("Ping", (payload) => {
  console.log(payload.data); // true
  return true;
});
```

## Concepts

### Application Patterns
`EventBus` is a one-way event broadcast with many potential receivers. An event is emitted by a sender, and N receivers may listen for that event. There is _no_ acknowledgement to the sender that listeners received the message.

`MessageBroker` is a two-way channel between a single sender and a single receiver. A message is sent with a target receiver, and that receiver is expected to return a response. The sender _will_ know if the target received the message via the acknowlegement of receiving a response. Useful for situations where the callee needs receipt of the message, or for request/response style patterns where a callee needs data from another component.

### Transport Layers

The `chrome` API has a few different ways to send messages:

  - `chrome.runtime.sendMessage` — Used to send messages to "background" components. For example, the `background.js` or `worker.js` script sending a message to your extension's popup, or a content script sending a message back to your `background.js` or `worker.js` instance
  - `chrome.tabs.sendMessage` — Used to send messages to content scripts on a specific tab
  - `port.postMessage` — If using `chrome.runtime.connect()` to generate a long-lived (and direct) communication channel between a tab/content-script and a background script, this API provides a common interface to send and receive messages.

These APIs can start to get a little confusing/convoluted. For instance: if you are doing simple message passing between a content script and your background/worker script, the content script will need to listen for and send messages via `chrome.runtime` (`sendMessage`/`onMessage`), and your background/worker script will need to send messages via `chrome.tabs.sendMessage` but listen for messages on `chrome.runtime.onMessage` (since the content script will send messages via `chrome.runtime.sendMessage`).

This package abstracts these APIs (and the overhead of thinking about them) from your application logic by requiring you to define the operating environment of your class instances, via specifying a `component`, and optionally a `context`. These components are:
  - `Component.Main` [single instance] - your background/worker instance.
  - `Component.Popup` [multiple instances] - your extension popup. There is (typically) only one of these, but certain user flows could cause multiple (multiple windows each with the popup open at the same time)
  - `Component.ContentScript` [multiple instances] - any script injected into a tab (isolated world context).
  - `Component.HostScript` [multiple instances] - any script injected directly into the tab page (not the isolated world context). There many
  - `Component.Foreground` [multiple instances] - any extension component created via a new page (e.g., `options.html`) or via the Offscreen Documents API

For the components that can have multiple instances, it's recommended to provide a `context` parameter when instantiating classes to allow for more accurate handling of messages/events.

### Strongly-typed events and messages

By providing a type mapping for events and messages when instantiating an `EventBus` or `MessageBroker`, you will get type safety and auto-completion in your IDE.

The `EventBus` map is simple: keys are your event names, and the value is your payload.

```ts
type MyEvents = {
  EventOne: {
    my: true;
    payload: number;
  };
}

const bus = new EventBus<MyEvents>({ component: Component.Main });
```

The `MessageBroker` map is similar: keys are your event names, and the value is keyed by `request` and `response` structures.

```ts
type MyMessages = {
  HelloWorld: {
    request: { message: string };
    response: boolean;
  };
}

const broker = new MessageBroker<MyMessages>({ component: Component.Main });
```

## Event Bus
The event bus is a pattern that the browser-provided APIs closely resemble. The `EventBus` class in this package goes a bit further by broadcasting messages across multiple communication channels, to ensure that you don't have to think about whether an event emitted from one area of your extension has made it to another.

**Example:** Your background/worker script emits an event that you want _all_ other extension components and tabs to receive. 

Achieving this with the browser-provided APIs means you'd need to send that message with `chrome.runtime.sendMessage` and `chrome.tabs.sendMessage`. And for `chrome.tabs.sendMessage` you would first need to query for all tabs and loop over them.

Using the `EventBus`, you simply `bus.emit()` and the class takes care of the rest. On the receiving end, you simply subscribe to the event via `bus.on()`.

** Emitting an event in your popup, and receiving it in a content script **
```ts
// In your popup
const bus = new EventBus<MyEvents>({ component: Component.Popup });

await bus.emit({
  name: "Ping",
  data: true,
});

// In your content script
const bus = new EventBus<MyEvents>({ component: Component.ContentScript });

bus.on("Ping", (payload) => {
  console.log(payload.data); // true
})
```

**Note**: This package currently assumes that you will have an instance of the `EventBus` and/or `MessageBroker` in your background/worker (e.g., a `Component.Main` instance) for message/event routing to work correctly. For example: a message or event sent from a popup or foreground instance that is targeting a specific tab's content script, will forward the event to the `Component.Main` instance, which will then send the event/message to its destination. This can be changed, as popup and foreground instances do have access to the `chrome.tabs` API.

## Message Broker

The message broker is a pattern that is partial implemented in the `chrome` APIs, via the `sendResponse` parameter of `chrome.runtime.onMessage` handlers. However, it leaves too much room for error and based on our experience, doesn't appear to handle concurrency very well.

There are a few important details for routing a message to the correct target. If a single `Component` is not specific enough, you can provide an object to the `target` parameter in `.send()` help target the receiver:

  1. `component` - the high-level extension component
  2. `context` - your application-specific context for any component that may have multiple instances
  3. `tabId` - when trying to send a message to a content script of a specific tab

**Targeting a specific content script on a specific tab**
```ts
const response = await broker.send({
  name: "HelloWorld",
  target: {
    component: Component.ContentScript,
    context: "my-script-context",
    tabId: 123,
  },
  data: { message: "hi" },
});
```