TLSocketRoom
See source codeTable of contents
A server-side room that manages WebSocket connections and synchronizes tldraw document state between multiple clients in real-time. Each room represents a collaborative document space where users can work together on drawings with automatic conflict resolution.
TLSocketRoom handles:
- WebSocket connection lifecycle management
- Real-time synchronization of document changes
- Session management and presence tracking
- Message chunking for large payloads
- Automatic client timeout and cleanup
class TLSocketRoom<
R extends UnknownRecord = UnknownRecord,
SessionMeta = void,
> {}
Example
// Basic room setup
const room = new TLSocketRoom({
onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {
console.log(
`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`
)
if (numSessionsRemaining === 0) {
room.close()
}
},
onDataChange: () => {
console.log('Document data changed, consider persisting')
},
})
// Handle new client connections
room.handleSocketConnect({
sessionId: 'user-session-123',
socket: webSocket,
isReadonly: false,
})
// Room with initial snapshot and schema
const room = new TLSocketRoom({
initialSnapshot: existingSnapshot,
schema: myCustomSchema,
clientTimeout: 30000,
log: {
warn: (...args) => logger.warn('SYNC:', ...args),
error: (...args) => logger.error('SYNC:', ...args),
},
})
// Update document programmatically
await room.updateStore((store) => {
const shape = store.get('shape:abc123')
if (shape) {
shape.x = 100
store.put(shape)
}
})
Constructor
Creates a new TLSocketRoom instance for managing collaborative document synchronization.
opts - Configuration options for the room
- initialSnapshot - Optional initial document state to load
- schema - Store schema defining record types and validation
- clientTimeout - Milliseconds to wait before disconnecting inactive clients
- log - Optional logger for warnings and errors
- onSessionRemoved - Called when a client session is removed
- onBeforeSendMessage - Called before sending messages to clients
- onAfterReceiveMessage - Called after receiving messages from clients
- onDataChange - Called when document data changes
- onPresenceChange - Called when presence data changes
Parameters
Name | Description |
---|---|
|
|
Properties
log
readonly log?: TLSyncLog
opts
readonly opts: {
onPresenceChange?(): void
clientTimeout?: number
initialSnapshot?: RoomSnapshot | TLStoreSnapshot
log?: TLSyncLog
onAfterReceiveMessage?: (args: {
message: TLSocketServerSentEvent<R>
meta: SessionMeta
sessionId: string
stringified: string
}) => void
onBeforeSendMessage?: (args: {
message: TLSocketServerSentEvent<R>
meta: SessionMeta
sessionId: string
stringified: string
}) => void
onDataChange?(): void
onSessionRemoved?: (
room: TLSocketRoom<R, SessionMeta>,
args: {
meta: SessionMeta
numSessionsRemaining: number
sessionId: string
}
) => void
schema?: StoreSchema<R, any>
}
Methods
close( )
Closes the room and disconnects all connected clients. This should be called when shutting down the room permanently, such as during server shutdown or when the room is no longer needed. Once closed, the room cannot be reopened.
close(): void
Example
// Clean shutdown when no users remain
if (room.getNumActiveSessions() === 0) {
await persistSnapshot(room.getCurrentSnapshot())
room.close()
}
// Server shutdown
process.on('SIGTERM', () => {
for (const room of activeRooms.values()) {
room.close()
}
})
closeSession( )
Immediately removes a session from the room and closes its WebSocket connection. The client will attempt to reconnect automatically unless a fatal reason is provided.
closeSession(
sessionId: string,
fatalReason?: string | TLSyncErrorCloseEventReason
): void
Example
// Kick a user (they can reconnect)
room.closeSession('session-troublemaker')
// Permanently ban a user
room.closeSession('session-banned', 'PERMISSION_DENIED')
// Close session due to inactivity
room.closeSession('session-idle', 'TIMEOUT')
Parameters
Name | Description |
---|---|
|
Session identifier to remove |
|
Optional fatal error reason that prevents reconnection |
Returns
void
getCurrentDocumentClock( )
Returns the current document clock value. The clock is a monotonically increasing integer that increments with each document change, providing a consistent ordering of changes across the distributed system.
getCurrentDocumentClock(): number
Example
const clock = room.getCurrentDocumentClock()
console.log(`Document is at version ${clock}`)
getCurrentSnapshot( )
Creates a complete snapshot of the current document state, including all records and synchronization metadata. This snapshot can be persisted to storage and used to restore the room state later or revert to a previous version.
getCurrentSnapshot(): RoomSnapshot
Example
// Capture current state for persistence
const snapshot = room.getCurrentSnapshot()
await saveToDatabase(roomId, JSON.stringify(snapshot))
// Later, restore from snapshot
const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))
const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })
getNumActiveSessions( )
Returns the number of active sessions. Note that this is not the same as the number of connected sockets! Sessions time out a few moments after sockets close, to smooth over network hiccups.
getNumActiveSessions(): number
getRecord( )
Retrieves a deeply cloned copy of a record from the document store. Returns undefined if the record doesn't exist. The returned record is safe to mutate without affecting the original store data.
getRecord(id: string): R | undefined
Example
const shape = room.getRecord('shape:abc123')
if (shape) {
console.log('Shape position:', shape.x, shape.y)
// Safe to modify without affecting store
shape.x = 100
}
Parameters
Name | Description |
---|---|
|
Unique identifier of the record to retrieve |
Returns
R | undefined
Deep clone of the record, or undefined if not found
getSessions( )
Returns information about all active sessions in the room. Each session represents a connected client with their current connection status and metadata.
getSessions(): Array<{
isConnected: boolean
isReadonly: boolean
meta: SessionMeta
sessionId: string
}>
Example
const sessions = room.getSessions()
console.log(`Room has ${sessions.length} active sessions`)
for (const session of sessions) {
console.log(
`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`
)
if (session.isReadonly) {
console.log(' (read-only access)')
}
}
handleSocketClose( )
Handles a WebSocket close event for the specified session. Use this in server environments where socket event listeners cannot be attached directly. This will initiate cleanup and session removal for the disconnected client.
handleSocketClose(sessionId: string): void
Example
// In a custom WebSocket handler
socket.addEventListener('close', () => {
room.handleSocketClose(sessionId)
})
Parameters
Name | Description |
---|---|
|
Session identifier matching the one used in handleSocketConnect |
Returns
void
handleSocketConnect( )
Handles a new client WebSocket connection, creating a session within the room. This should be called whenever a client establishes a WebSocket connection to join the collaborative document.
handleSocketConnect(
opts: {
isReadonly?: boolean
sessionId: string
socket: WebSocketMinimal
} & (SessionMeta extends void
? object
: {
meta: SessionMeta
})
): void
Example
// Handle new WebSocket connection
room.handleSocketConnect({
sessionId: 'user-session-abc123',
socket: webSocketConnection,
isReadonly: !userHasEditPermission,
})
// With session metadata
room.handleSocketConnect({
sessionId: 'session-xyz',
socket: ws,
meta: { userId: 'user-123', name: 'Alice' },
})
Parameters
Name | Description |
---|---|
|
Connection options
|
Returns
void
handleSocketError( )
Handles a WebSocket error for the specified session. Use this in server environments where socket event listeners cannot be attached directly. This will initiate cleanup and session removal for the affected client.
handleSocketError(sessionId: string): void
Example
// In a custom WebSocket handler
socket.addEventListener('error', () => {
room.handleSocketError(sessionId)
})
Parameters
Name | Description |
---|---|
|
Session identifier matching the one used in handleSocketConnect |
Returns
void
handleSocketMessage( )
Processes a message received from a client WebSocket. Use this method in server environments where WebSocket event listeners cannot be attached directly to socket instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).
The method handles message chunking/reassembly and forwards complete messages to the underlying sync room for processing.
handleSocketMessage(
sessionId: string,
message: AllowSharedBufferSource | string
): void
Example
// In a Bun.serve handler
server.upgrade(req, {
data: { sessionId, room },
upgrade(res, req) {
// Connection established
},
message(ws, message) {
const { sessionId, room } = ws.data
room.handleSocketMessage(sessionId, message)
},
})
Parameters
Name | Description |
---|---|
|
Session identifier matching the one used in handleSocketConnect |
|
Raw message data from the client (string or binary) |
Returns
void
isClosed( )
Checks whether the room has been permanently closed. Closed rooms cannot accept new connections or process further changes.
isClosed(): boolean
Example
if (room.isClosed()) {
console.log('Room has been shut down')
// Create a new room or redirect users
} else {
// Room is still accepting connections
room.handleSocketConnect({ sessionId, socket })
}
loadSnapshot( )
Loads a document snapshot, completely replacing the current room state. This will disconnect all current clients and update the document to match the provided snapshot. Use this for restoring from backups or implementing document versioning.
loadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot): void
Example
// Restore from a saved snapshot
const backup = JSON.parse(await loadBackup(roomId))
room.loadSnapshot(backup)
// All clients will be disconnected and need to reconnect
// to see the restored document state
Parameters
Name | Description |
---|---|
| Room or store snapshot to load |
Returns
void
sendCustomMessage( )
Sends a custom message to a specific client session. This allows sending application-specific data that doesn't modify the document state, such as notifications, chat messages, or custom commands.
sendCustomMessage(sessionId: string, data: any): void
Example
// Send a notification to a specific user
room.sendCustomMessage('session-123', {
type: 'notification',
message: 'Your changes have been saved',
})
// Send a chat message
room.sendCustomMessage('session-456', {
type: 'chat',
from: 'Alice',
text: 'Great work on this design!',
})
Parameters
Name | Description |
---|---|
|
Target session identifier |
|
Custom payload to send (will be JSON serialized) |
Returns
void
updateStore( )
Executes a transaction to modify the document store. Changes made within the transaction are atomic and will be synchronized to all connected clients. The transaction provides isolation from concurrent changes until it commits.
updateStore(
updater: (store: RoomStoreMethods<R>) => Promise<void> | void
): Promise<void>
Example
// Update multiple shapes in a single transaction
await room.updateStore((store) => {
const shape1 = store.get('shape:abc123')
const shape2 = store.get('shape:def456')
if (shape1) {
shape1.x = 100
store.put(shape1)
}
if (shape2) {
shape2.meta.approved = true
store.put(shape2)
}
})
// Async transaction with external API call
await room.updateStore(async (store) => {
const doc = store.get('document:main')
if (doc) {
doc.lastModified = await getCurrentTimestamp()
store.put(doc)
}
})
Parameters
Name | Description |
---|---|
|
Function that receives store methods to make changes
|
Returns
Promise<void>
Promise that resolves when the transaction completes