@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.
Files changed (67) hide show
  1. package/dist-cjs/index.d.ts +605 -75
  2. package/dist-cjs/index.js +1 -1
  3. package/dist-cjs/lib/ClientWebSocketAdapter.js +144 -0
  4. package/dist-cjs/lib/ClientWebSocketAdapter.js.map +2 -2
  5. package/dist-cjs/lib/RoomSession.js +3 -0
  6. package/dist-cjs/lib/RoomSession.js.map +2 -2
  7. package/dist-cjs/lib/ServerSocketAdapter.js +23 -0
  8. package/dist-cjs/lib/ServerSocketAdapter.js.map +2 -2
  9. package/dist-cjs/lib/TLRemoteSyncError.js +8 -0
  10. package/dist-cjs/lib/TLRemoteSyncError.js.map +2 -2
  11. package/dist-cjs/lib/TLSocketRoom.js +280 -56
  12. package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
  13. package/dist-cjs/lib/TLSyncClient.js +45 -2
  14. package/dist-cjs/lib/TLSyncClient.js.map +2 -2
  15. package/dist-cjs/lib/TLSyncRoom.js +161 -16
  16. package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
  17. package/dist-cjs/lib/chunk.js +30 -0
  18. package/dist-cjs/lib/chunk.js.map +2 -2
  19. package/dist-cjs/lib/diff.js.map +2 -2
  20. package/dist-cjs/lib/findMin.js.map +2 -2
  21. package/dist-cjs/lib/interval.js.map +2 -2
  22. package/dist-cjs/lib/protocol.js.map +2 -2
  23. package/dist-cjs/lib/server-types.js.map +1 -1
  24. package/dist-esm/index.d.mts +605 -75
  25. package/dist-esm/index.mjs +1 -1
  26. package/dist-esm/lib/ClientWebSocketAdapter.mjs +144 -0
  27. package/dist-esm/lib/ClientWebSocketAdapter.mjs.map +2 -2
  28. package/dist-esm/lib/RoomSession.mjs +3 -0
  29. package/dist-esm/lib/RoomSession.mjs.map +2 -2
  30. package/dist-esm/lib/ServerSocketAdapter.mjs +23 -0
  31. package/dist-esm/lib/ServerSocketAdapter.mjs.map +2 -2
  32. package/dist-esm/lib/TLRemoteSyncError.mjs +8 -0
  33. package/dist-esm/lib/TLRemoteSyncError.mjs.map +2 -2
  34. package/dist-esm/lib/TLSocketRoom.mjs +280 -56
  35. package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
  36. package/dist-esm/lib/TLSyncClient.mjs +45 -2
  37. package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
  38. package/dist-esm/lib/TLSyncRoom.mjs +161 -16
  39. package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
  40. package/dist-esm/lib/chunk.mjs +30 -0
  41. package/dist-esm/lib/chunk.mjs.map +2 -2
  42. package/dist-esm/lib/diff.mjs.map +2 -2
  43. package/dist-esm/lib/findMin.mjs.map +2 -2
  44. package/dist-esm/lib/interval.mjs.map +2 -2
  45. package/dist-esm/lib/protocol.mjs.map +2 -2
  46. package/package.json +6 -6
  47. package/src/lib/ClientWebSocketAdapter.test.ts +712 -129
  48. package/src/lib/ClientWebSocketAdapter.ts +240 -9
  49. package/src/lib/RoomSession.test.ts +97 -0
  50. package/src/lib/RoomSession.ts +105 -3
  51. package/src/lib/ServerSocketAdapter.test.ts +228 -0
  52. package/src/lib/ServerSocketAdapter.ts +124 -5
  53. package/src/lib/TLRemoteSyncError.ts +50 -1
  54. package/src/lib/TLSocketRoom.ts +377 -60
  55. package/src/lib/TLSyncClient.test.ts +828 -0
  56. package/src/lib/TLSyncClient.ts +251 -26
  57. package/src/lib/TLSyncRoom.ts +284 -24
  58. package/src/lib/chunk.ts +72 -1
  59. package/src/lib/diff.ts +128 -14
  60. package/src/lib/findMin.ts +6 -0
  61. package/src/lib/interval.ts +40 -0
  62. package/src/lib/protocol.ts +185 -7
  63. package/src/lib/server-types.test.ts +44 -0
  64. package/src/lib/server-types.ts +45 -1
  65. package/src/test/TLSocketRoom.test.ts +438 -29
  66. package/src/test/chunk.test.ts +200 -3
  67. 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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- *@internal
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
@@ -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
@@ -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)
@@ -3,12 +3,41 @@ import { NetworkDiff, ObjectDiff, RecordOpType } from './diff'
3
3
 
4
4
  const TLSYNC_PROTOCOL_VERSION = 7
5
5
 
6
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
- /** @internal */
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
+ })
@@ -1,6 +1,50 @@
1
1
  import { RoomSnapshot } from './TLSyncRoom'
2
2
 
3
- /** @internal */
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