@tldraw/sync-core
Version:
tldraw infinite canvas SDK (multiplayer sync).
272 lines (261 loc) • 7.52 kB
text/typescript
import { SerializedSchema, UnknownRecord } from '@tldraw/store'
import { NetworkDiff, ObjectDiff, RecordOpType } from './diff'
const TLSYNC_PROTOCOL_VERSION = 8
/**
* Gets the current tldraw sync protocol version number.
*
* This version number is used during WebSocket connection handshake to ensure
* client and server compatibility. When versions don't match, the connection
* will be rejected with an incompatibility error.
*
* @returns The current protocol version number
*
* @example
* ```ts
* const version = getTlsyncProtocolVersion()
* console.log(`Using protocol version: ${version}`)
* ```
*
* @internal
*/
export function getTlsyncProtocolVersion() {
return TLSYNC_PROTOCOL_VERSION
}
/**
* Constants defining the different types of protocol incompatibility reasons.
*
* These values indicate why a client-server connection was rejected due to
* version or compatibility issues. Each reason helps diagnose specific problems
* during the connection handshake.
*
* @example
* ```ts
* if (error.reason === TLIncompatibilityReason.ClientTooOld) {
* showUpgradeMessage('Please update your client')
* }
* ```
*
* @internal
* @deprecated Replaced by websocket .close status/reason
*/
export const TLIncompatibilityReason = {
ClientTooOld: 'clientTooOld',
ServerTooOld: 'serverTooOld',
InvalidRecord: 'invalidRecord',
InvalidOperation: 'invalidOperation',
} as const
/**
* Union type representing all possible incompatibility reason values.
*
* This type represents the different reasons why a client-server connection
* might fail due to protocol or version mismatches.
*
* @example
* ```ts
* function handleIncompatibility(reason: TLIncompatibilityReason) {
* switch (reason) {
* case 'clientTooOld':
* return 'Client needs to be updated'
* case 'serverTooOld':
* return 'Server needs to be updated'
* }
* }
* ```
*
* @internal
* @deprecated replaced by websocket .close status/reason
*/
export type TLIncompatibilityReason =
(typeof TLIncompatibilityReason)[keyof typeof TLIncompatibilityReason]
/**
* Union type representing all possible message types that can be sent from server to client.
*
* This encompasses the complete set of server-originated WebSocket messages in the tldraw
* sync protocol, including connection establishment, data synchronization, and error handling.
*
* @param R - The record type being synchronized (extends UnknownRecord)
*
* @example
* ```ts
* syncClient.onReceiveMessage((message: TLSocketServerSentEvent<MyRecord>) => {
* switch (message.type) {
* case 'connect':
* console.log('Connected to room with clock:', message.serverClock)
* break
* case 'data':
* console.log('Received data updates:', message.data)
* break
* }
* })
* ```
*
* @internal
*/
export type TLSocketServerSentEvent<R extends UnknownRecord> =
| {
type: 'connect'
hydrationType: 'wipe_all' | 'wipe_presence'
connectRequestId: string
protocolVersion: number
schema: SerializedSchema
diff: NetworkDiff<R>
serverClock: number
isReadonly: boolean
}
| {
type: 'incompatibility_error'
// eslint-disable-next-line @typescript-eslint/no-deprecated
reason: TLIncompatibilityReason
}
| {
type: 'pong'
}
| { type: 'data'; data: TLSocketServerSentDataEvent<R>[] }
| { type: 'custom'; data: any }
| TLSocketServerSentDataEvent<R>
/**
* Union type representing data-related messages sent from server to client.
*
* These messages handle the core synchronization operations: applying patches from
* other clients and confirming the results of client push operations.
*
* @param R - The record type being synchronized (extends UnknownRecord)
*
* @example
* ```ts
* function handleDataEvent(event: TLSocketServerSentDataEvent<MyRecord>) {
* if (event.type === 'patch') {
* // Apply changes from other clients
* applyNetworkDiff(event.diff)
* } else if (event.type === 'push_result') {
* // Handle result of our push request
* if (event.action === 'commit') {
* console.log('Changes accepted by server')
* }
* }
* }
* ```
*
* @internal
*/
export type TLSocketServerSentDataEvent<R extends UnknownRecord> =
| {
type: 'patch'
diff: NetworkDiff<R>
serverClock: number
}
| {
type: 'push_result'
clientClock: number
serverClock: number
action: 'discard' | 'commit' | { rebaseWithDiff: NetworkDiff<R> }
}
/**
* Interface defining a client-to-server push request message.
*
* Push requests are sent when the client wants to synchronize local changes
* with the server. They contain document changes and optionally presence updates
* (like cursor position or user selection).
*
* @param R - The record type being synchronized (extends UnknownRecord)
*
* @example
* ```ts
* const pushRequest: TLPushRequest<MyRecord> = {
* type: 'push',
* clientClock: 15,
* diff: {
* 'shape:abc123': [RecordOpType.Patch, { x: [ValueOpType.Put, 100] }]
* },
* presence: [RecordOpType.Put, { cursor: { x: 150, y: 200 } }]
* }
* socket.sendMessage(pushRequest)
* ```
*
* @internal
*/
export interface TLPushRequest<R extends UnknownRecord> {
type: 'push'
clientClock: number
diff?: NetworkDiff<R>
presence?: [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R]
}
/**
* Interface defining a client-to-server connection request message.
*
* This message initiates a WebSocket connection to a sync room. It includes
* the client's schema, protocol version, and last known server clock for
* proper synchronization state management.
*
* @example
* ```ts
* const connectRequest: TLConnectRequest = {
* type: 'connect',
* connectRequestId: 'conn-123',
* lastServerClock: 42,
* protocolVersion: getTlsyncProtocolVersion(),
* schema: mySchema.serialize()
* }
* socket.sendMessage(connectRequest)
* ```
*
* @internal
*/
export interface TLConnectRequest {
type: 'connect'
connectRequestId: string
lastServerClock: number
protocolVersion: number
schema: SerializedSchema
}
/**
* Interface defining a client-to-server ping request message.
*
* Ping requests are used to measure network latency and ensure the connection
* is still active. The server responds with a 'pong' message.
*
* @example
* ```ts
* const pingRequest: TLPingRequest = { type: 'ping' }
* socket.sendMessage(pingRequest)
*
* // Server will respond with { type: 'pong' }
* ```
*
* @internal
*/
export interface TLPingRequest {
type: 'ping'
}
/**
* Union type representing all possible message types that can be sent from client to server.
*
* This encompasses the complete set of client-originated WebSocket messages in the tldraw
* sync protocol, covering connection establishment, data synchronization, and connectivity checks.
*
* @param R - The record type being synchronized (extends UnknownRecord)
*
* @example
* ```ts
* function sendMessage(message: TLSocketClientSentEvent<MyRecord>) {
* switch (message.type) {
* case 'connect':
* console.log('Establishing connection...')
* break
* case 'push':
* console.log('Pushing changes:', message.diff)
* break
* case 'ping':
* console.log('Checking connection latency')
* break
* }
* socket.send(JSON.stringify(message))
* }
* ```
*
* @internal
*/
export type TLSocketClientSentEvent<R extends UnknownRecord> =
| TLPushRequest<R>
| TLConnectRequest
| TLPingRequest