TLSocketRoom

See source code
Table 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

NameDescription

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>
}

Properties

log

readonlyoptional
readonly log?: TLSyncLog

opts

readonly
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

NameDescription

sessionId

string

Session identifier to remove

fatalReason

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

NameDescription

id

string

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

NameDescription

sessionId

string

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

NameDescription

opts

{
  isReadonly?: boolean
  sessionId: string
  socket: WebSocketMinimal
} & (SessionMeta extends void
  ? object
  : {
      meta: SessionMeta
    })

Connection options

  • sessionId - Unique identifier for the client session (typically from browser tab)
  • socket - WebSocket-like object for client communication
  • isReadonly - Whether the client can modify the document (defaults to false)
  • meta - Additional session metadata (required if SessionMeta is not void)

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

NameDescription

sessionId

string

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

NameDescription

sessionId

string

Session identifier matching the one used in handleSocketConnect

message

AllowSharedBufferSource | string

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

NameDescription

snapshot

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

NameDescription

sessionId

string

Target session identifier

data

any

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

NameDescription

updater

(
  store: RoomStoreMethods<R>
) => Promise<void> | void

Function that receives store methods to make changes

  • store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)
  • store.put(record) - Save a modified record
  • store.getAll() - Get all records in the store
  • store.delete(id) - Remove a record from the store

Returns

Promise<void>

Promise that resolves when the transaction completes


Prev
TLRemoteSyncError
Next
ArrowBindingUtil