npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@jam.dev/extension-messaging

v1.0.3

Published

Communication patterns for browser extension development

Downloads

11

Readme

@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

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

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.

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.

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 **

// 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

const response = await broker.send({
  name: "HelloWorld",
  target: {
    component: Component.ContentScript,
    context: "my-script-context",
    tabId: 123,
  },
  data: { message: "hi" },
});