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

TLSocketRoomOptions<R, SessionMeta>;

Properties

log

readonlyoptional
readonly log?: TLSyncLog;

opts

readonly
readonly opts: TLSocketRoomOptions<R, SessionMeta>;

storage

storage: TLSyncStorage<R>;

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( )

Deprecated: if you need to do this use

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;

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;

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)");
  }
}

getSessionSnapshot( )

Returns a snapshot of a connected session's state that can be persisted and later used with TLSocketRoom.handleSocketResume to restore the session after hibernation.

Returns null if the session doesn't exist or isn't in the Connected state.

getSessionSnapshot(sessionId: string): null | SessionStateSnapshot;

Example

// Store snapshot in a Cloudflare WebSocket attachment
const snapshot = room.getSessionSnapshot(sessionId);
if (snapshot) {
  ws.serializeAttachment({ sessionId, snapshot });
}

Parameters

NameDescription

sessionId

string;

The session to snapshot

Returns


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;

handleSocketResume( )

Resumes a previously-connected session directly into Connected state, bypassing the connect handshake. Use this after server hibernation (e.g., Cloudflare Durable Object hibernation) when WebSocket connections survived but all in-memory state was lost.

The session is restored using a SessionStateSnapshot previously obtained via TLSocketRoom.getSessionSnapshot. The client is unaware the server restarted and continues sending messages normally.

Unlike TLSocketRoom.handleSocketConnect, this method does NOT attach WebSocket event listeners. In hibernation environments, events are delivered via class methods (e.g., webSocketMessage) rather than addEventListener.

handleSocketResume(
  opts: {
    sessionId: string;
    snapshot: SessionStateSnapshot;
    socket: WebSocketMinimal;
  } & (SessionMeta extends void
    ? object
    : {
        meta: SessionMeta;
      }),
): void;

Example

// After Cloudflare DO hibernation wake
for (const ws of ctx.getWebSockets()) {
  const data = ws.deserializeAttachment();
  room.handleSocketResume({
    sessionId: data.sessionId,
    socket: ws,
    snapshot: data.snapshot,
  });
}

Parameters

NameDescription

opts

{
  sessionId: string;
  snapshot: SessionStateSnapshot;
  socket: WebSocketMinimal;
} & (SessionMeta extends void
  ? object
  : {
      meta: SessionMeta;
    });

Resume options - sessionId - Unique identifier for the client session - socket - WebSocket-like object for client communication - snapshot - Session state snapshot from TLSocketRoom.getSessionSnapshot - meta - Additional session metadata (required if SessionMeta is not void)

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( )

Deprecated: use the storage.transaction method instead

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
TLSyncClient