@tldraw/sync-core 5.2.0-canary.fe03bcdddf34 → 5.2.0-canary.fff413eea248
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/DOCS.md +662 -0
- package/README.md +9 -1
- package/dist-cjs/index.js +1 -1
- package/dist-cjs/lib/TLSocketRoom.js +4 -1
- package/dist-cjs/lib/TLSocketRoom.js.map +2 -2
- package/dist-cjs/lib/TLSyncClient.js +2 -3
- package/dist-cjs/lib/TLSyncClient.js.map +2 -2
- package/dist-cjs/lib/TLSyncRoom.js +3 -2
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-esm/index.mjs +1 -1
- package/dist-esm/lib/TLSocketRoom.mjs +4 -1
- package/dist-esm/lib/TLSocketRoom.mjs.map +2 -2
- package/dist-esm/lib/TLSyncClient.mjs +2 -4
- package/dist-esm/lib/TLSyncClient.mjs.map +2 -2
- package/dist-esm/lib/TLSyncRoom.mjs +3 -2
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/package.json +9 -8
- package/src/lib/ClientWebSocketAdapter.test.ts +486 -508
- package/src/lib/DurableObjectSqliteSyncWrapper.test.ts +102 -0
- package/src/lib/InMemorySyncStorage.test.ts +294 -0
- package/src/lib/MicrotaskNotifier.test.ts +79 -254
- package/src/lib/{NodeSqliteSyncWrapper.test.ts → NodeSqliteWrapper.test.ts} +29 -25
- package/src/lib/SQLiteSyncStorage.test.ts +239 -0
- package/src/lib/ServerSocketAdapter.test.ts +60 -199
- package/src/lib/TLSocketRoom.ts +6 -1
- package/src/lib/TLSyncClient.test.ts +1127 -846
- package/src/lib/TLSyncClient.ts +4 -4
- package/src/lib/TLSyncRoom.ts +5 -2
- package/src/lib/TLSyncStorage.test.ts +225 -0
- package/src/lib/chunk.test.ts +372 -0
- package/src/lib/diff.test.ts +885 -0
- package/src/lib/interval.test.ts +43 -0
- package/src/lib/protocol.test.ts +43 -0
- package/src/lib/recordDiff.test.ts +159 -0
- package/src/test/TLSocketRoom.test.ts +1109 -894
- package/src/test/TLSyncRoom.test.ts +1735 -893
- package/src/test/storageContractSuite.ts +618 -0
- package/src/test/upgradeDowngrade.test.ts +137 -37
- package/src/lib/NodeSqliteSyncWrapper.integration.test.ts +0 -270
- package/src/lib/RoomSession.test.ts +0 -101
- package/src/lib/computeTombstonePruning.test.ts +0 -352
- package/src/lib/server-types.test.ts +0 -44
- package/src/test/InMemorySyncStorage.test.ts +0 -1780
- package/src/test/SQLiteSyncStorage.test.ts +0 -1485
- package/src/test/chunk.test.ts +0 -385
- package/src/test/customMessages.test.ts +0 -36
- package/src/test/diff.test.ts +0 -784
- package/src/test/presenceMode.test.ts +0 -149
- package/src/test/validation.test.ts +0 -186
|
@@ -1,352 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
computeTombstonePruning,
|
|
4
|
-
MAX_TOMBSTONES,
|
|
5
|
-
TOMBSTONE_PRUNE_BUFFER_SIZE,
|
|
6
|
-
} from './InMemorySyncStorage'
|
|
7
|
-
|
|
8
|
-
// Helper to create tombstone array
|
|
9
|
-
function makeTombstones(
|
|
10
|
-
count: number,
|
|
11
|
-
clockFn: (i: number) => number = (i) => i + 1
|
|
12
|
-
): Array<{ id: string; clock: number }> {
|
|
13
|
-
return Array.from({ length: count }, (_, i) => ({
|
|
14
|
-
id: `doc${i}`,
|
|
15
|
-
clock: clockFn(i),
|
|
16
|
-
}))
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
describe('computeTombstonePruning', () => {
|
|
20
|
-
describe('basic behavior', () => {
|
|
21
|
-
it('returns null when tombstone count is below threshold', () => {
|
|
22
|
-
const tombstones = makeTombstones(100)
|
|
23
|
-
const result = computeTombstonePruning({
|
|
24
|
-
tombstones,
|
|
25
|
-
documentClock: 1000,
|
|
26
|
-
maxTombstones: 500,
|
|
27
|
-
})
|
|
28
|
-
expect(result).toBeNull()
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
it('returns null when tombstone count equals threshold exactly', () => {
|
|
32
|
-
const tombstones = makeTombstones(500)
|
|
33
|
-
const result = computeTombstonePruning({
|
|
34
|
-
tombstones,
|
|
35
|
-
documentClock: 1000,
|
|
36
|
-
maxTombstones: 500,
|
|
37
|
-
})
|
|
38
|
-
expect(result).toBeNull()
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
it('returns null for empty tombstones array', () => {
|
|
42
|
-
const result = computeTombstonePruning({ tombstones: [], documentClock: 1000 })
|
|
43
|
-
expect(result).toBeNull()
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
it('returns pruning result when exceeding threshold', () => {
|
|
47
|
-
const tombstones = makeTombstones(600)
|
|
48
|
-
const result = computeTombstonePruning({
|
|
49
|
-
tombstones,
|
|
50
|
-
documentClock: 1000,
|
|
51
|
-
maxTombstones: 500,
|
|
52
|
-
pruneBufferSize: 50,
|
|
53
|
-
})
|
|
54
|
-
expect(result).not.toBeNull()
|
|
55
|
-
expect(result!.idsToDelete.length).toBeGreaterThan(0)
|
|
56
|
-
})
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
describe('pruning calculation', () => {
|
|
60
|
-
it('deletes oldest tombstones first', () => {
|
|
61
|
-
// 10 tombstones, max 5, buffer 2 => delete 10 - 5 + 2 = 7
|
|
62
|
-
const tombstones = makeTombstones(10)
|
|
63
|
-
const result = computeTombstonePruning({
|
|
64
|
-
tombstones,
|
|
65
|
-
documentClock: 100,
|
|
66
|
-
maxTombstones: 5,
|
|
67
|
-
pruneBufferSize: 2,
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
expect(result).not.toBeNull()
|
|
71
|
-
expect(result!.idsToDelete).toEqual(['doc0', 'doc1', 'doc2', 'doc3', 'doc4', 'doc5', 'doc6'])
|
|
72
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(8) // clock of doc7
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('keeps newest tombstones', () => {
|
|
76
|
-
const tombstones = makeTombstones(10)
|
|
77
|
-
const result = computeTombstonePruning({
|
|
78
|
-
tombstones,
|
|
79
|
-
documentClock: 100,
|
|
80
|
-
maxTombstones: 5,
|
|
81
|
-
pruneBufferSize: 2,
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
// Should keep doc7, doc8, doc9
|
|
85
|
-
const keptIds = tombstones.map((t) => t.id).filter((id) => !result!.idsToDelete.includes(id))
|
|
86
|
-
expect(keptIds).toEqual(['doc7', 'doc8', 'doc9'])
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
it('sets newTombstoneHistoryStartsAtClock to oldest remaining tombstone clock', () => {
|
|
90
|
-
// Tombstones with clocks 1-20
|
|
91
|
-
const tombstones = makeTombstones(20)
|
|
92
|
-
// max 10, buffer 5 => delete 20 - 10 + 5 = 15
|
|
93
|
-
const result = computeTombstonePruning({
|
|
94
|
-
tombstones,
|
|
95
|
-
documentClock: 1000,
|
|
96
|
-
maxTombstones: 10,
|
|
97
|
-
pruneBufferSize: 5,
|
|
98
|
-
})
|
|
99
|
-
|
|
100
|
-
expect(result).not.toBeNull()
|
|
101
|
-
// After deleting 15, oldest remaining is doc15 with clock 16
|
|
102
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(16)
|
|
103
|
-
})
|
|
104
|
-
})
|
|
105
|
-
|
|
106
|
-
describe('duplicate clock handling', () => {
|
|
107
|
-
it('avoids splitting tombstones with the same clock value', () => {
|
|
108
|
-
// Create tombstones where multiple have the same clock
|
|
109
|
-
// Clocks: [1, 1, 1, 2, 2, 2, 3, 3, 3, 4]
|
|
110
|
-
const tombstones = [
|
|
111
|
-
{ id: 'a1', clock: 1 },
|
|
112
|
-
{ id: 'a2', clock: 1 },
|
|
113
|
-
{ id: 'a3', clock: 1 },
|
|
114
|
-
{ id: 'b1', clock: 2 },
|
|
115
|
-
{ id: 'b2', clock: 2 },
|
|
116
|
-
{ id: 'b3', clock: 2 },
|
|
117
|
-
{ id: 'c1', clock: 3 },
|
|
118
|
-
{ id: 'c2', clock: 3 },
|
|
119
|
-
{ id: 'c3', clock: 3 },
|
|
120
|
-
{ id: 'd1', clock: 4 },
|
|
121
|
-
]
|
|
122
|
-
|
|
123
|
-
// max 5, buffer 1 => initial cutoff = 10 - 5 + 1 = 6
|
|
124
|
-
// cutoff 6 would split clock=2 (b3) from clock=3 (c1)
|
|
125
|
-
// But tombstones[5].clock (2) !== tombstones[6].clock (3), so no adjustment needed
|
|
126
|
-
const result = computeTombstonePruning({
|
|
127
|
-
tombstones,
|
|
128
|
-
documentClock: 100,
|
|
129
|
-
maxTombstones: 5,
|
|
130
|
-
pruneBufferSize: 1,
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
expect(result).not.toBeNull()
|
|
134
|
-
expect(result!.idsToDelete).toEqual(['a1', 'a2', 'a3', 'b1', 'b2', 'b3'])
|
|
135
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(3) // clock of c1
|
|
136
|
-
})
|
|
137
|
-
|
|
138
|
-
it('extends cutoff when it would split a clock value', () => {
|
|
139
|
-
// Clocks: [1, 2, 2, 2, 2, 3]
|
|
140
|
-
const tombstones = [
|
|
141
|
-
{ id: 'a', clock: 1 },
|
|
142
|
-
{ id: 'b1', clock: 2 },
|
|
143
|
-
{ id: 'b2', clock: 2 },
|
|
144
|
-
{ id: 'b3', clock: 2 },
|
|
145
|
-
{ id: 'b4', clock: 2 },
|
|
146
|
-
{ id: 'c', clock: 3 },
|
|
147
|
-
]
|
|
148
|
-
|
|
149
|
-
// max 4, buffer 1 => initial cutoff = 6 - 4 + 1 = 3
|
|
150
|
-
// cutoff 3 points to b3 (clock 2), cutoff-1 points to b2 (clock 2)
|
|
151
|
-
// Same clock, so extend cutoff until boundary
|
|
152
|
-
// cutoff 4: b4 (clock 2), cutoff-1: b3 (clock 2) - same, extend
|
|
153
|
-
// cutoff 5: c (clock 3), cutoff-1: b4 (clock 2) - different, stop
|
|
154
|
-
const result = computeTombstonePruning({
|
|
155
|
-
tombstones,
|
|
156
|
-
documentClock: 100,
|
|
157
|
-
maxTombstones: 4,
|
|
158
|
-
pruneBufferSize: 1,
|
|
159
|
-
})
|
|
160
|
-
|
|
161
|
-
expect(result).not.toBeNull()
|
|
162
|
-
// Must delete all clock=2 tombstones to avoid split
|
|
163
|
-
expect(result!.idsToDelete).toEqual(['a', 'b1', 'b2', 'b3', 'b4'])
|
|
164
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(3)
|
|
165
|
-
})
|
|
166
|
-
|
|
167
|
-
it('handles all tombstones with same clock value', () => {
|
|
168
|
-
// All tombstones have clock=5
|
|
169
|
-
const tombstones = makeTombstones(100, () => 5)
|
|
170
|
-
const result = computeTombstonePruning({
|
|
171
|
-
tombstones,
|
|
172
|
-
documentClock: 1000,
|
|
173
|
-
maxTombstones: 50,
|
|
174
|
-
pruneBufferSize: 10,
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
expect(result).not.toBeNull()
|
|
178
|
-
// Since all have same clock, cutoff extends to delete all
|
|
179
|
-
expect(result!.idsToDelete.length).toBe(100)
|
|
180
|
-
// Falls back to documentClock since no remaining tombstones
|
|
181
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(1000)
|
|
182
|
-
})
|
|
183
|
-
})
|
|
184
|
-
|
|
185
|
-
describe('edge cases', () => {
|
|
186
|
-
it('handles exactly one tombstone over threshold', () => {
|
|
187
|
-
const tombstones = makeTombstones(101)
|
|
188
|
-
const result = computeTombstonePruning({
|
|
189
|
-
tombstones,
|
|
190
|
-
documentClock: 1000,
|
|
191
|
-
maxTombstones: 100,
|
|
192
|
-
pruneBufferSize: 0,
|
|
193
|
-
})
|
|
194
|
-
|
|
195
|
-
expect(result).not.toBeNull()
|
|
196
|
-
expect(result!.idsToDelete).toEqual(['doc0'])
|
|
197
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(2)
|
|
198
|
-
})
|
|
199
|
-
|
|
200
|
-
it('handles buffer size larger than excess', () => {
|
|
201
|
-
// 105 tombstones, max 100, buffer 50
|
|
202
|
-
// cutoff = 50 + 105 - 100 = 55
|
|
203
|
-
const tombstones = makeTombstones(105)
|
|
204
|
-
const result = computeTombstonePruning({
|
|
205
|
-
tombstones,
|
|
206
|
-
documentClock: 1000,
|
|
207
|
-
maxTombstones: 100,
|
|
208
|
-
pruneBufferSize: 50,
|
|
209
|
-
})
|
|
210
|
-
|
|
211
|
-
expect(result).not.toBeNull()
|
|
212
|
-
expect(result!.idsToDelete.length).toBe(55)
|
|
213
|
-
})
|
|
214
|
-
|
|
215
|
-
it('uses documentClock when all tombstones are deleted', () => {
|
|
216
|
-
// Small array, aggressive pruning
|
|
217
|
-
const tombstones = makeTombstones(10)
|
|
218
|
-
// max 5, buffer 10 => cutoff = 10 + 10 - 5 = 15, but only 10 items
|
|
219
|
-
// So all get deleted
|
|
220
|
-
const result = computeTombstonePruning({
|
|
221
|
-
tombstones,
|
|
222
|
-
documentClock: 999,
|
|
223
|
-
maxTombstones: 5,
|
|
224
|
-
pruneBufferSize: 10,
|
|
225
|
-
})
|
|
226
|
-
|
|
227
|
-
expect(result).not.toBeNull()
|
|
228
|
-
expect(result!.idsToDelete.length).toBe(10)
|
|
229
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(999)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('handles non-contiguous clock values', () => {
|
|
233
|
-
// Clocks with gaps: [1, 5, 10, 50, 100]
|
|
234
|
-
const tombstones = [
|
|
235
|
-
{ id: 'a', clock: 1 },
|
|
236
|
-
{ id: 'b', clock: 5 },
|
|
237
|
-
{ id: 'c', clock: 10 },
|
|
238
|
-
{ id: 'd', clock: 50 },
|
|
239
|
-
{ id: 'e', clock: 100 },
|
|
240
|
-
]
|
|
241
|
-
const result = computeTombstonePruning({
|
|
242
|
-
tombstones,
|
|
243
|
-
documentClock: 200,
|
|
244
|
-
maxTombstones: 3,
|
|
245
|
-
pruneBufferSize: 1,
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
expect(result).not.toBeNull()
|
|
249
|
-
// cutoff = 1 + 5 - 3 = 3
|
|
250
|
-
expect(result!.idsToDelete).toEqual(['a', 'b', 'c'])
|
|
251
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(50)
|
|
252
|
-
})
|
|
253
|
-
|
|
254
|
-
it('handles large clock values', () => {
|
|
255
|
-
const tombstones = [
|
|
256
|
-
{ id: 'a', clock: 1_000_000_000 },
|
|
257
|
-
{ id: 'b', clock: 1_000_000_001 },
|
|
258
|
-
{ id: 'c', clock: 1_000_000_002 },
|
|
259
|
-
]
|
|
260
|
-
const result = computeTombstonePruning({
|
|
261
|
-
tombstones,
|
|
262
|
-
documentClock: 2000000000,
|
|
263
|
-
maxTombstones: 2,
|
|
264
|
-
pruneBufferSize: 0,
|
|
265
|
-
})
|
|
266
|
-
|
|
267
|
-
expect(result).not.toBeNull()
|
|
268
|
-
expect(result!.idsToDelete).toEqual(['a'])
|
|
269
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(1_000_000_001)
|
|
270
|
-
})
|
|
271
|
-
})
|
|
272
|
-
|
|
273
|
-
describe('with default constants', () => {
|
|
274
|
-
it('does not prune at exactly MAX_TOMBSTONES', () => {
|
|
275
|
-
const tombstones = makeTombstones(MAX_TOMBSTONES)
|
|
276
|
-
const result = computeTombstonePruning({ tombstones, documentClock: 100000 })
|
|
277
|
-
expect(result).toBeNull()
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
it('prunes when exceeding MAX_TOMBSTONES by 1', () => {
|
|
281
|
-
const tombstones = makeTombstones(MAX_TOMBSTONES + 1)
|
|
282
|
-
const result = computeTombstonePruning({ tombstones, documentClock: 100000 })
|
|
283
|
-
|
|
284
|
-
expect(result).not.toBeNull()
|
|
285
|
-
// cutoff = BUFFER + (MAX+1) - MAX = BUFFER + 1
|
|
286
|
-
expect(result!.idsToDelete.length).toBe(TOMBSTONE_PRUNE_BUFFER_SIZE + 1)
|
|
287
|
-
})
|
|
288
|
-
|
|
289
|
-
it('prunes correctly with large excess', () => {
|
|
290
|
-
const totalTombstones = MAX_TOMBSTONES * 2
|
|
291
|
-
const tombstones = makeTombstones(totalTombstones)
|
|
292
|
-
const result = computeTombstonePruning({ tombstones, documentClock: 100000 })
|
|
293
|
-
|
|
294
|
-
expect(result).not.toBeNull()
|
|
295
|
-
// cutoff = BUFFER + 2*MAX - MAX = BUFFER + MAX
|
|
296
|
-
const expectedDeletes = TOMBSTONE_PRUNE_BUFFER_SIZE + MAX_TOMBSTONES
|
|
297
|
-
expect(result!.idsToDelete.length).toBe(expectedDeletes)
|
|
298
|
-
|
|
299
|
-
// Remaining should be MAX - BUFFER
|
|
300
|
-
const remaining = totalTombstones - result!.idsToDelete.length
|
|
301
|
-
expect(remaining).toBe(MAX_TOMBSTONES - TOMBSTONE_PRUNE_BUFFER_SIZE)
|
|
302
|
-
})
|
|
303
|
-
})
|
|
304
|
-
|
|
305
|
-
describe('input validation assumptions', () => {
|
|
306
|
-
it('assumes tombstones are sorted by clock ascending', () => {
|
|
307
|
-
// If not sorted, results are undefined - this documents the assumption
|
|
308
|
-
const sortedTombstones = [
|
|
309
|
-
{ id: 'a', clock: 1 },
|
|
310
|
-
{ id: 'b', clock: 2 },
|
|
311
|
-
{ id: 'c', clock: 3 },
|
|
312
|
-
]
|
|
313
|
-
const result = computeTombstonePruning({
|
|
314
|
-
tombstones: sortedTombstones,
|
|
315
|
-
documentClock: 100,
|
|
316
|
-
maxTombstones: 2,
|
|
317
|
-
pruneBufferSize: 0,
|
|
318
|
-
})
|
|
319
|
-
expect(result).not.toBeNull()
|
|
320
|
-
expect(result!.idsToDelete).toEqual(['a'])
|
|
321
|
-
})
|
|
322
|
-
|
|
323
|
-
it('works with zero buffer size', () => {
|
|
324
|
-
const tombstones = makeTombstones(10)
|
|
325
|
-
const result = computeTombstonePruning({
|
|
326
|
-
tombstones,
|
|
327
|
-
documentClock: 100,
|
|
328
|
-
maxTombstones: 5,
|
|
329
|
-
pruneBufferSize: 0,
|
|
330
|
-
})
|
|
331
|
-
|
|
332
|
-
expect(result).not.toBeNull()
|
|
333
|
-
// cutoff = 0 + 10 - 5 = 5
|
|
334
|
-
expect(result!.idsToDelete.length).toBe(5)
|
|
335
|
-
})
|
|
336
|
-
|
|
337
|
-
it('works with zero max tombstones (aggressive pruning)', () => {
|
|
338
|
-
const tombstones = makeTombstones(10)
|
|
339
|
-
const result = computeTombstonePruning({
|
|
340
|
-
tombstones,
|
|
341
|
-
documentClock: 100,
|
|
342
|
-
maxTombstones: 0,
|
|
343
|
-
pruneBufferSize: 0,
|
|
344
|
-
})
|
|
345
|
-
|
|
346
|
-
expect(result).not.toBeNull()
|
|
347
|
-
// cutoff = 0 + 10 - 0 = 10
|
|
348
|
-
expect(result!.idsToDelete.length).toBe(10)
|
|
349
|
-
expect(result!.newTombstoneHistoryStartsAtClock).toBe(100)
|
|
350
|
-
})
|
|
351
|
-
})
|
|
352
|
-
})
|
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
})
|