@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.
- package/dist-cjs/index.d.ts +605 -75
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ClientWebSocketAdapter.js +144 -0
- package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js +3 -0
- package/dist-cjs/lib/RoomSession.js.map +2 -2
- package/dist-cjs/lib/ServerSocketAdapter.js +23 -0
- package/dist-cjs/lib/ServerSocketAdapter.js.map +2 -2
- package/dist-cjs/lib/TLRemoteSyncError.js +8 -0
- package/dist-cjs/lib/TLRemoteSyncError.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +280 -56
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +45 -2
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +161 -16
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-cjs/lib/chunk.js +30 -0
- package/dist-cjs/lib/chunk.js.map +2 -2
- package/dist-cjs/lib/diff.js.map +2 -2
- package/dist-cjs/lib/findMin.js.map +2 -2
- package/dist-cjs/lib/interval.js.map +2 -2
- package/dist-cjs/lib/protocol.js.map +2 -2
- package/dist-cjs/lib/server-types.js.map +1 -1
- package/dist-esm/index.d.mts +605 -75
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ClientWebSocketAdapter.mjs +144 -0
- package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs +3 -0
- package/dist-esm/lib/RoomSession.mjs.map +2 -2
- package/dist-esm/lib/ServerSocketAdapter.mjs +23 -0
- package/dist-esm/lib/ServerSocketAdapter.mjs.map +2 -2
- package/dist-esm/lib/TLRemoteSyncError.mjs +8 -0
- package/dist-esm/lib/TLRemoteSyncError.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +280 -56
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +45 -2
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +161 -16
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/dist-esm/lib/chunk.mjs +30 -0
- package/dist-esm/lib/chunk.mjs.map +2 -2
- package/dist-esm/lib/diff.mjs.map +2 -2
- package/dist-esm/lib/findMin.mjs.map +2 -2
- package/dist-esm/lib/interval.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs.map +2 -2
- package/package.json +6 -6
- package/src/lib/ClientWebSocketAdapter.test.ts +712 -129
- package/src/lib/ClientWebSocketAdapter.ts +240 -9
- package/src/lib/RoomSession.test.ts +97 -0
- package/src/lib/RoomSession.ts +105 -3
- package/src/lib/ServerSocketAdapter.test.ts +228 -0
- package/src/lib/ServerSocketAdapter.ts +124 -5
- package/src/lib/TLRemoteSyncError.ts +50 -1
- package/src/lib/TLSocketRoom.ts +377 -60
- package/src/lib/TLSyncClient.test.ts +828 -0
- package/src/lib/TLSyncClient.ts +251 -26
- package/src/lib/TLSyncRoom.ts +284 -24
- package/src/lib/chunk.ts +72 -1
- package/src/lib/diff.ts +128 -14
- package/src/lib/findMin.ts +6 -0
- package/src/lib/interval.ts +40 -0
- package/src/lib/protocol.ts +185 -7
- package/src/lib/server-types.test.ts +44 -0
- package/src/lib/server-types.ts +45 -1
- package/src/test/TLSocketRoom.test.ts +438 -29
- package/src/test/chunk.test.ts +200 -3
- package/src/test/diff.test.ts +396 -1
package/src/lib/TLSocketRoom.ts
CHANGED
|
@@ -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
|
-
|
|
12
|
-
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
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
|
-
* @
|
|
168
|
-
*
|
|
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
|
-
*
|
|
215
|
-
*
|
|
216
|
-
*
|
|
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
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
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
|
|
233
|
-
*
|
|
234
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
244
|
-
*
|
|
245
|
-
*
|
|
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
|
|
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
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
274
|
-
*
|
|
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
|
-
*
|
|
295
|
-
*
|
|
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
|
-
*
|
|
304
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
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
|
-
*
|
|
356
|
-
*
|
|
357
|
-
*
|
|
358
|
-
* store.
|
|
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
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
375
|
-
*
|
|
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
|
|
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
|
-
*
|
|
652
|
+
* @example
|
|
653
|
+
* ```ts
|
|
654
|
+
* // Kick a user (they can reconnect)
|
|
655
|
+
* room.closeSession('session-troublemaker')
|
|
385
656
|
*
|
|
386
|
-
*
|
|
657
|
+
* // Permanently ban a user
|
|
658
|
+
* room.closeSession('session-banned', 'PERMISSION_DENIED')
|
|
387
659
|
*
|
|
388
|
-
*
|
|
389
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
}
|