tinyroom
Version:
A minimalistic room/lobby system for multiplayer applications. Built on TinyPeer with automatic reconnection support.
371 lines (255 loc) • 8.95 kB
Markdown
# TinyRoom
[](https://www.npmjs.com/package/tinyroom)
[](https://github.com/jamsinclair/tinyroom/blob/main/LICENSE.md)
[](https://bundlephobia.com/package/tinyroom)
> [!WARNING]
> This project is in early development. The API may change before a stable 1.0 release. Use with caution in production.
A tiny, host‑authoritative P2P room/lobby for the browser. Built on [TinyPeer](https://github.com/jamsinclair/tinypeer) with [PeerJS Server](https://peerjs.com/server) signaling. One peer hosts the room (authority) and everyone, including the host, joins as a client.
This gives a clean separation of concerns: write client code as if everyone talks to a single remote server.
## Why TinyRoom?
- **Host‑authoritative rooms** – One peer acts as the room/server; same client API for everyone (host included)
- **No server** – Uses a public signaling server (PeerJS) only; messages sent peer-to-peer over WebRTC data channels
- **Reconnect that sticks** – Optional layer with health checks, grace periods, and stable IDs across refreshes
- **Small and focused** – Minimal bundle, zero deps beyond [TinyPeer](https://github.com/jamsinclair/tinypeer)
- **Flexible by design** – Start with basic rooms; add reconnection when you need it
Great for multiplayer games, collaborative tools, and real‑time apps where a single peer manages connections and game state.
## Installation
```bash
npm install tinyroom
```
## Quick Start
```typescript
import { joinOrCreateRoom } from 'tinyroom'
const { room, client } = await joinOrCreateRoom('game-123', {
metadata: { username: 'Alice' }
})
if (room) {
// Handle peers connecting
room.onPeerJoin((peer) => {
console.log(`${peer.id} joined`)
})
// Handle messages from clients
room.on('move', (data, fromId) => {
// Validate and relay to everyone
room.broadcast('move', { player: fromId, ...data })
})
}
// All players can have the same client code (including host!)
client.send('move', { x: 5, y: 10 })
client.on('move', (data) => {
console.log(`Player ${data.player} moved`)
})
```
## API
### Factory Functions
#### `createRoom(roomId, options?)`
Creates a new room with the caller as host.
```typescript
import { createRoom } from 'tinyroom'
const { room, client } = await createRoom('my-room', {
metadata: { username: 'Alice' }, // Optional: peer metadata
// ... all TinyPeer options (host, port, path, etc.)
})
```
**Returns:** `{ room: HostRoom, client: Client }`
- `room` - Room management interface (host only)
- `client` - Client interface for the host (hosts participate as a regular player too)
#### `joinRoom(roomId, options?)`
Joins an existing room as a client.
```typescript
import { joinRoom } from 'tinyroom'
const client = await joinRoom('my-room', {
metadata: { username: 'Bob' }
})
```
**Returns:** `Client`
**Throws:** If room doesn't exist or connection fails.
#### `joinOrCreateRoom(roomId, options?)`
Attempts to join a room, creates it if it doesn't exist.
```typescript
import { joinOrCreateRoom } from 'tinyroom'
const { client, room } = await joinOrCreateRoom('my-room', {
metadata: { username: 'Charlie' }
})
if (room) {
console.log('Created room - I am host')
room.onPeerJoin((peer) => {
console.log(`${peer.id} joined`)
})
} else {
console.log('Joined existing room')
}
// Everyone uses client the same way
client.send('hello', { message: 'Hi!' })
```
### HostRoom Interface
The `room` object provides host-only management capabilities.
**Properties:**
- `id: string` - Room ID (same as host's peer ID)
- `peers: Map<string, Peer>` - All connected peers (including host)
**Methods:**
##### `room.on(event, handler)`
Receives custom messages from all clients.
```typescript
room.on('move', (data, fromId, metadata) => {
console.log(`Client ${fromId} wants to move`, data)
// Validate, process, then broadcast
if (isValidMove(data)) {
room.broadcast('move', { player: fromId, ...data })
}
console.log('Optional metadata from client', metadata)
})
```
##### `room.broadcast(event, data, metadata?)`
Send message to all connected clients (including host's own client).
```typescript
await room.broadcast('game-start', { level: 1 })
```
##### `room.sendToPeer(event, data, target, metadata?)`
Send message to specific client(s).
```typescript
// Single target
await room.sendToPeer('welcome', { msg: 'Hi!' }, 'peer-123')
// Multiple targets
await room.sendToPeer('team-update', data, ['peer-1', 'peer-2'])
```
##### `room.ping(peerId)`
Measure latency to a specific peer.
```typescript
const latency = await room.ping('peer-123')
console.log(`Latency: ${latency}ms`)
```
##### `room.onPeerJoin(handler)`
Called when a peer joins. **Immediately fires for host when handler first set**.
```typescript
room.onPeerJoin((peer) => {
console.log(`${peer.id} joined`, peer.metadata)
console.log(`Is host: ${peer.isHost}`)
// Kick a peer
if (shouldKick(peer)) {
peer.close('You are not allowed')
}
})
```
The handler receives:
- `peer.id` - Peer's unique ID
- `peer.metadata` - Connection metadata
- `peer.isHost` - Whether this is the host
- `peer.close(reason?)` - Function to disconnect the peer
##### `room.onPeerLeave(handler)`
Called when a peer permanently leaves.
```typescript
room.onPeerLeave((peerId) => {
console.log(`${peerId} left`)
removeFromGame(peerId)
})
```
##### `room.close()`
Closes the room and disconnects all clients.
```typescript
await room.close()
```
### Client Interface
The `client` object is used by everyone (including host) for player behavior.
**Properties:**
- `id: string` - This client's peer ID
**Methods:**
##### `client.send(event, data, metadata?)`
Send message to host (who will process/relay it).
```typescript
await client.send('move', { x: 10, y: 20 })
```
##### `client.on(event, handler)`
Receive messages (broadcasts from host).
```typescript
client.on('move', (data, fromId, metadata) => {
console.log(`Player ${fromId} moved to`, data)
updateSprite(fromId, data.x, data.y)
console.log('Optional metadata from client', metadata)
})
```
##### `client.ping()`
Measure latency to host (client only, host should use `room.ping(peerId)`).
```typescript
const latency = await client.ping()
console.log(`Latency to host: ${latency}ms`)
```
##### `client.onClose(handler)`
Called when connection closes.
```typescript
client.onClose((reason) => {
console.log('Connection closed:', reason)
})
```
##### `client.onError(handler)`
Called on connection errors.
```typescript
client.onError((error) => {
console.error('Connection error:', error)
})
```
##### `client.leave()`
Disconnect from the room.
```typescript
client.leave()
```
## Reconnection (Experimental)
TinyRoom is experimenting with reconnection support with stable peer identities.
See [reconnect.md](./docs/reconnect.md) for full documentation.
## Advanced Topics
### Host as Player
The host uses their `client` object just like everyone else:
```typescript
const { room, client } = await createRoom('game-123')
// Host sends moves as a client
client.send('move', { x: 10, y: 10 })
// This triggers the room.on() handler
room.on('move', (data, fromId) => {
// fromId will be the host's own ID when host sends
room.broadcast('move', { player: fromId, ...data })
})
// Host receives broadcasts like everyone else
client.on('move', (data) => {
updatePlayerSprite(data.player, data.x, data.y)
})
```
### Message Routing
**Client → Host:**
```typescript
client.send('move', data) // Goes to host
```
**Host receives and processes:**
```typescript
room.on('move', (data, fromId) => {
// Validate, process, then broadcast
room.broadcast('move', { player: fromId, ...data })
})
```
**Everyone receives broadcast:**
```typescript
client.on('move', (data) => {
// Both host and clients receive this
})
```
## Examples
### Basic Room
See the [basic room example](examples/basic/) for a complete working demo.
### Reconnection
See the [reconnection example](examples/reconnect/) for a complete working demo with:
- Host controls (simulate disconnect, kick peer)
- Client reconnection UI
- Health checks
- Grace periods
- All lifecycle events
## Browser Support
Requires modern browsers with WebRTC support (ES2020+).
TinyRoom uses modern APIs and assumes recent browser versions. If you encounter browser-specific issues, please [open an issue](https://github.com/jamsinclair/tinyroom/issues).
## License
MIT
## Built With
- [TinyPeer](https://github.com/jamsinclair/tinypeer) - Minimalistic WebRTC peer-to-peer library
## Acknowledgements
Inspired by:
- [Colyseus](https://github.com/colyseus/colyseus)
- [PeerJS](https://peerjs.com/)
- [Trystero](https://github.com/dmotz/trystero)