@vuer-ai/vuer-rtc 0.7.0 → 0.8.2
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/CLAUDE.md +3 -2
- package/dist/client/EditBuffer.d.ts +4 -4
- package/dist/client/EditBuffer.d.ts.map +1 -1
- package/dist/client/EditBuffer.js +26 -25
- package/dist/client/EditBuffer.js.map +1 -1
- package/dist/client/actions.d.ts +3 -3
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +71 -70
- package/dist/client/actions.js.map +1 -1
- package/dist/client/coalesceGraphOps.d.ts +4 -4
- package/dist/client/coalesceGraphOps.js +4 -4
- package/dist/client/coalesceTextOperations.d.ts.map +1 -1
- package/dist/client/coalesceTextOperations.js +23 -20
- package/dist/client/coalesceTextOperations.js.map +1 -1
- package/dist/client/coalescence/lwwOperations.js +3 -3
- package/dist/client/coalescence/lwwOperations.js.map +1 -1
- package/dist/client/coalescence/numberOperations.js +2 -2
- package/dist/client/coalescence/numberOperations.js.map +1 -1
- package/dist/client/coalescence/registry.d.ts +3 -3
- package/dist/client/coalescence/registry.d.ts.map +1 -1
- package/dist/client/coalescence/registry.js +11 -11
- package/dist/client/coalescence/registry.js.map +1 -1
- package/dist/client/coalescence/textDeletes.d.ts +8 -7
- package/dist/client/coalescence/textDeletes.d.ts.map +1 -1
- package/dist/client/coalescence/textDeletes.js +11 -11
- package/dist/client/coalescence/textDeletes.js.map +1 -1
- package/dist/client/coalescence/textInserts.d.ts +8 -5
- package/dist/client/coalescence/textInserts.d.ts.map +1 -1
- package/dist/client/coalescence/textInserts.js +32 -12
- package/dist/client/coalescence/textInserts.js.map +1 -1
- package/dist/client/coalescence/utils.d.ts +3 -9
- package/dist/client/coalescence/utils.d.ts.map +1 -1
- package/dist/client/coalescence/utils.js +10 -8
- package/dist/client/coalescence/utils.js.map +1 -1
- package/dist/client/coalescence/vector3Operations.js +2 -2
- package/dist/client/coalescence/vector3Operations.js.map +1 -1
- package/dist/client/createGraph.d.ts +2 -2
- package/dist/client/createGraph.js +4 -4
- package/dist/client/createGraph.js.map +1 -1
- package/dist/client/createTextDocument.d.ts +1 -1
- package/dist/client/createTextDocument.js +3 -3
- package/dist/client/createTextDocument.js.map +1 -1
- package/dist/client/hooks.d.ts +3 -3
- package/dist/client/hooks.d.ts.map +1 -1
- package/dist/client/hooks.js +4 -4
- package/dist/client/hooks.js.map +1 -1
- package/dist/client/textActions.d.ts +2 -2
- package/dist/client/textActions.d.ts.map +1 -1
- package/dist/client/textActions.js +47 -47
- package/dist/client/textActions.js.map +1 -1
- package/dist/client/textTypes.d.ts +8 -8
- package/dist/client/textTypes.d.ts.map +1 -1
- package/dist/client/types.d.ts +4 -4
- package/dist/client/types.d.ts.map +1 -1
- package/dist/crdt/GraphTextCRDT.d.ts +2 -2
- package/dist/crdt/GraphTextCRDT.d.ts.map +1 -1
- package/dist/crdt/GraphTextCRDT.js +6 -6
- package/dist/crdt/GraphTextCRDT.js.map +1 -1
- package/dist/crdt/Rope.d.ts +13 -14
- package/dist/crdt/Rope.d.ts.map +1 -1
- package/dist/crdt/Rope.js +130 -59
- package/dist/crdt/Rope.js.map +1 -1
- package/dist/crdt/index.d.ts +1 -1
- package/dist/crdt/index.d.ts.map +1 -1
- package/dist/crdt/index.js +1 -1
- package/dist/crdt/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/operations/OperationTypes.d.ts +45 -48
- package/dist/operations/OperationTypes.d.ts.map +1 -1
- package/dist/operations/OperationValidator.js +11 -11
- package/dist/operations/OperationValidator.js.map +1 -1
- package/dist/operations/apply/node.js +3 -3
- package/dist/operations/apply/node.js.map +1 -1
- package/dist/operations/apply/text.d.ts.map +1 -1
- package/dist/operations/apply/text.js +35 -32
- package/dist/operations/apply/text.js.map +1 -1
- package/dist/operations/apply/types.d.ts +4 -4
- package/dist/operations/apply/types.d.ts.map +1 -1
- package/dist/operations/apply/types.js +8 -8
- package/dist/operations/apply/types.js.map +1 -1
- package/dist/operations/dispatcher.d.ts.map +1 -1
- package/dist/operations/dispatcher.js +52 -13
- package/dist/operations/dispatcher.js.map +1 -1
- package/dist/serdes.d.ts +1 -1
- package/dist/serdes.d.ts.map +1 -1
- package/dist/state/ConflictResolver.d.ts +9 -9
- package/dist/state/ConflictResolver.d.ts.map +1 -1
- package/dist/state/ConflictResolver.js +20 -20
- package/dist/state/ConflictResolver.js.map +1 -1
- package/dist/state/DType.d.ts +2 -2
- package/dist/state/DType.d.ts.map +1 -1
- package/dist/state/DType.js +14 -14
- package/dist/state/DType.js.map +1 -1
- package/dist/state/VectorClock.d.ts +6 -6
- package/dist/state/VectorClock.d.ts.map +1 -1
- package/dist/state/VectorClock.js +14 -14
- package/dist/state/VectorClock.js.map +1 -1
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/examples/01-basic-usage.ts +16 -16
- package/examples/02-concurrent-edits.ts +29 -29
- package/examples/03-scene-building.ts +28 -28
- package/examples/04-conflict-resolution.ts +56 -56
- package/examples/05-coalescence-usage.ts +23 -23
- package/examples/README.md +12 -12
- package/package.json +1 -1
- package/src/client/EditBuffer.ts +28 -27
- package/src/client/TEXT_DOCUMENT_API.md +9 -9
- package/src/client/actions.ts +74 -70
- package/src/client/coalesceGraphOps.ts +4 -4
- package/src/client/coalesceTextOperations.ts +26 -22
- package/src/client/coalescence/lwwOperations.ts +3 -3
- package/src/client/coalescence/numberOperations.ts +2 -2
- package/src/client/coalescence/registry.ts +13 -12
- package/src/client/coalescence/textDeletes.ts +22 -18
- package/src/client/coalescence/textInserts.ts +49 -25
- package/src/client/coalescence/utils.ts +14 -11
- package/src/client/coalescence/vector3Operations.ts +2 -2
- package/src/client/createGraph.ts +4 -4
- package/src/client/createTextDocument.ts +3 -3
- package/src/client/hooks.tsx +5 -5
- package/src/client/textActions.ts +47 -47
- package/src/client/textTypes.ts +8 -8
- package/src/client/types.ts +4 -4
- package/src/crdt/GraphTextCRDT.ts +6 -6
- package/src/crdt/Rope.ts +156 -71
- package/src/crdt/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/operations/OperationTypes.ts +47 -47
- package/src/operations/OperationValidator.ts +11 -11
- package/src/operations/apply/node.ts +3 -3
- package/src/operations/apply/text.ts +38 -32
- package/src/operations/apply/types.ts +11 -11
- package/src/operations/dispatcher.ts +57 -13
- package/src/serdes.ts +1 -1
- package/src/state/ConflictResolver.ts +23 -23
- package/src/state/DType.ts +16 -16
- package/src/state/VectorClock.ts +14 -14
- package/src/state/index.ts +1 -1
- package/tests/client/actions.test.ts +76 -76
- package/tests/client/coalesce-graph-operations.test.ts +84 -84
- package/tests/client/coalesce-text-operations.test.ts +91 -114
- package/tests/client/compaction.test.ts +18 -18
- package/tests/client/delete-coalescence-bug.test.ts +34 -34
- package/tests/client/edit-buffer.test.ts +27 -30
- package/tests/client/graph-coalescence-phase1.test.ts +66 -66
- package/tests/client/graph-coalescence.test.ts +50 -50
- package/tests/client/journal-benchmark.test.ts +5 -5
- package/tests/crdt/graph-text-crdt.test.ts +60 -64
- package/tests/crdt/rope.test.ts +9 -8
- package/tests/crdt/text-operations.test.ts +28 -28
- package/tests/fixtures/array-ops.jsonl +6 -6
- package/tests/fixtures/boolean-ops.jsonl +6 -6
- package/tests/fixtures/color-ops.jsonl +4 -4
- package/tests/fixtures/edit-buffer.jsonl +3 -3
- package/tests/fixtures/node-ops.jsonl +6 -6
- package/tests/fixtures/number-ops.jsonl +7 -7
- package/tests/fixtures/object-ops.jsonl +4 -4
- package/tests/fixtures/operations.jsonl +7 -7
- package/tests/fixtures/string-ops.jsonl +4 -4
- package/tests/fixtures/undo-redo.jsonl +3 -3
- package/tests/fixtures/vector-ops.jsonl +17 -17
- package/tests/operations/collections.test.ts +4 -4
- package/tests/operations/nodes.test.ts +5 -5
- package/tests/operations/operation-ordering.test.ts +406 -0
- package/tests/operations/primitives.test.ts +4 -4
- package/tests/operations/unified-schema.test.ts +27 -27
- package/tests/operations/vectors.test.ts +4 -4
- package/tests/sync/digest.test.ts +5 -5
|
@@ -13,42 +13,37 @@ describe('coalesceTextOperations', () => {
|
|
|
13
13
|
// Simulate typing "Hello" quickly (within 300ms)
|
|
14
14
|
const ops: TextOperation[] = [
|
|
15
15
|
{
|
|
16
|
-
|
|
16
|
+
ot: 'insert',
|
|
17
17
|
id: 'alice:1',
|
|
18
|
-
|
|
19
|
-
parentId: null,
|
|
18
|
+
value: [null, 'H'],
|
|
20
19
|
seq: 1,
|
|
21
20
|
ts: 1000.0,
|
|
22
21
|
},
|
|
23
22
|
{
|
|
24
|
-
|
|
23
|
+
ot: 'insert',
|
|
25
24
|
id: 'alice:2',
|
|
26
|
-
|
|
27
|
-
parentId: 'alice:1', // Forms chain with previous
|
|
25
|
+
value: ['alice:1', 'e'], // Forms chain with previous
|
|
28
26
|
seq: 2,
|
|
29
27
|
ts: 1000.05, // 50ms later
|
|
30
28
|
},
|
|
31
29
|
{
|
|
32
|
-
|
|
30
|
+
ot: 'insert',
|
|
33
31
|
id: 'alice:3',
|
|
34
|
-
|
|
35
|
-
parentId: 'alice:2', // Forms chain with previous
|
|
32
|
+
value: ['alice:2', 'l'], // Forms chain with previous
|
|
36
33
|
seq: 3,
|
|
37
34
|
ts: 1000.1, // 100ms from start
|
|
38
35
|
},
|
|
39
36
|
{
|
|
40
|
-
|
|
37
|
+
ot: 'insert',
|
|
41
38
|
id: 'alice:4',
|
|
42
|
-
|
|
43
|
-
parentId: 'alice:3', // Forms chain with previous
|
|
39
|
+
value: ['alice:3', 'l'], // Forms chain with previous
|
|
44
40
|
seq: 4,
|
|
45
41
|
ts: 1000.15, // 150ms from start
|
|
46
42
|
},
|
|
47
43
|
{
|
|
48
|
-
|
|
44
|
+
ot: 'insert',
|
|
49
45
|
id: 'alice:5',
|
|
50
|
-
|
|
51
|
-
parentId: 'alice:4', // Forms chain with previous
|
|
46
|
+
value: ['alice:4', 'o'], // Forms chain with previous
|
|
52
47
|
seq: 5,
|
|
53
48
|
ts: 1000.2, // 200ms from start
|
|
54
49
|
},
|
|
@@ -56,54 +51,49 @@ describe('coalesceTextOperations', () => {
|
|
|
56
51
|
|
|
57
52
|
const result = coalesceTextOperations(ops, { thresholdMs: 300 });
|
|
58
53
|
|
|
59
|
-
// Should coalesce into ONE operation with
|
|
54
|
+
// Should coalesce into ONE operation with value="Hello"
|
|
60
55
|
expect(result).toHaveLength(1);
|
|
61
|
-
expect(result[0].
|
|
62
|
-
expect((result[0] as any).
|
|
56
|
+
expect(result[0].ot).toBe('insert');
|
|
57
|
+
expect((result[0] as any).value[1]).toBe('Hello');
|
|
63
58
|
expect((result[0] as any).id).toBe('alice:1'); // Keep first ID
|
|
64
|
-
expect((result[0] as any).
|
|
59
|
+
expect((result[0] as any).value[0]).toBeNull(); // Keep first anchor (null)
|
|
65
60
|
});
|
|
66
61
|
|
|
67
62
|
it('should respect time threshold and create separate operations for slow typing', () => {
|
|
68
63
|
// Simulate typing "He" quickly, then pause, then "llo" quickly
|
|
69
64
|
const ops: TextOperation[] = [
|
|
70
65
|
{
|
|
71
|
-
|
|
66
|
+
ot: 'insert',
|
|
72
67
|
id: 'alice:1',
|
|
73
|
-
|
|
74
|
-
parentId: null,
|
|
68
|
+
value: [null, 'H'],
|
|
75
69
|
seq: 1,
|
|
76
70
|
ts: 1000.0,
|
|
77
71
|
},
|
|
78
72
|
{
|
|
79
|
-
|
|
73
|
+
ot: 'insert',
|
|
80
74
|
id: 'alice:2',
|
|
81
|
-
|
|
82
|
-
parentId: 'alice:1',
|
|
75
|
+
value: ['alice:1', 'e'],
|
|
83
76
|
seq: 2,
|
|
84
77
|
ts: 1000.05, // 50ms later - within threshold
|
|
85
78
|
},
|
|
86
79
|
{
|
|
87
|
-
|
|
80
|
+
ot: 'insert',
|
|
88
81
|
id: 'alice:3',
|
|
89
|
-
|
|
90
|
-
parentId: 'alice:2',
|
|
82
|
+
value: ['alice:2', 'l'],
|
|
91
83
|
seq: 3,
|
|
92
84
|
ts: 1000.5, // 450ms later - EXCEEDS threshold
|
|
93
85
|
},
|
|
94
86
|
{
|
|
95
|
-
|
|
87
|
+
ot: 'insert',
|
|
96
88
|
id: 'alice:4',
|
|
97
|
-
|
|
98
|
-
parentId: 'alice:3',
|
|
89
|
+
value: ['alice:3', 'l'],
|
|
99
90
|
seq: 4,
|
|
100
91
|
ts: 1000.55, // 50ms after 'l'
|
|
101
92
|
},
|
|
102
93
|
{
|
|
103
|
-
|
|
94
|
+
ot: 'insert',
|
|
104
95
|
id: 'alice:5',
|
|
105
|
-
|
|
106
|
-
parentId: 'alice:4',
|
|
96
|
+
value: ['alice:4', 'o'],
|
|
107
97
|
seq: 5,
|
|
108
98
|
ts: 1000.6, // 50ms after second 'l'
|
|
109
99
|
},
|
|
@@ -113,35 +103,32 @@ describe('coalesceTextOperations', () => {
|
|
|
113
103
|
|
|
114
104
|
// Should create TWO operations: "He" and "llo"
|
|
115
105
|
expect(result).toHaveLength(2);
|
|
116
|
-
expect((result[0] as any).
|
|
106
|
+
expect((result[0] as any).value[1]).toBe('He');
|
|
117
107
|
expect((result[0] as any).id).toBe('alice:1');
|
|
118
|
-
expect((result[1] as any).
|
|
108
|
+
expect((result[1] as any).value[1]).toBe('llo');
|
|
119
109
|
expect((result[1] as any).id).toBe('alice:3');
|
|
120
110
|
});
|
|
121
111
|
|
|
122
112
|
it('should handle single character operations (no coalescence)', () => {
|
|
123
113
|
const ops: TextOperation[] = [
|
|
124
114
|
{
|
|
125
|
-
|
|
115
|
+
ot: 'insert',
|
|
126
116
|
id: 'alice:1',
|
|
127
|
-
|
|
128
|
-
parentId: null,
|
|
117
|
+
value: [null, 'H'],
|
|
129
118
|
seq: 1,
|
|
130
119
|
ts: 1000.0,
|
|
131
120
|
},
|
|
132
121
|
{
|
|
133
|
-
|
|
122
|
+
ot: 'insert',
|
|
134
123
|
id: 'alice:2',
|
|
135
|
-
|
|
136
|
-
parentId: 'alice:1',
|
|
124
|
+
value: ['alice:1', 'e'],
|
|
137
125
|
seq: 2,
|
|
138
126
|
ts: 1001.5, // 1500ms later - exceeds threshold
|
|
139
127
|
},
|
|
140
128
|
{
|
|
141
|
-
|
|
129
|
+
ot: 'insert',
|
|
142
130
|
id: 'alice:3',
|
|
143
|
-
|
|
144
|
-
parentId: 'alice:2',
|
|
131
|
+
value: ['alice:2', 'l'],
|
|
145
132
|
seq: 3,
|
|
146
133
|
ts: 1003.0, // 1500ms later - exceeds threshold
|
|
147
134
|
},
|
|
@@ -151,26 +138,24 @@ describe('coalesceTextOperations', () => {
|
|
|
151
138
|
|
|
152
139
|
// Should create THREE separate operations (no coalescence)
|
|
153
140
|
expect(result).toHaveLength(3);
|
|
154
|
-
expect((result[0] as any).
|
|
155
|
-
expect((result[1] as any).
|
|
156
|
-
expect((result[2] as any).
|
|
141
|
+
expect((result[0] as any).value[1]).toBe('H');
|
|
142
|
+
expect((result[1] as any).value[1]).toBe('e');
|
|
143
|
+
expect((result[2] as any).value[1]).toBe('l');
|
|
157
144
|
});
|
|
158
145
|
|
|
159
146
|
it('should handle non-sequential IDs (different agents)', () => {
|
|
160
147
|
const ops: TextOperation[] = [
|
|
161
148
|
{
|
|
162
|
-
|
|
149
|
+
ot: 'insert',
|
|
163
150
|
id: 'alice:1',
|
|
164
|
-
|
|
165
|
-
parentId: null,
|
|
151
|
+
value: [null, 'H'],
|
|
166
152
|
seq: 1,
|
|
167
153
|
ts: 1000.0,
|
|
168
154
|
},
|
|
169
155
|
{
|
|
170
|
-
|
|
156
|
+
ot: 'insert',
|
|
171
157
|
id: 'bob:1', // Different agent
|
|
172
|
-
|
|
173
|
-
parentId: 'alice:1',
|
|
158
|
+
value: ['alice:1', 'e'],
|
|
174
159
|
seq: 2,
|
|
175
160
|
ts: 1000.05,
|
|
176
161
|
},
|
|
@@ -180,33 +165,29 @@ describe('coalesceTextOperations', () => {
|
|
|
180
165
|
|
|
181
166
|
// Should NOT coalesce (different agents)
|
|
182
167
|
expect(result).toHaveLength(2);
|
|
183
|
-
expect((result[0] as any).
|
|
184
|
-
expect((result[1] as any).
|
|
168
|
+
expect((result[0] as any).value[1]).toBe('H');
|
|
169
|
+
expect((result[1] as any).value[1]).toBe('e');
|
|
185
170
|
});
|
|
186
171
|
|
|
187
172
|
it('should handle delete operations (flush pending inserts)', () => {
|
|
188
173
|
const ops: TextOperation[] = [
|
|
189
174
|
{
|
|
190
|
-
|
|
175
|
+
ot: 'insert',
|
|
191
176
|
id: 'alice:1',
|
|
192
|
-
|
|
193
|
-
parentId: null,
|
|
177
|
+
value: [null, 'H'],
|
|
194
178
|
seq: 1,
|
|
195
179
|
ts: 1000.0,
|
|
196
180
|
},
|
|
197
181
|
{
|
|
198
|
-
|
|
182
|
+
ot: 'insert',
|
|
199
183
|
id: 'alice:2',
|
|
200
|
-
|
|
201
|
-
parentId: 'alice:1',
|
|
184
|
+
value: ['alice:1', 'e'],
|
|
202
185
|
seq: 2,
|
|
203
186
|
ts: 1000.05,
|
|
204
187
|
},
|
|
205
188
|
{
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
seq: 3,
|
|
209
|
-
ts: 1000.1,
|
|
189
|
+
ot: 'delete',
|
|
190
|
+
rm: [['alice:2', 1]],
|
|
210
191
|
} as any,
|
|
211
192
|
];
|
|
212
193
|
|
|
@@ -214,34 +195,31 @@ describe('coalesceTextOperations', () => {
|
|
|
214
195
|
|
|
215
196
|
// Should flush "He" and then add the delete
|
|
216
197
|
expect(result).toHaveLength(2);
|
|
217
|
-
expect(result[0].
|
|
218
|
-
expect((result[0] as any).
|
|
219
|
-
expect(result[1].
|
|
198
|
+
expect(result[0].ot).toBe('insert');
|
|
199
|
+
expect((result[0] as any).value[1]).toBe('He');
|
|
200
|
+
expect(result[1].ot).toBe('delete');
|
|
220
201
|
});
|
|
221
202
|
|
|
222
203
|
it('should verify _lastCharId metadata is tracked correctly', () => {
|
|
223
204
|
const ops: TextOperation[] = [
|
|
224
205
|
{
|
|
225
|
-
|
|
206
|
+
ot: 'insert',
|
|
226
207
|
id: 'alice:1',
|
|
227
|
-
|
|
228
|
-
parentId: null,
|
|
208
|
+
value: [null, 'a'],
|
|
229
209
|
seq: 1,
|
|
230
210
|
ts: 1000.0,
|
|
231
211
|
},
|
|
232
212
|
{
|
|
233
|
-
|
|
213
|
+
ot: 'insert',
|
|
234
214
|
id: 'alice:2',
|
|
235
|
-
|
|
236
|
-
parentId: 'alice:1',
|
|
215
|
+
value: ['alice:1', 'b'],
|
|
237
216
|
seq: 2,
|
|
238
217
|
ts: 1000.05,
|
|
239
218
|
},
|
|
240
219
|
{
|
|
241
|
-
|
|
220
|
+
ot: 'insert',
|
|
242
221
|
id: 'alice:3',
|
|
243
|
-
|
|
244
|
-
parentId: 'alice:2', // This should match _lastCharId after first merge
|
|
222
|
+
value: ['alice:2', 'c'], // This should match _lastCharId after first merge
|
|
245
223
|
seq: 3,
|
|
246
224
|
ts: 1000.1,
|
|
247
225
|
},
|
|
@@ -251,7 +229,7 @@ describe('coalesceTextOperations', () => {
|
|
|
251
229
|
|
|
252
230
|
// Should coalesce all three
|
|
253
231
|
expect(result).toHaveLength(1);
|
|
254
|
-
expect((result[0] as any).
|
|
232
|
+
expect((result[0] as any).value[1]).toBe('abc');
|
|
255
233
|
expect((result[0] as any)._lastCharId).toBe('alice:3');
|
|
256
234
|
});
|
|
257
235
|
});
|
|
@@ -261,32 +239,32 @@ describe('coalesceTextOperations', () => {
|
|
|
261
239
|
// Simulate pressing backspace 7 times (7 separate delete operations)
|
|
262
240
|
const ops: TextOperation[] = [
|
|
263
241
|
{
|
|
264
|
-
|
|
265
|
-
|
|
242
|
+
ot: 'delete',
|
|
243
|
+
rm: [['alice:7', 1]],
|
|
266
244
|
} as any,
|
|
267
245
|
{
|
|
268
|
-
|
|
269
|
-
|
|
246
|
+
ot: 'delete',
|
|
247
|
+
rm: [['alice:6', 1]],
|
|
270
248
|
} as any,
|
|
271
249
|
{
|
|
272
|
-
|
|
273
|
-
|
|
250
|
+
ot: 'delete',
|
|
251
|
+
rm: [['alice:5', 1]],
|
|
274
252
|
} as any,
|
|
275
253
|
{
|
|
276
|
-
|
|
277
|
-
|
|
254
|
+
ot: 'delete',
|
|
255
|
+
rm: [['alice:4', 1]],
|
|
278
256
|
} as any,
|
|
279
257
|
{
|
|
280
|
-
|
|
281
|
-
|
|
258
|
+
ot: 'delete',
|
|
259
|
+
rm: [['alice:3', 1]],
|
|
282
260
|
} as any,
|
|
283
261
|
{
|
|
284
|
-
|
|
285
|
-
|
|
262
|
+
ot: 'delete',
|
|
263
|
+
rm: [['alice:2', 1]],
|
|
286
264
|
} as any,
|
|
287
265
|
{
|
|
288
|
-
|
|
289
|
-
|
|
266
|
+
ot: 'delete',
|
|
267
|
+
rm: [['alice:1', 1]],
|
|
290
268
|
} as any,
|
|
291
269
|
];
|
|
292
270
|
|
|
@@ -294,30 +272,30 @@ describe('coalesceTextOperations', () => {
|
|
|
294
272
|
|
|
295
273
|
// Should merge into ONE delete operation
|
|
296
274
|
expect(result).toHaveLength(1);
|
|
297
|
-
expect(result[0].
|
|
275
|
+
expect(result[0].ot).toBe('delete');
|
|
298
276
|
|
|
299
277
|
// CRITICAL: Deletions array should be optimized (7 entries → 1 entry)
|
|
300
278
|
const deleteOp = result[0] as any;
|
|
301
|
-
expect(deleteOp.
|
|
302
|
-
expect(deleteOp.
|
|
303
|
-
expect(deleteOp.
|
|
279
|
+
expect(deleteOp.rm).toBeDefined();
|
|
280
|
+
expect(deleteOp.rm).toHaveLength(1);
|
|
281
|
+
expect(deleteOp.rm[0]).toEqual(['alice:1', 7]);
|
|
304
282
|
});
|
|
305
283
|
|
|
306
284
|
it('should optimize deletions with gaps', () => {
|
|
307
285
|
// Deletions with non-consecutive IDs (can't be fully merged)
|
|
308
286
|
const ops: TextOperation[] = [
|
|
309
287
|
{
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
288
|
+
ot: 'delete',
|
|
289
|
+
rm: [
|
|
290
|
+
['alice:5', 1],
|
|
291
|
+
['alice:4', 1],
|
|
314
292
|
],
|
|
315
293
|
} as any,
|
|
316
294
|
{
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
295
|
+
ot: 'delete',
|
|
296
|
+
rm: [
|
|
297
|
+
['alice:2', 1], // Gap! alice:3 is missing
|
|
298
|
+
['alice:1', 1],
|
|
321
299
|
],
|
|
322
300
|
} as any,
|
|
323
301
|
];
|
|
@@ -326,14 +304,14 @@ describe('coalesceTextOperations', () => {
|
|
|
326
304
|
|
|
327
305
|
// Should merge into ONE delete operation
|
|
328
306
|
expect(result).toHaveLength(1);
|
|
329
|
-
expect(result[0].
|
|
307
|
+
expect(result[0].ot).toBe('delete');
|
|
330
308
|
|
|
331
309
|
// Deletions should be optimized: [alice:4-5 (len 2), alice:1-2 (len 2)]
|
|
332
310
|
const deleteOp = result[0] as any;
|
|
333
|
-
expect(deleteOp.
|
|
334
|
-
expect(deleteOp.
|
|
335
|
-
expect(deleteOp.
|
|
336
|
-
expect(deleteOp.
|
|
311
|
+
expect(deleteOp.rm).toBeDefined();
|
|
312
|
+
expect(deleteOp.rm).toHaveLength(2); // Two ranges due to gap
|
|
313
|
+
expect(deleteOp.rm).toContainEqual(['alice:4', 2]);
|
|
314
|
+
expect(deleteOp.rm).toContainEqual(['alice:1', 2]);
|
|
337
315
|
});
|
|
338
316
|
});
|
|
339
317
|
|
|
@@ -346,10 +324,9 @@ describe('coalesceTextOperations', () => {
|
|
|
346
324
|
it('should handle single operation', () => {
|
|
347
325
|
const ops: TextOperation[] = [
|
|
348
326
|
{
|
|
349
|
-
|
|
327
|
+
ot: 'insert',
|
|
350
328
|
id: 'alice:1',
|
|
351
|
-
|
|
352
|
-
parentId: null,
|
|
329
|
+
value: [null, 'H'],
|
|
353
330
|
seq: 1,
|
|
354
331
|
ts: 1000.0,
|
|
355
332
|
},
|
|
@@ -357,7 +334,7 @@ describe('coalesceTextOperations', () => {
|
|
|
357
334
|
|
|
358
335
|
const result = coalesceTextOperations(ops);
|
|
359
336
|
expect(result).toHaveLength(1);
|
|
360
|
-
expect((result[0] as any).
|
|
337
|
+
expect((result[0] as any).value[1]).toBe('H');
|
|
361
338
|
});
|
|
362
339
|
});
|
|
363
340
|
});
|
|
@@ -32,17 +32,17 @@ import type { CRDTMessage, Operation, SceneGraph } from '../../src/operations/Op
|
|
|
32
32
|
|
|
33
33
|
function makeRemoteMsg(
|
|
34
34
|
id: string,
|
|
35
|
-
|
|
35
|
+
client: string,
|
|
36
36
|
ops: Operation[],
|
|
37
|
-
|
|
37
|
+
lt: number,
|
|
38
38
|
clock?: Record<string, number>,
|
|
39
39
|
): CRDTMessage {
|
|
40
40
|
return {
|
|
41
41
|
id,
|
|
42
|
-
|
|
43
|
-
clock: clock ?? { [
|
|
44
|
-
|
|
45
|
-
|
|
42
|
+
client,
|
|
43
|
+
clock: clock ?? { [client]: lt },
|
|
44
|
+
lt,
|
|
45
|
+
ts: Date.now() / 1000,
|
|
46
46
|
ops,
|
|
47
47
|
};
|
|
48
48
|
}
|
|
@@ -50,31 +50,31 @@ function makeRemoteMsg(
|
|
|
50
50
|
function nodeInsertOp(parentKey: string, nodeKey: string, props: Record<string, unknown> = {}): Operation {
|
|
51
51
|
return {
|
|
52
52
|
key: parentKey,
|
|
53
|
-
|
|
53
|
+
ot: 'node.insert',
|
|
54
54
|
path: 'children',
|
|
55
55
|
value: { key: nodeKey, tag: 'Mesh', name: nodeKey, ...props },
|
|
56
56
|
} as Operation;
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function vec3SetOp(nodeKey: string, path: string, value: [number, number, number]): Operation {
|
|
60
|
-
return { key: nodeKey,
|
|
60
|
+
return { key: nodeKey, ot: 'vector3.set', path, value } as Operation;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
63
|
function numSetOp(nodeKey: string, path: string, value: number): Operation {
|
|
64
|
-
return { key: nodeKey,
|
|
64
|
+
return { key: nodeKey, ot: 'number.set', path, value } as Operation;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function numAddOp(nodeKey: string, path: string, value: number): Operation {
|
|
68
|
-
return { key: nodeKey,
|
|
68
|
+
return { key: nodeKey, ot: 'number.add', path, value } as Operation;
|
|
69
69
|
}
|
|
70
70
|
|
|
71
71
|
function nodeRemoveOp(parentKey: string, nodeKey: string): Operation {
|
|
72
|
-
return { key: parentKey,
|
|
72
|
+
return { key: parentKey, ot: 'node.remove', path: 'children', value: nodeKey } as Operation;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
75
|
/** Set up a state with a scene root + cube node */
|
|
76
|
-
function setupState(
|
|
77
|
-
let s = createInitialState(
|
|
76
|
+
function setupState(client = 'test-session'): ClientState {
|
|
77
|
+
let s = createInitialState(client);
|
|
78
78
|
s = onEdit(s, nodeInsertOp('', 'scene', { tag: 'Scene' }));
|
|
79
79
|
const { state: s1, msg: m1 } = commitEdits(s);
|
|
80
80
|
s = onServerAck(s1, m1!.id);
|
|
@@ -214,12 +214,12 @@ describe('Client compact() (Issue #50)', () => {
|
|
|
214
214
|
s = commitAndAck(s, vec3SetOp('cube', 'position', [1, 2, 3]));
|
|
215
215
|
s = commitAndAck(s, numSetOp('cube', 'opacity', 0.5));
|
|
216
216
|
|
|
217
|
-
const lamportBefore = s.snapshot.
|
|
217
|
+
const lamportBefore = s.snapshot.lt;
|
|
218
218
|
const indexBefore = s.snapshot.journalIndex;
|
|
219
219
|
|
|
220
220
|
s = compact(s);
|
|
221
221
|
|
|
222
|
-
expect(s.snapshot.
|
|
222
|
+
expect(s.snapshot.lt).toBeGreaterThan(lamportBefore);
|
|
223
223
|
expect(s.snapshot.journalIndex).toBeGreaterThan(indexBefore);
|
|
224
224
|
});
|
|
225
225
|
|
|
@@ -346,7 +346,7 @@ describe('initFromServer (state transfer)', () => {
|
|
|
346
346
|
const emptySnapshot = {
|
|
347
347
|
graph: createEmptyGraph(),
|
|
348
348
|
vectorClock: {},
|
|
349
|
-
|
|
349
|
+
lt: 0,
|
|
350
350
|
journalIndex: 0,
|
|
351
351
|
};
|
|
352
352
|
|
|
@@ -409,7 +409,7 @@ describe('initFromServer (state transfer)', () => {
|
|
|
409
409
|
const emptySnapshot = {
|
|
410
410
|
graph: createEmptyGraph(),
|
|
411
411
|
vectorClock: {},
|
|
412
|
-
|
|
412
|
+
lt: 0,
|
|
413
413
|
journalIndex: 0,
|
|
414
414
|
};
|
|
415
415
|
|
|
@@ -423,7 +423,7 @@ describe('initFromServer (state transfer)', () => {
|
|
|
423
423
|
// new-client's clock should include alice and bob
|
|
424
424
|
expect(state.vectorClock['alice']).toBeGreaterThanOrEqual(2);
|
|
425
425
|
expect(state.vectorClock['bob']).toBeGreaterThanOrEqual(1);
|
|
426
|
-
expect(state.
|
|
426
|
+
expect(state.lt).toBeGreaterThanOrEqual(3);
|
|
427
427
|
});
|
|
428
428
|
});
|
|
429
429
|
|