LocalTransport
LocalTransport is an in-memory transport implementation perfect for same-page multiplayer demos, unit tests, and local development. It uses a global registry to enable instant (0ms latency) communication between multiple game instances on the same page.
When to Use
✅ Perfect for:
- Side-by-side demo instances on the same page
- Unit and integration testing
- Local development without network overhead
- Prototyping multiplayer games quickly
❌ Not suitable for:
- Production games (no real networking)
- Cross-page/cross-tab communication
- Real-world latency simulation
Installation
pnpm add @martini-kit/transport-local
# or
npm install @martini-kit/transport-local API Reference
Constructor
class LocalTransport implements Transport {
constructor(config: LocalTransportConfig);
}
interface LocalTransportConfig {
roomId: string; // Unique room identifier
playerId?: string; // Optional custom player ID (auto-generated if omitted)
isHost: boolean; // Whether this instance is the host
} Properties
readonly playerId: string; // This peer's unique ID
readonly metrics: TransportMetrics; // Connection metrics and stats Methods
All methods from the Transport interface:
// Messaging
send(message: WireMessage, targetId?: string): void;
onMessage(handler: (message: WireMessage, senderId: string) => void): () => void;
// Peer lifecycle
onPeerJoin(handler: (peerId: string) => void): () => void;
onPeerLeave(handler: (peerId: string) => void): () => void;
getPeerIds(): string[];
// Identity
getPlayerId(): string;
isHost(): boolean;
// Cleanup
disconnect(): void; Quick Start
Single-Page Multiplayer Demo
import { LocalTransport } from '@martini-kit/transport-local';
import { GameRuntime } from '@martini-kit/core';
import { game } from './my-game';
// Create host instance
const hostTransport = new LocalTransport({
roomId: 'my-game-room',
isHost: true
});
const hostRuntime = new GameRuntime(game, hostTransport, {
isHost: true,
playerIds: [hostTransport.getPlayerId()]
});
// Create client instance
const clientTransport = new LocalTransport({
roomId: 'my-game-room', // Same room ID!
isHost: false
});
const clientRuntime = new GameRuntime(game, clientTransport, {
isHost: false,
playerIds: [hostTransport.getPlayerId(), clientTransport.getPlayerId()]
});
// Both instances are now connected!
console.log('Host sees peers:', hostTransport.getPeerIds()); // ["player-xxx"]
console.log('Client sees peers:', clientTransport.getPeerIds()); // ["player-yyy"] How It Works
LocalTransport uses a global in-memory registry (LocalTransportRegistry) to coordinate all instances in the same JavaScript context:
┌──────────────────────────────────────────┐
│ LocalTransportRegistry (Singleton) │
│ ┌────────────────────────────────────┐ │
│ │ Room: "my-game-room" │ │
│ │ ├─ LocalTransport (host) │ │
│ │ ├─ LocalTransport (client 1) │ │
│ │ └─ LocalTransport (client 2) │ │
│ └────────────────────────────────────┘ │
└──────────────────────────────────────────┘
▲ │
│ send() │ deliver()
│ ▼
LocalTransport ◄─────► Other instances Message Flow
- Send:
transport.send(message)→ Registry broadcasts to all peers in the same room - Receive: Registry calls
peer.deliver(message, senderId)on each peer - Handlers: Each peer’s
onMessagehandlers are invoked synchronously
Peer Discovery
When a new LocalTransport is created:
- Registry adds it to the room
- Existing peers are notified via
onPeerJoin - New peer is notified of existing peers via
onPeerJoin
When a transport calls disconnect():
- Registry removes it from the room
- Remaining peers are notified via
onPeerLeave
Examples
Testing Player Join/Leave
import { describe, it, expect } from 'vitest';
import { LocalTransport } from '@martini-kit/transport-local';
describe('Player Lifecycle', () => {
it('should notify peers when player joins', () => {
const host = new LocalTransport({ roomId: 'test', isHost: true });
let joinedPeer: string | null = null;
host.onPeerJoin((peerId) => {
joinedPeer = peerId;
});
// Create second peer - host should be notified
const client = new LocalTransport({ roomId: 'test', isHost: false });
expect(joinedPeer).toBe(client.getPlayerId());
expect(host.getPeerIds()).toContain(client.getPlayerId());
// Cleanup
host.disconnect();
client.disconnect();
});
it('should notify peers when player leaves', () => {
const host = new LocalTransport({ roomId: 'test', isHost: true });
const client = new LocalTransport({ roomId: 'test', isHost: false });
let leftPeer: string | null = null;
host.onPeerLeave((peerId) => {
leftPeer = peerId;
});
// Client disconnects
const clientId = client.getPlayerId();
client.disconnect();
expect(leftPeer).toBe(clientId);
expect(host.getPeerIds()).not.toContain(clientId);
// Cleanup
host.disconnect();
});
}); Testing Message Passing
it('should deliver messages between peers', () => {
const host = new LocalTransport({ roomId: 'test', isHost: true });
const client = new LocalTransport({ roomId: 'test', isHost: false });
let receivedMessage: any = null;
client.onMessage((message, senderId) => {
receivedMessage = { message, senderId };
});
// Host sends message
host.send({ type: 'action', payload: { x: 100 } });
// Client should receive it immediately
expect(receivedMessage).not.toBeNull();
expect(receivedMessage.message.type).toBe('action');
expect(receivedMessage.message.payload.x).toBe(100);
expect(receivedMessage.senderId).toBe(host.getPlayerId());
// Cleanup
host.disconnect();
client.disconnect();
}); Testing Unicast (Targeted Messages)
it('should send message to specific peer', () => {
const host = new LocalTransport({ roomId: 'test', isHost: true });
const client1 = new LocalTransport({ roomId: 'test', isHost: false });
const client2 = new LocalTransport({ roomId: 'test', isHost: false });
let client1Received = false;
let client2Received = false;
client1.onMessage(() => { client1Received = true; });
client2.onMessage(() => { client2Received = true; });
// Host sends to client1 only
host.send({ type: 'action', payload: 'test' }, client1.getPlayerId());
expect(client1Received).toBe(true);
expect(client2Received).toBe(false);
// Cleanup
host.disconnect();
client1.disconnect();
client2.disconnect();
}); Metrics
LocalTransport provides full metrics support:
const transport = new LocalTransport({ roomId: 'test', isHost: true });
// Connection state (always 'connected' for LocalTransport)
console.log(transport.metrics.getConnectionState()); // "connected"
// Message statistics
const stats = transport.metrics.getMessageStats();
console.log('Sent:', stats.sent);
console.log('Received:', stats.received);
console.log('Errors:', stats.errors);
// Peer count
console.log('Peers:', transport.metrics.getPeerCount());
// Reset stats
transport.metrics.resetStats(); Tracking Messages
const host = new LocalTransport({ roomId: 'test', isHost: true });
const client = new LocalTransport({ roomId: 'test', isHost: false });
// Send some messages
host.send({ type: 'state_sync', payload: {} });
host.send({ type: 'state_sync', payload: {} });
client.send({ type: 'action', payload: {} });
// Check stats
const hostStats = host.metrics.getMessageStats();
console.log('Host sent:', hostStats.sent); // 2
console.log('Host received:', hostStats.received); // 1
const clientStats = client.metrics.getMessageStats();
console.log('Client sent:', clientStats.sent); // 1
console.log('Client received:', clientStats.received); // 2 Advanced Usage
Custom Player IDs
const host = new LocalTransport({
roomId: 'my-game',
playerId: 'host-player', // Custom ID instead of auto-generated
isHost: true
});
console.log(host.getPlayerId()); // "host-player" Multiple Rooms
Different rooms are isolated from each other:
// Room 1
const room1Host = new LocalTransport({ roomId: 'room-1', isHost: true });
const room1Client = new LocalTransport({ roomId: 'room-1', isHost: false });
// Room 2
const room2Host = new LocalTransport({ roomId: 'room-2', isHost: true });
const room2Client = new LocalTransport({ roomId: 'room-2', isHost: false });
// Peers in different rooms don't see each other
console.log(room1Host.getPeerIds().length); // 1 (only room1Client)
console.log(room2Host.getPeerIds().length); // 1 (only room2Client) Host Disconnect Handling
When the host disconnects, clients are notified:
const host = new LocalTransport({ roomId: 'test', isHost: true });
const client = new LocalTransport({ roomId: 'test', isHost: false });
client.onHostDisconnect(() => {
console.log('Host disconnected! Game over.');
// Show "Host left" screen
});
host.disconnect(); // Client's onHostDisconnect handler is called Cleanup
Always call disconnect() when done to remove the transport from the global registry:
const transport = new LocalTransport({ roomId: 'test', isHost: true });
// Use the transport...
// Cleanup
transport.disconnect(); Important: Failing to call disconnect() will keep the transport in the global registry, which can cause memory leaks and unexpected behavior in tests.
Cleanup in Tests
import { describe, it, afterEach } from 'vitest';
let transports: LocalTransport[] = [];
afterEach(() => {
// Clean up all transports after each test
transports.forEach(t => t.disconnect());
transports = [];
});
it('should work', () => {
const host = new LocalTransport({ roomId: 'test', isHost: true });
const client = new LocalTransport({ roomId: 'test', isHost: false });
transports.push(host, client);
// Test logic...
// No manual cleanup needed - afterEach handles it
}); Performance Characteristics
- Latency: 0ms (synchronous, in-memory)
- Bandwidth: N/A (no network)
- Peer Limit: No practical limit (memory only)
- Message Size Limit: No limit (in-memory objects)
- Connection State: Always “connected”
Limitations
- Same Page Only: Only works within a single JavaScript context
- No Real Latency: Can’t simulate network delays (use custom transport for that)
- No Persistence: Messages are not persisted anywhere
- Dev Only: Not suitable for production games
Debugging
Enable Logging
import { Logger } from '@martini-kit/core';
const logger = new Logger('LocalTransport');
logger.log('Created transport:', transport.getPlayerId());
logger.log('Peers:', transport.getPeerIds()); Common Issues
Problem: Peers not discovering each other Solution: Ensure both use the same roomId
// ❌ Wrong - different room IDs
const host = new LocalTransport({ roomId: 'room-1', isHost: true });
const client = new LocalTransport({ roomId: 'room-2', isHost: false });
// ✅ Correct - same room ID
const host = new LocalTransport({ roomId: 'my-game', isHost: true });
const client = new LocalTransport({ roomId: 'my-game', isHost: false }); Problem: Tests failing with “unexpected peer” Solution: Call disconnect() in test cleanup to prevent cross-test contamination
afterEach(() => {
transport.disconnect(); // Always cleanup!
}); See Also
- Transports Overview
- IframeBridgeTransport - For iframe-based multiplayer
- TrysteroTransport - For P2P production games
- Testing Guide