@tldraw/sync 5.2.0-canary.5e29f4eb77b9 → 5.2.0-canary.c0d1b9467a7f
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DOCS.md +637 -0
- package/README.md +4 -0
- package/dist-cjs/index.js +1 -1
- package/dist-esm/index.mjs +1 -1
- package/package.json +8 -7
package/DOCS.md
ADDED
|
@@ -0,0 +1,637 @@
|
|
|
1
|
+
# @tldraw/sync
|
|
2
|
+
|
|
3
|
+
The `@tldraw/sync` package provides React hooks and utilities for integrating real-time collaboration into your tldraw applications. Built on top of `@tldraw/sync-core`, it offers a developer-friendly API that enables multiplayer functionality with minimal configuration.
|
|
4
|
+
|
|
5
|
+
## 1. Introduction
|
|
6
|
+
|
|
7
|
+
Real-time collaboration transforms single-user drawing applications into shared creative spaces where multiple users can work together simultaneously. The sync package handles the complex coordination required for multiplayer experiences: synchronizing changes, managing user presence, handling network interruptions, and resolving conflicts.
|
|
8
|
+
|
|
9
|
+
You create collaborative tldraw applications using two main hooks:
|
|
10
|
+
|
|
11
|
+
- `useSync` - For production applications with custom servers
|
|
12
|
+
- `useSyncDemo` - For prototypes and demos using tldraw's hosted demo server
|
|
13
|
+
|
|
14
|
+
Both hooks return a store wrapped with connection status, allowing you to build responsive UIs that gracefully handle loading states, connection issues, and real-time updates.
|
|
15
|
+
|
|
16
|
+
## 2. Core Concepts
|
|
17
|
+
|
|
18
|
+
### Multiplayer Store State
|
|
19
|
+
|
|
20
|
+
The foundation of sync integration is the **RemoteTLStoreWithStatus**, an enhanced store wrapper that tracks both your drawing data and connection state:
|
|
21
|
+
|
|
22
|
+
```ts
|
|
23
|
+
import { useSync } from '@tldraw/sync'
|
|
24
|
+
|
|
25
|
+
function MyApp() {
|
|
26
|
+
const store = useSync({
|
|
27
|
+
uri: 'wss://myserver.com/sync/room-123',
|
|
28
|
+
assets: myAssetStore,
|
|
29
|
+
userInfo: { id: 'user-1', name: 'Alice', color: '#ff0000' }
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
// Store progresses through these states:
|
|
33
|
+
if (store.status === 'loading') return <div>Connecting...</div>
|
|
34
|
+
if (store.status === 'error') return <div>Connection failed: {store.error.message}</div>
|
|
35
|
+
|
|
36
|
+
// store.status === 'synced-remote'
|
|
37
|
+
return <Tldraw store={store.store} />
|
|
38
|
+
}
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The store moves through three distinct states as it establishes and maintains connection:
|
|
42
|
+
|
|
43
|
+
1. **loading** - Initial connection and synchronization with the server
|
|
44
|
+
2. **synced-remote** - Successfully connected and actively synchronizing changes
|
|
45
|
+
3. **error** - Connection failed or a synchronization error occurred
|
|
46
|
+
|
|
47
|
+
### User Presence
|
|
48
|
+
|
|
49
|
+
**/User presence** encompasses the real-time information about other users in your collaborative session. This includes cursor positions, current selections, and any custom presence data you want to share. The `useSync` hook handles this automatically, but you can provide a custom `getUserPresence` function to send additional data.
|
|
50
|
+
|
|
51
|
+
```ts
|
|
52
|
+
const store = useSync({
|
|
53
|
+
uri: wsUri,
|
|
54
|
+
assets: myAssets,
|
|
55
|
+
})
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The presence system automatically optimizes itself based on room occupancy, switching between 'solo' mode when you're alone and 'full' mode when collaborating with others.
|
|
59
|
+
|
|
60
|
+
### Asset Management
|
|
61
|
+
|
|
62
|
+
**Assets** are files like images, videos, and other media that users embed in their drawings. The sync package requires an asset store implementation that handles uploading files and resolving them for display:
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
const myAssetStore = {
|
|
66
|
+
upload: async (asset, file) => {
|
|
67
|
+
// Upload file to your storage service
|
|
68
|
+
const url = await uploadToStorage(file)
|
|
69
|
+
return { src: url }
|
|
70
|
+
},
|
|
71
|
+
|
|
72
|
+
resolve: (asset, context) => {
|
|
73
|
+
// Return optimized URLs based on context
|
|
74
|
+
// (screen DPI, network quality, display size)
|
|
75
|
+
return getOptimizedUrl(asset.src, context)
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
## 3. Basic Usage
|
|
81
|
+
|
|
82
|
+
### Quick Start with Demo Server
|
|
83
|
+
|
|
84
|
+
The fastest way to add collaboration to your application is using the demo server. This requires no backend setup and works immediately:
|
|
85
|
+
|
|
86
|
+
```ts
|
|
87
|
+
import { useSyncDemo } from '@tldraw/sync'
|
|
88
|
+
import { Tldraw } from 'tldraw'
|
|
89
|
+
|
|
90
|
+
function CollaborativeApp() {
|
|
91
|
+
const store = useSyncDemo({
|
|
92
|
+
roomId: 'my-prototype-room-123',
|
|
93
|
+
userInfo: {
|
|
94
|
+
id: 'user-' + Math.random(),
|
|
95
|
+
name: 'Anonymous User',
|
|
96
|
+
color: '#' + Math.floor(Math.random()*16777215).toString(16)
|
|
97
|
+
}
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
if (store.status === 'loading') {
|
|
101
|
+
return <div>Connecting to room...</div>
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (store.status === 'error') {
|
|
105
|
+
return <div>Failed to connect: {store.error.message}</div>
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return <Tldraw store={store.store} />
|
|
109
|
+
}
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
> Tip: The demo server is perfect for prototyping and testing, but data is automatically deleted after ~24 hours and rooms are publicly accessible to anyone with the room ID.
|
|
113
|
+
|
|
114
|
+
### Production Integration
|
|
115
|
+
|
|
116
|
+
For production applications, use `useSync` with your own WebSocket server:
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
import { useSync } from '@tldraw/sync'
|
|
120
|
+
import { Tldraw } from 'tldraw'
|
|
121
|
+
|
|
122
|
+
function ProductionApp() {
|
|
123
|
+
const store = useSync({
|
|
124
|
+
uri: 'wss://myserver.com/sync/project-collaboration-session',
|
|
125
|
+
userInfo: getCurrentUser(), // Your user system integration
|
|
126
|
+
assets: productionAssetStore, // Your asset storage integration
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
return (
|
|
130
|
+
<div>
|
|
131
|
+
{store.status === 'loading' && <LoadingSpinner />}
|
|
132
|
+
{store.status === 'error' && <ErrorMessage error={store.error} />}
|
|
133
|
+
{store.status === 'synced-remote' && (
|
|
134
|
+
<>
|
|
135
|
+
<ConnectionIndicator status={store.connectionStatus} />
|
|
136
|
+
<Tldraw store={store.store} />
|
|
137
|
+
</>
|
|
138
|
+
)}
|
|
139
|
+
</div>
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## 4. Advanced Configuration
|
|
145
|
+
|
|
146
|
+
### Dynamic Connection URIs
|
|
147
|
+
|
|
148
|
+
You can provide connection URIs dynamically, which is essential for authentication and room-specific routing:
|
|
149
|
+
|
|
150
|
+
```ts
|
|
151
|
+
const store = useSync({
|
|
152
|
+
uri: async () => {
|
|
153
|
+
const token = await getAuthToken()
|
|
154
|
+
const roomId = getCurrentRoomId()
|
|
155
|
+
return `wss://myserver.com/sync/${roomId}?token=${token}`
|
|
156
|
+
},
|
|
157
|
+
assets: authenticatedAssetStore,
|
|
158
|
+
userInfo: userSignal, // Can be a reactive signal that updates
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
When the URI function is async, the sync system waits for it to resolve before attempting connection. This ensures your authentication flow completes before establishing the WebSocket connection.
|
|
163
|
+
|
|
164
|
+
### Reactive User Information
|
|
165
|
+
|
|
166
|
+
User information can be static or reactive. Using reactive signals allows the presence system to automatically update when user details change:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import { atom } from '@tldraw/state'
|
|
170
|
+
|
|
171
|
+
const currentUser = atom('currentUser', {
|
|
172
|
+
id: 'user-123',
|
|
173
|
+
name: 'Alice',
|
|
174
|
+
color: '#ff0000',
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
const store = useSync({
|
|
178
|
+
uri: wsUri,
|
|
179
|
+
assets: myAssets,
|
|
180
|
+
userInfo: currentUser, // Reactive signal
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
// Later, when user updates their profile:
|
|
184
|
+
currentUser.set({
|
|
185
|
+
id: 'user-123',
|
|
186
|
+
name: 'Alice Cooper', // Updated name
|
|
187
|
+
color: '#00ff00', // New color
|
|
188
|
+
})
|
|
189
|
+
// Presence automatically updates for all connected users
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
### Custom Presence Data
|
|
193
|
+
|
|
194
|
+
The `getUserPresence` function allows you to include custom presence information beyond the standard cursor and selection data. The function receives the `store` and the current `user` info. It should return an object that conforms to the `TLPresenceStateInfo` type.
|
|
195
|
+
|
|
196
|
+
Note that the `store` object passed to this function is a `TLStore` instance, and does not have an `editor` property. To access editor-specific state like the current tool or cursor position, you will need to find a way to access the `Editor` instance from your component.
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
const store = useSync({
|
|
200
|
+
uri: wsUri,
|
|
201
|
+
assets: myAssets,
|
|
202
|
+
getUserPresence: (store, user) => {
|
|
203
|
+
// This function is called whenever the store changes.
|
|
204
|
+
// You can use it to derive presence information from the store.
|
|
205
|
+
// To get information like cursor position, you may need to
|
|
206
|
+
// find a way to access your <Tldraw /> component's editor instance.
|
|
207
|
+
|
|
208
|
+
return {
|
|
209
|
+
userId: user.id,
|
|
210
|
+
userName: user.name,
|
|
211
|
+
// ... and other properties from TLPresenceStateInfo
|
|
212
|
+
}
|
|
213
|
+
},
|
|
214
|
+
})
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### Asset Store Implementation
|
|
218
|
+
|
|
219
|
+
A complete asset store handles both uploading new files and resolving existing assets for optimal display:
|
|
220
|
+
|
|
221
|
+
```ts
|
|
222
|
+
const productionAssetStore = {
|
|
223
|
+
upload: async (asset, file) => {
|
|
224
|
+
// Generate unique filename
|
|
225
|
+
const filename = `${Date.now()}-${file.name}`
|
|
226
|
+
|
|
227
|
+
// Upload to your storage service
|
|
228
|
+
const formData = new FormData()
|
|
229
|
+
formData.append('file', file)
|
|
230
|
+
formData.append('filename', filename)
|
|
231
|
+
|
|
232
|
+
const response = await fetch('/api/upload', {
|
|
233
|
+
method: 'POST',
|
|
234
|
+
body: formData,
|
|
235
|
+
headers: {
|
|
236
|
+
Authorization: `Bearer ${await getAuthToken()}`,
|
|
237
|
+
},
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
const { url } = await response.json()
|
|
241
|
+
return { src: url }
|
|
242
|
+
},
|
|
243
|
+
|
|
244
|
+
resolve: (asset, context) => {
|
|
245
|
+
const baseUrl = asset.src
|
|
246
|
+
|
|
247
|
+
// Return different resolutions based on context
|
|
248
|
+
if (context.shouldResolveToOriginal) {
|
|
249
|
+
return baseUrl // Full quality for printing/export
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Optimize based on display size and screen density
|
|
253
|
+
const targetWidth = Math.ceil(context.screenScale * context.imageSize.w)
|
|
254
|
+
const targetHeight = Math.ceil(context.screenScale * context.imageSize.h)
|
|
255
|
+
|
|
256
|
+
return `${baseUrl}?w=${targetWidth}&h=${targetHeight}&q=${context.networkQuality}`
|
|
257
|
+
},
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
## 5. Connection Management
|
|
262
|
+
|
|
263
|
+
### Understanding Connection States
|
|
264
|
+
|
|
265
|
+
The sync system provides granular connection status information to help you build responsive UIs:
|
|
266
|
+
|
|
267
|
+
```ts
|
|
268
|
+
function ConnectionAwareApp() {
|
|
269
|
+
const store = useSync({ /* ... */ })
|
|
270
|
+
|
|
271
|
+
if (store.status === 'loading') {
|
|
272
|
+
return <div>Establishing connection...</div>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (store.status === 'error') {
|
|
276
|
+
return (
|
|
277
|
+
<div>
|
|
278
|
+
<h3>Connection Error</h3>
|
|
279
|
+
<p>{store.error.message}</p>
|
|
280
|
+
<button onClick={() => window.location.reload()}>
|
|
281
|
+
Retry Connection
|
|
282
|
+
</button>
|
|
283
|
+
</div>
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// store.status === 'synced-remote'
|
|
288
|
+
return (
|
|
289
|
+
<div>
|
|
290
|
+
<NetworkIndicator status={store.connectionStatus} />
|
|
291
|
+
<Tldraw store={store.store} />
|
|
292
|
+
</div>
|
|
293
|
+
)
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function NetworkIndicator({ status }) {
|
|
297
|
+
if (status === 'offline') {
|
|
298
|
+
return <div className="warning">Working offline - changes will sync when reconnected</div>
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (status === 'online') {
|
|
302
|
+
return <div className="success">Connected and syncing</div>
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
return null
|
|
306
|
+
}
|
|
307
|
+
```
|
|
308
|
+
|
|
309
|
+
The connection status operates independently of the sync status. Even when `store.status` is 'synced-remote', the `connectionStatus` can be 'offline' if network connectivity is lost, allowing the application to continue working locally.
|
|
310
|
+
|
|
311
|
+
### Automatic Reconnection
|
|
312
|
+
|
|
313
|
+
The sync system handles network interruptions gracefully with automatic reconnection and state recovery:
|
|
314
|
+
|
|
315
|
+
```ts
|
|
316
|
+
// No additional code needed - reconnection is automatic
|
|
317
|
+
const store = useSync({
|
|
318
|
+
uri: 'wss://myserver.com/sync/room-123',
|
|
319
|
+
assets: myAssets,
|
|
320
|
+
userInfo: currentUser,
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
// The system automatically:
|
|
324
|
+
// 1. Detects network disconnection
|
|
325
|
+
// 2. Queues local changes while offline
|
|
326
|
+
// 3. Attempts reconnection with exponential backoff
|
|
327
|
+
// 4. Reconciles state when connection is restored
|
|
328
|
+
// 5. Handles conflicts between local and server changes
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
### Error Handling and Recovery
|
|
332
|
+
|
|
333
|
+
Different types of connection errors require different handling strategies:
|
|
334
|
+
|
|
335
|
+
```ts
|
|
336
|
+
function ErrorHandlingApp() {
|
|
337
|
+
const store = useSync({ /* ... */ })
|
|
338
|
+
|
|
339
|
+
if (store.status === 'error') {
|
|
340
|
+
const error = store.error
|
|
341
|
+
|
|
342
|
+
// Check specific error types for appropriate responses
|
|
343
|
+
if (error.reason === 'NOT_FOUND') {
|
|
344
|
+
return <div>Room not found. Please check the room ID.</div>
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (error.reason === 'FORBIDDEN') {
|
|
348
|
+
return <div>Access denied. Please check your permissions.</div>
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (error.reason === 'NOT_AUTHENTICATED') {
|
|
352
|
+
return <div>Authentication required. <button onClick={login}>Login</button></div>
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
if (error.reason === 'RATE_LIMITED') {
|
|
356
|
+
return <div>Too many requests. Please wait before retrying.</div>
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Generic network or server error
|
|
360
|
+
return (
|
|
361
|
+
<div>
|
|
362
|
+
<h3>Connection Error</h3>
|
|
363
|
+
<p>Unable to connect to collaboration server.</p>
|
|
364
|
+
<button onClick={() => window.location.reload()}>Retry</button>
|
|
365
|
+
</div>
|
|
366
|
+
)
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return <Tldraw store={store.store} />
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
## 6. Debugging
|
|
374
|
+
|
|
375
|
+
### Understanding Connection Behavior
|
|
376
|
+
|
|
377
|
+
The sync package provides several tools for understanding and debugging connection behavior in your application.
|
|
378
|
+
|
|
379
|
+
#### Connection Status Monitoring
|
|
380
|
+
|
|
381
|
+
You can monitor connection events by logging the status changes:
|
|
382
|
+
|
|
383
|
+
```ts
|
|
384
|
+
import { useEffect } from 'react'
|
|
385
|
+
|
|
386
|
+
function DebuggableApp() {
|
|
387
|
+
const store = useSync({
|
|
388
|
+
uri: 'wss://myserver.com/sync/room-123',
|
|
389
|
+
assets: myAssets,
|
|
390
|
+
userInfo: currentUser,
|
|
391
|
+
trackAnalyticsEvent: (name, data) => {
|
|
392
|
+
console.log('Sync Event:', name, data)
|
|
393
|
+
}
|
|
394
|
+
})
|
|
395
|
+
|
|
396
|
+
useEffect(() => {
|
|
397
|
+
console.log('Store status changed:', store.status)
|
|
398
|
+
|
|
399
|
+
if (store.status === 'synced-remote') {
|
|
400
|
+
console.log('Connection status:', store.connectionStatus)
|
|
401
|
+
}
|
|
402
|
+
}, [store.status, store.status === 'synced-remote' ? store.connectionStatus : null])
|
|
403
|
+
|
|
404
|
+
return <Tldraw store={store.store} />
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
This will log events like:
|
|
409
|
+
|
|
410
|
+
```
|
|
411
|
+
Sync Event: room-not-found { roomId: "room-123" }
|
|
412
|
+
Sync Event: connected { isReadonly: false }
|
|
413
|
+
Store status changed: synced-remote
|
|
414
|
+
Connection status: online
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
#### Network Quality Detection
|
|
418
|
+
|
|
419
|
+
The demo asset store includes network quality detection that affects image resolution:
|
|
420
|
+
|
|
421
|
+
```ts
|
|
422
|
+
// In the demo environment, you can observe network adaptation:
|
|
423
|
+
const store = useSyncDemo({
|
|
424
|
+
roomId: 'debug-room',
|
|
425
|
+
userInfo: currentUser,
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
// Images automatically adjust quality based on:
|
|
429
|
+
// - Connection speed (detected from WebSocket latency)
|
|
430
|
+
// - Screen pixel density
|
|
431
|
+
// - Actual display size
|
|
432
|
+
// - File size thresholds
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
#### Presence System Debugging
|
|
436
|
+
|
|
437
|
+
You can debug presence updates by monitoring presence mode changes:
|
|
438
|
+
|
|
439
|
+
```ts
|
|
440
|
+
import { getDefaultUserPresence } from 'tldraw'
|
|
441
|
+
|
|
442
|
+
function PresenceDebuggingApp() {
|
|
443
|
+
const store = useSync({
|
|
444
|
+
uri: wsUri,
|
|
445
|
+
assets: myAssets,
|
|
446
|
+
getUserPresence: (store, user) => {
|
|
447
|
+
// See the "Custom Presence Data" section for details
|
|
448
|
+
// on how to implement this function.
|
|
449
|
+
const presence = getDefaultUserPresence(store, user)
|
|
450
|
+
console.log('Updating presence:', presence)
|
|
451
|
+
return presence
|
|
452
|
+
},
|
|
453
|
+
})
|
|
454
|
+
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (store.status === 'synced-remote') {
|
|
457
|
+
// Monitor presence mode changes
|
|
458
|
+
const unsubscribe = store.store.listen(() => {
|
|
459
|
+
const presences = store.store.allRecords().filter(r => r.typeName === 'instance_presence')
|
|
460
|
+
console.log('Active users:', presences.length)
|
|
461
|
+
console.log('Presence mode:', presences.length > 1 ? 'collaborative' : 'solo')
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
return unsubscribe
|
|
465
|
+
}
|
|
466
|
+
}, [store])
|
|
467
|
+
|
|
468
|
+
return <Tldraw store={store.store} />
|
|
469
|
+
}
|
|
470
|
+
```
|
|
471
|
+
|
|
472
|
+
#### Common Connection Issues
|
|
473
|
+
|
|
474
|
+
**WebSocket Connection Failures**
|
|
475
|
+
|
|
476
|
+
If connections consistently fail, verify the WebSocket URL format:
|
|
477
|
+
|
|
478
|
+
```ts
|
|
479
|
+
// ✅ Correct formats:
|
|
480
|
+
'wss://myserver.com/sync'
|
|
481
|
+
'wss://myserver.com/sync/room-123'
|
|
482
|
+
'ws://localhost:3001/sync' // Development only
|
|
483
|
+
|
|
484
|
+
// ❌ Common mistakes:
|
|
485
|
+
'https://myserver.com/sync' // Wrong protocol
|
|
486
|
+
'wss://myserver.com/sync/' // Trailing slash may cause issues
|
|
487
|
+
'myserver.com/sync' // Missing protocol
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
**Authentication Token Issues**
|
|
491
|
+
|
|
492
|
+
When using token authentication, ensure tokens remain valid throughout the session:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
const store = useSync({
|
|
496
|
+
uri: async () => {
|
|
497
|
+
const token = await refreshTokenIfNeeded() // Ensure token is fresh
|
|
498
|
+
return `wss://myserver.com/sync?token=${token}`
|
|
499
|
+
},
|
|
500
|
+
assets: myAssets,
|
|
501
|
+
userInfo: currentUser,
|
|
502
|
+
})
|
|
503
|
+
```
|
|
504
|
+
|
|
505
|
+
**Asset Upload Problems**
|
|
506
|
+
|
|
507
|
+
Asset upload failures often relate to CORS configuration or authentication:
|
|
508
|
+
|
|
509
|
+
```ts
|
|
510
|
+
const debugAssetStore = {
|
|
511
|
+
upload: async (asset, file) => {
|
|
512
|
+
console.log('Uploading asset:', {
|
|
513
|
+
name: file.name,
|
|
514
|
+
size: file.size,
|
|
515
|
+
type: file.type,
|
|
516
|
+
})
|
|
517
|
+
|
|
518
|
+
try {
|
|
519
|
+
const result = await uploadToServer(file)
|
|
520
|
+
console.log('Upload successful:', result)
|
|
521
|
+
return result
|
|
522
|
+
} catch (error) {
|
|
523
|
+
console.error('Upload failed:', error)
|
|
524
|
+
throw error
|
|
525
|
+
}
|
|
526
|
+
},
|
|
527
|
+
|
|
528
|
+
resolve: (asset, context) => {
|
|
529
|
+
console.log('Resolving asset:', asset.src, 'with context:', context)
|
|
530
|
+
return asset.src
|
|
531
|
+
},
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
## 7. Integration with Authentication
|
|
536
|
+
|
|
537
|
+
### Token-Based Authentication
|
|
538
|
+
|
|
539
|
+
Most production applications require authentication. The sync package supports token-based auth through dynamic URI generation:
|
|
540
|
+
|
|
541
|
+
```ts
|
|
542
|
+
import { useAuth } from './auth-system'
|
|
543
|
+
|
|
544
|
+
function AuthenticatedApp() {
|
|
545
|
+
const { user, getToken } = useAuth()
|
|
546
|
+
|
|
547
|
+
const store = useSync({
|
|
548
|
+
uri: async () => {
|
|
549
|
+
if (!user) throw new Error('Not authenticated')
|
|
550
|
+
|
|
551
|
+
const token = await getToken()
|
|
552
|
+
return `wss://myserver.com/sync/room-123?token=${token}&userId=${user.id}`
|
|
553
|
+
},
|
|
554
|
+
userInfo: {
|
|
555
|
+
id: user.id,
|
|
556
|
+
name: user.displayName,
|
|
557
|
+
color: user.preferredColor
|
|
558
|
+
},
|
|
559
|
+
assets: createAuthenticatedAssetStore(getToken),
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
if (!user) {
|
|
563
|
+
return <LoginPrompt />
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
return <Tldraw store={store.store} />
|
|
567
|
+
}
|
|
568
|
+
```
|
|
569
|
+
|
|
570
|
+
### Session Management
|
|
571
|
+
|
|
572
|
+
Handle authentication state changes gracefully by recreating the sync connection:
|
|
573
|
+
|
|
574
|
+
```ts
|
|
575
|
+
function SessionManagedApp() {
|
|
576
|
+
const { user, sessionId } = useAuth()
|
|
577
|
+
|
|
578
|
+
// Recreate sync connection when session changes
|
|
579
|
+
const store = useSync({
|
|
580
|
+
uri: `wss://myserver.com/sync?session=${sessionId}`,
|
|
581
|
+
userInfo: user ? {
|
|
582
|
+
id: user.id,
|
|
583
|
+
name: user.name,
|
|
584
|
+
color: user.color
|
|
585
|
+
} : null,
|
|
586
|
+
assets: myAssetStore,
|
|
587
|
+
})
|
|
588
|
+
|
|
589
|
+
// Handle logout
|
|
590
|
+
const handleLogout = () => {
|
|
591
|
+
// Sync connection will automatically clean up
|
|
592
|
+
// when component unmounts or deps change
|
|
593
|
+
logout()
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
return (
|
|
597
|
+
<div>
|
|
598
|
+
{user && <button onClick={handleLogout}>Logout</button>}
|
|
599
|
+
<Tldraw store={store.store} />
|
|
600
|
+
</div>
|
|
601
|
+
)
|
|
602
|
+
}
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### Role-Based Permissions
|
|
606
|
+
|
|
607
|
+
Integrate with permission systems by handling readonly mode:
|
|
608
|
+
|
|
609
|
+
```ts
|
|
610
|
+
function PermissionAwareApp() {
|
|
611
|
+
const store = useSync({
|
|
612
|
+
uri: wsUri,
|
|
613
|
+
assets: myAssets,
|
|
614
|
+
userInfo: currentUser,
|
|
615
|
+
})
|
|
616
|
+
|
|
617
|
+
if (store.status === 'synced-remote') {
|
|
618
|
+
// Server can set readonly mode based on user permissions
|
|
619
|
+
const isReadonly = store.store.collaboration?.mode === 'readonly'
|
|
620
|
+
|
|
621
|
+
if (isReadonly) {
|
|
622
|
+
return (
|
|
623
|
+
<div>
|
|
624
|
+
<div className="notice">You have view-only access to this room</div>
|
|
625
|
+
<Tldraw store={store.store} />
|
|
626
|
+
</div>
|
|
627
|
+
)
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
return <Tldraw store={store.store} />
|
|
632
|
+
}
|
|
633
|
+
```
|
|
634
|
+
|
|
635
|
+
The sync system automatically handles permission enforcement when the server sets readonly mode, preventing local changes from being applied or synchronized.
|
|
636
|
+
|
|
637
|
+
This comprehensive guide covers the essential concepts, practical implementation patterns, and advanced features of the `@tldraw/sync` package. The reactive nature of the sync system, combined with robust error handling and flexible configuration options, enables you to build reliable collaborative experiences that gracefully handle the complexities of real-time multiplayer interaction.
|
package/README.md
CHANGED
|
@@ -2,6 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
This project contains source code for [tldraw sync](https://tldraw.dev/docs/sync). [Click here](https://tldraw.dev/blog/product/announcing-tldraw-sync) to learn more.
|
|
4
4
|
|
|
5
|
+
## Documentation
|
|
6
|
+
|
|
7
|
+
A `DOCS.md` file is included alongside this README in the published package, with detailed API documentation and usage examples.
|
|
8
|
+
|
|
5
9
|
## License
|
|
6
10
|
|
|
7
11
|
This project is part of the tldraw SDK. It is provided under the [tldraw SDK license](https://github.com/tldraw/tldraw/blob/main/LICENSE.md).
|
package/dist-cjs/index.js
CHANGED
|
@@ -29,7 +29,7 @@ var import_useSync = require("./useSync");
|
|
|
29
29
|
var import_useSyncDemo = require("./useSyncDemo");
|
|
30
30
|
(0, import_utils.registerTldrawLibraryVersion)(
|
|
31
31
|
"@tldraw/sync",
|
|
32
|
-
"5.2.0-canary.
|
|
32
|
+
"5.2.0-canary.c0d1b9467a7f",
|
|
33
33
|
"cjs"
|
|
34
34
|
);
|
|
35
35
|
//# sourceMappingURL=index.js.map
|
package/dist-esm/index.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tldraw/sync",
|
|
3
3
|
"description": "tldraw infinite canvas SDK (multiplayer sync react bindings).",
|
|
4
|
-
"version": "5.2.0-canary.
|
|
4
|
+
"version": "5.2.0-canary.c0d1b9467a7f",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "tldraw GB Ltd.",
|
|
7
7
|
"email": "hello@tldraw.com"
|
|
@@ -29,7 +29,8 @@
|
|
|
29
29
|
"files": [
|
|
30
30
|
"dist-esm",
|
|
31
31
|
"dist-cjs",
|
|
32
|
-
"src"
|
|
32
|
+
"src",
|
|
33
|
+
"DOCS.md"
|
|
33
34
|
],
|
|
34
35
|
"scripts": {
|
|
35
36
|
"test-ci": "yarn run -T vitest run --passWithNoTests",
|
|
@@ -53,12 +54,12 @@
|
|
|
53
54
|
"vitest": "^3.2.4"
|
|
54
55
|
},
|
|
55
56
|
"dependencies": {
|
|
56
|
-
"@tldraw/state": "5.2.0-canary.
|
|
57
|
-
"@tldraw/state-react": "5.2.0-canary.
|
|
58
|
-
"@tldraw/sync-core": "5.2.0-canary.
|
|
59
|
-
"@tldraw/utils": "5.2.0-canary.
|
|
57
|
+
"@tldraw/state": "5.2.0-canary.c0d1b9467a7f",
|
|
58
|
+
"@tldraw/state-react": "5.2.0-canary.c0d1b9467a7f",
|
|
59
|
+
"@tldraw/sync-core": "5.2.0-canary.c0d1b9467a7f",
|
|
60
|
+
"@tldraw/utils": "5.2.0-canary.c0d1b9467a7f",
|
|
60
61
|
"nanoevents": "^7.0.1",
|
|
61
|
-
"tldraw": "5.2.0-canary.
|
|
62
|
+
"tldraw": "5.2.0-canary.c0d1b9467a7f",
|
|
62
63
|
"ws": "^8.18.0"
|
|
63
64
|
},
|
|
64
65
|
"peerDependencies": {
|