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

@eden_labs/tree

v0.5.0

Published

Event manager for the Eden ecosystem

Downloads

19

Readme

@eden_labs/tree

Event manager for the Eden ecosystem. Fast, reliable event bus with at-least-once delivery guarantees — works on the same machine, across a local network, or across the internet through NAT via P2P hole punching and relay fallback.

How it works

Eden A                        Eden B
  |                             |
  | emit("eden:user:created")   |
  |──────── UDP ───────────────>|
  |                             | on("eden:user:created", handler)
  |<──────── ACK ───────────────|

Events are sent as UDP packets. The emitter retries automatically if no ACK arrives within timeoutMs. The receiver deduplicates repeated deliveries — your handler is never called twice for the same event.


Quick start

Same machine / same network (UdpTransport, default)

import { Eden } from "@eden_labs/tree";

const a = new Eden({
  listenPort: 5000,
  remote: { host: "127.0.0.1", port: 5001 },
});

const b = new Eden({
  listenPort: 5001,
  remote: { host: "127.0.0.1", port: 5000 },
});

b.on("eden:user:created", (envelope) => {
  console.log(envelope.payload); // { id: "123" }
});

b.on("eden:chat:message", (envelope) => {
  console.log(envelope.payload);
}, { room: "sala-1" });

a.emit("eden:user:created", { id: "123" });
a.emit("eden:chat:message", { text: "hi" }, { room: "sala-1" });

a.stop();
b.stop();

Multiple peers on a single socket (MultiUdpTransport)

When one process needs to communicate with many peers simultaneously, use MultiUdpTransport. It uses a single UDP socket regardless of how many peers are registered — saving file descriptors and event loop handles.

import { MultiUdpTransport } from "@eden_labs/tree";

const hub = new MultiUdpTransport();

// Register peers dynamically
hub.addPeer({ host: "192.168.1.10", port: 5000 });
hub.addPeer({ host: "192.168.1.11", port: 5000 });
hub.addPeer({ host: "192.168.1.12", port: 5000 });

// send() fans out to all registered peers
hub.send(Buffer.from("hello everyone"));

// bind() listens for messages from any peer on a single port
hub.bind(6000, (msg) => {
  console.log("received:", msg.toString());
});

// Remove a peer when it disconnects
hub.removePeer({ host: "192.168.1.12", port: 5000 });

hub.close();

Benchmark: overhead vs N individual UdpTransport instances is negligible (<2%), while using only 1 file descriptor regardless of N.


Across the internet (P2PTransport — NAT traversal)

For peers behind NAT (home networks, mobile, cloud VMs without public IP), use P2PTransport. It automatically tries:

  1. STUN — discovers your public IP/port
  2. UDP hole punching — opens a direct path through NAT (~85% of cases)
  3. WebSocket relay — fallback when hole punching fails (~15% of symmetric NAT)

You need a signaling server with a public IP — a small WebSocket server peers use to exchange endpoints before connecting. See Signaling server below.

import { Eden, P2PTransport } from "@eden_labs/tree";

const transport = new P2PTransport("peer-alice", "ws://your-signal-server:8080", {
  stunServers: [
    { host: "stun.l.google.com", port: 19302 },
    { host: "stun.cloudflare.com", port: 3478 },
  ],
  punchTimeoutMs: 3000,
  signalingTimeoutMs: 5000,
});

const alice = new Eden({
  listenPort: 0,
  remote: { host: "0.0.0.0", port: 0 },
  transport: () => transport,
});

// Connect to the other peer (both sides call connect simultaneously)
await transport.connect("peer-bob");

alice.on("eden:chat:message", (envelope) => {
  console.log(envelope.payload);
});

alice.emit("eden:chat:message", { text: "hello over the internet!" });

API

new Eden(options)

| Option | Type | Default | Description | |--------|------|---------|-------------| | listenPort | number | required | Local UDP port to listen on (0 = OS-assigned, for P2P) | | remote.host | string | required | Remote host to send events to | | remote.port | number | required | Remote port to send events to | | timeoutMs | number | 5000 | Time (ms) before retrying an unacknowledged event | | retryIntervalMs | number | 1000 | How often to check for expired events | | transport | (target) => EdenTransport | UdpTransport | Custom transport factory |

eden.on(type, handler, options?)

Registers a listener for an event type. Returns an unsubscribe function.

const unsubscribe = eden.on("eden:user:created", (envelope) => {
  console.log(envelope.type, envelope.payload);
});

// stop listening
unsubscribe();

Options:

  • room?: string — only receive events sent to this room

eden.emit(type, payload, options?)

Emits an event to the remote instance.

eden.emit("eden:user:created", { id: "1" });
eden.emit("eden:chat:message", { text: "hi" }, { room: "sala-1" });

Options:

  • room?: string — send to a specific room

eden.stop()

Closes all sockets and stops the retry interval. Must be called when the instance is no longer needed.

process.on("SIGTERM", () => {
  eden.stop();
  process.exit(0);
});

MultiUdpTransport

new MultiUdpTransport()

No constructor arguments. Creates a single UDP socket internally.

| Method | Description | |--------|-------------| | addPeer(endpoint) | Register a peer endpoint { host, port } | | removePeer(endpoint) | Unregister a peer endpoint | | getPeerCount() | Returns the number of currently registered peers | | send(msg) | Sends msg to all registered peers | | bind(port, onMessage) | Listens on port, calls onMessage for any received message | | close() | Closes the socket and clears all peers. Idempotent. |


P2PTransport

new P2PTransport(peerId, signalingUrl, options?)

| Option | Type | Default | Description | |--------|------|---------|-------------| | peerId | string | required | Unique ID for this peer | | signalingUrl | string | required | WebSocket URL of your signaling server | | stunServers | { host, port }[] | Google + Cloudflare STUN | STUN servers to discover public IP. Pass [] to skip STUN (same network) | | stunTimeoutMs | number | 3000 | Timeout per STUN attempt | | punchTimeoutMs | number | 3000 | Timeout for hole punching. Pass 0 to skip punching and go straight to relay | | signalingTimeoutMs | number | 5000 | Timeout for signaling server responses |

transport.connect(targetPeerId)

Executes the full connection sequence (STUN → register → hole punch → relay fallback). Both peers must call connect pointing at each other simultaneously.

// Both peers call this at roughly the same time
await Promise.all([
  transportAlice.connect("peer-bob"),
  transportBob.connect("peer-alice"),
]);

Signaling server

The signaling server is a small WebSocket server with a public IP that plays two roles:

Role 1 — Registry: peers exchange their public {host, port} before attempting hole punching. The server only stores endpoint metadata — it never sees your event payloads.

Role 2 — Relay: when hole punching fails (~15% of symmetric NAT cases), the server forwards encrypted UDP frames between peers over WebSocket. Both roles share the same URL and the same server process.

Protocol

// Role 1 — Registry (hole punching setup)
Client → Server: { type: "register", peerId, endpoint: { host, port } }
Server → Client: { type: "registered" }

Client → Server: { type: "request_connect", myId, targetId }
Server → Client: { type: "peer_endpoint", endpoint: { host, port } }
             or: { type: "error", reason: "peer_not_found" }

// Role 2 — Relay (symmetric NAT fallback)
Client → Server: { type: "identify", peerId }
Client → Server: { type: "relay", fromPeerId, targetPeerId, payload: base64 }
Server → Target: { type: "data", from: fromPeerId, payload: base64 }

P2PTransport first tries hole punching via the registry; if that fails, it falls back to relay — both use the same signalingUrl you passed to the constructor.

Minimal Node.js implementation

import { WebSocketServer, WebSocket } from "ws";

const server = new WebSocketServer({ port: 8080 });

interface PeerEntry {
  endpoint: { host: string; port: number };
  ws: WebSocket;
}

const peers = new Map<string, PeerEntry>();

server.on("connection", (ws) => {
  ws.on("message", (data) => {
    const msg = JSON.parse(data.toString());

    // Role 1 — register public endpoint for hole punching
    if (msg.type === "register") {
      peers.set(msg.peerId, { endpoint: msg.endpoint, ws });
      ws.send(JSON.stringify({ type: "registered" }));
    }

    // Role 1 — return remote peer's endpoint so both sides can punch
    if (msg.type === "request_connect") {
      const peer = peers.get(msg.targetId);
      if (peer) {
        ws.send(JSON.stringify({ type: "peer_endpoint", endpoint: peer.endpoint }));
      } else {
        ws.send(JSON.stringify({ type: "error", reason: "peer_not_found" }));
      }
    }

    // Role 2 — re-associate WebSocket with peerId after relay reconnect
    if (msg.type === "identify") {
      const existing = peers.get(msg.peerId);
      if (existing) peers.set(msg.peerId, { ...existing, ws });
    }

    // Role 2 — forward relay frame to target peer
    if (msg.type === "relay") {
      const target = peers.get(msg.targetPeerId);
      if (target?.ws.readyState === WebSocket.OPEN) {
        target.ws.send(
          JSON.stringify({ type: "data", from: msg.fromPeerId, payload: msg.payload })
        );
      }
    }
  });

  ws.on("close", () => {
    // clean up disconnected peers
    for (const [id, entry] of peers) {
      if (entry.ws === ws) peers.delete(id);
    }
  });
});

console.log("Signaling server listening on ws://0.0.0.0:8080");

Deployment notes

  • The server must have a public IP — it cannot itself be behind NAT
  • It holds state in memory; for multi-instance deployments you need a shared store (Redis, etc.)
  • For production, add auth to prevent arbitrary peers from relaying through your server

Event types

Must follow the format {namespace}:{domain}:{action}:

eden:user:created      ✓
eden:order:updated     ✓
eden:chat:message      ✓

user:created           ✗  (missing namespace)
created                ✗  (only one part)

Throws EdenInvalidEventTypeError if the format is invalid.


Envelope

Every event is wrapped in an envelope automatically:

{
  id: string;        // UUID v4 — used for deduplication
  type: string;      // "eden:user:created"
  payload: unknown;  // your event data
  timestamp: number; // Unix ms
  version: string;   // protocol version
  room?: string;     // present if emitted to a room
}

Delivery guarantees

| What | How | |------|-----| | At-least-once | Emitter retries until ACK received | | No duplicates | Receiver deduplicates by envelope id | | Effectively exactly-once | Both combined |


Rooms

  • No room → delivered to all listeners of that event type (broadcast)
  • With room → delivered only to listeners subscribed to that room
// only receives events emitted to "sala-1"
eden.on("eden:chat:message", handler, { room: "sala-1" });

// receives all "eden:chat:message" events regardless of room
eden.on("eden:chat:message", handler);

Errors

All errors extend EdenError:

| Error | When | |-------|------| | EdenInvalidEventTypeError | Event type doesn't follow {ns}:{domain}:{action} format | | EdenInvalidEnvelopeError | Received message is not valid JSON or missing required fields | | EdenStunTimeoutError | No STUN server responded within stunTimeoutMs | | EdenSignalingError | Signaling server returned an error or timed out |

import { EdenError, EdenSignalingError, EdenStunTimeoutError } from "@eden_labs/tree";

try {
  await transport.connect("peer-bob");
} catch (err) {
  if (err instanceof EdenStunTimeoutError) {
    console.error("Could not discover public IP — check STUN server");
  }
  if (err instanceof EdenSignalingError) {
    console.error("Peer not found or signaling server unreachable");
  }
}

Performance (loopback 127.0.0.1)

Measured with sequential ping-pong RTT (1000 samples):

| Transport | p50 | p95 | p99 | Throughput | |-----------|-----|-----|-----|------------| | UdpTransport (baseline) | 0.051 ms | 0.077 ms | 0.116 ms | 15,578 msg/s | | P2PTransport (hole punch) | 0.053 ms | 0.074 ms | 0.141 ms | 15,230 msg/s | | P2PTransport (relay) | 0.104 ms | 0.158 ms | 0.222 ms | 8,176 msg/s |

P2PTransport adds ~2.6% overhead after connection is established (hole punch path). The relay path adds ~103% because it goes through a WebSocket/TCP intermediary — still fast enough for most use cases, and only used as a fallback.

MultiUdpTransport — fanout tail latency (time until last of N peers receives, loopback):

| N peers | MultiUdpTransport (1 socket) p50 | N × UdpTransport (N sockets) p50 | overhead | |---------|----------------------------------|-----------------------------------|---------| | 10 | 0.104 ms | 0.104 ms | ~0% | | 50 | 0.474 ms | 0.465 ms | ~2% | | 200 | 1.881 ms | 1.895 ms | ~0% |

Latency scales with N peers due to N sequential socket.send() calls, not due to the abstraction. The single-socket overhead vs N individual transports is negligible.

Real-world latencies will be higher due to actual network RTT.

Run the benchmark yourself:

npm run bench

Development

npm test                 # run all unit tests
npm run test:watch       # watch mode
npm run test:integration # real network tests (STUN against stun.l.google.com)
npm run build            # compile to dist/
npm run bench            # transport benchmark

All code follows strict TDD — no production code without a failing test first.