@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/diff.ts
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
import { RecordsDiff, UnknownRecord } from '@tldraw/store'
|
|
2
2
|
import { isEqual, objectMapEntries, objectMapValues } from '@tldraw/utils'
|
|
3
3
|
|
|
4
|
-
/**
|
|
4
|
+
/**
|
|
5
|
+
* Constants representing the types of operations that can be applied to records in network diffs.
|
|
6
|
+
* These operations describe how a record has been modified during synchronization.
|
|
7
|
+
*
|
|
8
|
+
* @internal
|
|
9
|
+
*/
|
|
5
10
|
export const RecordOpType = {
|
|
6
11
|
Put: 'put',
|
|
7
12
|
Patch: 'patch',
|
|
8
13
|
Remove: 'remove',
|
|
9
14
|
} as const
|
|
10
15
|
|
|
11
|
-
/**
|
|
16
|
+
/**
|
|
17
|
+
* Union type of all possible record operation types.
|
|
18
|
+
*
|
|
19
|
+
* @internal
|
|
20
|
+
*/
|
|
12
21
|
export type RecordOpType = (typeof RecordOpType)[keyof typeof RecordOpType]
|
|
13
22
|
|
|
14
|
-
/**
|
|
23
|
+
/**
|
|
24
|
+
* Represents a single operation to be applied to a record during synchronization.
|
|
25
|
+
*
|
|
26
|
+
* @param R - The record type being operated on
|
|
27
|
+
*
|
|
28
|
+
* @internal
|
|
29
|
+
*/
|
|
15
30
|
export type RecordOp<R extends UnknownRecord> =
|
|
16
31
|
| [typeof RecordOpType.Put, R]
|
|
17
32
|
| [typeof RecordOpType.Patch, ObjectDiff]
|
|
@@ -32,8 +47,29 @@ export interface NetworkDiff<R extends UnknownRecord> {
|
|
|
32
47
|
|
|
33
48
|
/**
|
|
34
49
|
* Converts a (reversible, verbose) RecordsDiff into a (non-reversible, concise) NetworkDiff
|
|
50
|
+
* suitable for transmission over the network. This function optimizes the diff representation
|
|
51
|
+
* for minimal bandwidth usage while maintaining all necessary change information.
|
|
52
|
+
*
|
|
53
|
+
* @param diff - The RecordsDiff containing added, updated, and removed records
|
|
54
|
+
* @returns A compact NetworkDiff for network transmission, or null if no changes exist
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```ts
|
|
58
|
+
* const recordsDiff = {
|
|
59
|
+
* added: { 'shape:1': newShape },
|
|
60
|
+
* updated: { 'shape:2': [oldShape, updatedShape] },
|
|
61
|
+
* removed: { 'shape:3': removedShape }
|
|
62
|
+
* }
|
|
63
|
+
*
|
|
64
|
+
* const networkDiff = getNetworkDiff(recordsDiff)
|
|
65
|
+
* // Returns: {
|
|
66
|
+
* // 'shape:1': ['put', newShape],
|
|
67
|
+
* // 'shape:2': ['patch', { x: ['put', 100] }],
|
|
68
|
+
* // 'shape:3': ['remove']
|
|
69
|
+
* // }
|
|
70
|
+
* ```
|
|
35
71
|
*
|
|
36
|
-
|
|
72
|
+
* @internal
|
|
37
73
|
*/
|
|
38
74
|
export function getNetworkDiff<R extends UnknownRecord>(
|
|
39
75
|
diff: RecordsDiff<R>
|
|
@@ -61,34 +97,90 @@ export function getNetworkDiff<R extends UnknownRecord>(
|
|
|
61
97
|
return res
|
|
62
98
|
}
|
|
63
99
|
|
|
64
|
-
/**
|
|
100
|
+
/**
|
|
101
|
+
* Constants representing the types of operations that can be applied to individual values
|
|
102
|
+
* within object diffs. These operations describe how object properties have changed.
|
|
103
|
+
*
|
|
104
|
+
* @internal
|
|
105
|
+
*/
|
|
65
106
|
export const ValueOpType = {
|
|
66
107
|
Put: 'put',
|
|
67
108
|
Delete: 'delete',
|
|
68
109
|
Append: 'append',
|
|
69
110
|
Patch: 'patch',
|
|
70
111
|
} as const
|
|
71
|
-
/**
|
|
112
|
+
/**
|
|
113
|
+
* Union type of all possible value operation types.
|
|
114
|
+
*
|
|
115
|
+
* @internal
|
|
116
|
+
*/
|
|
72
117
|
export type ValueOpType = (typeof ValueOpType)[keyof typeof ValueOpType]
|
|
73
118
|
|
|
74
|
-
/**
|
|
119
|
+
/**
|
|
120
|
+
* Operation that replaces a value entirely with a new value.
|
|
121
|
+
*
|
|
122
|
+
* @internal
|
|
123
|
+
*/
|
|
75
124
|
export type PutOp = [type: typeof ValueOpType.Put, value: unknown]
|
|
76
|
-
/**
|
|
125
|
+
/**
|
|
126
|
+
* Operation that appends new values to the end of an array.
|
|
127
|
+
*
|
|
128
|
+
* @internal
|
|
129
|
+
*/
|
|
77
130
|
export type AppendOp = [type: typeof ValueOpType.Append, values: unknown[], offset: number]
|
|
78
|
-
/**
|
|
131
|
+
/**
|
|
132
|
+
* Operation that applies a nested diff to an object or array.
|
|
133
|
+
*
|
|
134
|
+
* @internal
|
|
135
|
+
*/
|
|
79
136
|
export type PatchOp = [type: typeof ValueOpType.Patch, diff: ObjectDiff]
|
|
80
|
-
/**
|
|
137
|
+
/**
|
|
138
|
+
* Operation that removes a property from an object.
|
|
139
|
+
*
|
|
140
|
+
* @internal
|
|
141
|
+
*/
|
|
81
142
|
export type DeleteOp = [type: typeof ValueOpType.Delete]
|
|
82
143
|
|
|
83
|
-
/**
|
|
144
|
+
/**
|
|
145
|
+
* Union type representing any value operation that can be applied during diffing.
|
|
146
|
+
*
|
|
147
|
+
* @internal
|
|
148
|
+
*/
|
|
84
149
|
export type ValueOp = PutOp | AppendOp | PatchOp | DeleteOp
|
|
85
150
|
|
|
86
|
-
/**
|
|
151
|
+
/**
|
|
152
|
+
* Represents the differences between two objects as a mapping of property names
|
|
153
|
+
* to the operations needed to transform one object into another.
|
|
154
|
+
*
|
|
155
|
+
* @internal
|
|
156
|
+
*/
|
|
87
157
|
export interface ObjectDiff {
|
|
88
158
|
[k: string]: ValueOp
|
|
89
159
|
}
|
|
90
160
|
|
|
91
|
-
/**
|
|
161
|
+
/**
|
|
162
|
+
* Computes the difference between two record objects, generating an ObjectDiff
|
|
163
|
+
* that describes how to transform the previous record into the next record.
|
|
164
|
+
* This function is optimized for tldraw records and treats 'props' as a nested object.
|
|
165
|
+
*
|
|
166
|
+
* @param prev - The previous version of the record
|
|
167
|
+
* @param next - The next version of the record
|
|
168
|
+
* @returns An ObjectDiff describing the changes, or null if no changes exist
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* ```ts
|
|
172
|
+
* const oldShape = { id: 'shape:1', x: 100, y: 200, props: { color: 'red' } }
|
|
173
|
+
* const newShape = { id: 'shape:1', x: 150, y: 200, props: { color: 'blue' } }
|
|
174
|
+
*
|
|
175
|
+
* const diff = diffRecord(oldShape, newShape)
|
|
176
|
+
* // Returns: {
|
|
177
|
+
* // x: ['put', 150],
|
|
178
|
+
* // props: ['patch', { color: ['put', 'blue'] }]
|
|
179
|
+
* // }
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
92
184
|
export function diffRecord(prev: object, next: object): ObjectDiff | null {
|
|
93
185
|
return diffObject(prev, next, new Set(['props']))
|
|
94
186
|
}
|
|
@@ -197,7 +289,29 @@ function diffArray(prevArray: unknown[], nextArray: unknown[]): PutOp | AppendOp
|
|
|
197
289
|
return [ValueOpType.Append, nextArray.slice(prevArray.length), prevArray.length]
|
|
198
290
|
}
|
|
199
291
|
|
|
200
|
-
/**
|
|
292
|
+
/**
|
|
293
|
+
* Applies an ObjectDiff to an object, returning a new object with the changes applied.
|
|
294
|
+
* This function handles all value operation types and creates a shallow copy when modifications
|
|
295
|
+
* are needed. If no changes are required, the original object is returned.
|
|
296
|
+
*
|
|
297
|
+
* @param object - The object to apply the diff to
|
|
298
|
+
* @param objectDiff - The ObjectDiff containing the operations to apply
|
|
299
|
+
* @returns A new object with the diff applied, or the original object if no changes were needed
|
|
300
|
+
*
|
|
301
|
+
* @example
|
|
302
|
+
* ```ts
|
|
303
|
+
* const original = { x: 100, y: 200, props: { color: 'red' } }
|
|
304
|
+
* const diff = {
|
|
305
|
+
* x: ['put', 150],
|
|
306
|
+
* props: ['patch', { color: ['put', 'blue'] }]
|
|
307
|
+
* }
|
|
308
|
+
*
|
|
309
|
+
* const updated = applyObjectDiff(original, diff)
|
|
310
|
+
* // Returns: { x: 150, y: 200, props: { color: 'blue' } }
|
|
311
|
+
* ```
|
|
312
|
+
*
|
|
313
|
+
* @internal
|
|
314
|
+
*/
|
|
201
315
|
export function applyObjectDiff<T extends object>(object: T, objectDiff: ObjectDiff): T {
|
|
202
316
|
// don't patch nulls
|
|
203
317
|
if (!object || typeof object !== 'object') return object
|
package/src/lib/findMin.ts
CHANGED
|
@@ -3,6 +3,12 @@
|
|
|
3
3
|
*
|
|
4
4
|
* @param values - An iterable of numbers to find the minimum from
|
|
5
5
|
* @returns The minimum value, or null if the iterable is empty
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* findMin([3, 1, 4, 1, 5]) // returns 1
|
|
9
|
+
* findMin(new Set([10, 5, 8])) // returns 5
|
|
10
|
+
* findMin([]) // returns null
|
|
11
|
+
* ```
|
|
6
12
|
*/
|
|
7
13
|
export function findMin(values: Iterable<number>): number | null {
|
|
8
14
|
let min: number | null = null
|
package/src/lib/interval.ts
CHANGED
|
@@ -1,3 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a repeating timer that executes a callback at regular intervals and returns a cleanup function.
|
|
3
|
+
*
|
|
4
|
+
* This utility function wraps the standard `setInterval`/`clearInterval` pattern into a more convenient
|
|
5
|
+
* interface that returns a dispose function for cleanup. It's commonly used in the sync system for
|
|
6
|
+
* periodic tasks like health checks, ping operations, and session pruning.
|
|
7
|
+
*
|
|
8
|
+
* @param cb - The callback function to execute at each interval
|
|
9
|
+
* @param timeout - The time interval in milliseconds between callback executions
|
|
10
|
+
* @returns A cleanup function that stops the interval when called
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* // Create a periodic health check
|
|
15
|
+
* const stopHealthCheck = interval(() => {
|
|
16
|
+
* console.log('Checking server health...')
|
|
17
|
+
* checkServerConnection()
|
|
18
|
+
* }, 5000)
|
|
19
|
+
*
|
|
20
|
+
* // Later, stop the health check
|
|
21
|
+
* stopHealthCheck()
|
|
22
|
+
* ```
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```ts
|
|
26
|
+
* // Use in a disposables array for cleanup management
|
|
27
|
+
* class MyClass {
|
|
28
|
+
* private disposables = [
|
|
29
|
+
* interval(() => this.sendPing(), 30000),
|
|
30
|
+
* interval(() => this.pruneSessions(), 2000)
|
|
31
|
+
* ]
|
|
32
|
+
*
|
|
33
|
+
* dispose() {
|
|
34
|
+
* this.disposables.forEach(dispose => dispose())
|
|
35
|
+
* }
|
|
36
|
+
* }
|
|
37
|
+
* ```
|
|
38
|
+
*
|
|
39
|
+
* @public
|
|
40
|
+
*/
|
|
1
41
|
export function interval(cb: () => void, timeout: number) {
|
|
2
42
|
const i = setInterval(cb, timeout)
|
|
3
43
|
return () => clearInterval(i)
|
package/src/lib/protocol.ts
CHANGED
|
@@ -3,12 +3,41 @@ import { NetworkDiff, ObjectDiff, RecordOpType } from './diff'
|
|
|
3
3
|
|
|
4
4
|
const TLSYNC_PROTOCOL_VERSION = 7
|
|
5
5
|
|
|
6
|
-
/**
|
|
6
|
+
/**
|
|
7
|
+
* Gets the current tldraw sync protocol version number.
|
|
8
|
+
*
|
|
9
|
+
* This version number is used during WebSocket connection handshake to ensure
|
|
10
|
+
* client and server compatibility. When versions don't match, the connection
|
|
11
|
+
* will be rejected with an incompatibility error.
|
|
12
|
+
*
|
|
13
|
+
* @returns The current protocol version number
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* const version = getTlsyncProtocolVersion()
|
|
18
|
+
* console.log(`Using protocol version: ${version}`)
|
|
19
|
+
* ```
|
|
20
|
+
*
|
|
21
|
+
* @internal
|
|
22
|
+
*/
|
|
7
23
|
export function getTlsyncProtocolVersion() {
|
|
8
24
|
return TLSYNC_PROTOCOL_VERSION
|
|
9
25
|
}
|
|
10
26
|
|
|
11
27
|
/**
|
|
28
|
+
* Constants defining the different types of protocol incompatibility reasons.
|
|
29
|
+
*
|
|
30
|
+
* These values indicate why a client-server connection was rejected due to
|
|
31
|
+
* version or compatibility issues. Each reason helps diagnose specific problems
|
|
32
|
+
* during the connection handshake.
|
|
33
|
+
*
|
|
34
|
+
* @example
|
|
35
|
+
* ```ts
|
|
36
|
+
* if (error.reason === TLIncompatibilityReason.ClientTooOld) {
|
|
37
|
+
* showUpgradeMessage('Please update your client')
|
|
38
|
+
* }
|
|
39
|
+
* ```
|
|
40
|
+
*
|
|
12
41
|
* @internal
|
|
13
42
|
* @deprecated Replaced by websocket .close status/reason
|
|
14
43
|
*/
|
|
@@ -20,13 +49,53 @@ export const TLIncompatibilityReason = {
|
|
|
20
49
|
} as const
|
|
21
50
|
|
|
22
51
|
/**
|
|
52
|
+
* Union type representing all possible incompatibility reason values.
|
|
53
|
+
*
|
|
54
|
+
* This type represents the different reasons why a client-server connection
|
|
55
|
+
* might fail due to protocol or version mismatches.
|
|
56
|
+
*
|
|
57
|
+
* @example
|
|
58
|
+
* ```ts
|
|
59
|
+
* function handleIncompatibility(reason: TLIncompatibilityReason) {
|
|
60
|
+
* switch (reason) {
|
|
61
|
+
* case 'clientTooOld':
|
|
62
|
+
* return 'Client needs to be updated'
|
|
63
|
+
* case 'serverTooOld':
|
|
64
|
+
* return 'Server needs to be updated'
|
|
65
|
+
* }
|
|
66
|
+
* }
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
23
69
|
* @internal
|
|
24
70
|
* @deprecated replaced by websocket .close status/reason
|
|
25
71
|
*/
|
|
26
72
|
export type TLIncompatibilityReason =
|
|
27
73
|
(typeof TLIncompatibilityReason)[keyof typeof TLIncompatibilityReason]
|
|
28
74
|
|
|
29
|
-
/**
|
|
75
|
+
/**
|
|
76
|
+
* Union type representing all possible message types that can be sent from server to client.
|
|
77
|
+
*
|
|
78
|
+
* This encompasses the complete set of server-originated WebSocket messages in the tldraw
|
|
79
|
+
* sync protocol, including connection establishment, data synchronization, and error handling.
|
|
80
|
+
*
|
|
81
|
+
* @param R - The record type being synchronized (extends UnknownRecord)
|
|
82
|
+
*
|
|
83
|
+
* @example
|
|
84
|
+
* ```ts
|
|
85
|
+
* syncClient.onReceiveMessage((message: TLSocketServerSentEvent<MyRecord>) => {
|
|
86
|
+
* switch (message.type) {
|
|
87
|
+
* case 'connect':
|
|
88
|
+
* console.log('Connected to room with clock:', message.serverClock)
|
|
89
|
+
* break
|
|
90
|
+
* case 'data':
|
|
91
|
+
* console.log('Received data updates:', message.data)
|
|
92
|
+
* break
|
|
93
|
+
* }
|
|
94
|
+
* })
|
|
95
|
+
* ```
|
|
96
|
+
*
|
|
97
|
+
* @internal
|
|
98
|
+
*/
|
|
30
99
|
export type TLSocketServerSentEvent<R extends UnknownRecord> =
|
|
31
100
|
| {
|
|
32
101
|
type: 'connect'
|
|
@@ -50,7 +119,31 @@ export type TLSocketServerSentEvent<R extends UnknownRecord> =
|
|
|
50
119
|
| { type: 'custom'; data: any }
|
|
51
120
|
| TLSocketServerSentDataEvent<R>
|
|
52
121
|
|
|
53
|
-
/**
|
|
122
|
+
/**
|
|
123
|
+
* Union type representing data-related messages sent from server to client.
|
|
124
|
+
*
|
|
125
|
+
* These messages handle the core synchronization operations: applying patches from
|
|
126
|
+
* other clients and confirming the results of client push operations.
|
|
127
|
+
*
|
|
128
|
+
* @param R - The record type being synchronized (extends UnknownRecord)
|
|
129
|
+
*
|
|
130
|
+
* @example
|
|
131
|
+
* ```ts
|
|
132
|
+
* function handleDataEvent(event: TLSocketServerSentDataEvent<MyRecord>) {
|
|
133
|
+
* if (event.type === 'patch') {
|
|
134
|
+
* // Apply changes from other clients
|
|
135
|
+
* applyNetworkDiff(event.diff)
|
|
136
|
+
* } else if (event.type === 'push_result') {
|
|
137
|
+
* // Handle result of our push request
|
|
138
|
+
* if (event.action === 'commit') {
|
|
139
|
+
* console.log('Changes accepted by server')
|
|
140
|
+
* }
|
|
141
|
+
* }
|
|
142
|
+
* }
|
|
143
|
+
* ```
|
|
144
|
+
*
|
|
145
|
+
* @internal
|
|
146
|
+
*/
|
|
54
147
|
export type TLSocketServerSentDataEvent<R extends UnknownRecord> =
|
|
55
148
|
| {
|
|
56
149
|
type: 'patch'
|
|
@@ -64,7 +157,30 @@ export type TLSocketServerSentDataEvent<R extends UnknownRecord> =
|
|
|
64
157
|
action: 'discard' | 'commit' | { rebaseWithDiff: NetworkDiff<R> }
|
|
65
158
|
}
|
|
66
159
|
|
|
67
|
-
/**
|
|
160
|
+
/**
|
|
161
|
+
* Interface defining a client-to-server push request message.
|
|
162
|
+
*
|
|
163
|
+
* Push requests are sent when the client wants to synchronize local changes
|
|
164
|
+
* with the server. They contain document changes and optionally presence updates
|
|
165
|
+
* (like cursor position or user selection).
|
|
166
|
+
*
|
|
167
|
+
* @param R - The record type being synchronized (extends UnknownRecord)
|
|
168
|
+
*
|
|
169
|
+
* @example
|
|
170
|
+
* ```ts
|
|
171
|
+
* const pushRequest: TLPushRequest<MyRecord> = {
|
|
172
|
+
* type: 'push',
|
|
173
|
+
* clientClock: 15,
|
|
174
|
+
* diff: {
|
|
175
|
+
* 'shape:abc123': [RecordOpType.Patch, { x: [ValueOpType.Put, 100] }]
|
|
176
|
+
* },
|
|
177
|
+
* presence: [RecordOpType.Put, { cursor: { x: 150, y: 200 } }]
|
|
178
|
+
* }
|
|
179
|
+
* socket.sendMessage(pushRequest)
|
|
180
|
+
* ```
|
|
181
|
+
*
|
|
182
|
+
* @internal
|
|
183
|
+
*/
|
|
68
184
|
export interface TLPushRequest<R extends UnknownRecord> {
|
|
69
185
|
type: 'push'
|
|
70
186
|
clientClock: number
|
|
@@ -72,7 +188,27 @@ export interface TLPushRequest<R extends UnknownRecord> {
|
|
|
72
188
|
presence?: [typeof RecordOpType.Patch, ObjectDiff] | [typeof RecordOpType.Put, R]
|
|
73
189
|
}
|
|
74
190
|
|
|
75
|
-
/**
|
|
191
|
+
/**
|
|
192
|
+
* Interface defining a client-to-server connection request message.
|
|
193
|
+
*
|
|
194
|
+
* This message initiates a WebSocket connection to a sync room. It includes
|
|
195
|
+
* the client's schema, protocol version, and last known server clock for
|
|
196
|
+
* proper synchronization state management.
|
|
197
|
+
*
|
|
198
|
+
* @example
|
|
199
|
+
* ```ts
|
|
200
|
+
* const connectRequest: TLConnectRequest = {
|
|
201
|
+
* type: 'connect',
|
|
202
|
+
* connectRequestId: 'conn-123',
|
|
203
|
+
* lastServerClock: 42,
|
|
204
|
+
* protocolVersion: getTlsyncProtocolVersion(),
|
|
205
|
+
* schema: mySchema.serialize()
|
|
206
|
+
* }
|
|
207
|
+
* socket.sendMessage(connectRequest)
|
|
208
|
+
* ```
|
|
209
|
+
*
|
|
210
|
+
* @internal
|
|
211
|
+
*/
|
|
76
212
|
export interface TLConnectRequest {
|
|
77
213
|
type: 'connect'
|
|
78
214
|
connectRequestId: string
|
|
@@ -81,12 +217,54 @@ export interface TLConnectRequest {
|
|
|
81
217
|
schema: SerializedSchema
|
|
82
218
|
}
|
|
83
219
|
|
|
84
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Interface defining a client-to-server ping request message.
|
|
222
|
+
*
|
|
223
|
+
* Ping requests are used to measure network latency and ensure the connection
|
|
224
|
+
* is still active. The server responds with a 'pong' message.
|
|
225
|
+
*
|
|
226
|
+
* @example
|
|
227
|
+
* ```ts
|
|
228
|
+
* const pingRequest: TLPingRequest = { type: 'ping' }
|
|
229
|
+
* socket.sendMessage(pingRequest)
|
|
230
|
+
*
|
|
231
|
+
* // Server will respond with { type: 'pong' }
|
|
232
|
+
* ```
|
|
233
|
+
*
|
|
234
|
+
* @internal
|
|
235
|
+
*/
|
|
85
236
|
export interface TLPingRequest {
|
|
86
237
|
type: 'ping'
|
|
87
238
|
}
|
|
88
239
|
|
|
89
|
-
/**
|
|
240
|
+
/**
|
|
241
|
+
* Union type representing all possible message types that can be sent from client to server.
|
|
242
|
+
*
|
|
243
|
+
* This encompasses the complete set of client-originated WebSocket messages in the tldraw
|
|
244
|
+
* sync protocol, covering connection establishment, data synchronization, and connectivity checks.
|
|
245
|
+
*
|
|
246
|
+
* @param R - The record type being synchronized (extends UnknownRecord)
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* ```ts
|
|
250
|
+
* function sendMessage(message: TLSocketClientSentEvent<MyRecord>) {
|
|
251
|
+
* switch (message.type) {
|
|
252
|
+
* case 'connect':
|
|
253
|
+
* console.log('Establishing connection...')
|
|
254
|
+
* break
|
|
255
|
+
* case 'push':
|
|
256
|
+
* console.log('Pushing changes:', message.diff)
|
|
257
|
+
* break
|
|
258
|
+
* case 'ping':
|
|
259
|
+
* console.log('Checking connection latency')
|
|
260
|
+
* break
|
|
261
|
+
* }
|
|
262
|
+
* socket.send(JSON.stringify(message))
|
|
263
|
+
* }
|
|
264
|
+
* ```
|
|
265
|
+
*
|
|
266
|
+
* @internal
|
|
267
|
+
*/
|
|
90
268
|
export type TLSocketClientSentEvent<R extends UnknownRecord> =
|
|
91
269
|
| TLPushRequest<R>
|
|
92
270
|
| TLConnectRequest
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { describe, expect, it } from 'vitest'
|
|
2
|
+
import { PersistedRoomSnapshotForSupabase } from './server-types'
|
|
3
|
+
import { RoomSnapshot } from './TLSyncRoom'
|
|
4
|
+
|
|
5
|
+
describe('PersistedRoomSnapshotForSupabase', () => {
|
|
6
|
+
describe('JSON serialization compatibility', () => {
|
|
7
|
+
it('should properly serialize and deserialize for database storage', () => {
|
|
8
|
+
// This tests actual business logic: compatibility with database storage
|
|
9
|
+
const roomSnapshot: RoomSnapshot = {
|
|
10
|
+
clock: 42,
|
|
11
|
+
documentClock: 38,
|
|
12
|
+
documents: [
|
|
13
|
+
{
|
|
14
|
+
state: {
|
|
15
|
+
id: 'shape:test' as any,
|
|
16
|
+
typeName: 'shape',
|
|
17
|
+
type: 'geo',
|
|
18
|
+
x: 100,
|
|
19
|
+
y: 200,
|
|
20
|
+
} as any,
|
|
21
|
+
lastChangedClock: 35,
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
tombstones: { 'shape:deleted': 20 },
|
|
25
|
+
tombstoneHistoryStartsAtClock: 15,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const persistedData: PersistedRoomSnapshotForSupabase = {
|
|
29
|
+
id: 'test-room-id',
|
|
30
|
+
slug: 'test-room-slug',
|
|
31
|
+
drawing: roomSnapshot,
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Test actual serialization behavior that matters for database storage
|
|
35
|
+
const serialized = JSON.stringify(persistedData)
|
|
36
|
+
const deserialized = JSON.parse(serialized) as PersistedRoomSnapshotForSupabase
|
|
37
|
+
|
|
38
|
+
// Verify critical data survives serialization roundtrip
|
|
39
|
+
expect(deserialized.drawing.clock).toBe(42)
|
|
40
|
+
expect(deserialized.drawing.documents[0].state.id).toBe('shape:test')
|
|
41
|
+
expect(deserialized.drawing.tombstones!['shape:deleted']).toBe(20)
|
|
42
|
+
})
|
|
43
|
+
})
|
|
44
|
+
})
|
package/src/lib/server-types.ts
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import { RoomSnapshot } from './TLSyncRoom'
|
|
2
2
|
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* Database schema interface for persisting tldraw room snapshots in Supabase.
|
|
5
|
+
*
|
|
6
|
+
* This interface defines the structure used to store collaborative drawing rooms
|
|
7
|
+
* in a Supabase database, containing both metadata and the complete room state.
|
|
8
|
+
* The room snapshot includes all documents, tombstones, and synchronization clocks
|
|
9
|
+
* needed to restore a collaborative session.
|
|
10
|
+
*
|
|
11
|
+
* @param id - Unique identifier for the persisted room record in the database
|
|
12
|
+
* @param slug - Human-readable URL slug or identifier for the room (e.g., "my-drawing-123")
|
|
13
|
+
* @param drawing - Complete room snapshot containing all synchronized state and metadata
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```ts
|
|
17
|
+
* // Saving a room snapshot to Supabase
|
|
18
|
+
* const roomSnapshot = syncRoom.getSnapshot()
|
|
19
|
+
* const persistedData: PersistedRoomSnapshotForSupabase = {
|
|
20
|
+
* id: crypto.randomUUID(),
|
|
21
|
+
* slug: 'collaborative-whiteboard-session',
|
|
22
|
+
* drawing: roomSnapshot
|
|
23
|
+
* }
|
|
24
|
+
*
|
|
25
|
+
* await supabase
|
|
26
|
+
* .from('rooms')
|
|
27
|
+
* .insert(persistedData)
|
|
28
|
+
* ```
|
|
29
|
+
*
|
|
30
|
+
* @example
|
|
31
|
+
* ```ts
|
|
32
|
+
* // Loading a room snapshot from Supabase
|
|
33
|
+
* const { data } = await supabase
|
|
34
|
+
* .from('rooms')
|
|
35
|
+
* .select('*')
|
|
36
|
+
* .eq('slug', roomSlug)
|
|
37
|
+
* .single()
|
|
38
|
+
*
|
|
39
|
+
* const roomData = data as PersistedRoomSnapshotForSupabase
|
|
40
|
+
* const room = new TLSyncRoom({
|
|
41
|
+
* schema: mySchema,
|
|
42
|
+
* snapshot: roomData.drawing
|
|
43
|
+
* })
|
|
44
|
+
* ```
|
|
45
|
+
*
|
|
46
|
+
* @internal
|
|
47
|
+
*/
|
|
4
48
|
export interface PersistedRoomSnapshotForSupabase {
|
|
5
49
|
id: string
|
|
6
50
|
slug: string
|