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

@martini-kit/transport-trystero

v0.2.0

Published

P2P WebRTC transport for martini-kit. Multiplayer without networking servers.

Readme

@martini-kit/transport-trystero

P2P WebRTC transport for @martini-kit/core using Trystero.

Enables serverless peer-to-peer multiplayer with zero infrastructure costs.


Features

  • Zero server costs - Fully P2P via WebRTC
  • Sticky host pattern - First peer = permanent host (simple, predictable)
  • URL-based host selection - Jackbox-style room joining
  • Reliable messaging - Trystero handles message delivery
  • Auto host discovery - Finds existing host or becomes host
  • Host disconnect detection - Game ends if host leaves

Installation

pnpm add @martini-kit/transport-trystero @martini-kit/core trystero

Quick Start

Jackbox-Style Room Joining

import { TrysteroTransport } from '@martini-kit/transport-trystero';
import { GameRuntime, defineGame } from '@martini-kit/core';

// Determine host from URL
const urlParams = new URLSearchParams(window.location.search);
const roomId = urlParams.get('room');
const isHost = !roomId; // No room ID = host

// Generate room ID if host
const finalRoomId = isHost
  ? 'room-' + Math.random().toString(36).substring(2, 8)
  : roomId;

// Create transport with explicit host mode
const transport = new TrysteroTransport({
  roomId: finalRoomId,
  isHost: isHost // URL determines host!
});

// Create runtime
const runtime = new GameRuntime(gameLogic, transport, {
  isHost: isHost,
  playerIds: [transport.getPlayerId()]
});

// Show join link for clients
if (isHost) {
  const joinUrl = `${window.location.origin}?room=${finalRoomId}`;
  console.log('Share this link:', joinUrl);
}

How It Works

Sticky Host Pattern

HOST (opens without ?room param)
  ↓
  Creates new room ID
  ↓
  Shares link: https://game.com?room=ABC123
  ↓
CLIENT clicks link
  ↓
  Joins room ABC123
  ↓
  Connects to HOST via WebRTC
  ↓
  ✅ Game session established

Key Points:

  • Host is determined by URL (no ?room = host)
  • Host never changes during session
  • If host disconnects, game ends
  • Simple, predictable, works like Jackbox

WebRTC + Nostr Signaling

┌─────────┐         Nostr Relays        ┌─────────┐
│  HOST   │◄──────(signal only)────────►│ CLIENT  │
└─────────┘                              └─────────┘
     │                                        │
     └────────── WebRTC Direct P2P ──────────┘
             (game data flows here)
  • Nostr relays only for WebRTC signaling (establishing connection)
  • Game data flows directly peer-to-peer (no server)
  • Low latency, zero server costs
  • Decentralized protocol with 18+ relay redundancy

API

Constructor

new TrysteroTransport(options: TrysteroTransportOptions)

Options:

interface TrysteroTransportOptions {
  /** Unique room identifier for P2P session */
  roomId: string;

  /** Application ID for Trystero (prevents cross-app collisions) */
  appId?: string;

  /** Custom STUN/TURN servers for NAT traversal */
  rtcConfig?: RTCConfiguration;

  /**
   * Explicitly set this peer as host (industry standard: separate host/join URLs)
   * - true: This peer becomes host immediately
   * - false: This peer will never be host (always client)
   * - undefined: Automatic election (alphabetically lowest peer ID)
   */
  isHost?: boolean;
}

Example:

const transport = new TrysteroTransport({
  roomId: 'game-room-123',
  appId: 'my-game',
  isHost: true,
  rtcConfig: {
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' }
    ]
  }
});

Methods

Implements the Transport interface:

  • send(message, targetId?) - Send message to peer or broadcast
  • onMessage(handler) - Listen for messages
  • onPeerJoin(handler) - Listen for peer joins
  • onPeerLeave(handler) - Listen for peer leaves
  • getPlayerId() - Get this peer's ID
  • getPeerIds() - Get connected peer IDs
  • isHost() - Check if this peer is host

Additional Methods:

waitForReady(): Promise<void>

Wait for host discovery to complete (useful for automatic election mode).

const transport = new TrysteroTransport({ roomId: 'room-123' });
await transport.waitForReady();

const isHost = transport.isHost(); // Now reliable!

getCurrentHost(): string | null

Get the current host's peer ID.

const hostId = transport.getCurrentHost();
console.log('Host:', hostId);

onHostDisconnect(callback): () => void

Listen for host disconnection (game should end).

transport.onHostDisconnect(() => {
  alert('Host left the game!');
  window.location.reload();
});

getRoom(): Room

Get the Trystero room instance (for advanced use).

const room = transport.getRoom();

Host Selection Modes

1. URL-Based (Recommended)

Best for: Jackbox-style games, classroom multiplayer

const isHost = !new URLSearchParams(window.location.search).get('room');

const transport = new TrysteroTransport({
  roomId: isHost ? generateRoomId() : roomIdFromUrl,
  isHost: isHost // Explicit
});

Pros:

  • Predictable (URL determines everything)
  • User understands who's host
  • No race conditions

2. Automatic Election

Best for: Symmetric multiplayer (no designated host)

const transport = new TrysteroTransport({
  roomId: 'shared-room-123',
  isHost: undefined // Auto-elect
});

await transport.waitForReady(); // Wait for election
const isHost = transport.isHost();

Pros:

  • No URL manipulation needed
  • First peer auto-becomes host

Cons:

  • Race condition if multiple peers join simultaneously
  • Uses waitForReady() for host discovery

Host Discovery Protocol

When isHost: undefined (automatic mode), the transport performs active host discovery:

1. Broadcast "host_query" message
   ↓
2. Wait 3 seconds for "host_announce" response
   ↓
3a. If response received → Use announced host
3b. If no response && no peers → Become solo host
3c. If conflict (two hosts) → Deterministic tiebreaker (lowest ID)

Tiebreaker: If multiple peers think they're host, the peer with the alphabetically lowest ID wins.


NAT Traversal

WebRTC requires STUN/TURN servers for NAT traversal:

Default (Good for 90% of users)

const transport = new TrysteroTransport({
  roomId: 'room-123',
  // Uses Google's public STUN server
});

Custom TURN Server (99%+ success rate)

const transport = new TrysteroTransport({
  roomId: 'room-123',
  rtcConfig: {
    iceServers: [
      { urls: 'stun:stun.l.google.com:19302' },
      {
        urls: 'turn:your-turn-server.com',
        username: 'user',
        credential: 'pass'
      }
    ]
  }
});

When to use TURN:

  • Corporate firewalls
  • Symmetric NAT
  • 5-10% of users that STUN can't help

TURN Providers:


Testing

# Run tests
pnpm test

# Watch mode
pnpm test:watch

# Coverage
pnpm test:coverage

Current coverage: Comprehensive transport interface tests ✅


Development

# Build
pnpm build

# Watch mode
pnpm dev

# Clean
pnpm clean

Troubleshooting

Peers can't connect

Symptoms: onPeerJoin never fires, peers list stays empty

Solutions:

  1. Check if both peers use same roomId
  2. Check if both peers use same appId
  3. Try custom TURN server (NAT traversal)
  4. Check browser console for WebRTC errors

Host election conflicts

Symptoms: Both peers think they're host

Solutions:

  • Use URL-based mode (isHost: true/false)
  • Or use waitForReady() in automatic mode
  • Check for race conditions (both opening simultaneously)

Game ends when host refreshes

This is by design! Sticky host pattern = game ends if host leaves.

Solutions:

  • Implement host migration (advanced, not currently supported)
  • Or use WebSocket transport where server persists state

Limitations

P2P Limitations

  • No host migration: If host disconnects, game ends
  • NAT traversal: 5-10% of users may fail without TURN
  • Limited scale: 2-8 players optimal (WebRTC mesh doesn't scale)
  • No persistence: State lost when all peers disconnect

When to Use WebSocket Instead

  • Need host migration
  • Need 8+ players
  • Need state persistence
  • Corporate/enterprise users (strict firewalls)

See Also


License

MIT - See LICENSE