@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.
- 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/dist-cjs/index.d.ts
CHANGED
|
@@ -34,7 +34,19 @@ import { UnknownRecord } from '@tldraw/store';
|
|
|
34
34
|
|
|
35
35
|
/* Excluded from this release type: ObjectDiff */
|
|
36
36
|
|
|
37
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
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
|
-
*
|
|
182
|
-
*
|
|
183
|
-
*
|
|
184
|
-
*
|
|
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
|
-
* @
|
|
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
|
-
*
|
|
197
|
-
*
|
|
198
|
-
*
|
|
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
|
-
*
|
|
201
|
-
*
|
|
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
|
-
*
|
|
206
|
-
*
|
|
207
|
-
*
|
|
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
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
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
|
|
218
|
-
*
|
|
219
|
-
*
|
|
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
|
-
*
|
|
226
|
-
*
|
|
227
|
-
*
|
|
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
|
|
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
|
-
*
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
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
|
-
*
|
|
250
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
257
|
-
*
|
|
258
|
-
*
|
|
259
|
-
*
|
|
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
|
-
*
|
|
264
|
-
*
|
|
265
|
-
*
|
|
266
|
-
* store.
|
|
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
|
-
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
280
|
-
*
|
|
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
|
|
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
|
-
*
|
|
661
|
+
* @param sessionId - Session identifier to remove
|
|
662
|
+
* @param fatalReason - Optional fatal error reason that prevents reconnection
|
|
287
663
|
*
|
|
288
|
-
*
|
|
664
|
+
* @example
|
|
665
|
+
* ```ts
|
|
666
|
+
* // Kick a user (they can reconnect)
|
|
667
|
+
* room.closeSession('session-troublemaker')
|
|
289
668
|
*
|
|
290
|
-
*
|
|
291
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
316
|
-
* the connection is being
|
|
317
|
-
*
|
|
318
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
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
|
-
|
|
788
|
+
/** Client sent invalid or corrupted record data */
|
|
338
789
|
readonly INVALID_RECORD: "INVALID_RECORD";
|
|
339
|
-
|
|
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
|
-
*
|
|
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
|
-
/**
|
|
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
|
-
*
|
|
369
|
-
* - The
|
|
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
|
|