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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@thalesrc/hermes

v7.5.6

Published

Javascript messaging library

Readme

@thalesrc/hermes

npm npm codecov TypeScript License: MIT

Javascript messaging library for cross-context communication

Part of the Thalesrc monorepo

Installation

npm install @thalesrc/hermes
# or
yarn add @thalesrc/hermes
# or
pnpm add @thalesrc/hermes

Overview

Hermes provides a unified, decorator-based API for cross-context messaging in JavaScript applications. Built on RxJS, it supports:

  • 🖼️ Iframe Communication - Parent-child window messaging
  • 🧩 Chrome Extensions - Background scripts, content scripts, and popups
  • 👷 Web Workers - Main thread and worker communication
  • 📡 Broadcast Channel - Tab-to-tab messaging

Core Concepts

MessageClient & MessageHost

The library uses two main abstractions:

  • MessageClient: Sends requests and receives responses (uses @Request decorator)
  • MessageHost: Listens for requests and sends responses (uses @Listen decorator)
  • MessageService: Combines both client and host (bidirectional communication)

Core Architecture

Decorators

  • @Request(path): Marks a method as a message sender
  • @Listen(path): Marks a method as a message listener

Usage

Iframe Communication

Send and receive messages between iframes and parent windows.

Iframe Communication

Client-Only (Iframe)

import { IframeMessageClient, Request } from '@thalesrc/hermes/iframe';
import { Observable } from 'rxjs';

class IframeClient extends IframeMessageClient {
  @Request('greeting')
  sayHello(name: string): Observable<string> {
    return null!; // Implementation handled by decorator
  }
}

// Default: sends messages to parent window
const client = new IframeClient();

// Optional: specify channel name
const clientWithChannel = new IframeClient('my-channel');

// Optional: target specific iframe (from parent window)
const iframe = document.querySelector('iframe');
const clientToIframe = new IframeClient('my-channel', iframe);

// Optional: use a function to get iframe dynamically
const clientWithDynamicIframe = new IframeClient('my-channel', () => 
  document.querySelector('iframe#dynamic')
);

client.sayHello('John').subscribe(response => {
  console.log(response); // 'Hello John!'
});

Host-Only (Parent Window)

import { IframeMessageHost, Listen, UpcomingMessage } from '@thalesrc/hermes/iframe';
import { of } from 'rxjs';

class ParentHost extends IframeMessageHost {
  @Listen('greeting')
  handleGreeting({ data }: UpcomingMessage<string>): Observable<string> {
    return of(`Hello ${data}!`);
  }
}

// Default: listens to all iframes
const host = new ParentHost();

// Optional: specify channel name
const hostWithChannel = new ParentHost('my-channel');

// Optional: listen only to specific iframe
const iframe = document.querySelector('iframe');
const hostForSpecificIframe = new ParentHost('my-channel', iframe);

// Optional: use a function to get iframe dynamically
const hostWithDynamicIframe = new ParentHost('my-channel', () => 
  document.querySelector('iframe#dynamic')
);

Bidirectional Communication (MessageService)

import { IframeMessageService, Request, Listen } from '@thalesrc/hermes/iframe';
import { of, Observable } from 'rxjs';

class IframeBidirectional extends IframeMessageService {
  // Send messages
  @Request('getData')
  requestData(): Observable<any> {
    return null!;
  }

  // Receive messages
  @Listen('update')
  handleUpdate(data: any): Observable<string> {
    console.log('Received update:', data);
    return of('Update processed');
  }
}

// Default: communicates with parent window
const service = new IframeBidirectional();

// Optional: specify channel name and target frame
const iframe = document.querySelector('iframe');
const serviceWithTarget = new IframeBidirectional('my-channel', iframe);

// From within iframe (communicates with parent)
const iframeService = new IframeBidirectional('my-channel');

Chrome Extensions

Communicate across extension contexts (background, content scripts, popups).

Chrome Extension Communication

Content Script

import { ChromeMessageClient, Request } from '@thalesrc/hermes/chrome';
import { Observable } from 'rxjs';

class ContentScript extends ChromeMessageClient {
  @Request('fetchData')
  getData(query: string): Observable<any> {
    return null!;
  }

  @Request('saveSettings')
  saveSettings(settings: object): Observable<boolean> {
    return null!;
  }
}

const contentScript = new ContentScript();

contentScript.getData('user').subscribe(data => {
  console.log('Received:', data);
});

Background Script

import { ChromeMessageHost, Listen } from '@thalesrc/hermes/chrome';
import { of, Observable } from 'rxjs';

class BackgroundScript extends ChromeMessageHost {
  @Listen('fetchData')
  handleFetchData(query: string): Observable<any> {
    // Fetch from API or storage
    return of({ name: 'John', age: 30 });
  }

  @Listen('saveSettings')
  handleSaveSettings(settings: object): Observable<boolean> {
    // Save to chrome.storage
    return of(true);
  }
}

const background = new BackgroundScript();

Web Workers

Communicate between main thread and web workers.

Web Worker Communication

Main Thread

import { WorkerMessageService, Request, Listen } from '@thalesrc/hermes/worker';
import { of, Observable } from 'rxjs';

class MainThread extends WorkerMessageService {
  @Request('processData')
  sendDataToWorker(data: number[]): Observable<number> {
    return null!;
  }

  @Listen('progress')
  handleProgress(percent: number): Observable<void> {
    console.log(`Progress: ${percent}%`);
    return of(void 0);
  }
}

// Must provide Worker instance in main thread
const worker = new Worker('./worker.js');
const mainService = new MainThread(worker);

mainService.sendDataToWorker([1, 2, 3, 4, 5]).subscribe(result => {
  console.log('Worker result:', result);
});
Flexible Worker Initialization

The worker parameter supports multiple initialization patterns for different use cases:

// Direct Worker instance
const service1 = new MainThread(new Worker('./worker.js'));

// Promise that resolves to a Worker (for async initialization)
const workerPromise = import('./worker.js').then(m => new Worker(m.default));
const service2 = new MainThread(workerPromise);

// Function that returns a Worker (for lazy initialization)
const service3 = new MainThread(() => new Worker('./worker.js'));

// Function that returns a Promise<Worker> (for async lazy initialization)
const service4 = new MainThread(async () => {
  const module = await import('./worker.js');
  return new Worker(module.default);
});
Dynamic Worker Management with initialize()

The initialize() method allows you to switch workers at runtime or re-establish connections:

class MainThread extends WorkerMessageService {
  // ... decorators and methods
}

// Start without a worker
const service = new MainThread();

// Later, connect to a worker dynamically
service.initialize(new Worker('./worker.js'));

// Switch to a different worker
service.initialize(new Worker('./different-worker.js'));

// Re-establish connection after worker error
const worker = new Worker('./worker.js');
worker.onerror = (error) => {
  console.error('Worker error, reinitializing...', error);
  service.initialize(new Worker('./worker.js'));
};
service.initialize(worker);

// Conditional worker initialization
function getWorker() {
  return navigator.hardwareConcurrency > 2 
    ? new Worker('./heavy-worker.js')
    : new Worker('./light-worker.js');
}
service.initialize(getWorker);

Worker Thread

import { WorkerMessageService, Request, Listen } from '@thalesrc/hermes/worker';
import { of, Observable } from 'rxjs';

class WorkerThread extends WorkerMessageService {
  @Listen('processData')
  handleProcessData(data: number[]): Observable<number> {
    // Heavy computation
    const result = data.reduce((sum, n) => sum + n, 0);
    return of(result);
  }

  @Request('progress')
  reportProgress(percent: number): Observable<void> {
    return null!;
  }
}

// Inside worker: no argument needed (uses self)
const workerService = new WorkerThread();

Broadcast Channel

Communicate between different tabs/windows of the same origin.

Broadcast Channel Communication

Tab 1

import { BroadcastMessageService, Request, Listen } from '@thalesrc/hermes/broadcast';
import { of, Observable } from 'rxjs';

class Tab1 extends BroadcastMessageService {
  @Request('sync')
  requestSync(data: any): Observable<string> {
    return null!;
  }

  @Listen('notification')
  handleNotification(message: string): Observable<void> {
    console.log('Notification:', message);
    return of(void 0);
  }
}

const tab1 = new Tab1('my-app-channel');

tab1.requestSync({ user: 'John' }).subscribe(response => {
  console.log(response); // 'Sync completed'
});

Tab 2

import { BroadcastMessageService, Request, Listen } from '@thalesrc/hermes/broadcast';
import { of, Observable } from 'rxjs';

class Tab2 extends BroadcastMessageService {
  @Listen('sync')
  handleSync(data: any): Observable<string> {
    console.log('Syncing data:', data);
    return of('Sync completed');
  }

  @Request('notification')
  sendNotification(message: string): Observable<void> {
    return null!;
  }
}

const tab2 = new Tab2('my-app-channel');

Advanced Features

Streaming Responses

Return multiple values over time using RxJS operators:

import { Listen } from '@thalesrc/hermes/iframe';
import { interval } from 'rxjs';
import { map, take } from 'rxjs/operators';

class StreamingHost extends IframeMessageHost {
  @Listen('countdown')
  handleCountdown(start: number): Observable<number> {
    return interval(1000).pipe(
      map(i => start - i),
      take(start + 1)
    );
  }
}

The client receives each value as it's emitted:

client.countdown(5).subscribe(
  value => console.log(value), // 5, 4, 3, 2, 1, 0
  error => console.error(error),
  () => console.log('Complete!')
);

Error Handling

import { Listen } from '@thalesrc/hermes/iframe';
import { throwError } from 'rxjs';

class ErrorHost extends IframeMessageHost {
  @Listen('riskyOperation')
  handleRiskyOperation(data: any): Observable<any> {
    if (!data.valid) {
      return throwError(() => new Error('Invalid data'));
    }
    return of({ success: true });
  }
}

Constructor Parameters

Iframe

IframeMessageClient / IframeMessageHost / IframeMessageService

constructor(channelName?: string, targetFrame?: HTMLIFrameElement | (() => HTMLIFrameElement))
  • channelName (optional): Channel identifier for namespacing messages. Default: 'hermes-iframe-message'
  • targetFrame (optional): Specific iframe to communicate with. Can be:
    • HTMLIFrameElement: Direct reference to iframe element
    • () => HTMLIFrameElement: Function returning iframe (useful for dynamic iframes)
    • Omit to communicate with parent window (from iframe) or all iframes (from parent)

Examples:

// From iframe: communicate with parent
const client = new IframeMessageClient();

// From parent: communicate with specific iframe
const iframe = document.querySelector('iframe');
const host = new IframeMessageHost('my-channel', iframe);

// Dynamic iframe reference
const service = new IframeMessageService('my-channel', () => 
  document.querySelector('iframe[data-active="true"]')
);

Worker

WorkerMessageClient / WorkerMessageHost / WorkerMessageService

constructor(worker?: Worker)
  • worker (optional): Worker instance for main thread communication
    • In main thread: Must provide Worker instance
    • In worker thread: Omit parameter (uses self automatically)

Examples:

// Main thread: must provide worker
const worker = new Worker('./worker.js');
const service = new WorkerMessageService(worker);

// Inside worker: no parameter needed
const service = new WorkerMessageService();

Broadcast

BroadcastMessageClient / BroadcastMessageHost / BroadcastMessageService

constructor(channelName?: string)
  • channelName (optional): Broadcast channel name. Default: 'hermes-broadcast-message'

Example:

const service = new BroadcastMessageService('app-sync-channel');

Chrome

ChromeMessageClient / ChromeMessageHost

constructor(connectionName?: string)
  • connectionName (optional): Connection identifier. Default: 'hermes-chrome-message'

Example:

const client = new ChromeMessageClient('extension-port');

API Reference

Classes

  • MessageClient - Base class for message senders
  • MessageHost - Base class for message receivers
  • IframeMessageClient - Iframe client implementation
  • IframeMessageHost - Iframe host implementation
  • IframeMessageService - Bidirectional iframe communication
  • ChromeMessageClient - Chrome extension client
  • ChromeMessageHost - Chrome extension host
  • WorkerMessageService - Bidirectional worker communication
  • BroadcastMessageClient - Broadcast channel client
  • BroadcastMessageHost - Broadcast channel host
  • BroadcastMessageService - Bidirectional broadcast communication

Decorators

  • @Request(path: string) - Decorator for sending messages
  • @Listen(path: string) - Decorator for receiving messages

Types

  • Message - Message payload structure
  • MessageResponse - Response payload structure
  • UpcomingMessage<T> - Incoming message with sender info (iframe)

Requirements

  • RxJS 7.x or higher
  • TypeScript 4.x or higher (for decorator support)
  • Modern browser with ES2015+ support

Development

Testing

Hermes uses a modular testing structure with separate configurations for each submodule:

# Run all tests (all submodules in parallel)
pnpm nx run hermes:test

# Run specific submodule tests
pnpm nx run hermes:test/core          # Core functionality (Node.js)
pnpm nx run hermes:test/worker        # Web Worker tests (Browser)
pnpm nx run hermes:test/chrome        # Chrome extension tests (Browser)
pnpm nx run hermes:test/broadcast     # Broadcast Channel tests (Browser)
pnpm nx run hermes:test/iframe        # Iframe communication tests (Browser)

# Run all tests with coverage and merge reports
pnpm nx run hermes:test/coverage

# Debug browser tests (visible browser)
pnpm nx run hermes:test/headed

For detailed testing documentation, see:

Building

# Build the library
pnpm nx build hermes

# Build and watch for changes
pnpm nx build hermes --watch

Contributing

When adding new submodule support:

  1. Create submodule directory in src/
  2. Add test files alongside code: *.spec.ts
  3. Create submodule's vitest.config.ts in the submodule directory
  4. Add test target in project.json
  5. Update main test target to include new submodule

See TESTING-GUIDE.md for details on adding new submodules.

License

MIT © Thalesrc