@tldraw/sync-core 5.2.0-canary.4a316fdfb2bb → 5.2.0-canary.4e00299c3e28
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
package/src/test/diff.test.ts
DELETED
|
@@ -1,784 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it } from 'vitest'
|
|
2
|
-
import {
|
|
3
|
-
applyObjectDiff,
|
|
4
|
-
diffRecord,
|
|
5
|
-
getNetworkDiff,
|
|
6
|
-
RecordOpType,
|
|
7
|
-
ValueOpType,
|
|
8
|
-
type ObjectDiff,
|
|
9
|
-
} from '../lib/diff'
|
|
10
|
-
|
|
11
|
-
describe('nested arrays', () => {
|
|
12
|
-
it('should be patchable at the end', () => {
|
|
13
|
-
const a = {
|
|
14
|
-
arr: [
|
|
15
|
-
[1, 2, 3],
|
|
16
|
-
[4, 5, 6],
|
|
17
|
-
],
|
|
18
|
-
}
|
|
19
|
-
const b = {
|
|
20
|
-
arr: [
|
|
21
|
-
[1, 2, 3],
|
|
22
|
-
[4, 5, 6, 7, 8],
|
|
23
|
-
],
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
expect(diffRecord(a, b)).toMatchInlineSnapshot(`
|
|
27
|
-
{
|
|
28
|
-
"arr": [
|
|
29
|
-
"patch",
|
|
30
|
-
{
|
|
31
|
-
"1": [
|
|
32
|
-
"append",
|
|
33
|
-
[
|
|
34
|
-
7,
|
|
35
|
-
8,
|
|
36
|
-
],
|
|
37
|
-
3,
|
|
38
|
-
],
|
|
39
|
-
},
|
|
40
|
-
],
|
|
41
|
-
}
|
|
42
|
-
`)
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
it('should be patchable at the beginning', () => {
|
|
46
|
-
const a = {
|
|
47
|
-
arr: [
|
|
48
|
-
[1, 2, 3],
|
|
49
|
-
[4, 5, 6],
|
|
50
|
-
],
|
|
51
|
-
}
|
|
52
|
-
const b = {
|
|
53
|
-
arr: [
|
|
54
|
-
[1, 2, 3, 4, 5, 6],
|
|
55
|
-
[4, 5, 6],
|
|
56
|
-
],
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
expect(diffRecord(a, b)).toMatchInlineSnapshot(`
|
|
60
|
-
{
|
|
61
|
-
"arr": [
|
|
62
|
-
"patch",
|
|
63
|
-
{
|
|
64
|
-
"0": [
|
|
65
|
-
"append",
|
|
66
|
-
[
|
|
67
|
-
4,
|
|
68
|
-
5,
|
|
69
|
-
6,
|
|
70
|
-
],
|
|
71
|
-
3,
|
|
72
|
-
],
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
}
|
|
76
|
-
`)
|
|
77
|
-
})
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
describe('objects inside arrays', () => {
|
|
81
|
-
it('should be patchable if only item changes', () => {
|
|
82
|
-
const a = {
|
|
83
|
-
arr: [
|
|
84
|
-
{ a: 1, b: 2, c: 3 },
|
|
85
|
-
{ a: 4, b: 5, c: 6 },
|
|
86
|
-
],
|
|
87
|
-
}
|
|
88
|
-
const b = {
|
|
89
|
-
arr: [
|
|
90
|
-
{ a: 1, b: 2, c: 3 },
|
|
91
|
-
{ a: 4, b: 5, c: 7 },
|
|
92
|
-
],
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
expect(diffRecord(a, b)).toMatchInlineSnapshot(`
|
|
96
|
-
{
|
|
97
|
-
"arr": [
|
|
98
|
-
"patch",
|
|
99
|
-
{
|
|
100
|
-
"1": [
|
|
101
|
-
"patch",
|
|
102
|
-
{
|
|
103
|
-
"c": [
|
|
104
|
-
"put",
|
|
105
|
-
7,
|
|
106
|
-
],
|
|
107
|
-
},
|
|
108
|
-
],
|
|
109
|
-
},
|
|
110
|
-
],
|
|
111
|
-
}
|
|
112
|
-
`)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should return a put op if many items change', () => {
|
|
116
|
-
const a = {
|
|
117
|
-
arr: [
|
|
118
|
-
{ a: 1, b: 2, c: 3 },
|
|
119
|
-
{ a: 4, b: 5, c: 6 },
|
|
120
|
-
],
|
|
121
|
-
}
|
|
122
|
-
const b = {
|
|
123
|
-
arr: [
|
|
124
|
-
{ a: 1, b: 2, c: 5 },
|
|
125
|
-
{ a: 4, b: 5, c: 7 },
|
|
126
|
-
],
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
expect(diffRecord(a, b)).toMatchInlineSnapshot(`
|
|
130
|
-
{
|
|
131
|
-
"arr": [
|
|
132
|
-
"put",
|
|
133
|
-
[
|
|
134
|
-
{
|
|
135
|
-
"a": 1,
|
|
136
|
-
"b": 2,
|
|
137
|
-
"c": 5,
|
|
138
|
-
},
|
|
139
|
-
{
|
|
140
|
-
"a": 4,
|
|
141
|
-
"b": 5,
|
|
142
|
-
"c": 7,
|
|
143
|
-
},
|
|
144
|
-
],
|
|
145
|
-
],
|
|
146
|
-
}
|
|
147
|
-
`)
|
|
148
|
-
})
|
|
149
|
-
})
|
|
150
|
-
|
|
151
|
-
test('deleting things from a record', () => {
|
|
152
|
-
const a = {
|
|
153
|
-
a: 1,
|
|
154
|
-
b: 2,
|
|
155
|
-
c: 3,
|
|
156
|
-
}
|
|
157
|
-
const b = {
|
|
158
|
-
a: 1,
|
|
159
|
-
b: 2,
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
const patch = diffRecord(a, b)
|
|
163
|
-
expect(patch).toMatchInlineSnapshot(`
|
|
164
|
-
{
|
|
165
|
-
"c": [
|
|
166
|
-
"delete",
|
|
167
|
-
],
|
|
168
|
-
}
|
|
169
|
-
`)
|
|
170
|
-
|
|
171
|
-
expect(applyObjectDiff(a, patch!)).toEqual(b)
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
test('adding things things to a record', () => {
|
|
175
|
-
const a = {
|
|
176
|
-
a: 1,
|
|
177
|
-
b: 2,
|
|
178
|
-
}
|
|
179
|
-
const b = {
|
|
180
|
-
a: 1,
|
|
181
|
-
b: 2,
|
|
182
|
-
c: 3,
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
const patch = diffRecord(a, b)
|
|
186
|
-
|
|
187
|
-
expect(patch).toMatchInlineSnapshot(`
|
|
188
|
-
{
|
|
189
|
-
"c": [
|
|
190
|
-
"put",
|
|
191
|
-
3,
|
|
192
|
-
],
|
|
193
|
-
}
|
|
194
|
-
`)
|
|
195
|
-
|
|
196
|
-
expect(applyObjectDiff(a, patch!)).toEqual(b)
|
|
197
|
-
})
|
|
198
|
-
|
|
199
|
-
describe('getNetworkDiff', () => {
|
|
200
|
-
it('should return null for empty diff', () => {
|
|
201
|
-
const diff = { added: {}, updated: {}, removed: {} }
|
|
202
|
-
expect(getNetworkDiff(diff)).toBeNull()
|
|
203
|
-
})
|
|
204
|
-
|
|
205
|
-
it('should handle added records', () => {
|
|
206
|
-
const record = { id: 'test:1', type: 'test', data: 'value' }
|
|
207
|
-
const diff = {
|
|
208
|
-
added: { 'test:1': record },
|
|
209
|
-
updated: {},
|
|
210
|
-
removed: {},
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
const networkDiff = getNetworkDiff(diff)
|
|
214
|
-
expect(networkDiff).toEqual({
|
|
215
|
-
'test:1': [RecordOpType.Put, record],
|
|
216
|
-
})
|
|
217
|
-
})
|
|
218
|
-
|
|
219
|
-
it('should handle removed records', () => {
|
|
220
|
-
const diff = {
|
|
221
|
-
added: {},
|
|
222
|
-
updated: {},
|
|
223
|
-
removed: { 'test:1': { id: 'test:1', type: 'test' } },
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const networkDiff = getNetworkDiff(diff)
|
|
227
|
-
expect(networkDiff).toEqual({
|
|
228
|
-
'test:1': [RecordOpType.Remove],
|
|
229
|
-
})
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
it('should handle updated records with patches', () => {
|
|
233
|
-
const prev = { id: 'test:1', type: 'test', x: 100, y: 200 }
|
|
234
|
-
const next = { id: 'test:1', type: 'test', x: 150, y: 200 }
|
|
235
|
-
const diff = {
|
|
236
|
-
added: {},
|
|
237
|
-
updated: { 'test:1': [prev, next] },
|
|
238
|
-
removed: {},
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
const networkDiff = getNetworkDiff(diff)
|
|
242
|
-
expect(networkDiff).toEqual({
|
|
243
|
-
'test:1': [RecordOpType.Patch, { x: [ValueOpType.Put, 150] }],
|
|
244
|
-
})
|
|
245
|
-
})
|
|
246
|
-
|
|
247
|
-
it('should skip updates when no diff exists', () => {
|
|
248
|
-
const record = { id: 'test:1', type: 'test', x: 100 }
|
|
249
|
-
const diff = {
|
|
250
|
-
added: {},
|
|
251
|
-
updated: { 'test:1': [record, record] },
|
|
252
|
-
removed: {},
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const networkDiff = getNetworkDiff(diff)
|
|
256
|
-
expect(networkDiff).toBeNull()
|
|
257
|
-
})
|
|
258
|
-
|
|
259
|
-
it('should handle mixed operations', () => {
|
|
260
|
-
const addedRecord = { id: 'test:1', type: 'test', data: 'new' }
|
|
261
|
-
const prevRecord = { id: 'test:2', type: 'test', x: 100 }
|
|
262
|
-
const nextRecord = { id: 'test:2', type: 'test', x: 200 }
|
|
263
|
-
const removedRecord = { id: 'test:3', type: 'test' }
|
|
264
|
-
|
|
265
|
-
const diff = {
|
|
266
|
-
added: { 'test:1': addedRecord },
|
|
267
|
-
updated: { 'test:2': [prevRecord, nextRecord] },
|
|
268
|
-
removed: { 'test:3': removedRecord },
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
const networkDiff = getNetworkDiff(diff)
|
|
272
|
-
expect(networkDiff).toEqual({
|
|
273
|
-
'test:1': [RecordOpType.Put, addedRecord],
|
|
274
|
-
'test:2': [RecordOpType.Patch, { x: [ValueOpType.Put, 200] }],
|
|
275
|
-
'test:3': [RecordOpType.Remove],
|
|
276
|
-
})
|
|
277
|
-
})
|
|
278
|
-
})
|
|
279
|
-
|
|
280
|
-
describe('diffRecord comprehensive tests', () => {
|
|
281
|
-
it('should return null for identical records', () => {
|
|
282
|
-
const record = { id: 'test:1', x: 100, y: 200 }
|
|
283
|
-
expect(diffRecord(record, record)).toBeNull()
|
|
284
|
-
})
|
|
285
|
-
|
|
286
|
-
it('should handle simple property changes', () => {
|
|
287
|
-
const prev = { id: 'test:1', x: 100, y: 200 }
|
|
288
|
-
const next = { id: 'test:1', x: 150, y: 200 }
|
|
289
|
-
|
|
290
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
291
|
-
x: [ValueOpType.Put, 150],
|
|
292
|
-
})
|
|
293
|
-
})
|
|
294
|
-
|
|
295
|
-
it('should handle nested props changes', () => {
|
|
296
|
-
const prev = { id: 'test:1', props: { color: 'red', size: 'medium' } }
|
|
297
|
-
const next = { id: 'test:1', props: { color: 'blue', size: 'medium' } }
|
|
298
|
-
|
|
299
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
300
|
-
props: [ValueOpType.Patch, { color: [ValueOpType.Put, 'blue'] }],
|
|
301
|
-
})
|
|
302
|
-
})
|
|
303
|
-
|
|
304
|
-
it('should handle adding nested props', () => {
|
|
305
|
-
const prev = { id: 'test:1', props: { color: 'red' } }
|
|
306
|
-
const next = { id: 'test:1', props: { color: 'red', size: 'large' } }
|
|
307
|
-
|
|
308
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
309
|
-
props: [ValueOpType.Patch, { size: [ValueOpType.Put, 'large'] }],
|
|
310
|
-
})
|
|
311
|
-
})
|
|
312
|
-
|
|
313
|
-
it('should handle removing nested props', () => {
|
|
314
|
-
const prev = { id: 'test:1', props: { color: 'red', size: 'large' } }
|
|
315
|
-
const next = { id: 'test:1', props: { color: 'red' } }
|
|
316
|
-
|
|
317
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
318
|
-
props: [ValueOpType.Patch, { size: [ValueOpType.Delete] }],
|
|
319
|
-
})
|
|
320
|
-
})
|
|
321
|
-
|
|
322
|
-
it('should handle multiple property changes', () => {
|
|
323
|
-
const prev = { id: 'test:1', x: 100, y: 200, rotation: 0 }
|
|
324
|
-
const next = { id: 'test:1', x: 150, y: 250, rotation: 45 }
|
|
325
|
-
|
|
326
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
327
|
-
x: [ValueOpType.Put, 150],
|
|
328
|
-
y: [ValueOpType.Put, 250],
|
|
329
|
-
rotation: [ValueOpType.Put, 45],
|
|
330
|
-
})
|
|
331
|
-
})
|
|
332
|
-
|
|
333
|
-
it('should handle null and undefined values', () => {
|
|
334
|
-
const prev = { id: 'test:1', optional: 'value', nullable: null }
|
|
335
|
-
const next = { id: 'test:1', optional: undefined, nullable: 'value' }
|
|
336
|
-
|
|
337
|
-
const diff = diffRecord(prev, next)
|
|
338
|
-
expect(diff).toBeTruthy()
|
|
339
|
-
expect(diff!.optional).toEqual([ValueOpType.Put, undefined])
|
|
340
|
-
expect(diff!.nullable).toEqual([ValueOpType.Put, 'value'])
|
|
341
|
-
})
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
describe('array diffing comprehensive', () => {
|
|
345
|
-
describe('simple arrays', () => {
|
|
346
|
-
it('should handle identical arrays', () => {
|
|
347
|
-
const prev = { arr: [1, 2, 3] }
|
|
348
|
-
const next = { arr: [1, 2, 3] }
|
|
349
|
-
|
|
350
|
-
expect(diffRecord(prev, next)).toBeNull()
|
|
351
|
-
})
|
|
352
|
-
|
|
353
|
-
it('should handle array appends', () => {
|
|
354
|
-
const prev = { arr: [1, 2, 3] }
|
|
355
|
-
const next = { arr: [1, 2, 3, 4, 5] }
|
|
356
|
-
|
|
357
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
358
|
-
arr: [ValueOpType.Append, [4, 5], 3],
|
|
359
|
-
})
|
|
360
|
-
})
|
|
361
|
-
|
|
362
|
-
it('should replace array when prefix changes', () => {
|
|
363
|
-
const prev = { arr: [1, 2, 3] }
|
|
364
|
-
const next = { arr: [1, 3, 4] }
|
|
365
|
-
|
|
366
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
367
|
-
arr: [ValueOpType.Put, [1, 3, 4]],
|
|
368
|
-
})
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
it('should patch few items in same-length arrays', () => {
|
|
372
|
-
const prev = { arr: [1, 2, 3, 4, 5] }
|
|
373
|
-
const next = { arr: [1, 9, 3, 4, 5] }
|
|
374
|
-
|
|
375
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
376
|
-
arr: [ValueOpType.Patch, { '1': [ValueOpType.Put, 9] }],
|
|
377
|
-
})
|
|
378
|
-
})
|
|
379
|
-
|
|
380
|
-
it('should replace array when too many items change', () => {
|
|
381
|
-
const prev = { arr: [1, 2, 3, 4, 5] }
|
|
382
|
-
const next = { arr: [6, 7, 8, 9, 10] }
|
|
383
|
-
|
|
384
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
385
|
-
arr: [ValueOpType.Put, [6, 7, 8, 9, 10]],
|
|
386
|
-
})
|
|
387
|
-
})
|
|
388
|
-
})
|
|
389
|
-
|
|
390
|
-
describe('empty arrays', () => {
|
|
391
|
-
it('should handle empty to non-empty', () => {
|
|
392
|
-
const prev = { arr: [] }
|
|
393
|
-
const next = { arr: [1, 2, 3] }
|
|
394
|
-
|
|
395
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
396
|
-
arr: [ValueOpType.Append, [1, 2, 3], 0],
|
|
397
|
-
})
|
|
398
|
-
})
|
|
399
|
-
|
|
400
|
-
it('should handle non-empty to empty', () => {
|
|
401
|
-
const prev = { arr: [1, 2, 3] }
|
|
402
|
-
const next = { arr: [] }
|
|
403
|
-
|
|
404
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
405
|
-
arr: [ValueOpType.Put, []],
|
|
406
|
-
})
|
|
407
|
-
})
|
|
408
|
-
})
|
|
409
|
-
})
|
|
410
|
-
|
|
411
|
-
describe('string appending', () => {
|
|
412
|
-
describe('basic string appending', () => {
|
|
413
|
-
it('should handle string appends', () => {
|
|
414
|
-
const prev = { text: 'Hello' }
|
|
415
|
-
const next = { text: 'Hello world' }
|
|
416
|
-
|
|
417
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
418
|
-
text: [ValueOpType.Append, ' world', 5],
|
|
419
|
-
})
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
it('should handle empty string to non-empty', () => {
|
|
423
|
-
const prev = { text: '' }
|
|
424
|
-
const next = { text: 'Hello' }
|
|
425
|
-
|
|
426
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
427
|
-
text: [ValueOpType.Append, 'Hello', 0],
|
|
428
|
-
})
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
it('should use put when string is replaced (not appended)', () => {
|
|
432
|
-
const prev = { text: 'Hello' }
|
|
433
|
-
const next = { text: 'Goodbye' }
|
|
434
|
-
|
|
435
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
436
|
-
text: [ValueOpType.Put, 'Goodbye'],
|
|
437
|
-
})
|
|
438
|
-
})
|
|
439
|
-
|
|
440
|
-
it('should use put when string is shortened', () => {
|
|
441
|
-
const prev = { text: 'Hello world' }
|
|
442
|
-
const next = { text: 'Hello' }
|
|
443
|
-
|
|
444
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
445
|
-
text: [ValueOpType.Put, 'Hello'],
|
|
446
|
-
})
|
|
447
|
-
})
|
|
448
|
-
|
|
449
|
-
it('should handle identical strings', () => {
|
|
450
|
-
const prev = { text: 'Hello' }
|
|
451
|
-
const next = { text: 'Hello' }
|
|
452
|
-
|
|
453
|
-
expect(diffRecord(prev, next)).toBeNull()
|
|
454
|
-
})
|
|
455
|
-
|
|
456
|
-
it('should handle large text append', () => {
|
|
457
|
-
const prev = { text: 'Start' }
|
|
458
|
-
const longText = ' '.repeat(1000) + 'end'
|
|
459
|
-
const next = { text: 'Start' + longText }
|
|
460
|
-
|
|
461
|
-
const diff = diffRecord(prev, next)
|
|
462
|
-
expect(diff).toEqual({
|
|
463
|
-
text: [ValueOpType.Append, longText, 5],
|
|
464
|
-
})
|
|
465
|
-
})
|
|
466
|
-
})
|
|
467
|
-
|
|
468
|
-
describe('string appending in nested props', () => {
|
|
469
|
-
it('should handle string appending in nested props', () => {
|
|
470
|
-
const prev = { id: 'test:1', props: { label: 'Hello' } }
|
|
471
|
-
const next = { id: 'test:1', props: { label: 'Hello world' } }
|
|
472
|
-
|
|
473
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
474
|
-
props: [ValueOpType.Patch, { label: [ValueOpType.Append, ' world', 5] }],
|
|
475
|
-
})
|
|
476
|
-
})
|
|
477
|
-
|
|
478
|
-
it('should combine string appending with other property changes', () => {
|
|
479
|
-
const prev = { text: 'Hello', x: 100 }
|
|
480
|
-
const next = { text: 'Hello world', x: 200 }
|
|
481
|
-
|
|
482
|
-
expect(diffRecord(prev, next)).toEqual({
|
|
483
|
-
text: [ValueOpType.Append, ' world', 5],
|
|
484
|
-
x: [ValueOpType.Put, 200],
|
|
485
|
-
})
|
|
486
|
-
})
|
|
487
|
-
})
|
|
488
|
-
|
|
489
|
-
describe('apply string appending', () => {
|
|
490
|
-
it('should apply append operations correctly', () => {
|
|
491
|
-
const obj = { text: 'Hello' }
|
|
492
|
-
const diff: ObjectDiff = {
|
|
493
|
-
text: [ValueOpType.Append, ' world', 5],
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
const result = applyObjectDiff(obj, diff)
|
|
497
|
-
expect(result).toEqual({ text: 'Hello world' })
|
|
498
|
-
expect(result).not.toBe(obj)
|
|
499
|
-
})
|
|
500
|
-
|
|
501
|
-
it('should handle append from empty string', () => {
|
|
502
|
-
const obj = { text: '' }
|
|
503
|
-
const diff: ObjectDiff = {
|
|
504
|
-
text: [ValueOpType.Append, 'Hello', 0],
|
|
505
|
-
}
|
|
506
|
-
|
|
507
|
-
const result = applyObjectDiff(obj, diff)
|
|
508
|
-
expect(result).toEqual({ text: 'Hello' })
|
|
509
|
-
})
|
|
510
|
-
|
|
511
|
-
it('should ignore append operation with wrong offset', () => {
|
|
512
|
-
const obj = { text: 'Hello' }
|
|
513
|
-
const diff: ObjectDiff = {
|
|
514
|
-
text: [ValueOpType.Append, ' world', 10], // Wrong offset
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
const result = applyObjectDiff(obj, diff)
|
|
518
|
-
expect(result).toBe(obj) // No change, same reference
|
|
519
|
-
})
|
|
520
|
-
|
|
521
|
-
it('should ignore append operation on non-string value', () => {
|
|
522
|
-
const obj = { text: 123 }
|
|
523
|
-
const diff: ObjectDiff = {
|
|
524
|
-
text: [ValueOpType.Append, ' world', 3],
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const result = applyObjectDiff(obj, diff)
|
|
528
|
-
expect(result).toBe(obj) // No change, same reference
|
|
529
|
-
})
|
|
530
|
-
|
|
531
|
-
it('should handle multiple stream operations', () => {
|
|
532
|
-
const obj = { a: 'Hello', b: 'Foo' }
|
|
533
|
-
const diff: ObjectDiff = {
|
|
534
|
-
a: [ValueOpType.Append, ' world', 5],
|
|
535
|
-
b: [ValueOpType.Append, 'bar', 3],
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
const result = applyObjectDiff(obj, diff)
|
|
539
|
-
expect(result).toEqual({ a: 'Hello world', b: 'Foobar' })
|
|
540
|
-
})
|
|
541
|
-
|
|
542
|
-
it('should integrate with network diff workflow', () => {
|
|
543
|
-
const prev = { id: 'shape:1', type: 'text', text: 'Hello' }
|
|
544
|
-
const next = { id: 'shape:1', type: 'text', text: 'Hello world' }
|
|
545
|
-
|
|
546
|
-
const recordsDiff = {
|
|
547
|
-
added: {},
|
|
548
|
-
updated: { 'shape:1': [prev, next] },
|
|
549
|
-
removed: {},
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
const networkDiff = getNetworkDiff(recordsDiff)
|
|
553
|
-
expect(networkDiff).toEqual({
|
|
554
|
-
'shape:1': [RecordOpType.Patch, { text: [ValueOpType.Append, ' world', 5] }],
|
|
555
|
-
})
|
|
556
|
-
})
|
|
557
|
-
})
|
|
558
|
-
})
|
|
559
|
-
|
|
560
|
-
describe('applyObjectDiff comprehensive', () => {
|
|
561
|
-
describe('basic operations', () => {
|
|
562
|
-
it('should create new object when changes are needed', () => {
|
|
563
|
-
const obj = { a: 1, b: 2 }
|
|
564
|
-
const diff: ObjectDiff = { a: [ValueOpType.Put, 5] }
|
|
565
|
-
|
|
566
|
-
const result = applyObjectDiff(obj, diff)
|
|
567
|
-
expect(result).not.toBe(obj)
|
|
568
|
-
expect(result).toEqual({ a: 5, b: 2 })
|
|
569
|
-
})
|
|
570
|
-
|
|
571
|
-
it('should handle put operations', () => {
|
|
572
|
-
const obj = { a: 1, b: 2 }
|
|
573
|
-
const diff: ObjectDiff = {
|
|
574
|
-
a: [ValueOpType.Put, 10],
|
|
575
|
-
c: [ValueOpType.Put, 30],
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const result = applyObjectDiff(obj, diff)
|
|
579
|
-
expect(result).toEqual({ a: 10, b: 2, c: 30 })
|
|
580
|
-
})
|
|
581
|
-
|
|
582
|
-
it('should handle delete operations', () => {
|
|
583
|
-
const obj = { a: 1, b: 2, c: 3 }
|
|
584
|
-
const diff: ObjectDiff = { b: [ValueOpType.Delete] }
|
|
585
|
-
|
|
586
|
-
const result = applyObjectDiff(obj, diff)
|
|
587
|
-
expect(result).toEqual({ a: 1, c: 3 })
|
|
588
|
-
expect('b' in result).toBe(false)
|
|
589
|
-
})
|
|
590
|
-
})
|
|
591
|
-
|
|
592
|
-
describe('nested patch operations', () => {
|
|
593
|
-
it('should handle nested object patches', () => {
|
|
594
|
-
const obj = { a: 1, nested: { x: 10, y: 20 } }
|
|
595
|
-
const diff: ObjectDiff = {
|
|
596
|
-
nested: [ValueOpType.Patch, { x: [ValueOpType.Put, 100] }],
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
const result = applyObjectDiff(obj, diff)
|
|
600
|
-
expect(result).toEqual({ a: 1, nested: { x: 100, y: 20 } })
|
|
601
|
-
expect(result.nested).not.toBe(obj.nested)
|
|
602
|
-
})
|
|
603
|
-
|
|
604
|
-
it('should handle deeply nested patches', () => {
|
|
605
|
-
const obj = {
|
|
606
|
-
level1: {
|
|
607
|
-
level2: {
|
|
608
|
-
level3: { value: 'old' },
|
|
609
|
-
},
|
|
610
|
-
},
|
|
611
|
-
}
|
|
612
|
-
const diff: ObjectDiff = {
|
|
613
|
-
level1: [
|
|
614
|
-
ValueOpType.Patch,
|
|
615
|
-
{
|
|
616
|
-
level2: [
|
|
617
|
-
ValueOpType.Patch,
|
|
618
|
-
{
|
|
619
|
-
level3: [
|
|
620
|
-
ValueOpType.Patch,
|
|
621
|
-
{
|
|
622
|
-
value: [ValueOpType.Put, 'new'],
|
|
623
|
-
},
|
|
624
|
-
],
|
|
625
|
-
},
|
|
626
|
-
],
|
|
627
|
-
},
|
|
628
|
-
],
|
|
629
|
-
}
|
|
630
|
-
|
|
631
|
-
const result = applyObjectDiff(obj, diff)
|
|
632
|
-
expect(result.level1.level2.level3.value).toBe('new')
|
|
633
|
-
})
|
|
634
|
-
})
|
|
635
|
-
|
|
636
|
-
describe('array operations', () => {
|
|
637
|
-
it('should handle array append operations', () => {
|
|
638
|
-
const obj = { arr: [1, 2, 3] }
|
|
639
|
-
const diff: ObjectDiff = {
|
|
640
|
-
arr: [ValueOpType.Append, [4, 5], 3],
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
const result = applyObjectDiff(obj, diff)
|
|
644
|
-
expect(result).toEqual({ arr: [1, 2, 3, 4, 5] })
|
|
645
|
-
})
|
|
646
|
-
|
|
647
|
-
it('should handle array patch operations', () => {
|
|
648
|
-
const obj = { arr: [{ a: 1 }, { b: 2 }, { c: 3 }] }
|
|
649
|
-
const diff: ObjectDiff = {
|
|
650
|
-
arr: [
|
|
651
|
-
ValueOpType.Patch,
|
|
652
|
-
{
|
|
653
|
-
'1': [ValueOpType.Patch, { b: [ValueOpType.Put, 20] }],
|
|
654
|
-
},
|
|
655
|
-
],
|
|
656
|
-
}
|
|
657
|
-
|
|
658
|
-
const result = applyObjectDiff(obj, diff)
|
|
659
|
-
expect(result.arr[1]).toEqual({ b: 20 })
|
|
660
|
-
expect(result.arr[0]).toBe(obj.arr[0]) // Unchanged items should be same reference
|
|
661
|
-
})
|
|
662
|
-
})
|
|
663
|
-
|
|
664
|
-
describe('edge cases', () => {
|
|
665
|
-
it('should handle empty diffs', () => {
|
|
666
|
-
const obj = { a: 1, b: 2 }
|
|
667
|
-
const diff: ObjectDiff = {}
|
|
668
|
-
|
|
669
|
-
const result = applyObjectDiff(obj, diff)
|
|
670
|
-
expect(result).toBe(obj) // Should be same reference
|
|
671
|
-
})
|
|
672
|
-
})
|
|
673
|
-
})
|
|
674
|
-
|
|
675
|
-
describe('complex scenarios', () => {
|
|
676
|
-
it('should handle shape-like record updates', () => {
|
|
677
|
-
const prev = {
|
|
678
|
-
id: 'shape:123',
|
|
679
|
-
type: 'geo',
|
|
680
|
-
x: 100,
|
|
681
|
-
y: 200,
|
|
682
|
-
props: {
|
|
683
|
-
color: 'red',
|
|
684
|
-
size: 'medium',
|
|
685
|
-
geo: 'rectangle',
|
|
686
|
-
},
|
|
687
|
-
meta: {},
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
const next = {
|
|
691
|
-
id: 'shape:123',
|
|
692
|
-
type: 'geo',
|
|
693
|
-
x: 150,
|
|
694
|
-
y: 200,
|
|
695
|
-
props: {
|
|
696
|
-
color: 'blue',
|
|
697
|
-
size: 'medium',
|
|
698
|
-
geo: 'rectangle',
|
|
699
|
-
},
|
|
700
|
-
meta: { timestamp: Date.now() },
|
|
701
|
-
}
|
|
702
|
-
|
|
703
|
-
const diff = diffRecord(prev, next)
|
|
704
|
-
expect(diff).toBeTruthy()
|
|
705
|
-
expect(diff!.x).toEqual([ValueOpType.Put, 150])
|
|
706
|
-
expect(diff!.props).toEqual([ValueOpType.Patch, { color: [ValueOpType.Put, 'blue'] }])
|
|
707
|
-
expect(diff!.meta).toBeTruthy()
|
|
708
|
-
|
|
709
|
-
// Apply the diff and verify result
|
|
710
|
-
const result = applyObjectDiff(prev, diff!)
|
|
711
|
-
expect(result).toEqual(next)
|
|
712
|
-
})
|
|
713
|
-
|
|
714
|
-
it('should handle complete network diff workflow', () => {
|
|
715
|
-
const shape1 = { id: 'shape:1', type: 'geo', x: 100 }
|
|
716
|
-
const shape2prev = { id: 'shape:2', type: 'geo', x: 200 }
|
|
717
|
-
const shape2next = { id: 'shape:2', type: 'geo', x: 300 }
|
|
718
|
-
const shape3 = { id: 'shape:3', type: 'geo', x: 400 }
|
|
719
|
-
|
|
720
|
-
const recordsDiff = {
|
|
721
|
-
added: { 'shape:1': shape1 },
|
|
722
|
-
updated: { 'shape:2': [shape2prev, shape2next] },
|
|
723
|
-
removed: { 'shape:3': shape3 },
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
const networkDiff = getNetworkDiff(recordsDiff)
|
|
727
|
-
expect(networkDiff).toEqual({
|
|
728
|
-
'shape:1': [RecordOpType.Put, shape1],
|
|
729
|
-
'shape:2': [RecordOpType.Patch, { x: [ValueOpType.Put, 300] }],
|
|
730
|
-
'shape:3': [RecordOpType.Remove],
|
|
731
|
-
})
|
|
732
|
-
})
|
|
733
|
-
})
|
|
734
|
-
|
|
735
|
-
describe('nested key primitive value bug', () => {
|
|
736
|
-
it('should handle string changes in nested keys', () => {
|
|
737
|
-
// This tests the bug where nested keys (like 'props') with primitive values
|
|
738
|
-
// are silently dropped instead of being diffed properly
|
|
739
|
-
const prev = { id: 'shape:1', props: 'hello' }
|
|
740
|
-
const next = { id: 'shape:1', props: 'world' }
|
|
741
|
-
|
|
742
|
-
const diff = diffRecord(prev, next)
|
|
743
|
-
|
|
744
|
-
// The diff should contain a 'put' operation for props
|
|
745
|
-
expect(diff).toEqual({
|
|
746
|
-
props: [ValueOpType.Put, 'world'],
|
|
747
|
-
})
|
|
748
|
-
})
|
|
749
|
-
|
|
750
|
-
it('should handle string appending in nested keys', () => {
|
|
751
|
-
const prev = { id: 'shape:1', props: 'hello' }
|
|
752
|
-
const next = { id: 'shape:1', props: 'hello world' }
|
|
753
|
-
|
|
754
|
-
const diff = diffRecord(prev, next)
|
|
755
|
-
|
|
756
|
-
// The diff should contain an 'append' operation for props
|
|
757
|
-
expect(diff).toEqual({
|
|
758
|
-
props: [ValueOpType.Append, ' world', 5],
|
|
759
|
-
})
|
|
760
|
-
})
|
|
761
|
-
|
|
762
|
-
it('should handle number changes in nested keys', () => {
|
|
763
|
-
const prev = { id: 'shape:1', props: 42 }
|
|
764
|
-
const next = { id: 'shape:1', props: 100 }
|
|
765
|
-
|
|
766
|
-
const diff = diffRecord(prev, next)
|
|
767
|
-
|
|
768
|
-
expect(diff).toEqual({
|
|
769
|
-
props: [ValueOpType.Put, 100],
|
|
770
|
-
})
|
|
771
|
-
})
|
|
772
|
-
|
|
773
|
-
it('should still handle object changes in nested keys normally', () => {
|
|
774
|
-
const prev = { id: 'shape:1', props: { color: 'red' } }
|
|
775
|
-
const next = { id: 'shape:1', props: { color: 'blue' } }
|
|
776
|
-
|
|
777
|
-
const diff = diffRecord(prev, next)
|
|
778
|
-
|
|
779
|
-
// Objects in nested keys should still use patch
|
|
780
|
-
expect(diff).toEqual({
|
|
781
|
-
props: [ValueOpType.Patch, { color: [ValueOpType.Put, 'blue'] }],
|
|
782
|
-
})
|
|
783
|
-
})
|
|
784
|
-
})
|