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

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
@@ -41,7 +41,37 @@ function debug(...args: any[]) {
41
41
  // they don't seem to be surfaced in browser APIs and can't be relied on. Therefore,
42
42
  // pings need to be implemented one level up, on the application API side, which for our
43
43
  // codebase means whatever code that uses ClientWebSocketAdapter.
44
- /** @internal */
44
+ /**
45
+ * A WebSocket adapter that provides persistent connection management for tldraw synchronization.
46
+ * This adapter handles connection establishment, reconnection logic, and message routing between
47
+ * the sync client and server. It implements automatic reconnection with exponential backoff
48
+ * and supports connection loss detection.
49
+ *
50
+ * Note: This adapter requires users to implement their own connection loss detection (e.g., pings)
51
+ * as browser WebSocket APIs don't reliably surface protocol-level ping/pong frames.
52
+ *
53
+ * @internal
54
+ * @example
55
+ * ```ts
56
+ * // Create a WebSocket adapter with connection URI
57
+ * const adapter = new ClientWebSocketAdapter(() => 'ws://localhost:3000/sync')
58
+ *
59
+ * // Listen for connection status changes
60
+ * adapter.onStatusChange((status) => {
61
+ * console.log('Connection status:', status)
62
+ * })
63
+ *
64
+ * // Listen for incoming messages
65
+ * adapter.onReceiveMessage((message) => {
66
+ * console.log('Received:', message)
67
+ * })
68
+ *
69
+ * // Send a message when connected
70
+ * if (adapter.connectionStatus === 'online') {
71
+ * adapter.sendMessage({ type: 'ping' })
72
+ * }
73
+ * ```
74
+ */
45
75
  export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord> {
46
76
  _ws: WebSocket | null = null
47
77
 
@@ -50,6 +80,11 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
50
80
  /** @internal */
51
81
  readonly _reconnectManager: ReconnectManager
52
82
 
83
+ /**
84
+ * Permanently closes the WebSocket adapter and disposes of all resources.
85
+ * Once closed, the adapter cannot be reused and should be discarded.
86
+ * This method is idempotent - calling it multiple times has no additional effect.
87
+ */
53
88
  // TODO: .close should be a project-wide interface with a common contract (.close()d thing
54
89
  // can only be garbage collected, and can't be used anymore)
55
90
  close() {
@@ -59,6 +94,14 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
59
94
  this._ws?.close()
60
95
  }
61
96
 
97
+ /**
98
+ * Creates a new ClientWebSocketAdapter instance.
99
+ *
100
+ * @param getUri - Function that returns the WebSocket URI to connect to.
101
+ * Can return a string directly or a Promise that resolves to a string.
102
+ * This function is called each time a connection attempt is made,
103
+ * allowing for dynamic URI generation (e.g., for authentication tokens).
104
+ */
62
105
  constructor(getUri: () => Promise<string> | string) {
63
106
  this._reconnectManager = new ReconnectManager(this, getUri)
64
107
  }
@@ -189,12 +232,31 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
189
232
  'initial'
190
233
  )
191
234
 
235
+ /**
236
+ * Gets the current connection status of the WebSocket.
237
+ *
238
+ * @returns The current connection status: 'online', 'offline', or 'error'
239
+ */
192
240
  // eslint-disable-next-line no-restricted-syntax
193
241
  get connectionStatus(): TLPersistentClientSocketStatus {
194
242
  const status = this._connectionStatus.get()
195
243
  return status === 'initial' ? 'offline' : status
196
244
  }
197
245
 
246
+ /**
247
+ * Sends a message to the server through the WebSocket connection.
248
+ * Messages are automatically chunked if they exceed size limits.
249
+ *
250
+ * @param msg - The message to send to the server
251
+ *
252
+ * @example
253
+ * ```ts
254
+ * adapter.sendMessage({
255
+ * type: 'push',
256
+ * diff: { 'shape:abc123': [2, { x: [1, 150] }] }
257
+ * })
258
+ * ```
259
+ */
198
260
  sendMessage(msg: TLSocketClientSentEvent<TLRecord>) {
199
261
  assert(!this.isDisposed, 'Tried to send message on a disposed socket')
200
262
 
@@ -210,6 +272,29 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
210
272
  }
211
273
 
212
274
  private messageListeners = new Set<(msg: TLSocketServerSentEvent<TLRecord>) => void>()
275
+ /**
276
+ * Registers a callback to handle incoming messages from the server.
277
+ *
278
+ * @param cb - Callback function that will be called with each received message
279
+ * @returns A cleanup function to remove the message listener
280
+ *
281
+ * @example
282
+ * ```ts
283
+ * const unsubscribe = adapter.onReceiveMessage((message) => {
284
+ * switch (message.type) {
285
+ * case 'connect':
286
+ * console.log('Connected to room')
287
+ * break
288
+ * case 'data':
289
+ * console.log('Received data:', message.diff)
290
+ * break
291
+ * }
292
+ * })
293
+ *
294
+ * // Later, remove the listener
295
+ * unsubscribe()
296
+ * ```
297
+ */
213
298
  onReceiveMessage(cb: (val: TLSocketServerSentEvent<TLRecord>) => void) {
214
299
  assert(!this.isDisposed, 'Tried to add message listener on a disposed socket')
215
300
 
@@ -220,6 +305,26 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
220
305
  }
221
306
 
222
307
  private statusListeners = new Set<TLSocketStatusListener>()
308
+ /**
309
+ * Registers a callback to handle connection status changes.
310
+ *
311
+ * @param cb - Callback function that will be called when the connection status changes
312
+ * @returns A cleanup function to remove the status listener
313
+ *
314
+ * @example
315
+ * ```ts
316
+ * const unsubscribe = adapter.onStatusChange((status) => {
317
+ * if (status.status === 'error') {
318
+ * console.error('Connection error:', status.reason)
319
+ * } else {
320
+ * console.log('Status changed to:', status.status)
321
+ * }
322
+ * })
323
+ *
324
+ * // Later, remove the listener
325
+ * unsubscribe()
326
+ * ```
327
+ */
223
328
  onStatusChange(cb: TLSocketStatusListener) {
224
329
  assert(!this.isDisposed, 'Tried to add status listener on a disposed socket')
225
330
 
@@ -229,6 +334,19 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
229
334
  }
230
335
  }
231
336
 
337
+ /**
338
+ * Manually restarts the WebSocket connection.
339
+ * This closes the current connection (if any) and attempts to establish a new one.
340
+ * Useful for implementing connection loss detection and recovery.
341
+ *
342
+ * @example
343
+ * ```ts
344
+ * // Restart connection after detecting it's stale
345
+ * if (lastPongTime < Date.now() - 30000) {
346
+ * adapter.restart()
347
+ * }
348
+ * ```
349
+ */
232
350
  restart() {
233
351
  assert(!this.isDisposed, 'Tried to restart a disposed socket')
234
352
  debug('restarting')
@@ -238,21 +356,78 @@ export class ClientWebSocketAdapter implements TLPersistentClientSocket<TLRecord
238
356
  }
239
357
  }
240
358
 
241
- // Those constants are exported primarily for tests
242
- // ACTIVE_ means the tab is active, document.hidden is false
359
+ /**
360
+ * Minimum reconnection delay in milliseconds when the browser tab is active and focused.
361
+ *
362
+ * @internal
363
+ */
243
364
  export const ACTIVE_MIN_DELAY = 500
365
+
366
+ /**
367
+ * Maximum reconnection delay in milliseconds when the browser tab is active and focused.
368
+ *
369
+ * @internal
370
+ */
244
371
  export const ACTIVE_MAX_DELAY = 2000
245
- // Correspondingly, here document.hidden is true. It's intended to reduce the load and battery drain
246
- // on client devices somewhat when they aren't looking at the tab. We don't disconnect completely
247
- // to minimise issues with reconnection/sync when the tab becomes visible again
372
+
373
+ /**
374
+ * Minimum reconnection delay in milliseconds when the browser tab is inactive or hidden.
375
+ * This longer delay helps reduce battery drain and server load when users aren't actively viewing the tab.
376
+ *
377
+ * @internal
378
+ */
248
379
  export const INACTIVE_MIN_DELAY = 1000
380
+
381
+ /**
382
+ * Maximum reconnection delay in milliseconds when the browser tab is inactive or hidden.
383
+ * Set to 5 minutes to balance between maintaining sync and conserving resources.
384
+ *
385
+ * @internal
386
+ */
249
387
  export const INACTIVE_MAX_DELAY = 1000 * 60 * 5
388
+
389
+ /**
390
+ * Exponential backoff multiplier for calculating reconnection delays.
391
+ * Each failed connection attempt increases the delay by this factor until max delay is reached.
392
+ *
393
+ * @internal
394
+ */
250
395
  export const DELAY_EXPONENT = 1.5
251
- // this is a tradeoff between quickly detecting connections stuck in the CONNECTING state and
252
- // not needlessly reconnecting if the connection is just slow to establish
396
+
397
+ /**
398
+ * Maximum time in milliseconds to wait for a connection attempt before considering it failed.
399
+ * This helps detect connections stuck in the CONNECTING state and retry with fresh attempts.
400
+ *
401
+ * @internal
402
+ */
253
403
  export const ATTEMPT_TIMEOUT = 1000
254
404
 
255
- /** @internal */
405
+ /**
406
+ * Manages automatic reconnection logic for WebSocket connections with intelligent backoff strategies.
407
+ * This class handles connection attempts, tracks connection state, and implements exponential backoff
408
+ * with different delays based on whether the browser tab is active or inactive.
409
+ *
410
+ * The ReconnectManager responds to various browser events like network status changes,
411
+ * tab visibility changes, and connection events to optimize reconnection timing and
412
+ * minimize unnecessary connection attempts.
413
+ *
414
+ * @internal
415
+ *
416
+ * @example
417
+ * ```ts
418
+ * const manager = new ReconnectManager(
419
+ * socketAdapter,
420
+ * () => 'ws://localhost:3000/sync'
421
+ * )
422
+ *
423
+ * // Manager automatically handles:
424
+ * // - Initial connection
425
+ * // - Reconnection on disconnect
426
+ * // - Exponential backoff on failures
427
+ * // - Tab visibility-aware delays
428
+ * // - Network status change responses
429
+ * ```
430
+ */
256
431
  export class ReconnectManager {
257
432
  private isDisposed = false
258
433
  private disposables: (() => void)[] = [
@@ -268,6 +443,12 @@ export class ReconnectManager {
268
443
  intendedDelay: number = ACTIVE_MIN_DELAY
269
444
  private state: 'pendingAttempt' | 'pendingAttemptResult' | 'delay' | 'connected'
270
445
 
446
+ /**
447
+ * Creates a new ReconnectManager instance.
448
+ *
449
+ * socketAdapter - The ClientWebSocketAdapter instance to manage
450
+ * getUri - Function that returns the WebSocket URI for connection attempts
451
+ */
271
452
  constructor(
272
453
  private socketAdapter: ClientWebSocketAdapter,
273
454
  private getUri: () => Promise<string> | string
@@ -356,6 +537,22 @@ export class ReconnectManager {
356
537
  }
357
538
  }
358
539
 
540
+ /**
541
+ * Checks if reconnection should be attempted and initiates it if appropriate.
542
+ * This method is called in response to network events, tab visibility changes,
543
+ * and other hints that connectivity may have been restored.
544
+ *
545
+ * The method intelligently handles various connection states:
546
+ * - Already connected: no action needed
547
+ * - Currently connecting: waits or retries based on attempt age
548
+ * - Disconnected: initiates immediate reconnection attempt
549
+ *
550
+ * @example
551
+ * ```ts
552
+ * // Called automatically on network/visibility events, but can be called manually
553
+ * manager.maybeReconnected()
554
+ * ```
555
+ */
359
556
  maybeReconnected() {
360
557
  debug('ReconnectManager.maybeReconnected')
361
558
  // It doesn't make sense to have another check scheduled if we're already checking it now.
@@ -409,6 +606,22 @@ export class ReconnectManager {
409
606
  this.disconnected()
410
607
  }
411
608
 
609
+ /**
610
+ * Handles disconnection events and schedules reconnection attempts with exponential backoff.
611
+ * This method is called when the WebSocket connection is lost or fails to establish.
612
+ *
613
+ * It implements intelligent delay calculation based on:
614
+ * - Previous attempt timing
615
+ * - Current tab visibility (active vs inactive delays)
616
+ * - Exponential backoff for repeated failures
617
+ *
618
+ * @example
619
+ * ```ts
620
+ * // Called automatically when connection is lost
621
+ * // Schedules reconnection with appropriate delay
622
+ * manager.disconnected()
623
+ * ```
624
+ */
412
625
  disconnected() {
413
626
  debug('ReconnectManager.disconnected')
414
627
  // This either means we're freshly disconnected, or the last connection attempt failed;
@@ -458,6 +671,19 @@ export class ReconnectManager {
458
671
  }
459
672
  }
460
673
 
674
+ /**
675
+ * Handles successful connection events and resets reconnection state.
676
+ * This method is called when the WebSocket successfully connects to the server.
677
+ *
678
+ * It clears any pending reconnection attempts and resets the delay back to minimum
679
+ * for future connection attempts.
680
+ *
681
+ * @example
682
+ * ```ts
683
+ * // Called automatically when WebSocket opens successfully
684
+ * manager.connected()
685
+ * ```
686
+ */
461
687
  connected() {
462
688
  debug('ReconnectManager.connected')
463
689
  // this notification could've been delayed, recheck synchronously
@@ -469,6 +695,11 @@ export class ReconnectManager {
469
695
  }
470
696
  }
471
697
 
698
+ /**
699
+ * Permanently closes the reconnection manager and cleans up all resources.
700
+ * This stops all pending reconnection attempts and removes event listeners.
701
+ * Once closed, the manager cannot be reused.
702
+ */
472
703
  close() {
473
704
  this.disposables.forEach((d) => d())
474
705
  this.isDisposed = true
@@ -0,0 +1,97 @@
1
+ import { SerializedSchema } from '@tldraw/store'
2
+ import { TLRecord } from '@tldraw/tlschema'
3
+ import { describe, expect, it, vi } from 'vitest'
4
+ import {
5
+ RoomSession,
6
+ RoomSessionState,
7
+ SESSION_IDLE_TIMEOUT,
8
+ SESSION_REMOVAL_WAIT_TIME,
9
+ SESSION_START_WAIT_TIME,
10
+ } from './RoomSession'
11
+ import { TLRoomSocket } from './TLSyncRoom'
12
+
13
+ // Mock socket implementation for testing
14
+ const createMockSocket = (): TLRoomSocket<TLRecord> => ({
15
+ isOpen: true,
16
+ sendMessage: vi.fn(),
17
+ close: vi.fn(),
18
+ })
19
+
20
+ // Mock serialized schema for testing
21
+ const mockSerializedSchema: SerializedSchema = {
22
+ schemaVersion: 1,
23
+ storeVersion: 1,
24
+ recordVersions: {},
25
+ }
26
+
27
+ describe('RoomSession timeout constants', () => {
28
+ it('should have logical timeout ordering for session management', () => {
29
+ // This test ensures the timeout constants have a logical relationship
30
+ // that supports proper session lifecycle management:
31
+ // - Quick cleanup for disconnected sessions
32
+ // - Reasonable wait time for initial connections
33
+ // - Patient timeout for active sessions
34
+ expect(SESSION_REMOVAL_WAIT_TIME).toBeLessThan(SESSION_START_WAIT_TIME)
35
+ expect(SESSION_START_WAIT_TIME).toBeLessThan(SESSION_IDLE_TIMEOUT)
36
+ })
37
+ })
38
+
39
+ describe('RoomSession state transitions', () => {
40
+ const baseSessionData = {
41
+ sessionId: 'test-session-id',
42
+ presenceId: 'test-presence-id',
43
+ socket: createMockSocket(),
44
+ meta: { userId: 'test-user' },
45
+ isReadonly: false,
46
+ requiresLegacyRejection: false,
47
+ }
48
+
49
+ it('should support complete session lifecycle', () => {
50
+ // Test that sessions can progress through their full lifecycle
51
+ // This validates the discriminated union works correctly for state management
52
+
53
+ // Start in awaiting state
54
+ const initialSession: RoomSession<TLRecord, { userId: string }> = {
55
+ state: RoomSessionState.AwaitingConnectMessage,
56
+ sessionStartTime: Date.now(),
57
+ ...baseSessionData,
58
+ }
59
+
60
+ // Progress to connected state (simulates successful connection)
61
+ const connectedSession: RoomSession<TLRecord, { userId: string }> = {
62
+ state: RoomSessionState.Connected,
63
+ sessionId: initialSession.sessionId,
64
+ presenceId: initialSession.presenceId,
65
+ socket: initialSession.socket,
66
+ meta: initialSession.meta,
67
+ isReadonly: initialSession.isReadonly,
68
+ requiresLegacyRejection: initialSession.requiresLegacyRejection,
69
+ serializedSchema: mockSerializedSchema,
70
+ lastInteractionTime: Date.now(),
71
+ debounceTimer: null,
72
+ outstandingDataMessages: [],
73
+ }
74
+
75
+ // End in awaiting removal state (simulates disconnection)
76
+ const removalSession: RoomSession<TLRecord, { userId: string }> = {
77
+ state: RoomSessionState.AwaitingRemoval,
78
+ sessionId: connectedSession.sessionId,
79
+ presenceId: connectedSession.presenceId,
80
+ socket: connectedSession.socket,
81
+ meta: connectedSession.meta,
82
+ isReadonly: connectedSession.isReadonly,
83
+ requiresLegacyRejection: connectedSession.requiresLegacyRejection,
84
+ cancellationTime: Date.now(),
85
+ }
86
+
87
+ // Verify session ID remains consistent across state changes
88
+ // This is critical for tracking sessions through their lifecycle
89
+ expect(initialSession.sessionId).toBe(connectedSession.sessionId)
90
+ expect(connectedSession.sessionId).toBe(removalSession.sessionId)
91
+
92
+ // Verify state-specific properties are present when expected
93
+ expect(initialSession.state).toBe(RoomSessionState.AwaitingConnectMessage)
94
+ expect(connectedSession.state).toBe(RoomSessionState.Connected)
95
+ expect(removalSession.state).toBe(RoomSessionState.AwaitingRemoval)
96
+ })
97
+ })
@@ -2,52 +2,154 @@ import { SerializedSchema, UnknownRecord } from '@tldraw/store'
2
2
  import { TLRoomSocket } from './TLSyncRoom'
3
3
  import { TLSocketServerSentDataEvent } from './protocol'
4
4
 
5
- /** @internal */
5
+ /**
6
+ * Enumeration of possible states for a room session during its lifecycle.
7
+ *
8
+ * Room sessions progress through these states as clients connect, authenticate,
9
+ * and disconnect from collaborative rooms.
10
+ *
11
+ * @internal
12
+ */
6
13
  export const RoomSessionState = {
14
+ /** Session is waiting for the initial connect message from the client */
7
15
  AwaitingConnectMessage: 'awaiting-connect-message',
16
+ /** Session is disconnected but waiting for final cleanup before removal */
8
17
  AwaitingRemoval: 'awaiting-removal',
18
+ /** Session is fully connected and actively synchronizing */
9
19
  Connected: 'connected',
10
20
  } as const
11
21
 
12
- /** @internal */
22
+ /**
23
+ * Type representing the possible states a room session can be in.
24
+ *
25
+ * @example
26
+ * ```ts
27
+ * const sessionState: RoomSessionState = RoomSessionState.Connected
28
+ * if (sessionState === RoomSessionState.AwaitingConnectMessage) {
29
+ * console.log('Session waiting for connect message')
30
+ * }
31
+ * ```
32
+ *
33
+ * @internal
34
+ */
13
35
  export type RoomSessionState = (typeof RoomSessionState)[keyof typeof RoomSessionState]
14
36
 
37
+ /**
38
+ * Maximum time in milliseconds to wait for a connect message after socket connection.
39
+ *
40
+ * If a client connects but doesn't send a connect message within this time,
41
+ * the session will be terminated.
42
+ *
43
+ * @public
44
+ */
15
45
  export const SESSION_START_WAIT_TIME = 10000
46
+
47
+ /**
48
+ * Time in milliseconds to wait before completely removing a disconnected session.
49
+ *
50
+ * This grace period allows for quick reconnections without losing session state,
51
+ * which is especially helpful for brief network interruptions.
52
+ *
53
+ * @public
54
+ */
16
55
  export const SESSION_REMOVAL_WAIT_TIME = 5000
56
+
57
+ /**
58
+ * Maximum time in milliseconds a connected session can remain idle before cleanup.
59
+ *
60
+ * Sessions that don't receive any messages or interactions for this duration
61
+ * may be considered for cleanup to free server resources.
62
+ *
63
+ * @public
64
+ */
17
65
  export const SESSION_IDLE_TIMEOUT = 20000
18
66
 
19
- /** @internal */
67
+ /**
68
+ * Represents a client session within a collaborative room, tracking the connection
69
+ * state, permissions, and synchronization details for a single user.
70
+ *
71
+ * Each session corresponds to one WebSocket connection and progresses through
72
+ * different states during its lifecycle. The session type is a discriminated union
73
+ * based on the current state, ensuring type safety when accessing state-specific properties.
74
+ *
75
+ * @example
76
+ * ```ts
77
+ * // Check session state and access appropriate properties
78
+ * function handleSession(session: RoomSession<MyRecord, UserMeta>) {
79
+ * switch (session.state) {
80
+ * case RoomSessionState.AwaitingConnectMessage:
81
+ * console.log(`Session ${session.sessionId} started at ${session.sessionStartTime}`)
82
+ * break
83
+ * case RoomSessionState.Connected:
84
+ * console.log(`Connected session has ${session.outstandingDataMessages.length} pending messages`)
85
+ * break
86
+ * case RoomSessionState.AwaitingRemoval:
87
+ * console.log(`Session will be removed at ${session.cancellationTime}`)
88
+ * break
89
+ * }
90
+ * }
91
+ * ```
92
+ *
93
+ * @internal
94
+ */
20
95
  export type RoomSession<R extends UnknownRecord, Meta> =
21
96
  | {
97
+ /** Current state of the session */
22
98
  state: typeof RoomSessionState.AwaitingConnectMessage
99
+ /** Unique identifier for this session */
23
100
  sessionId: string
101
+ /** Presence identifier for live cursor/selection tracking, if available */
24
102
  presenceId: string | null
103
+ /** WebSocket connection wrapper for this session */
25
104
  socket: TLRoomSocket<R>
105
+ /** Timestamp when the session was created */
26
106
  sessionStartTime: number
107
+ /** Custom metadata associated with this session */
27
108
  meta: Meta
109
+ /** Whether this session has read-only permissions */
28
110
  isReadonly: boolean
111
+ /** Whether this session requires legacy protocol rejection handling */
29
112
  requiresLegacyRejection: boolean
30
113
  }
31
114
  | {
115
+ /** Current state of the session */
32
116
  state: typeof RoomSessionState.AwaitingRemoval
117
+ /** Unique identifier for this session */
33
118
  sessionId: string
119
+ /** Presence identifier for live cursor/selection tracking, if available */
34
120
  presenceId: string | null
121
+ /** WebSocket connection wrapper for this session */
35
122
  socket: TLRoomSocket<R>
123
+ /** Timestamp when the session was marked for removal */
36
124
  cancellationTime: number
125
+ /** Custom metadata associated with this session */
37
126
  meta: Meta
127
+ /** Whether this session has read-only permissions */
38
128
  isReadonly: boolean
129
+ /** Whether this session requires legacy protocol rejection handling */
39
130
  requiresLegacyRejection: boolean
40
131
  }
41
132
  | {
133
+ /** Current state of the session */
42
134
  state: typeof RoomSessionState.Connected
135
+ /** Unique identifier for this session */
43
136
  sessionId: string
137
+ /** Presence identifier for live cursor/selection tracking, if available */
44
138
  presenceId: string | null
139
+ /** WebSocket connection wrapper for this session */
45
140
  socket: TLRoomSocket<R>
141
+ /** Serialized schema information for this connected session */
46
142
  serializedSchema: SerializedSchema
143
+ /** Timestamp of the last interaction or message from this session */
47
144
  lastInteractionTime: number
145
+ /** Timer for debouncing operations, if active */
48
146
  debounceTimer: ReturnType<typeof setTimeout> | null
147
+ /** Queue of data messages waiting to be sent to this session */
49
148
  outstandingDataMessages: TLSocketServerSentDataEvent<R>[]
149
+ /** Custom metadata associated with this session */
50
150
  meta: Meta
151
+ /** Whether this session has read-only permissions */
51
152
  isReadonly: boolean
153
+ /** Whether this session requires legacy protocol rejection handling */
52
154
  requiresLegacyRejection: boolean
53
155
  }