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 🙏

© 2025 – Pkg Stats / Ryan Hefner

@tripod311/dispatch

v0.0.6

Published

dispatcher to connect js in browser and node

Readme

Dispatch is a lightweight framework for building event-driven applications by splitting logic into independent units and connecting them through a unified communication layer.

Core Concepts

Application logic can be divided into multiple nodes — independent logic units. Dispatch provides simple interfaces to define these nodes and connect them in a flexible way. A node can run locally, in a worker, on a remote host, or even inside a browser.

Nodes interact through a built-in transport layer, which makes communication seamless: to send a message, a node only needs to know the recipient’s address and call Node.send(). The framework takes care of delivering the event, regardless of where the recipient is running.

This architecture allows you to start developing without worrying about performance bottlenecks. When bottlenecks appear, you can easily move nodes to another process, machine, or environment — without rewriting their logic.

In Dispatch:

  • Nodes encapsulate logic.
  • Addresses identify nodes in the system.
  • Events are the universal way nodes communicate.

Core Concepts

Dispatch applications are built around four core components:

  • Dispatcher — the glue that connects all nodes. It manages the application lifecycle, always holds a root node, and is mostly used to start or stop the application.
  • Node — a unit of logic in the system. Nodes receive and process events.
  • Address — a unique sequence of strings that identifies a node’s position in the system. Every node must have an address.
  • Event — a message sent from one node to another. An event carries:
    • system meta information,
    • a command (used by the recipient node to decide what action to take),
    • a payload (custom data for the command).

Nodes send events by calling Node.send(address, event). The dispatcher ensures the event reaches the correct recipient, regardless of where it runs (local, worker, remote host, or browser).

Short Example

import { Dispatcher, Node, Address, Event } from "@tripod311/dispatch";

class MyRoot extends Node {
  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    // Listen for "call" events
    this.setListener("call", this.callHandler.bind(this));
  }

  callHandler(event: Event) {
    console.log("Hello");

    event.response({
      command: "call",
    });
  }
}

class MyChildNode extends Node {
  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    this.setListener("call", this.callHandler.bind(this));
  }

  callHandler(event: Event) {
    console.log("World");
  }
}

const dispatcher = new Dispatcher();
const root = new MyRoot();
const childNode = new MyChildNode();

dispatcher.setRoot(root, new Address(["root"]));
// childNode will have address ["root", "child"]
root.addChild("child", childNode);

// Child sends event to root
childNode.send(["root"], {
  command: "call",
});

Output:

Hello World

Why Dispatch?

Dispatch is not designed to be the fastest service-to-service communication layer — if raw speed is your priority, you might prefer tools like gRPC or other optimized solutions.

Instead, Dispatch focuses on simplicity and structure. It allows you to design applications that:

Run seamlessly across multiple hosts (local, remote, browser, worker threads).

Treat clients and services as equal parts of the same event-driven system.

Use the same transport interface everywhere — no matter where the code is running.

With Dispatch, you don’t have to worry about whether a node is local, remote, or in another process. You just call:

node.send(address, event);
node.chain(address, event, callback);

…and the framework handles all routing, serialization, and delivery for you.

This unified approach makes it easy to split application logic into independent units, move them between environments, and still have them work together as a single, coherent system.

API

Dispatcher

The Dispatcher is the entry point of every Dispatch application (or sub-application). It is responsible for initializing and shutting down the application.

In most cases, starting a root application looks like this:

import { Dispatcher, Address } from "dispatch";

const dispatcher = new Dispatcher();
const root = new MyRoot();

// Graceful shutdown
process.on("SIGINT", () => {
  dispatcher.removeRoot();
});

process.on("SIGTERM", () => {
  dispatcher.removeRoot();
});

// Set root node
dispatcher.setRoot(root, new Address(["root"]));

Key points:

  • Dispatcher manages the application lifecycle.
  • setRoot() registers the root node at a given Address.
  • removeRoot() gracefully shuts down the root node (e.g., on termination signals).

Address

The Address class represents a unique position of a node in the system. Internally, it’s just a sequence of strings.

Usage

const addr1 = new Address(["root", "child"]);
const addr2 = new Address(addr1);

console.log(addr1.equals(addr2)); // true
console.log(addr1.toString());    // [root, child]
console.log(addr1.parent);        // [root]
console.log(addr1.length);        // 2

Methods

constructor(data: string[] | Address) Creates an address from an array of strings or clones an existing Address.

equals(addr: Address): boolean Checks if two addresses are identical.

clone(): Address Returns a copy of the address.

isParentOf(addr: Address): boolean Returns true if this address is a prefix (parent) of another.

toString(): string Converts address to string format: [part1, part2, ...].

get data(): string[] / set data(data: string[]) Gets or sets the underlying string sequence.

get parent(): Address Returns the parent address (one level up).

get length(): number Returns the number of segments in the address.

Event

An Event is the fundamental data unit that nodes exchange. It contains system metadata (sender, destination, etc.) and an EventData payload.

In most cases you won't be creating event manually and will be using Node.send, Node.chain and event.response methods. In most cases, you’ll only need to work with the command and data fields inside the event.

Example

// Create and send event
childNode.send(["root"], {
  command: "sayHello",
  data: { name: "Alice" }
});

// Handle event in root node
this.setListener("sayHello", (event: Event) => {
  console.log("Hello,", event.data?.name);
  event.response({
    command: "sayHello",
    data: { status: "ok" }
  });
});

Output:

Hello, Alice

EventData interface

interface EventData {
  command: string;     // Command name (required)
  error?: boolean;     // Marks the event as error
  details?: string;    // Error details or extra info
  data?: any;          // Custom payload
}

Methods

constructor(dispatcher, sender, destination, data, isResponse = false, trace = false) Creates a new event. sender and destination are Address instances. data must contain a command.

dispatch(hop = 0): void Sends the event via the dispatcher.

response(obj: EventData): void Sends a response back to the original sender. Automatically reuses the request ID if present.

captureTransfers(): any[] Extracts all ArrayBuffer objects from the event data (useful for binary transfers).

System Fields:

  • sender: Address — who sent the event.
  • destination: Address — where the event should go.
  • data: EventData — payload.
  • isResponse: boolean — marks event as a response.
  • trace: boolean — debug/trace flag.
  • reqId?: number — request ID for correlating requests and responses. You shouldn't modify this field, it's used only in chain requests.

Node

A Node is a logic unit of the application. When attached to a dispatcher, it gets a unique Address and can send or receive Events.

Essentials:

setListener(command, fn) — register an event handler.

send(destination, data, trace = false) — send an event to another node.

chain(destination, data, callback, trace = false) — send an event and wait for a response (request/response pattern).

addChild(id, node) — attach a child node under a sub-address.

delChild(id) - delete/detach a child node

Example

class MyNode extends Node {
  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    this.setListener("ping", (event) => {
      console.log("Got ping from", event.sender.toString());
      event.response({ command: "pong" });
    });
  }
}

const dispatcher = new Dispatcher();
const root = new MyNode();

dispatcher.setRoot(root, new Address(["root"]));

// Root node receives "ping"
root.send(["root"], { command: "ping" });

Output:

Got ping from [root]

In most cases, the typical approach for developers is to subclass Node, add custom methods, register listeners and add child nodes inside the attach() method. This turns the subclass into a fully functional logical unit of the application.

class MyService extends Node {
  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    this.setListener("process", this.processHandler.bind(this));
  }

  processHandler(event: Event) {
    console.log("Processing:", event.data);
    event.response({ command: "process", data: "done" });
  }
}

Subscribable

The Subscribable class is a special type of Node with built-in subscription management. Other nodes can subscribe/unsubscribe to it and receive notifications when events are published.

Usage

import { Subscribable } from "@tripod311/dispatch"

class MyChannel extends Subscribable {
  subscribe(event: Event) {
    // Always call super.subscribe first
    super.subscribe(event);

    // Optionally send initial state or welcome data
    event.response({
      command: "init",
      data: { status: "subscribed" },
    });
  }
}

const channel = new MyChannel();
root.addChild("channel", channel);

// Somewhere else in the system
someNode.send(["root", "channel"], { command: "subscribe" });

//notify all subscribed nodes
channel.notify({
  command: "somethingHappened",
  data: { ... }
})

Methods

subscribe(event: Event) Registers a subscriber (based on sender address). Can be overridden — just make sure to call super.subscribe(event) first.

unsubscribe(event: Event) Removes a subscriber.

notify(data: EventData, except: Address[]) Sends data to all subscribers except those listed in except.

Moving Nodes to Separate Threads

If you need to handle heavy tasks without blocking the main thread, you can move logic into a worker thread. Dispatch provides ThreadNode and ThreadConnector classes for this purpose.

Example

myNode.ts

import { Node, ThreadNode, Dispatcher, Address, Event } from "@tripod311/dispatch";
import WorkerScript from "./workerScript.js";

export default class MyNode extends Node {
  private worker!: ThreadNode;

  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    this.worker = new ThreadNode(WorkerScript, {
      // These options prevent a buggy worker from endlessly eating CPU.
      // interval = ping interval (ms)
      // threshold = number of failed pings before termination
      // To disable, set interval=0
      interval: 1000,
      threshold: 5,
    });

    this.addChild("worker", this.worker);

    this.chain(this.address!.data.concat(["worker", "receiver"]), {
      command: "performHighloadTask",
      data: {
        // some data for task
      },
    }, (response: Event) => {
      // handle worker response
    });
  }
}

workerScript.ts

import { ThreadConnector, Event } from "@tripod311/dispatch";

const connector = new ThreadConnector();
connector.readyPromise.then(() => {
  connector.setListener("performHighloadTask", (event: Event) => {
    // heavy logic here
    event.response({
      command: "performHighloadTaskResponse",
      data: {
        // some result
      },
    });
  });
}, () => {
  // something went wrong during worker startup
});

Moving Nodes to a Separate Host

Sometimes, parts of an application need to run on a separate host. Dispatch supports this out of the box with TCPEndpoint (server side) and TCPConnector (client side).

Example

myNode.ts (main host)

import net from "net";
import { Node, TCPEndpoint, Dispatcher, Address, Event } from "@tripod311/dispatch";

export default class MyNode extends Node {
  private endpoint!: TCPEndpoint;

  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    const server = net.createServer();
    this.endpoint = new TCPEndpoint(server, {
      // works the same as ThreadNode ping options
      interval: 500,
      threshold: 5,
    });
    server.listen(0);

    this.addChild("endpoint", this.endpoint);

    this.setListener("subServiceData", this.subServiceDataHandle.bind(this));
  }

  subServiceDataHandle(event: Event) {
    // process data from remote host
  }
}

remoteHostNode.ts (remote host)

import net from "net";
import { TCPConnector, Dispatcher, Address, Event } from "@tripod311/dispatch";

const dispatcher = new Dispatcher();
const connector = new TCPConnector({
  interval: 500,
  threshold: 5,
  host: "127.0.0.1",
  port: port, // replace with actual port
});

dispatcher.setRoot(connector, new Address([]));

connector.readyPromise.then(() => {
  connector.send(connector.address.parent, {
    command: "subServiceData",
    data: {
      // event payload
    },
  });
}, (err: any) => {
  // connection error
  dispatcher.removeRoot();
});

Notes on Design

  • TCPEndpoint does not create or start a server by itself. You must provide an existing net.Server instance to the constructor and call server.listen() manually. Likewise, you need to close the server manually when it’s no longer needed — simply detaching the node is not enough.
  • TCPConnector, on the other hand, manages its own socket. It creates the connection when attached and closes it automatically when detached.
  • TCPEndpoint restriction mechanism
    • By default, a TCPEndpoint node only allows events to flow to addresses that are its children.
    • To relax this restriction, you can pass a third constructor argument: allowedAddresses. This must be either a Set of Addresses or an array of Address instances.
    • Or you may do endpoint.restrictions.add(address) to whitelist address on a created endpoint.
    • This mechanism acts as a safety layer, preventing unwanted event forwarding outside the intended scope.
  • Dispatch applications are structured hierarchically: nodes form a tree. When a TCPConnector attaches to the dispatcher, it creates a connection and waits for a special "register" event, which assigns its real address in the system. Its initial address does not matter — it will be overwritten.
  • Because of the hierarchy, the TCPConnector should be the root node in the sub-service. All other nodes must be attached only after it receives its address.
  • If you need a non-hierarchical design, you can implement your own connection node (covered later).

Connecting a Browser

For web applications you have several options:

  • API. Just take express or fastify or any tool that you like and convert api calls into chained events.
  • WebSockets — Use WSEndpoint (server) with WSConnector (browser).
  • Long Polling — Use HTTPEndpoint (server) with HTTPConnector (browser).

Both WebSocket and HTTP connectors follow the same design as TCPEndpoint–TCPConnector.

WebSocket Example

myNode.ts (main host)

import { WebSocketServer } from "ws";
import { Node, WSEndpoint, Dispatcher, Address, Event } from "@tripod311/dispatch";

export default class MyNode extends Node {
  private endpoint!: WSEndpoint;

  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    const server = new WebSocketServer({ port: 0 });
    this.endpoint = new WSEndpoint(server, {
      interval: 500,
      threshold: 5,
    });

    this.addChild("endpoint", this.endpoint);

    this.setListener("wsClientData", this.wsDataHandle.bind(this));
  }

  wsDataHandle(event: Event) {
    // process data from socket
  }
}

websocketConnector.ts (browser)

const dispatcher = new Dispatcher();
const connector = new WSConnector("ws://127.0.0.1:8080", {
  interval: 500,
  threshold: 5,
});
dispatcher.setRoot(connector, new Address([]));

connector.readyPromise.then(() => {
  connector.send(connector.address.parent, {
    command: "wsClientData",
    data: {
      // event payload
    },
  });
}, (err: any) => {
  // connection error
  dispatcher.removeRoot();
});

Notes:

  • WSEndpoint is designed to work with the ws library (the de facto WebSocket standard for Node.js).
  • It uses the same restriction mechanism as TCPEndpoint:
    • Pass allowed addresses as the third constructor argument (Set or Address[]),
    • Or use endpoint.restrictions.add(address).

HTTP Long Polling Example

myNode.ts (main host)

import http from "http";
import { Node, HTTPEndpoint, Dispatcher, Address, Event } from "@tripod311/dispatch";

export default class MyNode extends Node {
  private endpoint!: HTTPEndpoint;

  attach(dispatcher: Dispatcher, address: Address) {
    super.attach(dispatcher, address);

    const server = http.createServer();
    this.endpoint = new HTTPEndpoint(server, 60000, 30000); // sessionExpire, pollTime
    server.listen(8080);

    this.addChild("endpoint", this.endpoint);

    this.setListener("httpClientData", this.httpDataHandle.bind(this));
  }

  httpDataHandle(event: Event) {
    // process data from socket
  }
}

httpConnector.ts (browser)

const dispatcher = new Dispatcher();
const connector = new HTTPConnector("http://127.0.0.1:8080", 1000); // pollInterval
dispatcher.setRoot(connector, new Address([]));

connector.readyPromise.then(() => {
  connector.send(connector.address.parent, {
    command: "httpClientData",
    data: {
      // event payload
    },
  });
}, (err: any) => {
  // connection error
  dispatcher.removeRoot();
});

Differences Between WebSocket and HTTP Connectors

HTTPEndpoint constructor accepts:

  • server — HTTP/HTTPS server (you must call listen() and close() manually).
  • sessionExpireTime — how long a session can live without communication (ms).
  • sessionPollTime — how long the server holds a poll request if no events are available (ms).
  • addresses — allowed addresses (Array or Set of Address instances).

HTTPConnector constructor accepts:

  • host — endpoint URL.
  • pollInterval — delay (ms) between the end of one poll and the next request.

Event Serialization & Custom Connectors

Dispatch provides tools for building your own connectors and transport mechanisms. Events can be serialized into binary form, transmitted through any channel, and deserialized back into valid Event objects.

Binary Data in Events

Events support binary payloads. You can pass Uint8Array inside event data, which makes it possible to transfer files — for example, via HTTPConnector (it uses multipart under the hood).

EndpointNode & ConnectionNode

EndpointNode is similar to a regular Node, but:

  • it implements the restriction mechanism (only allows events for whitelisted addresses),
  • it expects all its children (added via addChild) to be instances of ConnectionNode.

To see how this works in practice, check out the implementations of TCPEndpoint, TCPConnection, and TCPConnector.

Serialization Helpers

Dispatch provides a set of utility functions for converting events to and from binary:

SerializeEvent(event: Event): Uint8Array Serializes an event into a binary buffer.

DeserializeEvent(dispatcher: Dispatcher, data: Uint8Array): Event Restores a single event from binary.

DeserializeSequence(dispatcher: Dispatcher, data: Uint8Array): Event[] Works like DeserializeEvent, but can process a buffer containing multiple events.

Example:

import { Dispatcher, Event, Address, SerializeEvent, DeserializeEvent } from "@tripod311/dispatch";

const dispatcher = new Dispatcher();

const ev = new Event(dispatcher, new Address(["sender"]), new Address(["destination"]), {
  command: "someEvent",
  data: {},
});

// Serialize and restore
const serialized = SerializeEvent(ev);
const restored = DeserializeEvent(dispatcher, serialized);

StreamProcessor

The StreamProcessor class can receive data from a TCP socket (or any stream) and emit Event objects.

import { Dispatcher, Event, StreamProcessor } from "@tripod311/dispatch";

const dispatcher = new Dispatcher();

/* obtain socket somehow */

const processor = new StreamProcessor(dispatcher, socket);

processor.on("message", (event: Event) => {
  // process event, or forward into system
  event.dispatch();
});

// Cleanup
processor.destructor();

Custom Connectors

To implement your own connector:

  • Choose your transport (e.g. custom TCP, WebRTC, in-memory).
  • Use SerializeEvent and DeserializeEvent (or DeserializeSequence) to convert events to/from binary form.
  • Implement a ConnectionNode subclass that receives raw data, deserializes it into events, and dispatches them.
  • Optionally use StreamProcessor to simplify handling of TCP-like streams.

This gives you complete flexibility to design connectors for any communication channel while keeping Dispatch’s event-driven model intact.