Durable Objects System
Overview
PadawanForge implements a sophisticated Durable Objects system that provides stateful, real-time communication capabilities. This system includes ChatLobby for managing chat rooms and RoomManager for coordinating game sessions with persistent state management.
Architecture
Core Components
- ChatLobby: Real-time chat room management
- RoomManager: Game session coordination
- WebSocket Management: Connection handling and message routing
- State Persistence: Durable state across restarts
- Message Broadcasting: Real-time message distribution
Durable Objects Features
- Stateful Sessions: Persistent state across Cloudflare Worker restarts
- Real-time Communication: WebSocket-based messaging
- Room Management: Dynamic room creation and management
- Player Coordination: Multi-player session handling
- Message History: Persistent chat and game message storage
ChatLobby Implementation
Core ChatLobby Class
import { ChatLobby } from '@/durable-objects/ChatLobby';
// ChatLobby manages real-time chat rooms with persistent state
class ChatLobby {
private state: DurableObjectState;
private env: Env;
private sessions: Map<string, WebSocket>;
private rooms: Map<string, ChatRoom>;
private messageHistory: Map<string, ChatMessage[]>;
constructor(state: DurableObjectState, env: Env) {
this.state = state;
this.env = env;
this.sessions = new Map();
this.rooms = new Map();
this.messageHistory = new Map();
}
// Handle WebSocket connections
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/websocket') {
return this.handleWebSocket(request);
}
return new Response('Not found', { status: 404 });
}
// WebSocket connection handler
private async handleWebSocket(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
server.addEventListener('message', (event) => {
this.handleMessage(server, event.data);
});
server.addEventListener('close', () => {
this.handleDisconnect(server);
});
return new Response(null, { status: 101, webSocket: client });
}
}
Chat Room Management
interface ChatRoom {
id: string;
name: string;
participants: Set<string>;
maxParticipants: number;
isPrivate: boolean;
createdAt: Date;
lastActivity: Date;
}
interface ChatMessage {
id: string;
roomId: string;
senderId: string;
senderName: string;
content: string;
timestamp: Date;
type: 'text' | 'system' | 'join' | 'leave';
}
// Room management methods
class ChatLobby {
// Create a new chat room
async createRoom(roomData: {
id: string;
name: string;
maxParticipants?: number;
isPrivate?: boolean;
}): Promise<ChatRoom> {
const room: ChatRoom = {
id: roomData.id,
name: roomData.name,
participants: new Set(),
maxParticipants: roomData.maxParticipants || 50,
isPrivate: roomData.isPrivate || false,
createdAt: new Date(),
lastActivity: new Date()
};
this.rooms.set(room.id, room);
this.messageHistory.set(room.id, []);
// Persist room data
await this.state.storage.put(`room:${room.id}`, room);
return room;
}
// Join a chat room
async joinRoom(roomId: string, playerId: string, playerName: string): Promise<boolean> {
const room = this.rooms.get(roomId);
if (!room) return false;
if (room.participants.size >= room.maxParticipants) {
return false;
}
room.participants.add(playerId);
room.lastActivity = new Date();
// Add join message
const joinMessage: ChatMessage = {
id: generateMessageId(),
roomId,
senderId: 'system',
senderName: 'System',
content: `${playerName} joined the room`,
timestamp: new Date(),
type: 'join'
};
await this.addMessage(roomId, joinMessage);
await this.broadcastToRoom(roomId, {
type: 'user_joined',
playerId,
playerName,
participants: Array.from(room.participants)
});
return true;
}
// Leave a chat room
async leaveRoom(roomId: string, playerId: string, playerName: string): Promise<void> {
const room = this.rooms.get(roomId);
if (!room) return;
room.participants.delete(playerId);
room.lastActivity = new Date();
// Add leave message
const leaveMessage: ChatMessage = {
id: generateMessageId(),
roomId,
senderId: 'system',
senderName: 'System',
content: `${playerName} left the room`,
timestamp: new Date(),
type: 'leave'
};
await this.addMessage(roomId, leaveMessage);
await this.broadcastToRoom(roomId, {
type: 'user_left',
playerId,
playerName,
participants: Array.from(room.participants)
});
}
// Get room information
async getRoomInfo(roomId: string): Promise<ChatRoom | null> {
return this.rooms.get(roomId) || null;
}
// List all rooms
async listRooms(): Promise<ChatRoom[]> {
return Array.from(this.rooms.values());
}
}
Message Handling
// Message processing and broadcasting
class ChatLobby {
// Handle incoming WebSocket messages
private async handleMessage(websocket: WebSocket, data: any): Promise<void> {
try {
const message = JSON.parse(data as string);
switch (message.type) {
case 'join_room':
await this.handleJoinRoom(websocket, message);
break;
case 'leave_room':
await this.handleLeaveRoom(websocket, message);
break;
case 'send_message':
await this.handleSendMessage(websocket, message);
break;
case 'get_history':
await this.handleGetHistory(websocket, message);
break;
case 'ping':
websocket.send(JSON.stringify({ type: 'pong' }));
break;
default:
websocket.send(JSON.stringify({
type: 'error',
message: 'Unknown message type'
}));
}
} catch (error) {
websocket.send(JSON.stringify({
type: 'error',
message: 'Invalid message format'
}));
}
}
// Handle join room request
private async handleJoinRoom(websocket: WebSocket, message: any): Promise<void> {
const { roomId, playerId, playerName } = message;
const success = await this.joinRoom(roomId, playerId, playerName);
if (success) {
// Store websocket connection
this.sessions.set(playerId, websocket);
// Send room info
const room = await this.getRoomInfo(roomId);
websocket.send(JSON.stringify({
type: 'room_joined',
room,
participants: Array.from(room!.participants)
}));
} else {
websocket.send(JSON.stringify({
type: 'join_failed',
message: 'Failed to join room'
}));
}
}
// Handle leave room request
private async handleLeaveRoom(websocket: WebSocket, message: any): Promise<void> {
const { roomId, playerId, playerName } = message;
await this.leaveRoom(roomId, playerId, playerName);
// Remove websocket connection
this.sessions.delete(playerId);
websocket.send(JSON.stringify({
type: 'room_left',
roomId
}));
}
// Handle send message request
private async handleSendMessage(websocket: WebSocket, message: any): Promise<void> {
const { roomId, playerId, playerName, content } = message;
const chatMessage: ChatMessage = {
id: generateMessageId(),
roomId,
senderId: playerId,
senderName: playerName,
content,
timestamp: new Date(),
type: 'text'
};
await this.addMessage(roomId, chatMessage);
await this.broadcastToRoom(roomId, {
type: 'new_message',
message: chatMessage
});
}
// Handle get history request
private async handleGetHistory(websocket: WebSocket, message: any): Promise<void> {
const { roomId, limit = 50 } = message;
const history = await this.getMessageHistory(roomId, limit);
websocket.send(JSON.stringify({
type: 'message_history',
roomId,
messages: history
}));
}
// Add message to history
private async addMessage(roomId: string, message: ChatMessage): Promise<void> {
const history = this.messageHistory.get(roomId) || [];
history.push(message);
// Keep only last 1000 messages
if (history.length > 1000) {
history.splice(0, history.length - 1000);
}
this.messageHistory.set(roomId, history);
// Persist message
await this.state.storage.put(`message:${message.id}`, message);
}
// Get message history
private async getMessageHistory(roomId: string, limit: number): Promise<ChatMessage[]> {
const history = this.messageHistory.get(roomId) || [];
return history.slice(-limit);
}
// Broadcast message to room participants
private async broadcastToRoom(roomId: string, message: any): Promise<void> {
const room = this.rooms.get(roomId);
if (!room) return;
const messageStr = JSON.stringify(message);
for (const playerId of room.participants) {
const websocket = this.sessions.get(playerId);
if (websocket && websocket.readyState === WebSocket.READY_STATE_OPEN) {
websocket.send(messageStr);
}
}
}
// Handle WebSocket disconnection
private async handleDisconnect(websocket: WebSocket): Promise<void> {
// Find and remove the disconnected session
for (const [playerId, session] of this.sessions.entries()) {
if (session === websocket) {
this.sessions.delete(playerId);
// Remove from all rooms
for (const room of this.rooms.values()) {
if (room.participants.has(playerId)) {
await this.leaveRoom(room.id, playerId, 'Unknown Player');
}
}
break;
}
}
}
}
RoomManager Implementation
Core RoomManager Class
import { RoomManager } from '@/durable-objects/RoomManager';
// RoomManager coordinates game sessions and player state
class RoomManager {
private state: DurableObjectState;
private env: Env;
private rooms: Map<string, GameRoom>;
private sessions: Map<string, WebSocket>;
private gameStates: Map<string, GameState>;
constructor(state: DurableObjectState, env: Env) {
this.state = state;
this.env = env;
this.rooms = new Map();
this.sessions = new Map();
this.gameStates = new Map();
}
// Handle HTTP requests and WebSocket connections
async fetch(request: Request): Promise<Response> {
const url = new URL(request.url);
if (url.pathname === '/websocket') {
return this.handleWebSocket(request);
}
if (url.pathname === '/rooms') {
return this.handleRoomsRequest(request);
}
return new Response('Not found', { status: 404 });
}
}
Game Room Management
interface GameRoom {
id: string;
name: string;
gameType: 'cognitive' | 'social' | 'competitive';
maxPlayers: number;
currentPlayers: Set<string>;
status: 'waiting' | 'playing' | 'finished';
createdAt: Date;
startedAt?: Date;
endedAt?: Date;
settings: GameSettings;
}
interface GameState {
roomId: string;
currentTurn: number;
players: PlayerState[];
gameData: any;
lastUpdate: Date;
}
interface PlayerState {
playerId: string;
playerName: string;
score: number;
status: 'ready' | 'playing' | 'finished';
lastActivity: Date;
}
interface GameSettings {
difficulty: 'easy' | 'medium' | 'hard';
duration: number; // in seconds
maxRounds: number;
allowSpectators: boolean;
}
// Room management methods
class RoomManager {
// Create a new game room
async createRoom(roomData: {
id: string;
name: string;
gameType: GameRoom['gameType'];
maxPlayers?: number;
settings?: Partial<GameSettings>;
}): Promise<GameRoom> {
const room: GameRoom = {
id: roomData.id,
name: roomData.name,
gameType: roomData.gameType,
maxPlayers: roomData.maxPlayers || 4,
currentPlayers: new Set(),
status: 'waiting',
createdAt: new Date(),
settings: {
difficulty: 'medium',
duration: 420, // 7 minutes
maxRounds: 10,
allowSpectators: true,
...roomData.settings
}
};
this.rooms.set(room.id, room);
// Initialize game state
const gameState: GameState = {
roomId: room.id,
currentTurn: 0,
players: [],
gameData: {},
lastUpdate: new Date()
};
this.gameStates.set(room.id, gameState);
// Persist room data
await this.state.storage.put(`room:${room.id}`, room);
await this.state.storage.put(`gamestate:${room.id}`, gameState);
return room;
}
// Join a game room
async joinRoom(roomId: string, playerId: string, playerName: string): Promise<boolean> {
const room = this.rooms.get(roomId);
if (!room) return false;
if (room.status !== 'waiting') {
return false; // Game already started
}
if (room.currentPlayers.size >= room.maxPlayers) {
return false; // Room is full
}
room.currentPlayers.add(playerId);
// Add player to game state
const gameState = this.gameStates.get(roomId);
if (gameState) {
gameState.players.push({
playerId,
playerName,
score: 0,
status: 'ready',
lastActivity: new Date()
});
}
// Broadcast player joined
await this.broadcastToRoom(roomId, {
type: 'player_joined',
playerId,
playerName,
players: gameState?.players || []
});
return true;
}
// Start a game
async startGame(roomId: string, initiatorId: string): Promise<boolean> {
const room = this.rooms.get(roomId);
if (!room) return false;
// Check if initiator is in the room
if (!room.currentPlayers.has(initiatorId)) {
return false;
}
// Check if enough players
if (room.currentPlayers.size < 2) {
return false;
}
room.status = 'playing';
room.startedAt = new Date();
// Initialize game session
await this.initializeGameSession(roomId);
// Broadcast game started
await this.broadcastToRoom(roomId, {
type: 'game_started',
roomId,
startedAt: room.startedAt
});
return true;
}
// End a game
async endGame(roomId: string): Promise<void> {
const room = this.rooms.get(roomId);
if (!room) return;
room.status = 'finished';
room.endedAt = new Date();
// Calculate final scores
const gameState = this.gameStates.get(roomId);
if (gameState) {
const finalScores = gameState.players.map(player => ({
playerId: player.playerId,
playerName: player.playerName,
finalScore: player.score
}));
// Broadcast game ended
await this.broadcastToRoom(roomId, {
type: 'game_ended',
roomId,
endedAt: room.endedAt,
finalScores
});
}
}
// Update player score
async updateScore(roomId: string, playerId: string, score: number): Promise<void> {
const gameState = this.gameStates.get(roomId);
if (!gameState) return;
const player = gameState.players.find(p => p.playerId === playerId);
if (player) {
player.score = score;
player.lastActivity = new Date();
gameState.lastUpdate = new Date();
// Broadcast score update
await this.broadcastToRoom(roomId, {
type: 'score_updated',
playerId,
score,
players: gameState.players
});
}
}
}
WebSocket Message Handling
// WebSocket message processing for RoomManager
class RoomManager {
// Handle WebSocket connections
private async handleWebSocket(request: Request): Promise<Response> {
const pair = new WebSocketPair();
const [client, server] = Object.values(pair);
server.accept();
server.addEventListener('message', (event) => {
this.handleMessage(server, event.data);
});
server.addEventListener('close', () => {
this.handleDisconnect(server);
});
return new Response(null, { status: 101, webSocket: client });
}
// Handle incoming messages
private async handleMessage(websocket: WebSocket, data: any): Promise<void> {
try {
const message = JSON.parse(data as string);
switch (message.type) {
case 'join_room':
await this.handleJoinRoom(websocket, message);
break;
case 'leave_room':
await this.handleLeaveRoom(websocket, message);
break;
case 'start_game':
await this.handleStartGame(websocket, message);
break;
case 'update_score':
await this.handleUpdateScore(websocket, message);
break;
case 'game_action':
await this.handleGameAction(websocket, message);
break;
case 'ping':
websocket.send(JSON.stringify({ type: 'pong' }));
break;
default:
websocket.send(JSON.stringify({
type: 'error',
message: 'Unknown message type'
}));
}
} catch (error) {
websocket.send(JSON.stringify({
type: 'error',
message: 'Invalid message format'
}));
}
}
// Handle join room request
private async handleJoinRoom(websocket: WebSocket, message: any): Promise<void> {
const { roomId, playerId, playerName } = message;
const success = await this.joinRoom(roomId, playerId, playerName);
if (success) {
// Store websocket connection
this.sessions.set(playerId, websocket);
// Send room info
const room = this.rooms.get(roomId);
const gameState = this.gameStates.get(roomId);
websocket.send(JSON.stringify({
type: 'room_joined',
room,
gameState
}));
} else {
websocket.send(JSON.stringify({
type: 'join_failed',
message: 'Failed to join room'
}));
}
}
// Handle start game request
private async handleStartGame(websocket: WebSocket, message: any): Promise<void> {
const { roomId, playerId } = message;
const success = await this.startGame(roomId, playerId);
if (success) {
websocket.send(JSON.stringify({
type: 'game_started_success'
}));
} else {
websocket.send(JSON.stringify({
type: 'start_game_failed',
message: 'Failed to start game'
}));
}
}
// Handle score update
private async handleUpdateScore(websocket: WebSocket, message: any): Promise<void> {
const { roomId, playerId, score } = message;
await this.updateScore(roomId, playerId, score);
}
// Handle game action
private async handleGameAction(websocket: WebSocket, message: any): Promise<void> {
const { roomId, playerId, action, data } = message;
// Process game-specific actions
await this.processGameAction(roomId, playerId, action, data);
}
// Broadcast to room participants
private async broadcastToRoom(roomId: string, message: any): Promise<void> {
const room = this.rooms.get(roomId);
if (!room) return;
const messageStr = JSON.stringify(message);
for (const playerId of room.currentPlayers) {
const websocket = this.sessions.get(playerId);
if (websocket && websocket.readyState === WebSocket.READY_STATE_OPEN) {
websocket.send(messageStr);
}
}
}
}
Integration Examples
Client-Side Integration
// Connect to ChatLobby
class ChatClient {
private websocket: WebSocket | null = null;
private playerId: string;
private currentRoom: string | null = null;
constructor(playerId: string) {
this.playerId = playerId;
}
// Connect to chat lobby
async connect(): Promise<void> {
const response = await fetch('/api/lobby/websocket');
this.websocket = response.webSocket!;
this.websocket.addEventListener('message', (event) => {
this.handleMessage(JSON.parse(event.data));
});
this.websocket.addEventListener('close', () => {
console.log('Chat connection closed');
});
}
// Join a chat room
joinRoom(roomId: string, playerName: string): void {
if (!this.websocket) return;
this.currentRoom = roomId;
this.websocket.send(JSON.stringify({
type: 'join_room',
roomId,
playerId: this.playerId,
playerName
}));
}
// Send a message
sendMessage(content: string): void {
if (!this.websocket || !this.currentRoom) return;
this.websocket.send(JSON.stringify({
type: 'send_message',
roomId: this.currentRoom,
playerId: this.playerId,
playerName: 'Player Name',
content
}));
}
// Handle incoming messages
private handleMessage(message: any): void {
switch (message.type) {
case 'new_message':
this.displayMessage(message.message);
break;
case 'user_joined':
this.displaySystemMessage(`${message.playerName} joined`);
break;
case 'user_left':
this.displaySystemMessage(`${message.playerName} left`);
break;
}
}
private displayMessage(message: ChatMessage): void {
// Update UI with new message
console.log(`${message.senderName}: ${message.content}`);
}
private displaySystemMessage(content: string): void {
// Display system message
console.log(`System: ${content}`);
}
}
// Connect to RoomManager
class GameClient {
private websocket: WebSocket | null = null;
private playerId: string;
private currentRoom: string | null = null;
constructor(playerId: string) {
this.playerId = playerId;
}
// Connect to room manager
async connect(): Promise<void> {
const response = await fetch('/api/room-manager/websocket');
this.websocket = response.webSocket!;
this.websocket.addEventListener('message', (event) => {
this.handleMessage(JSON.parse(event.data));
});
}
// Join a game room
joinRoom(roomId: string, playerName: string): void {
if (!this.websocket) return;
this.currentRoom = roomId;
this.websocket.send(JSON.stringify({
type: 'join_room',
roomId,
playerId: this.playerId,
playerName
}));
}
// Start a game
startGame(): void {
if (!this.websocket || !this.currentRoom) return;
this.websocket.send(JSON.stringify({
type: 'start_game',
roomId: this.currentRoom,
playerId: this.playerId
}));
}
// Update score
updateScore(score: number): void {
if (!this.websocket || !this.currentRoom) return;
this.websocket.send(JSON.stringify({
type: 'update_score',
roomId: this.currentRoom,
playerId: this.playerId,
score
}));
}
// Handle incoming messages
private handleMessage(message: any): void {
switch (message.type) {
case 'game_started':
this.onGameStarted(message);
break;
case 'score_updated':
this.onScoreUpdated(message);
break;
case 'game_ended':
this.onGameEnded(message);
break;
}
}
private onGameStarted(message: any): void {
console.log('Game started!');
// Initialize game UI
}
private onScoreUpdated(message: any): void {
console.log(`Score updated: ${message.score}`);
// Update score display
}
private onGameEnded(message: any): void {
console.log('Game ended!', message.finalScores);
// Show final results
}
}
API Integration
// API endpoints for Durable Objects
export async function GET(request: Request, locals: any) {
const url = new URL(request.url);
const path = url.pathname;
if (path.startsWith('/api/lobby/')) {
const lobbyId = locals.runtime.env.CHAT_LOBBY.idFromName('main');
const lobby = locals.runtime.env.CHAT_LOBBY.get(lobbyId);
return lobby.fetch(request);
}
if (path.startsWith('/api/room-manager/')) {
const roomManagerId = locals.runtime.env.ROOM_MANAGER.idFromName('main');
const roomManager = locals.runtime.env.ROOM_MANAGER.get(roomManagerId);
return roomManager.fetch(request);
}
return new Response('Not found', { status: 404 });
}
// Create chat room
export async function POST(request: Request, locals: any) {
const url = new URL(request.url);
if (url.pathname === '/api/lobby/rooms') {
const body = await request.json();
const { roomId, name, maxParticipants, isPrivate } = body;
const lobbyId = locals.runtime.env.CHAT_LOBBY.idFromName('main');
const lobby = locals.runtime.env.CHAT_LOBBY.get(lobbyId);
const response = await lobby.fetch(new Request(request.url, {
method: 'POST',
body: JSON.stringify({ roomId, name, maxParticipants, isPrivate })
}));
return response;
}
return new Response('Not found', { status: 404 });
}
Testing
Durable Objects Testing
describe('Durable Objects', () => {
let chatLobby: ChatLobby;
let roomManager: RoomManager;
beforeEach(() => {
chatLobby = new ChatLobby(mockState, mockEnv);
roomManager = new RoomManager(mockState, mockEnv);
});
it('should create chat room', async () => {
const room = await chatLobby.createRoom({
id: 'test-room',
name: 'Test Room',
maxParticipants: 10
});
expect(room.id).toBe('test-room');
expect(room.name).toBe('Test Room');
expect(room.maxParticipants).toBe(10);
});
it('should join chat room', async () => {
await chatLobby.createRoom({
id: 'test-room',
name: 'Test Room'
});
const success = await chatLobby.joinRoom('test-room', 'player1', 'Player 1');
expect(success).toBe(true);
const room = await chatLobby.getRoomInfo('test-room');
expect(room?.participants.has('player1')).toBe(true);
});
it('should create game room', async () => {
const room = await roomManager.createRoom({
id: 'game-room',
name: 'Game Room',
gameType: 'cognitive'
});
expect(room.id).toBe('game-room');
expect(room.gameType).toBe('cognitive');
expect(room.status).toBe('waiting');
});
it('should start game', async () => {
await roomManager.createRoom({
id: 'game-room',
name: 'Game Room',
gameType: 'cognitive'
});
await roomManager.joinRoom('game-room', 'player1', 'Player 1');
await roomManager.joinRoom('game-room', 'player2', 'Player 2');
const success = await roomManager.startGame('game-room', 'player1');
expect(success).toBe(true);
const room = await roomManager.getRoomInfo('game-room');
expect(room?.status).toBe('playing');
});
});
This comprehensive Durable Objects system provides robust real-time communication and game session management with persistent state across Cloudflare Worker restarts.