@bonfire-ember/server
v0.1.1
Published
Server implementation for Bonfire with Socket.io and database adapters
Maintainers
Readme
@bonfire/server
Server infrastructure for Bonfire party game framework, providing multi-room orchestration, Socket.io integration, and database abstraction.
Status: Milestone 3 Complete - All 4 phases done! (Tests require Firebase emulator)
Features
- Multi-room orchestration - Manage multiple concurrent game rooms
- Realtime communication - Socket.io-based state synchronization
- Backend abstraction - Swap databases without code changes
- Automatic cleanup - TTL-based room expiration
- Type-safe events - Full TypeScript event contracts
- Comprehensive testing - Mock Socket.io utilities for testing
Installation
npm install @bonfire/serverDependencies:
@bonfire/core- Game enginesocket.io- Realtime communicationexpress- HTTP serverfirebase-admin- Firebase integration (optional)
Quick Start
Using SocketServer (Recommended - Phase 3+):
import { SocketServer, InMemoryAdapter } from '@bonfire/server'
import { SocialGame } from '@bonfire/core'
// Create database adapter
const adapter = new InMemoryAdapter()
// Create game factory
const gameFactory = (roomId, synchronizer) => {
return new SocialGame({
roomId,
maxPlayers: 8,
stateSynchronizer: synchronizer,
// ... your game config
})
}
// Create and start server
const server = new SocketServer(
{
port: 3000,
nodeEnv: 'development',
room: {
defaultTTL: 24 * 60 * 60 * 1000, // 24 hours
maxRooms: 1000,
},
admin: {
enabled: true,
apiKey: 'your-secret-key',
},
cors: {
origin: ['http://localhost:5173'],
credentials: true,
},
},
adapter,
gameFactory,
'my-game'
)
await server.initialize()
await server.start()
console.log('Server running on port 3000')
// Graceful shutdown
process.on('SIGINT', async () => {
await server.shutdown()
process.exit(0)
})Using RoomManager directly (Advanced - Phases 1-2):
import { RoomManager, InMemoryAdapter } from '@bonfire/server'
import { Server as SocketIOServer } from 'socket.io'
import { SocialGame } from '@bonfire/core'
import express from 'express'
import { createServer } from 'http'
// Create Express + Socket.io server
const app = express()
const httpServer = createServer(app)
const io = new SocketIOServer(httpServer)
// Create database adapter
const adapter = new InMemoryAdapter()
await adapter.initialize()
// Create game factory
const gameFactory = (roomId, synchronizer) => {
return new SocialGame({
roomId,
maxPlayers: 8,
stateSynchronizer: synchronizer,
// ... game config
})
}
// Create room manager
const roomManager = new RoomManager(
io,
adapter,
gameFactory,
'my-game',
{
defaultTTL: 24 * 60 * 60 * 1000, // 24 hours
maxRooms: 1000,
cleanupInterval: 60 * 60 * 1000, // 1 hour
}
)
// Start cleanup
roomManager.startCleanup()
// Start server
httpServer.listen(3000, () => {
console.log('Server running on port 3000')
})API Reference
SocketServer
Main server class for Bonfire games - integrates Express, Socket.io, and RoomManager into a production-ready multiplayer game server.
Constructor
constructor(
config: ServerConfig,
databaseAdapter: IDatabaseAdapter,
gameFactory: GameFactory<T>,
gameType: string
)Parameters:
config- Server configuration (port, CORS, admin, room settings)databaseAdapter- Database adapter implementation (InMemoryAdapter or FirebaseAdapter)gameFactory- Function to create game instancesgameType- String identifier for game type
Server Configuration:
interface ServerConfig {
port: number // HTTP server port
nodeEnv?: 'development' | 'production' | 'test'
room?: {
defaultTTL?: number // Room expiration (default: 24h)
maxRooms?: number // Max concurrent rooms (default: 1000)
cleanupInterval?: number // Cleanup scan frequency (default: 1h)
}
admin?: {
enabled: boolean // Enable admin endpoints
apiKey: string // API key for authentication
}
cors?: {
origin: string[] // Allowed origins
credentials: boolean // Allow credentials
}
}Lifecycle Methods
initialize(): Promise<void>
Initialize the server (Express, Socket.io, RoomManager, database adapter).
await server.initialize()What it does:
- Initializes database adapter
- Sets up Express app with CORS
- Creates Socket.io server
- Initializes RoomManager
- Wires up event handlers
- Starts room cleanup
- Sets up admin endpoints (if enabled)
start(): Promise<void>
Start the HTTP server on configured port.
await server.start()
// Server now listening on portNotes:
- Automatically calls
initialize()if not already initialized - Returns promise that resolves when server is listening
stop(): Promise<void>
Stop the HTTP server (but keep RoomManager running).
await server.stop()Use case: Restart server without losing room state
shutdown(): Promise<void>
Gracefully shut down server and clean up all resources.
await server.shutdown()What it does:
- Stops cleanup timers
- Closes all Socket.io connections
- Closes database adapter
- Stops HTTP server
Utility Methods
getStats(): ServerStats
Get current server statistics.
const stats = server.getStats()
console.log(stats)
// {
// totalRooms: 5,
// totalPlayers: 12,
// roomsByStatus: { waiting: 2, playing: 3, ended: 0, closed: 0 },
// uptime: 3600000, // milliseconds
// memoryUsage: { rss: ..., heapUsed: ..., ... }
// }Returns:
interface ServerStats {
totalRooms: number
totalPlayers: number
roomsByStatus: Record<string, number>
uptime: number
memoryUsage: NodeJS.MemoryUsage
}getHttpServer(): HTTPServer
Get the underlying HTTP server instance.
const httpServer = server.getHttpServer()Use case: Access HTTP server for additional middleware or testing
Client Events
SocketServer handles these Socket.io events from clients:
room:create
Create a new game room.
socket.emit('room:create', gameType, hostName, (response) => {
if (response.success) {
console.log('Room created:', response.roomId)
console.log('Initial state:', response.state)
}
})Parameters:
gameType: string- Game type identifierhostName: string- Host player's display namecallback: (response: RoomCreateResponse) => void
Response:
interface RoomCreateResponse {
success: boolean
roomId?: RoomId // 6-character room code
playerId?: PlayerId // Host player ID
state?: GameState // Initial game state
error?: string
}room:join
Join an existing room.
socket.emit('room:join', roomId, playerName, (response) => {
if (response.success) {
console.log('Joined room:', response.playerId)
}
})Parameters:
roomId: RoomId- 6-character room codeplayerName: string- Player's display namecallback: (response: RoomJoinResponse) => void
Response:
interface RoomJoinResponse {
success: boolean
playerId?: PlayerId
state?: GameState
error?: string
}room:reconnect
Reconnect to a room after a page refresh. Session data is automatically saved to sessionStorage by the client library on room:create / room:join.
socket.emit('room:reconnect', roomId, playerId, (response) => {
if (response.success && response.state) {
console.log('Reconnected to room:', response.state.roomId)
}
})Response:
interface RoomReconnectResponse {
success: boolean
playerId?: PlayerId
state?: GameState
error?: string
}room:leave
Leave the current room.
socket.emit('room:leave', (response) => {
if (response.success) {
console.log('Left room')
}
})game:start
Start the game (host only).
socket.emit('game:start', (response) => {
if (response.success) {
console.log('Game started')
}
})game:action
Submit a game action.
socket.emit('game:action', 'submit-answer', { answer: 'My answer' }, (response) => {
if (response.success) {
console.log('Action processed')
}
})Parameters:
actionType: string- Action identifierpayload: unknown- Action datacallback: (response: ActionResponse) => void
state:request
Request current game state (for reconnection).
socket.emit('state:request', (response) => {
if (response.success) {
console.log('Current state:', response.state)
}
})Admin Endpoints
REST endpoints for server management (require API key):
GET /health
Health check endpoint.
curl http://localhost:3000/health
# { "status": "ok" }GET /admin/stats
Get server statistics.
curl -H "x-api-key: your-secret-key" http://localhost:3000/admin/stats
# {
# "totalRooms": 5,
# "totalPlayers": 12,
# "roomsByStatus": { "waiting": 2, "playing": 3, "ended": 0, "closed": 0 },
# "uptime": 3600000,
# "memoryUsage": { "rss": 52428800, "heapUsed": 18874368 }
# }Headers:
x-api-key: string- Admin API key (required)
POST /admin/force-end/:roomId
Force-end a room.
curl -X POST -H "x-api-key: your-secret-key" \
http://localhost:3000/admin/force-end/A3K7N2Params:
roomId: string- Room to end
POST /admin/kick/:roomId/:playerId
Kick a player from a room.
curl -X POST -H "x-api-key: your-secret-key" \
http://localhost:3000/admin/kick/A3K7N2/player-123Params:
roomId: string- Room IDplayerId: string- Player to kick
RoomManager
Orchestrates multiple game rooms with lifecycle management, player tracking, and automatic cleanup.
Constructor
constructor(
io: TypedSocketServer,
databaseAdapter: IDatabaseAdapter,
gameFactory: GameFactory<T>,
gameType: string,
config?: RoomManagerConfig
)Parameters:
io- Socket.io server instance (typed)databaseAdapter- Database adapter implementationgameFactory- Function to create game instancesgameType- String identifier for game typeconfig- Optional configuration
Config Options:
interface RoomManagerConfig {
defaultTTL?: number // Room expiration time (default: 24 hours)
maxRooms?: number // Max concurrent rooms (default: 1000)
cleanupInterval?: number // Cleanup scan interval (default: 1 hour)
}Room Management Methods
createRoom(hostPlayerId: PlayerId): Promise<RoomInstance<T>>
Create a new room with unique room code.
const room = await roomManager.createRoom('host-player-id')
console.log(`Room created: ${room.roomId}`) // e.g., "A3K7N2"Features:
- Generates 6-character alphanumeric room code
- Retries up to 10 times on collision
- Creates game instance via factory
- Initializes state synchronizer
- Persists metadata to database
Throws:
Error- If max rooms limit reachedError- If unique code generation fails
getRoom(roomId: RoomId): RoomInstance<T>
Get room instance by ID.
const room = roomManager.getRoom('A3K7N2')
console.log(room.game.getPlayers()) // Access game stateThrows:
RoomNotFoundError- If room doesn't exist
hasRoom(roomId: RoomId): boolean
Check if room exists.
if (roomManager.hasRoom('A3K7N2')) {
// Room exists
}deleteRoom(roomId: RoomId): Promise<void>
Delete a room and cleanup all resources.
await roomManager.deleteRoom('A3K7N2')Cleanup:
- Clears room cleanup timer
- Removes all player-to-room mappings
- Clears synchronizer socket mappings
- Deletes from database
- Removes from memory
listRooms(filter?: (room: RoomInstance<T>) => boolean): RoomInfo[]
List all rooms with optional filtering.
// List all rooms
const allRooms = roomManager.listRooms()
// List only active rooms
const activeRooms = roomManager.listRooms(
(room) => room.metadata.status === 'active'
)
// List rooms with space
const openRooms = roomManager.listRooms(
(room) => room.metadata.playerCount < room.game.config.maxPlayers
)Returns:
interface RoomInfo {
roomId: RoomId
status: RoomStatus
playerCount: number
maxPlayers: number
hostName: string
gameType: string
createdAt: number
isPrivate?: boolean
}Player Tracking Methods
trackPlayer(playerId: PlayerId, roomId: RoomId): void
Track player's current room.
roomManager.trackPlayer('player-123', 'A3K7N2')getRoomIdForPlayer(playerId: PlayerId): RoomId | undefined
Get room ID for a player.
const roomId = roomManager.getRoomIdForPlayer('player-123')
if (roomId) {
const room = roomManager.getRoom(roomId)
}untrackPlayer(playerId: PlayerId): void
Remove player tracking.
roomManager.untrackPlayer('player-123')Activity & Metadata Methods
updateActivity(roomId: RoomId): Promise<void>
Update room's last activity timestamp and reset TTL timer.
await roomManager.updateActivity('A3K7N2')Behavior:
- Updates
lastActivityto current time - Persists to database
- Cancels existing cleanup timer
- Sets new cleanup timer for
defaultTTLmilliseconds
updateRoomMetadata(roomId: RoomId, updates: Partial<RoomMetadata>): Promise<void>
Update room metadata.
await roomManager.updateRoomMetadata('A3K7N2', {
status: 'active',
playerCount: 5,
})Throws:
RoomNotFoundError- If room doesn't exist
Cleanup Methods
startCleanup(): void
Start periodic cleanup of inactive rooms.
roomManager.startCleanup()Behavior:
- Sets interval for
cleanupIntervalduration - Queries database for rooms inactive longer than
defaultTTL - Deletes inactive rooms
- Safe to call multiple times (no-op if already running)
stopCleanup(): void
Stop periodic cleanup.
roomManager.stopCleanup()Utility Methods
getRoomCount(): number
Get total number of active rooms.
const count = roomManager.getRoomCount()
console.log(`${count} rooms active`)getPlayerCount(): number
Get total number of tracked players.
const count = roomManager.getPlayerCount()
console.log(`${count} players online`)shutdown(): Promise<void>
Gracefully shutdown room manager.
await roomManager.shutdown()Cleanup:
- Stops cleanup interval
- Clears all room cleanup timers
- Clears all data from memory
SocketStateSynchronizer
Broadcasts game state and events via Socket.io and persists to database. Implements IStateSynchronizer from @bonfire/core.
Constructor
constructor(
roomId: RoomId,
io: TypedSocketServer,
databaseAdapter: IDatabaseAdapter
)Parameters:
roomId- Room identifier (used as Socket.io room name)io- Socket.io server instancedatabaseAdapter- Database adapter for persistence
State Synchronization Methods
broadcastState(state: GameState): Promise<void>
Broadcast state to all players in room.
await synchronizer.broadcastState(gameState)Behavior:
- Emits
state:updateevent to Socket.io room - Persists state to database
- Both operations happen concurrently
Socket Event:
// Server
io.to(roomId).emit('state:update', gameState)
// Client receives
socket.on('state:update', (state) => {
// Update UI with new state
})sendToPlayer(playerId: PlayerId, state: GameState): Promise<void>
Send state to specific player (for reconnection).
await synchronizer.sendToPlayer('player-123', gameState)Behavior:
- Looks up socket ID for player
- Emits
state:syncevent to that socket - No-op if player socket not found (already disconnected)
- Does NOT persist to database (broadcast handles that)
Socket Event:
// Server
io.to(socketId).emit('state:sync', gameState)
// Client receives
socket.on('state:sync', (state) => {
// Sync local state after reconnection
})broadcastEvent(event: string, payload: unknown): Promise<void>
Broadcast custom game event to room.
await synchronizer.broadcastEvent('timer:tick', { secondsLeft: 30 })
await synchronizer.broadcastEvent('player:voted', { playerId: 'p1', vote: 'yes' })Socket Event:
// Server
io.to(roomId).emit('event:emit', { type: event, payload })
// Client receives
socket.on('event:emit', ({ type, payload }) => {
if (type === 'timer:tick') {
// Update timer UI
}
})Player Mapping Methods
registerPlayer(playerId: PlayerId, socketId: string): void
Register player's socket ID for targeted sends.
synchronizer.registerPlayer('player-123', 'socket-abc')Usage:
io.on('connection', (socket) => {
socket.on('room:join', (roomId, playerName, callback) => {
const { playerId } = await game.addPlayer(playerName)
// Register for targeted sends
synchronizer.registerPlayer(playerId, socket.id)
// Join Socket.io room for broadcasts
socket.join(roomId)
})
})unregisterPlayer(playerId: PlayerId): void
Unregister player's socket mapping.
synchronizer.unregisterPlayer('player-123')clearPlayerMappings(): void
Clear all player-socket mappings (room cleanup).
synchronizer.clearPlayerMappings()IDatabaseAdapter
Backend-agnostic interface for database operations. Implement this interface to support different databases.
Required Methods
interface IDatabaseAdapter {
// Lifecycle
initialize(): Promise<void>
close(): Promise<void>
// Game state
saveGameState(roomId: RoomId, state: GameState): Promise<void>
loadGameState(roomId: RoomId): Promise<GameState | null>
// Room metadata
updateRoomMetadata(roomId: RoomId, metadata: RoomMetadata): Promise<void>
getRoomMetadata(roomId: RoomId): Promise<RoomMetadata | null>
getAllRoomMetadata(): Promise<RoomMetadata[]>
// Cleanup
getInactiveRooms(olderThan: number): Promise<RoomId[]>
deleteRoom(roomId: RoomId): Promise<void>
roomExists(roomId: RoomId): Promise<boolean>
}InMemoryAdapter
In-memory database adapter for testing and development.
Constructor
constructor()Usage
const adapter = new InMemoryAdapter()
await adapter.initialize()
// Use with RoomManager
const roomManager = new RoomManager(io, adapter, gameFactory, 'my-game')Features:
- Stores data in JavaScript Maps
- Fully synchronous (wrapped in Promises)
- No persistence (data lost on restart)
- Perfect for testing
Limitations:
- ⚠️ NOT for production use
- ⚠️ No data persistence
- ⚠️ Single-process only
FirebaseAdapter
Firebase Realtime Database adapter for production persistence.
Constructor
constructor(config: FirebaseAdapterConfig)Configuration:
interface FirebaseAdapterConfig {
projectId: string // Firebase project ID
databaseURL: string // Firebase Realtime Database URL
credentialsPath?: string // Path to service account JSON
credentials?: object // Service account object
useEmulator?: boolean // Use Firebase emulator (local dev)
}Usage
Production (with credentials file):
import { FirebaseAdapter } from '@bonfire/server'
const adapter = new FirebaseAdapter({
projectId: process.env.FIREBASE_PROJECT_ID!,
databaseURL: process.env.FIREBASE_DATABASE_URL!,
credentialsPath: '/path/to/firebase-service-account.json',
})
await adapter.initialize()
// Use with SocketServer
const server = new SocketServer({
port: 3000,
databaseAdapter: adapter,
gameFactory: () => new SocialGame(),
// ... other config
})Local Development (with emulator):
const adapter = new FirebaseAdapter({
projectId: 'bonfire-dev',
databaseURL: 'http://localhost:9000?ns=bonfire-dev',
useEmulator: true, // Connects to Firebase Emulator
})
await adapter.initialize()Features:
- ✅ Production-ready persistence
- ✅ Automatic data synchronization
- ✅ Firebase Emulator support for local development
- ✅ No credentials needed for emulator
- ✅ Real-time data updates
- ✅ Scalable for production use
Setup:
Local Development:
- Install Firebase CLI:
npm install -g firebase-tools - Start emulator:
npm run firebase:emulator - No Firebase account required!
- Install Firebase CLI:
Production:
- Create Firebase project at https://console.firebase.google.com
- Enable Realtime Database
- Download service account credentials
- Test connection:
npm run firebase:test - See
docs/api/FIREBASE.mdfor complete setup guide
Database Structure:
/rooms/
/{roomId}/
/state - Game state object
/metadata - Room metadataTypes
Server Configuration
interface ServerConfig {
port: number
nodeEnv?: 'development' | 'production' | 'test'
firebase?: {
projectId: string
databaseURL: string
credentialsPath?: string
}
room?: {
defaultTTL?: number
maxRooms?: number
cleanupInterval?: number
}
rateLimit?: {
windowMs?: number
maxRequests?: number
}
admin?: {
enabled?: boolean
apiKey?: string
}
cors?: {
origin: string | string[]
credentials?: boolean
}
}Socket.io Event Contracts
Client → Server:
interface ClientToServerEvents {
'room:create': (gameType: string, hostName: string, callback: (response: RoomCreateResponse) => void) => void
'room:join': (roomId: RoomId, playerName: string, callback: (response: RoomJoinResponse) => void) => void
'room:leave': (callback?: (response: BaseResponse) => void) => void
'room:reconnect': (roomId: RoomId, playerId: PlayerId, callback: (response: RoomReconnectResponse) => void) => void
'game:start': (callback?: (response: BaseResponse) => void) => void
'game:action': (actionType: string, payload: unknown, callback?: (response: ActionResponse) => void) => void
'state:request': (callback: (response: StateResponse) => void) => void
}Server → Client:
interface ServerToClientEvents {
'state:update': (state: GameState) => void
'state:sync': (state: GameState) => void
'event:emit': (event: { type: string; payload: unknown }) => void
'error': (error: ErrorResponse) => void
'room:closed': (reason: string) => void
}Room Data Structures
interface RoomInstance<T extends SocialGame<any>> {
roomId: RoomId
game: T
synchronizer: SocketStateSynchronizer<any>
metadata: RoomMetadata
cleanupTimer?: NodeJS.Timeout
}
interface RoomMetadata {
roomId: RoomId
createdAt: number
lastActivity: number
hostId: PlayerId
playerCount: number
status: RoomStatus
gameType: string
custom?: Record<string, unknown>
}
interface RoomInfo {
roomId: RoomId
status: RoomStatus
playerCount: number
maxPlayers: number
hostName: string
gameType: string
createdAt: number
isPrivate?: boolean
}Utilities
Room Code Generation
import { generateRoomCode, isValidRoomCode } from '@bonfire/server'
const roomId = generateRoomCode() // e.g., "A3K7N2"
const isValid = isValidRoomCode(roomId) // trueRoom Code Format:
- 6 characters
- Uppercase alphanumeric
- Excludes ambiguous characters (0/O, 1/I/l)
- Characters:
23456789ABCDEFGHJKLMNPQRSTUVWXYZ
Error Classes
import {
ServerError,
RoomNotFoundError,
RoomFullError,
RoomClosedError,
UnauthorizedError,
ValidationError,
} from '@bonfire/server'
try {
const room = roomManager.getRoom(roomId)
} catch (error) {
if (error instanceof RoomNotFoundError) {
socket.emit('error', {
message: 'Room not found',
code: 'ROOM_NOT_FOUND',
})
}
}Error Hierarchy:
ServerError (extends Error)
├── RoomNotFoundError
├── RoomFullError
├── RoomClosedError
├── UnauthorizedError
└── ValidationErrorTesting
Running Tests
# Run all tests (unit + integration)
npm test
# Run with coverage
npm run test:coverage
# Test Firebase connection (production)
npm run firebase:test
# Run Firebase unit tests with emulator
npm run test:firebaseNote: Firebase tests require the emulator to be running:
# Terminal 1: Start emulator
npm run firebase:emulator
# Terminal 2: Run tests
npm run test:firebaseMock Socket.io Utilities
For testing server code that uses Socket.io:
import { MockSocket, MockSocketServer } from '@bonfire/server/__mocks__/mockSocketIo'
// Create mock server
const mockIo = new MockSocketServer()
// Create mock socket
const mockSocket = new MockSocket()
// Use with synchronizer
const sync = new SocketStateSynchronizer('room1', mockIo, adapter)
// Verify emitted events
expect(mockSocket.emittedEvents).toContainEqual({
event: 'state:update',
args: [gameState]
})Mock API:
class MockSocket {
rooms: Set<string>
emittedEvents: Array<{ event: string; args: any[] }>
emit(event: string, ...args: any[]): void
join(room: string): void
leave(room: string): void
to(room: string): MockSocket
}
class MockSocketServer {
sockets: Map<string, MockSocket>
rooms: Map<string, Set<string>>
to(room: string): MockSocket
emit(event: string, ...args: any[]): void
}Examples
Complete Server Setup
import express from 'express'
import { createServer } from 'http'
import { Server as SocketIOServer } from 'socket.io'
import { RoomManager, InMemoryAdapter } from '@bonfire/server'
import { SocialGame } from '@bonfire/core'
const app = express()
const httpServer = createServer(app)
const io = new SocketIOServer(httpServer, {
cors: { origin: '*' }
})
const adapter = new InMemoryAdapter()
await adapter.initialize()
const gameFactory = (roomId, synchronizer) => {
return new SocialGame({
roomId,
maxPlayers: 8,
stateSynchronizer: synchronizer,
})
}
const roomManager = new RoomManager(io, adapter, gameFactory, 'party-game')
roomManager.startCleanup()
// Socket.io event handlers
io.on('connection', (socket) => {
console.log('Player connected:', socket.id)
socket.on('room:create', async (gameType, hostName, callback) => {
try {
const room = await roomManager.createRoom(socket.id)
const { playerId } = await room.game.addPlayer(hostName, { isHost: true })
room.synchronizer.registerPlayer(playerId, socket.id)
socket.join(room.roomId)
roomManager.trackPlayer(socket.id, room.roomId)
callback({ success: true, roomId: room.roomId, state: room.game.getState() })
} catch (error) {
callback({ success: false, error: error.message })
}
})
socket.on('room:join', async (roomId, playerName, callback) => {
try {
const room = roomManager.getRoom(roomId)
const { playerId } = await room.game.addPlayer(playerName)
room.synchronizer.registerPlayer(playerId, socket.id)
socket.join(roomId)
roomManager.trackPlayer(socket.id, roomId)
callback({ success: true, playerId, state: room.game.getState() })
} catch (error) {
callback({ success: false, error: error.message })
}
})
socket.on('disconnect', () => {
const roomId = roomManager.getRoomIdForPlayer(socket.id)
if (roomId) {
const room = roomManager.getRoom(roomId)
room.game.handlePlayerDisconnect(socket.id)
roomManager.untrackPlayer(socket.id)
}
})
})
httpServer.listen(3000)Architecture
See docs/architecture/server-infrastructure.md for detailed architecture documentation including:
- Design decisions and patterns
- Data flow diagrams
- Testing strategy
- Production considerations
- Phase 3 & 4 roadmap
Phase Status
Phase 1: Foundation ✅ Complete
- Package setup, types, room code generator, errors, database abstraction, InMemoryAdapter
Phase 2: Room Management Core ✅ Complete
- RoomManager, SocketStateSynchronizer, mock Socket.io utilities, 97 tests
Phase 3: Socket.io Integration ✅ Complete
- SocketServer class, event handlers, integration tests, 41 integration tests
Phase 4: Firebase Integration ✅ Complete
- FirebaseAdapter implementation, emulator setup, production deployment guide
License
MIT
