Multiplayer Technical Architecture¶
This document provides detailed technical information about DaggerDice's multiplayer system, including connection handling, message flow, and deployment architecture.
Architecture Overview¶
DaggerDice uses a hybrid peer-to-peer and relay architecture built on Cloudflare Workers and Durable Objects to enable real-time multiplayer dice rolling sessions.
graph TB
A[Client Browser] -->|WebSocket| B[Cloudflare Worker]
B --> C[Durable Object<br/>Session Room]
C -->|Relay Messages| D[Other Clients]
D -->|WebSocket| B
E[Static Assets] -->|Served via| B
F[ASSETS Binding] --> E
G[Session State] -->|Managed by| C
H[Connection Pool] --> C
Connection Management¶
WebSocket Protocol¶
DaggerDice establishes WebSocket connections through Cloudflare Workers at the endpoint:
- Development: ws://localhost:8787/api/room/{sessionId}
- Production: wss://daggerdice.smileychris.workers.dev/api/room/{sessionId}
Connection Lifecycle¶
1. Connection Establishment¶
// Client connects to a 6-character session ID
const wsUrl = `${protocol}//${window.location.host}/api/room/${sessionId}`;
this.websocket = new WebSocket(wsUrl);
2. Player Registration¶
Upon connection, the client sends a JOIN_ANNOUNCEMENT
message:
{
type: 'JOIN_ANNOUNCEMENT',
player: {
id: string, // Generated client-side (8-char random)
name: string, // Sanitized player name (max 20 chars)
joinedAt: number, // Timestamp
lastSeen: number, // Timestamp
isActive: boolean // Always true on join
}
}
3. Peer Discovery¶
Existing players respond with PLAYER_RESPONSE
messages to share their information with the new player.
Reconnection Strategy¶
The system implements exponential backoff reconnection:
- Max Attempts: 5 reconnection attempts
- Initial Delay: 1 second
- Backoff: Exponential (delay × 2^attempt)
- Auto-Reconnect Triggers:
- Window focus events
- Visibility change events
- Connection loss (non-manual disconnects)
private reconnectDelay = 1000; // Start with 1 second
setTimeout(() => {
this.attemptReconnect(playerName);
}, this.reconnectDelay * Math.pow(2, this.reconnectAttempts));
Connection Health Monitoring¶
Heartbeat System¶
- Ping Interval: 30 seconds
- Pong Timeout: 10 seconds
- Health Check: Validates pong received within 60 seconds
public isConnectionHealthy(): boolean {
const timeSinceLastPong = Date.now() - this.lastPongReceived;
return timeSinceLastPong < 60000;
}
Message Types and Flow¶
Core Message Types¶
Message Type | Direction | Purpose |
---|---|---|
JOIN_ANNOUNCEMENT |
Client → Server | Player joins session |
PLAYER_RESPONSE |
Client → Server | Response to join announcement |
LEAVE_ANNOUNCEMENT |
Client → Server | Player leaves session |
ROLL |
Client → Server | Broadcast dice roll results |
HISTORY_SHARE |
Client → Server | Share roll history with new players |
PING / PONG |
Bidirectional | Connection health monitoring |
Message Relay Architecture¶
The Durable Object acts as a message relay:
private relayMessage(message: ClientMessage, senderWs: WebSocket): void {
const messageStr = JSON.stringify(message);
// Send to all connections except sender
for (const ws of this.connections) {
if (ws !== senderWs && ws.readyState === WebSocket.READY_STATE_OPEN) {
ws.send(messageStr);
}
}
}
Roll Data Broadcasting¶
When a player rolls dice, the complete roll data is broadcast:
interface SharedRollHistoryItem extends RollData {
playerId: string; // Unique player identifier
playerName: string; // Display name
timestamp: number; // UTC timestamp
}
Session Management¶
Session ID Format¶
- Length: 6 characters
- Character Set: Alphanumeric
[a-z0-9]
- Validation: Regex
/^[a-z0-9]{6}$/i
Durable Object State Management¶
Each session is managed by a dedicated Durable Object instance:
// Session routing
const roomId = env.SESSION_ROOMS.idFromName(sessionId);
const roomStub = env.SESSION_ROOMS.get(roomId);
Connection Pool Management¶
The Durable Object maintains a Set of active WebSocket connections:
private connections: Set<WebSocket> = new Set();
// Auto-cleanup on disconnect
server.addEventListener('close', () => {
this.connections.delete(server);
});
Roll History Synchronization¶
History Keeper Algorithm¶
To prevent duplicate history sharing, DaggerDice uses a deterministic history keeper selection:
- Selection Criteria: Alphabetically first player ID among connected players
- Exclusion Logic: When a new player joins, existing players (excluding the new player) determine the keeper
- History Sharing: Only the designated keeper sends
HISTORY_SHARE
messages
private isHistoryKeeperExcluding(excludePlayerId: string): boolean {
const existingPlayerIds = [this.playerId, ...this.connectedPlayers.keys()]
.filter(id => id !== excludePlayerId);
const uniquePlayerIds = [...new Set(existingPlayerIds)];
uniquePlayerIds.sort();
return uniquePlayerIds[0] === this.playerId;
}
History Filtering¶
Private GM rolls are filtered during history sharing:
const publicHistory = this.localRollHistory.filter(roll =>
!(roll.rollType === 'gm' && roll.gmPrivate)
);
Security and Privacy¶
Input Sanitization¶
Player names are sanitized to prevent XSS:
private sanitizePlayerName(name: string): string {
const sanitized = name.trim().substring(0, 20);
return sanitized.replace(/<[^>]*>/g, '') || 'Anonymous';
}
Connection Security¶
- HTTPS Required: Production multiplayer requires HTTPS
- Origin Validation: WebSocket connections validated by Cloudflare Workers
- Rate Limiting: Implicit through Cloudflare Workers limits
Privacy Features¶
- Streamer Mode: Hides room codes and QR codes
- Private GM Rolls: Can be excluded from shared history
- Local Storage: Session data stored locally, not server-side
Deployment Architecture¶
Cloudflare Workers Configuration¶
# wrangler.toml
[assets]
directory = "./dist/client"
binding = "ASSETS"
[[durable_objects.bindings]]
name = "SESSION_ROOMS"
class_name = "SessionDurableObject"
Request Routing¶
- WebSocket Requests:
/api/room/{sessionId}
→ Durable Object - Static Assets:
/*
→ ASSETS binding - SPA Routing: 404 non-file requests →
index.html
Durable Object Lifecycle¶
- Creation: On-demand when first client connects to session ID
- Persistence: Automatic hibernation when no active connections
- Cleanup: Automatic garbage collection after inactivity
Error Handling and Resilience¶
Connection Failure Recovery¶
// Connection state management
export type ConnectionState = 'disconnected' | 'connecting' | 'connected' | 'error';
// Graceful degradation
if (!this.available) {
console.warn('Session features not available');
return false; // Fall back to solo mode
}
Message Delivery Guarantees¶
- At-most-once delivery: No message duplication
- Best-effort delivery: No guaranteed delivery (WebSocket nature)
- Idempotent operations: Roll history can handle duplicate entries
Browser Compatibility¶
- WebSocket Support: Required for multiplayer functionality
- LocalStorage: Used for session persistence
- Modern Browsers: ES6+ features used throughout
Performance Characteristics¶
Scalability Limits¶
- Concurrent Connections: Limited by Cloudflare Workers WebSocket limits
- Message Throughput: Optimized for low-frequency dice rolls
- Memory Usage: Minimal state stored in Durable Objects
Latency Optimization¶
- Edge Deployment: Cloudflare's global edge network
- Message Size: Minimal JSON payloads
- Connection Reuse: Persistent WebSocket connections
Monitoring and Debugging¶
Client-Side Logging¶
console.log('WebSocket connected');
console.log('Broadcasting roll with player ID:', playerId);
console.warn('Connection failed after multiple attempts');
Server-Side Observability¶
- Durable Object Logs: Available in Cloudflare dashboard
- WebSocket Metrics: Connection counts and message rates
- Error Tracking: Built-in Cloudflare Workers error reporting
Development and Testing¶
Local Development Setup¶
# Start local worker with Wrangler
npm run dev:worker
# WebSocket endpoint becomes available at:
# ws://localhost:8787/api/room/{sessionId}
Testing Strategy¶
- Unit Tests: Session client logic (
session-client.test.ts
) - Integration Tests: Worker functionality (
worker.test.ts
) - Manual Testing: Real WebSocket connections in development
This architecture provides a robust, scalable foundation for real-time multiplayer dice rolling while maintaining simplicity and cost-effectiveness through Cloudflare's serverless platform.