@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
|
@@ -23,7 +23,7 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
23
23
|
describe('number.add coalescence', () => {
|
|
24
24
|
it('should merge consecutive number.add operations on same target', () => {
|
|
25
25
|
const store = createGraph({
|
|
26
|
-
|
|
26
|
+
client: 'alice',
|
|
27
27
|
coalescingEnabled: true,
|
|
28
28
|
coalescingDelayMs: 0,
|
|
29
29
|
coalescingThresholdMs: 1000,
|
|
@@ -31,20 +31,20 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
31
31
|
});
|
|
32
32
|
|
|
33
33
|
// Create a score counter
|
|
34
|
-
store.edit({
|
|
34
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { score: 0 } });
|
|
35
35
|
store.commit('create player');
|
|
36
36
|
|
|
37
37
|
// Add to score 3 times rapidly
|
|
38
|
-
store.edit({
|
|
39
|
-
store.edit({
|
|
40
|
-
store.edit({
|
|
38
|
+
store.edit({ ot: 'number.add', key: 'player', path: 'score', value: 10 });
|
|
39
|
+
store.edit({ ot: 'number.add', key: 'player', path: 'score', value: 5 });
|
|
40
|
+
store.edit({ ot: 'number.add', key: 'player', path: 'score', value: 3 });
|
|
41
41
|
store.commit('add to score');
|
|
42
42
|
|
|
43
43
|
// Should coalesce into 1 operation with sum: 10 + 5 + 3 = 18
|
|
44
44
|
const scoreMsg = messages[1];
|
|
45
45
|
expect(scoreMsg.ops).toHaveLength(1);
|
|
46
46
|
expect(scoreMsg.ops[0]).toMatchObject({
|
|
47
|
-
|
|
47
|
+
ot: 'number.add',
|
|
48
48
|
key: 'player',
|
|
49
49
|
path: 'score',
|
|
50
50
|
value: 18,
|
|
@@ -53,32 +53,32 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
53
53
|
|
|
54
54
|
it('should NOT merge number.add operations on different targets', () => {
|
|
55
55
|
const store = createGraph({
|
|
56
|
-
|
|
56
|
+
client: 'alice',
|
|
57
57
|
coalescingEnabled: true,
|
|
58
58
|
coalescingDelayMs: 0,
|
|
59
59
|
coalescingThresholdMs: 1000,
|
|
60
60
|
onSend: (msg) => messages.push(msg),
|
|
61
61
|
});
|
|
62
62
|
|
|
63
|
-
store.edit({
|
|
64
|
-
store.edit({
|
|
63
|
+
store.edit({ ot: 'node.insert', key: 'player1', value: { score: 0 } });
|
|
64
|
+
store.edit({ ot: 'node.insert', key: 'player2', value: { score: 0 } });
|
|
65
65
|
store.commit('create players');
|
|
66
66
|
|
|
67
67
|
// Add to different players
|
|
68
|
-
store.edit({
|
|
69
|
-
store.edit({
|
|
68
|
+
store.edit({ ot: 'number.add', key: 'player1', path: 'score', value: 10 });
|
|
69
|
+
store.edit({ ot: 'number.add', key: 'player2', path: 'score', value: 5 });
|
|
70
70
|
store.commit('add to scores');
|
|
71
71
|
|
|
72
72
|
// Should NOT coalesce (different keys)
|
|
73
73
|
const scoreMsg = messages[1];
|
|
74
74
|
expect(scoreMsg.ops).toHaveLength(2);
|
|
75
75
|
expect(scoreMsg.ops[0]).toMatchObject({
|
|
76
|
-
|
|
76
|
+
ot: 'number.add',
|
|
77
77
|
key: 'player1',
|
|
78
78
|
value: 10,
|
|
79
79
|
});
|
|
80
80
|
expect(scoreMsg.ops[1]).toMatchObject({
|
|
81
|
-
|
|
81
|
+
ot: 'number.add',
|
|
82
82
|
key: 'player2',
|
|
83
83
|
value: 5,
|
|
84
84
|
});
|
|
@@ -86,19 +86,19 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
86
86
|
|
|
87
87
|
it('should NOT merge number.add operations on different paths', () => {
|
|
88
88
|
const store = createGraph({
|
|
89
|
-
|
|
89
|
+
client: 'alice',
|
|
90
90
|
coalescingEnabled: true,
|
|
91
91
|
coalescingDelayMs: 0,
|
|
92
92
|
coalescingThresholdMs: 1000,
|
|
93
93
|
onSend: (msg) => messages.push(msg),
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
store.edit({
|
|
96
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { score: 0, health: 100 } });
|
|
97
97
|
store.commit('create player');
|
|
98
98
|
|
|
99
99
|
// Add to different properties
|
|
100
|
-
store.edit({
|
|
101
|
-
store.edit({
|
|
100
|
+
store.edit({ ot: 'number.add', key: 'player', path: 'score', value: 10 });
|
|
101
|
+
store.edit({ ot: 'number.add', key: 'player', path: 'health', value: -5 });
|
|
102
102
|
store.commit('update stats');
|
|
103
103
|
|
|
104
104
|
// Should NOT coalesce (different paths)
|
|
@@ -112,27 +112,27 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
112
112
|
describe('vector3.add coalescence', () => {
|
|
113
113
|
it('should merge consecutive vector3.add operations on same target', () => {
|
|
114
114
|
const store = createGraph({
|
|
115
|
-
|
|
115
|
+
client: 'alice',
|
|
116
116
|
coalescingEnabled: true,
|
|
117
117
|
coalescingDelayMs: 0,
|
|
118
118
|
coalescingThresholdMs: 1000,
|
|
119
119
|
onSend: (msg) => messages.push(msg),
|
|
120
120
|
});
|
|
121
121
|
|
|
122
|
-
store.edit({
|
|
122
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { position: [0, 0, 0] } });
|
|
123
123
|
store.commit('create player');
|
|
124
124
|
|
|
125
125
|
// Move player in multiple steps
|
|
126
|
-
store.edit({
|
|
127
|
-
store.edit({
|
|
128
|
-
store.edit({
|
|
126
|
+
store.edit({ ot: 'vector3.add', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
127
|
+
store.edit({ ot: 'vector3.add', key: 'player', path: 'position', value: [0, 2, 0] });
|
|
128
|
+
store.edit({ ot: 'vector3.add', key: 'player', path: 'position', value: [0, 0, 3] });
|
|
129
129
|
store.commit('move player');
|
|
130
130
|
|
|
131
131
|
// Should coalesce into 1 operation with sum: [1+0+0, 0+2+0, 0+0+3] = [1, 2, 3]
|
|
132
132
|
const moveMsg = messages[1];
|
|
133
133
|
expect(moveMsg.ops).toHaveLength(1);
|
|
134
134
|
expect(moveMsg.ops[0]).toMatchObject({
|
|
135
|
-
|
|
135
|
+
ot: 'vector3.add',
|
|
136
136
|
key: 'player',
|
|
137
137
|
path: 'position',
|
|
138
138
|
value: [1, 2, 3],
|
|
@@ -141,19 +141,19 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
141
141
|
|
|
142
142
|
it('should NOT merge vector3.add operations on different targets', () => {
|
|
143
143
|
const store = createGraph({
|
|
144
|
-
|
|
144
|
+
client: 'alice',
|
|
145
145
|
coalescingEnabled: true,
|
|
146
146
|
coalescingDelayMs: 0,
|
|
147
147
|
coalescingThresholdMs: 1000,
|
|
148
148
|
onSend: (msg) => messages.push(msg),
|
|
149
149
|
});
|
|
150
150
|
|
|
151
|
-
store.edit({
|
|
151
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { position: [0, 0, 0], velocity: [0, 0, 0] } });
|
|
152
152
|
store.commit('create player');
|
|
153
153
|
|
|
154
154
|
// Add to different properties
|
|
155
|
-
store.edit({
|
|
156
|
-
store.edit({
|
|
155
|
+
store.edit({ ot: 'vector3.add', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
156
|
+
store.edit({ ot: 'vector3.add', key: 'player', path: 'velocity', value: [0, 1, 0] });
|
|
157
157
|
store.commit('update player');
|
|
158
158
|
|
|
159
159
|
// Should NOT coalesce (different paths)
|
|
@@ -167,27 +167,27 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
167
167
|
describe('LWW (*.set) coalescence', () => {
|
|
168
168
|
it('should keep only last number.set operation on same target', () => {
|
|
169
169
|
const store = createGraph({
|
|
170
|
-
|
|
170
|
+
client: 'alice',
|
|
171
171
|
coalescingEnabled: true,
|
|
172
172
|
coalescingDelayMs: 0,
|
|
173
173
|
coalescingThresholdMs: 1000,
|
|
174
174
|
onSend: (msg) => messages.push(msg),
|
|
175
175
|
});
|
|
176
176
|
|
|
177
|
-
store.edit({
|
|
177
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { score: 0 } });
|
|
178
178
|
store.commit('create player');
|
|
179
179
|
|
|
180
180
|
// Set score multiple times
|
|
181
|
-
store.edit({
|
|
182
|
-
store.edit({
|
|
183
|
-
store.edit({
|
|
181
|
+
store.edit({ ot: 'number.set', key: 'player', path: 'score', value: 10 });
|
|
182
|
+
store.edit({ ot: 'number.set', key: 'player', path: 'score', value: 20 });
|
|
183
|
+
store.edit({ ot: 'number.set', key: 'player', path: 'score', value: 30 });
|
|
184
184
|
store.commit('set score');
|
|
185
185
|
|
|
186
186
|
// Should keep only last set (LWW)
|
|
187
187
|
const scoreMsg = messages[1];
|
|
188
188
|
expect(scoreMsg.ops).toHaveLength(1);
|
|
189
189
|
expect(scoreMsg.ops[0]).toMatchObject({
|
|
190
|
-
|
|
190
|
+
ot: 'number.set',
|
|
191
191
|
key: 'player',
|
|
192
192
|
path: 'score',
|
|
193
193
|
value: 30, // Only last value
|
|
@@ -196,27 +196,27 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
196
196
|
|
|
197
197
|
it('should keep only last string.set operation on same target', () => {
|
|
198
198
|
const store = createGraph({
|
|
199
|
-
|
|
199
|
+
client: 'alice',
|
|
200
200
|
coalescingEnabled: true,
|
|
201
201
|
coalescingDelayMs: 0,
|
|
202
202
|
coalescingThresholdMs: 1000,
|
|
203
203
|
onSend: (msg) => messages.push(msg),
|
|
204
204
|
});
|
|
205
205
|
|
|
206
|
-
store.edit({
|
|
206
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { name: '' } });
|
|
207
207
|
store.commit('create player');
|
|
208
208
|
|
|
209
209
|
// Set name multiple times
|
|
210
|
-
store.edit({
|
|
211
|
-
store.edit({
|
|
212
|
-
store.edit({
|
|
210
|
+
store.edit({ ot: 'string.set', key: 'player', path: 'name', value: 'Alice' });
|
|
211
|
+
store.edit({ ot: 'string.set', key: 'player', path: 'name', value: 'Bob' });
|
|
212
|
+
store.edit({ ot: 'string.set', key: 'player', path: 'name', value: 'Charlie' });
|
|
213
213
|
store.commit('set name');
|
|
214
214
|
|
|
215
215
|
// Should keep only last set (LWW)
|
|
216
216
|
const nameMsg = messages[1];
|
|
217
217
|
expect(nameMsg.ops).toHaveLength(1);
|
|
218
218
|
expect(nameMsg.ops[0]).toMatchObject({
|
|
219
|
-
|
|
219
|
+
ot: 'string.set',
|
|
220
220
|
key: 'player',
|
|
221
221
|
path: 'name',
|
|
222
222
|
value: 'Charlie', // Only last value
|
|
@@ -225,27 +225,27 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
225
225
|
|
|
226
226
|
it('should keep only last boolean.set operation on same target', () => {
|
|
227
227
|
const store = createGraph({
|
|
228
|
-
|
|
228
|
+
client: 'alice',
|
|
229
229
|
coalescingEnabled: true,
|
|
230
230
|
coalescingDelayMs: 0,
|
|
231
231
|
coalescingThresholdMs: 1000,
|
|
232
232
|
onSend: (msg) => messages.push(msg),
|
|
233
233
|
});
|
|
234
234
|
|
|
235
|
-
store.edit({
|
|
235
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { active: false } });
|
|
236
236
|
store.commit('create player');
|
|
237
237
|
|
|
238
238
|
// Toggle active multiple times
|
|
239
|
-
store.edit({
|
|
240
|
-
store.edit({
|
|
241
|
-
store.edit({
|
|
239
|
+
store.edit({ ot: 'boolean.set', key: 'player', path: 'active', value: true });
|
|
240
|
+
store.edit({ ot: 'boolean.set', key: 'player', path: 'active', value: false });
|
|
241
|
+
store.edit({ ot: 'boolean.set', key: 'player', path: 'active', value: true });
|
|
242
242
|
store.commit('toggle active');
|
|
243
243
|
|
|
244
244
|
// Should keep only last set (LWW)
|
|
245
245
|
const activeMsg = messages[1];
|
|
246
246
|
expect(activeMsg.ops).toHaveLength(1);
|
|
247
247
|
expect(activeMsg.ops[0]).toMatchObject({
|
|
248
|
-
|
|
248
|
+
ot: 'boolean.set',
|
|
249
249
|
key: 'player',
|
|
250
250
|
path: 'active',
|
|
251
251
|
value: true, // Only last value
|
|
@@ -254,27 +254,27 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
254
254
|
|
|
255
255
|
it('should keep only last vector3.set operation on same target', () => {
|
|
256
256
|
const store = createGraph({
|
|
257
|
-
|
|
257
|
+
client: 'alice',
|
|
258
258
|
coalescingEnabled: true,
|
|
259
259
|
coalescingDelayMs: 0,
|
|
260
260
|
coalescingThresholdMs: 1000,
|
|
261
261
|
onSend: (msg) => messages.push(msg),
|
|
262
262
|
});
|
|
263
263
|
|
|
264
|
-
store.edit({
|
|
264
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { position: [0, 0, 0] } });
|
|
265
265
|
store.commit('create player');
|
|
266
266
|
|
|
267
267
|
// Set position multiple times
|
|
268
|
-
store.edit({
|
|
269
|
-
store.edit({
|
|
270
|
-
store.edit({
|
|
268
|
+
store.edit({ ot: 'vector3.set', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
269
|
+
store.edit({ ot: 'vector3.set', key: 'player', path: 'position', value: [2, 0, 0] });
|
|
270
|
+
store.edit({ ot: 'vector3.set', key: 'player', path: 'position', value: [3, 0, 0] });
|
|
271
271
|
store.commit('set position');
|
|
272
272
|
|
|
273
273
|
// Should keep only last set (LWW)
|
|
274
274
|
const posMsg = messages[1];
|
|
275
275
|
expect(posMsg.ops).toHaveLength(1);
|
|
276
276
|
expect(posMsg.ops[0]).toMatchObject({
|
|
277
|
-
|
|
277
|
+
ot: 'vector3.set',
|
|
278
278
|
key: 'player',
|
|
279
279
|
path: 'position',
|
|
280
280
|
value: [3, 0, 0], // Only last value
|
|
@@ -283,19 +283,19 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
283
283
|
|
|
284
284
|
it('should NOT merge set operations on different paths', () => {
|
|
285
285
|
const store = createGraph({
|
|
286
|
-
|
|
286
|
+
client: 'alice',
|
|
287
287
|
coalescingEnabled: true,
|
|
288
288
|
coalescingDelayMs: 0,
|
|
289
289
|
coalescingThresholdMs: 1000,
|
|
290
290
|
onSend: (msg) => messages.push(msg),
|
|
291
291
|
});
|
|
292
292
|
|
|
293
|
-
store.edit({
|
|
293
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { score: 0, health: 100 } });
|
|
294
294
|
store.commit('create player');
|
|
295
295
|
|
|
296
296
|
// Set different properties
|
|
297
|
-
store.edit({
|
|
298
|
-
store.edit({
|
|
297
|
+
store.edit({ ot: 'number.set', key: 'player', path: 'score', value: 10 });
|
|
298
|
+
store.edit({ ot: 'number.set', key: 'player', path: 'health', value: 50 });
|
|
299
299
|
store.commit('set stats');
|
|
300
300
|
|
|
301
301
|
// Should NOT coalesce (different paths)
|
|
@@ -309,23 +309,23 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
309
309
|
describe('Mixed operations', () => {
|
|
310
310
|
it('should coalesce operations independently by type', () => {
|
|
311
311
|
const store = createGraph({
|
|
312
|
-
|
|
312
|
+
client: 'alice',
|
|
313
313
|
coalescingEnabled: true,
|
|
314
314
|
coalescingDelayMs: 0,
|
|
315
315
|
coalescingThresholdMs: 1000,
|
|
316
316
|
onSend: (msg) => messages.push(msg),
|
|
317
317
|
});
|
|
318
318
|
|
|
319
|
-
store.edit({
|
|
319
|
+
store.edit({ ot: 'node.insert', key: 'player', value: { score: 0, position: [0, 0, 0], name: '' } });
|
|
320
320
|
store.commit('create player');
|
|
321
321
|
|
|
322
322
|
// Mix of operations
|
|
323
|
-
store.edit({
|
|
324
|
-
store.edit({
|
|
325
|
-
store.edit({
|
|
326
|
-
store.edit({
|
|
327
|
-
store.edit({
|
|
328
|
-
store.edit({
|
|
323
|
+
store.edit({ ot: 'number.add', key: 'player', path: 'score', value: 10 });
|
|
324
|
+
store.edit({ ot: 'number.add', key: 'player', path: 'score', value: 5 });
|
|
325
|
+
store.edit({ ot: 'vector3.add', key: 'player', path: 'position', value: [1, 0, 0] });
|
|
326
|
+
store.edit({ ot: 'vector3.add', key: 'player', path: 'position', value: [0, 2, 0] });
|
|
327
|
+
store.edit({ ot: 'string.set', key: 'player', path: 'name', value: 'Alice' });
|
|
328
|
+
store.edit({ ot: 'string.set', key: 'player', path: 'name', value: 'Bob' });
|
|
329
329
|
store.commit('update player');
|
|
330
330
|
|
|
331
331
|
// Should coalesce each type independently
|
|
@@ -334,21 +334,21 @@ describe('Phase 1 Graph Coalescence', () => {
|
|
|
334
334
|
|
|
335
335
|
// number.add should be coalesced: 10 + 5 = 15
|
|
336
336
|
expect(updateMsg.ops[0]).toMatchObject({
|
|
337
|
-
|
|
337
|
+
ot: 'number.add',
|
|
338
338
|
path: 'score',
|
|
339
339
|
value: 15,
|
|
340
340
|
});
|
|
341
341
|
|
|
342
342
|
// vector3.add should be coalesced: [1, 0, 0] + [0, 2, 0] = [1, 2, 0]
|
|
343
343
|
expect(updateMsg.ops[1]).toMatchObject({
|
|
344
|
-
|
|
344
|
+
ot: 'vector3.add',
|
|
345
345
|
path: 'position',
|
|
346
346
|
value: [1, 2, 0],
|
|
347
347
|
});
|
|
348
348
|
|
|
349
349
|
// string.set should keep last: 'Bob'
|
|
350
350
|
expect(updateMsg.ops[2]).toMatchObject({
|
|
351
|
-
|
|
351
|
+
ot: 'string.set',
|
|
352
352
|
path: 'name',
|
|
353
353
|
value: 'Bob',
|
|
354
354
|
});
|
|
@@ -18,7 +18,7 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
18
18
|
const messages: CRDTMessage[] = [];
|
|
19
19
|
|
|
20
20
|
const store = createGraph({
|
|
21
|
-
|
|
21
|
+
client: 'alice',
|
|
22
22
|
coalescingEnabled: true,
|
|
23
23
|
coalescingDelayMs: 0,
|
|
24
24
|
coalescingThresholdMs: 1000,
|
|
@@ -26,27 +26,27 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
26
26
|
});
|
|
27
27
|
|
|
28
28
|
// Initialize scene and text node
|
|
29
|
-
store.edit({
|
|
29
|
+
store.edit({ ot: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
|
|
30
30
|
store.commit('init scene');
|
|
31
31
|
|
|
32
|
-
store.edit({
|
|
32
|
+
store.edit({ ot: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
|
|
33
33
|
store.commit('add text node');
|
|
34
34
|
|
|
35
|
-
store.edit({
|
|
35
|
+
store.edit({ ot: 'text.init', key: 'text-doc', path: 'content', value: '' } as any);
|
|
36
36
|
store.commit('init text');
|
|
37
37
|
|
|
38
38
|
// Type "Hello" (5 operations)
|
|
39
|
-
store.edit({
|
|
40
|
-
store.edit({
|
|
41
|
-
store.edit({
|
|
42
|
-
store.edit({
|
|
43
|
-
store.edit({
|
|
39
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 0, value: 'H' } as any);
|
|
40
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 1, value: 'e' } as any);
|
|
41
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 2, value: 'l' } as any);
|
|
42
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 3, value: 'l' } as any);
|
|
43
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 4, value: 'o' } as any);
|
|
44
44
|
store.commit('type Hello');
|
|
45
45
|
|
|
46
46
|
messages.length = 0; // Clear previous messages
|
|
47
47
|
|
|
48
48
|
// Delete all 5 characters (1 operation)
|
|
49
|
-
store.edit({
|
|
49
|
+
store.edit({ ot: 'text.delete', key: 'text-doc', path: 'content', position: 0, length: 5 } as any);
|
|
50
50
|
store.commit('delete all');
|
|
51
51
|
|
|
52
52
|
// Verify we sent exactly 1 message
|
|
@@ -54,22 +54,22 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
54
54
|
|
|
55
55
|
const msg = messages[0];
|
|
56
56
|
expect(msg.ops).toHaveLength(1);
|
|
57
|
-
expect(msg.ops[0].
|
|
57
|
+
expect(msg.ops[0].ot).toBe('text.delete');
|
|
58
58
|
|
|
59
59
|
const deleteOp = msg.ops[0] as any;
|
|
60
|
-
expect(deleteOp.
|
|
60
|
+
expect(deleteOp.rm).toBeDefined();
|
|
61
61
|
|
|
62
62
|
// CRITICAL: This verifies the coalescence optimization works!
|
|
63
63
|
// Without the fix, this would have many single-char deletions
|
|
64
|
-
expect(deleteOp.
|
|
65
|
-
expect(deleteOp.
|
|
64
|
+
expect(deleteOp.rm).toHaveLength(1);
|
|
65
|
+
expect(deleteOp.rm[0][1]).toBe(5);
|
|
66
66
|
});
|
|
67
67
|
|
|
68
68
|
it('should coalesce multiple consecutive delete operations', () => {
|
|
69
69
|
const messages: CRDTMessage[] = [];
|
|
70
70
|
|
|
71
71
|
const store = createGraph({
|
|
72
|
-
|
|
72
|
+
client: 'alice',
|
|
73
73
|
coalescingEnabled: true,
|
|
74
74
|
coalescingDelayMs: 0,
|
|
75
75
|
coalescingThresholdMs: 1000,
|
|
@@ -77,23 +77,23 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
77
77
|
});
|
|
78
78
|
|
|
79
79
|
// Initialize
|
|
80
|
-
store.edit({
|
|
80
|
+
store.edit({ ot: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
|
|
81
81
|
store.commit('init');
|
|
82
82
|
|
|
83
|
-
store.edit({
|
|
83
|
+
store.edit({ ot: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
|
|
84
84
|
store.commit('add text');
|
|
85
85
|
|
|
86
|
-
store.edit({
|
|
86
|
+
store.edit({ ot: 'text.init', key: 'text-doc', path: 'content', value: 'Hello' } as any);
|
|
87
87
|
store.commit('init text with Hello');
|
|
88
88
|
|
|
89
89
|
messages.length = 0;
|
|
90
90
|
|
|
91
91
|
// Simulate rapid backspace (5 separate delete operations)
|
|
92
|
-
store.edit({
|
|
93
|
-
store.edit({
|
|
94
|
-
store.edit({
|
|
95
|
-
store.edit({
|
|
96
|
-
store.edit({
|
|
92
|
+
store.edit({ ot: 'text.delete', key: 'text-doc', path: 'content', position: 4, length: 1 } as any); // 'o'
|
|
93
|
+
store.edit({ ot: 'text.delete', key: 'text-doc', path: 'content', position: 3, length: 1 } as any); // 'l'
|
|
94
|
+
store.edit({ ot: 'text.delete', key: 'text-doc', path: 'content', position: 2, length: 1 } as any); // 'l'
|
|
95
|
+
store.edit({ ot: 'text.delete', key: 'text-doc', path: 'content', position: 1, length: 1 } as any); // 'e'
|
|
96
|
+
store.edit({ ot: 'text.delete', key: 'text-doc', path: 'content', position: 0, length: 1 } as any); // 'H'
|
|
97
97
|
store.commit('rapid backspace');
|
|
98
98
|
|
|
99
99
|
// Verify coalescence worked
|
|
@@ -102,22 +102,22 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
102
102
|
const msg = messages[0];
|
|
103
103
|
|
|
104
104
|
// Find the delete operation(s)
|
|
105
|
-
const deleteOps = msg.ops.filter(op => op.
|
|
105
|
+
const deleteOps = msg.ops.filter(op => op.ot === 'text.delete');
|
|
106
106
|
expect(deleteOps.length).toBeGreaterThan(0);
|
|
107
107
|
|
|
108
108
|
// CRITICAL TEST: Verify each operation has optimized deletions
|
|
109
109
|
// The key fix is that isTextDeleteOp now accepts operations with deletions arrays
|
|
110
110
|
for (const op of deleteOps) {
|
|
111
111
|
const deleteOp = op as any;
|
|
112
|
-
expect(deleteOp.
|
|
113
|
-
expect(Array.isArray(deleteOp.
|
|
114
|
-
expect(deleteOp.
|
|
112
|
+
expect(deleteOp.rm).toBeDefined();
|
|
113
|
+
expect(Array.isArray(deleteOp.rm)).toBe(true);
|
|
114
|
+
expect(deleteOp.rm.length).toBeGreaterThan(0);
|
|
115
115
|
|
|
116
116
|
// Each deletion should be valid
|
|
117
|
-
for (const del of deleteOp.
|
|
118
|
-
expect(del
|
|
119
|
-
expect(typeof del
|
|
120
|
-
expect(del
|
|
117
|
+
for (const del of deleteOp.rm) {
|
|
118
|
+
expect(del[0]).toBeDefined();
|
|
119
|
+
expect(typeof del[1]).toBe('number');
|
|
120
|
+
expect(del[1]).toBeGreaterThan(0);
|
|
121
121
|
}
|
|
122
122
|
}
|
|
123
123
|
|
|
@@ -130,7 +130,7 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
130
130
|
const messages: CRDTMessage[] = [];
|
|
131
131
|
|
|
132
132
|
const store = createGraph({
|
|
133
|
-
|
|
133
|
+
client: 'alice',
|
|
134
134
|
coalescingEnabled: true,
|
|
135
135
|
coalescingDelayMs: 0,
|
|
136
136
|
coalescingThresholdMs: 1000,
|
|
@@ -138,19 +138,19 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
138
138
|
});
|
|
139
139
|
|
|
140
140
|
// Initialize
|
|
141
|
-
store.edit({
|
|
141
|
+
store.edit({ ot: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
|
|
142
142
|
store.commit('init');
|
|
143
143
|
|
|
144
|
-
store.edit({
|
|
144
|
+
store.edit({ ot: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
|
|
145
145
|
store.commit('add text');
|
|
146
146
|
|
|
147
|
-
store.edit({
|
|
147
|
+
store.edit({ ot: 'text.init', key: 'text-doc', path: 'content', value: 'Test' } as any);
|
|
148
148
|
store.commit('init text');
|
|
149
149
|
|
|
150
150
|
messages.length = 0;
|
|
151
151
|
|
|
152
152
|
// Single delete operation
|
|
153
|
-
store.edit({
|
|
153
|
+
store.edit({ ot: 'text.delete', key: 'text-doc', path: 'content', position: 0, length: 4 } as any);
|
|
154
154
|
store.commit('delete');
|
|
155
155
|
|
|
156
156
|
expect(messages).toHaveLength(1);
|
|
@@ -159,9 +159,9 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
159
159
|
|
|
160
160
|
// CRITICAL: Verify the operation has the deletions field
|
|
161
161
|
// This is what isTextDeleteOp checks for
|
|
162
|
-
expect(deleteOp.
|
|
163
|
-
expect(Array.isArray(deleteOp.
|
|
164
|
-
expect(deleteOp.
|
|
162
|
+
expect(deleteOp.ot).toBe('text.delete');
|
|
163
|
+
expect(Array.isArray(deleteOp.rm)).toBe(true);
|
|
164
|
+
expect(deleteOp.rm.length).toBeGreaterThan(0);
|
|
165
165
|
|
|
166
166
|
// Verify it does NOT have seq/ts (those are on TextInsertOp only!)
|
|
167
167
|
expect(deleteOp.seq).toBeUndefined();
|
|
@@ -174,7 +174,7 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
174
174
|
const messages: CRDTMessage[] = [];
|
|
175
175
|
|
|
176
176
|
const store = createGraph({
|
|
177
|
-
|
|
177
|
+
client: 'alice',
|
|
178
178
|
coalescingEnabled: true,
|
|
179
179
|
coalescingDelayMs: 0,
|
|
180
180
|
coalescingThresholdMs: 1000,
|
|
@@ -182,23 +182,23 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
182
182
|
});
|
|
183
183
|
|
|
184
184
|
// Initialize
|
|
185
|
-
store.edit({
|
|
185
|
+
store.edit({ ot: 'node.init', key: 'scene', path: '', value: { tag: 'Scene' } });
|
|
186
186
|
store.commit('init');
|
|
187
187
|
|
|
188
|
-
store.edit({
|
|
188
|
+
store.edit({ ot: 'node.insert', key: 'scene', path: 'children', value: { key: 'text-doc', tag: 'Text' } });
|
|
189
189
|
store.commit('add text');
|
|
190
190
|
|
|
191
|
-
store.edit({
|
|
191
|
+
store.edit({ ot: 'text.init', key: 'text-doc', path: 'content', value: '' } as any);
|
|
192
192
|
store.commit('init text');
|
|
193
193
|
|
|
194
194
|
messages.length = 0;
|
|
195
195
|
|
|
196
196
|
// Type "Hello" (5 operations)
|
|
197
|
-
store.edit({
|
|
198
|
-
store.edit({
|
|
199
|
-
store.edit({
|
|
200
|
-
store.edit({
|
|
201
|
-
store.edit({
|
|
197
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 0, value: 'H' } as any);
|
|
198
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 1, value: 'e' } as any);
|
|
199
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 2, value: 'l' } as any);
|
|
200
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 3, value: 'l' } as any);
|
|
201
|
+
store.edit({ ot: 'text.insert', key: 'text-doc', path: 'content', position: 4, value: 'o' } as any);
|
|
202
202
|
store.commit('type Hello');
|
|
203
203
|
|
|
204
204
|
expect(messages).toHaveLength(1);
|
|
@@ -207,10 +207,10 @@ describe('Graph Operation Coalescence (Integration)', () => {
|
|
|
207
207
|
|
|
208
208
|
// Verify inserts were coalesced
|
|
209
209
|
expect(msg.ops).toHaveLength(1);
|
|
210
|
-
expect(msg.ops[0].
|
|
210
|
+
expect(msg.ops[0].ot).toBe('text.insert');
|
|
211
211
|
|
|
212
212
|
const insertOp = msg.ops[0] as any;
|
|
213
|
-
expect(insertOp.
|
|
213
|
+
expect(insertOp.value[1]).toBe('Hello');
|
|
214
214
|
expect(insertOp.id).toBeDefined();
|
|
215
215
|
expect(insertOp.seq).toBeDefined();
|
|
216
216
|
expect(insertOp.ts).toBeDefined();
|
|
@@ -27,18 +27,18 @@ import type { Operation } from '../../src/operations/OperationTypes.js';
|
|
|
27
27
|
function nodeInsertOp(parentKey: string, nodeKey: string, props: Record<string, unknown> = {}): Operation {
|
|
28
28
|
return {
|
|
29
29
|
key: parentKey,
|
|
30
|
-
|
|
30
|
+
ot: 'node.insert',
|
|
31
31
|
path: 'children',
|
|
32
32
|
value: { key: nodeKey, tag: 'Mesh', name: nodeKey, ...props },
|
|
33
33
|
} as Operation;
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function vec3SetOp(nodeKey: string, path: string, value: [number, number, number]): Operation {
|
|
37
|
-
return { key: nodeKey,
|
|
37
|
+
return { key: nodeKey, ot: 'vector3.set', path, value } as Operation;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
function numAddOp(nodeKey: string, path: string, value: number): Operation {
|
|
41
|
-
return { key: nodeKey,
|
|
41
|
+
return { key: nodeKey, ot: 'number.add', path, value } as Operation;
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function commitAndAck(state: ClientState, op: Operation): ClientState {
|
|
@@ -47,8 +47,8 @@ function commitAndAck(state: ClientState, op: Operation): ClientState {
|
|
|
47
47
|
return onServerAck(committed, msg!.id);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function setupState(
|
|
51
|
-
let s = createInitialState(
|
|
50
|
+
function setupState(client = 'bench-session'): ClientState {
|
|
51
|
+
let s = createInitialState(client);
|
|
52
52
|
s = onEdit(s, nodeInsertOp('', 'scene', { tag: 'Scene' }));
|
|
53
53
|
const { state: s1, msg: m1 } = commitEdits(s);
|
|
54
54
|
s = onServerAck(s1, m1!.id);
|