@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
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
import { UnknownRecord } from '@tldraw/store'
|
|
2
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
3
|
+
import { ServerSocketAdapter, WebSocketMinimal } from './ServerSocketAdapter'
|
|
4
|
+
import { TLSocketServerSentEvent } from './protocol'
|
|
5
|
+
|
|
6
|
+
// Mock WebSocket implementations for testing different scenarios
|
|
7
|
+
class MockWebSocket implements WebSocketMinimal {
|
|
8
|
+
readyState: number = 1 // OPEN by default
|
|
9
|
+
send = vi.fn()
|
|
10
|
+
close = vi.fn()
|
|
11
|
+
addEventListener = vi.fn()
|
|
12
|
+
removeEventListener = vi.fn()
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class MinimalMockWebSocket implements WebSocketMinimal {
|
|
16
|
+
readyState: number = 1
|
|
17
|
+
send = vi.fn()
|
|
18
|
+
close = vi.fn()
|
|
19
|
+
// No addEventListener/removeEventListener
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe('ServerSocketAdapter', () => {
|
|
23
|
+
describe('sendMessage', () => {
|
|
24
|
+
it('should JSON stringify and send the message', () => {
|
|
25
|
+
const mockWs = new MockWebSocket()
|
|
26
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs })
|
|
27
|
+
|
|
28
|
+
const message: TLSocketServerSentEvent<UnknownRecord> = {
|
|
29
|
+
type: 'connect',
|
|
30
|
+
hydrationType: 'wipe_all',
|
|
31
|
+
connectRequestId: 'test-request-123',
|
|
32
|
+
protocolVersion: 1,
|
|
33
|
+
schema: { schemaVersion: 1, storeVersion: 0, recordVersions: {} },
|
|
34
|
+
diff: {},
|
|
35
|
+
serverClock: 0,
|
|
36
|
+
isReadonly: false,
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
adapter.sendMessage(message)
|
|
40
|
+
|
|
41
|
+
expect(mockWs.send).toHaveBeenCalledTimes(1)
|
|
42
|
+
expect(mockWs.send).toHaveBeenCalledWith(JSON.stringify(message))
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('should call onBeforeSendMessage callback when provided', () => {
|
|
46
|
+
const mockWs = new MockWebSocket()
|
|
47
|
+
const onBeforeSendMessage = vi.fn()
|
|
48
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs, onBeforeSendMessage })
|
|
49
|
+
|
|
50
|
+
const message: TLSocketServerSentEvent<UnknownRecord> = {
|
|
51
|
+
type: 'data',
|
|
52
|
+
data: [{ type: 'patch', diff: {}, serverClock: 1 }],
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
adapter.sendMessage(message)
|
|
56
|
+
|
|
57
|
+
expect(onBeforeSendMessage).toHaveBeenCalledTimes(1)
|
|
58
|
+
expect(onBeforeSendMessage).toHaveBeenCalledWith(message, JSON.stringify(message))
|
|
59
|
+
expect(mockWs.send).toHaveBeenCalledTimes(1)
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
it('should call onBeforeSendMessage before sending to WebSocket', () => {
|
|
63
|
+
const mockWs = new MockWebSocket()
|
|
64
|
+
const callOrder: string[] = []
|
|
65
|
+
|
|
66
|
+
const onBeforeSendMessage = vi.fn(() => {
|
|
67
|
+
callOrder.push('callback')
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
mockWs.send.mockImplementation(() => {
|
|
71
|
+
callOrder.push('websocket')
|
|
72
|
+
})
|
|
73
|
+
|
|
74
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs, onBeforeSendMessage })
|
|
75
|
+
|
|
76
|
+
const message: TLSocketServerSentEvent<UnknownRecord> = {
|
|
77
|
+
type: 'pong',
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
adapter.sendMessage(message)
|
|
81
|
+
|
|
82
|
+
expect(callOrder).toEqual(['callback', 'websocket'])
|
|
83
|
+
})
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
describe('close method', () => {
|
|
87
|
+
it('should call close on the underlying WebSocket with no parameters', () => {
|
|
88
|
+
const mockWs = new MockWebSocket()
|
|
89
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs })
|
|
90
|
+
|
|
91
|
+
adapter.close()
|
|
92
|
+
|
|
93
|
+
expect(mockWs.close).toHaveBeenCalledTimes(1)
|
|
94
|
+
expect(mockWs.close).toHaveBeenCalledWith(undefined, undefined)
|
|
95
|
+
})
|
|
96
|
+
|
|
97
|
+
it('should call close on the underlying WebSocket with code only', () => {
|
|
98
|
+
const mockWs = new MockWebSocket()
|
|
99
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs })
|
|
100
|
+
|
|
101
|
+
const closeCode = 1000
|
|
102
|
+
adapter.close(closeCode)
|
|
103
|
+
|
|
104
|
+
expect(mockWs.close).toHaveBeenCalledTimes(1)
|
|
105
|
+
expect(mockWs.close).toHaveBeenCalledWith(closeCode, undefined)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
it('should call close on the underlying WebSocket with code and reason', () => {
|
|
109
|
+
const mockWs = new MockWebSocket()
|
|
110
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs })
|
|
111
|
+
|
|
112
|
+
const closeCode = 1001
|
|
113
|
+
const closeReason = 'Going away'
|
|
114
|
+
adapter.close(closeCode, closeReason)
|
|
115
|
+
|
|
116
|
+
expect(mockWs.close).toHaveBeenCalledTimes(1)
|
|
117
|
+
expect(mockWs.close).toHaveBeenCalledWith(closeCode, closeReason)
|
|
118
|
+
})
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
describe('Error handling', () => {
|
|
122
|
+
it('should handle WebSocket send errors gracefully', () => {
|
|
123
|
+
const mockWs = new MockWebSocket()
|
|
124
|
+
mockWs.send.mockImplementation(() => {
|
|
125
|
+
throw new Error('WebSocket send failed')
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs })
|
|
129
|
+
|
|
130
|
+
const message: TLSocketServerSentEvent<UnknownRecord> = {
|
|
131
|
+
type: 'pong',
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
expect(() => adapter.sendMessage(message)).toThrow('WebSocket send failed')
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should handle WebSocket close errors gracefully', () => {
|
|
138
|
+
const mockWs = new MockWebSocket()
|
|
139
|
+
mockWs.close.mockImplementation(() => {
|
|
140
|
+
throw new Error('WebSocket close failed')
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs })
|
|
144
|
+
|
|
145
|
+
expect(() => adapter.close()).toThrow('WebSocket close failed')
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
it('should handle onBeforeSendMessage callback errors', () => {
|
|
149
|
+
const mockWs = new MockWebSocket()
|
|
150
|
+
const faultyCallback = vi.fn(() => {
|
|
151
|
+
throw new Error('Callback error')
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs, onBeforeSendMessage: faultyCallback })
|
|
155
|
+
|
|
156
|
+
const message: TLSocketServerSentEvent<UnknownRecord> = {
|
|
157
|
+
type: 'pong',
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
expect(() => adapter.sendMessage(message)).toThrow('Callback error')
|
|
161
|
+
expect(faultyCallback).toHaveBeenCalled()
|
|
162
|
+
// WebSocket send should not be called if callback throws
|
|
163
|
+
expect(mockWs.send).not.toHaveBeenCalled()
|
|
164
|
+
})
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
describe('Integration scenarios', () => {
|
|
168
|
+
it('should work with different WebSocket implementations', () => {
|
|
169
|
+
const scenarios = [
|
|
170
|
+
{ name: 'Standard WebSocket', ws: new MockWebSocket() },
|
|
171
|
+
{ name: 'Minimal WebSocket', ws: new MinimalMockWebSocket() },
|
|
172
|
+
]
|
|
173
|
+
|
|
174
|
+
scenarios.forEach(({ name, ws }) => {
|
|
175
|
+
const adapter = new ServerSocketAdapter({ ws })
|
|
176
|
+
|
|
177
|
+
expect(adapter, `${name} should create adapter`).toBeInstanceOf(ServerSocketAdapter)
|
|
178
|
+
|
|
179
|
+
const message: TLSocketServerSentEvent<UnknownRecord> = {
|
|
180
|
+
type: 'pong',
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
expect(
|
|
184
|
+
() => adapter.sendMessage(message),
|
|
185
|
+
`${name} should handle sendMessage`
|
|
186
|
+
).not.toThrow()
|
|
187
|
+
expect(() => adapter.close(), `${name} should handle close`).not.toThrow()
|
|
188
|
+
})
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('should handle rapid message sending', () => {
|
|
192
|
+
const mockWs = new MockWebSocket()
|
|
193
|
+
const onBeforeSendMessage = vi.fn()
|
|
194
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs, onBeforeSendMessage })
|
|
195
|
+
|
|
196
|
+
// Send multiple messages rapidly
|
|
197
|
+
for (let i = 0; i < 100; i++) {
|
|
198
|
+
const message: TLSocketServerSentEvent<UnknownRecord> = {
|
|
199
|
+
type: 'pong',
|
|
200
|
+
}
|
|
201
|
+
adapter.sendMessage(message)
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
expect(mockWs.send).toHaveBeenCalledTimes(100)
|
|
205
|
+
expect(onBeforeSendMessage).toHaveBeenCalledTimes(100)
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
it('should maintain consistent behavior across state changes', () => {
|
|
209
|
+
const mockWs = new MockWebSocket()
|
|
210
|
+
const adapter = new ServerSocketAdapter({ ws: mockWs })
|
|
211
|
+
|
|
212
|
+
// Test behavior when WebSocket changes state
|
|
213
|
+
expect(adapter.isOpen).toBe(true)
|
|
214
|
+
|
|
215
|
+
mockWs.readyState = 0 // CONNECTING
|
|
216
|
+
expect(adapter.isOpen).toBe(false)
|
|
217
|
+
|
|
218
|
+
mockWs.readyState = 2 // CLOSING
|
|
219
|
+
expect(adapter.isOpen).toBe(false)
|
|
220
|
+
|
|
221
|
+
mockWs.readyState = 3 // CLOSED
|
|
222
|
+
expect(adapter.isOpen).toBe(false)
|
|
223
|
+
|
|
224
|
+
mockWs.readyState = 1 // OPEN
|
|
225
|
+
expect(adapter.isOpen).toBe(true)
|
|
226
|
+
})
|
|
227
|
+
})
|
|
228
|
+
})
|
|
@@ -3,49 +3,168 @@ import { TLRoomSocket } from './TLSyncRoom'
|
|
|
3
3
|
import { TLSocketServerSentEvent } from './protocol'
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
|
-
* Minimal server-side WebSocket interface that is compatible with
|
|
6
|
+
* Minimal server-side WebSocket interface that is compatible with various WebSocket implementations.
|
|
7
|
+
* This interface abstracts over different WebSocket libraries and platforms to provide a consistent
|
|
8
|
+
* API for the ServerSocketAdapter.
|
|
7
9
|
*
|
|
8
|
-
*
|
|
9
|
-
* - The
|
|
10
|
+
* Supports:
|
|
11
|
+
* - The standard WebSocket interface (Cloudflare, Deno, some Node.js setups)
|
|
12
|
+
* - The 'ws' WebSocket interface (Node.js ws library)
|
|
10
13
|
* - The Bun.serve socket implementation
|
|
11
14
|
*
|
|
12
15
|
* @public
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* // Standard WebSocket
|
|
19
|
+
* const standardWs: WebSocketMinimal = new WebSocket('ws://localhost:8080')
|
|
20
|
+
*
|
|
21
|
+
* // Node.js 'ws' library WebSocket
|
|
22
|
+
* import WebSocket from 'ws'
|
|
23
|
+
* const nodeWs: WebSocketMinimal = new WebSocket('ws://localhost:8080')
|
|
24
|
+
*
|
|
25
|
+
* // Bun WebSocket (in server context)
|
|
26
|
+
* // const bunWs: WebSocketMinimal = server.upgrade(request)
|
|
27
|
+
* ```
|
|
13
28
|
*/
|
|
14
29
|
export interface WebSocketMinimal {
|
|
30
|
+
/**
|
|
31
|
+
* Optional method to add event listeners for WebSocket events.
|
|
32
|
+
* Not all WebSocket implementations provide this method.
|
|
33
|
+
*
|
|
34
|
+
* @param type - The event type to listen for
|
|
35
|
+
* @param listener - The event handler function
|
|
36
|
+
*/
|
|
15
37
|
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
16
38
|
addEventListener?: (type: 'message' | 'close' | 'error', listener: (event: any) => void) => void
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Optional method to remove event listeners for WebSocket events.
|
|
42
|
+
* Not all WebSocket implementations provide this method.
|
|
43
|
+
*
|
|
44
|
+
* @param type - The event type to stop listening for
|
|
45
|
+
* @param listener - The event handler function to remove
|
|
46
|
+
*/
|
|
17
47
|
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
18
48
|
removeEventListener?: (
|
|
19
49
|
type: 'message' | 'close' | 'error',
|
|
20
50
|
listener: (event: any) => void
|
|
21
51
|
) => void
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Sends a string message through the WebSocket connection.
|
|
55
|
+
*
|
|
56
|
+
* @param data - The string data to send
|
|
57
|
+
*/
|
|
22
58
|
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
23
59
|
send: (data: string) => void
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Closes the WebSocket connection.
|
|
63
|
+
*
|
|
64
|
+
* @param code - Optional close code (default: 1000 for normal closure)
|
|
65
|
+
* @param reason - Optional human-readable close reason
|
|
66
|
+
*/
|
|
24
67
|
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
25
68
|
close: (code?: number, reason?: string) => void
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* The current state of the WebSocket connection.
|
|
72
|
+
* - 0: CONNECTING
|
|
73
|
+
* - 1: OPEN
|
|
74
|
+
* - 2: CLOSING
|
|
75
|
+
* - 3: CLOSED
|
|
76
|
+
*/
|
|
26
77
|
readyState: number
|
|
27
78
|
}
|
|
28
79
|
|
|
29
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* Configuration options for creating a ServerSocketAdapter instance.
|
|
82
|
+
*
|
|
83
|
+
* @internal
|
|
84
|
+
*/
|
|
30
85
|
export interface ServerSocketAdapterOptions<R extends UnknownRecord> {
|
|
86
|
+
/** The underlying WebSocket connection to wrap */
|
|
31
87
|
readonly ws: WebSocketMinimal
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Optional callback invoked before each message is sent to the client.
|
|
91
|
+
* Useful for logging, metrics, or message transformation.
|
|
92
|
+
*
|
|
93
|
+
* @param msg - The message object being sent
|
|
94
|
+
* @param stringified - The JSON stringified version of the message
|
|
95
|
+
*/
|
|
32
96
|
// eslint-disable-next-line @typescript-eslint/method-signature-style
|
|
33
97
|
readonly onBeforeSendMessage?: (msg: TLSocketServerSentEvent<R>, stringified: string) => void
|
|
34
98
|
}
|
|
35
99
|
|
|
36
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Server-side adapter that wraps various WebSocket implementations to provide a consistent
|
|
102
|
+
* TLRoomSocket interface for the TLSyncRoom. This adapter handles the differences between
|
|
103
|
+
* WebSocket libraries and platforms, allowing sync-core to work across different server
|
|
104
|
+
* environments.
|
|
105
|
+
*
|
|
106
|
+
* The adapter implements the TLRoomSocket interface, providing methods for sending messages,
|
|
107
|
+
* checking connection status, and closing connections.
|
|
108
|
+
*
|
|
109
|
+
* @internal
|
|
110
|
+
* @example
|
|
111
|
+
* ```ts
|
|
112
|
+
* import { ServerSocketAdapter } from '@tldraw/sync-core'
|
|
113
|
+
*
|
|
114
|
+
* // Wrap a standard WebSocket
|
|
115
|
+
* const adapter = new ServerSocketAdapter({
|
|
116
|
+
* ws: webSocketConnection,
|
|
117
|
+
* onBeforeSendMessage: (msg, json) => {
|
|
118
|
+
* console.log('Sending:', msg.type)
|
|
119
|
+
* }
|
|
120
|
+
* })
|
|
121
|
+
*
|
|
122
|
+
* // Use with TLSyncRoom
|
|
123
|
+
* room.handleNewSession({
|
|
124
|
+
* sessionId: 'session-123',
|
|
125
|
+
* socket: adapter,
|
|
126
|
+
* isReadonly: false
|
|
127
|
+
* })
|
|
128
|
+
* ```
|
|
129
|
+
*/
|
|
37
130
|
export class ServerSocketAdapter<R extends UnknownRecord> implements TLRoomSocket<R> {
|
|
131
|
+
/**
|
|
132
|
+
* Creates a new ServerSocketAdapter instance.
|
|
133
|
+
*
|
|
134
|
+
* opts - Configuration options for the adapter
|
|
135
|
+
*/
|
|
38
136
|
constructor(public readonly opts: ServerSocketAdapterOptions<R>) {}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Checks if the underlying WebSocket connection is currently open and ready to send messages.
|
|
140
|
+
*
|
|
141
|
+
* @returns True if the connection is open (readyState === 1), false otherwise
|
|
142
|
+
*/
|
|
39
143
|
// eslint-disable-next-line no-restricted-syntax
|
|
40
144
|
get isOpen(): boolean {
|
|
41
145
|
return this.opts.ws.readyState === 1 // ready state open
|
|
42
146
|
}
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Sends a sync protocol message to the connected client. The message is JSON stringified
|
|
150
|
+
* before being sent through the WebSocket. If configured, the onBeforeSendMessage callback
|
|
151
|
+
* is invoked before sending.
|
|
152
|
+
*
|
|
153
|
+
* @param msg - The sync protocol message to send
|
|
154
|
+
*/
|
|
43
155
|
// see TLRoomSocket for details on why this accepts a union and not just arrays
|
|
44
156
|
sendMessage(msg: TLSocketServerSentEvent<R>) {
|
|
45
157
|
const message = JSON.stringify(msg)
|
|
46
158
|
this.opts.onBeforeSendMessage?.(msg, message)
|
|
47
159
|
this.opts.ws.send(message)
|
|
48
160
|
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Closes the WebSocket connection with an optional close code and reason.
|
|
164
|
+
*
|
|
165
|
+
* @param code - Optional close code (default: 1000 for normal closure)
|
|
166
|
+
* @param reason - Optional human-readable reason for closing
|
|
167
|
+
*/
|
|
49
168
|
close(code?: number, reason?: string) {
|
|
50
169
|
this.opts.ws.close(code, reason)
|
|
51
170
|
}
|
|
@@ -1,8 +1,57 @@
|
|
|
1
1
|
import { TLSyncErrorCloseEventReason } from './TLSyncClient'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Specialized error class for synchronization-related failures in tldraw collaboration.
|
|
5
|
+
*
|
|
6
|
+
* This error is thrown when the sync client encounters fatal errors that prevent
|
|
7
|
+
* successful synchronization with the server. It captures both the error message
|
|
8
|
+
* and the specific reason code that triggered the failure.
|
|
9
|
+
*
|
|
10
|
+
* Common scenarios include schema version mismatches, authentication failures,
|
|
11
|
+
* network connectivity issues, and server-side validation errors.
|
|
12
|
+
*
|
|
13
|
+
* @example
|
|
14
|
+
* ```ts
|
|
15
|
+
* import { TLRemoteSyncError, TLSyncErrorCloseEventReason } from '@tldraw/sync-core'
|
|
16
|
+
*
|
|
17
|
+
* // Handle sync errors in your application
|
|
18
|
+
* syncClient.onSyncError((error) => {
|
|
19
|
+
* if (error instanceof TLRemoteSyncError) {
|
|
20
|
+
* switch (error.reason) {
|
|
21
|
+
* case TLSyncErrorCloseEventReason.NOT_AUTHENTICATED:
|
|
22
|
+
* // Redirect user to login
|
|
23
|
+
* break
|
|
24
|
+
* case TLSyncErrorCloseEventReason.CLIENT_TOO_OLD:
|
|
25
|
+
* // Show update required message
|
|
26
|
+
* break
|
|
27
|
+
* default:
|
|
28
|
+
* console.error('Sync error:', error.message)
|
|
29
|
+
* }
|
|
30
|
+
* }
|
|
31
|
+
* })
|
|
32
|
+
* ```
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* // Server-side: throwing a sync error
|
|
37
|
+
* if (!hasPermission(userId, roomId)) {
|
|
38
|
+
* throw new TLRemoteSyncError(TLSyncErrorCloseEventReason.FORBIDDEN)
|
|
39
|
+
* }
|
|
40
|
+
* ```
|
|
41
|
+
*
|
|
42
|
+
* @public
|
|
43
|
+
*/
|
|
4
44
|
export class TLRemoteSyncError extends Error {
|
|
5
45
|
override name = 'RemoteSyncError'
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates a new TLRemoteSyncError with the specified reason.
|
|
49
|
+
*
|
|
50
|
+
* reason - The specific reason code or custom string describing why the sync failed.
|
|
51
|
+
* When using predefined reasons from TLSyncErrorCloseEventReason, the client
|
|
52
|
+
* can handle specific error types appropriately. Custom strings allow for
|
|
53
|
+
* application-specific error details.
|
|
54
|
+
*/
|
|
6
55
|
constructor(public readonly reason: TLSyncErrorCloseEventReason | string) {
|
|
7
56
|
super(`sync error: ${reason}`)
|
|
8
57
|
}
|