npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@bonfire-ember/server

v0.1.1

Published

Server implementation for Bonfire with Socket.io and database adapters

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/server

Dependencies:

  • @bonfire/core - Game engine
  • socket.io - Realtime communication
  • express - HTTP server
  • firebase-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 instances
  • gameType - 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 port

Notes:

  • 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 identifier
  • hostName: string - Host player's display name
  • callback: (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 code
  • playerName: string - Player's display name
  • callback: (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 identifier
  • payload: unknown - Action data
  • callback: (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/A3K7N2

Params:

  • 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-123

Params:

  • roomId: string - Room ID
  • playerId: 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 implementation
  • gameFactory - Function to create game instances
  • gameType - String identifier for game type
  • config - 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 reached
  • Error - 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 state

Throws:

  • 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 lastActivity to current time
  • Persists to database
  • Cancels existing cleanup timer
  • Sets new cleanup timer for defaultTTL milliseconds

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 cleanupInterval duration
  • 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 instance
  • databaseAdapter - 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:update event 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:sync event 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:

  1. Local Development:

    • Install Firebase CLI: npm install -g firebase-tools
    • Start emulator: npm run firebase:emulator
    • No Firebase account required!
  2. 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.md for complete setup guide

Database Structure:

/rooms/
  /{roomId}/
    /state - Game state object
    /metadata - Room metadata

Types

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) // true

Room 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
└── ValidationError

Testing

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:firebase

Note: Firebase tests require the emulator to be running:

# Terminal 1: Start emulator
npm run firebase:emulator

# Terminal 2: Run tests
npm run test:firebase

Mock 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