@tldraw/sync-core 5.2.0-canary.e22474d279fb → 5.2.0-canary.ed81413e0a67

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 ADDED
@@ -0,0 +1,662 @@
1
+ # @tldraw/sync-core
2
+
3
+ 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.
4
+
5
+ ## 1. Introduction
6
+
7
+ **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.
8
+
9
+ 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.
10
+
11
+ ```ts
12
+ import { TLSyncClient } from '@tldraw/sync-core'
13
+
14
+ // Connect to a collaborative room
15
+ const syncClient = new TLSyncClient({
16
+ store: myTldrawStore,
17
+ socket: myWebSocketAdapter,
18
+ roomId: 'drawing-room-123',
19
+ })
20
+
21
+ syncClient.connect()
22
+ // Now all changes to myTldrawStore are synchronized with other users
23
+ ```
24
+
25
+ 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.
26
+
27
+ > 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.
28
+
29
+ ## 2. Core Concepts
30
+
31
+ ### Client-Server Architecture
32
+
33
+ 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:
34
+
35
+ - **Optimistic Updates**: Your local changes apply immediately for responsive UI
36
+ - **Server Validation**: The server validates and potentially modifies your changes
37
+ - **Conflict Resolution**: If conflicts occur, the server's version takes precedence
38
+
39
+ ### Rooms and Sessions
40
+
41
+ A **room** represents a collaborative document space where multiple users can work together:
42
+
43
+ ```ts
44
+ // Server-side room management
45
+ const room = new TLSyncRoom({
46
+ store: serverStore,
47
+ roomId: 'drawing-room-123',
48
+ })
49
+
50
+ // Each connected client creates a session
51
+ room.handleSocketConnect(clientSocket, sessionMeta)
52
+ ```
53
+
54
+ Each client connection creates a **session** within the room, tracking that user's connection state, permissions, and presence information.
55
+
56
+ ### Network Diffs and Synchronization
57
+
58
+ Instead of sending entire document states, sync-core uses **network diffs** - compact representations of what actually changed:
59
+
60
+ ```ts
61
+ // Example network diff for updating a shape's position
62
+ const diff = {
63
+ 'shape:abc123': [
64
+ RecordOpType.Patch,
65
+ {
66
+ x: [ValueOpType.Put, 150],
67
+ y: [ValueOpType.Put, 200],
68
+ },
69
+ ],
70
+ }
71
+ ```
72
+
73
+ This approach minimizes bandwidth usage and enables efficient synchronization even with large documents.
74
+
75
+ ## 3. Basic Usage
76
+
77
+ ### Setting Up a Sync Client
78
+
79
+ To enable synchronization in your tldraw application, you need three components: a store, a WebSocket adapter, and a sync client:
80
+
81
+ ```ts
82
+ import { createTLStore } from '@tldraw/store'
83
+ import { createTLSchema } from '@tldraw/tlschema'
84
+ import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core'
85
+
86
+ // Create your tldraw store
87
+ const store = createTLStore({
88
+ schema: createTLSchema(),
89
+ })
90
+
91
+ // Create a WebSocket connection
92
+ const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync')
93
+
94
+ // Create the sync client
95
+ const syncClient = new TLSyncClient({
96
+ store,
97
+ socket,
98
+ roomId: 'my-drawing-room',
99
+ })
100
+
101
+ // Start synchronization
102
+ syncClient.connect()
103
+ ```
104
+
105
+ Once connected, any changes to your store will automatically sync with other clients in the same room.
106
+
107
+ ### Monitoring Connection Status
108
+
109
+ The sync client provides reactive status information:
110
+
111
+ ```ts
112
+ import { react } from '@tldraw/state'
113
+
114
+ // React to connection status changes
115
+ react('connection status', () => {
116
+ const status = syncClient.status.get()
117
+
118
+ switch (status) {
119
+ case 'offline':
120
+ console.log('No network connection')
121
+ break
122
+ case 'connecting':
123
+ console.log('Connecting to server...')
124
+ break
125
+ case 'online':
126
+ console.log('Connected and synchronized')
127
+ break
128
+ }
129
+ })
130
+ ```
131
+
132
+ The status signal automatically updates as network conditions change, allowing your UI to reflect the current connection state.
133
+
134
+ ### Handling Connection Events
135
+
136
+ You can listen to specific sync events for custom behavior:
137
+
138
+ ```ts
139
+ syncClient.onReceiveMessage((message) => {
140
+ switch (message.type) {
141
+ case 'connect':
142
+ console.log('Successfully connected to room')
143
+ break
144
+ case 'incompatibility-error':
145
+ console.log('Client version incompatible with server')
146
+ break
147
+ }
148
+ })
149
+ ```
150
+
151
+ > Tip: Always handle incompatibility errors gracefully - they indicate version mismatches between your client and server.
152
+
153
+ ## 4. Advanced Topics
154
+
155
+ ### Server-Side Room Management
156
+
157
+ On the server side, you manage rooms that coordinate multiple client sessions:
158
+
159
+ ```ts
160
+ import { TLSyncRoom } from '@tldraw/sync-core'
161
+
162
+ class CollaborationServer {
163
+ private rooms = new Map<string, TLSyncRoom>()
164
+
165
+ getOrCreateRoom(roomId: string) {
166
+ if (!this.rooms.has(roomId)) {
167
+ const room = new TLSyncRoom({
168
+ store: this.createRoomStore(),
169
+ roomId,
170
+ // Optional persistence adapter
171
+ persistenceAdapter: this.createPersistenceAdapter(roomId),
172
+ })
173
+
174
+ this.rooms.set(roomId, room)
175
+ }
176
+
177
+ return this.rooms.get(roomId)!
178
+ }
179
+
180
+ handleClientConnection(socket: WebSocket, roomId: string) {
181
+ const room = this.getOrCreateRoom(roomId)
182
+ room.handleSocketConnect(socket, {
183
+ sessionId: generateSessionId(),
184
+ userId: extractUserId(socket),
185
+ isReadonly: checkPermissions(socket),
186
+ })
187
+ }
188
+ }
189
+ ```
190
+
191
+ Rooms automatically handle session lifecycle, broadcasting changes, and cleaning up disconnected clients.
192
+
193
+ ### Custom WebSocket Adapters
194
+
195
+ While sync-core provides a `ClientWebSocketAdapter`, you can implement custom adapters for specific requirements:
196
+
197
+ ```ts
198
+ import { TLPersistentClientSocket } from '@tldraw/sync-core'
199
+
200
+ class CustomSocketAdapter implements TLPersistentClientSocket {
201
+ status = atom<TLPersistentClientSocketStatus>('offline')
202
+
203
+ sendMessage(message: any): void {
204
+ // Your custom sending logic
205
+ this.customWebSocket.send(JSON.stringify(message))
206
+ }
207
+
208
+ onReceiveMessage = createNanoEvents<any>()
209
+ onStatusChange = createNanoEvents<TLPersistentClientSocketStatus>()
210
+
211
+ restart(): void {
212
+ // Your reconnection logic
213
+ }
214
+ }
215
+ ```
216
+
217
+ Custom adapters let you integrate with existing WebSocket libraries or add custom authentication and error handling.
218
+
219
+ ### Conflict Resolution Strategies
220
+
221
+ When multiple users edit simultaneously, conflicts can occur. Sync-core's server-authoritative model resolves these automatically:
222
+
223
+ ```ts
224
+ // Client A moves shape to x: 100
225
+ store.update('shape:abc', (shape) => ({ ...shape, x: 100 }))
226
+
227
+ // Simultaneously, Client B moves same shape to x: 200
228
+ // Server receives both changes and determines the final state
229
+ // All clients receive the server's authoritative version
230
+
231
+ react('shape changes', () => {
232
+ const shape = store.get('shape:abc')
233
+ // Final position will be whatever the server decided
234
+ console.log('Final position:', shape?.x)
235
+ })
236
+ ```
237
+
238
+ The server applies changes in the order it receives them, with later changes taking precedence for conflicting properties.
239
+
240
+ ### Presence and Live Cursors
241
+
242
+ Sync-core supports real-time presence information like cursor positions:
243
+
244
+ ```ts
245
+ // Client sends presence updates
246
+ syncClient.updatePresence({
247
+ cursor: { x: 150, y: 200 },
248
+ selection: ['shape:abc123'],
249
+ userName: 'Alice',
250
+ })
251
+
252
+ // Other clients receive presence updates
253
+ syncClient.onPresenceUpdate((presenceUpdates) => {
254
+ for (const [sessionId, presence] of presenceUpdates) {
255
+ updateLiveCursor(sessionId, presence.cursor)
256
+ updateUserSelection(sessionId, presence.selection)
257
+ }
258
+ })
259
+ ```
260
+
261
+ Presence updates are ephemeral - they don't persist to storage and are only visible to currently connected users.
262
+
263
+ ### Schema Evolution and Migrations
264
+
265
+ When your application's data schema changes, sync-core coordinates migrations across clients:
266
+
267
+ ```ts
268
+ const schema = createTLSchema({
269
+ // Your shape definitions
270
+ shapes: {
271
+ myShape: MyShapeUtil,
272
+ },
273
+ })
274
+
275
+ // The client sends its schema version during connection
276
+ const syncClient = new TLSyncClient({
277
+ store: createTLStore({ schema }),
278
+ socket,
279
+ roomId: 'room-123',
280
+ })
281
+ ```
282
+
283
+ If schema versions don't match between client and server, sync-core will:
284
+
285
+ 1. Attempt automatic migration if possible
286
+ 2. Send an incompatibility error if migration fails
287
+ 3. Allow graceful degradation for unknown record types
288
+
289
+ > Tip: Design your schema changes to be backward-compatible when possible to avoid forcing all users to upgrade simultaneously.
290
+
291
+ ## 5. Debugging
292
+
293
+ Sync-core provides several tools for understanding and debugging synchronization behavior in your collaborative applications.
294
+
295
+ ### Connection Diagnostics
296
+
297
+ Monitor the detailed connection lifecycle:
298
+
299
+ ```ts
300
+ import { TLSyncClient } from '@tldraw/sync-core'
301
+
302
+ const syncClient = new TLSyncClient({
303
+ /* ... */
304
+ })
305
+
306
+ // Enable detailed logging
307
+ syncClient.onReceiveMessage((message) => {
308
+ console.log('Received:', message.type, message)
309
+ })
310
+
311
+ syncClient.onStatusChange((status, previous) => {
312
+ console.log(`Status: ${previous} → ${status}`)
313
+ })
314
+
315
+ // Connection attempt
316
+ syncClient.connect()
317
+
318
+ // Output shows the complete handshake:
319
+ // Status: offline → connecting
320
+ // Received: connect { hydrationType: 'wipe_all', ... }
321
+ // Status: connecting → online
322
+ ```
323
+
324
+ This reveals the exact sequence of messages during connection establishment and any errors that occur.
325
+
326
+ ### Message Flow Analysis
327
+
328
+ Track all synchronization messages to understand data flow:
329
+
330
+ ```ts
331
+ // Log outgoing messages
332
+ const originalSend = syncClient.socket.sendMessage
333
+ syncClient.socket.sendMessage = (message) => {
334
+ console.log('Sending:', message.type, message)
335
+ originalSend.call(syncClient.socket, message)
336
+ }
337
+
338
+ // Example output when making a change:
339
+ // Sending: push { diff: { "shape:abc123": [2, { x: [1, 150] }] } }
340
+ // Received: data { diff: { "shape:abc123": [2, { x: [1, 150] }] } }
341
+ ```
342
+
343
+ This shows how local changes become push messages and return as data messages from the server.
344
+
345
+ ### Network Diff Inspection
346
+
347
+ Understand what changes are being synchronized:
348
+
349
+ ```ts
350
+ import { diffRecord } from '@tldraw/sync-core'
351
+
352
+ // Monitor store changes and see their diff representation
353
+ const unsubscribe = store.listen(
354
+ (entry) => {
355
+ if (entry.changes.length > 0) {
356
+ for (const change of entry.changes) {
357
+ console.log('Change type:', change.source)
358
+ console.log('Record diff:', change)
359
+
360
+ // For detailed diff analysis
361
+ if (change.type === 'update') {
362
+ const diff = diffRecord(change.prev, change.record)
363
+ console.log('Network diff would be:', diff)
364
+ }
365
+ }
366
+ }
367
+ },
368
+ { source: 'user' }
369
+ )
370
+
371
+ // Example output:
372
+ // Change type: user
373
+ // Record diff: { type: 'update', id: 'shape:abc123', ... }
374
+ // Network diff would be: { x: [1, 150], y: [1, 200] }
375
+ ```
376
+
377
+ ### Session and Room Debugging
378
+
379
+ On the server side, inspect room and session states:
380
+
381
+ ```ts
382
+ class DebuggableRoom extends TLSyncRoom {
383
+ debugSessions() {
384
+ console.log(`Room ${this.roomId} has ${this.getNumActiveConnections()} connections:`)
385
+
386
+ for (const [sessionId, session] of this.sessions) {
387
+ console.log(
388
+ ` ${sessionId}: ${session.state} (${session.isReadonly ? 'readonly' : 'read-write'})`
389
+ )
390
+ }
391
+ }
392
+
393
+ debugLastChange() {
394
+ console.log('Last document change:', this.documentState.clock)
395
+ console.log('Store has', Object.keys(this.store.serialize()).length, 'records')
396
+ }
397
+ }
398
+
399
+ // Use during development
400
+ const room = new DebuggableRoom({
401
+ /* ... */
402
+ })
403
+ setInterval(() => room.debugSessions(), 5000)
404
+ ```
405
+
406
+ ### Error Diagnosis
407
+
408
+ Handle and debug common synchronization errors:
409
+
410
+ ```ts
411
+ syncClient.onReceiveMessage((message) => {
412
+ switch (message.type) {
413
+ case 'incompatibility-error':
414
+ console.error('Schema mismatch:', {
415
+ clientSchema: message.clientSchema,
416
+ serverSchema: message.serverSchema,
417
+ reason: message.reason,
418
+ })
419
+ break
420
+
421
+ case 'error':
422
+ console.error('Sync error:', message.error)
423
+ // Common causes:
424
+ // - Room not found (check roomId)
425
+ // - Permission denied (check authentication)
426
+ // - Invalid record data (check schema validation)
427
+ break
428
+ }
429
+ })
430
+
431
+ // Network-level debugging
432
+ syncClient.socket.onStatusChange((status) => {
433
+ if (status === 'offline') {
434
+ console.log('Connection lost - check network and server health')
435
+
436
+ // Attempt manual reconnection
437
+ setTimeout(() => {
438
+ syncClient.socket.restart()
439
+ }, 1000)
440
+ }
441
+ })
442
+ ```
443
+
444
+ ### Performance Monitoring
445
+
446
+ Track synchronization performance metrics:
447
+
448
+ ```ts
449
+ class SyncProfiler {
450
+ private messageCount = 0
451
+ private bytesTransferred = 0
452
+ private roundTripTimes: number[] = []
453
+
454
+ profile(syncClient: TLSyncClient) {
455
+ const startTime = Date.now()
456
+
457
+ syncClient.onReceiveMessage((message) => {
458
+ this.messageCount++
459
+ this.bytesTransferred += JSON.stringify(message).length
460
+
461
+ // Track ping/pong for latency
462
+ if (message.type === 'pong') {
463
+ const roundTrip = Date.now() - message.sentAt
464
+ this.roundTripTimes.push(roundTrip)
465
+ }
466
+ })
467
+
468
+ // Periodic reporting
469
+ setInterval(() => {
470
+ const avgLatency =
471
+ this.roundTripTimes.length > 0
472
+ ? this.roundTripTimes.reduce((a, b) => a + b, 0) / this.roundTripTimes.length
473
+ : 0
474
+
475
+ console.log('Sync Performance:', {
476
+ uptime: Date.now() - startTime,
477
+ messages: this.messageCount,
478
+ bytesTransferred: this.bytesTransferred,
479
+ avgLatencyMs: avgLatency,
480
+ })
481
+
482
+ this.roundTripTimes = [] // Reset for next period
483
+ }, 30000)
484
+ }
485
+ }
486
+
487
+ new SyncProfiler().profile(syncClient)
488
+ ```
489
+
490
+ > Tip: High message counts or latency often indicate network issues or inefficient change patterns. Consider batching rapid changes or optimizing your shape update logic.
491
+
492
+ ## 6. Integration
493
+
494
+ ### React Integration
495
+
496
+ Sync-core integrates seamlessly with React applications through the store's reactive signals:
497
+
498
+ ```ts
499
+ import { useEditor } from '@tldraw/editor'
500
+ import { react } from '@tldraw/state'
501
+ import { useEffect, useState } from 'react'
502
+
503
+ function CollaborationStatusBadge() {
504
+ const editor = useEditor()
505
+ const [status, setStatus] = useState<string>('offline')
506
+
507
+ useEffect(() => {
508
+ if (!editor.store.syncClient) return
509
+
510
+ return react('sync status', () => {
511
+ setStatus(editor.store.syncClient.status.get())
512
+ })
513
+ }, [editor])
514
+
515
+ return (
516
+ <div className={`status-badge ${status}`}>
517
+ {status === 'online' ? '🟢 Connected' : '🔴 Offline'}
518
+ </div>
519
+ )
520
+ }
521
+ ```
522
+
523
+ The reactive nature of sync-core means your React components automatically update when connection status or synchronized data changes.
524
+
525
+ ### Custom Persistence
526
+
527
+ Integrate with your existing database or storage systems:
528
+
529
+ ```ts
530
+ import { TLSyncRoom } from '@tldraw/sync-core'
531
+
532
+ class DatabasePersistenceAdapter {
533
+ constructor(
534
+ private db: Database,
535
+ private roomId: string
536
+ ) {}
537
+
538
+ async loadRoom(): Promise<SerializedStore> {
539
+ const roomData = await this.db.query('SELECT document_state FROM rooms WHERE id = ?', [
540
+ this.roomId,
541
+ ])
542
+ return JSON.parse(roomData.document_state)
543
+ }
544
+
545
+ async saveRoom(serializedStore: SerializedStore): Promise<void> {
546
+ await this.db.query('UPDATE rooms SET document_state = ?, updated_at = NOW() WHERE id = ?', [
547
+ JSON.stringify(serializedStore),
548
+ this.roomId,
549
+ ])
550
+ }
551
+ }
552
+
553
+ const room = new TLSyncRoom({
554
+ store: createTLStore({ schema }),
555
+ roomId: 'room-123',
556
+ persistenceAdapter: new DatabasePersistenceAdapter(myDatabase, 'room-123'),
557
+ })
558
+ ```
559
+
560
+ This allows rooms to persist their state to your preferred storage backend while maintaining real-time synchronization.
561
+
562
+ ### Authentication and Authorization
563
+
564
+ Implement custom authentication by extending the WebSocket adapter:
565
+
566
+ ```ts
567
+ class AuthenticatedSocketAdapter extends ClientWebSocketAdapter {
568
+ constructor(
569
+ url: string,
570
+ private authToken: string
571
+ ) {
572
+ super(url)
573
+ }
574
+
575
+ protected connect(): void {
576
+ this.ws = new WebSocket(this.url, [], {
577
+ headers: {
578
+ Authorization: `Bearer ${this.authToken}`,
579
+ },
580
+ })
581
+
582
+ this.setupEventHandlers()
583
+ }
584
+ }
585
+
586
+ // Server-side authentication
587
+ room.handleSocketConnect(socket, {
588
+ sessionId: generateSessionId(),
589
+ userId: extractUserFromToken(authToken),
590
+ isReadonly: !hasEditPermission(authToken, roomId),
591
+ })
592
+ ```
593
+
594
+ ### Multi-Room Applications
595
+
596
+ Manage multiple collaborative documents in a single application:
597
+
598
+ ```ts
599
+ class RoomManager {
600
+ private rooms = new Map<string, TLSyncClient>()
601
+
602
+ joinRoom(roomId: string): TLSyncClient {
603
+ if (this.rooms.has(roomId)) {
604
+ return this.rooms.get(roomId)!
605
+ }
606
+
607
+ const store = createTLStore({ schema: mySchema })
608
+ const socket = new ClientWebSocketAdapter(`ws://localhost:3000/rooms/${roomId}`)
609
+ const syncClient = new TLSyncClient({ store, socket, roomId })
610
+
611
+ this.rooms.set(roomId, syncClient)
612
+ syncClient.connect()
613
+
614
+ return syncClient
615
+ }
616
+
617
+ leaveRoom(roomId: string): void {
618
+ const client = this.rooms.get(roomId)
619
+ if (client) {
620
+ client.disconnect()
621
+ this.rooms.delete(roomId)
622
+ }
623
+ }
624
+ }
625
+
626
+ const roomManager = new RoomManager()
627
+ const drawingRoom = roomManager.joinRoom('drawing-123')
628
+ const presentationRoom = roomManager.joinRoom('slides-456')
629
+ ```
630
+
631
+ ### Edge Computing and Cloudflare Workers
632
+
633
+ Sync-core works well with edge computing platforms:
634
+
635
+ ```ts
636
+ // Cloudflare Worker example
637
+ export default {
638
+ async fetch(request: Request, env: Env): Promise<Response> {
639
+ if (request.headers.get('Upgrade') !== 'websocket') {
640
+ return new Response('Expected websocket', { status: 426 })
641
+ }
642
+
643
+ const { 0: client, 1: server } = new WebSocketPair()
644
+ const roomId = new URL(request.url).pathname.split('/').pop()
645
+
646
+ const room = this.getOrCreateRoom(roomId, env)
647
+ room.handleSocketConnect(server, {
648
+ sessionId: crypto.randomUUID(),
649
+ // Extract user info from request headers or auth
650
+ })
651
+
652
+ return new Response(null, {
653
+ status: 101,
654
+ webSocket: client,
655
+ })
656
+ },
657
+ }
658
+ ```
659
+
660
+ The lightweight nature of sync-core makes it suitable for serverless and edge environments where traditional long-running connections might be challenging.
661
+
662
+ > Tip: When deploying to edge environments, consider the trade-offs between geographical distribution (lower latency) and consistency (potential for split-brain scenarios).
package/README.md CHANGED
@@ -2,6 +2,14 @@
2
2
 
3
3
  This project contains core functionality 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
+ Documentation for the most recent release can be found on [tldraw.dev/docs](https://tldraw.dev/docs), including [reference docs](https://tldraw.dev/reference/editor/Editor). Our release notes can be found [here](https://tldraw.dev/releases).
8
+
9
+ For more agent-friendly docs, see our [LLMs.txt](https://tldraw.dev/llms.txt).
10
+
11
+ A `DOCS.md` file is included alongside this README in the published package, with detailed API documentation and usage examples.
12
+
5
13
  ## License
6
14
 
7
15
  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).
@@ -18,7 +26,7 @@ You can find tldraw on npm [here](https://www.npmjs.com/package/@tldraw/tldraw?a
18
26
 
19
27
  ## Contribution
20
28
 
21
- Please see our [contributing guide](https://github.com/tldraw/tldraw/blob/main/CONTRIBUTING.md). Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
29
+ Found a bug? Please [submit an issue](https://github.com/tldraw/tldraw/issues/new).
22
30
 
23
31
  ## Community
24
32
 
package/dist-cjs/index.js CHANGED
@@ -61,7 +61,7 @@ var import_TLSyncRoom = require("./lib/TLSyncRoom");
61
61
  var import_TLSyncStorage = require("./lib/TLSyncStorage");
62
62
  (0, import_utils.registerTldrawLibraryVersion)(
63
63
  "@tldraw/sync-core",
64
- "5.2.0-canary.e22474d279fb",
64
+ "5.2.0-canary.ed81413e0a67",
65
65
  "cjs"
66
66
  );
67
67
  //# sourceMappingURL=index.js.map
@@ -36,7 +36,7 @@ import {
36
36
  } from "./lib/TLSyncStorage.mjs";
37
37
  registerTldrawLibraryVersion(
38
38
  "@tldraw/sync-core",
39
- "5.2.0-canary.e22474d279fb",
39
+ "5.2.0-canary.ed81413e0a67",
40
40
  "esm"
41
41
  );
42
42
  export {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tldraw/sync-core",
3
3
  "description": "tldraw infinite canvas SDK (multiplayer sync).",
4
- "version": "5.2.0-canary.e22474d279fb",
4
+ "version": "5.2.0-canary.ed81413e0a67",
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",
@@ -48,17 +49,17 @@
48
49
  "@types/uuid-readable": "^0.0.3",
49
50
  "react": "^19.2.1",
50
51
  "react-dom": "^19.2.1",
51
- "tldraw": "5.2.0-canary.e22474d279fb",
52
+ "tldraw": "5.2.0-canary.ed81413e0a67",
52
53
  "typescript": "^5.8.3",
53
54
  "uuid-by-string": "^4.0.0",
54
55
  "uuid-readable": "^0.0.2",
55
- "vitest": "^3.2.4"
56
+ "vitest": "^4.1.7"
56
57
  },
57
58
  "dependencies": {
58
- "@tldraw/state": "5.2.0-canary.e22474d279fb",
59
- "@tldraw/store": "5.2.0-canary.e22474d279fb",
60
- "@tldraw/tlschema": "5.2.0-canary.e22474d279fb",
61
- "@tldraw/utils": "5.2.0-canary.e22474d279fb",
59
+ "@tldraw/state": "5.2.0-canary.ed81413e0a67",
60
+ "@tldraw/store": "5.2.0-canary.ed81413e0a67",
61
+ "@tldraw/tlschema": "5.2.0-canary.ed81413e0a67",
62
+ "@tldraw/utils": "5.2.0-canary.ed81413e0a67",
62
63
  "nanoevents": "^7.0.1",
63
64
  "ws": "^8.18.0"
64
65
  },
@@ -1,8 +1,10 @@
1
1
  /* eslint-disable @typescript-eslint/no-deprecated */
2
2
  import {
3
+ createUserId,
3
4
  InstancePresenceRecordType,
4
5
  PageRecordType,
5
6
  TLDocument,
7
+ TLInstancePresence,
6
8
  TLPage,
7
9
  TLRecord,
8
10
  } from '@tldraw/tlschema'
@@ -192,14 +194,14 @@ describe(TLSocketRoom, () => {
192
194
  // Create presence records for each session
193
195
  const presence1 = InstancePresenceRecordType.create({
194
196
  id: InstancePresenceRecordType.createId('presence1'),
195
- userId: 'user1',
197
+ userId: createUserId('user1'),
196
198
  userName: 'User 1',
197
199
  currentPageId: PageRecordType.createId('page'),
198
200
  })
199
201
 
200
202
  const presence2 = InstancePresenceRecordType.create({
201
203
  id: InstancePresenceRecordType.createId('presence2'),
202
- userId: 'user2',
204
+ userId: createUserId('user2'),
203
205
  userName: 'User 2',
204
206
  currentPageId: PageRecordType.createId('page'),
205
207
  })
@@ -226,8 +228,12 @@ describe(TLSocketRoom, () => {
226
228
  expect(Object.keys(presenceRecords)).toHaveLength(2)
227
229
 
228
230
  // Find the presence records by their user data since the IDs are generated by the room
229
- const user1Presence = Object.values(presenceRecords).find((p) => (p as any).userId === 'user1')
230
- const user2Presence = Object.values(presenceRecords).find((p) => (p as any).userId === 'user2')
231
+ const user1Presence = Object.values(presenceRecords).find(
232
+ (p) => (p as TLInstancePresence).userId === createUserId('user1')
233
+ )
234
+ const user2Presence = Object.values(presenceRecords).find(
235
+ (p) => (p as TLInstancePresence).userId === createUserId('user2')
236
+ )
231
237
 
232
238
  expect(user1Presence).toBeDefined()
233
239
  expect(user2Presence).toBeDefined()
@@ -235,13 +241,13 @@ describe(TLSocketRoom, () => {
235
241
  // Verify the records are properly structured
236
242
  expect(user1Presence).toMatchObject({
237
243
  typeName: 'instance_presence',
238
- userId: 'user1',
244
+ userId: createUserId('user1'),
239
245
  userName: 'User 1',
240
246
  })
241
247
 
242
248
  expect(user2Presence).toMatchObject({
243
249
  typeName: 'instance_presence',
244
- userId: 'user2',
250
+ userId: createUserId('user2'),
245
251
  userName: 'User 2',
246
252
  })
247
253
 
@@ -429,7 +435,7 @@ describe(TLSocketRoom, () => {
429
435
 
430
436
  describe('Session management', () => {
431
437
  let room: TLSocketRoom
432
- let onSessionRemoved: ReturnType<typeof vi.fn>
438
+ let onSessionRemoved: ReturnType<typeof vi.fn<(...args: any[]) => any>>
433
439
 
434
440
  beforeEach(() => {
435
441
  onSessionRemoved = vi.fn()
@@ -488,8 +494,8 @@ describe(TLSocketRoom, () => {
488
494
  describe('Message handling', () => {
489
495
  let room: TLSocketRoom
490
496
  let socket: WebSocketMinimal
491
- let onBeforeSendMessage: ReturnType<typeof vi.fn>
492
- let onAfterReceiveMessage: ReturnType<typeof vi.fn>
497
+ let onBeforeSendMessage: ReturnType<typeof vi.fn<(...args: any[]) => any>>
498
+ let onAfterReceiveMessage: ReturnType<typeof vi.fn<(...args: any[]) => any>>
493
499
 
494
500
  beforeEach(() => {
495
501
  onBeforeSendMessage = vi.fn()
@@ -854,7 +860,7 @@ describe('Hibernation support', () => {
854
860
 
855
861
  const presence = InstancePresenceRecordType.create({
856
862
  id: InstancePresenceRecordType.createId('p1'),
857
- userId: 'user1',
863
+ userId: createUserId('user1'),
858
864
  userName: 'User 1',
859
865
  currentPageId: PageRecordType.createId('page'),
860
866
  })
@@ -868,7 +874,7 @@ describe('Hibernation support', () => {
868
874
  const snapshot = room.getSessionSnapshot('test')
869
875
  expect(snapshot).not.toBeNull()
870
876
  expect(snapshot!.presenceRecord).not.toBeNull()
871
- expect((snapshot!.presenceRecord as any).userId).toBe('user1')
877
+ expect((snapshot!.presenceRecord as TLInstancePresence).userId).toBe(createUserId('user1'))
872
878
  })
873
879
  })
874
880
 
@@ -944,7 +950,7 @@ describe('Hibernation support', () => {
944
950
  // Push presence
945
951
  const presence = InstancePresenceRecordType.create({
946
952
  id: InstancePresenceRecordType.createId('p1'),
947
- userId: 'user1',
953
+ userId: createUserId('user1'),
948
954
  userName: 'User 1',
949
955
  currentPageId: PageRecordType.createId('page'),
950
956
  })
@@ -973,7 +979,7 @@ describe('Hibernation support', () => {
973
979
  const presenceRecords = room2.getPresenceRecords()
974
980
  expect(Object.keys(presenceRecords)).toHaveLength(1)
975
981
  const restored = Object.values(presenceRecords)[0]
976
- expect((restored as any).userId).toBe('user1')
982
+ expect((restored as TLInstancePresence).userId).toBe(createUserId('user1'))
977
983
  })
978
984
 
979
985
  it('does not attach event listeners', () => {
@@ -1167,7 +1173,7 @@ describe('Hibernation support', () => {
1167
1173
  // Push presence
1168
1174
  const presence = InstancePresenceRecordType.create({
1169
1175
  id: InstancePresenceRecordType.createId('p1'),
1170
- userId: 'user1',
1176
+ userId: createUserId('user1'),
1171
1177
  userName: 'User 1',
1172
1178
  currentPageId: PageRecordType.createId('page'),
1173
1179
  })
@@ -12,6 +12,7 @@ import {
12
12
  TLRecord,
13
13
  TLShapeId,
14
14
  createTLSchema,
15
+ createUserId,
15
16
  } from '@tldraw/tlschema'
16
17
  import { IndexKey, ZERO_INDEX_KEY, mockUniqueId, sortById } from '@tldraw/utils'
17
18
  import { vi } from 'vitest'
@@ -301,7 +302,7 @@ describe('isReadonly', () => {
301
302
  InstancePresenceRecordType.create({
302
303
  id: InstancePresenceRecordType.createId('foo'),
303
304
  currentPageId: 'page:page_2' as any,
304
- userId: 'foo',
305
+ userId: createUserId('foo'),
305
306
  userName: 'Jimbo',
306
307
  }),
307
308
  ],
@@ -349,7 +350,7 @@ describe('isReadonly', () => {
349
350
  "scribbles": [],
350
351
  "selectedShapeIds": [],
351
352
  "typeName": "instance_presence",
352
- "userId": "foo",
353
+ "userId": "user:foo",
353
354
  "userName": "Jimbo",
354
355
  },
355
356
  ],
@@ -904,7 +905,7 @@ describe('Presence store isolation', () => {
904
905
  InstancePresenceRecordType.create({
905
906
  id: InstancePresenceRecordType.createId('presence-1'),
906
907
  currentPageId: PageRecordType.createId('page_2'),
907
- userId: 'user-1',
908
+ userId: createUserId('user-1'),
908
909
  userName: 'Test User',
909
910
  }),
910
911
  ],
@@ -949,7 +950,7 @@ describe('Presence store isolation', () => {
949
950
  InstancePresenceRecordType.create({
950
951
  id: InstancePresenceRecordType.createId('any'),
951
952
  currentPageId: PageRecordType.createId('page_2'),
952
- userId: 'user-1',
953
+ userId: createUserId('user-1'),
953
954
  userName: 'Test User',
954
955
  }),
955
956
  ],
@@ -999,7 +1000,7 @@ describe('Presence store isolation', () => {
999
1000
  InstancePresenceRecordType.create({
1000
1001
  id: InstancePresenceRecordType.createId('any'),
1001
1002
  currentPageId: PageRecordType.createId('page_2'),
1002
- userId: 'user-1',
1003
+ userId: createUserId('user-1'),
1003
1004
  userName: 'Test User',
1004
1005
  }),
1005
1006
  ],