@tldraw/sync-core
Version:
tldraw infinite canvas SDK (multiplayer sync).
1,247 lines (1,195 loc) • 43.1 kB
text/typescript
import { Atom } from '@tldraw/state';
import { AtomMap } from '@tldraw/store';
import { Emitter } from 'nanoevents';
import { RecordsDiff } from '@tldraw/store';
import { RecordType } from '@tldraw/store';
import { Result } from '@tldraw/utils';
import { SerializedSchema } from '@tldraw/store';
import { Signal } from '@tldraw/state';
import { Store } from '@tldraw/store';
import { StoreSchema } from '@tldraw/store';
import { TLRecord } from '@tldraw/tlschema';
import { TLStoreSnapshot } from '@tldraw/tlschema';
import { UnknownRecord } from '@tldraw/store';
/* Excluded from this release type: AppendOp */
/* Excluded from this release type: applyObjectDiff */
/* Excluded from this release type: chunk */
/* Excluded from this release type: ClientWebSocketAdapter */
/* Excluded from this release type: DeleteOp */
/* Excluded from this release type: diffRecord */
/* Excluded from this release type: DocumentState */
/* Excluded from this release type: getNetworkDiff */
/* Excluded from this release type: getTlsyncProtocolVersion */
/**
* Assembles chunked JSON messages back into complete objects.
* Handles both regular JSON messages and chunked messages created by the chunk() function.
* Maintains internal state to track partially received chunked messages.
*
* @example
* ```ts
* const assembler = new JsonChunkAssembler()
*
* // Handle regular JSON message
* const result1 = assembler.handleMessage('{"hello": "world"}')
* // Returns: { data: { hello: "world" }, stringified: '{"hello": "world"}' }
*
* // Handle chunked message
* assembler.handleMessage('1_hello') // Returns: null (partial)
* const result2 = assembler.handleMessage('0_ world')
* // Returns: { data: "hello world", stringified: "hello world" }
* ```
*
* @public
*/
export declare class JsonChunkAssembler {
/**
* Current assembly state - either 'idle' or tracking chunks being received
*/
state: 'idle' | {
chunksReceived: string[];
totalChunks: number;
};
/**
* Processes a single message, which can be either a complete JSON object or a chunk.
* For complete JSON objects (starting with '\{'), parses immediately.
* For chunks (prefixed with "\{number\}_"), accumulates until all chunks received.
*
* @param msg - The message to process, either JSON or chunk format
* @returns Result object with data/stringified on success, error object on failure, or null for incomplete chunks
* - `\{ data: object, stringified: string \}` - Successfully parsed complete message
* - `\{ error: Error \}` - Parse error or invalid chunk sequence
* - `null` - Chunk received but more chunks expected
*
* @example
* ```ts
* const assembler = new JsonChunkAssembler()
*
* // Complete JSON message
* const result = assembler.handleMessage('{"key": "value"}')
* if (result && 'data' in result) {
* console.log(result.data) // { key: "value" }
* }
*
* // Chunked message sequence
* assembler.handleMessage('2_hel') // null - more chunks expected
* assembler.handleMessage('1_lo ') // null - more chunks expected
* assembler.handleMessage('0_wor') // { data: "hello wor", stringified: "hello wor" }
* ```
*/
handleMessage(msg: string): {
data: object;
stringified: string;
} | {
error: Error;
} | null;
}
/* Excluded from this release type: NetworkDiff */
/* Excluded from this release type: ObjectDiff */
/**
* Utility type that removes properties with void values from an object type.
* This is used internally to conditionally require session metadata based on
* whether SessionMeta extends void.
*
* @example
* ```ts
* type Example = { a: string, b: void, c: number }
* type Result = OmitVoid<Example> // { a: string, c: number }
* ```
*
* @public
*/
export declare type OmitVoid<T, KS extends keyof T = keyof T> = {
[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K];
};
/* Excluded from this release type: PatchOp */
/* Excluded from this release type: PersistedRoomSnapshotForSupabase */
/* Excluded from this release type: PutOp */
/* Excluded from this release type: ReconnectManager */
/* Excluded from this release type: RecordOp */
/* Excluded from this release type: RecordOpType */
/* Excluded from this release type: RoomSession */
/* Excluded from this release type: RoomSessionBase */
/* Excluded from this release type: RoomSessionState */
/**
* Snapshot of a room's complete state that can be persisted and restored.
* Contains all documents, tombstones, and metadata needed to reconstruct the room.
*
* @public
*/
export declare interface RoomSnapshot {
/**
* The current logical clock value for the room
*/
clock: number;
/**
* Clock value when document data was last changed (optional for backwards compatibility)
*/
documentClock?: number;
/**
* Array of all document records with their last modification clocks
*/
documents: Array<{
lastChangedClock: number;
state: UnknownRecord;
}>;
/**
* Map of deleted record IDs to their deletion clock values (optional)
*/
tombstones?: Record<string, number>;
/**
* Clock value where tombstone history begins - older deletions are not tracked (optional)
*/
tombstoneHistoryStartsAtClock?: number;
/**
* Serialized schema used when creating this snapshot (optional)
*/
schema?: SerializedSchema;
}
/**
* Interface for making transactional changes to room store data. Used within
* updateStore transactions to modify documents atomically.
*
* @example
* ```ts
* await room.updateStore((store) => {
* const shape = store.get('shape:123')
* if (shape) {
* store.put({ ...shape, x: shape.x + 10 })
* }
* store.delete('shape:456')
* })
* ```
*
* @public
*/
export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {
/**
* Add or update a record in the store.
*
* @param record - The record to store
*/
put(record: R): void;
/**
* Delete a record from the store.
*
* @param recordOrId - The record or record ID to delete
*/
delete(recordOrId: R | string): void;
/**
* Get a record by its ID.
*
* @param id - The record ID
* @returns The record or null if not found
*/
get(id: string): null | R;
/**
* Get all records in the store.
*
* @returns Array of all records
*/
getAll(): R[];
}
/**
* Function type for subscribing to events with a callback.
* Returns an unsubscribe function to clean up the listener.
*
* @param cb - Callback function that receives the event value
* @returns Function to call when you want to unsubscribe from the events
*
* @public
*/
export declare type SubscribingFn<T> = (cb: (val: T) => void) => () => void;
/* Excluded from this release type: TLConnectRequest */
/**
* Handler function for custom application messages sent through the sync protocol.
* These are user-defined messages that can be sent between clients via the sync server,
* separate from the standard document synchronization messages.
*
* @param data - Custom message payload (application-defined structure)
*
* @example
* ```ts
* const customMessageHandler: TLCustomMessageHandler = (data) => {
* if (data.type === 'user_joined') {
* console.log(`${data.username} joined the session`)
* showToast(`${data.username} is now collaborating`)
* }
* }
*
* const syncClient = new TLSyncClient({
* // ... other config
* onCustomMessageReceived: customMessageHandler
* })
* ```
*
* @public
*/
export declare type TLCustomMessageHandler = (this: null, data: any) => void;
/* Excluded from this release type: TLIncompatibilityReason */
/**
* Interface for persistent WebSocket-like connections used by TLSyncClient.
* Handles automatic reconnection and provides event-based communication with the sync server.
* Implementations should maintain connection resilience and handle network interruptions gracefully.
*
* @example
* ```ts
* class MySocketAdapter implements TLPersistentClientSocket {
* connectionStatus: 'offline' | 'online' | 'error' = 'offline'
*
* sendMessage(msg: TLSocketClientSentEvent) {
* if (this.ws && this.ws.readyState === WebSocket.OPEN) {
* this.ws.send(JSON.stringify(msg))
* }
* }
*
* onReceiveMessage = (callback) => {
* // Set up message listener and return cleanup function
* }
*
* restart() {
* this.disconnect()
* this.connect()
* }
* }
* ```
*
* @public
*/
export declare interface TLPersistentClientSocket<ClientSentMessage extends object = object, ServerSentMessage extends object = object> {
/** Current connection state - online means actively connected and ready */
connectionStatus: 'error' | 'offline' | 'online';
/**
* Send a protocol message to the sync server
* @param msg - Message to send (connect, push, ping, etc.)
*/
sendMessage(msg: ClientSentMessage): void;
/**
* Subscribe to messages received from the server
* @param callback - Function called for each received message
* @returns Cleanup function to remove the listener
*/
onReceiveMessage: SubscribingFn<ServerSentMessage>;
/**
* Subscribe to connection status changes
* @param callback - Function called when connection status changes
* @returns Cleanup function to remove the listener
*/
onStatusChange: SubscribingFn<TLSocketStatusChangeEvent>;
/**
* Force a connection restart (disconnect then reconnect)
* Used for error recovery or when connection health checks fail
*/
restart(): void;
/**
* Close the connection
*/
close(): void;
}
/* Excluded from this release type: TLPersistentClientSocketStatus */
/* Excluded from this release type: TLPingRequest */
/**
* Mode for handling presence information in sync sessions.
* Controls whether presence data (cursors, selections) is shared with other clients.
*
* @public
*/
export declare type TLPresenceMode =
/** No presence sharing - client operates independently */
'full'
| 'solo'
/** Full presence sharing - cursors and selections visible to others */;
/* Excluded from this release type: TLPushRequest */
/**
* Specialized error class for synchronization-related failures in tldraw collaboration.
*
* This error is thrown when the sync client encounters fatal errors that prevent
* successful synchronization with the server. It captures both the error message
* and the specific reason code that triggered the failure.
*
* Common scenarios include schema version mismatches, authentication failures,
* network connectivity issues, and server-side validation errors.
*
* @example
* ```ts
* import { TLRemoteSyncError, TLSyncErrorCloseEventReason } from '@tldraw/sync-core'
*
* // Handle sync errors in your application
* syncClient.onSyncError((error) => {
* if (error instanceof TLRemoteSyncError) {
* switch (error.reason) {
* case TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:
* // Redirect user to login
* break
* case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:
* // Show update required message
* break
* default:
* console.error('Sync error:', error.message)
* }
* }
* })
* ```
*
* @example
* ```ts
* // Server-side: throwing a sync error
* if (!hasPermission(userId, roomId)) {
* throw new TLRemoteSyncError(TLSyncErrorCloseEventReason.FORBIDDEN)
* }
* ```
*
* @public
*/
export declare class TLRemoteSyncError extends Error {
readonly reason: string | TLSyncErrorCloseEventReason;
name: string;
/**
* Creates a new TLRemoteSyncError with the specified reason.
*
* reason - The specific reason code or custom string describing why the sync failed.
* When using predefined reasons from TLSyncErrorCloseEventReason, the client
* can handle specific error types appropriately. Custom strings allow for
* application-specific error details.
*/
constructor(reason: string | TLSyncErrorCloseEventReason);
}
/* Excluded from this release type: TLRoomSocket */
/* Excluded from this release type: TLSocketClientSentEvent */
/**
* 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
*
* @example
* ```ts
* // 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
* })
* ```
*
* @example
* ```ts
* // 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)
* }
* })
* ```
*
* @public
*/
export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {
readonly opts: {
/* Excluded from this release type: onPresenceChange */
clientTimeout?: number;
initialSnapshot?: RoomSnapshot | TLStoreSnapshot;
log?: TLSyncLog;
onAfterReceiveMessage?: (args: {
/* Excluded from this release type: message */
meta: SessionMeta;
sessionId: string;
stringified: string;
}) => void;
onBeforeSendMessage?: (args: {
/* Excluded from this release type: message */
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>;
};
private room;
private readonly sessions;
readonly log?: TLSyncLog;
private readonly syncCallbacks;
/**
* 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
*/
constructor(opts: {
/* Excluded from this release type: onPresenceChange */
clientTimeout?: number;
initialSnapshot?: RoomSnapshot | TLStoreSnapshot;
log?: TLSyncLog;
onAfterReceiveMessage?: (args: {
/* Excluded from this release type: message */
meta: SessionMeta;
sessionId: string;
stringified: string;
}) => void;
onBeforeSendMessage?: (args: {
/* Excluded from this release type: message */
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>;
});
/**
* 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.
*
* @returns the number of active sessions
*/
getNumActiveSessions(): number;
/**
* 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.
*
* @param opts - 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)
*
* @example
* ```ts
* // Handle new WebSocket connection
* room.handleSocketConnect({
* sessionId: 'user-session-abc123',
* socket: webSocketConnection,
* isReadonly: !userHasEditPermission
* })
* ```
*
* @example
* ```ts
* // With session metadata
* room.handleSocketConnect({
* sessionId: 'session-xyz',
* socket: ws,
* meta: { userId: 'user-123', name: 'Alice' }
* })
* ```
*/
handleSocketConnect(opts: {
isReadonly?: boolean;
sessionId: string;
socket: WebSocketMinimal;
} & (SessionMeta extends void ? object : {
meta: SessionMeta;
})): void;
/**
* 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.
*
* @param sessionId - Session identifier matching the one used in handleSocketConnect
* @param message - Raw message data from the client (string or binary)
*
* @example
* ```ts
* // 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)
* }
* })
* ```
*/
handleSocketMessage(sessionId: string, message: AllowSharedBufferSource | string): void;
/**
* 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.
*
* @param sessionId - Session identifier matching the one used in handleSocketConnect
*
* @example
* ```ts
* // In a custom WebSocket handler
* socket.addEventListener('error', () => {
* room.handleSocketError(sessionId)
* })
* ```
*/
handleSocketError(sessionId: string): void;
/**
* 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.
*
* @param sessionId - Session identifier matching the one used in handleSocketConnect
*
* @example
* ```ts
* // In a custom WebSocket handler
* socket.addEventListener('close', () => {
* room.handleSocketClose(sessionId)
* })
* ```
*/
handleSocketClose(sessionId: string): void;
/**
* 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.
*
* @returns The current document clock value
*
* @example
* ```ts
* const clock = room.getCurrentDocumentClock()
* console.log(`Document is at version ${clock}`)
* ```
*/
getCurrentDocumentClock(): number;
/**
* 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.
*
* @param id - Unique identifier of the record to retrieve
* @returns Deep clone of the record, or undefined if not found
*
* @example
* ```ts
* 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
* }
* ```
*/
getRecord(id: string): R | undefined;
/**
* Returns information about all active sessions in the room. Each session
* represents a connected client with their current connection status and metadata.
*
* @returns Array of session information objects containing:
* - sessionId - Unique session identifier
* - isConnected - Whether the session has an active WebSocket connection
* - isReadonly - Whether the session can modify the document
* - meta - Custom session metadata
*
* @example
* ```ts
* 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)')
* }
* }
* ```
*/
getSessions(): Array<{
isConnected: boolean;
isReadonly: boolean;
meta: SessionMeta;
sessionId: string;
}>;
/**
* 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.
*
* @returns Complete room snapshot including documents, clock values, and tombstones
*
* @example
* ```ts
* // 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 })
* ```
*/
getCurrentSnapshot(): RoomSnapshot;
/* Excluded from this release type: getPresenceRecords */
/* Excluded from this release type: getCurrentSerializedSnapshot */
/**
* 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.
*
* @param snapshot - Room or store snapshot to load
*
* @example
* ```ts
* // 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
* ```
*/
loadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot): void;
/**
* 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.
*
* @param updater - 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 that resolves when the transaction completes
*
* @example
* ```ts
* // 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)
* }
* })
* ```
*
* @example
* ```ts
* // 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)
* }
* })
* ```
*/
updateStore(updater: (store: RoomStoreMethods<R>) => Promise<void> | void): Promise<void>;
/**
* 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.
*
* @param sessionId - Target session identifier
* @param data - Custom payload to send (will be JSON serialized)
*
* @example
* ```ts
* // 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!'
* })
* ```
*/
sendCustomMessage(sessionId: string, data: any): void;
/**
* 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.
*
* @param sessionId - Session identifier to remove
* @param fatalReason - Optional fatal error reason that prevents reconnection
*
* @example
* ```ts
* // 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')
* ```
*/
closeSession(sessionId: string, fatalReason?: string | TLSyncErrorCloseEventReason): void;
/**
* 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.
*
* @example
* ```ts
* // 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()
* }
* })
* ```
*/
close(): void;
/**
* Checks whether the room has been permanently closed. Closed rooms cannot
* accept new connections or process further changes.
*
* @returns True if the room is closed, false if still active
*
* @example
* ```ts
* 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 })
* }
* ```
*/
isClosed(): boolean;
}
/* Excluded from this release type: TLSocketServerSentDataEvent */
/* Excluded from this release type: TLSocketServerSentEvent */
/**
* Event object describing changes in socket connection status.
* Contains either a basic status change or an error with details.
*
* @public
*/
export declare type TLSocketStatusChangeEvent = {
/** Connection came online or went offline */
status: 'offline' | 'online';
} | {
/** Connection encountered an error */
status: 'error';
/** Description of the error that occurred */
reason: string;
};
/* Excluded from this release type: TLSocketStatusListener */
/**
* Main client-side synchronization engine for collaborative tldraw applications.
*
* TLSyncClient manages bidirectional synchronization between a local tldraw Store
* and a remote sync server. It uses an optimistic update model where local changes
* are immediately applied for responsive UI, then sent to the server for validation
* and distribution to other clients.
*
* The synchronization follows a git-like push/pull/rebase model:
* - **Push**: Local changes are sent to server as diff operations
* - **Pull**: Server changes are received and applied locally
* - **Rebase**: Conflicting changes are resolved by undoing local changes,
* applying server changes, then re-applying local changes on top
*
* @example
* ```ts
* import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core'
* import { createTLStore } from '@tldraw/store'
*
* // Create store and socket
* const store = createTLStore({ schema: mySchema })
* const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync')
*
* // Create sync client
* const syncClient = new TLSyncClient({
* store,
* socket,
* presence: atom(null),
* onLoad: () => console.log('Connected and loaded'),
* onSyncError: (reason) => console.error('Sync failed:', reason)
* })
*
* // Changes to store are now automatically synchronized
* store.put([{ id: 'shape1', type: 'geo', x: 100, y: 100 }])
* ```
*
* @example
* ```ts
* // Advanced usage with presence and custom messages
* const syncClient = new TLSyncClient({
* store,
* socket,
* presence: atom({ cursor: { x: 0, y: 0 }, userName: 'Alice' }),
* presenceMode: atom('full'),
* onCustomMessageReceived: (data) => {
* if (data.type === 'chat') {
* showChatMessage(data.message, data.from)
* }
* },
* onAfterConnect: (client, { isReadonly }) => {
* if (isReadonly) {
* showNotification('Connected in read-only mode')
* }
* }
* })
* ```
*
* @public
*/
export declare class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>> {
/** The last clock time from the most recent server update */
private lastServerClock;
private lastServerInteractionTimestamp;
/** The queue of in-flight push requests that have not yet been acknowledged by the server */
private pendingPushRequests;
/**
* The diff of 'unconfirmed', 'optimistic' changes that have been made locally by the user if we
* take this diff, reverse it, and apply that to the store, our store will match exactly the most
* recent state of the server that we know about
*/
private speculativeChanges;
private disposables;
/* Excluded from this release type: store */
/* Excluded from this release type: socket */
/* Excluded from this release type: presenceState */
/* Excluded from this release type: presenceMode */
/* Excluded from this release type: isConnectedToRoom */
/**
* The client clock is essentially a counter for push requests Each time a push request is created
* the clock is incremented. This clock is sent with the push request to the server, and the
* server returns it with the response so that we can match up the response with the request.
*
* The clock may also be used at one point in the future to allow the client to re-send push
* requests idempotently (i.e. the server will keep track of each client's clock and not execute
* requests it has already handled), but at the time of writing this is neither needed nor
* implemented.
*/
private clientClock;
/**
* Callback executed immediately after successful connection to sync room.
* Use this to perform any post-connection setup required for your application,
* such as initializing default content or updating UI state.
*
* @param self - The TLSyncClient instance that connected
* @param details - Connection details
* - isReadonly - Whether the connection is in read-only mode
*/
private readonly onAfterConnect?;
private readonly onCustomMessageReceived?;
private isDebugging;
private debug;
private readonly presenceType;
private didCancel?;
/**
* Creates a new TLSyncClient instance to manage synchronization with a remote server.
*
* @param config - Configuration object for the sync client
* - store - The local tldraw store to synchronize
* - socket - WebSocket adapter for server communication
* - presence - Reactive signal containing current user's presence data
* - presenceMode - Optional signal controlling presence sharing (defaults to 'full')
* - onLoad - Callback fired when initial sync completes successfully
* - onSyncError - Callback fired when sync fails with error reason
* - onCustomMessageReceived - Optional handler for custom messages
* - onAfterConnect - Optional callback fired after successful connection
* - self - The TLSyncClient instance
* - details - Connection details including readonly status
* - didCancel - Optional function to check if sync should be cancelled
*/
constructor(config: {
didCancel?(): boolean;
onAfterConnect?(self: TLSyncClient<R, S>, details: {
isReadonly: boolean;
}): void;
onCustomMessageReceived?: TLCustomMessageHandler;
onLoad(self: TLSyncClient<R, S>): void;
onSyncError(reason: string): void;
presence: Signal<null | R>;
presenceMode?: Signal<TLPresenceMode>;
socket: TLPersistentClientSocket<any, any>;
store: S;
});
/* Excluded from this release type: latestConnectRequestId */
/**
* This is the first message that is sent over a newly established socket connection. And we need
* to wait for the response before this client can be used.
*/
private sendConnectMessage;
/** Switch to offline mode */
private resetConnection;
/**
* Invoked when the socket connection comes online, either for the first time or as the result of
* a reconnect. The goal is to rebase on the server's state and fire off a new push request for
* any local changes that were made while offline.
*/
private didReconnect;
private incomingDiffBuffer;
/** Handle events received from the server */
private handleServerEvent;
/**
* Closes the sync client and cleans up all resources.
*
* Call this method when you no longer need the sync client to prevent
* memory leaks and close the WebSocket connection. After calling close(),
* the client cannot be reused.
*
* @example
* ```ts
* // Clean shutdown
* syncClient.close()
* ```
*/
close(): void;
private lastPushedPresenceState;
private pushPresence;
/** Push a change to the server, or stash it locally if we're offline */
private push;
/** Send any unsent push requests to the server */
private flushPendingPushRequests;
/**
* Applies a 'network' diff to the store this does value-based equality checking so that if the
* data is the same (as opposed to merely identical with ===), then no change is made and no
* changes will be propagated back to store listeners
*/
private applyNetworkDiff;
private rebase;
private scheduleRebase;
}
/**
* WebSocket close code used by the server to signal a non-recoverable sync error.
* This close code indicates that the connection is being terminated due to an error
* that cannot be automatically recovered from, such as authentication failures,
* incompatible client versions, or invalid data.
*
* @example
* ```ts
* // Server-side: Close connection with specific error reason
* socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.NOT_FOUND)
*
* // Client-side: Handle the error in your sync error handler
* const syncClient = new TLSyncClient({
* // ... other config
* onSyncError: (reason) => {
* console.error('Sync failed:', reason) // Will receive 'NOT_FOUND'
* }
* })
* ```
*
* @public
*/
export declare const TLSyncErrorCloseEventCode: 4099;
/**
* Predefined reasons for server-initiated connection closures.
* These constants represent different error conditions that can cause
* the sync server to terminate a WebSocket connection.
*
* @example
* ```ts
* // Server usage
* if (!user.hasPermission(roomId)) {
* socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.FORBIDDEN)
* }
*
* // Client error handling
* syncClient.onSyncError((reason) => {
* switch (reason) {
* case TLSyncErrorCloseEventReason.NOT_FOUND:
* showError('Room does not exist')
* break
* case TLSyncErrorCloseEventReason.FORBIDDEN:
* showError('Access denied')
* break
* case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:
* showError('Please update your app')
* break
* }
* })
* ```
*
* @public
*/
export declare const TLSyncErrorCloseEventReason: {
/** Client exceeded rate limits */
readonly RATE_LIMITED: "RATE_LIMITED";
/** Client protocol version too old */
readonly CLIENT_TOO_OLD: "CLIENT_TOO_OLD";
/** Client sent invalid or corrupted record data */
readonly INVALID_RECORD: "INVALID_RECORD";
/** Room has reached maximum capacity */
readonly ROOM_FULL: "ROOM_FULL";
/** Room or resource not found */
readonly NOT_FOUND: "NOT_FOUND";
/** Server protocol version too old */
readonly SERVER_TOO_OLD: "SERVER_TOO_OLD";
/** Unexpected server error occurred */
readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
/** User authentication required or invalid */
readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
/** User lacks permission to access the room */
readonly FORBIDDEN: "FORBIDDEN";
};
/**
* Union type of all possible server connection close reasons.
* Represents the string values that can be passed when a server closes
* a sync connection due to an error condition.
*
* @public
*/
export declare type TLSyncErrorCloseEventReason = (typeof TLSyncErrorCloseEventReason)[keyof typeof TLSyncErrorCloseEventReason];
/**
* Logging interface for TLSocketRoom operations. Provides optional methods
* for warning and error logging during synchronization operations.
*
* @example
* ```ts
* const logger: TLSyncLog = {
* warn: (...args) => console.warn('[SYNC]', ...args),
* error: (...args) => console.error('[SYNC]', ...args)
* }
*
* const room = new TLSocketRoom({ log: logger })
* ```
*
* @public
*/
export declare interface TLSyncLog {
/**
* Optional warning logger for non-fatal sync issues
* @param args - Arguments to log
*/
warn?(...args: any[]): void;
/**
* Optional error logger for sync errors and failures
* @param args - Arguments to log
*/
error?(...args: any[]): void;
}
/* Excluded from this release type: TLSyncRoom */
/* Excluded from this release type: ValueOp */
/* Excluded from this release type: ValueOpType */
/**
* Minimal server-side WebSocket interface that is compatible with various WebSocket implementations.
* This interface abstracts over different WebSocket libraries and platforms to provide a consistent
* API for the ServerSocketAdapter.
*
* Supports:
* - The standard WebSocket interface (Cloudflare, Deno, some Node.js setups)
* - The 'ws' WebSocket interface (Node.js ws library)
* - The Bun.serve socket implementation
*
* @public
* @example
* ```ts
* // Standard WebSocket
* const standardWs: WebSocketMinimal = new WebSocket('ws://localhost:8080')
*
* // Node.js 'ws' library WebSocket
* import WebSocket from 'ws'
* const nodeWs: WebSocketMinimal = new WebSocket('ws://localhost:8080')
*
* // Bun WebSocket (in server context)
* // const bunWs: WebSocketMinimal = server.upgrade(request)
* ```
*/
export declare interface WebSocketMinimal {
/**
* Optional method to add event listeners for WebSocket events.
* Not all WebSocket implementations provide this method.
*
* @param type - The event type to listen for
* @param listener - The event handler function
*/
addEventListener?: (type: 'close' | 'error' | 'message', listener: (event: any) => void) => void;
/**
* Optional method to remove event listeners for WebSocket events.
* Not all WebSocket implementations provide this method.
*
* @param type - The event type to stop listening for
* @param listener - The event handler function to remove
*/
removeEventListener?: (type: 'close' | 'error' | 'message', listener: (event: any) => void) => void;
/**
* Sends a string message through the WebSocket connection.
*
* @param data - The string data to send
*/
send: (data: string) => void;
/**
* Closes the WebSocket connection.
*
* @param code - Optional close code (default: 1000 for normal closure)
* @param reason - Optional human-readable close reason
*/
close: (code?: number, reason?: string) => void;
/**
* The current state of the WebSocket connection.
* - 0: CONNECTING
* - 1: OPEN
* - 2: CLOSING
* - 3: CLOSED
*/
readyState: number;
}
export { }