🎩 You're Invited:Meet the Socket team at Black Hat in Las Vegas, August 3-6.RSVP
Sign In

@martini-kit/transport-ws

Package Overview
Dependencies
Maintainers
1
Versions
2
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@martini-kit/transport-ws

WebSocket transport for @martini-kit/core - production-ready client-server networking

latest
Source
npmnpm
Version
0.2.0
Version published
Maintainers
1
Created
Source

@martini-kit/transport-ws

Production-ready WebSocket transport for martini-kit multiplayer games.

Use this transport when you need:

  • Server-authoritative games
  • Scalable infrastructure
  • Room management
  • Backend integration (auth, persistence, matchmaking)

Quick Start

1. Install

pnpm add @martini-kit/transport-ws @martini-kit/core @martini-kit/phaser

2. Start the Server

Create a relay server using the example from examples/server.ts:

pnpm tsx examples/server.ts

Or use the provided example:

import { WebSocketServer, WebSocket } from 'ws';

class martini-kitServer {
  private wss: WebSocketServer;
  private players = new Map<WebSocket, Player>();
  private rooms = new Map<string, Room>();

  constructor(port: number) {
    this.wss = new WebSocketServer({ port });
    console.log(`[martini-kitServer] Listening on port ${port}`);

    this.wss.on('connection', (ws) => {
      ws.on('message', (data) => {
        const message = JSON.parse(data.toString());
        this.handleMessage(ws, message);
      });
    });
  }

  private handleMessage(ws: WebSocket, message: any): void {
    const { type } = message;

    switch (type) {
      case 'handshake':
        this.handleHandshake(ws, message);
        break;
      case 'join_room':
        this.handleJoinRoom(ws, message);
        break;
      default:
        // Relay messages to appropriate recipients
        this.forwardMessage(ws, message);
        break;
    }
  }
}

const PORT = parseInt(process.env.PORT || '8080');
new martini-kitServer(PORT);

3. Connect Clients

import { defineGame } from '@martini-kit/core';
import { PhaserAdapter } from '@martini-kit/phaser';
import { WebSocketTransport } from '@martini-kit/transport-ws';

const game = defineGame({
  setup: ({ playerIds }) => ({
    players: Object.fromEntries(
      playerIds.map(id => [id, { x: 100, y: 100, score: 0 }])
    )
  }),

  actions: {
    move: {
      input: { x: 'number', y: 'number' },
      apply(state, context, input) {
        state.players[context.targetId].x = input.x;
        state.players[context.targetId].y = input.y;
      }
    }
  }
});

// Create transport
const transport = new WebSocketTransport('ws://localhost:8080', {
  playerId: 'player-123', // Optional - auto-generated if not provided
  reconnect: true,
  reconnectDelay: 1000,
  maxReconnectAttempts: 10
});

// Wait for connection
await transport.waitForReady();

// Join a room
transport.send({ type: 'join_room', roomId: 'room-abc' });

// Start Phaser adapter
PhaserAdapter.start({
  game,
  transport,
  scenes: [GameScene]
});

API Reference

WebSocketTransport

class WebSocketTransport implements Transport {
  constructor(url: string, config?: WebSocketTransportConfig);

  // Transport interface
  send(message: WireMessage, targetId?: string): void;
  onMessage(handler: MessageHandler): () => void;
  onPeerJoin(handler: PeerHandler): () => void;
  onPeerLeave(handler: PeerHandler): () => void;
  getPlayerId(): string;
  getPeerIds(): string[];
  isHost(): boolean;

  // Additional methods
  waitForReady(): Promise<void>;
  onError(handler: ErrorHandler): () => void;
  disconnect(): void;
}

WebSocketTransportConfig

interface WebSocketTransportConfig {
  /** Player ID (generated if not provided) */
  playerId?: string;

  /** Enable automatic reconnection (default: true) */
  reconnect?: boolean;

  /** Reconnection delay in ms (default: 1000) */
  reconnectDelay?: number;

  /** Max reconnection attempts (default: Infinity) */
  maxReconnectAttempts?: number;
}

Server Implementation

The WebSocket server acts as a relay - it forwards messages between clients without running game logic. The host client runs the actual game logic.

Message Protocol

Client → Server:

// Handshake (sent automatically on connection)
{ type: 'handshake', playerId: 'player-123' }

// Join room
{ type: 'join_room', roomId: 'room-abc' }

// Leave room
{ type: 'leave_room' }

// Game messages (relayed to other players)
{
  type: 'action',
  senderId: 'player-123',
  targetId: 'player-456', // Optional - omit for broadcast
  payload: { actionName: 'move', input: { x: 100, y: 200 } }
}

Server → Client:

// Handshake confirmation
{ type: 'handshake_ack', playerId: 'player-123' }

// Current peers in room
{ type: 'peers_list', payload: { peers: ['player-456', 'player-789'] } }

// Host announcement
{ type: 'host_announce', hostId: 'player-123' }

// Player joined
{ type: 'player_join', payload: { playerId: 'player-456' } }

// Player left
{ type: 'player_leave', payload: { playerId: 'player-456' } }

// Relayed game messages
{
  type: 'action',
  senderId: 'player-456',
  payload: { actionName: 'move', input: { x: 100, y: 200 } }
}

Host Election

The server automatically elects the first player in a room as the host:

private handleJoinRoom(ws: WebSocket, message: any): void {
  const room = this.rooms.get(roomId);

  // First player becomes host
  if (!room.hostId) {
    room.hostId = player.id;
    console.log(`Player ${player.id} is now host`);
  }

  // Announce host to all players
  this.send(ws, {
    type: 'host_announce',
    hostId: room.hostId
  });
}

When the host disconnects, the server elects a new host:

private removePlayerFromRoom(player: Player, room: Room): void {
  const wasHost = room.hostId === player.id;

  room.players.delete(player.id);

  // Notify remaining players
  this.broadcast(room, {
    type: 'player_leave',
    payload: { playerId: player.id }
  });

  // Re-elect host if needed
  if (wasHost && room.players.size > 0) {
    const newHostId = Array.from(room.players.keys())[0];
    room.hostId = newHostId;

    this.broadcast(room, {
      type: 'host_announce',
      hostId: newHostId
    });
  }
}

Error Handling

The transport provides error callbacks for connection issues:

const transport = new WebSocketTransport('ws://localhost:8080');

transport.onError((error) => {
  console.error('WebSocket error:', error);

  // Handle connection failures
  if (error.message.includes('ECONNREFUSED')) {
    alert('Cannot connect to server. Please try again later.');
  }
});

Reconnection

By default, the transport automatically reconnects on disconnect:

const transport = new WebSocketTransport('ws://localhost:8080', {
  reconnect: true,
  reconnectDelay: 1000, // Wait 1 second between attempts
  maxReconnectAttempts: 10 // Give up after 10 attempts
});

The transport maintains the same playerId across reconnections, so the server can restore the player's session.

Production Deployment

Scaling Considerations

Horizontal Scaling:

  • Use a load balancer (e.g., Nginx, HAProxy) to distribute connections
  • Each server instance manages its own set of rooms
  • Consider sticky sessions to keep players on the same instance

Room Distribution:

  • Store room state in Redis for multi-instance deployments
  • Use pub/sub to relay messages between server instances

Example with Redis:

import { createClient } from 'redis';

class Scalablemartini-kitServer {
  private redis = createClient();

  async handleJoinRoom(ws: WebSocket, message: any): Promise<void> {
    const { roomId } = message;

    // Subscribe to room channel
    await this.redis.subscribe(`room:${roomId}`, (message) => {
      // Forward messages to local players
      this.broadcast(roomId, JSON.parse(message));
    });

    // Store player in room
    await this.redis.sAdd(`room:${roomId}:players`, player.id);
  }

  private forwardMessage(ws: WebSocket, message: any): void {
    const { roomId } = this.players.get(ws);

    // Publish to Redis for all server instances
    this.redis.publish(`room:${roomId}`, JSON.stringify(message));
  }
}

Security

Authentication:

this.wss.on('connection', async (ws, req) => {
  const token = new URL(req.url, 'ws://localhost').searchParams.get('token');

  // Verify JWT token
  const user = await verifyToken(token);
  if (!user) {
    ws.close(1008, 'Unauthorized');
    return;
  }

  // Associate player with authenticated user
  const player: Player = {
    id: user.id,
    ws,
    roomId: null
  };

  this.players.set(ws, player);
});

Rate Limiting:

private rateLimiter = new Map<string, number>();

private handleMessage(ws: WebSocket, message: any): void {
  const player = this.players.get(ws);

  // Simple rate limit (100 messages per second)
  const now = Date.now();
  const count = this.rateLimiter.get(player.id) || 0;

  if (count > 100 && now - (count as any).timestamp < 1000) {
    ws.close(1008, 'Rate limit exceeded');
    return;
  }

  this.rateLimiter.set(player.id, count + 1);

  this.handleMessageInternal(ws, message);
}

Comparison: WebSocket vs P2P

FeatureWebSocket (@martini-kit/transport-ws)P2P (@martini-kit/transport-p2p)
ArchitectureClient-server (relay)Peer-to-peer (WebRTC)
LatencyHigher (2 hops: client → server → client)Lower (direct connection)
NAT TraversalNot needed (server has public IP)Required (STUN/TURN servers)
ScalabilityScales horizontally (add servers)Limited by peer bandwidth
Backend IntegrationEasy (auth, persistence, matchmaking)Hard (needs coordination server)
InfrastructureRequires serverCan work without server
Connection ReliabilityHigh (server is stable)Medium (peers may disconnect)
Use CasesProduction games, competitive gamesPrototypes, local multiplayer, casual games

Rule of thumb:

  • Use WebSocket for production games, games with backend needs, or games requiring >4 players
  • Use P2P for prototypes, game jams, or simple local multiplayer

Integration with Colyseus

You can use Colyseus for room management and martini-kit for game logic:

Server (Colyseus):

import { Room } from 'colyseus';

class GameRoom extends Room {
  onCreate() {
    // Just relay martini-kit actions
    this.onMessage('action', (client, action) => {
      this.broadcast('action', action, { except: client });
    });
  }
}

Client (martini-kit):

import { Client } from 'colyseus.js';

const client = new Client('ws://localhost:2567');
const room = await client.joinOrCreate('game_room');

// Bridge Colyseus to WebSocketTransport
class ColyseusTransport implements Transport {
  constructor(private room: Room) {
    room.onMessage('action', (message) => {
      this.notifyMessage(message, message.senderId);
    });
  }

  send(message: WireMessage, targetId?: string): void {
    this.room.send('action', { ...message, senderId: this.getPlayerId(), targetId });
  }

  // ... implement other Transport methods
}

const transport = new ColyseusTransport(room);
PhaserAdapter.start({ game, transport });

See 08-platform-comparison.md for more integration patterns.

Troubleshooting

"WebSocket connection failed"

Cause: Server is not running or wrong URL

Fix:

# Start the example server
cd @martini-kit/transport-ws
pnpm tsx examples/server.ts

# Check server is listening
curl http://localhost:8080 # Should fail (WebSocket only)

"Cannot connect after disconnect"

Cause: Reconnection disabled or max attempts reached

Fix:

const transport = new WebSocketTransport('ws://localhost:8080', {
  reconnect: true,
  maxReconnectAttempts: Infinity // Never give up
});

"Messages not received by other players"

Cause: Server not relaying messages, or players in different rooms

Fix:

// Ensure all players join the same room
transport.send({ type: 'join_room', roomId: 'room-123' });

// Wait for peers_list before sending game messages
transport.onMessage((message) => {
  if (message.type === 'peers_list') {
    console.log('Connected peers:', message.payload.peers);
  }
});

Examples

See examples/ for:

  • server.ts - Basic relay server with room management
  • client.html - Browser client example
  • integration.test.ts - End-to-end test with real server

Contributing

Contributions welcome! Please:

  • Add tests for new features
  • Update this README
  • Run pnpm test before submitting PR

See CONTRIBUTING.md for guidelines.

License

MIT

FAQs

Package last updated on 04 Dec 2025

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts