@tldraw/sync-core 4.0.2 → 4.1.0-canary.0259516ffb8c

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
@@ -34,7 +34,19 @@ import { UnknownRecord } from '@tldraw/store';
34
34
 
35
35
  /* Excluded from this release type: ObjectDiff */
36
36
 
37
- /** @public */
37
+ /**
38
+ * Utility type that removes properties with void values from an object type.
39
+ * This is used internally to conditionally require session metadata based on
40
+ * whether SessionMeta extends void.
41
+ *
42
+ * @example
43
+ * ```ts
44
+ * type Example = { a: string, b: void, c: number }
45
+ * type Result = OmitVoid<Example> // { a: string, c: number }
46
+ * ```
47
+ *
48
+ * @public
49
+ */
38
50
  export declare type OmitVoid<T, KS extends keyof T = keyof T> = {
39
51
  [K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K];
40
52
  };
@@ -55,26 +67,84 @@ export declare type OmitVoid<T, KS extends keyof T = keyof T> = {
55
67
 
56
68
  /* Excluded from this release type: RoomSessionState */
57
69
 
58
- /** @public */
70
+ /**
71
+ * Snapshot of a room's complete state that can be persisted and restored.
72
+ * Contains all documents, tombstones, and metadata needed to reconstruct the room.
73
+ *
74
+ * @public
75
+ */
59
76
  export declare interface RoomSnapshot {
77
+ /**
78
+ * The current logical clock value for the room
79
+ */
60
80
  clock: number;
81
+ /**
82
+ * Clock value when document data was last changed (optional for backwards compatibility)
83
+ */
61
84
  documentClock?: number;
85
+ /**
86
+ * Array of all document records with their last modification clocks
87
+ */
62
88
  documents: Array<{
63
89
  lastChangedClock: number;
64
90
  state: UnknownRecord;
65
91
  }>;
92
+ /**
93
+ * Map of deleted record IDs to their deletion clock values (optional)
94
+ */
66
95
  tombstones?: Record<string, number>;
96
+ /**
97
+ * Clock value where tombstone history begins - older deletions are not tracked (optional)
98
+ */
67
99
  tombstoneHistoryStartsAtClock?: number;
100
+ /**
101
+ * Serialized schema used when creating this snapshot (optional)
102
+ */
68
103
  schema?: SerializedSchema;
69
104
  }
70
105
 
71
106
  /**
107
+ * Interface for making transactional changes to room store data. Used within
108
+ * updateStore transactions to modify documents atomically.
109
+ *
110
+ * @example
111
+ * ```ts
112
+ * await room.updateStore((store) => {
113
+ * const shape = store.get('shape:123')
114
+ * if (shape) {
115
+ * store.put({ ...shape, x: shape.x + 10 })
116
+ * }
117
+ * store.delete('shape:456')
118
+ * })
119
+ * ```
120
+ *
72
121
  * @public
73
122
  */
74
123
  export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecord> {
124
+ /**
125
+ * Add or update a record in the store.
126
+ *
127
+ * @param record - The record to store
128
+ */
75
129
  put(record: R): void;
130
+ /**
131
+ * Delete a record from the store.
132
+ *
133
+ * @param recordOrId - The record or record ID to delete
134
+ */
76
135
  delete(recordOrId: R | string): void;
136
+ /**
137
+ * Get a record by its ID.
138
+ *
139
+ * @param id - The record ID
140
+ * @returns The record or null if not found
141
+ */
77
142
  get(id: string): null | R;
143
+ /**
144
+ * Get all records in the store.
145
+ *
146
+ * @returns Array of all records
147
+ */
78
148
  getAll(): R[];
79
149
  }
80
150
 
@@ -83,7 +153,27 @@ export declare interface RoomStoreMethods<R extends UnknownRecord = UnknownRecor
83
153
  /* Excluded from this release type: TLConnectRequest */
84
154
 
85
155
  /**
86
- * Event handler for userland socket messages
156
+ * Handler function for custom application messages sent through the sync protocol.
157
+ * These are user-defined messages that can be sent between clients via the sync server,
158
+ * separate from the standard document synchronization messages.
159
+ *
160
+ * @param data - Custom message payload (application-defined structure)
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * const customMessageHandler: TLCustomMessageHandler = (data) => {
165
+ * if (data.type === 'user_joined') {
166
+ * console.log(`${data.username} joined the session`)
167
+ * showToast(`${data.username} is now collaborating`)
168
+ * }
169
+ * }
170
+ *
171
+ * const syncClient = new TLSyncClient({
172
+ * // ... other config
173
+ * onCustomMessageReceived: customMessageHandler
174
+ * })
175
+ * ```
176
+ *
87
177
  * @public
88
178
  */
89
179
  export declare type TLCustomMessageHandler = (this: null, data: any) => void;
@@ -100,10 +190,58 @@ export declare type TLCustomMessageHandler = (this: null, data: any) => void;
100
190
 
101
191
  /* Excluded from this release type: TLPushRequest */
102
192
 
103
- /** @public */
193
+ /**
194
+ * Specialized error class for synchronization-related failures in tldraw collaboration.
195
+ *
196
+ * This error is thrown when the sync client encounters fatal errors that prevent
197
+ * successful synchronization with the server. It captures both the error message
198
+ * and the specific reason code that triggered the failure.
199
+ *
200
+ * Common scenarios include schema version mismatches, authentication failures,
201
+ * network connectivity issues, and server-side validation errors.
202
+ *
203
+ * @example
204
+ * ```ts
205
+ * import { TLRemoteSyncError, TLSyncErrorCloseEventReason } from '@tldraw/sync-core'
206
+ *
207
+ * // Handle sync errors in your application
208
+ * syncClient.onSyncError((error) => {
209
+ * if (error instanceof TLRemoteSyncError) {
210
+ * switch (error.reason) {
211
+ * case TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:
212
+ * // Redirect user to login
213
+ * break
214
+ * case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:
215
+ * // Show update required message
216
+ * break
217
+ * default:
218
+ * console.error('Sync error:', error.message)
219
+ * }
220
+ * }
221
+ * })
222
+ * ```
223
+ *
224
+ * @example
225
+ * ```ts
226
+ * // Server-side: throwing a sync error
227
+ * if (!hasPermission(userId, roomId)) {
228
+ * throw new TLRemoteSyncError(TLSyncErrorCloseEventReason.FORBIDDEN)
229
+ * }
230
+ * ```
231
+ *
232
+ * @public
233
+ */
104
234
  export declare class TLRemoteSyncError extends Error {
105
235
  readonly reason: string | TLSyncErrorCloseEventReason;
106
236
  name: string;
237
+ /**
238
+ * Creates a new TLRemoteSyncError with the specified reason.
239
+ *
240
+ * reason - The specific reason code or custom string describing why the sync failed.
241
+ * When using predefined reasons from TLSyncErrorCloseEventReason, the client
242
+ * can handle specific error types appropriately. Custom strings allow for
243
+ * application-specific error details.
244
+ */
107
245
  constructor(reason: string | TLSyncErrorCloseEventReason);
108
246
  }
109
247
 
@@ -111,7 +249,66 @@ export declare class TLRemoteSyncError extends Error {
111
249
 
112
250
  /* Excluded from this release type: TLSocketClientSentEvent */
113
251
 
114
- /** @public */
252
+ /**
253
+ * A server-side room that manages WebSocket connections and synchronizes tldraw document state
254
+ * between multiple clients in real-time. Each room represents a collaborative document space
255
+ * where users can work together on drawings with automatic conflict resolution.
256
+ *
257
+ * TLSocketRoom handles:
258
+ * - WebSocket connection lifecycle management
259
+ * - Real-time synchronization of document changes
260
+ * - Session management and presence tracking
261
+ * - Message chunking for large payloads
262
+ * - Automatic client timeout and cleanup
263
+ *
264
+ * @example
265
+ * ```ts
266
+ * // Basic room setup
267
+ * const room = new TLSocketRoom({
268
+ * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {
269
+ * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)
270
+ * if (numSessionsRemaining === 0) {
271
+ * room.close()
272
+ * }
273
+ * },
274
+ * onDataChange: () => {
275
+ * console.log('Document data changed, consider persisting')
276
+ * }
277
+ * })
278
+ *
279
+ * // Handle new client connections
280
+ * room.handleSocketConnect({
281
+ * sessionId: 'user-session-123',
282
+ * socket: webSocket,
283
+ * isReadonly: false
284
+ * })
285
+ * ```
286
+ *
287
+ * @example
288
+ * ```ts
289
+ * // Room with initial snapshot and schema
290
+ * const room = new TLSocketRoom({
291
+ * initialSnapshot: existingSnapshot,
292
+ * schema: myCustomSchema,
293
+ * clientTimeout: 30000,
294
+ * log: {
295
+ * warn: (...args) => logger.warn('SYNC:', ...args),
296
+ * error: (...args) => logger.error('SYNC:', ...args)
297
+ * }
298
+ * })
299
+ *
300
+ * // Update document programmatically
301
+ * await room.updateStore(store => {
302
+ * const shape = store.get('shape:abc123')
303
+ * if (shape) {
304
+ * shape.x = 100
305
+ * store.put(shape)
306
+ * }
307
+ * })
308
+ * ```
309
+ *
310
+ * @public
311
+ */
115
312
  export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {
116
313
  readonly opts: {
117
314
  /* Excluded from this release type: onPresenceChange */
@@ -142,6 +339,20 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
142
339
  private readonly sessions;
143
340
  readonly log?: TLSyncLog;
144
341
  private readonly syncCallbacks;
342
+ /**
343
+ * Creates a new TLSocketRoom instance for managing collaborative document synchronization.
344
+ *
345
+ * opts - Configuration options for the room
346
+ * - initialSnapshot - Optional initial document state to load
347
+ * - schema - Store schema defining record types and validation
348
+ * - clientTimeout - Milliseconds to wait before disconnecting inactive clients
349
+ * - log - Optional logger for warnings and errors
350
+ * - onSessionRemoved - Called when a client session is removed
351
+ * - onBeforeSendMessage - Called before sending messages to clients
352
+ * - onAfterReceiveMessage - Called after receiving messages from clients
353
+ * - onDataChange - Called when document data changes
354
+ * - onPresenceChange - Called when presence data changes
355
+ */
145
356
  constructor(opts: {
146
357
  /* Excluded from this release type: onPresenceChange */
147
358
  clientTimeout?: number;
@@ -176,14 +387,35 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
176
387
  */
177
388
  getNumActiveSessions(): number;
178
389
  /**
179
- * Call this when a client establishes a new socket connection.
390
+ * Handles a new client WebSocket connection, creating a session within the room.
391
+ * This should be called whenever a client establishes a WebSocket connection to join
392
+ * the collaborative document.
180
393
  *
181
- * - `sessionId` is a unique ID for a browser tab. This is passed as a query param by the useSync hook.
182
- * - `socket` is a WebSocket-like object that the server uses to communicate with the client.
183
- * - `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.
184
- * - `meta` is an optional object that can be used to store additional information about the session.
394
+ * @param opts - Connection options
395
+ * - sessionId - Unique identifier for the client session (typically from browser tab)
396
+ * - socket - WebSocket-like object for client communication
397
+ * - isReadonly - Whether the client can modify the document (defaults to false)
398
+ * - meta - Additional session metadata (required if SessionMeta is not void)
399
+ *
400
+ * @example
401
+ * ```ts
402
+ * // Handle new WebSocket connection
403
+ * room.handleSocketConnect({
404
+ * sessionId: 'user-session-abc123',
405
+ * socket: webSocketConnection,
406
+ * isReadonly: !userHasEditPermission
407
+ * })
408
+ * ```
185
409
  *
186
- * @param opts - The options object
410
+ * @example
411
+ * ```ts
412
+ * // With session metadata
413
+ * room.handleSocketConnect({
414
+ * sessionId: 'session-xyz',
415
+ * socket: ws,
416
+ * meta: { userId: 'user-123', name: 'Alice' }
417
+ * })
418
+ * ```
187
419
  */
188
420
  handleSocketConnect(opts: {
189
421
  isReadonly?: boolean;
@@ -193,42 +425,119 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
193
425
  meta: SessionMeta;
194
426
  })): void;
195
427
  /**
196
- * If executing in a server environment where sockets do not have instance-level listeners
197
- * (e.g. Bun.serve, Cloudflare Worker with WebSocket hibernation), you should call this
198
- * method when messages are received. See our self-hosting example for Bun.serve for an example.
428
+ * Processes a message received from a client WebSocket. Use this method in server
429
+ * environments where WebSocket event listeners cannot be attached directly to socket
430
+ * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).
199
431
  *
200
- * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)
201
- * @param message - The message received from the client.
432
+ * The method handles message chunking/reassembly and forwards complete messages
433
+ * to the underlying sync room for processing.
434
+ *
435
+ * @param sessionId - Session identifier matching the one used in handleSocketConnect
436
+ * @param message - Raw message data from the client (string or binary)
437
+ *
438
+ * @example
439
+ * ```ts
440
+ * // In a Bun.serve handler
441
+ * server.upgrade(req, {
442
+ * data: { sessionId, room },
443
+ * upgrade(res, req) {
444
+ * // Connection established
445
+ * },
446
+ * message(ws, message) {
447
+ * const { sessionId, room } = ws.data
448
+ * room.handleSocketMessage(sessionId, message)
449
+ * }
450
+ * })
451
+ * ```
202
452
  */
203
453
  handleSocketMessage(sessionId: string, message: AllowSharedBufferSource | string): void;
204
454
  /**
205
- * If executing in a server environment where sockets do not have instance-level listeners,
206
- * call this when a socket error occurs.
207
- * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)
455
+ * Handles a WebSocket error for the specified session. Use this in server environments
456
+ * where socket event listeners cannot be attached directly. This will initiate cleanup
457
+ * and session removal for the affected client.
458
+ *
459
+ * @param sessionId - Session identifier matching the one used in handleSocketConnect
460
+ *
461
+ * @example
462
+ * ```ts
463
+ * // In a custom WebSocket handler
464
+ * socket.addEventListener('error', () => {
465
+ * room.handleSocketError(sessionId)
466
+ * })
467
+ * ```
208
468
  */
209
469
  handleSocketError(sessionId: string): void;
210
470
  /**
211
- * If executing in a server environment where sockets do not have instance-level listeners,
212
- * call this when a socket is closed.
213
- * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)
471
+ * Handles a WebSocket close event for the specified session. Use this in server
472
+ * environments where socket event listeners cannot be attached directly. This will
473
+ * initiate cleanup and session removal for the disconnected client.
474
+ *
475
+ * @param sessionId - Session identifier matching the one used in handleSocketConnect
476
+ *
477
+ * @example
478
+ * ```ts
479
+ * // In a custom WebSocket handler
480
+ * socket.addEventListener('close', () => {
481
+ * room.handleSocketClose(sessionId)
482
+ * })
483
+ * ```
214
484
  */
215
485
  handleSocketClose(sessionId: string): void;
216
486
  /**
217
- * Returns the current 'clock' of the document.
218
- * The clock is an integer that increments every time the document changes.
219
- * The clock is stored as part of the snapshot of the document for consistency purposes.
487
+ * Returns the current document clock value. The clock is a monotonically increasing
488
+ * integer that increments with each document change, providing a consistent ordering
489
+ * of changes across the distributed system.
220
490
  *
221
- * @returns The clock
491
+ * @returns The current document clock value
492
+ *
493
+ * @example
494
+ * ```ts
495
+ * const clock = room.getCurrentDocumentClock()
496
+ * console.log(`Document is at version ${clock}`)
497
+ * ```
222
498
  */
223
499
  getCurrentDocumentClock(): number;
224
500
  /**
225
- * Returns a deeply cloned record from the store, if available.
226
- * @param id - The id of the record
227
- * @returns the cloned record
501
+ * Retrieves a deeply cloned copy of a record from the document store.
502
+ * Returns undefined if the record doesn't exist. The returned record is
503
+ * safe to mutate without affecting the original store data.
504
+ *
505
+ * @param id - Unique identifier of the record to retrieve
506
+ * @returns Deep clone of the record, or undefined if not found
507
+ *
508
+ * @example
509
+ * ```ts
510
+ * const shape = room.getRecord('shape:abc123')
511
+ * if (shape) {
512
+ * console.log('Shape position:', shape.x, shape.y)
513
+ * // Safe to modify without affecting store
514
+ * shape.x = 100
515
+ * }
516
+ * ```
228
517
  */
229
518
  getRecord(id: string): R | undefined;
230
519
  /**
231
- * Returns a list of the sessions in the room.
520
+ * Returns information about all active sessions in the room. Each session
521
+ * represents a connected client with their current connection status and metadata.
522
+ *
523
+ * @returns Array of session information objects containing:
524
+ * - sessionId - Unique session identifier
525
+ * - isConnected - Whether the session has an active WebSocket connection
526
+ * - isReadonly - Whether the session can modify the document
527
+ * - meta - Custom session metadata
528
+ *
529
+ * @example
530
+ * ```ts
531
+ * const sessions = room.getSessions()
532
+ * console.log(`Room has ${sessions.length} active sessions`)
533
+ *
534
+ * for (const session of sessions) {
535
+ * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)
536
+ * if (session.isReadonly) {
537
+ * console.log(' (read-only access)')
538
+ * }
539
+ * }
540
+ * ```
232
541
  */
233
542
  getSessions(): Array<{
234
543
  isConnected: boolean;
@@ -237,66 +546,172 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
237
546
  sessionId: string;
238
547
  }>;
239
548
  /**
240
- * Return a snapshot of the document state, including clock-related bookkeeping.
241
- * You can store this and load it later on when initializing a TLSocketRoom.
242
- * You can also pass a snapshot to {@link TLSocketRoom#loadSnapshot} if you need to revert to a previous state.
243
- * @returns The snapshot
549
+ * Creates a complete snapshot of the current document state, including all records
550
+ * and synchronization metadata. This snapshot can be persisted to storage and used
551
+ * to restore the room state later or revert to a previous version.
552
+ *
553
+ * @returns Complete room snapshot including documents, clock values, and tombstones
554
+ *
555
+ * @example
556
+ * ```ts
557
+ * // Capture current state for persistence
558
+ * const snapshot = room.getCurrentSnapshot()
559
+ * await saveToDatabase(roomId, JSON.stringify(snapshot))
560
+ *
561
+ * // Later, restore from snapshot
562
+ * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))
563
+ * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })
564
+ * ```
244
565
  */
245
566
  getCurrentSnapshot(): RoomSnapshot;
246
567
  /* Excluded from this release type: getPresenceRecords */
247
568
  /* Excluded from this release type: getCurrentSerializedSnapshot */
248
569
  /**
249
- * Load a snapshot of the document state, overwriting the current state.
250
- * @param snapshot - The snapshot to load
570
+ * Loads a document snapshot, completely replacing the current room state.
571
+ * This will disconnect all current clients and update the document to match
572
+ * the provided snapshot. Use this for restoring from backups or implementing
573
+ * document versioning.
574
+ *
575
+ * @param snapshot - Room or store snapshot to load
576
+ *
577
+ * @example
578
+ * ```ts
579
+ * // Restore from a saved snapshot
580
+ * const backup = JSON.parse(await loadBackup(roomId))
581
+ * room.loadSnapshot(backup)
582
+ *
583
+ * // All clients will be disconnected and need to reconnect
584
+ * // to see the restored document state
585
+ * ```
251
586
  */
252
587
  loadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot): void;
253
588
  /**
254
- * Allow applying changes to the store inside of a transaction.
589
+ * Executes a transaction to modify the document store. Changes made within the
590
+ * transaction are atomic and will be synchronized to all connected clients.
591
+ * The transaction provides isolation from concurrent changes until it commits.
255
592
  *
256
- * You can get values from the store by id with `store.get(id)`.
257
- * These values are safe to mutate, but to commit the changes you must call `store.put(...)` with the updated value.
258
- * You can get all values in the store with `store.getAll()`.
259
- * You can also delete values with `store.delete(id)`.
593
+ * @param updater - Function that receives store methods to make changes
594
+ * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)
595
+ * - store.put(record) - Save a modified record
596
+ * - store.getAll() - Get all records in the store
597
+ * - store.delete(id) - Remove a record from the store
598
+ * @returns Promise that resolves when the transaction completes
260
599
  *
261
600
  * @example
262
601
  * ```ts
263
- * room.updateStore(store => {
264
- * const shape = store.get('shape:abc123')
265
- * shape.meta.approved = true
266
- * store.put(shape)
602
+ * // Update multiple shapes in a single transaction
603
+ * await room.updateStore(store => {
604
+ * const shape1 = store.get('shape:abc123')
605
+ * const shape2 = store.get('shape:def456')
606
+ *
607
+ * if (shape1) {
608
+ * shape1.x = 100
609
+ * store.put(shape1)
610
+ * }
611
+ *
612
+ * if (shape2) {
613
+ * shape2.meta.approved = true
614
+ * store.put(shape2)
615
+ * }
267
616
  * })
268
617
  * ```
269
618
  *
270
- * Changes to the store inside the callback are isolated from changes made by other clients until the transaction commits.
271
- *
272
- * @param updater - A function that will be called with a store object that can be used to make changes.
273
- * @returns A promise that resolves when the transaction is complete.
619
+ * @example
620
+ * ```ts
621
+ * // Async transaction with external API call
622
+ * await room.updateStore(async store => {
623
+ * const doc = store.get('document:main')
624
+ * if (doc) {
625
+ * doc.lastModified = await getCurrentTimestamp()
626
+ * store.put(doc)
627
+ * }
628
+ * })
629
+ * ```
274
630
  */
275
631
  updateStore(updater: (store: RoomStoreMethods<R>) => Promise<void> | void): Promise<void>;
276
632
  /**
277
- * Send a custom message to a connected client.
633
+ * Sends a custom message to a specific client session. This allows sending
634
+ * application-specific data that doesn't modify the document state, such as
635
+ * notifications, chat messages, or custom commands.
636
+ *
637
+ * @param sessionId - Target session identifier
638
+ * @param data - Custom payload to send (will be JSON serialized)
639
+ *
640
+ * @example
641
+ * ```ts
642
+ * // Send a notification to a specific user
643
+ * room.sendCustomMessage('session-123', {
644
+ * type: 'notification',
645
+ * message: 'Your changes have been saved'
646
+ * })
278
647
  *
279
- * @param sessionId - The id of the session to send the message to.
280
- * @param data - The payload to send.
648
+ * // Send a chat message
649
+ * room.sendCustomMessage('session-456', {
650
+ * type: 'chat',
651
+ * from: 'Alice',
652
+ * text: 'Great work on this design!'
653
+ * })
654
+ * ```
281
655
  */
282
656
  sendCustomMessage(sessionId: string, data: any): void;
283
657
  /**
284
- * Immediately remove a session from the room, and close its socket if not already closed.
658
+ * Immediately removes a session from the room and closes its WebSocket connection.
659
+ * The client will attempt to reconnect automatically unless a fatal reason is provided.
285
660
  *
286
- * The client will attempt to reconnect unless you provide a `fatalReason` parameter.
661
+ * @param sessionId - Session identifier to remove
662
+ * @param fatalReason - Optional fatal error reason that prevents reconnection
287
663
  *
288
- * The `fatalReason` parameter will be available in the return value of the `useSync` hook as `useSync().error.reason`.
664
+ * @example
665
+ * ```ts
666
+ * // Kick a user (they can reconnect)
667
+ * room.closeSession('session-troublemaker')
289
668
  *
290
- * @param sessionId - The id of the session to remove
291
- * @param fatalReason - The reason message to use when calling .close on the underlying websocket
669
+ * // Permanently ban a user
670
+ * room.closeSession('session-banned', 'PERMISSION_DENIED')
671
+ *
672
+ * // Close session due to inactivity
673
+ * room.closeSession('session-idle', 'TIMEOUT')
674
+ * ```
292
675
  */
293
676
  closeSession(sessionId: string, fatalReason?: string | TLSyncErrorCloseEventReason): void;
294
677
  /**
295
- * Close the room and disconnect all clients. Call this before discarding the room instance or shutting down the server.
678
+ * Closes the room and disconnects all connected clients. This should be called
679
+ * when shutting down the room permanently, such as during server shutdown or
680
+ * when the room is no longer needed. Once closed, the room cannot be reopened.
681
+ *
682
+ * @example
683
+ * ```ts
684
+ * // Clean shutdown when no users remain
685
+ * if (room.getNumActiveSessions() === 0) {
686
+ * await persistSnapshot(room.getCurrentSnapshot())
687
+ * room.close()
688
+ * }
689
+ *
690
+ * // Server shutdown
691
+ * process.on('SIGTERM', () => {
692
+ * for (const room of activeRooms.values()) {
693
+ * room.close()
694
+ * }
695
+ * })
696
+ * ```
296
697
  */
297
698
  close(): void;
298
699
  /**
299
- * @returns true if the room is closed
700
+ * Checks whether the room has been permanently closed. Closed rooms cannot
701
+ * accept new connections or process further changes.
702
+ *
703
+ * @returns True if the room is closed, false if still active
704
+ *
705
+ * @example
706
+ * ```ts
707
+ * if (room.isClosed()) {
708
+ * console.log('Room has been shut down')
709
+ * // Create a new room or redirect users
710
+ * } else {
711
+ * // Room is still accepting connections
712
+ * room.handleSocketConnect({ sessionId, socket })
713
+ * }
714
+ * ```
300
715
  */
301
716
  isClosed(): boolean;
302
717
  }
@@ -312,47 +727,115 @@ export declare class TLSocketRoom<R extends UnknownRecord = UnknownRecord, Sessi
312
727
  /* Excluded from this release type: TLSyncClient */
313
728
 
314
729
  /**
315
- * This the close code that we use on the server to signal to a socket that
316
- * the connection is being closed because of a non-recoverable error.
317
- *
318
- * You should use this if you need to close a connection.
730
+ * WebSocket close code used by the server to signal a non-recoverable sync error.
731
+ * This close code indicates that the connection is being terminated due to an error
732
+ * that cannot be automatically recovered from, such as authentication failures,
733
+ * incompatible client versions, or invalid data.
319
734
  *
320
735
  * @example
321
736
  * ```ts
737
+ * // Server-side: Close connection with specific error reason
322
738
  * socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.NOT_FOUND)
323
- * ```
324
739
  *
325
- * The `reason` parameter that you pass to `socket.close()` will be made available at `useSync().error.reason`
740
+ * // Client-side: Handle the error in your sync error handler
741
+ * const syncClient = new TLSyncClient({
742
+ * // ... other config
743
+ * onSyncError: (reason) => {
744
+ * console.error('Sync failed:', reason) // Will receive 'NOT_FOUND'
745
+ * }
746
+ * })
747
+ * ```
326
748
  *
327
749
  * @public
328
750
  */
329
751
  export declare const TLSyncErrorCloseEventCode: 4099;
330
752
 
331
753
  /**
332
- * The set of reasons that a connection can be closed by the server
754
+ * Predefined reasons for server-initiated connection closures.
755
+ * These constants represent different error conditions that can cause
756
+ * the sync server to terminate a WebSocket connection.
757
+ *
758
+ * @example
759
+ * ```ts
760
+ * // Server usage
761
+ * if (!user.hasPermission(roomId)) {
762
+ * socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.FORBIDDEN)
763
+ * }
764
+ *
765
+ * // Client error handling
766
+ * syncClient.onSyncError((reason) => {
767
+ * switch (reason) {
768
+ * case TLSyncErrorCloseEventReason.NOT_FOUND:
769
+ * showError('Room does not exist')
770
+ * break
771
+ * case TLSyncErrorCloseEventReason.FORBIDDEN:
772
+ * showError('Access denied')
773
+ * break
774
+ * case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:
775
+ * showError('Please update your app')
776
+ * break
777
+ * }
778
+ * })
779
+ * ```
780
+ *
333
781
  * @public
334
782
  */
335
783
  export declare const TLSyncErrorCloseEventReason: {
784
+ /** Client exceeded rate limits */
785
+ readonly RATE_LIMITED: "RATE_LIMITED";
786
+ /** Client protocol version too old */
336
787
  readonly CLIENT_TOO_OLD: "CLIENT_TOO_OLD";
337
- readonly FORBIDDEN: "FORBIDDEN";
788
+ /** Client sent invalid or corrupted record data */
338
789
  readonly INVALID_RECORD: "INVALID_RECORD";
339
- readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
340
- readonly NOT_FOUND: "NOT_FOUND";
341
- readonly RATE_LIMITED: "RATE_LIMITED";
790
+ /** Room has reached maximum capacity */
342
791
  readonly ROOM_FULL: "ROOM_FULL";
792
+ /** Room or resource not found */
793
+ readonly NOT_FOUND: "NOT_FOUND";
794
+ /** Server protocol version too old */
343
795
  readonly SERVER_TOO_OLD: "SERVER_TOO_OLD";
796
+ /** Unexpected server error occurred */
344
797
  readonly UNKNOWN_ERROR: "UNKNOWN_ERROR";
798
+ /** User authentication required or invalid */
799
+ readonly NOT_AUTHENTICATED: "NOT_AUTHENTICATED";
800
+ /** User lacks permission to access the room */
801
+ readonly FORBIDDEN: "FORBIDDEN";
345
802
  };
346
803
 
347
804
  /**
348
- * The set of reasons that a connection can be closed by the server
805
+ * Union type of all possible server connection close reasons.
806
+ * Represents the string values that can be passed when a server closes
807
+ * a sync connection due to an error condition.
808
+ *
349
809
  * @public
350
810
  */
351
811
  export declare type TLSyncErrorCloseEventReason = (typeof TLSyncErrorCloseEventReason)[keyof typeof TLSyncErrorCloseEventReason];
352
812
 
353
- /** @public */
813
+ /**
814
+ * Logging interface for TLSocketRoom operations. Provides optional methods
815
+ * for warning and error logging during synchronization operations.
816
+ *
817
+ * @example
818
+ * ```ts
819
+ * const logger: TLSyncLog = {
820
+ * warn: (...args) => console.warn('[SYNC]', ...args),
821
+ * error: (...args) => console.error('[SYNC]', ...args)
822
+ * }
823
+ *
824
+ * const room = new TLSocketRoom({ log: logger })
825
+ * ```
826
+ *
827
+ * @public
828
+ */
354
829
  export declare interface TLSyncLog {
830
+ /**
831
+ * Optional warning logger for non-fatal sync issues
832
+ * @param args - Arguments to log
833
+ */
355
834
  warn?(...args: any[]): void;
835
+ /**
836
+ * Optional error logger for sync errors and failures
837
+ * @param args - Arguments to log
838
+ */
356
839
  error?(...args: any[]): void;
357
840
  }
358
841
 
@@ -363,19 +846,66 @@ export declare interface TLSyncLog {
363
846
  /* Excluded from this release type: ValueOpType */
364
847
 
365
848
  /**
366
- * Minimal server-side WebSocket interface that is compatible with
849
+ * Minimal server-side WebSocket interface that is compatible with various WebSocket implementations.
850
+ * This interface abstracts over different WebSocket libraries and platforms to provide a consistent
851
+ * API for the ServerSocketAdapter.
367
852
  *
368
- * - The standard WebSocket interface (cloudflare, deno, some node setups)
369
- * - The 'ws' WebSocket interface (some node setups)
853
+ * Supports:
854
+ * - The standard WebSocket interface (Cloudflare, Deno, some Node.js setups)
855
+ * - The 'ws' WebSocket interface (Node.js ws library)
370
856
  * - The Bun.serve socket implementation
371
857
  *
372
858
  * @public
859
+ * @example
860
+ * ```ts
861
+ * // Standard WebSocket
862
+ * const standardWs: WebSocketMinimal = new WebSocket('ws://localhost:8080')
863
+ *
864
+ * // Node.js 'ws' library WebSocket
865
+ * import WebSocket from 'ws'
866
+ * const nodeWs: WebSocketMinimal = new WebSocket('ws://localhost:8080')
867
+ *
868
+ * // Bun WebSocket (in server context)
869
+ * // const bunWs: WebSocketMinimal = server.upgrade(request)
870
+ * ```
373
871
  */
374
872
  export declare interface WebSocketMinimal {
873
+ /**
874
+ * Optional method to add event listeners for WebSocket events.
875
+ * Not all WebSocket implementations provide this method.
876
+ *
877
+ * @param type - The event type to listen for
878
+ * @param listener - The event handler function
879
+ */
375
880
  addEventListener?: (type: 'close' | 'error' | 'message', listener: (event: any) => void) => void;
881
+ /**
882
+ * Optional method to remove event listeners for WebSocket events.
883
+ * Not all WebSocket implementations provide this method.
884
+ *
885
+ * @param type - The event type to stop listening for
886
+ * @param listener - The event handler function to remove
887
+ */
376
888
  removeEventListener?: (type: 'close' | 'error' | 'message', listener: (event: any) => void) => void;
889
+ /**
890
+ * Sends a string message through the WebSocket connection.
891
+ *
892
+ * @param data - The string data to send
893
+ */
377
894
  send: (data: string) => void;
895
+ /**
896
+ * Closes the WebSocket connection.
897
+ *
898
+ * @param code - Optional close code (default: 1000 for normal closure)
899
+ * @param reason - Optional human-readable close reason
900
+ */
378
901
  close: (code?: number, reason?: string) => void;
902
+ /**
903
+ * The current state of the WebSocket connection.
904
+ * - 0: CONNECTING
905
+ * - 1: OPEN
906
+ * - 2: CLOSING
907
+ * - 3: CLOSED
908
+ */
379
909
  readyState: number;
380
910
  }
381
911