@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
|
@@ -6,6 +6,20 @@ import { TLSyncErrorCloseEventReason } from "./TLSyncClient.mjs";
|
|
|
6
6
|
import { TLSyncRoom } from "./TLSyncRoom.mjs";
|
|
7
7
|
import { JsonChunkAssembler } from "./chunk.mjs";
|
|
8
8
|
class TLSocketRoom {
|
|
9
|
+
/**
|
|
10
|
+
* Creates a new TLSocketRoom instance for managing collaborative document synchronization.
|
|
11
|
+
*
|
|
12
|
+
* opts - Configuration options for the room
|
|
13
|
+
* - initialSnapshot - Optional initial document state to load
|
|
14
|
+
* - schema - Store schema defining record types and validation
|
|
15
|
+
* - clientTimeout - Milliseconds to wait before disconnecting inactive clients
|
|
16
|
+
* - log - Optional logger for warnings and errors
|
|
17
|
+
* - onSessionRemoved - Called when a client session is removed
|
|
18
|
+
* - onBeforeSendMessage - Called before sending messages to clients
|
|
19
|
+
* - onAfterReceiveMessage - Called after receiving messages from clients
|
|
20
|
+
* - onDataChange - Called when document data changes
|
|
21
|
+
* - onPresenceChange - Called when presence data changes
|
|
22
|
+
*/
|
|
9
23
|
constructor(opts) {
|
|
10
24
|
this.opts = opts;
|
|
11
25
|
const initialSnapshot = opts.initialSnapshot && "store" in opts.initialSnapshot ? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot) : opts.initialSnapshot;
|
|
@@ -46,14 +60,35 @@ class TLSocketRoom {
|
|
|
46
60
|
return this.room.sessions.size;
|
|
47
61
|
}
|
|
48
62
|
/**
|
|
49
|
-
*
|
|
63
|
+
* Handles a new client WebSocket connection, creating a session within the room.
|
|
64
|
+
* This should be called whenever a client establishes a WebSocket connection to join
|
|
65
|
+
* the collaborative document.
|
|
50
66
|
*
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
67
|
+
* @param opts - Connection options
|
|
68
|
+
* - sessionId - Unique identifier for the client session (typically from browser tab)
|
|
69
|
+
* - socket - WebSocket-like object for client communication
|
|
70
|
+
* - isReadonly - Whether the client can modify the document (defaults to false)
|
|
71
|
+
* - meta - Additional session metadata (required if SessionMeta is not void)
|
|
55
72
|
*
|
|
56
|
-
* @
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* // Handle new WebSocket connection
|
|
76
|
+
* room.handleSocketConnect({
|
|
77
|
+
* sessionId: 'user-session-abc123',
|
|
78
|
+
* socket: webSocketConnection,
|
|
79
|
+
* isReadonly: !userHasEditPermission
|
|
80
|
+
* })
|
|
81
|
+
* ```
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* // With session metadata
|
|
86
|
+
* room.handleSocketConnect({
|
|
87
|
+
* sessionId: 'session-xyz',
|
|
88
|
+
* socket: ws,
|
|
89
|
+
* meta: { userId: 'user-123', name: 'Alice' }
|
|
90
|
+
* })
|
|
91
|
+
* ```
|
|
57
92
|
*/
|
|
58
93
|
handleSocketConnect(opts) {
|
|
59
94
|
const { sessionId, socket, isReadonly = false } = opts;
|
|
@@ -88,12 +123,30 @@ class TLSocketRoom {
|
|
|
88
123
|
socket.addEventListener?.("error", handleSocketError);
|
|
89
124
|
}
|
|
90
125
|
/**
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
126
|
+
* Processes a message received from a client WebSocket. Use this method in server
|
|
127
|
+
* environments where WebSocket event listeners cannot be attached directly to socket
|
|
128
|
+
* instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).
|
|
129
|
+
*
|
|
130
|
+
* The method handles message chunking/reassembly and forwards complete messages
|
|
131
|
+
* to the underlying sync room for processing.
|
|
132
|
+
*
|
|
133
|
+
* @param sessionId - Session identifier matching the one used in handleSocketConnect
|
|
134
|
+
* @param message - Raw message data from the client (string or binary)
|
|
94
135
|
*
|
|
95
|
-
* @
|
|
96
|
-
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```ts
|
|
138
|
+
* // In a Bun.serve handler
|
|
139
|
+
* server.upgrade(req, {
|
|
140
|
+
* data: { sessionId, room },
|
|
141
|
+
* upgrade(res, req) {
|
|
142
|
+
* // Connection established
|
|
143
|
+
* },
|
|
144
|
+
* message(ws, message) {
|
|
145
|
+
* const { sessionId, room } = ws.data
|
|
146
|
+
* room.handleSocketMessage(sessionId, message)
|
|
147
|
+
* }
|
|
148
|
+
* })
|
|
149
|
+
* ```
|
|
97
150
|
*/
|
|
98
151
|
handleSocketMessage(sessionId, message) {
|
|
99
152
|
const assembler = this.sessions.get(sessionId)?.assembler;
|
|
@@ -130,41 +183,100 @@ class TLSocketRoom {
|
|
|
130
183
|
}
|
|
131
184
|
}
|
|
132
185
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
186
|
+
* Handles a WebSocket error for the specified session. Use this in server environments
|
|
187
|
+
* where socket event listeners cannot be attached directly. This will initiate cleanup
|
|
188
|
+
* and session removal for the affected client.
|
|
189
|
+
*
|
|
190
|
+
* @param sessionId - Session identifier matching the one used in handleSocketConnect
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```ts
|
|
194
|
+
* // In a custom WebSocket handler
|
|
195
|
+
* socket.addEventListener('error', () => {
|
|
196
|
+
* room.handleSocketError(sessionId)
|
|
197
|
+
* })
|
|
198
|
+
* ```
|
|
136
199
|
*/
|
|
137
200
|
handleSocketError(sessionId) {
|
|
138
201
|
this.room.handleClose(sessionId);
|
|
139
202
|
}
|
|
140
203
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
*
|
|
204
|
+
* Handles a WebSocket close event for the specified session. Use this in server
|
|
205
|
+
* environments where socket event listeners cannot be attached directly. This will
|
|
206
|
+
* initiate cleanup and session removal for the disconnected client.
|
|
207
|
+
*
|
|
208
|
+
* @param sessionId - Session identifier matching the one used in handleSocketConnect
|
|
209
|
+
*
|
|
210
|
+
* @example
|
|
211
|
+
* ```ts
|
|
212
|
+
* // In a custom WebSocket handler
|
|
213
|
+
* socket.addEventListener('close', () => {
|
|
214
|
+
* room.handleSocketClose(sessionId)
|
|
215
|
+
* })
|
|
216
|
+
* ```
|
|
144
217
|
*/
|
|
145
218
|
handleSocketClose(sessionId) {
|
|
146
219
|
this.room.handleClose(sessionId);
|
|
147
220
|
}
|
|
148
221
|
/**
|
|
149
|
-
* Returns the current
|
|
150
|
-
*
|
|
151
|
-
*
|
|
222
|
+
* Returns the current document clock value. The clock is a monotonically increasing
|
|
223
|
+
* integer that increments with each document change, providing a consistent ordering
|
|
224
|
+
* of changes across the distributed system.
|
|
225
|
+
*
|
|
226
|
+
* @returns The current document clock value
|
|
152
227
|
*
|
|
153
|
-
* @
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* const clock = room.getCurrentDocumentClock()
|
|
231
|
+
* console.log(`Document is at version ${clock}`)
|
|
232
|
+
* ```
|
|
154
233
|
*/
|
|
155
234
|
getCurrentDocumentClock() {
|
|
156
235
|
return this.room.documentClock;
|
|
157
236
|
}
|
|
158
237
|
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
238
|
+
* Retrieves a deeply cloned copy of a record from the document store.
|
|
239
|
+
* Returns undefined if the record doesn't exist. The returned record is
|
|
240
|
+
* safe to mutate without affecting the original store data.
|
|
241
|
+
*
|
|
242
|
+
* @param id - Unique identifier of the record to retrieve
|
|
243
|
+
* @returns Deep clone of the record, or undefined if not found
|
|
244
|
+
*
|
|
245
|
+
* @example
|
|
246
|
+
* ```ts
|
|
247
|
+
* const shape = room.getRecord('shape:abc123')
|
|
248
|
+
* if (shape) {
|
|
249
|
+
* console.log('Shape position:', shape.x, shape.y)
|
|
250
|
+
* // Safe to modify without affecting store
|
|
251
|
+
* shape.x = 100
|
|
252
|
+
* }
|
|
253
|
+
* ```
|
|
162
254
|
*/
|
|
163
255
|
getRecord(id) {
|
|
164
256
|
return structuredClone(this.room.documents.get(id)?.state);
|
|
165
257
|
}
|
|
166
258
|
/**
|
|
167
|
-
* Returns
|
|
259
|
+
* Returns information about all active sessions in the room. Each session
|
|
260
|
+
* represents a connected client with their current connection status and metadata.
|
|
261
|
+
*
|
|
262
|
+
* @returns Array of session information objects containing:
|
|
263
|
+
* - sessionId - Unique session identifier
|
|
264
|
+
* - isConnected - Whether the session has an active WebSocket connection
|
|
265
|
+
* - isReadonly - Whether the session can modify the document
|
|
266
|
+
* - meta - Custom session metadata
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```ts
|
|
270
|
+
* const sessions = room.getSessions()
|
|
271
|
+
* console.log(`Room has ${sessions.length} active sessions`)
|
|
272
|
+
*
|
|
273
|
+
* for (const session of sessions) {
|
|
274
|
+
* console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)
|
|
275
|
+
* if (session.isReadonly) {
|
|
276
|
+
* console.log(' (read-only access)')
|
|
277
|
+
* }
|
|
278
|
+
* }
|
|
279
|
+
* ```
|
|
168
280
|
*/
|
|
169
281
|
getSessions() {
|
|
170
282
|
return [...this.room.sessions.values()].map((session) => {
|
|
@@ -177,15 +289,31 @@ class TLSocketRoom {
|
|
|
177
289
|
});
|
|
178
290
|
}
|
|
179
291
|
/**
|
|
180
|
-
*
|
|
181
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
292
|
+
* Creates a complete snapshot of the current document state, including all records
|
|
293
|
+
* and synchronization metadata. This snapshot can be persisted to storage and used
|
|
294
|
+
* to restore the room state later or revert to a previous version.
|
|
295
|
+
*
|
|
296
|
+
* @returns Complete room snapshot including documents, clock values, and tombstones
|
|
297
|
+
*
|
|
298
|
+
* @example
|
|
299
|
+
* ```ts
|
|
300
|
+
* // Capture current state for persistence
|
|
301
|
+
* const snapshot = room.getCurrentSnapshot()
|
|
302
|
+
* await saveToDatabase(roomId, JSON.stringify(snapshot))
|
|
303
|
+
*
|
|
304
|
+
* // Later, restore from snapshot
|
|
305
|
+
* const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))
|
|
306
|
+
* const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })
|
|
307
|
+
* ```
|
|
184
308
|
*/
|
|
185
309
|
getCurrentSnapshot() {
|
|
186
310
|
return this.room.getSnapshot();
|
|
187
311
|
}
|
|
188
312
|
/**
|
|
313
|
+
* Retrieves all presence records from the document store. Presence records
|
|
314
|
+
* contain ephemeral user state like cursor positions and selections.
|
|
315
|
+
*
|
|
316
|
+
* @returns Object mapping record IDs to presence record data
|
|
189
317
|
* @internal
|
|
190
318
|
*/
|
|
191
319
|
getPresenceRecords() {
|
|
@@ -198,16 +326,32 @@ class TLSocketRoom {
|
|
|
198
326
|
return result;
|
|
199
327
|
}
|
|
200
328
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
329
|
+
* Returns a JSON-serialized snapshot of the current document state. This is
|
|
330
|
+
* equivalent to JSON.stringify(getCurrentSnapshot()) but provided as a convenience.
|
|
331
|
+
*
|
|
332
|
+
* @returns JSON string representation of the room snapshot
|
|
203
333
|
* @internal
|
|
204
334
|
*/
|
|
205
335
|
getCurrentSerializedSnapshot() {
|
|
206
336
|
return JSON.stringify(this.room.getSnapshot());
|
|
207
337
|
}
|
|
208
338
|
/**
|
|
209
|
-
*
|
|
210
|
-
*
|
|
339
|
+
* Loads a document snapshot, completely replacing the current room state.
|
|
340
|
+
* This will disconnect all current clients and update the document to match
|
|
341
|
+
* the provided snapshot. Use this for restoring from backups or implementing
|
|
342
|
+
* document versioning.
|
|
343
|
+
*
|
|
344
|
+
* @param snapshot - Room or store snapshot to load
|
|
345
|
+
*
|
|
346
|
+
* @example
|
|
347
|
+
* ```ts
|
|
348
|
+
* // Restore from a saved snapshot
|
|
349
|
+
* const backup = JSON.parse(await loadBackup(roomId))
|
|
350
|
+
* room.loadSnapshot(backup)
|
|
351
|
+
*
|
|
352
|
+
* // All clients will be disconnected and need to reconnect
|
|
353
|
+
* // to see the restored document state
|
|
354
|
+
* ```
|
|
211
355
|
*/
|
|
212
356
|
loadSnapshot(snapshot) {
|
|
213
357
|
if ("store" in snapshot) {
|
|
@@ -245,60 +389,140 @@ class TLSocketRoom {
|
|
|
245
389
|
oldRoom.close();
|
|
246
390
|
}
|
|
247
391
|
/**
|
|
248
|
-
*
|
|
392
|
+
* Executes a transaction to modify the document store. Changes made within the
|
|
393
|
+
* transaction are atomic and will be synchronized to all connected clients.
|
|
394
|
+
* The transaction provides isolation from concurrent changes until it commits.
|
|
249
395
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
396
|
+
* @param updater - Function that receives store methods to make changes
|
|
397
|
+
* - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)
|
|
398
|
+
* - store.put(record) - Save a modified record
|
|
399
|
+
* - store.getAll() - Get all records in the store
|
|
400
|
+
* - store.delete(id) - Remove a record from the store
|
|
401
|
+
* @returns Promise that resolves when the transaction completes
|
|
254
402
|
*
|
|
255
403
|
* @example
|
|
256
404
|
* ```ts
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
260
|
-
* store.
|
|
405
|
+
* // Update multiple shapes in a single transaction
|
|
406
|
+
* await room.updateStore(store => {
|
|
407
|
+
* const shape1 = store.get('shape:abc123')
|
|
408
|
+
* const shape2 = store.get('shape:def456')
|
|
409
|
+
*
|
|
410
|
+
* if (shape1) {
|
|
411
|
+
* shape1.x = 100
|
|
412
|
+
* store.put(shape1)
|
|
413
|
+
* }
|
|
414
|
+
*
|
|
415
|
+
* if (shape2) {
|
|
416
|
+
* shape2.meta.approved = true
|
|
417
|
+
* store.put(shape2)
|
|
418
|
+
* }
|
|
261
419
|
* })
|
|
262
420
|
* ```
|
|
263
421
|
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
*
|
|
267
|
-
*
|
|
422
|
+
* @example
|
|
423
|
+
* ```ts
|
|
424
|
+
* // Async transaction with external API call
|
|
425
|
+
* await room.updateStore(async store => {
|
|
426
|
+
* const doc = store.get('document:main')
|
|
427
|
+
* if (doc) {
|
|
428
|
+
* doc.lastModified = await getCurrentTimestamp()
|
|
429
|
+
* store.put(doc)
|
|
430
|
+
* }
|
|
431
|
+
* })
|
|
432
|
+
* ```
|
|
268
433
|
*/
|
|
269
434
|
async updateStore(updater) {
|
|
270
435
|
return this.room.updateStore(updater);
|
|
271
436
|
}
|
|
272
437
|
/**
|
|
273
|
-
*
|
|
438
|
+
* Sends a custom message to a specific client session. This allows sending
|
|
439
|
+
* application-specific data that doesn't modify the document state, such as
|
|
440
|
+
* notifications, chat messages, or custom commands.
|
|
274
441
|
*
|
|
275
|
-
* @param sessionId -
|
|
276
|
-
* @param data -
|
|
442
|
+
* @param sessionId - Target session identifier
|
|
443
|
+
* @param data - Custom payload to send (will be JSON serialized)
|
|
444
|
+
*
|
|
445
|
+
* @example
|
|
446
|
+
* ```ts
|
|
447
|
+
* // Send a notification to a specific user
|
|
448
|
+
* room.sendCustomMessage('session-123', {
|
|
449
|
+
* type: 'notification',
|
|
450
|
+
* message: 'Your changes have been saved'
|
|
451
|
+
* })
|
|
452
|
+
*
|
|
453
|
+
* // Send a chat message
|
|
454
|
+
* room.sendCustomMessage('session-456', {
|
|
455
|
+
* type: 'chat',
|
|
456
|
+
* from: 'Alice',
|
|
457
|
+
* text: 'Great work on this design!'
|
|
458
|
+
* })
|
|
459
|
+
* ```
|
|
277
460
|
*/
|
|
278
461
|
sendCustomMessage(sessionId, data) {
|
|
279
462
|
this.room.sendCustomMessage(sessionId, data);
|
|
280
463
|
}
|
|
281
464
|
/**
|
|
282
|
-
* Immediately
|
|
465
|
+
* Immediately removes a session from the room and closes its WebSocket connection.
|
|
466
|
+
* The client will attempt to reconnect automatically unless a fatal reason is provided.
|
|
283
467
|
*
|
|
284
|
-
*
|
|
468
|
+
* @param sessionId - Session identifier to remove
|
|
469
|
+
* @param fatalReason - Optional fatal error reason that prevents reconnection
|
|
285
470
|
*
|
|
286
|
-
*
|
|
471
|
+
* @example
|
|
472
|
+
* ```ts
|
|
473
|
+
* // Kick a user (they can reconnect)
|
|
474
|
+
* room.closeSession('session-troublemaker')
|
|
287
475
|
*
|
|
288
|
-
*
|
|
289
|
-
*
|
|
476
|
+
* // Permanently ban a user
|
|
477
|
+
* room.closeSession('session-banned', 'PERMISSION_DENIED')
|
|
478
|
+
*
|
|
479
|
+
* // Close session due to inactivity
|
|
480
|
+
* room.closeSession('session-idle', 'TIMEOUT')
|
|
481
|
+
* ```
|
|
290
482
|
*/
|
|
291
483
|
closeSession(sessionId, fatalReason) {
|
|
292
484
|
this.room.rejectSession(sessionId, fatalReason);
|
|
293
485
|
}
|
|
294
486
|
/**
|
|
295
|
-
*
|
|
487
|
+
* Closes the room and disconnects all connected clients. This should be called
|
|
488
|
+
* when shutting down the room permanently, such as during server shutdown or
|
|
489
|
+
* when the room is no longer needed. Once closed, the room cannot be reopened.
|
|
490
|
+
*
|
|
491
|
+
* @example
|
|
492
|
+
* ```ts
|
|
493
|
+
* // Clean shutdown when no users remain
|
|
494
|
+
* if (room.getNumActiveSessions() === 0) {
|
|
495
|
+
* await persistSnapshot(room.getCurrentSnapshot())
|
|
496
|
+
* room.close()
|
|
497
|
+
* }
|
|
498
|
+
*
|
|
499
|
+
* // Server shutdown
|
|
500
|
+
* process.on('SIGTERM', () => {
|
|
501
|
+
* for (const room of activeRooms.values()) {
|
|
502
|
+
* room.close()
|
|
503
|
+
* }
|
|
504
|
+
* })
|
|
505
|
+
* ```
|
|
296
506
|
*/
|
|
297
507
|
close() {
|
|
298
508
|
this.room.close();
|
|
299
509
|
}
|
|
300
510
|
/**
|
|
301
|
-
*
|
|
511
|
+
* Checks whether the room has been permanently closed. Closed rooms cannot
|
|
512
|
+
* accept new connections or process further changes.
|
|
513
|
+
*
|
|
514
|
+
* @returns True if the room is closed, false if still active
|
|
515
|
+
*
|
|
516
|
+
* @example
|
|
517
|
+
* ```ts
|
|
518
|
+
* if (room.isClosed()) {
|
|
519
|
+
* console.log('Room has been shut down')
|
|
520
|
+
* // Create a new room or redirect users
|
|
521
|
+
* } else {
|
|
522
|
+
* // Room is still accepting connections
|
|
523
|
+
* room.handleSocketConnect({ sessionId, socket })
|
|
524
|
+
* }
|
|
525
|
+
* ```
|
|
302
526
|
*/
|
|
303
527
|
isClosed() {
|
|
304
528
|
return this.room.isClosed();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/lib/TLSocketRoom.ts"],
|
|
4
|
-
"sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot, createTLSchema } from '@tldraw/tlschema'\nimport { objectMapValues, structuredClone } from '@tldraw/utils'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n// TODO: structured logging support\n/** @public */\nexport interface TLSyncLog {\n\twarn?(...args: any[]): void\n\terror?(...args: any[]): void\n}\n\n/** @public */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\tprivate readonly syncCallbacks: {\n\t\tonDataChange?(): void\n\t\tonPresenceChange?(): void\n\t}\n\n\tconstructor(\n\t\tpublic readonly opts: {\n\t\t\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t\t\tschema?: StoreSchema<R, any>\n\t\t\t// how long to wait for a client to communicate before disconnecting them\n\t\t\tclientTimeout?: number\n\t\t\tlog?: TLSyncLog\n\t\t\t// a callback that is called when a client is disconnected\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonSessionRemoved?: (\n\t\t\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\t\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t\t\t) => void\n\t\t\t// a callback that is called whenever a message is sent\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonBeforeSendMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonAfterReceiveMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\tonDataChange?(): void\n\t\t\t/** @internal */\n\t\t\tonPresenceChange?(): void\n\t\t}\n\t) {\n\t\tconst initialSnapshot =\n\t\t\topts.initialSnapshot && 'store' in opts.initialSnapshot\n\t\t\t\t? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)\n\t\t\t\t: opts.initialSnapshot\n\n\t\tthis.syncCallbacks = {\n\t\t\tonDataChange: opts.onDataChange,\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tsnapshot: initialSnapshot,\n\t\t\tlog: opts.log,\n\t\t})\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Call this when a client establishes a new socket connection.\n\t *\n\t * - `sessionId` is a unique ID for a browser tab. This is passed as a query param by the useSync hook.\n\t * - `socket` is a WebSocket-like object that the server uses to communicate with the client.\n\t * - `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.\n\t * - `meta` is an optional object that can be used to store additional information about the session.\n\t *\n\t * @param opts - The options object\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners\n\t * (e.g. Bun.serve, Cloudflare Worker with WebSocket hibernation), you should call this\n\t * method when messages are received. See our self-hosting example for Bun.serve for an example.\n\t *\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t * @param message - The message received from the client.\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket error occurs.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * If executing in a server environment where sockets do not have instance-level listeners,\n\t * call this when a socket is closed.\n\t * @param sessionId - The id of the session. (should match the one used when calling handleSocketConnect)\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current 'clock' of the document.\n\t * The clock is an integer that increments every time the document changes.\n\t * The clock is stored as part of the snapshot of the document for consistency purposes.\n\t *\n\t * @returns The clock\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.room.documentClock\n\t}\n\n\t/**\n\t * Returns a deeply cloned record from the store, if available.\n\t * @param id - The id of the record\n\t * @returns the cloned record\n\t */\n\tgetRecord(id: string) {\n\t\treturn structuredClone(this.room.documents.get(id)?.state)\n\t}\n\n\t/**\n\t * Returns a list of the sessions in the room.\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Return a snapshot of the document state, including clock-related bookkeeping.\n\t * You can store this and load it later on when initializing a TLSocketRoom.\n\t * You can also pass a snapshot to {@link TLSocketRoom#loadSnapshot} if you need to revert to a previous state.\n\t * @returns The snapshot\n\t */\n\tgetCurrentSnapshot() {\n\t\treturn this.room.getSnapshot()\n\t}\n\n\t/**\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const document of this.room.documents.values()) {\n\t\t\tif (document.state.typeName === this.room.presenceType?.typeName) {\n\t\t\t\tresult[document.state.id] = document.state\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Return a serialized snapshot of the document state, including clock-related bookkeeping.\n\t * @returns The serialized snapshot\n\t * @internal\n\t */\n\tgetCurrentSerializedSnapshot() {\n\t\treturn JSON.stringify(this.room.getSnapshot())\n\t}\n\n\t/**\n\t * Load a snapshot of the document state, overwriting the current state.\n\t * @param snapshot - The snapshot to load\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tif ('store' in snapshot) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\t\t}\n\t\tconst oldRoom = this.room\n\t\tconst oldRoomSnapshot = oldRoom.getSnapshot()\n\t\tconst oldIds = oldRoomSnapshot.documents.map((d) => d.state.id)\n\t\tconst newIds = new Set(snapshot.documents.map((d) => d.state.id))\n\t\tconst removedIds = oldIds.filter((id) => !newIds.has(id))\n\n\t\tconst tombstones: RoomSnapshot['tombstones'] = { ...oldRoomSnapshot.tombstones }\n\t\tremovedIds.forEach((id) => {\n\t\t\ttombstones[id] = oldRoom.clock + 1\n\t\t})\n\t\tnewIds.forEach((id) => {\n\t\t\tdelete tombstones[id]\n\t\t})\n\n\t\tconst newRoom = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: oldRoom.schema,\n\t\t\tsnapshot: {\n\t\t\t\tclock: oldRoom.clock + 1,\n\t\t\t\tdocumentClock: oldRoom.clock + 1,\n\t\t\t\tdocuments: snapshot.documents.map((d) => ({\n\t\t\t\t\tlastChangedClock: oldRoom.clock + 1,\n\t\t\t\t\tstate: d.state,\n\t\t\t\t})),\n\t\t\t\tschema: snapshot.schema,\n\t\t\t\ttombstones,\n\t\t\t\ttombstoneHistoryStartsAtClock: oldRoomSnapshot.tombstoneHistoryStartsAtClock,\n\t\t\t},\n\t\t\tlog: this.log,\n\t\t})\n\t\t// replace room with new one and kick out all the clients\n\t\tthis.room = newRoom\n\t\toldRoom.close()\n\t}\n\n\t/**\n\t * Allow applying changes to the store inside of a transaction.\n\t *\n\t * You can get values from the store by id with `store.get(id)`.\n\t * These values are safe to mutate, but to commit the changes you must call `store.put(...)` with the updated value.\n\t * You can get all values in the store with `store.getAll()`.\n\t * You can also delete values with `store.delete(id)`.\n\t *\n\t * @example\n\t * ```ts\n\t * room.updateStore(store => {\n\t * const shape = store.get('shape:abc123')\n\t * shape.meta.approved = true\n\t * store.put(shape)\n\t * })\n\t * ```\n\t *\n\t * Changes to the store inside the callback are isolated from changes made by other clients until the transaction commits.\n\t *\n\t * @param updater - A function that will be called with a store object that can be used to make changes.\n\t * @returns A promise that resolves when the transaction is complete.\n\t */\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\treturn this.room.updateStore(updater)\n\t}\n\n\t/**\n\t * Send a custom message to a connected client.\n\t *\n\t * @param sessionId - The id of the session to send the message to.\n\t * @param data - The payload to send.\n\t */\n\tsendCustomMessage(sessionId: string, data: any) {\n\t\tthis.room.sendCustomMessage(sessionId, data)\n\t}\n\n\t/**\n\t * Immediately remove a session from the room, and close its socket if not already closed.\n\t *\n\t * The client will attempt to reconnect unless you provide a `fatalReason` parameter.\n\t *\n\t * The `fatalReason` parameter will be available in the return value of the `useSync` hook as `useSync().error.reason`.\n\t *\n\t * @param sessionId - The id of the session to remove\n\t * @param fatalReason - The reason message to use when calling .close on the underlying websocket\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Close the room and disconnect all clients. Call this before discarding the room instance or shutting down the server.\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t}\n\n\t/**\n\t * @returns true if the room is closed\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/** @public */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\nfunction convertStoreSnapshotToRoomSnapshot(snapshot: TLStoreSnapshot): RoomSnapshot {\n\treturn {\n\t\tclock: 0,\n\t\tdocumentClock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
|
|
5
|
-
"mappings": "AACA,SAA0B,sBAAsB;AAChD,SAAS,iBAAiB,uBAAuB;AACjD,SAAS,wBAAwB;AACjC,SAAS,2BAA6C;AACtD,SAAS,mCAAmC;AAC5C,SAAyC,kBAAkB;AAC3D,SAAS,0BAA0B;
|
|
4
|
+
"sourcesContent": ["import type { StoreSchema, UnknownRecord } from '@tldraw/store'\nimport { TLStoreSnapshot, createTLSchema } from '@tldraw/tlschema'\nimport { objectMapValues, structuredClone } from '@tldraw/utils'\nimport { RoomSessionState } from './RoomSession'\nimport { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'\nimport { TLSyncErrorCloseEventReason } from './TLSyncClient'\nimport { RoomSnapshot, RoomStoreMethods, TLSyncRoom } from './TLSyncRoom'\nimport { JsonChunkAssembler } from './chunk'\nimport { TLSocketServerSentEvent } from './protocol'\n\n/**\n * Logging interface for TLSocketRoom operations. Provides optional methods\n * for warning and error logging during synchronization operations.\n *\n * @example\n * ```ts\n * const logger: TLSyncLog = {\n * warn: (...args) => console.warn('[SYNC]', ...args),\n * error: (...args) => console.error('[SYNC]', ...args)\n * }\n *\n * const room = new TLSocketRoom({ log: logger })\n * ```\n *\n * @public\n */\nexport interface TLSyncLog {\n\t/**\n\t * Optional warning logger for non-fatal sync issues\n\t * @param args - Arguments to log\n\t */\n\twarn?(...args: any[]): void\n\t/**\n\t * Optional error logger for sync errors and failures\n\t * @param args - Arguments to log\n\t */\n\terror?(...args: any[]): void\n}\n\n/**\n * A server-side room that manages WebSocket connections and synchronizes tldraw document state\n * between multiple clients in real-time. Each room represents a collaborative document space\n * where users can work together on drawings with automatic conflict resolution.\n *\n * TLSocketRoom handles:\n * - WebSocket connection lifecycle management\n * - Real-time synchronization of document changes\n * - Session management and presence tracking\n * - Message chunking for large payloads\n * - Automatic client timeout and cleanup\n *\n * @example\n * ```ts\n * // Basic room setup\n * const room = new TLSocketRoom({\n * onSessionRemoved: (room, { sessionId, numSessionsRemaining }) => {\n * console.log(`Client ${sessionId} disconnected, ${numSessionsRemaining} remaining`)\n * if (numSessionsRemaining === 0) {\n * room.close()\n * }\n * },\n * onDataChange: () => {\n * console.log('Document data changed, consider persisting')\n * }\n * })\n *\n * // Handle new client connections\n * room.handleSocketConnect({\n * sessionId: 'user-session-123',\n * socket: webSocket,\n * isReadonly: false\n * })\n * ```\n *\n * @example\n * ```ts\n * // Room with initial snapshot and schema\n * const room = new TLSocketRoom({\n * initialSnapshot: existingSnapshot,\n * schema: myCustomSchema,\n * clientTimeout: 30000,\n * log: {\n * warn: (...args) => logger.warn('SYNC:', ...args),\n * error: (...args) => logger.error('SYNC:', ...args)\n * }\n * })\n *\n * // Update document programmatically\n * await room.updateStore(store => {\n * const shape = store.get('shape:abc123')\n * if (shape) {\n * shape.x = 100\n * store.put(shape)\n * }\n * })\n * ```\n *\n * @public\n */\nexport class TLSocketRoom<R extends UnknownRecord = UnknownRecord, SessionMeta = void> {\n\tprivate room: TLSyncRoom<R, SessionMeta>\n\tprivate readonly sessions = new Map<\n\t\tstring,\n\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t{ assembler: JsonChunkAssembler; socket: WebSocketMinimal; unlisten: () => void }\n\t>()\n\treadonly log?: TLSyncLog\n\tprivate readonly syncCallbacks: {\n\t\tonDataChange?(): void\n\t\tonPresenceChange?(): void\n\t}\n\n\t/**\n\t * Creates a new TLSocketRoom instance for managing collaborative document synchronization.\n\t *\n\t * opts - Configuration options for the room\n\t * - initialSnapshot - Optional initial document state to load\n\t * - schema - Store schema defining record types and validation\n\t * - clientTimeout - Milliseconds to wait before disconnecting inactive clients\n\t * - log - Optional logger for warnings and errors\n\t * - onSessionRemoved - Called when a client session is removed\n\t * - onBeforeSendMessage - Called before sending messages to clients\n\t * - onAfterReceiveMessage - Called after receiving messages from clients\n\t * - onDataChange - Called when document data changes\n\t * - onPresenceChange - Called when presence data changes\n\t */\n\tconstructor(\n\t\tpublic readonly opts: {\n\t\t\tinitialSnapshot?: RoomSnapshot | TLStoreSnapshot\n\t\t\tschema?: StoreSchema<R, any>\n\t\t\t// how long to wait for a client to communicate before disconnecting them\n\t\t\tclientTimeout?: number\n\t\t\tlog?: TLSyncLog\n\t\t\t// a callback that is called when a client is disconnected\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonSessionRemoved?: (\n\t\t\t\troom: TLSocketRoom<R, SessionMeta>,\n\t\t\t\targs: { sessionId: string; numSessionsRemaining: number; meta: SessionMeta }\n\t\t\t) => void\n\t\t\t// a callback that is called whenever a message is sent\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonBeforeSendMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\t// eslint-disable-next-line @typescript-eslint/method-signature-style\n\t\t\tonAfterReceiveMessage?: (args: {\n\t\t\t\tsessionId: string\n\t\t\t\t/** @internal keep the protocol private for now */\n\t\t\t\tmessage: TLSocketServerSentEvent<R>\n\t\t\t\tstringified: string\n\t\t\t\tmeta: SessionMeta\n\t\t\t}) => void\n\t\t\tonDataChange?(): void\n\t\t\t/** @internal */\n\t\t\tonPresenceChange?(): void\n\t\t}\n\t) {\n\t\tconst initialSnapshot =\n\t\t\topts.initialSnapshot && 'store' in opts.initialSnapshot\n\t\t\t\t? convertStoreSnapshotToRoomSnapshot(opts.initialSnapshot!)\n\t\t\t\t: opts.initialSnapshot\n\n\t\tthis.syncCallbacks = {\n\t\t\tonDataChange: opts.onDataChange,\n\t\t\tonPresenceChange: opts.onPresenceChange,\n\t\t}\n\t\tthis.room = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: opts.schema ?? (createTLSchema() as any),\n\t\t\tsnapshot: initialSnapshot,\n\t\t\tlog: opts.log,\n\t\t})\n\t\tthis.room.events.on('session_removed', (args) => {\n\t\t\tthis.sessions.delete(args.sessionId)\n\t\t\tif (this.opts.onSessionRemoved) {\n\t\t\t\tthis.opts.onSessionRemoved(this, {\n\t\t\t\t\tsessionId: args.sessionId,\n\t\t\t\t\tnumSessionsRemaining: this.room.sessions.size,\n\t\t\t\t\tmeta: args.meta,\n\t\t\t\t})\n\t\t\t}\n\t\t})\n\t\tthis.log = 'log' in opts ? opts.log : { error: console.error }\n\t}\n\n\t/**\n\t * Returns the number of active sessions.\n\t * Note that this is not the same as the number of connected sockets!\n\t * Sessions time out a few moments after sockets close, to smooth over network hiccups.\n\t *\n\t * @returns the number of active sessions\n\t */\n\tgetNumActiveSessions() {\n\t\treturn this.room.sessions.size\n\t}\n\n\t/**\n\t * Handles a new client WebSocket connection, creating a session within the room.\n\t * This should be called whenever a client establishes a WebSocket connection to join\n\t * the collaborative document.\n\t *\n\t * @param opts - Connection options\n\t * - sessionId - Unique identifier for the client session (typically from browser tab)\n\t * - socket - WebSocket-like object for client communication\n\t * - isReadonly - Whether the client can modify the document (defaults to false)\n\t * - meta - Additional session metadata (required if SessionMeta is not void)\n\t *\n\t * @example\n\t * ```ts\n\t * // Handle new WebSocket connection\n\t * room.handleSocketConnect({\n\t * sessionId: 'user-session-abc123',\n\t * socket: webSocketConnection,\n\t * isReadonly: !userHasEditPermission\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // With session metadata\n\t * room.handleSocketConnect({\n\t * sessionId: 'session-xyz',\n\t * socket: ws,\n\t * meta: { userId: 'user-123', name: 'Alice' }\n\t * })\n\t * ```\n\t */\n\thandleSocketConnect(\n\t\topts: {\n\t\t\tsessionId: string\n\t\t\tsocket: WebSocketMinimal\n\t\t\tisReadonly?: boolean\n\t\t} & (SessionMeta extends void ? object : { meta: SessionMeta })\n\t) {\n\t\tconst { sessionId, socket, isReadonly = false } = opts\n\t\tconst handleSocketMessage = (event: MessageEvent) =>\n\t\t\tthis.handleSocketMessage(sessionId, event.data)\n\t\tconst handleSocketError = this.handleSocketError.bind(this, sessionId)\n\t\tconst handleSocketClose = this.handleSocketClose.bind(this, sessionId)\n\n\t\tthis.sessions.set(sessionId, {\n\t\t\tassembler: new JsonChunkAssembler(),\n\t\t\tsocket,\n\t\t\tunlisten: () => {\n\t\t\t\tsocket.removeEventListener?.('message', handleSocketMessage)\n\t\t\t\tsocket.removeEventListener?.('close', handleSocketClose)\n\t\t\t\tsocket.removeEventListener?.('error', handleSocketError)\n\t\t\t},\n\t\t})\n\n\t\tthis.room.handleNewSession({\n\t\t\tsessionId,\n\t\t\tisReadonly,\n\t\t\tsocket: new ServerSocketAdapter({\n\t\t\t\tws: socket,\n\t\t\t\tonBeforeSendMessage: this.opts.onBeforeSendMessage\n\t\t\t\t\t? (message, stringified) =>\n\t\t\t\t\t\t\tthis.opts.onBeforeSendMessage!({\n\t\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\t\tmessage,\n\t\t\t\t\t\t\t\tstringified,\n\t\t\t\t\t\t\t\tmeta: this.room.sessions.get(sessionId)?.meta as SessionMeta,\n\t\t\t\t\t\t\t})\n\t\t\t\t\t: undefined,\n\t\t\t}),\n\t\t\tmeta: 'meta' in opts ? (opts.meta as any) : undefined,\n\t\t})\n\n\t\tsocket.addEventListener?.('message', handleSocketMessage)\n\t\tsocket.addEventListener?.('close', handleSocketClose)\n\t\tsocket.addEventListener?.('error', handleSocketError)\n\t}\n\n\t/**\n\t * Processes a message received from a client WebSocket. Use this method in server\n\t * environments where WebSocket event listeners cannot be attached directly to socket\n\t * instances (e.g., Bun.serve, Cloudflare Workers with WebSocket hibernation).\n\t *\n\t * The method handles message chunking/reassembly and forwards complete messages\n\t * to the underlying sync room for processing.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t * @param message - Raw message data from the client (string or binary)\n\t *\n\t * @example\n\t * ```ts\n\t * // In a Bun.serve handler\n\t * server.upgrade(req, {\n\t * data: { sessionId, room },\n\t * upgrade(res, req) {\n\t * // Connection established\n\t * },\n\t * message(ws, message) {\n\t * const { sessionId, room } = ws.data\n\t * room.handleSocketMessage(sessionId, message)\n\t * }\n\t * })\n\t * ```\n\t */\n\thandleSocketMessage(sessionId: string, message: string | AllowSharedBufferSource) {\n\t\tconst assembler = this.sessions.get(sessionId)?.assembler\n\t\tif (!assembler) {\n\t\t\tthis.log?.warn?.('Received message from unknown session', sessionId)\n\t\t\treturn\n\t\t}\n\n\t\ttry {\n\t\t\tconst messageString =\n\t\t\t\ttypeof message === 'string' ? message : new TextDecoder().decode(message)\n\t\t\tconst res = assembler.handleMessage(messageString)\n\t\t\tif (!res) {\n\t\t\t\t// not enough chunks yet\n\t\t\t\treturn\n\t\t\t}\n\t\t\tif ('data' in res) {\n\t\t\t\t// need to do this first in case the session gets removed as a result of handling the message\n\t\t\t\tif (this.opts.onAfterReceiveMessage) {\n\t\t\t\t\tconst session = this.room.sessions.get(sessionId)\n\t\t\t\t\tif (session) {\n\t\t\t\t\t\tthis.opts.onAfterReceiveMessage({\n\t\t\t\t\t\t\tsessionId,\n\t\t\t\t\t\t\tmessage: res.data as any,\n\t\t\t\t\t\t\tstringified: res.stringified,\n\t\t\t\t\t\t\tmeta: session.meta,\n\t\t\t\t\t\t})\n\t\t\t\t\t}\n\t\t\t\t}\n\n\t\t\t\tthis.room.handleMessage(sessionId, res.data as any)\n\t\t\t} else {\n\t\t\t\tthis.log?.error?.('Error assembling message', res.error)\n\t\t\t\t// close the socket to reset the connection\n\t\t\t\tthis.handleSocketError(sessionId)\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log?.error?.(e)\n\t\t\t// here we use rejectSession rather than removeSession to support legacy clients\n\t\t\t// that use the old incompatibility_error close event\n\t\t\tthis.room.rejectSession(sessionId, TLSyncErrorCloseEventReason.UNKNOWN_ERROR)\n\t\t}\n\t}\n\n\t/**\n\t * Handles a WebSocket error for the specified session. Use this in server environments\n\t * where socket event listeners cannot be attached directly. This will initiate cleanup\n\t * and session removal for the affected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('error', () => {\n\t * room.handleSocketError(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketError(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Handles a WebSocket close event for the specified session. Use this in server\n\t * environments where socket event listeners cannot be attached directly. This will\n\t * initiate cleanup and session removal for the disconnected client.\n\t *\n\t * @param sessionId - Session identifier matching the one used in handleSocketConnect\n\t *\n\t * @example\n\t * ```ts\n\t * // In a custom WebSocket handler\n\t * socket.addEventListener('close', () => {\n\t * room.handleSocketClose(sessionId)\n\t * })\n\t * ```\n\t */\n\thandleSocketClose(sessionId: string) {\n\t\tthis.room.handleClose(sessionId)\n\t}\n\n\t/**\n\t * Returns the current document clock value. The clock is a monotonically increasing\n\t * integer that increments with each document change, providing a consistent ordering\n\t * of changes across the distributed system.\n\t *\n\t * @returns The current document clock value\n\t *\n\t * @example\n\t * ```ts\n\t * const clock = room.getCurrentDocumentClock()\n\t * console.log(`Document is at version ${clock}`)\n\t * ```\n\t */\n\tgetCurrentDocumentClock() {\n\t\treturn this.room.documentClock\n\t}\n\n\t/**\n\t * Retrieves a deeply cloned copy of a record from the document store.\n\t * Returns undefined if the record doesn't exist. The returned record is\n\t * safe to mutate without affecting the original store data.\n\t *\n\t * @param id - Unique identifier of the record to retrieve\n\t * @returns Deep clone of the record, or undefined if not found\n\t *\n\t * @example\n\t * ```ts\n\t * const shape = room.getRecord('shape:abc123')\n\t * if (shape) {\n\t * console.log('Shape position:', shape.x, shape.y)\n\t * // Safe to modify without affecting store\n\t * shape.x = 100\n\t * }\n\t * ```\n\t */\n\tgetRecord(id: string) {\n\t\treturn structuredClone(this.room.documents.get(id)?.state)\n\t}\n\n\t/**\n\t * Returns information about all active sessions in the room. Each session\n\t * represents a connected client with their current connection status and metadata.\n\t *\n\t * @returns Array of session information objects containing:\n\t * - sessionId - Unique session identifier\n\t * - isConnected - Whether the session has an active WebSocket connection\n\t * - isReadonly - Whether the session can modify the document\n\t * - meta - Custom session metadata\n\t *\n\t * @example\n\t * ```ts\n\t * const sessions = room.getSessions()\n\t * console.log(`Room has ${sessions.length} active sessions`)\n\t *\n\t * for (const session of sessions) {\n\t * console.log(`${session.sessionId}: ${session.isConnected ? 'online' : 'offline'}`)\n\t * if (session.isReadonly) {\n\t * console.log(' (read-only access)')\n\t * }\n\t * }\n\t * ```\n\t */\n\tgetSessions(): Array<{\n\t\tsessionId: string\n\t\tisConnected: boolean\n\t\tisReadonly: boolean\n\t\tmeta: SessionMeta\n\t}> {\n\t\treturn [...this.room.sessions.values()].map((session) => {\n\t\t\treturn {\n\t\t\t\tsessionId: session.sessionId,\n\t\t\t\tisConnected: session.state === RoomSessionState.Connected,\n\t\t\t\tisReadonly: session.isReadonly,\n\t\t\t\tmeta: session.meta,\n\t\t\t}\n\t\t})\n\t}\n\n\t/**\n\t * Creates a complete snapshot of the current document state, including all records\n\t * and synchronization metadata. This snapshot can be persisted to storage and used\n\t * to restore the room state later or revert to a previous version.\n\t *\n\t * @returns Complete room snapshot including documents, clock values, and tombstones\n\t *\n\t * @example\n\t * ```ts\n\t * // Capture current state for persistence\n\t * const snapshot = room.getCurrentSnapshot()\n\t * await saveToDatabase(roomId, JSON.stringify(snapshot))\n\t *\n\t * // Later, restore from snapshot\n\t * const savedSnapshot = JSON.parse(await loadFromDatabase(roomId))\n\t * const newRoom = new TLSocketRoom({ initialSnapshot: savedSnapshot })\n\t * ```\n\t */\n\tgetCurrentSnapshot() {\n\t\treturn this.room.getSnapshot()\n\t}\n\n\t/**\n\t * Retrieves all presence records from the document store. Presence records\n\t * contain ephemeral user state like cursor positions and selections.\n\t *\n\t * @returns Object mapping record IDs to presence record data\n\t * @internal\n\t */\n\tgetPresenceRecords() {\n\t\tconst result = {} as Record<string, UnknownRecord>\n\t\tfor (const document of this.room.documents.values()) {\n\t\t\tif (document.state.typeName === this.room.presenceType?.typeName) {\n\t\t\t\tresult[document.state.id] = document.state\n\t\t\t}\n\t\t}\n\t\treturn result\n\t}\n\n\t/**\n\t * Returns a JSON-serialized snapshot of the current document state. This is\n\t * equivalent to JSON.stringify(getCurrentSnapshot()) but provided as a convenience.\n\t *\n\t * @returns JSON string representation of the room snapshot\n\t * @internal\n\t */\n\tgetCurrentSerializedSnapshot() {\n\t\treturn JSON.stringify(this.room.getSnapshot())\n\t}\n\n\t/**\n\t * Loads a document snapshot, completely replacing the current room state.\n\t * This will disconnect all current clients and update the document to match\n\t * the provided snapshot. Use this for restoring from backups or implementing\n\t * document versioning.\n\t *\n\t * @param snapshot - Room or store snapshot to load\n\t *\n\t * @example\n\t * ```ts\n\t * // Restore from a saved snapshot\n\t * const backup = JSON.parse(await loadBackup(roomId))\n\t * room.loadSnapshot(backup)\n\t *\n\t * // All clients will be disconnected and need to reconnect\n\t * // to see the restored document state\n\t * ```\n\t */\n\tloadSnapshot(snapshot: RoomSnapshot | TLStoreSnapshot) {\n\t\tif ('store' in snapshot) {\n\t\t\tsnapshot = convertStoreSnapshotToRoomSnapshot(snapshot)\n\t\t}\n\t\tconst oldRoom = this.room\n\t\tconst oldRoomSnapshot = oldRoom.getSnapshot()\n\t\tconst oldIds = oldRoomSnapshot.documents.map((d) => d.state.id)\n\t\tconst newIds = new Set(snapshot.documents.map((d) => d.state.id))\n\t\tconst removedIds = oldIds.filter((id) => !newIds.has(id))\n\n\t\tconst tombstones: RoomSnapshot['tombstones'] = { ...oldRoomSnapshot.tombstones }\n\t\tremovedIds.forEach((id) => {\n\t\t\ttombstones[id] = oldRoom.clock + 1\n\t\t})\n\t\tnewIds.forEach((id) => {\n\t\t\tdelete tombstones[id]\n\t\t})\n\n\t\tconst newRoom = new TLSyncRoom<R, SessionMeta>({\n\t\t\t...this.syncCallbacks,\n\t\t\tschema: oldRoom.schema,\n\t\t\tsnapshot: {\n\t\t\t\tclock: oldRoom.clock + 1,\n\t\t\t\tdocumentClock: oldRoom.clock + 1,\n\t\t\t\tdocuments: snapshot.documents.map((d) => ({\n\t\t\t\t\tlastChangedClock: oldRoom.clock + 1,\n\t\t\t\t\tstate: d.state,\n\t\t\t\t})),\n\t\t\t\tschema: snapshot.schema,\n\t\t\t\ttombstones,\n\t\t\t\ttombstoneHistoryStartsAtClock: oldRoomSnapshot.tombstoneHistoryStartsAtClock,\n\t\t\t},\n\t\t\tlog: this.log,\n\t\t})\n\t\t// replace room with new one and kick out all the clients\n\t\tthis.room = newRoom\n\t\toldRoom.close()\n\t}\n\n\t/**\n\t * Executes a transaction to modify the document store. Changes made within the\n\t * transaction are atomic and will be synchronized to all connected clients.\n\t * The transaction provides isolation from concurrent changes until it commits.\n\t *\n\t * @param updater - Function that receives store methods to make changes\n\t * - store.get(id) - Retrieve a record (safe to mutate, but must call put() to commit)\n\t * - store.put(record) - Save a modified record\n\t * - store.getAll() - Get all records in the store\n\t * - store.delete(id) - Remove a record from the store\n\t * @returns Promise that resolves when the transaction completes\n\t *\n\t * @example\n\t * ```ts\n\t * // Update multiple shapes in a single transaction\n\t * await room.updateStore(store => {\n\t * const shape1 = store.get('shape:abc123')\n\t * const shape2 = store.get('shape:def456')\n\t *\n\t * if (shape1) {\n\t * shape1.x = 100\n\t * store.put(shape1)\n\t * }\n\t *\n\t * if (shape2) {\n\t * shape2.meta.approved = true\n\t * store.put(shape2)\n\t * }\n\t * })\n\t * ```\n\t *\n\t * @example\n\t * ```ts\n\t * // Async transaction with external API call\n\t * await room.updateStore(async store => {\n\t * const doc = store.get('document:main')\n\t * if (doc) {\n\t * doc.lastModified = await getCurrentTimestamp()\n\t * store.put(doc)\n\t * }\n\t * })\n\t * ```\n\t */\n\tasync updateStore(updater: (store: RoomStoreMethods<R>) => void | Promise<void>) {\n\t\treturn this.room.updateStore(updater)\n\t}\n\n\t/**\n\t * Sends a custom message to a specific client session. This allows sending\n\t * application-specific data that doesn't modify the document state, such as\n\t * notifications, chat messages, or custom commands.\n\t *\n\t * @param sessionId - Target session identifier\n\t * @param data - Custom payload to send (will be JSON serialized)\n\t *\n\t * @example\n\t * ```ts\n\t * // Send a notification to a specific user\n\t * room.sendCustomMessage('session-123', {\n\t * type: 'notification',\n\t * message: 'Your changes have been saved'\n\t * })\n\t *\n\t * // Send a chat message\n\t * room.sendCustomMessage('session-456', {\n\t * type: 'chat',\n\t * from: 'Alice',\n\t * text: 'Great work on this design!'\n\t * })\n\t * ```\n\t */\n\tsendCustomMessage(sessionId: string, data: any) {\n\t\tthis.room.sendCustomMessage(sessionId, data)\n\t}\n\n\t/**\n\t * Immediately removes a session from the room and closes its WebSocket connection.\n\t * The client will attempt to reconnect automatically unless a fatal reason is provided.\n\t *\n\t * @param sessionId - Session identifier to remove\n\t * @param fatalReason - Optional fatal error reason that prevents reconnection\n\t *\n\t * @example\n\t * ```ts\n\t * // Kick a user (they can reconnect)\n\t * room.closeSession('session-troublemaker')\n\t *\n\t * // Permanently ban a user\n\t * room.closeSession('session-banned', 'PERMISSION_DENIED')\n\t *\n\t * // Close session due to inactivity\n\t * room.closeSession('session-idle', 'TIMEOUT')\n\t * ```\n\t */\n\tcloseSession(sessionId: string, fatalReason?: TLSyncErrorCloseEventReason | string) {\n\t\tthis.room.rejectSession(sessionId, fatalReason)\n\t}\n\n\t/**\n\t * Closes the room and disconnects all connected clients. This should be called\n\t * when shutting down the room permanently, such as during server shutdown or\n\t * when the room is no longer needed. Once closed, the room cannot be reopened.\n\t *\n\t * @example\n\t * ```ts\n\t * // Clean shutdown when no users remain\n\t * if (room.getNumActiveSessions() === 0) {\n\t * await persistSnapshot(room.getCurrentSnapshot())\n\t * room.close()\n\t * }\n\t *\n\t * // Server shutdown\n\t * process.on('SIGTERM', () => {\n\t * for (const room of activeRooms.values()) {\n\t * room.close()\n\t * }\n\t * })\n\t * ```\n\t */\n\tclose() {\n\t\tthis.room.close()\n\t}\n\n\t/**\n\t * Checks whether the room has been permanently closed. Closed rooms cannot\n\t * accept new connections or process further changes.\n\t *\n\t * @returns True if the room is closed, false if still active\n\t *\n\t * @example\n\t * ```ts\n\t * if (room.isClosed()) {\n\t * console.log('Room has been shut down')\n\t * // Create a new room or redirect users\n\t * } else {\n\t * // Room is still accepting connections\n\t * room.handleSocketConnect({ sessionId, socket })\n\t * }\n\t * ```\n\t */\n\tisClosed() {\n\t\treturn this.room.isClosed()\n\t}\n}\n\n/**\n * Utility type that removes properties with void values from an object type.\n * This is used internally to conditionally require session metadata based on\n * whether SessionMeta extends void.\n *\n * @example\n * ```ts\n * type Example = { a: string, b: void, c: number }\n * type Result = OmitVoid<Example> // { a: string, c: number }\n * ```\n *\n * @public\n */\nexport type OmitVoid<T, KS extends keyof T = keyof T> = {\n\t[K in KS extends any ? (void extends T[KS] ? never : KS) : never]: T[K]\n}\n\nfunction convertStoreSnapshotToRoomSnapshot(snapshot: TLStoreSnapshot): RoomSnapshot {\n\treturn {\n\t\tclock: 0,\n\t\tdocumentClock: 0,\n\t\tdocuments: objectMapValues(snapshot.store).map((state) => ({\n\t\t\tstate,\n\t\t\tlastChangedClock: 0,\n\t\t})),\n\t\tschema: snapshot.schema,\n\t\ttombstones: {},\n\t}\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|