UNPKG

@tldraw/sync-core

Version:

tldraw infinite canvas SDK (multiplayer sync).

1,247 lines (1,195 loc) • 43.1 kB
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 { }