@tldraw/sync-core 4.1.0-next.b6dfe9bccde9 → 4.1.0-next.b9999db71010
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist-cjs/index.d.ts +605 -75
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/ClientWebSocketAdapter.js +144 -0
- package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js +3 -0
- package/dist-cjs/lib/RoomSession.js.map +2 -2
- package/dist-cjs/lib/ServerSocketAdapter.js +23 -0
- package/dist-cjs/lib/ServerSocketAdapter.js.map +2 -2
- package/dist-cjs/lib/TLRemoteSyncError.js +8 -0
- package/dist-cjs/lib/TLRemoteSyncError.js.map +2 -2
- package/dist-cjs/lib/TLSocketRoom.js +280 -56
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +45 -2
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +161 -16
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-cjs/lib/chunk.js +30 -0
- package/dist-cjs/lib/chunk.js.map +2 -2
- package/dist-cjs/lib/diff.js.map +2 -2
- package/dist-cjs/lib/findMin.js.map +2 -2
- package/dist-cjs/lib/interval.js.map +2 -2
- package/dist-cjs/lib/protocol.js.map +2 -2
- package/dist-cjs/lib/server-types.js.map +1 -1
- package/dist-esm/index.d.mts +605 -75
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/ClientWebSocketAdapter.mjs +144 -0
- package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs +3 -0
- package/dist-esm/lib/RoomSession.mjs.map +2 -2
- package/dist-esm/lib/ServerSocketAdapter.mjs +23 -0
- package/dist-esm/lib/ServerSocketAdapter.mjs.map +2 -2
- package/dist-esm/lib/TLRemoteSyncError.mjs +8 -0
- package/dist-esm/lib/TLRemoteSyncError.mjs.map +2 -2
- package/dist-esm/lib/TLSocketRoom.mjs +280 -56
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +45 -2
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +161 -16
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/dist-esm/lib/chunk.mjs +30 -0
- package/dist-esm/lib/chunk.mjs.map +2 -2
- package/dist-esm/lib/diff.mjs.map +2 -2
- package/dist-esm/lib/findMin.mjs.map +2 -2
- package/dist-esm/lib/interval.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs.map +2 -2
- package/package.json +6 -6
- package/src/lib/ClientWebSocketAdapter.test.ts +712 -129
- package/src/lib/ClientWebSocketAdapter.ts +240 -9
- package/src/lib/RoomSession.test.ts +97 -0
- package/src/lib/RoomSession.ts +105 -3
- package/src/lib/ServerSocketAdapter.test.ts +228 -0
- package/src/lib/ServerSocketAdapter.ts +124 -5
- package/src/lib/TLRemoteSyncError.ts +50 -1
- package/src/lib/TLSocketRoom.ts +377 -60
- package/src/lib/TLSyncClient.test.ts +828 -0
- package/src/lib/TLSyncClient.ts +251 -26
- package/src/lib/TLSyncRoom.ts +284 -24
- package/src/lib/chunk.ts +72 -1
- package/src/lib/diff.ts +128 -14
- package/src/lib/findMin.ts +6 -0
- package/src/lib/interval.ts +40 -0
- package/src/lib/protocol.ts +185 -7
- package/src/lib/server-types.test.ts +44 -0
- package/src/lib/server-types.ts +45 -1
- package/src/test/TLSocketRoom.test.ts +438 -29
- package/src/test/chunk.test.ts +200 -3
- package/src/test/diff.test.ts +396 -1
package/src/lib/TLSyncClient.ts
CHANGED
|
@@ -24,90 +24,229 @@ import {
|
|
|
24
24
|
getTlsyncProtocolVersion,
|
|
25
25
|
} from './protocol'
|
|
26
26
|
|
|
27
|
-
/**
|
|
27
|
+
/**
|
|
28
|
+
* Function type for subscribing to events with a callback.
|
|
29
|
+
* Returns an unsubscribe function to clean up the listener.
|
|
30
|
+
*
|
|
31
|
+
* @param cb - Callback function that receives the event value
|
|
32
|
+
* @returns Function to call when you want to unsubscribe from the events
|
|
33
|
+
*
|
|
34
|
+
* @internal
|
|
35
|
+
*/
|
|
28
36
|
export type SubscribingFn<T> = (cb: (val: T) => void) => () => void
|
|
29
37
|
|
|
30
38
|
/**
|
|
31
|
-
*
|
|
32
|
-
* the connection is being
|
|
33
|
-
*
|
|
34
|
-
*
|
|
39
|
+
* WebSocket close code used by the server to signal a non-recoverable sync error.
|
|
40
|
+
* This close code indicates that the connection is being terminated due to an error
|
|
41
|
+
* that cannot be automatically recovered from, such as authentication failures,
|
|
42
|
+
* incompatible client versions, or invalid data.
|
|
35
43
|
*
|
|
36
44
|
* @example
|
|
37
45
|
* ```ts
|
|
46
|
+
* // Server-side: Close connection with specific error reason
|
|
38
47
|
* socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.NOT_FOUND)
|
|
39
|
-
* ```
|
|
40
48
|
*
|
|
41
|
-
*
|
|
49
|
+
* // Client-side: Handle the error in your sync error handler
|
|
50
|
+
* const syncClient = new TLSyncClient({
|
|
51
|
+
* // ... other config
|
|
52
|
+
* onSyncError: (reason) => {
|
|
53
|
+
* console.error('Sync failed:', reason) // Will receive 'NOT_FOUND'
|
|
54
|
+
* }
|
|
55
|
+
* })
|
|
56
|
+
* ```
|
|
42
57
|
*
|
|
43
58
|
* @public
|
|
44
59
|
*/
|
|
45
60
|
export const TLSyncErrorCloseEventCode = 4099 as const
|
|
46
61
|
|
|
47
62
|
/**
|
|
48
|
-
*
|
|
63
|
+
* Predefined reasons for server-initiated connection closures.
|
|
64
|
+
* These constants represent different error conditions that can cause
|
|
65
|
+
* the sync server to terminate a WebSocket connection.
|
|
66
|
+
*
|
|
67
|
+
* @example
|
|
68
|
+
* ```ts
|
|
69
|
+
* // Server usage
|
|
70
|
+
* if (!user.hasPermission(roomId)) {
|
|
71
|
+
* socket.close(TLSyncErrorCloseEventCode, TLSyncErrorCloseEventReason.FORBIDDEN)
|
|
72
|
+
* }
|
|
73
|
+
*
|
|
74
|
+
* // Client error handling
|
|
75
|
+
* syncClient.onSyncError((reason) => {
|
|
76
|
+
* switch (reason) {
|
|
77
|
+
* case TLSyncErrorCloseEventReason.NOT_FOUND:
|
|
78
|
+
* showError('Room does not exist')
|
|
79
|
+
* break
|
|
80
|
+
* case TLSyncErrorCloseEventReason.FORBIDDEN:
|
|
81
|
+
* showError('Access denied')
|
|
82
|
+
* break
|
|
83
|
+
* case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:
|
|
84
|
+
* showError('Please update your app')
|
|
85
|
+
* break
|
|
86
|
+
* }
|
|
87
|
+
* })
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
49
90
|
* @public
|
|
50
91
|
*/
|
|
51
92
|
export const TLSyncErrorCloseEventReason = {
|
|
93
|
+
/** Room or resource not found */
|
|
52
94
|
NOT_FOUND: 'NOT_FOUND',
|
|
95
|
+
/** User lacks permission to access the room */
|
|
53
96
|
FORBIDDEN: 'FORBIDDEN',
|
|
97
|
+
/** User authentication required or invalid */
|
|
54
98
|
NOT_AUTHENTICATED: 'NOT_AUTHENTICATED',
|
|
99
|
+
/** Unexpected server error occurred */
|
|
55
100
|
UNKNOWN_ERROR: 'UNKNOWN_ERROR',
|
|
101
|
+
/** Client protocol version too old */
|
|
56
102
|
CLIENT_TOO_OLD: 'CLIENT_TOO_OLD',
|
|
103
|
+
/** Server protocol version too old */
|
|
57
104
|
SERVER_TOO_OLD: 'SERVER_TOO_OLD',
|
|
105
|
+
/** Client sent invalid or corrupted record data */
|
|
58
106
|
INVALID_RECORD: 'INVALID_RECORD',
|
|
107
|
+
/** Client exceeded rate limits */
|
|
59
108
|
RATE_LIMITED: 'RATE_LIMITED',
|
|
109
|
+
/** Room has reached maximum capacity */
|
|
60
110
|
ROOM_FULL: 'ROOM_FULL',
|
|
61
111
|
} as const
|
|
62
112
|
/**
|
|
63
|
-
*
|
|
113
|
+
* Union type of all possible server connection close reasons.
|
|
114
|
+
* Represents the string values that can be passed when a server closes
|
|
115
|
+
* a sync connection due to an error condition.
|
|
116
|
+
*
|
|
64
117
|
* @public
|
|
65
118
|
*/
|
|
66
119
|
export type TLSyncErrorCloseEventReason =
|
|
67
120
|
(typeof TLSyncErrorCloseEventReason)[keyof typeof TLSyncErrorCloseEventReason]
|
|
68
121
|
|
|
69
122
|
/**
|
|
70
|
-
*
|
|
123
|
+
* Handler function for custom application messages sent through the sync protocol.
|
|
124
|
+
* These are user-defined messages that can be sent between clients via the sync server,
|
|
125
|
+
* separate from the standard document synchronization messages.
|
|
126
|
+
*
|
|
127
|
+
* @param data - Custom message payload (application-defined structure)
|
|
128
|
+
*
|
|
129
|
+
* @example
|
|
130
|
+
* ```ts
|
|
131
|
+
* const customMessageHandler: TLCustomMessageHandler = (data) => {
|
|
132
|
+
* if (data.type === 'user_joined') {
|
|
133
|
+
* console.log(`${data.username} joined the session`)
|
|
134
|
+
* showToast(`${data.username} is now collaborating`)
|
|
135
|
+
* }
|
|
136
|
+
* }
|
|
137
|
+
*
|
|
138
|
+
* const syncClient = new TLSyncClient({
|
|
139
|
+
* // ... other config
|
|
140
|
+
* onCustomMessageReceived: customMessageHandler
|
|
141
|
+
* })
|
|
142
|
+
* ```
|
|
143
|
+
*
|
|
71
144
|
* @public
|
|
72
145
|
*/
|
|
73
146
|
export type TLCustomMessageHandler = (this: null, data: any) => void
|
|
74
147
|
|
|
75
148
|
/**
|
|
149
|
+
* Event object describing changes in socket connection status.
|
|
150
|
+
* Contains either a basic status change or an error with details.
|
|
151
|
+
*
|
|
76
152
|
* @internal
|
|
77
153
|
*/
|
|
78
154
|
export type TlSocketStatusChangeEvent =
|
|
79
155
|
| {
|
|
156
|
+
/** Connection came online or went offline */
|
|
80
157
|
status: 'online' | 'offline'
|
|
81
158
|
}
|
|
82
159
|
| {
|
|
160
|
+
/** Connection encountered an error */
|
|
83
161
|
status: 'error'
|
|
162
|
+
/** Description of the error that occurred */
|
|
84
163
|
reason: string
|
|
85
164
|
}
|
|
86
|
-
/**
|
|
165
|
+
/**
|
|
166
|
+
* Callback function type for listening to socket status changes.
|
|
167
|
+
*
|
|
168
|
+
* @param params - Event object containing the new status and optional error details
|
|
169
|
+
*
|
|
170
|
+
* @internal
|
|
171
|
+
*/
|
|
87
172
|
export type TLSocketStatusListener = (params: TlSocketStatusChangeEvent) => void
|
|
88
173
|
|
|
89
|
-
/**
|
|
174
|
+
/**
|
|
175
|
+
* Possible connection states for a persistent client socket.
|
|
176
|
+
* Represents the current connectivity status between client and server.
|
|
177
|
+
*
|
|
178
|
+
* @internal
|
|
179
|
+
*/
|
|
90
180
|
export type TLPersistentClientSocketStatus = 'online' | 'offline' | 'error'
|
|
91
181
|
|
|
92
|
-
/** @internal */
|
|
93
|
-
export type TLPresenceMode = 'solo' | 'full'
|
|
94
182
|
/**
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
183
|
+
* Mode for handling presence information in sync sessions.
|
|
184
|
+
* Controls whether presence data (cursors, selections) is shared with other clients.
|
|
185
|
+
*
|
|
186
|
+
* @internal
|
|
187
|
+
*/
|
|
188
|
+
export type TLPresenceMode =
|
|
189
|
+
/** No presence sharing - client operates independently */
|
|
190
|
+
| 'solo'
|
|
191
|
+
/** Full presence sharing - cursors and selections visible to others */
|
|
192
|
+
| 'full'
|
|
193
|
+
/**
|
|
194
|
+
* Interface for persistent WebSocket-like connections used by TLSyncClient.
|
|
195
|
+
* Handles automatic reconnection and provides event-based communication with the sync server.
|
|
196
|
+
* Implementations should maintain connection resilience and handle network interruptions gracefully.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* class MySocketAdapter implements TLPersistentClientSocket {
|
|
201
|
+
* connectionStatus: 'offline' | 'online' | 'error' = 'offline'
|
|
202
|
+
*
|
|
203
|
+
* sendMessage(msg: TLSocketClientSentEvent) {
|
|
204
|
+
* if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
205
|
+
* this.ws.send(JSON.stringify(msg))
|
|
206
|
+
* }
|
|
207
|
+
* }
|
|
208
|
+
*
|
|
209
|
+
* onReceiveMessage = (callback) => {
|
|
210
|
+
* // Set up message listener and return cleanup function
|
|
211
|
+
* }
|
|
212
|
+
*
|
|
213
|
+
* restart() {
|
|
214
|
+
* this.disconnect()
|
|
215
|
+
* this.connect()
|
|
216
|
+
* }
|
|
217
|
+
* }
|
|
218
|
+
* ```
|
|
98
219
|
*
|
|
99
220
|
* @internal
|
|
100
221
|
*/
|
|
101
222
|
export interface TLPersistentClientSocket<R extends UnknownRecord = UnknownRecord> {
|
|
102
|
-
/**
|
|
223
|
+
/** Current connection state - online means actively connected and ready */
|
|
103
224
|
connectionStatus: 'online' | 'offline' | 'error'
|
|
104
|
-
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Send a protocol message to the sync server
|
|
228
|
+
* @param msg - Message to send (connect, push, ping, etc.)
|
|
229
|
+
*/
|
|
105
230
|
sendMessage(msg: TLSocketClientSentEvent<R>): void
|
|
106
|
-
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Subscribe to messages received from the server
|
|
234
|
+
* @param callback - Function called for each received message
|
|
235
|
+
* @returns Cleanup function to remove the listener
|
|
236
|
+
*/
|
|
107
237
|
onReceiveMessage: SubscribingFn<TLSocketServerSentEvent<R>>
|
|
108
|
-
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Subscribe to connection status changes
|
|
241
|
+
* @param callback - Function called when connection status changes
|
|
242
|
+
* @returns Cleanup function to remove the listener
|
|
243
|
+
*/
|
|
109
244
|
onStatusChange: SubscribingFn<TlSocketStatusChangeEvent>
|
|
110
|
-
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Force a connection restart (disconnect then reconnect)
|
|
248
|
+
* Used for error recovery or when connection health checks fail
|
|
249
|
+
*/
|
|
111
250
|
restart(): void
|
|
112
251
|
}
|
|
113
252
|
|
|
@@ -117,9 +256,61 @@ const MAX_TIME_TO_WAIT_FOR_SERVER_INTERACTION_BEFORE_RESETTING_CONNECTION = PING
|
|
|
117
256
|
// Should connect support chunking the response to allow for large payloads?
|
|
118
257
|
|
|
119
258
|
/**
|
|
120
|
-
*
|
|
259
|
+
* Main client-side synchronization engine for collaborative tldraw applications.
|
|
121
260
|
*
|
|
122
|
-
*
|
|
261
|
+
* TLSyncClient manages bidirectional synchronization between a local tldraw Store
|
|
262
|
+
* and a remote sync server. It uses an optimistic update model where local changes
|
|
263
|
+
* are immediately applied for responsive UI, then sent to the server for validation
|
|
264
|
+
* and distribution to other clients.
|
|
265
|
+
*
|
|
266
|
+
* The synchronization follows a git-like push/pull/rebase model:
|
|
267
|
+
* - **Push**: Local changes are sent to server as diff operations
|
|
268
|
+
* - **Pull**: Server changes are received and applied locally
|
|
269
|
+
* - **Rebase**: Conflicting changes are resolved by undoing local changes,
|
|
270
|
+
* applying server changes, then re-applying local changes on top
|
|
271
|
+
*
|
|
272
|
+
* @example
|
|
273
|
+
* ```ts
|
|
274
|
+
* import { TLSyncClient, ClientWebSocketAdapter } from '@tldraw/sync-core'
|
|
275
|
+
* import { createTLStore } from '@tldraw/store'
|
|
276
|
+
*
|
|
277
|
+
* // Create store and socket
|
|
278
|
+
* const store = createTLStore({ schema: mySchema })
|
|
279
|
+
* const socket = new ClientWebSocketAdapter('ws://localhost:3000/sync')
|
|
280
|
+
*
|
|
281
|
+
* // Create sync client
|
|
282
|
+
* const syncClient = new TLSyncClient({
|
|
283
|
+
* store,
|
|
284
|
+
* socket,
|
|
285
|
+
* presence: atom(null),
|
|
286
|
+
* onLoad: () => console.log('Connected and loaded'),
|
|
287
|
+
* onSyncError: (reason) => console.error('Sync failed:', reason)
|
|
288
|
+
* })
|
|
289
|
+
*
|
|
290
|
+
* // Changes to store are now automatically synchronized
|
|
291
|
+
* store.put([{ id: 'shape1', type: 'geo', x: 100, y: 100 }])
|
|
292
|
+
* ```
|
|
293
|
+
*
|
|
294
|
+
* @example
|
|
295
|
+
* ```ts
|
|
296
|
+
* // Advanced usage with presence and custom messages
|
|
297
|
+
* const syncClient = new TLSyncClient({
|
|
298
|
+
* store,
|
|
299
|
+
* socket,
|
|
300
|
+
* presence: atom({ cursor: { x: 0, y: 0 }, userName: 'Alice' }),
|
|
301
|
+
* presenceMode: atom('full'),
|
|
302
|
+
* onCustomMessageReceived: (data) => {
|
|
303
|
+
* if (data.type === 'chat') {
|
|
304
|
+
* showChatMessage(data.message, data.from)
|
|
305
|
+
* }
|
|
306
|
+
* },
|
|
307
|
+
* onAfterConnect: (client, { isReadonly }) => {
|
|
308
|
+
* if (isReadonly) {
|
|
309
|
+
* showNotification('Connected in read-only mode')
|
|
310
|
+
* }
|
|
311
|
+
* }
|
|
312
|
+
* })
|
|
313
|
+
* ```
|
|
123
314
|
*
|
|
124
315
|
* @internal
|
|
125
316
|
*/
|
|
@@ -167,8 +358,13 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
|
|
167
358
|
private clientClock = 0
|
|
168
359
|
|
|
169
360
|
/**
|
|
170
|
-
*
|
|
171
|
-
*
|
|
361
|
+
* Callback executed immediately after successful connection to sync room.
|
|
362
|
+
* Use this to perform any post-connection setup required for your application,
|
|
363
|
+
* such as initializing default content or updating UI state.
|
|
364
|
+
*
|
|
365
|
+
* @param self - The TLSyncClient instance that connected
|
|
366
|
+
* @param details - Connection details
|
|
367
|
+
* - isReadonly - Whether the connection is in read-only mode
|
|
172
368
|
*/
|
|
173
369
|
public readonly onAfterConnect?: (self: this, details: { isReadonly: boolean }) => void
|
|
174
370
|
|
|
@@ -186,6 +382,22 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
|
|
186
382
|
|
|
187
383
|
didCancel?: () => boolean
|
|
188
384
|
|
|
385
|
+
/**
|
|
386
|
+
* Creates a new TLSyncClient instance to manage synchronization with a remote server.
|
|
387
|
+
*
|
|
388
|
+
* @param config - Configuration object for the sync client
|
|
389
|
+
* - store - The local tldraw store to synchronize
|
|
390
|
+
* - socket - WebSocket adapter for server communication
|
|
391
|
+
* - presence - Reactive signal containing current user's presence data
|
|
392
|
+
* - presenceMode - Optional signal controlling presence sharing (defaults to 'full')
|
|
393
|
+
* - onLoad - Callback fired when initial sync completes successfully
|
|
394
|
+
* - onSyncError - Callback fired when sync fails with error reason
|
|
395
|
+
* - onCustomMessageReceived - Optional handler for custom messages
|
|
396
|
+
* - onAfterConnect - Optional callback fired after successful connection
|
|
397
|
+
* - self - The TLSyncClient instance
|
|
398
|
+
* - details - Connection details including readonly status
|
|
399
|
+
* - didCancel - Optional function to check if sync should be cancelled
|
|
400
|
+
*/
|
|
189
401
|
constructor(config: {
|
|
190
402
|
store: S
|
|
191
403
|
socket: TLPersistentClientSocket<R>
|
|
@@ -469,6 +681,19 @@ export class TLSyncClient<R extends UnknownRecord, S extends Store<R> = Store<R>
|
|
|
469
681
|
}
|
|
470
682
|
}
|
|
471
683
|
|
|
684
|
+
/**
|
|
685
|
+
* Closes the sync client and cleans up all resources.
|
|
686
|
+
*
|
|
687
|
+
* Call this method when you no longer need the sync client to prevent
|
|
688
|
+
* memory leaks and close the WebSocket connection. After calling close(),
|
|
689
|
+
* the client cannot be reused.
|
|
690
|
+
*
|
|
691
|
+
* @example
|
|
692
|
+
* ```ts
|
|
693
|
+
* // Clean shutdown
|
|
694
|
+
* syncClient.close()
|
|
695
|
+
* ```
|
|
696
|
+
*/
|
|
472
697
|
close() {
|
|
473
698
|
this.debug('closing')
|
|
474
699
|
this.disposables.forEach((dispose) => dispose())
|