@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
|
@@ -16,13 +16,13 @@ console.log('🎬 Example 04: Conflict Resolution\n');
|
|
|
16
16
|
|
|
17
17
|
let graph = applyMessage(createEmptyGraph(), {
|
|
18
18
|
id: 'setup',
|
|
19
|
-
|
|
19
|
+
client: 'server',
|
|
20
20
|
clock: { server: 1 },
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
lt: 0,
|
|
22
|
+
ts: Date.now() / 1000,
|
|
23
23
|
ops: [{
|
|
24
24
|
key: 'cube',
|
|
25
|
-
|
|
25
|
+
ot: 'node.insert',
|
|
26
26
|
path: 'cube',
|
|
27
27
|
value: {
|
|
28
28
|
id: 'uuid-cube',
|
|
@@ -49,22 +49,22 @@ console.log('Alice sets color to red (lamport: 10) - arrives AFTER Bob\n');
|
|
|
49
49
|
// Bob's message arrives first (higher lamport)
|
|
50
50
|
graph = applyMessage(graph, {
|
|
51
51
|
id: 'bob-color',
|
|
52
|
-
|
|
52
|
+
client: 'bob',
|
|
53
53
|
clock: { bob: 1 },
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
ops: [{ key: 'cube',
|
|
54
|
+
lt: 11,
|
|
55
|
+
ts: Date.now() / 1000,
|
|
56
|
+
ops: [{ key: 'cube', ot: 'color.set', path: 'color', value: '#0000ff' }],
|
|
57
57
|
});
|
|
58
58
|
console.log('After Bob (lamport 11):', graph.nodes['cube'].color);
|
|
59
59
|
|
|
60
60
|
// Alice's message arrives later (lower lamport - should be ignored)
|
|
61
61
|
graph = applyMessage(graph, {
|
|
62
62
|
id: 'alice-color',
|
|
63
|
-
|
|
63
|
+
client: 'alice',
|
|
64
64
|
clock: { alice: 1 },
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
ops: [{ key: 'cube',
|
|
65
|
+
lt: 10,
|
|
66
|
+
ts: Date.now() / 1000,
|
|
67
|
+
ops: [{ key: 'cube', ot: 'color.set', path: 'color', value: '#ff0000' }],
|
|
68
68
|
});
|
|
69
69
|
console.log('After Alice (lamport 10):', graph.nodes['cube'].color);
|
|
70
70
|
|
|
@@ -81,41 +81,41 @@ console.log('Messages can arrive in any order, result is the same.\n');
|
|
|
81
81
|
// Reset position
|
|
82
82
|
graph = applyMessage(graph, {
|
|
83
83
|
id: 'reset',
|
|
84
|
-
|
|
84
|
+
client: 'server',
|
|
85
85
|
clock: { server: 2 },
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
ops: [{ key: 'cube',
|
|
86
|
+
lt: 20,
|
|
87
|
+
ts: Date.now() / 1000,
|
|
88
|
+
ops: [{ key: 'cube', ot: 'vector3.set', path: 'transform.position', value: [0, 0, 0] }],
|
|
89
89
|
});
|
|
90
90
|
|
|
91
91
|
console.log('Position reset to [0, 0, 0]');
|
|
92
92
|
|
|
93
93
|
// Apply in order: Alice, Bob, Charlie
|
|
94
94
|
let graph1 = applyMessage(graph, {
|
|
95
|
-
id: 'alice-move',
|
|
96
|
-
ops: [{ key: 'cube',
|
|
95
|
+
id: 'alice-move', client: 'alice', clock: { alice: 2 }, lt: 21, ts: Date.now() / 1000,
|
|
96
|
+
ops: [{ key: 'cube', ot: 'vector3.add', path: 'transform.position', value: [1, 0, 0] }],
|
|
97
97
|
});
|
|
98
98
|
graph1 = applyMessage(graph1, {
|
|
99
|
-
id: 'bob-move',
|
|
100
|
-
ops: [{ key: 'cube',
|
|
99
|
+
id: 'bob-move', client: 'bob', clock: { bob: 2 }, lt: 22, ts: Date.now() / 1000,
|
|
100
|
+
ops: [{ key: 'cube', ot: 'vector3.add', path: 'transform.position', value: [0, 2, 0] }],
|
|
101
101
|
});
|
|
102
102
|
graph1 = applyMessage(graph1, {
|
|
103
|
-
id: 'charlie-move',
|
|
104
|
-
ops: [{ key: 'cube',
|
|
103
|
+
id: 'charlie-move', client: 'charlie', clock: { charlie: 1 }, lt: 23, ts: Date.now() / 1000,
|
|
104
|
+
ops: [{ key: 'cube', ot: 'vector3.add', path: 'transform.position', value: [0, 0, 3] }],
|
|
105
105
|
});
|
|
106
106
|
|
|
107
107
|
// Apply in reverse order: Charlie, Bob, Alice
|
|
108
108
|
let graph2 = applyMessage(graph, {
|
|
109
|
-
id: 'charlie-move',
|
|
110
|
-
ops: [{ key: 'cube',
|
|
109
|
+
id: 'charlie-move', client: 'charlie', clock: { charlie: 1 }, lt: 23, ts: Date.now() / 1000,
|
|
110
|
+
ops: [{ key: 'cube', ot: 'vector3.add', path: 'transform.position', value: [0, 0, 3] }],
|
|
111
111
|
});
|
|
112
112
|
graph2 = applyMessage(graph2, {
|
|
113
|
-
id: 'bob-move',
|
|
114
|
-
ops: [{ key: 'cube',
|
|
113
|
+
id: 'bob-move', client: 'bob', clock: { bob: 2 }, lt: 22, ts: Date.now() / 1000,
|
|
114
|
+
ops: [{ key: 'cube', ot: 'vector3.add', path: 'transform.position', value: [0, 2, 0] }],
|
|
115
115
|
});
|
|
116
116
|
graph2 = applyMessage(graph2, {
|
|
117
|
-
id: 'alice-move',
|
|
118
|
-
ops: [{ key: 'cube',
|
|
117
|
+
id: 'alice-move', client: 'alice', clock: { alice: 2 }, lt: 21, ts: Date.now() / 1000,
|
|
118
|
+
ops: [{ key: 'cube', ot: 'vector3.add', path: 'transform.position', value: [1, 0, 0] }],
|
|
119
119
|
});
|
|
120
120
|
|
|
121
121
|
console.log('Order 1 (Alice → Bob → Charlie):', graph1.nodes['cube']['transform.position']);
|
|
@@ -135,13 +135,13 @@ console.log('Simulating two users editing simultaneously...\n');
|
|
|
135
135
|
// Start fresh
|
|
136
136
|
graph = applyMessage(createEmptyGraph(), {
|
|
137
137
|
id: 'init',
|
|
138
|
-
|
|
138
|
+
client: 'server',
|
|
139
139
|
clock: { server: 1 },
|
|
140
|
-
|
|
141
|
-
|
|
140
|
+
lt: 0,
|
|
141
|
+
ts: Date.now() / 1000,
|
|
142
142
|
ops: [{
|
|
143
143
|
key: 'box',
|
|
144
|
-
|
|
144
|
+
ot: 'node.insert',
|
|
145
145
|
path: 'box',
|
|
146
146
|
value: {
|
|
147
147
|
id: 'uuid-box',
|
|
@@ -158,26 +158,26 @@ console.log('Initial: color=#888888, position=[0,0,0]');
|
|
|
158
158
|
// Alice drags and changes color
|
|
159
159
|
const aliceMsg: CRDTMessage = {
|
|
160
160
|
id: 'alice-edit',
|
|
161
|
-
|
|
161
|
+
client: 'alice',
|
|
162
162
|
clock: { alice: 1 },
|
|
163
|
-
|
|
164
|
-
|
|
163
|
+
lt: 5,
|
|
164
|
+
ts: Date.now() / 1000,
|
|
165
165
|
ops: [
|
|
166
|
-
{ key: 'box',
|
|
167
|
-
{ key: 'box',
|
|
166
|
+
{ key: 'box', ot: 'vector3.add', path: 'transform.position', value: [10, 0, 0] },
|
|
167
|
+
{ key: 'box', ot: 'color.set', path: 'color', value: '#ff0000' },
|
|
168
168
|
],
|
|
169
169
|
};
|
|
170
170
|
|
|
171
171
|
// Bob drags and changes color (higher lamport)
|
|
172
172
|
const bobMsg: CRDTMessage = {
|
|
173
173
|
id: 'bob-edit',
|
|
174
|
-
|
|
174
|
+
client: 'bob',
|
|
175
175
|
clock: { bob: 1 },
|
|
176
|
-
|
|
177
|
-
|
|
176
|
+
lt: 6,
|
|
177
|
+
ts: Date.now() / 1000,
|
|
178
178
|
ops: [
|
|
179
|
-
{ key: 'box',
|
|
180
|
-
{ key: 'box',
|
|
179
|
+
{ key: 'box', ot: 'vector3.add', path: 'transform.position', value: [0, 5, 0] },
|
|
180
|
+
{ key: 'box', ot: 'color.set', path: 'color', value: '#00ff00' },
|
|
181
181
|
],
|
|
182
182
|
};
|
|
183
183
|
|
|
@@ -251,8 +251,8 @@ function processMessage(s: State, msg: CRDTMessage): State {
|
|
|
251
251
|
|
|
252
252
|
// Init
|
|
253
253
|
state.graph = applyMessage(state.graph, {
|
|
254
|
-
id: 'init',
|
|
255
|
-
ops: [{ key: 'obj',
|
|
254
|
+
id: 'init', client: 'server', clock: { server: 1 }, lt: 0, ts: Date.now() / 1000,
|
|
255
|
+
ops: [{ key: 'obj', ot: 'node.insert', path: 'obj', value: { key: 'uuid', tag: 'Mesh', name: 'Object', color: '#ffffff' }}],
|
|
256
256
|
});
|
|
257
257
|
|
|
258
258
|
console.log('\n📖 State = { graph, journal }');
|
|
@@ -266,18 +266,18 @@ for (let i = 0; i < messages.length; i++) {
|
|
|
266
266
|
|
|
267
267
|
const msg: CRDTMessage = {
|
|
268
268
|
id: `msg-${m.lamport}`,
|
|
269
|
-
|
|
269
|
+
client: m.name,
|
|
270
270
|
clock: { [m.name]: 1 },
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
ops: [{ key: 'obj',
|
|
271
|
+
lt: m.lamport,
|
|
272
|
+
ts: Date.now() / 1000,
|
|
273
|
+
ops: [{ key: 'obj', ot: 'color.set', path: 'color', value: m.color }],
|
|
274
274
|
};
|
|
275
275
|
|
|
276
276
|
// Process: append to journal + apply to graph
|
|
277
277
|
Object.assign(state, processMessage(state, msg));
|
|
278
278
|
|
|
279
279
|
const applied = state.graph.nodes['obj'].color !== prevColor;
|
|
280
|
-
const maxLamport = state.graph.nodes['obj']._crdt.
|
|
280
|
+
const maxLamport = state.graph.nodes['obj']._crdt.lt;
|
|
281
281
|
const appliedStr = applied ? '✓ applied' : '✗ ignored';
|
|
282
282
|
|
|
283
283
|
console.log(` ${i + 1} ${m.name.padEnd(16)} ${String(m.lamport).padStart(3)} ${String(maxLamport).padStart(3)} ${state.graph.nodes['obj'].color} ${appliedStr}`);
|
|
@@ -285,7 +285,7 @@ for (let i = 0; i < messages.length; i++) {
|
|
|
285
285
|
|
|
286
286
|
console.log('─'.repeat(65));
|
|
287
287
|
console.log(`\n state.journal: ${state.journal.length} messages`);
|
|
288
|
-
console.log(` state.graph: color = ${state.graph.nodes['obj'].color} (maxLamport = ${state.graph.nodes['obj']._crdt.
|
|
288
|
+
console.log(` state.graph: color = ${state.graph.nodes['obj'].color} (maxLamport = ${state.graph.nodes['obj']._crdt.lt})`);
|
|
289
289
|
|
|
290
290
|
const isCorrect = state.graph.nodes['obj'].color === '#ffff00';
|
|
291
291
|
console.log(`\n${isCorrect ? '✅' : '❌'} Result: ${isCorrect ? 'CORRECT' : 'WRONG'}`);
|
|
@@ -304,19 +304,19 @@ const results: string[] = [];
|
|
|
304
304
|
|
|
305
305
|
for (let i = 0; i < orders.length; i++) {
|
|
306
306
|
let g = applyMessage(createEmptyGraph(), {
|
|
307
|
-
id: 'init',
|
|
308
|
-
ops: [{ key: 'obj',
|
|
307
|
+
id: 'init', client: 'server', clock: { server: 1 }, lt: 0, ts: Date.now() / 1000,
|
|
308
|
+
ops: [{ key: 'obj', ot: 'node.insert', path: 'obj', value: { key: 'uuid', tag: 'Mesh', name: 'Object', color: '#ffffff' }}],
|
|
309
309
|
});
|
|
310
310
|
|
|
311
311
|
for (const idx of orders[i]) {
|
|
312
312
|
const msg = messages[idx];
|
|
313
313
|
g = applyMessage(g, {
|
|
314
314
|
id: `msg-${msg.lamport}`,
|
|
315
|
-
|
|
315
|
+
client: msg.name,
|
|
316
316
|
clock: { [msg.name]: 1 },
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
ops: [{ key: 'obj',
|
|
317
|
+
lt: msg.lamport,
|
|
318
|
+
ts: Date.now() / 1000,
|
|
319
|
+
ops: [{ key: 'obj', ot: 'color.set', path: 'color', value: msg.color }],
|
|
320
320
|
});
|
|
321
321
|
}
|
|
322
322
|
|
|
@@ -26,17 +26,17 @@ console.log('--- Example 1: No Coalescence ---\n');
|
|
|
26
26
|
|
|
27
27
|
const messages1: CRDTMessage[] = [];
|
|
28
28
|
const store1 = createGraph({
|
|
29
|
-
|
|
29
|
+
client: 'session-1',
|
|
30
30
|
coalescingEnabled: false, // No auto-commit
|
|
31
31
|
onSend: (msg) => messages1.push(msg),
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
// User types "hello"
|
|
35
|
-
store1.edit({
|
|
36
|
-
store1.edit({
|
|
37
|
-
store1.edit({
|
|
38
|
-
store1.edit({
|
|
39
|
-
store1.edit({
|
|
35
|
+
store1.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'h' });
|
|
36
|
+
store1.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'e' });
|
|
37
|
+
store1.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 2, value: 'l' });
|
|
38
|
+
store1.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 3, value: 'l' });
|
|
39
|
+
store1.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 4, value: 'o' });
|
|
40
40
|
|
|
41
41
|
// Manual commit (no coalescence since coalescingEnabled = false)
|
|
42
42
|
store1.commit();
|
|
@@ -53,7 +53,7 @@ console.log('--- Example 2: Batching Only ---\n');
|
|
|
53
53
|
|
|
54
54
|
const messages2: CRDTMessage[] = [];
|
|
55
55
|
const store2 = createGraph({
|
|
56
|
-
|
|
56
|
+
client: 'session-2',
|
|
57
57
|
coalescingEnabled: true, // Enable auto-commit
|
|
58
58
|
coalescingDelayMs: 100, // Wait 100ms before sending
|
|
59
59
|
coalescingThresholdMs: 0, // Don't merge operations (0 threshold)
|
|
@@ -61,11 +61,11 @@ const store2 = createGraph({
|
|
|
61
61
|
});
|
|
62
62
|
|
|
63
63
|
// User types "hello" quickly
|
|
64
|
-
store2.edit({
|
|
65
|
-
store2.edit({
|
|
66
|
-
store2.edit({
|
|
67
|
-
store2.edit({
|
|
68
|
-
store2.edit({
|
|
64
|
+
store2.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'h' });
|
|
65
|
+
store2.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'e' });
|
|
66
|
+
store2.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 2, value: 'l' });
|
|
67
|
+
store2.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 3, value: 'l' });
|
|
68
|
+
store2.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 4, value: 'o' });
|
|
69
69
|
|
|
70
70
|
// Wait for auto-commit
|
|
71
71
|
await new Promise(resolve => setTimeout(resolve, 150));
|
|
@@ -82,7 +82,7 @@ console.log('--- Example 3: Full Coalescence ---\n');
|
|
|
82
82
|
|
|
83
83
|
const messages3: CRDTMessage[] = [];
|
|
84
84
|
const store3 = createGraph({
|
|
85
|
-
|
|
85
|
+
client: 'session-3',
|
|
86
86
|
coalescingEnabled: true, // Enable auto-commit
|
|
87
87
|
coalescingDelayMs: 100, // Wait 100ms before sending
|
|
88
88
|
coalescingThresholdMs: 1000, // Merge ops within 1 second
|
|
@@ -91,15 +91,15 @@ const store3 = createGraph({
|
|
|
91
91
|
|
|
92
92
|
// User types "hello" quickly
|
|
93
93
|
const startTime = Date.now();
|
|
94
|
-
store3.edit({
|
|
94
|
+
store3.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'h' });
|
|
95
95
|
await sleep(10);
|
|
96
|
-
store3.edit({
|
|
96
|
+
store3.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'e' });
|
|
97
97
|
await sleep(10);
|
|
98
|
-
store3.edit({
|
|
98
|
+
store3.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 2, value: 'l' });
|
|
99
99
|
await sleep(10);
|
|
100
|
-
store3.edit({
|
|
100
|
+
store3.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 3, value: 'l' });
|
|
101
101
|
await sleep(10);
|
|
102
|
-
store3.edit({
|
|
102
|
+
store3.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 4, value: 'o' });
|
|
103
103
|
const endTime = Date.now();
|
|
104
104
|
const typingDuration = endTime - startTime;
|
|
105
105
|
|
|
@@ -109,7 +109,7 @@ await new Promise(resolve => setTimeout(resolve, 150));
|
|
|
109
109
|
console.log(`Typing duration: ${typingDuration}ms (< 1000ms threshold)`);
|
|
110
110
|
console.log(`Messages sent: ${messages3.length}`);
|
|
111
111
|
console.log(`Operations in message: ${messages3[0].ops.length}`);
|
|
112
|
-
if (messages3[0].ops[0].
|
|
112
|
+
if (messages3[0].ops[0].ot === 'text.insert') {
|
|
113
113
|
const op = messages3[0].ops[0] as any;
|
|
114
114
|
console.log(`Operation content: "${op.content}"`);
|
|
115
115
|
console.log('Result: 1 message with 1 operation (fully coalesced!)\n');
|
|
@@ -123,7 +123,7 @@ console.log('--- Example 4: Dynamic Configuration ---\n');
|
|
|
123
123
|
|
|
124
124
|
const messages4: CRDTMessage[] = [];
|
|
125
125
|
const store4 = createGraph({
|
|
126
|
-
|
|
126
|
+
client: 'session-4',
|
|
127
127
|
coalescingEnabled: false,
|
|
128
128
|
onSend: (msg) => messages4.push(msg),
|
|
129
129
|
});
|
|
@@ -144,15 +144,15 @@ console.log(` Delay: ${store4.getCoalescingDelay()}ms`);
|
|
|
144
144
|
console.log(` Threshold: ${store4.getCoalescingThreshold()}ms\n`);
|
|
145
145
|
|
|
146
146
|
// Type with new settings
|
|
147
|
-
store4.edit({
|
|
147
|
+
store4.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 0, value: 'h' });
|
|
148
148
|
await sleep(10);
|
|
149
|
-
store4.edit({
|
|
149
|
+
store4.edit({ ot: 'text.insert', key: 'doc', path: 'content', position: 1, value: 'i' });
|
|
150
150
|
|
|
151
151
|
// Wait for auto-commit
|
|
152
152
|
await sleep(150);
|
|
153
153
|
|
|
154
154
|
console.log(`Messages sent: ${messages4.length}`);
|
|
155
|
-
if (messages4.length > 0 && messages4[0].ops[0].
|
|
155
|
+
if (messages4.length > 0 && messages4[0].ops[0].ot === 'text.insert') {
|
|
156
156
|
const op = messages4[0].ops[0] as any;
|
|
157
157
|
console.log(`Coalesced content: "${op.content}"`);
|
|
158
158
|
}
|
package/examples/README.md
CHANGED
|
@@ -45,19 +45,19 @@ Demonstrates conflict resolution:
|
|
|
45
45
|
|
|
46
46
|
```typescript
|
|
47
47
|
interface CRDTMessage {
|
|
48
|
-
id: string;
|
|
49
|
-
|
|
50
|
-
clock: VectorClock
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
ops: Operation[];
|
|
48
|
+
id: string; // Message ID
|
|
49
|
+
client: string; // Who sent this
|
|
50
|
+
clock: VectorClock;// For causal ordering
|
|
51
|
+
lt: number; // Lamport time for total ordering (LWW)
|
|
52
|
+
ts: number; // Wall-clock timestamp
|
|
53
|
+
ops: Operation[]; // Batch of operations
|
|
54
54
|
}
|
|
55
55
|
```
|
|
56
56
|
|
|
57
57
|
### Operation Types
|
|
58
58
|
|
|
59
|
-
|
|
|
60
|
-
|
|
59
|
+
| ot | Merge Behavior | Use Case |
|
|
60
|
+
|----|---------------|----------|
|
|
61
61
|
| `node.insert` | Idempotent | Create node (use `parent` field to auto-add to parent's children) |
|
|
62
62
|
| `node.remove` | Tombstone | Delete node |
|
|
63
63
|
| `node.move` | Reparent | Move node to new parent |
|
|
@@ -83,9 +83,9 @@ Bob: position += [0, 3, 0]
|
|
|
83
83
|
Result: position += [5, 3, 0] ✅
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
**LWW** (`*.set`): Higher
|
|
86
|
+
**LWW** (`*.set`): Higher lt wins
|
|
87
87
|
```
|
|
88
|
-
Alice: color = red (
|
|
89
|
-
Bob: color = blue (
|
|
90
|
-
Result: color = blue ✅ (Bob's
|
|
88
|
+
Alice: color = red (lt: 10)
|
|
89
|
+
Bob: color = blue (lt: 11)
|
|
90
|
+
Result: color = blue ✅ (Bob's lt is higher)
|
|
91
91
|
```
|
package/package.json
CHANGED
package/src/client/EditBuffer.ts
CHANGED
|
@@ -21,30 +21,30 @@ import type { Operation } from '../operations/OperationTypes.js';
|
|
|
21
21
|
* separately (coalesced later in commitEdits).
|
|
22
22
|
*/
|
|
23
23
|
export function opDedupKey(op: Operation): string {
|
|
24
|
-
if (op.
|
|
25
|
-
if (op.
|
|
26
|
-
if (op.
|
|
24
|
+
if (op.ot === 'node.insert') return `${op.key}:${op.path}:insert:${(op as any).value.key}`;
|
|
25
|
+
if (op.ot === 'node.remove') return `${op.key}:${op.path}:remove:${(op as any).value}`;
|
|
26
|
+
if (op.ot === 'node.move') return `${op.key}:${op.path}:move:${(op as any).value.nodeKey}`;
|
|
27
27
|
// Text operations: include position so consecutive inserts aren't deduped
|
|
28
|
-
if (op.
|
|
29
|
-
if (op.
|
|
28
|
+
if (op.ot === 'text.insert') return `${op.key}:${op.path}:text.insert:${(op as any).position}`;
|
|
29
|
+
if (op.ot === 'text.delete') return `${op.key}:${op.path}:text.delete:${(op as any).position}:${(op as any).length}`;
|
|
30
30
|
return `${op.key}:${op.path}`;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Check if an operation type is additive (can be merged)
|
|
35
35
|
*/
|
|
36
|
-
export function isAdditiveOp(
|
|
37
|
-
return
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
export function isAdditiveOp(ot: string): boolean {
|
|
37
|
+
return ot === 'vector3.add' ||
|
|
38
|
+
ot === 'number.add' ||
|
|
39
|
+
ot === 'number.multiply' ||
|
|
40
|
+
ot === 'quaternion.multiply';
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
/**
|
|
44
44
|
* Type for position-based text insert (used locally before CRDT conversion)
|
|
45
45
|
*/
|
|
46
46
|
interface PositionTextInsertOp {
|
|
47
|
-
|
|
47
|
+
ot: 'text.insert';
|
|
48
48
|
key: string;
|
|
49
49
|
path: string;
|
|
50
50
|
position: number;
|
|
@@ -55,7 +55,7 @@ interface PositionTextInsertOp {
|
|
|
55
55
|
* Check if an operation is a position-based text insert
|
|
56
56
|
*/
|
|
57
57
|
export function isPositionTextInsertOp(op: Operation): op is PositionTextInsertOp {
|
|
58
|
-
return op.
|
|
58
|
+
return op.ot === 'text.insert' &&
|
|
59
59
|
typeof (op as any).position === 'number' &&
|
|
60
60
|
typeof (op as any).value === 'string';
|
|
61
61
|
}
|
|
@@ -64,7 +64,7 @@ export function isPositionTextInsertOp(op: Operation): op is PositionTextInsertO
|
|
|
64
64
|
* Type for position-based text delete
|
|
65
65
|
*/
|
|
66
66
|
interface PositionTextDeleteOp {
|
|
67
|
-
|
|
67
|
+
ot: 'text.delete';
|
|
68
68
|
key: string;
|
|
69
69
|
path: string;
|
|
70
70
|
position: number;
|
|
@@ -75,7 +75,7 @@ interface PositionTextDeleteOp {
|
|
|
75
75
|
* Check if an operation is a text delete with position
|
|
76
76
|
*/
|
|
77
77
|
export function isPositionTextDeleteOp(op: Operation): op is PositionTextDeleteOp {
|
|
78
|
-
return op.
|
|
78
|
+
return op.ot === 'text.delete' &&
|
|
79
79
|
typeof (op as any).position === 'number' &&
|
|
80
80
|
typeof (op as any).length === 'number';
|
|
81
81
|
}
|
|
@@ -144,7 +144,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
144
144
|
if (pendingInsert === null) {
|
|
145
145
|
// Start new pending insert
|
|
146
146
|
pendingInsert = {
|
|
147
|
-
|
|
147
|
+
ot: 'text.insert',
|
|
148
148
|
key: op.key,
|
|
149
149
|
path: op.path,
|
|
150
150
|
position: op.position,
|
|
@@ -157,7 +157,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
157
157
|
) {
|
|
158
158
|
// Sequential insert - merge values
|
|
159
159
|
pendingInsert = {
|
|
160
|
-
|
|
160
|
+
ot: 'text.insert',
|
|
161
161
|
key: pendingInsert.key,
|
|
162
162
|
path: pendingInsert.path,
|
|
163
163
|
position: pendingInsert.position,
|
|
@@ -167,7 +167,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
167
167
|
// Different target or non-sequential - flush pending and start new
|
|
168
168
|
result.push(pendingInsert as unknown as Operation);
|
|
169
169
|
pendingInsert = {
|
|
170
|
-
|
|
170
|
+
ot: 'text.insert',
|
|
171
171
|
key: op.key,
|
|
172
172
|
path: op.path,
|
|
173
173
|
position: op.position,
|
|
@@ -176,7 +176,8 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
176
176
|
}
|
|
177
177
|
} else if (isPositionTextDeleteOp(op)) {
|
|
178
178
|
// Check if operation has CRDT metadata - if so, don't coalesce it
|
|
179
|
-
|
|
179
|
+
// Support both old (deletions) and new (value) field names
|
|
180
|
+
const hasCRDTMetadata = (op as any).deletions !== undefined || (op as any).value !== undefined;
|
|
180
181
|
|
|
181
182
|
if (hasCRDTMetadata) {
|
|
182
183
|
// Flush any pending operations
|
|
@@ -202,7 +203,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
202
203
|
// Coalesce deletes (only for position-based operations without CRDT metadata)
|
|
203
204
|
if (pendingDelete === null) {
|
|
204
205
|
pendingDelete = {
|
|
205
|
-
|
|
206
|
+
ot: 'text.delete',
|
|
206
207
|
key: op.key,
|
|
207
208
|
path: op.path,
|
|
208
209
|
position: op.position,
|
|
@@ -219,7 +220,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
219
220
|
if (isForwardDelete) {
|
|
220
221
|
// Forward delete: accumulate lengths at same position
|
|
221
222
|
pendingDelete = {
|
|
222
|
-
|
|
223
|
+
ot: 'text.delete',
|
|
223
224
|
key: pendingDelete.key,
|
|
224
225
|
path: pendingDelete.path,
|
|
225
226
|
position: pendingDelete.position,
|
|
@@ -228,7 +229,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
228
229
|
} else if (isBackwardDelete) {
|
|
229
230
|
// Backward delete: position moves left, accumulate lengths
|
|
230
231
|
pendingDelete = {
|
|
231
|
-
|
|
232
|
+
ot: 'text.delete',
|
|
232
233
|
key: pendingDelete.key,
|
|
233
234
|
path: pendingDelete.path,
|
|
234
235
|
position: op.position, // use new (leftmost) position
|
|
@@ -238,7 +239,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
238
239
|
// Non-consecutive - flush and start new
|
|
239
240
|
result.push(pendingDelete as unknown as Operation);
|
|
240
241
|
pendingDelete = {
|
|
241
|
-
|
|
242
|
+
ot: 'text.delete',
|
|
242
243
|
key: op.key,
|
|
243
244
|
path: op.path,
|
|
244
245
|
position: op.position,
|
|
@@ -249,7 +250,7 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
249
250
|
// Different key/path - flush and start new
|
|
250
251
|
result.push(pendingDelete as unknown as Operation);
|
|
251
252
|
pendingDelete = {
|
|
252
|
-
|
|
253
|
+
ot: 'text.delete',
|
|
253
254
|
key: op.key,
|
|
254
255
|
path: op.path,
|
|
255
256
|
position: op.position,
|
|
@@ -284,8 +285,8 @@ export function coalesceTextOps(ops: Operation[]): Operation[] {
|
|
|
284
285
|
/**
|
|
285
286
|
* Merge two values for additive operations
|
|
286
287
|
*/
|
|
287
|
-
export function mergeValues(
|
|
288
|
-
switch (
|
|
288
|
+
export function mergeValues(ot: string, a: unknown, b: unknown): unknown {
|
|
289
|
+
switch (ot) {
|
|
289
290
|
case 'vector3.add': {
|
|
290
291
|
const va = a as [number, number, number];
|
|
291
292
|
const vb = b as [number, number, number];
|
|
@@ -325,9 +326,9 @@ export class EditBufferImpl {
|
|
|
325
326
|
const key = opDedupKey(op);
|
|
326
327
|
const existing = this.opsMap.get(key);
|
|
327
328
|
|
|
328
|
-
if (existing && existing.
|
|
329
|
+
if (existing && existing.ot === op.ot && isAdditiveOp(op.ot)) {
|
|
329
330
|
// Merge additive ops
|
|
330
|
-
const mergedValue = mergeValues(op.
|
|
331
|
+
const mergedValue = mergeValues(op.ot, (existing as any).value, (op as any).value);
|
|
331
332
|
this.opsMap.set(key, { ...existing, value: mergedValue } as Operation);
|
|
332
333
|
} else {
|
|
333
334
|
// New op or LWW replacement
|
|
@@ -18,7 +18,7 @@ import { createTextDocument } from '@vuer-ai/vuer-rtc';
|
|
|
18
18
|
|
|
19
19
|
// Create a text document
|
|
20
20
|
const doc = createTextDocument({
|
|
21
|
-
|
|
21
|
+
client: 'alice-123',
|
|
22
22
|
onSend: (msg) => websocket.send(JSON.stringify(msg)),
|
|
23
23
|
});
|
|
24
24
|
|
|
@@ -64,7 +64,7 @@ function createTextDocument(options: CreateTextDocumentOptions): TextDocumentSto
|
|
|
64
64
|
|
|
65
65
|
| Option | Type | Required | Default | Description |
|
|
66
66
|
|--------|------|----------|---------|-------------|
|
|
67
|
-
| `
|
|
67
|
+
| `client` | `string` | ✓ | - | Unique client identifier |
|
|
68
68
|
| `initialSnapshot` | `TextSnapshot` | - | `undefined` | Initialize from server snapshot |
|
|
69
69
|
| `onSend` | `(msg: TextMessage) => void` | - | `undefined` | Callback when messages are ready to send |
|
|
70
70
|
| `onStateChange` | `(state: TextDocumentState) => void` | - | `undefined` | Callback on every state change |
|
|
@@ -78,7 +78,7 @@ function createTextDocument(options: CreateTextDocumentOptions): TextDocumentSto
|
|
|
78
78
|
|
|
79
79
|
```typescript
|
|
80
80
|
const doc = createTextDocument({
|
|
81
|
-
|
|
81
|
+
client: 'user-123',
|
|
82
82
|
coalescingEnabled: true,
|
|
83
83
|
coalescingDelayMs: 500,
|
|
84
84
|
onSend: (msg) => {
|
|
@@ -116,7 +116,7 @@ function createTextDocumentFromServer(
|
|
|
116
116
|
const { snapshot, journal } = await fetch('/api/doc/123').then(r => r.json());
|
|
117
117
|
|
|
118
118
|
const doc = createTextDocumentFromServer({
|
|
119
|
-
|
|
119
|
+
client: 'user-123',
|
|
120
120
|
snapshot,
|
|
121
121
|
journal,
|
|
122
122
|
onSend: (msg) => ws.send(JSON.stringify(msg)),
|
|
@@ -507,7 +507,7 @@ interface TextDocumentState {
|
|
|
507
507
|
snapshot: TextSnapshot; // Checkpoint
|
|
508
508
|
lamportTime: number; // Logical clock
|
|
509
509
|
vectorClock: VectorClock; // Causality tracking
|
|
510
|
-
|
|
510
|
+
client: string; // This client's ID
|
|
511
511
|
}
|
|
512
512
|
```
|
|
513
513
|
|
|
@@ -518,7 +518,7 @@ A message containing text operations.
|
|
|
518
518
|
```typescript
|
|
519
519
|
interface TextMessage {
|
|
520
520
|
msgId: string; // Unique message ID
|
|
521
|
-
|
|
521
|
+
client: string; // Sender's client ID
|
|
522
522
|
operations: TextOperation[]; // Operations in this message
|
|
523
523
|
vectorClock: VectorClock; // Causal ordering
|
|
524
524
|
lamportTime: number; // Total ordering
|
|
@@ -579,7 +579,7 @@ interface TextSnapshot {
|
|
|
579
579
|
import { createTextDocument } from '@vuer-ai/vuer-rtc';
|
|
580
580
|
|
|
581
581
|
const doc = createTextDocument({
|
|
582
|
-
|
|
582
|
+
client: 'local-user',
|
|
583
583
|
});
|
|
584
584
|
|
|
585
585
|
// Edit
|
|
@@ -602,7 +602,7 @@ import { createTextDocument } from '@vuer-ai/vuer-rtc';
|
|
|
602
602
|
|
|
603
603
|
const ws = new WebSocket('ws://server.com/doc/123');
|
|
604
604
|
const doc = createTextDocument({
|
|
605
|
-
|
|
605
|
+
client: generateClientId(),
|
|
606
606
|
coalescingEnabled: true,
|
|
607
607
|
coalescingDelayMs: 300,
|
|
608
608
|
onSend: (msg) => {
|
|
@@ -652,7 +652,7 @@ function useTextDocument(docId: string) {
|
|
|
652
652
|
useEffect(() => {
|
|
653
653
|
const ws = new WebSocket(`ws://server/doc/${docId}`);
|
|
654
654
|
const doc = createTextDocument({
|
|
655
|
-
|
|
655
|
+
client: generateClientId(),
|
|
656
656
|
coalescingEnabled: true,
|
|
657
657
|
onSend: (msg) => ws.send(JSON.stringify(msg)),
|
|
658
658
|
});
|