@tldraw/sync-core 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b73a0d46b63f

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.
Files changed (67) hide show
  1. package/dist-cjs/index.d.ts +605 -75
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/ClientWebSocketAdapter.js +144 -0
  4. package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
  5. package/dist-cjs/lib/RoomSession.js +3 -0
  6. package/dist-cjs/lib/RoomSession.js.map +2 -2
  7. package/dist-cjs/lib/ServerSocketAdapter.js +23 -0
  8. package/dist-cjs/lib/ServerSocketAdapter.js.map +2 -2
  9. package/dist-cjs/lib/TLRemoteSyncError.js +8 -0
  10. package/dist-cjs/lib/TLRemoteSyncError.js.map +2 -2
  11. package/dist-cjs/lib/TLSocketRoom.js +280 -56
  12. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  13. package/dist-cjs/lib/TLSyncClient.js +45 -2
  14. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  15. package/dist-cjs/lib/TLSyncRoom.js +161 -16
  16. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  17. package/dist-cjs/lib/chunk.js +30 -0
  18. package/dist-cjs/lib/chunk.js.map +2 -2
  19. package/dist-cjs/lib/diff.js.map +2 -2
  20. package/dist-cjs/lib/findMin.js.map +2 -2
  21. package/dist-cjs/lib/interval.js.map +2 -2
  22. package/dist-cjs/lib/protocol.js.map +2 -2
  23. package/dist-cjs/lib/server-types.js.map +1 -1
  24. package/dist-esm/index.d.mts +605 -75
  25. package/dist-esm/index.mjs +1 -1
  26. package/dist-esm/lib/ClientWebSocketAdapter.mjs +144 -0
  27. package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
  28. package/dist-esm/lib/RoomSession.mjs +3 -0
  29. package/dist-esm/lib/RoomSession.mjs.map +2 -2
  30. package/dist-esm/lib/ServerSocketAdapter.mjs +23 -0
  31. package/dist-esm/lib/ServerSocketAdapter.mjs.map +2 -2
  32. package/dist-esm/lib/TLRemoteSyncError.mjs +8 -0
  33. package/dist-esm/lib/TLRemoteSyncError.mjs.map +2 -2
  34. package/dist-esm/lib/TLSocketRoom.mjs +280 -56
  35. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  36. package/dist-esm/lib/TLSyncClient.mjs +45 -2
  37. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  38. package/dist-esm/lib/TLSyncRoom.mjs +161 -16
  39. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  40. package/dist-esm/lib/chunk.mjs +30 -0
  41. package/dist-esm/lib/chunk.mjs.map +2 -2
  42. package/dist-esm/lib/diff.mjs.map +2 -2
  43. package/dist-esm/lib/findMin.mjs.map +2 -2
  44. package/dist-esm/lib/interval.mjs.map +2 -2
  45. package/dist-esm/lib/protocol.mjs.map +2 -2
  46. package/package.json +6 -6
  47. package/src/lib/ClientWebSocketAdapter.test.ts +712 -129
  48. package/src/lib/ClientWebSocketAdapter.ts +240 -9
  49. package/src/lib/RoomSession.test.ts +97 -0
  50. package/src/lib/RoomSession.ts +105 -3
  51. package/src/lib/ServerSocketAdapter.test.ts +228 -0
  52. package/src/lib/ServerSocketAdapter.ts +124 -5
  53. package/src/lib/TLRemoteSyncError.ts +50 -1
  54. package/src/lib/TLSocketRoom.ts +377 -60
  55. package/src/lib/TLSyncClient.test.ts +828 -0
  56. package/src/lib/TLSyncClient.ts +251 -26
  57. package/src/lib/TLSyncRoom.ts +284 -24
  58. package/src/lib/chunk.ts +72 -1
  59. package/src/lib/diff.ts +128 -14
  60. package/src/lib/findMin.ts +6 -0
  61. package/src/lib/interval.ts +40 -0
  62. package/src/lib/protocol.ts +185 -7
  63. package/src/lib/server-types.test.ts +44 -0
  64. package/src/lib/server-types.ts +45 -1
  65. package/src/test/TLSocketRoom.test.ts +438 -29
  66. package/src/test/chunk.test.ts +200 -3
  67. package/src/test/diff.test.ts +396 -1
@@ -8,14 +8,95 @@ import { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'
8
8
  import { JsonChunkAssembler } from './chunk'
9
9
  import { TLSocketServerSentEvent } from './protocol'
10
10
 
11
- // TODO: structured logging support
12
- /** @public */
11
+ /**
12
+ * Logging interface for TLSocketRoom operations. Provides optional methods
13
+ * for warning and error logging during synchronization operations.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * const logger: TLSyncLog = {
18
+ * warn: (...args) => console.warn('[SYNC]', ...args),
19
+ * error: (...args) => console.error('[SYNC]', ...args)
20
+ * }
21
+ *
22
+ * const room = new TLSocketRoom({ log: logger })
23
+ * ```
24
+ *
25
+ * @public
26
+ */
13
27
  export interface TLSyncLog {
28
+ /**
29
+ * Optional warning logger for non-fatal sync issues
30
+ * @param args - Arguments to log
31
+ */
14
32
  warn?(...args: any[]): void
33
+ /**
34
+ * Optional error logger for sync errors and failures
35
+ * @param args - Arguments to log
36
+ */
15
37
  error?(...args: any[]): void
16
38
  }
17
39
 
18
- /** @public */
40
+ /**
41
+ * A server-side room that manages WebSocket connections and synchronizes tldraw document state
42
+ * between multiple clients in real-time. Each room represents a collaborative document space
43
+ * where users can work together on drawings with automatic conflict resolution.
44
+ *
45
+ * TLSocketRoom handles:
46
+ * - WebSocket connection lifecycle management
47
+ * - Real-time synchronization of document changes
48
+ * - Session management and presence tracking
49
+ * - Message chunking for large payloads
50
+ * - Automatic client timeout and cleanup
51
+ *
52
+ * @example
53
+ * ```ts
54
+ * // Basic room setup
55
+ * const room = new TLSocketRoom({
56
+ * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {
57
+ * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)
58
+ * if (numSessionsRemaining === 0) {
59
+ * room.close()
60
+ * }
61
+ * },
62
+ * onDataChange: () => {
63
+ * console.log('Document data changed, consider persisting')
64
+ * }
65
+ * })
66
+ *
67
+ * // Handle new client connections
68
+ * room.handleSocketConnect({
69
+ * sessionId: 'user-session-123',
70
+ * socket: webSocket,
71
+ * isReadonly: false
72
+ * })
73
+ * ```
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // Room with initial snapshot and schema
78
+ * const room = new TLSocketRoom({
79
+ * initialSnapshot: existingSnapshot,
80
+ * schema: myCustomSchema,
81
+ * clientTimeout: 30000,
82
+ * log: {
83
+ * warn: (...args) => logger.warn('SYNC:', ...args),
84
+ * error: (...args) => logger.error('SYNC:', ...args)
85
+ * }
86
+ * })
87
+ *
88
+ * // Update document programmatically
89
+ * await room.updateStore(store => {
90
+ * const shape = store.get('shape:abc123')
91
+ * if (shape) {
92
+ * shape.x = 100
93
+ * store.put(shape)
94
+ * }
95
+ * })
96
+ * ```
97
+ *
98
+ * @public
99
+ */
19
100
  export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {
20
101
  private room: TLSyncRoom<R, SessionMeta>
21
102
  private readonly sessions = new Map<
@@ -29,6 +110,20 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
29
110
  onPresenceChange?(): void
30
111
  }
31
112
 
113
+ /**
114
+ * Creates a new TLSocketRoom instance for managing collaborative document synchronization.
115
+ *
116
+ * opts - Configuration options for the room
117
+ * - initialSnapshot - Optional initial document state to load
118
+ * - schema - Store schema defining record types and validation
119
+ * - clientTimeout - Milliseconds to wait before disconnecting inactive clients
120
+ * - log - Optional logger for warnings and errors
121
+ * - onSessionRemoved - Called when a client session is removed
122
+ * - onBeforeSendMessage - Called before sending messages to clients
123
+ * - onAfterReceiveMessage - Called after receiving messages from clients
124
+ * - onDataChange - Called when document data changes
125
+ * - onPresenceChange - Called when presence data changes
126
+ */
32
127
  constructor(
33
128
  public readonly opts: {
34
129
  initialSnapshot?: RoomSnapshot | TLStoreSnapshot
@@ -104,14 +199,35 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
104
199
  }
105
200
 
106
201
  /**
107
- * Call this when a client establishes a new socket connection.
202
+ * Handles a new client WebSocket connection, creating a session within the room.
203
+ * This should be called whenever a client establishes a WebSocket connection to join
204
+ * the collaborative document.
205
+ *
206
+ * @param opts - Connection options
207
+ * - sessionId - Unique identifier for the client session (typically from browser tab)
208
+ * - socket - WebSocket-like object for client communication
209
+ * - isReadonly - Whether the client can modify the document (defaults to false)
210
+ * - meta - Additional session metadata (required if SessionMeta is not void)
108
211
  *
109
- * - `sessionId` is a unique ID for a browser tab. This is passed as a query param by the useSync hook.
110
- * - `socket` is a WebSocket-like object that the server uses to communicate with the client.
111
- * - `isReadonly` is an optional boolean that can be set to true if the client should not be able to make changes to the document. They will still be able to send presence updates.
112
- * - `meta` is an optional object that can be used to store additional information about the session.
212
+ * @example
213
+ * ```ts
214
+ * // Handle new WebSocket connection
215
+ * room.handleSocketConnect({
216
+ * sessionId: 'user-session-abc123',
217
+ * socket: webSocketConnection,
218
+ * isReadonly: !userHasEditPermission
219
+ * })
220
+ * ```
113
221
  *
114
- * @param opts - The options object
222
+ * @example
223
+ * ```ts
224
+ * // With session metadata
225
+ * room.handleSocketConnect({
226
+ * sessionId: 'session-xyz',
227
+ * socket: ws,
228
+ * meta: { userId: 'user-123', name: 'Alice' }
229
+ * })
230
+ * ```
115
231
  */
116
232
  handleSocketConnect(
117
233
  opts: {
@@ -160,12 +276,30 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
160
276
  }
161
277
 
162
278
  /**
163
- * If executing in a server environment where sockets do not have instance-level listeners
164
- * (e.g. Bun.serve, Cloudflare Worker with WebSocket hibernation), you should call this
165
- * method when messages are received. See our self-hosting example for Bun.serve for an example.
279
+ * Processes a message received from a client WebSocket. Use this method in server
280
+ * environments where WebSocket event listeners cannot be attached directly to socket
281
+ * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).
282
+ *
283
+ * The method handles message chunking/reassembly and forwards complete messages
284
+ * to the underlying sync room for processing.
285
+ *
286
+ * @param sessionId - Session identifier matching the one used in handleSocketConnect
287
+ * @param message - Raw message data from the client (string or binary)
166
288
  *
167
- * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)
168
- * @param message - The message received from the client.
289
+ * @example
290
+ * ```ts
291
+ * // In a Bun.serve handler
292
+ * server.upgrade(req, {
293
+ * data: { sessionId, room },
294
+ * upgrade(res, req) {
295
+ * // Connection established
296
+ * },
297
+ * message(ws, message) {
298
+ * const { sessionId, room } = ws.data
299
+ * room.handleSocketMessage(sessionId, message)
300
+ * }
301
+ * })
302
+ * ```
169
303
  */
170
304
  handleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {
171
305
  const assembler = this.sessions.get(sessionId)?.assembler
@@ -211,45 +345,104 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
211
345
  }
212
346
 
213
347
  /**
214
- * If executing in a server environment where sockets do not have instance-level listeners,
215
- * call this when a socket error occurs.
216
- * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)
348
+ * Handles a WebSocket error for the specified session. Use this in server environments
349
+ * where socket event listeners cannot be attached directly. This will initiate cleanup
350
+ * and session removal for the affected client.
351
+ *
352
+ * @param sessionId - Session identifier matching the one used in handleSocketConnect
353
+ *
354
+ * @example
355
+ * ```ts
356
+ * // In a custom WebSocket handler
357
+ * socket.addEventListener('error', () => {
358
+ * room.handleSocketError(sessionId)
359
+ * })
360
+ * ```
217
361
  */
218
362
  handleSocketError(sessionId: string) {
219
363
  this.room.handleClose(sessionId)
220
364
  }
221
365
 
222
366
  /**
223
- * If executing in a server environment where sockets do not have instance-level listeners,
224
- * call this when a socket is closed.
225
- * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)
367
+ * Handles a WebSocket close event for the specified session. Use this in server
368
+ * environments where socket event listeners cannot be attached directly. This will
369
+ * initiate cleanup and session removal for the disconnected client.
370
+ *
371
+ * @param sessionId - Session identifier matching the one used in handleSocketConnect
372
+ *
373
+ * @example
374
+ * ```ts
375
+ * // In a custom WebSocket handler
376
+ * socket.addEventListener('close', () => {
377
+ * room.handleSocketClose(sessionId)
378
+ * })
379
+ * ```
226
380
  */
227
381
  handleSocketClose(sessionId: string) {
228
382
  this.room.handleClose(sessionId)
229
383
  }
230
384
 
231
385
  /**
232
- * Returns the current 'clock' of the document.
233
- * The clock is an integer that increments every time the document changes.
234
- * The clock is stored as part of the snapshot of the document for consistency purposes.
386
+ * Returns the current document clock value. The clock is a monotonically increasing
387
+ * integer that increments with each document change, providing a consistent ordering
388
+ * of changes across the distributed system.
389
+ *
390
+ * @returns The current document clock value
235
391
  *
236
- * @returns The clock
392
+ * @example
393
+ * ```ts
394
+ * const clock = room.getCurrentDocumentClock()
395
+ * console.log(`Document is at version ${clock}`)
396
+ * ```
237
397
  */
238
398
  getCurrentDocumentClock() {
239
399
  return this.room.documentClock
240
400
  }
241
401
 
242
402
  /**
243
- * Returns a deeply cloned record from the store, if available.
244
- * @param id - The id of the record
245
- * @returns the cloned record
403
+ * Retrieves a deeply cloned copy of a record from the document store.
404
+ * Returns undefined if the record doesn't exist. The returned record is
405
+ * safe to mutate without affecting the original store data.
406
+ *
407
+ * @param id - Unique identifier of the record to retrieve
408
+ * @returns Deep clone of the record, or undefined if not found
409
+ *
410
+ * @example
411
+ * ```ts
412
+ * const shape = room.getRecord('shape:abc123')
413
+ * if (shape) {
414
+ * console.log('Shape position:', shape.x, shape.y)
415
+ * // Safe to modify without affecting store
416
+ * shape.x = 100
417
+ * }
418
+ * ```
246
419
  */
247
420
  getRecord(id: string) {
248
421
  return structuredClone(this.room.documents.get(id)?.state)
249
422
  }
250
423
 
251
424
  /**
252
- * Returns a list of the sessions in the room.
425
+ * Returns information about all active sessions in the room. Each session
426
+ * represents a connected client with their current connection status and metadata.
427
+ *
428
+ * @returns Array of session information objects containing:
429
+ * - sessionId - Unique session identifier
430
+ * - isConnected - Whether the session has an active WebSocket connection
431
+ * - isReadonly - Whether the session can modify the document
432
+ * - meta - Custom session metadata
433
+ *
434
+ * @example
435
+ * ```ts
436
+ * const sessions = room.getSessions()
437
+ * console.log(`Room has ${sessions.length} active sessions`)
438
+ *
439
+ * for (const session of sessions) {
440
+ * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)
441
+ * if (session.isReadonly) {
442
+ * console.log(' (read-only access)')
443
+ * }
444
+ * }
445
+ * ```
253
446
  */
254
447
  getSessions(): Array<{
255
448
  sessionId: string
@@ -268,16 +461,32 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
268
461
  }
269
462
 
270
463
  /**
271
- * Return a snapshot of the document state, including clock-related bookkeeping.
272
- * You can store this and load it later on when initializing a TLSocketRoom.
273
- * You can also pass a snapshot to {@link TLSocketRoom#loadSnapshot} if you need to revert to a previous state.
274
- * @returns The snapshot
464
+ * Creates a complete snapshot of the current document state, including all records
465
+ * and synchronization metadata. This snapshot can be persisted to storage and used
466
+ * to restore the room state later or revert to a previous version.
467
+ *
468
+ * @returns Complete room snapshot including documents, clock values, and tombstones
469
+ *
470
+ * @example
471
+ * ```ts
472
+ * // Capture current state for persistence
473
+ * const snapshot = room.getCurrentSnapshot()
474
+ * await saveToDatabase(roomId, JSON.stringify(snapshot))
475
+ *
476
+ * // Later, restore from snapshot
477
+ * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))
478
+ * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })
479
+ * ```
275
480
  */
276
481
  getCurrentSnapshot() {
277
482
  return this.room.getSnapshot()
278
483
  }
279
484
 
280
485
  /**
486
+ * Retrieves all presence records from the document store. Presence records
487
+ * contain ephemeral user state like cursor positions and selections.
488
+ *
489
+ * @returns Object mapping record IDs to presence record data
281
490
  * @internal
282
491
  */
283
492
  getPresenceRecords() {
@@ -291,8 +500,10 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
291
500
  }
292
501
 
293
502
  /**
294
- * Return a serialized snapshot of the document state, including clock-related bookkeeping.
295
- * @returns The serialized snapshot
503
+ * Returns a JSON-serialized snapshot of the current document state. This is
504
+ * equivalent to JSON.stringify(getCurrentSnapshot()) but provided as a convenience.
505
+ *
506
+ * @returns JSON string representation of the room snapshot
296
507
  * @internal
297
508
  */
298
509
  getCurrentSerializedSnapshot() {
@@ -300,8 +511,22 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
300
511
  }
301
512
 
302
513
  /**
303
- * Load a snapshot of the document state, overwriting the current state.
304
- * @param snapshot - The snapshot to load
514
+ * Loads a document snapshot, completely replacing the current room state.
515
+ * This will disconnect all current clients and update the document to match
516
+ * the provided snapshot. Use this for restoring from backups or implementing
517
+ * document versioning.
518
+ *
519
+ * @param snapshot - Room or store snapshot to load
520
+ *
521
+ * @example
522
+ * ```ts
523
+ * // Restore from a saved snapshot
524
+ * const backup = JSON.parse(await loadBackup(roomId))
525
+ * room.loadSnapshot(backup)
526
+ *
527
+ * // All clients will be disconnected and need to reconnect
528
+ * // to see the restored document state
529
+ * ```
305
530
  */
306
531
  loadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {
307
532
  if ('store' in snapshot) {
@@ -343,71 +568,163 @@ export class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta =
343
568
  }
344
569
 
345
570
  /**
346
- * Allow applying changes to the store inside of a transaction.
571
+ * Executes a transaction to modify the document store. Changes made within the
572
+ * transaction are atomic and will be synchronized to all connected clients.
573
+ * The transaction provides isolation from concurrent changes until it commits.
347
574
  *
348
- * You can get values from the store by id with `store.get(id)`.
349
- * These values are safe to mutate, but to commit the changes you must call `store.put(...)` with the updated value.
350
- * You can get all values in the store with `store.getAll()`.
351
- * You can also delete values with `store.delete(id)`.
575
+ * @param updater - Function that receives store methods to make changes
576
+ * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)
577
+ * - store.put(record) - Save a modified record
578
+ * - store.getAll() - Get all records in the store
579
+ * - store.delete(id) - Remove a record from the store
580
+ * @returns Promise that resolves when the transaction completes
352
581
  *
353
582
  * @example
354
583
  * ```ts
355
- * room.updateStore(store => {
356
- * const shape = store.get('shape:abc123')
357
- * shape.meta.approved = true
358
- * store.put(shape)
584
+ * // Update multiple shapes in a single transaction
585
+ * await room.updateStore(store => {
586
+ * const shape1 = store.get('shape:abc123')
587
+ * const shape2 = store.get('shape:def456')
588
+ *
589
+ * if (shape1) {
590
+ * shape1.x = 100
591
+ * store.put(shape1)
592
+ * }
593
+ *
594
+ * if (shape2) {
595
+ * shape2.meta.approved = true
596
+ * store.put(shape2)
597
+ * }
359
598
  * })
360
599
  * ```
361
600
  *
362
- * Changes to the store inside the callback are isolated from changes made by other clients until the transaction commits.
363
- *
364
- * @param updater - A function that will be called with a store object that can be used to make changes.
365
- * @returns A promise that resolves when the transaction is complete.
601
+ * @example
602
+ * ```ts
603
+ * // Async transaction with external API call
604
+ * await room.updateStore(async store => {
605
+ * const doc = store.get('document:main')
606
+ * if (doc) {
607
+ * doc.lastModified = await getCurrentTimestamp()
608
+ * store.put(doc)
609
+ * }
610
+ * })
611
+ * ```
366
612
  */
367
613
  async updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {
368
614
  return this.room.updateStore(updater)
369
615
  }
370
616
 
371
617
  /**
372
- * Send a custom message to a connected client.
618
+ * Sends a custom message to a specific client session. This allows sending
619
+ * application-specific data that doesn't modify the document state, such as
620
+ * notifications, chat messages, or custom commands.
621
+ *
622
+ * @param sessionId - Target session identifier
623
+ * @param data - Custom payload to send (will be JSON serialized)
624
+ *
625
+ * @example
626
+ * ```ts
627
+ * // Send a notification to a specific user
628
+ * room.sendCustomMessage('session-123', {
629
+ * type: 'notification',
630
+ * message: 'Your changes have been saved'
631
+ * })
373
632
  *
374
- * @param sessionId - The id of the session to send the message to.
375
- * @param data - The payload to send.
633
+ * // Send a chat message
634
+ * room.sendCustomMessage('session-456', {
635
+ * type: 'chat',
636
+ * from: 'Alice',
637
+ * text: 'Great work on this design!'
638
+ * })
639
+ * ```
376
640
  */
377
641
  sendCustomMessage(sessionId: string, data: any) {
378
642
  this.room.sendCustomMessage(sessionId, data)
379
643
  }
380
644
 
381
645
  /**
382
- * Immediately remove a session from the room, and close its socket if not already closed.
646
+ * Immediately removes a session from the room and closes its WebSocket connection.
647
+ * The client will attempt to reconnect automatically unless a fatal reason is provided.
648
+ *
649
+ * @param sessionId - Session identifier to remove
650
+ * @param fatalReason - Optional fatal error reason that prevents reconnection
383
651
  *
384
- * The client will attempt to reconnect unless you provide a `fatalReason` parameter.
652
+ * @example
653
+ * ```ts
654
+ * // Kick a user (they can reconnect)
655
+ * room.closeSession('session-troublemaker')
385
656
  *
386
- * The `fatalReason` parameter will be available in the return value of the `useSync` hook as `useSync().error.reason`.
657
+ * // Permanently ban a user
658
+ * room.closeSession('session-banned', 'PERMISSION_DENIED')
387
659
  *
388
- * @param sessionId - The id of the session to remove
389
- * @param fatalReason - The reason message to use when calling .close on the underlying websocket
660
+ * // Close session due to inactivity
661
+ * room.closeSession('session-idle', 'TIMEOUT')
662
+ * ```
390
663
  */
391
664
  closeSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {
392
665
  this.room.rejectSession(sessionId, fatalReason)
393
666
  }
394
667
 
395
668
  /**
396
- * Close the room and disconnect all clients. Call this before discarding the room instance or shutting down the server.
669
+ * Closes the room and disconnects all connected clients. This should be called
670
+ * when shutting down the room permanently, such as during server shutdown or
671
+ * when the room is no longer needed. Once closed, the room cannot be reopened.
672
+ *
673
+ * @example
674
+ * ```ts
675
+ * // Clean shutdown when no users remain
676
+ * if (room.getNumActiveSessions() === 0) {
677
+ * await persistSnapshot(room.getCurrentSnapshot())
678
+ * room.close()
679
+ * }
680
+ *
681
+ * // Server shutdown
682
+ * process.on('SIGTERM', () => {
683
+ * for (const room of activeRooms.values()) {
684
+ * room.close()
685
+ * }
686
+ * })
687
+ * ```
397
688
  */
398
689
  close() {
399
690
  this.room.close()
400
691
  }
401
692
 
402
693
  /**
403
- * @returns true if the room is closed
694
+ * Checks whether the room has been permanently closed. Closed rooms cannot
695
+ * accept new connections or process further changes.
696
+ *
697
+ * @returns True if the room is closed, false if still active
698
+ *
699
+ * @example
700
+ * ```ts
701
+ * if (room.isClosed()) {
702
+ * console.log('Room has been shut down')
703
+ * // Create a new room or redirect users
704
+ * } else {
705
+ * // Room is still accepting connections
706
+ * room.handleSocketConnect({ sessionId, socket })
707
+ * }
708
+ * ```
404
709
  */
405
710
  isClosed() {
406
711
  return this.room.isClosed()
407
712
  }
408
713
  }
409
714
 
410
- /** @public */
715
+ /**
716
+ * Utility type that removes properties with void values from an object type.
717
+ * This is used internally to conditionally require session metadata based on
718
+ * whether SessionMeta extends void.
719
+ *
720
+ * @example
721
+ * ```ts
722
+ * type Example = { a: string, b: void, c: number }
723
+ * type Result = OmitVoid<Example> // { a: string, c: number }
724
+ * ```
725
+ *
726
+ * @public
727
+ */
411
728
  export type OmitVoid<T, KS extends keyof T = keyof T> = {
412
729
  [K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]
413
730
  }