snapshot of https://github.com/tldraw/tldraw.git (main, 2026-09-23; blobs > 131072B omitted)
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
Some checks failed
Deploy analytics worker / Deploy analytics worker to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
i18n upload strings / Upload latest strings (push) Failing after 1s
Deploy bemo / Deploy bemo to ${{ (github.ref == 'refs/heads/bemo-production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Failing after 1s
Deploy grafana resources / ${{ github.event_name == 'push' && 'Deploy grafana resources' || 'Validate grafana resources (dry run)' }} (push) Failing after 0s
End to end tests (dotcom) / Check for relevant changes (push) Failing after 0s
End to end tests (examples) / Check for relevant changes (push) Failing after 0s
End to end tests (mermaid readability) / Check for relevant changes (push) Failing after 1s
End to end tests (perf) / Check for relevant changes (push) Failing after 0s
Publish editor extensions / Publish editor extensions (push) Failing after 1s
Publish SDK packages / Remove the publish-packages label (push) Has been skipped
Checks / Tests & checks (push) Has been cancelled
Checks / Build all projects (push) Has been cancelled
Deploy templates / Deploy templates (push) Has been cancelled
End to end tests (dotcom) / End to end tests (dotcom) (push) Has been cancelled
End to end tests (mermaid readability) / End to end tests (mermaid readability) (push) Has been cancelled
End to end tests (perf) / End to end tests (perf) (push) Has been cancelled
Deploy .com / Deploy dotcom to ${{ (github.ref == 'refs/heads/production' && 'production') || (github.ref == 'refs/heads/main' && 'staging') || 'preview' }} (push) Has been cancelled
End to end tests (examples) / End to end tests (examples) (push) Has been cancelled
Publish SDK packages / Publish SDK packages (push) Has been cancelled
Publish SDK packages / Tell user released version (push) Has been cancelled
Publish SDK packages / Publishes code templates to separate repositories (push) Has been cancelled
Publish SDK packages / Deploys the template demos with the new SDK version (push) Has been cancelled
This commit is contained in:
662
packages/sync-core/DOCS.md
Normal file
662
packages/sync-core/DOCS.md
Normal file
@@ -0,0 +1,662 @@
|
||||
# @tldraw/sync-core
|
||||
|
||||
The `@tldraw/sync-core` package provides the foundational infrastructure for real-time collaboration and synchronization in tldraw applications. It implements a robust client-server protocol for sharing drawing state across multiple users, handling network reliability, conflict resolution, and maintaining data consistency in distributed environments.
|
||||
|
||||
## 1. Introduction
|
||||
|
||||
**Sync-core** is the engine that powers real-time collaboration in tldraw. It enables multiple users to work on the same drawing simultaneously, automatically synchronizing changes while gracefully handling network issues and conflicts.
|
||||
|
||||
You create collaborative tldraw applications by connecting clients to sync rooms, where each room manages the shared state for a document. Changes made by any user are automatically distributed to all other connected users in near real-time.
|
||||
|
||||
```ts
|
||||
import { TLSyncClient } from '@tldraw/sync-core'
|
||||
|
||||
// Connect to a collaborative room
|
||||
const syncClient = new TLSyncClient({
|
||||
store: myTldrawStore,
|
||||
socket: myWebSocketAdapter,
|
||||
roomId: 'drawing-room-123',
|
||||
})
|
||||
|
||||
syncClient.connect()
|
||||
// Now all changes to myTldrawStore are synchronized with other users
|
||||
```
|
||||
|
||||
When you update the store locally, the changes are immediately visible in your UI (optimistic updates), then sent to the server for validation and distribution to other clients.
|
||||
|
||||
> Tip: Sync-core is designed to work with any WebSocket implementation, making it suitable for various deployment scenarios from simple Node.js servers to edge computing platforms.
|
||||
|
||||
## 2. Core Concepts
|
||||
|
||||
### Client-Server Architecture
|
||||
|
||||
Sync-core uses a **server-authoritative** model where the server is the single source of truth for all changes. This ensures data consistency while still providing responsive local interactions:
|
||||
|
||||
- **Optimistic Updates**: Your local changes apply immediately for responsive UI
|
||||
- **Server Validation**: The server validates and potentially modifies your changes
|
||||
- **Conflict Resolution**: If conflicts occur, the server's version takes precedence
|
||||
|
||||
### Rooms and Sessions
|
||||
|
||||
A **room** represents a collaborative document space where multiple users can work together:
|
||||
|
||||
```ts
|
||||
// Server-side room management
|
||||
const room = new TLSyncRoom({
|
||||
store: serverStore,
|
||||
roomId: 'drawing-room-123',
|
||||
})
|
||||
|
||||
// Each connected client creates a session
|
||||
room.handleSocketConnect(clientSocket, sessionMeta)
|
||||
```
|
||||
|
||||
Each client connection creates a **session** within the room, tracking that user's connection state, permissions, and presence information.
|
||||
|
||||
### Network Diffs and Synchronization
|
||||
|
||||
Instead of sending entire document states, sync-core uses **network diffs** - compact representations of what actually changed:
|
||||
|
||||
```ts
|
||||
// Example network diff for updating a shape's position
|
||||
const diff = {
|
||||
'shape:abc123': [
|
||||
RecordOpType.Patch,
|
||||
{
|
||||
x: [ValueOpType.Put, 150],
|
||||
y: [ValueOpType.Put, 200],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
This approach minimizes bandwidth usage and enables efficient synchronization even with large documents.
|
||||
|
||||
## 3. Basic Usage
|
||||
|
||||
### Setting Up a Sync Client
|
||||
|
||||
To enable synchronization in your tldraw application, you need three components: a store, a WebSocket adapter, and a sync client:
|
||||
|
||||
```ts
|
||||
import { createTLStore } from '@tldraw/store'
|
||||
import { createTLSchema } from '@tldraw/tlschema'
|
||||
import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core'
|
||||
|
||||
// Create your tldraw store
|
||||
const store = createTLStore({
|
||||
schema: createTLSchema(),
|
||||
})
|
||||
|
||||
// Create a WebSocket connection
|
||||
const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync')
|
||||
|
||||
// Create the sync client
|
||||
const syncClient = new TLSyncClient({
|
||||
store,
|
||||
socket,
|
||||
roomId: 'my-drawing-room',
|
||||
})
|
||||
|
||||
// Start synchronization
|
||||
syncClient.connect()
|
||||
```
|
||||
|
||||
Once connected, any changes to your store will automatically sync with other clients in the same room.
|
||||
|
||||
### Monitoring Connection Status
|
||||
|
||||
The sync client provides reactive status information:
|
||||
|
||||
```ts
|
||||
import { react } from '@tldraw/state'
|
||||
|
||||
// React to connection status changes
|
||||
react('connection status', () => {
|
||||
const status = syncClient.status.get()
|
||||
|
||||
switch (status) {
|
||||
case 'offline':
|
||||
console.log('No network connection')
|
||||
break
|
||||
case 'connecting':
|
||||
console.log('Connecting to server...')
|
||||
break
|
||||
case 'online':
|
||||
console.log('Connected and synchronized')
|
||||
break
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
The status signal automatically updates as network conditions change, allowing your UI to reflect the current connection state.
|
||||
|
||||
### Handling Connection Events
|
||||
|
||||
You can listen to specific sync events for custom behavior:
|
||||
|
||||
```ts
|
||||
syncClient.onReceiveMessage((message) => {
|
||||
switch (message.type) {
|
||||
case 'connect':
|
||||
console.log('Successfully connected to room')
|
||||
break
|
||||
case 'incompatibility-error':
|
||||
console.log('Client version incompatible with server')
|
||||
break
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
> Tip: Always handle incompatibility errors gracefully - they indicate version mismatches between your client and server.
|
||||
|
||||
## 4. Advanced Topics
|
||||
|
||||
### Server-Side Room Management
|
||||
|
||||
On the server side, you manage rooms that coordinate multiple client sessions:
|
||||
|
||||
```ts
|
||||
import { TLSyncRoom } from '@tldraw/sync-core'
|
||||
|
||||
class CollaborationServer {
|
||||
private rooms = new Map<string, TLSyncRoom>()
|
||||
|
||||
getOrCreateRoom(roomId: string) {
|
||||
if (!this.rooms.has(roomId)) {
|
||||
const room = new TLSyncRoom({
|
||||
store: this.createRoomStore(),
|
||||
roomId,
|
||||
// Optional persistence adapter
|
||||
persistenceAdapter: this.createPersistenceAdapter(roomId),
|
||||
})
|
||||
|
||||
this.rooms.set(roomId, room)
|
||||
}
|
||||
|
||||
return this.rooms.get(roomId)!
|
||||
}
|
||||
|
||||
handleClientConnection(socket: WebSocket, roomId: string) {
|
||||
const room = this.getOrCreateRoom(roomId)
|
||||
room.handleSocketConnect(socket, {
|
||||
sessionId: generateSessionId(),
|
||||
userId: extractUserId(socket),
|
||||
isReadonly: checkPermissions(socket),
|
||||
})
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Rooms automatically handle session lifecycle, broadcasting changes, and cleaning up disconnected clients.
|
||||
|
||||
### Custom WebSocket Adapters
|
||||
|
||||
While sync-core provides a `ClientWebSocketAdapter`, you can implement custom adapters for specific requirements:
|
||||
|
||||
```ts
|
||||
import { TLPersistentClientSocket } from '@tldraw/sync-core'
|
||||
|
||||
class CustomSocketAdapter implements TLPersistentClientSocket {
|
||||
status = atom<TLPersistentClientSocketStatus>('offline')
|
||||
|
||||
sendMessage(message: any): void {
|
||||
// Your custom sending logic
|
||||
this.customWebSocket.send(JSON.stringify(message))
|
||||
}
|
||||
|
||||
onReceiveMessage = createNanoEvents<any>()
|
||||
onStatusChange = createNanoEvents<TLPersistentClientSocketStatus>()
|
||||
|
||||
restart(): void {
|
||||
// Your reconnection logic
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Custom adapters let you integrate with existing WebSocket libraries or add custom authentication and error handling.
|
||||
|
||||
### Conflict Resolution Strategies
|
||||
|
||||
When multiple users edit simultaneously, conflicts can occur. Sync-core's server-authoritative model resolves these automatically:
|
||||
|
||||
```ts
|
||||
// Client A moves shape to x: 100
|
||||
store.update('shape:abc', (shape) => ({ ...shape, x: 100 }))
|
||||
|
||||
// Simultaneously, Client B moves same shape to x: 200
|
||||
// Server receives both changes and determines the final state
|
||||
// All clients receive the server's authoritative version
|
||||
|
||||
react('shape changes', () => {
|
||||
const shape = store.get('shape:abc')
|
||||
// Final position will be whatever the server decided
|
||||
console.log('Final position:', shape?.x)
|
||||
})
|
||||
```
|
||||
|
||||
The server applies changes in the order it receives them, with later changes taking precedence for conflicting properties.
|
||||
|
||||
### Presence and Live Cursors
|
||||
|
||||
Sync-core supports real-time presence information like cursor positions:
|
||||
|
||||
```ts
|
||||
// Client sends presence updates
|
||||
syncClient.updatePresence({
|
||||
cursor: { x: 150, y: 200 },
|
||||
selection: ['shape:abc123'],
|
||||
userName: 'Alice',
|
||||
})
|
||||
|
||||
// Other clients receive presence updates
|
||||
syncClient.onPresenceUpdate((presenceUpdates) => {
|
||||
for (const [sessionId, presence] of presenceUpdates) {
|
||||
updateLiveCursor(sessionId, presence.cursor)
|
||||
updateUserSelection(sessionId, presence.selection)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
Presence updates are ephemeral - they don't persist to storage and are only visible to currently connected users.
|
||||
|
||||
### Schema Evolution and Migrations
|
||||
|
||||
When your application's data schema changes, sync-core coordinates migrations across clients:
|
||||
|
||||
```ts
|
||||
const schema = createTLSchema({
|
||||
// Your shape definitions
|
||||
shapes: {
|
||||
myShape: MyShapeUtil,
|
||||
},
|
||||
})
|
||||
|
||||
// The client sends its schema version during connection
|
||||
const syncClient = new TLSyncClient({
|
||||
store: createTLStore({ schema }),
|
||||
socket,
|
||||
roomId: 'room-123',
|
||||
})
|
||||
```
|
||||
|
||||
If schema versions don't match between client and server, sync-core will:
|
||||
|
||||
1. Attempt automatic migration if possible
|
||||
2. Send an incompatibility error if migration fails
|
||||
3. Allow graceful degradation for unknown record types
|
||||
|
||||
> Tip: Design your schema changes to be backward-compatible when possible to avoid forcing all users to upgrade simultaneously.
|
||||
|
||||
## 5. Debugging
|
||||
|
||||
Sync-core provides several tools for understanding and debugging synchronization behavior in your collaborative applications.
|
||||
|
||||
### Connection Diagnostics
|
||||
|
||||
Monitor the detailed connection lifecycle:
|
||||
|
||||
```ts
|
||||
import { TLSyncClient } from '@tldraw/sync-core'
|
||||
|
||||
const syncClient = new TLSyncClient({
|
||||
/* ... */
|
||||
})
|
||||
|
||||
// Enable detailed logging
|
||||
syncClient.onReceiveMessage((message) => {
|
||||
console.log('Received:', message.type, message)
|
||||
})
|
||||
|
||||
syncClient.onStatusChange((status, previous) => {
|
||||
console.log(`Status: ${previous} → ${status}`)
|
||||
})
|
||||
|
||||
// Connection attempt
|
||||
syncClient.connect()
|
||||
|
||||
// Output shows the complete handshake:
|
||||
// Status: offline → connecting
|
||||
// Received: connect { hydrationType: 'wipe_all', ... }
|
||||
// Status: connecting → online
|
||||
```
|
||||
|
||||
This reveals the exact sequence of messages during connection establishment and any errors that occur.
|
||||
|
||||
### Message Flow Analysis
|
||||
|
||||
Track all synchronization messages to understand data flow:
|
||||
|
||||
```ts
|
||||
// Log outgoing messages
|
||||
const originalSend = syncClient.socket.sendMessage
|
||||
syncClient.socket.sendMessage = (message) => {
|
||||
console.log('Sending:', message.type, message)
|
||||
originalSend.call(syncClient.socket, message)
|
||||
}
|
||||
|
||||
// Example output when making a change:
|
||||
// Sending: push { diff: { "shape:abc123": [2, { x: [1, 150] }] } }
|
||||
// Received: data { diff: { "shape:abc123": [2, { x: [1, 150] }] } }
|
||||
```
|
||||
|
||||
This shows how local changes become push messages and return as data messages from the server.
|
||||
|
||||
### Network Diff Inspection
|
||||
|
||||
Understand what changes are being synchronized:
|
||||
|
||||
```ts
|
||||
import { diffRecord } from '@tldraw/sync-core'
|
||||
|
||||
// Monitor store changes and see their diff representation
|
||||
const unsubscribe = store.listen(
|
||||
(entry) => {
|
||||
if (entry.changes.length > 0) {
|
||||
for (const change of entry.changes) {
|
||||
console.log('Change type:', change.source)
|
||||
console.log('Record diff:', change)
|
||||
|
||||
// For detailed diff analysis
|
||||
if (change.type === 'update') {
|
||||
const diff = diffRecord(change.prev, change.record)
|
||||
console.log('Network diff would be:', diff)
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
{ source: 'user' }
|
||||
)
|
||||
|
||||
// Example output:
|
||||
// Change type: user
|
||||
// Record diff: { type: 'update', id: 'shape:abc123', ... }
|
||||
// Network diff would be: { x: [1, 150], y: [1, 200] }
|
||||
```
|
||||
|
||||
### Session and Room Debugging
|
||||
|
||||
On the server side, inspect room and session states:
|
||||
|
||||
```ts
|
||||
class DebuggableRoom extends TLSyncRoom {
|
||||
debugSessions() {
|
||||
console.log(`Room ${this.roomId} has ${this.getNumActiveConnections()} connections:`)
|
||||
|
||||
for (const [sessionId, session] of this.sessions) {
|
||||
console.log(
|
||||
` ${sessionId}: ${session.state} (${session.isReadonly ? 'readonly' : 'read-write'})`
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
debugLastChange() {
|
||||
console.log('Last document change:', this.documentState.clock)
|
||||
console.log('Store has', Object.keys(this.store.serialize()).length, 'records')
|
||||
}
|
||||
}
|
||||
|
||||
// Use during development
|
||||
const room = new DebuggableRoom({
|
||||
/* ... */
|
||||
})
|
||||
setInterval(() => room.debugSessions(), 5000)
|
||||
```
|
||||
|
||||
### Error Diagnosis
|
||||
|
||||
Handle and debug common synchronization errors:
|
||||
|
||||
```ts
|
||||
syncClient.onReceiveMessage((message) => {
|
||||
switch (message.type) {
|
||||
case 'incompatibility-error':
|
||||
console.error('Schema mismatch:', {
|
||||
clientSchema: message.clientSchema,
|
||||
serverSchema: message.serverSchema,
|
||||
reason: message.reason,
|
||||
})
|
||||
break
|
||||
|
||||
case 'error':
|
||||
console.error('Sync error:', message.error)
|
||||
// Common causes:
|
||||
// - Room not found (check roomId)
|
||||
// - Permission denied (check authentication)
|
||||
// - Invalid record data (check schema validation)
|
||||
break
|
||||
}
|
||||
})
|
||||
|
||||
// Network-level debugging
|
||||
syncClient.socket.onStatusChange((status) => {
|
||||
if (status === 'offline') {
|
||||
console.log('Connection lost - check network and server health')
|
||||
|
||||
// Attempt manual reconnection
|
||||
setTimeout(() => {
|
||||
syncClient.socket.restart()
|
||||
}, 1000)
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
### Performance Monitoring
|
||||
|
||||
Track synchronization performance metrics:
|
||||
|
||||
```ts
|
||||
class SyncProfiler {
|
||||
private messageCount = 0
|
||||
private bytesTransferred = 0
|
||||
private roundTripTimes: number[] = []
|
||||
|
||||
profile(syncClient: TLSyncClient) {
|
||||
const startTime = Date.now()
|
||||
|
||||
syncClient.onReceiveMessage((message) => {
|
||||
this.messageCount++
|
||||
this.bytesTransferred += JSON.stringify(message).length
|
||||
|
||||
// Track ping/pong for latency
|
||||
if (message.type === 'pong') {
|
||||
const roundTrip = Date.now() - message.sentAt
|
||||
this.roundTripTimes.push(roundTrip)
|
||||
}
|
||||
})
|
||||
|
||||
// Periodic reporting
|
||||
setInterval(() => {
|
||||
const avgLatency =
|
||||
this.roundTripTimes.length > 0
|
||||
? this.roundTripTimes.reduce((a, b) => a + b, 0) / this.roundTripTimes.length
|
||||
: 0
|
||||
|
||||
console.log('Sync Performance:', {
|
||||
uptime: Date.now() - startTime,
|
||||
messages: this.messageCount,
|
||||
bytesTransferred: this.bytesTransferred,
|
||||
avgLatencyMs: avgLatency,
|
||||
})
|
||||
|
||||
this.roundTripTimes = [] // Reset for next period
|
||||
}, 30000)
|
||||
}
|
||||
}
|
||||
|
||||
new SyncProfiler().profile(syncClient)
|
||||
```
|
||||
|
||||
> Tip: High message counts or latency often indicate network issues or inefficient change patterns. Consider batching rapid changes or optimizing your shape update logic.
|
||||
|
||||
## 6. Integration
|
||||
|
||||
### React Integration
|
||||
|
||||
Sync-core integrates seamlessly with React applications through the store's reactive signals:
|
||||
|
||||
```ts
|
||||
import { useEditor } from '@tldraw/editor'
|
||||
import { react } from '@tldraw/state'
|
||||
import { useEffect, useState } from 'react'
|
||||
|
||||
function CollaborationStatusBadge() {
|
||||
const editor = useEditor()
|
||||
const [status, setStatus] = useState<string>('offline')
|
||||
|
||||
useEffect(() => {
|
||||
if (!editor.store.syncClient) return
|
||||
|
||||
return react('sync status', () => {
|
||||
setStatus(editor.store.syncClient.status.get())
|
||||
})
|
||||
}, [editor])
|
||||
|
||||
return (
|
||||
<div className={`status-badge ${status}`}>
|
||||
{status === 'online' ? '🟢 Connected' : '🔴 Offline'}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
```
|
||||
|
||||
The reactive nature of sync-core means your React components automatically update when connection status or synchronized data changes.
|
||||
|
||||
### Custom Persistence
|
||||
|
||||
Integrate with your existing database or storage systems:
|
||||
|
||||
```ts
|
||||
import { TLSyncRoom } from '@tldraw/sync-core'
|
||||
|
||||
class DatabasePersistenceAdapter {
|
||||
constructor(
|
||||
private db: Database,
|
||||
private roomId: string
|
||||
) {}
|
||||
|
||||
async loadRoom(): Promise<SerializedStore> {
|
||||
const roomData = await this.db.query('SELECT document_state FROM rooms WHERE id = ?', [
|
||||
this.roomId,
|
||||
])
|
||||
return JSON.parse(roomData.document_state)
|
||||
}
|
||||
|
||||
async saveRoom(serializedStore: SerializedStore): Promise<void> {
|
||||
await this.db.query('UPDATE rooms SET document_state = ?, updated_at = NOW() WHERE id = ?', [
|
||||
JSON.stringify(serializedStore),
|
||||
this.roomId,
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
const room = new TLSyncRoom({
|
||||
store: createTLStore({ schema }),
|
||||
roomId: 'room-123',
|
||||
persistenceAdapter: new DatabasePersistenceAdapter(myDatabase, 'room-123'),
|
||||
})
|
||||
```
|
||||
|
||||
This allows rooms to persist their state to your preferred storage backend while maintaining real-time synchronization.
|
||||
|
||||
### Authentication and Authorization
|
||||
|
||||
Implement custom authentication by extending the WebSocket adapter:
|
||||
|
||||
```ts
|
||||
class AuthenticatedSocketAdapter extends ClientWebSocketAdapter {
|
||||
constructor(
|
||||
url: string,
|
||||
private authToken: string
|
||||
) {
|
||||
super(url)
|
||||
}
|
||||
|
||||
protected connect(): void {
|
||||
this.ws = new WebSocket(this.url, [], {
|
||||
headers: {
|
||||
Authorization: `Bearer ${this.authToken}`,
|
||||
},
|
||||
})
|
||||
|
||||
this.setupEventHandlers()
|
||||
}
|
||||
}
|
||||
|
||||
// Server-side authentication
|
||||
room.handleSocketConnect(socket, {
|
||||
sessionId: generateSessionId(),
|
||||
userId: extractUserFromToken(authToken),
|
||||
isReadonly: !hasEditPermission(authToken, roomId),
|
||||
})
|
||||
```
|
||||
|
||||
### Multi-Room Applications
|
||||
|
||||
Manage multiple collaborative documents in a single application:
|
||||
|
||||
```ts
|
||||
class RoomManager {
|
||||
private rooms = new Map<string, TLSyncClient>()
|
||||
|
||||
joinRoom(roomId: string): TLSyncClient {
|
||||
if (this.rooms.has(roomId)) {
|
||||
return this.rooms.get(roomId)!
|
||||
}
|
||||
|
||||
const store = createTLStore({ schema: mySchema })
|
||||
const socket = new ClientWebSocketAdapter(`ws://localhost:3000/rooms/${roomId}`)
|
||||
const syncClient = new TLSyncClient({ store, socket, roomId })
|
||||
|
||||
this.rooms.set(roomId, syncClient)
|
||||
syncClient.connect()
|
||||
|
||||
return syncClient
|
||||
}
|
||||
|
||||
leaveRoom(roomId: string): void {
|
||||
const client = this.rooms.get(roomId)
|
||||
if (client) {
|
||||
client.disconnect()
|
||||
this.rooms.delete(roomId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const roomManager = new RoomManager()
|
||||
const drawingRoom = roomManager.joinRoom('drawing-123')
|
||||
const presentationRoom = roomManager.joinRoom('slides-456')
|
||||
```
|
||||
|
||||
### Edge Computing and Cloudflare Workers
|
||||
|
||||
Sync-core works well with edge computing platforms:
|
||||
|
||||
```ts
|
||||
// Cloudflare Worker example
|
||||
export default {
|
||||
async fetch(request: Request, env: Env): Promise<Response> {
|
||||
if (request.headers.get('Upgrade') !== 'websocket') {
|
||||
return new Response('Expected websocket', { status: 426 })
|
||||
}
|
||||
|
||||
const { 0: client, 1: server } = new WebSocketPair()
|
||||
const roomId = new URL(request.url).pathname.split('/').pop()
|
||||
|
||||
const room = this.getOrCreateRoom(roomId, env)
|
||||
room.handleSocketConnect(server, {
|
||||
sessionId: crypto.randomUUID(),
|
||||
// Extract user info from request headers or auth
|
||||
})
|
||||
|
||||
return new Response(null, {
|
||||
status: 101,
|
||||
webSocket: client,
|
||||
})
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
The lightweight nature of sync-core makes it suitable for serverless and edge environments where traditional long-running connections might be challenging.
|
||||
|
||||
> Tip: When deploying to edge environments, consider the trade-offs between geographical distribution (lower latency) and consistency (potential for split-brain scenarios).
|
||||
Reference in New Issue
Block a user