@tldraw/sync-core 4.2.0-next.f100cedfc45b → 4.3.0-canary.03ae87dcc44b
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 +66 -0
- package/dist-cjs/index.js +2 -1
- package/dist-cjs/index.js.map +2 -2
- package/dist-cjs/lib/RoomSession.js.map +1 -1
- package/dist-cjs/lib/TLSyncRoom.js +35 -9
- package/dist-cjs/lib/TLSyncRoom.js.map +2 -2
- package/dist-cjs/lib/chunk.js +4 -4
- package/dist-cjs/lib/chunk.js.map +1 -1
- package/dist-cjs/lib/diff.js +29 -29
- package/dist-cjs/lib/diff.js.map +2 -2
- package/dist-cjs/lib/protocol.js +1 -1
- package/dist-cjs/lib/protocol.js.map +1 -1
- package/dist-esm/index.d.mts +66 -0
- package/dist-esm/index.mjs +3 -2
- package/dist-esm/index.mjs.map +2 -2
- package/dist-esm/lib/RoomSession.mjs.map +1 -1
- package/dist-esm/lib/TLSyncRoom.mjs +35 -9
- package/dist-esm/lib/TLSyncRoom.mjs.map +2 -2
- package/dist-esm/lib/chunk.mjs +4 -4
- package/dist-esm/lib/chunk.mjs.map +1 -1
- package/dist-esm/lib/diff.mjs +29 -29
- package/dist-esm/lib/diff.mjs.map +2 -2
- package/dist-esm/lib/protocol.mjs +1 -1
- package/dist-esm/lib/protocol.mjs.map +1 -1
- package/package.json +6 -6
- package/src/index.ts +2 -2
- package/src/lib/RoomSession.test.ts +3 -0
- package/src/lib/RoomSession.ts +28 -42
- package/src/lib/TLSyncRoom.ts +42 -7
- package/src/lib/chunk.ts +4 -4
- package/src/lib/diff.ts +55 -32
- package/src/lib/protocol.ts +1 -1
- package/src/test/FuzzEditor.ts +4 -5
- package/src/test/TLSocketRoom.test.ts +2 -2
- package/src/test/TLSyncRoom.test.ts +23 -22
- package/src/test/diff.test.ts +200 -0
- package/src/test/syncFuzz.test.ts +2 -4
package/src/test/FuzzEditor.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Editor,
|
|
3
3
|
PageRecordType,
|
|
4
|
-
TLArrowBinding,
|
|
5
4
|
TLPage,
|
|
6
5
|
TLPageId,
|
|
7
6
|
TLShape,
|
|
@@ -41,8 +40,8 @@ export type Op =
|
|
|
41
40
|
}
|
|
42
41
|
| {
|
|
43
42
|
type: 'create-arrow'
|
|
44
|
-
start:
|
|
45
|
-
end:
|
|
43
|
+
start: VecModel
|
|
44
|
+
end: VecModel
|
|
46
45
|
}
|
|
47
46
|
| {
|
|
48
47
|
type: 'delete-shape'
|
|
@@ -315,8 +314,8 @@ export class FuzzEditor extends RandomSource {
|
|
|
315
314
|
x: 0,
|
|
316
315
|
y: 0,
|
|
317
316
|
props: {
|
|
318
|
-
start: op.start,
|
|
319
|
-
end: op.end,
|
|
317
|
+
start: op.start as any,
|
|
318
|
+
end: op.end as any,
|
|
320
319
|
},
|
|
321
320
|
})
|
|
322
321
|
break
|
|
@@ -159,7 +159,7 @@ describe(TLSocketRoom, () => {
|
|
|
159
159
|
type: 'connect' as const,
|
|
160
160
|
connectRequestId: 'connect-1',
|
|
161
161
|
lastServerClock: 0,
|
|
162
|
-
protocolVersion:
|
|
162
|
+
protocolVersion: 8,
|
|
163
163
|
schema: store.schema.serialize(),
|
|
164
164
|
}
|
|
165
165
|
room.handleSocketMessage(sessionId1, JSON.stringify(connectRequest1))
|
|
@@ -168,7 +168,7 @@ describe(TLSocketRoom, () => {
|
|
|
168
168
|
type: 'connect' as const,
|
|
169
169
|
connectRequestId: 'connect-2',
|
|
170
170
|
lastServerClock: 0,
|
|
171
|
-
protocolVersion:
|
|
171
|
+
protocolVersion: 8,
|
|
172
172
|
schema: store.schema.serialize(),
|
|
173
173
|
}
|
|
174
174
|
room.handleSocketMessage(sessionId2, JSON.stringify(connectRequest2))
|
|
@@ -169,7 +169,7 @@ describe('TLSyncRoom', () => {
|
|
|
169
169
|
|
|
170
170
|
const room = new TLSyncRoom({
|
|
171
171
|
schema,
|
|
172
|
-
snapshot: makeSnapshot([...records, oldArrow], {
|
|
172
|
+
snapshot: makeSnapshot([...records, oldArrow as any], {
|
|
173
173
|
schema: oldSerializedSchema,
|
|
174
174
|
}),
|
|
175
175
|
})
|
|
@@ -313,27 +313,28 @@ describe('TLSyncRoom.updateStore', () => {
|
|
|
313
313
|
expect(documentClock).toBeLessThan(room.documentClock)
|
|
314
314
|
|
|
315
315
|
expect(socketA.__lastMessage).toMatchInlineSnapshot(`
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
316
|
+
{
|
|
317
|
+
"data": [
|
|
318
|
+
{
|
|
319
|
+
"diff": {
|
|
320
|
+
"document:document": [
|
|
321
|
+
"patch",
|
|
322
|
+
{
|
|
323
|
+
"name": [
|
|
324
|
+
"append",
|
|
325
|
+
"My lovely document",
|
|
326
|
+
0,
|
|
327
|
+
],
|
|
328
|
+
},
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
"serverClock": 1,
|
|
332
|
+
"type": "patch",
|
|
333
|
+
},
|
|
334
|
+
],
|
|
335
|
+
"type": "data",
|
|
336
|
+
}
|
|
337
|
+
`)
|
|
337
338
|
expect(socketB.__lastMessage).toEqual(socketA.__lastMessage)
|
|
338
339
|
})
|
|
339
340
|
|
package/src/test/diff.test.ts
CHANGED
|
@@ -408,6 +408,155 @@ describe('array diffing comprehensive', () => {
|
|
|
408
408
|
})
|
|
409
409
|
})
|
|
410
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
|
+
|
|
411
560
|
describe('applyObjectDiff comprehensive', () => {
|
|
412
561
|
describe('basic operations', () => {
|
|
413
562
|
it('should create new object when changes are needed', () => {
|
|
@@ -582,3 +731,54 @@ describe('complex scenarios', () => {
|
|
|
582
731
|
})
|
|
583
732
|
})
|
|
584
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
|
+
})
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Editor,
|
|
3
|
-
TLArrowBinding,
|
|
4
|
-
TLArrowShape,
|
|
5
3
|
TLRecord,
|
|
6
4
|
TLStore,
|
|
7
5
|
computed,
|
|
@@ -111,9 +109,9 @@ let totalNumShapes = 0
|
|
|
111
109
|
let totalNumPages = 0
|
|
112
110
|
|
|
113
111
|
function arrowsAreSound(editor: Editor) {
|
|
114
|
-
const arrows = editor.getCurrentPageShapes().filter((s)
|
|
112
|
+
const arrows = editor.getCurrentPageShapes().filter((s) => s.type === 'arrow')
|
|
115
113
|
for (const arrow of arrows) {
|
|
116
|
-
const bindings = editor.getBindingsFromShape
|
|
114
|
+
const bindings = editor.getBindingsFromShape(arrow, 'arrow')
|
|
117
115
|
const terminalsSeen = new Set()
|
|
118
116
|
for (const binding of bindings) {
|
|
119
117
|
if (terminalsSeen.has(binding.props.terminal)) {
|