@vuer-ai/vuer-rtc-server 0.1.1
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/.env +1 -0
- package/PHASE1_SUMMARY.md +94 -0
- package/README.md +423 -0
- package/dist/broker/InMemoryBroker.d.ts +24 -0
- package/dist/broker/InMemoryBroker.d.ts.map +1 -0
- package/dist/broker/InMemoryBroker.js +65 -0
- package/dist/broker/InMemoryBroker.js.map +1 -0
- package/dist/broker/index.d.ts +3 -0
- package/dist/broker/index.d.ts.map +1 -0
- package/dist/broker/index.js +2 -0
- package/dist/broker/index.js.map +1 -0
- package/dist/broker/types.d.ts +47 -0
- package/dist/broker/types.d.ts.map +1 -0
- package/dist/broker/types.js +9 -0
- package/dist/broker/types.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/journal/JournalRepository.d.ts +39 -0
- package/dist/journal/JournalRepository.d.ts.map +1 -0
- package/dist/journal/JournalRepository.js +102 -0
- package/dist/journal/JournalRepository.js.map +1 -0
- package/dist/journal/JournalService.d.ts +69 -0
- package/dist/journal/JournalService.d.ts.map +1 -0
- package/dist/journal/JournalService.js +224 -0
- package/dist/journal/JournalService.js.map +1 -0
- package/dist/journal/index.d.ts +6 -0
- package/dist/journal/index.d.ts.map +1 -0
- package/dist/journal/index.js +6 -0
- package/dist/journal/index.js.map +1 -0
- package/dist/persistence/DocumentRepository.d.ts +22 -0
- package/dist/persistence/DocumentRepository.d.ts.map +1 -0
- package/dist/persistence/DocumentRepository.js +66 -0
- package/dist/persistence/DocumentRepository.js.map +1 -0
- package/dist/persistence/PrismaClient.d.ts +8 -0
- package/dist/persistence/PrismaClient.d.ts.map +1 -0
- package/dist/persistence/PrismaClient.js +21 -0
- package/dist/persistence/PrismaClient.js.map +1 -0
- package/dist/persistence/SessionRepository.d.ts +22 -0
- package/dist/persistence/SessionRepository.d.ts.map +1 -0
- package/dist/persistence/SessionRepository.js +103 -0
- package/dist/persistence/SessionRepository.js.map +1 -0
- package/dist/persistence/index.d.ts +7 -0
- package/dist/persistence/index.d.ts.map +1 -0
- package/dist/persistence/index.js +7 -0
- package/dist/persistence/index.js.map +1 -0
- package/dist/serve.d.ts +18 -0
- package/dist/serve.d.ts.map +1 -0
- package/dist/serve.js +211 -0
- package/dist/serve.js.map +1 -0
- package/dist/transport/RTCServer.d.ts +92 -0
- package/dist/transport/RTCServer.d.ts.map +1 -0
- package/dist/transport/RTCServer.js +273 -0
- package/dist/transport/RTCServer.js.map +1 -0
- package/dist/transport/index.d.ts +2 -0
- package/dist/transport/index.d.ts.map +1 -0
- package/dist/transport/index.js +2 -0
- package/dist/transport/index.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/jest.config.js +36 -0
- package/package.json +56 -0
- package/prisma/schema.prisma +121 -0
- package/src/broker/InMemoryBroker.ts +81 -0
- package/src/broker/index.ts +2 -0
- package/src/broker/types.ts +60 -0
- package/src/index.ts +23 -0
- package/src/journal/JournalRepository.ts +119 -0
- package/src/journal/JournalService.ts +291 -0
- package/src/journal/index.ts +10 -0
- package/src/persistence/DocumentRepository.ts +76 -0
- package/src/persistence/PrismaClient.ts +24 -0
- package/src/persistence/SessionRepository.ts +114 -0
- package/src/persistence/index.ts +7 -0
- package/src/serve.ts +240 -0
- package/src/transport/RTCServer.ts +327 -0
- package/src/transport/index.ts +1 -0
- package/src/version.ts +1 -0
- package/tests/README.md +112 -0
- package/tests/demo.ts +555 -0
- package/tests/e2e/convergence.test.ts +221 -0
- package/tests/e2e/helpers/assertions.ts +158 -0
- package/tests/e2e/helpers/createTestServer.ts +220 -0
- package/tests/e2e/latency.test.ts +512 -0
- package/tests/e2e/packet-loss.test.ts +229 -0
- package/tests/e2e/relay.test.ts +255 -0
- package/tests/e2e/sync-perf.test.ts +365 -0
- package/tests/e2e/sync-reconciliation.test.ts +237 -0
- package/tests/e2e/text-sync.test.ts +199 -0
- package/tests/e2e/tombstone-convergence.test.ts +356 -0
- package/tests/fixtures/array-ops.jsonl +6 -0
- package/tests/fixtures/boolean-ops.jsonl +6 -0
- package/tests/fixtures/color-ops.jsonl +4 -0
- package/tests/fixtures/edit-buffer.jsonl +3 -0
- package/tests/fixtures/messages.jsonl +4 -0
- package/tests/fixtures/node-ops.jsonl +6 -0
- package/tests/fixtures/number-ops.jsonl +7 -0
- package/tests/fixtures/object-ops.jsonl +4 -0
- package/tests/fixtures/operations.jsonl +7 -0
- package/tests/fixtures/string-ops.jsonl +4 -0
- package/tests/fixtures/undo-redo.jsonl +3 -0
- package/tests/fixtures/vector-ops.jsonl +9 -0
- package/tests/integration/repositories.test.ts +320 -0
- package/tests/journal/journal-service.test.ts +185 -0
- package/tests/test-data/datatypes.ts +677 -0
- package/tests/test-data/operations-example.ts +306 -0
- package/tests/test-data/scene-example.ts +247 -0
- package/tests/unit/operations.test.ts +310 -0
- package/tests/unit/vectorClock.test.ts +281 -0
- package/tsconfig.json +19 -0
- package/tsconfig.test.json +8 -0
package/tests/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Test Examples and Demo
|
|
2
|
+
|
|
3
|
+
## š¬ Working Demo
|
|
4
|
+
|
|
5
|
+
Run the interactive demo:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx tsx tests/demo.ts
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This demonstrates:
|
|
12
|
+
- Creating a scene with INSERT operations
|
|
13
|
+
- Updating properties with concurrent updates
|
|
14
|
+
- Conflict resolution (LWW and additive)
|
|
15
|
+
- Moving nodes (reparenting)
|
|
16
|
+
- Removing nodes (soft delete)
|
|
17
|
+
|
|
18
|
+
## š Test Data Examples
|
|
19
|
+
|
|
20
|
+
### `test-data/scene-example.ts`
|
|
21
|
+
Example scene graphs showing:
|
|
22
|
+
- Flattened map format: `{ nodes: Record<key, Node>, rootKey }`
|
|
23
|
+
- Dual identifiers (id + key)
|
|
24
|
+
- Children-only references (instancing support)
|
|
25
|
+
|
|
26
|
+
```typescript
|
|
27
|
+
import { exampleScene, exampleSceneWithInstancing } from './test-data/scene-example.js';
|
|
28
|
+
|
|
29
|
+
// Forest scene with trees and player
|
|
30
|
+
console.log(exampleScene);
|
|
31
|
+
|
|
32
|
+
// Scene demonstrating instancing (same node, multiple parents)
|
|
33
|
+
console.log(exampleSceneWithInstancing);
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### `test-data/operations-example.ts`
|
|
37
|
+
Example operations showing:
|
|
38
|
+
- INSERT, UPDATE, REMOVE, SET operations
|
|
39
|
+
- Concurrent update scenarios
|
|
40
|
+
- Move operations (reparenting)
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import {
|
|
44
|
+
exampleInsertOperation,
|
|
45
|
+
exampleUpdatePosition,
|
|
46
|
+
exampleConcurrentUpdates,
|
|
47
|
+
} from './test-data/operations-example.js';
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### `test-data/datatypes.ts`
|
|
51
|
+
Complete dtype definitions with:
|
|
52
|
+
- Merge functions for each operation
|
|
53
|
+
- Examples for each data type
|
|
54
|
+
- All supported operations
|
|
55
|
+
|
|
56
|
+
```typescript
|
|
57
|
+
import { NumberType, Vector3Type, ArrayType } from './test-data/datatypes.js';
|
|
58
|
+
|
|
59
|
+
// Example: Number with ADD operation
|
|
60
|
+
const summed = NumberType.add.merge([
|
|
61
|
+
{ value: 10, lamportTime: 100 },
|
|
62
|
+
{ value: 5, lamportTime: 101 },
|
|
63
|
+
]);
|
|
64
|
+
// Result: 15
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## š§Ŗ Running Tests
|
|
68
|
+
|
|
69
|
+
Vector clock tests (from Phase 1):
|
|
70
|
+
```bash
|
|
71
|
+
pnpm test
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## š Documentation
|
|
75
|
+
|
|
76
|
+
See `/docs` for detailed documentation:
|
|
77
|
+
- `SCENE_GRAPH.md` - Scene graph data structure
|
|
78
|
+
- `TYPE_BEHAVIORS.md` - CRDT type behaviors reference
|
|
79
|
+
|
|
80
|
+
## šÆ Quick Start
|
|
81
|
+
|
|
82
|
+
```typescript
|
|
83
|
+
import { StateManager } from '../src/state/StateManager.js';
|
|
84
|
+
|
|
85
|
+
// Create state manager
|
|
86
|
+
const manager = new StateManager();
|
|
87
|
+
|
|
88
|
+
// Apply an INSERT operation
|
|
89
|
+
manager.applyOperation({
|
|
90
|
+
otype: 'insert',
|
|
91
|
+
id: 'op-001',
|
|
92
|
+
target: 'uuid-node-001',
|
|
93
|
+
sessionId: 'session-1',
|
|
94
|
+
clock: { 'session-1': 1 },
|
|
95
|
+
lamportTime: 1,
|
|
96
|
+
timestamp: Date.now(),
|
|
97
|
+
data: {
|
|
98
|
+
key: 'my-cube',
|
|
99
|
+
tag: 'Mesh',
|
|
100
|
+
name: 'My Cube',
|
|
101
|
+
properties: {
|
|
102
|
+
color: '#ff0000',
|
|
103
|
+
'transform.position': [0, 0, 0],
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Get current state
|
|
109
|
+
const state = manager.getState();
|
|
110
|
+
const node = state.getNode('my-cube');
|
|
111
|
+
console.log(node?.name); // "My Cube"
|
|
112
|
+
```
|
package/tests/demo.ts
ADDED
|
@@ -0,0 +1,555 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Demo - CRDT Scene Graph with Operation Batching
|
|
3
|
+
*
|
|
4
|
+
* This demonstrates the new design:
|
|
5
|
+
* 1. CRDTMessage wrapper (envelope) with metadata
|
|
6
|
+
* 2. Batch operations with explicit dtypes (number.set, vector3.add, etc.)
|
|
7
|
+
* 3. Operations use `key` (node key) instead of UUID
|
|
8
|
+
* 4. True batching - multiple ops on multiple nodes in one message
|
|
9
|
+
*
|
|
10
|
+
* Run with: npx tsx tests/demo.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
// Type definitions for the new design
|
|
14
|
+
type VectorClock = Record<string, number>;
|
|
15
|
+
|
|
16
|
+
interface CRDTMessage {
|
|
17
|
+
id: string;
|
|
18
|
+
sessionId: string;
|
|
19
|
+
clock: VectorClock;
|
|
20
|
+
lamportTime: number;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
ops: Operation[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface BaseOp {
|
|
26
|
+
key: string;
|
|
27
|
+
otype: string;
|
|
28
|
+
path: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface NodeInsertOp extends BaseOp {
|
|
32
|
+
key: string;
|
|
33
|
+
otype: 'node.insert';
|
|
34
|
+
path: string;
|
|
35
|
+
value: {
|
|
36
|
+
key: string;
|
|
37
|
+
tag: string;
|
|
38
|
+
name: string;
|
|
39
|
+
[key: string]: any;
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface NodeRemoveOp extends BaseOp {
|
|
44
|
+
key: string;
|
|
45
|
+
otype: 'node.remove';
|
|
46
|
+
path: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
interface NumberSetOp extends BaseOp {
|
|
50
|
+
key: string;
|
|
51
|
+
otype: 'number.set';
|
|
52
|
+
path: string;
|
|
53
|
+
value: number;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
interface NumberAddOp extends BaseOp {
|
|
57
|
+
key: string;
|
|
58
|
+
otype: 'number.add';
|
|
59
|
+
path: string;
|
|
60
|
+
value: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
interface Vector3SetOp extends BaseOp {
|
|
64
|
+
key: string;
|
|
65
|
+
otype: 'vector3.set';
|
|
66
|
+
path: string;
|
|
67
|
+
value: [number, number, number];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
interface Vector3AddOp extends BaseOp {
|
|
71
|
+
key: string;
|
|
72
|
+
otype: 'vector3.add';
|
|
73
|
+
path: string;
|
|
74
|
+
value: [number, number, number];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
interface ColorSetOp extends BaseOp {
|
|
78
|
+
key: string;
|
|
79
|
+
otype: 'color.set';
|
|
80
|
+
path: string;
|
|
81
|
+
value: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
interface ArraySetOp extends BaseOp {
|
|
85
|
+
key: string;
|
|
86
|
+
otype: 'array.set';
|
|
87
|
+
path: string;
|
|
88
|
+
value: any[];
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
interface ArrayPushOp extends BaseOp {
|
|
92
|
+
key: string;
|
|
93
|
+
otype: 'array.push';
|
|
94
|
+
path: string;
|
|
95
|
+
value: any;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface ArrayRemoveOp extends BaseOp {
|
|
99
|
+
key: string;
|
|
100
|
+
otype: 'array.remove';
|
|
101
|
+
path: string;
|
|
102
|
+
value: any;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
type Operation =
|
|
106
|
+
| NodeInsertOp
|
|
107
|
+
| NodeRemoveOp
|
|
108
|
+
| NumberSetOp
|
|
109
|
+
| NumberAddOp
|
|
110
|
+
| Vector3SetOp
|
|
111
|
+
| Vector3AddOp
|
|
112
|
+
| ColorSetOp
|
|
113
|
+
| ArraySetOp
|
|
114
|
+
| ArrayPushOp
|
|
115
|
+
| ArrayRemoveOp;
|
|
116
|
+
|
|
117
|
+
console.log('š¬ CRDT Scene Graph Demo - Operation Batching\n');
|
|
118
|
+
console.log('='.repeat(60));
|
|
119
|
+
|
|
120
|
+
// ========================================
|
|
121
|
+
// 1. CREATE SCENE
|
|
122
|
+
// ========================================
|
|
123
|
+
console.log('\nš¦ Step 1: Create Scene\n');
|
|
124
|
+
|
|
125
|
+
const msg1: CRDTMessage = {
|
|
126
|
+
// === CRDT Wrapper (envelope) ===
|
|
127
|
+
id: 'msg-001',
|
|
128
|
+
sessionId: 'session-server',
|
|
129
|
+
clock: { 'session-server': 1 },
|
|
130
|
+
lamportTime: 1,
|
|
131
|
+
timestamp: Date.now() / 1000,
|
|
132
|
+
|
|
133
|
+
// === Operations (batch) ===
|
|
134
|
+
ops: [
|
|
135
|
+
{
|
|
136
|
+
key: 'scene',
|
|
137
|
+
otype: 'node.insert',
|
|
138
|
+
path: 'scene',
|
|
139
|
+
value: {
|
|
140
|
+
key: 'uuid-scene-001',
|
|
141
|
+
tag: 'Scene',
|
|
142
|
+
name: 'Main Scene',
|
|
143
|
+
background: '#87CEEB',
|
|
144
|
+
'transform.position': [0, 0, 0],
|
|
145
|
+
'transform.rotation': [0, 0, 0, 1],
|
|
146
|
+
'transform.scale': [1, 1, 1],
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
console.log('Message 1:');
|
|
153
|
+
console.log(' Envelope: id=%s, session=%s, lamport=%d', msg1.id, msg1.sessionId, msg1.lamportTime);
|
|
154
|
+
console.log(' Operations: %d ops', msg1.ops.length);
|
|
155
|
+
const op1 = msg1.ops[0];
|
|
156
|
+
if (op1.otype === 'node.insert') {
|
|
157
|
+
console.log(' ā %s: %s (tag=%s)', op1.otype, op1.key, op1.value.tag);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// ========================================
|
|
161
|
+
// 2. BATCH INSERT - Multiple nodes in one message
|
|
162
|
+
// ========================================
|
|
163
|
+
console.log('\nš¦ Step 2: Batch Insert - Multiple Nodes\n');
|
|
164
|
+
|
|
165
|
+
const msg2: CRDTMessage = {
|
|
166
|
+
id: 'msg-002',
|
|
167
|
+
sessionId: 'session-alice',
|
|
168
|
+
clock: { 'session-alice': 1 },
|
|
169
|
+
lamportTime: 2,
|
|
170
|
+
timestamp: Date.now() / 1000,
|
|
171
|
+
ops: [
|
|
172
|
+
// Insert cube
|
|
173
|
+
{
|
|
174
|
+
key: 'cube-1',
|
|
175
|
+
otype: 'node.insert',
|
|
176
|
+
path: 'cube-1',
|
|
177
|
+
value: {
|
|
178
|
+
key: 'uuid-cube-001',
|
|
179
|
+
tag: 'Mesh',
|
|
180
|
+
name: 'Red Cube',
|
|
181
|
+
color: '#ff0000',
|
|
182
|
+
'transform.position': [2, 1, 0],
|
|
183
|
+
'transform.rotation': [0, 0, 0, 1],
|
|
184
|
+
'transform.scale': [1, 1, 1],
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
// Insert sphere
|
|
188
|
+
{
|
|
189
|
+
key: 'sphere-1',
|
|
190
|
+
otype: 'node.insert',
|
|
191
|
+
path: 'sphere-1',
|
|
192
|
+
value: {
|
|
193
|
+
key: 'uuid-sphere-001',
|
|
194
|
+
tag: 'Mesh',
|
|
195
|
+
name: 'Blue Sphere',
|
|
196
|
+
color: '#0000ff',
|
|
197
|
+
'transform.position': [-2, 1, 0],
|
|
198
|
+
'transform.rotation': [0, 0, 0, 1],
|
|
199
|
+
'transform.scale': [1, 1, 1],
|
|
200
|
+
},
|
|
201
|
+
},
|
|
202
|
+
// Add both to scene's children
|
|
203
|
+
{
|
|
204
|
+
key: 'scene',
|
|
205
|
+
otype: 'array.set',
|
|
206
|
+
path: 'children',
|
|
207
|
+
value: ['cube-1', 'sphere-1'],
|
|
208
|
+
},
|
|
209
|
+
],
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
console.log('Message 2 (BATCH):');
|
|
213
|
+
console.log(' Envelope: id=%s, session=%s, lamport=%d', msg2.id, msg2.sessionId, msg2.lamportTime);
|
|
214
|
+
console.log(' Operations: %d ops (BATCHED!)', msg2.ops.length);
|
|
215
|
+
msg2.ops.forEach((op) => {
|
|
216
|
+
if (op.otype === 'node.insert') {
|
|
217
|
+
console.log(' ā %s: %s (tag=%s)', op.otype, op.key, op.value.tag);
|
|
218
|
+
} else {
|
|
219
|
+
console.log(' ā %s: %s.%s', op.otype, op.key, op.path);
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// ========================================
|
|
224
|
+
// 3. ADDITIVE TRANSFORM (Relative Movement)
|
|
225
|
+
// ========================================
|
|
226
|
+
console.log('\nšÆ Step 3: Additive Transform (Drag)\n');
|
|
227
|
+
|
|
228
|
+
const msg3: CRDTMessage = {
|
|
229
|
+
id: 'msg-003',
|
|
230
|
+
sessionId: 'session-alice',
|
|
231
|
+
clock: { 'session-alice': 2 },
|
|
232
|
+
lamportTime: 3,
|
|
233
|
+
timestamp: Date.now() / 1000,
|
|
234
|
+
ops: [
|
|
235
|
+
{
|
|
236
|
+
key: 'cube-1',
|
|
237
|
+
otype: 'vector3.add', // Additive!
|
|
238
|
+
path: 'transform.position',
|
|
239
|
+
value: [5, 0, 0], // Drag by +5 on X
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
};
|
|
243
|
+
|
|
244
|
+
console.log('Message 3 (Additive):');
|
|
245
|
+
const op3 = msg3.ops[0] as Vector3AddOp;
|
|
246
|
+
console.log(' ā %s: %s.%s += %s', op3.otype, op3.key, op3.path, JSON.stringify(op3.value));
|
|
247
|
+
console.log(' ā Relative movement (position.x += 5)');
|
|
248
|
+
|
|
249
|
+
// ========================================
|
|
250
|
+
// 4. ABSOLUTE TRANSFORM (Set Position)
|
|
251
|
+
// ========================================
|
|
252
|
+
console.log('\nšÆ Step 4: Absolute Transform (Set)\n');
|
|
253
|
+
|
|
254
|
+
const msg4: CRDTMessage = {
|
|
255
|
+
id: 'msg-004',
|
|
256
|
+
sessionId: 'session-bob',
|
|
257
|
+
clock: { 'session-bob': 1 },
|
|
258
|
+
lamportTime: 4,
|
|
259
|
+
timestamp: Date.now() / 1000,
|
|
260
|
+
ops: [
|
|
261
|
+
{
|
|
262
|
+
key: 'sphere-1',
|
|
263
|
+
otype: 'vector3.set', // Absolute!
|
|
264
|
+
path: 'transform.position',
|
|
265
|
+
value: [0, 5, 0], // Set to the exact position
|
|
266
|
+
},
|
|
267
|
+
],
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
console.log('Message 4 (Absolute):');
|
|
271
|
+
const op4 = msg4.ops[0] as Vector3SetOp;
|
|
272
|
+
console.log(' ā %s: %s.%s = %s', op4.otype, op4.key, op4.path, JSON.stringify(op4.value));
|
|
273
|
+
console.log(' ā Absolute position (position = [0, 5, 0])');
|
|
274
|
+
|
|
275
|
+
// ========================================
|
|
276
|
+
// 5. BATCH UPDATE - Multiple properties on same node
|
|
277
|
+
// ========================================
|
|
278
|
+
console.log('\nš Step 5: Batch Update - Same Node\n');
|
|
279
|
+
|
|
280
|
+
const msg5: CRDTMessage = {
|
|
281
|
+
id: 'msg-005',
|
|
282
|
+
sessionId: 'session-alice',
|
|
283
|
+
clock: { 'session-alice': 3 },
|
|
284
|
+
lamportTime: 5,
|
|
285
|
+
timestamp: Date.now() / 1000,
|
|
286
|
+
ops: [
|
|
287
|
+
{
|
|
288
|
+
key: 'cube-1',
|
|
289
|
+
otype: 'color.set',
|
|
290
|
+
path: 'color',
|
|
291
|
+
value: '#00ff00',
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
key: 'cube-1',
|
|
295
|
+
otype: 'number.set',
|
|
296
|
+
path: 'opacity',
|
|
297
|
+
value: 0.5,
|
|
298
|
+
},
|
|
299
|
+
{
|
|
300
|
+
key: 'cube-1',
|
|
301
|
+
otype: 'vector3.add',
|
|
302
|
+
path: 'transform.position',
|
|
303
|
+
value: [0, 2, 0], // Move up by 2
|
|
304
|
+
},
|
|
305
|
+
],
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
console.log('Message 5 (Batch - Same Node):');
|
|
309
|
+
console.log(' Envelope: lamport=%d', msg5.lamportTime);
|
|
310
|
+
console.log(' Operations on "%s": %d ops', msg5.ops[0].key, msg5.ops.length);
|
|
311
|
+
msg5.ops.forEach((op) => {
|
|
312
|
+
if ('value' in op) {
|
|
313
|
+
console.log(' ā %s: %s = %s', op.otype, op.path, JSON.stringify(op.value));
|
|
314
|
+
}
|
|
315
|
+
});
|
|
316
|
+
|
|
317
|
+
// ========================================
|
|
318
|
+
// 6. COMPOUND UPDATE - Multiple nodes
|
|
319
|
+
// ========================================
|
|
320
|
+
console.log('\nš Step 6: Compound Update - Multiple Nodes\n');
|
|
321
|
+
|
|
322
|
+
const msg6: CRDTMessage = {
|
|
323
|
+
id: 'msg-006',
|
|
324
|
+
sessionId: 'session-bob',
|
|
325
|
+
clock: { 'session-bob': 2 },
|
|
326
|
+
lamportTime: 6,
|
|
327
|
+
timestamp: Date.now() / 1000,
|
|
328
|
+
ops: [
|
|
329
|
+
// Update cube
|
|
330
|
+
{
|
|
331
|
+
key: 'cube-1',
|
|
332
|
+
otype: 'number.set',
|
|
333
|
+
path: 'metalness',
|
|
334
|
+
value: 0.8,
|
|
335
|
+
},
|
|
336
|
+
// Update sphere
|
|
337
|
+
{
|
|
338
|
+
key: 'sphere-1',
|
|
339
|
+
otype: 'color.set',
|
|
340
|
+
path: 'color',
|
|
341
|
+
value: '#ffff00',
|
|
342
|
+
},
|
|
343
|
+
],
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
console.log('Message 6 (Compound - Different Nodes):');
|
|
347
|
+
console.log(' Envelope: lamport=%d', msg6.lamportTime);
|
|
348
|
+
console.log(' Operations: %d nodes updated', new Set(msg6.ops.map((op) => op.key)).size);
|
|
349
|
+
msg6.ops.forEach((op) => {
|
|
350
|
+
if ('value' in op) {
|
|
351
|
+
console.log(' ā %s: %s.%s = %s', op.otype, op.key, op.path, JSON.stringify(op.value));
|
|
352
|
+
}
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
// ========================================
|
|
356
|
+
// 7. MULTI-SELECT DRAG - Additive on multiple nodes
|
|
357
|
+
// ========================================
|
|
358
|
+
console.log('\nšÆ Step 7: Multi-Select Drag\n');
|
|
359
|
+
|
|
360
|
+
const msg7: CRDTMessage = {
|
|
361
|
+
id: 'msg-007',
|
|
362
|
+
sessionId: 'session-alice',
|
|
363
|
+
clock: { 'session-alice': 4 },
|
|
364
|
+
lamportTime: 7,
|
|
365
|
+
timestamp: Date.now() / 1000,
|
|
366
|
+
ops: [
|
|
367
|
+
{
|
|
368
|
+
key: 'cube-1',
|
|
369
|
+
otype: 'vector3.add',
|
|
370
|
+
path: 'transform.position',
|
|
371
|
+
value: [3, 0, 0],
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
key: 'sphere-1',
|
|
375
|
+
otype: 'vector3.add',
|
|
376
|
+
path: 'transform.position',
|
|
377
|
+
value: [3, 0, 0],
|
|
378
|
+
},
|
|
379
|
+
],
|
|
380
|
+
};
|
|
381
|
+
|
|
382
|
+
console.log('Message 7 (Multi-Select Drag):');
|
|
383
|
+
console.log(' Selected nodes: %d', msg7.ops.length);
|
|
384
|
+
msg7.ops.forEach((op) => {
|
|
385
|
+
if ('value' in op) {
|
|
386
|
+
console.log(' ā %s += %s', op.key, JSON.stringify(op.value));
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
console.log(' ā All selected nodes dragged by [3, 0, 0]');
|
|
390
|
+
|
|
391
|
+
// ========================================
|
|
392
|
+
// 8. ADDITIVE SCORE - Concurrent updates
|
|
393
|
+
// ========================================
|
|
394
|
+
console.log('\nā Step 8: Additive Score (Concurrent)\n');
|
|
395
|
+
|
|
396
|
+
const msg8a: CRDTMessage = {
|
|
397
|
+
id: 'msg-008a',
|
|
398
|
+
sessionId: 'session-alice',
|
|
399
|
+
clock: { 'session-alice': 5 },
|
|
400
|
+
lamportTime: 8,
|
|
401
|
+
timestamp: Date.now() / 1000,
|
|
402
|
+
ops: [
|
|
403
|
+
{
|
|
404
|
+
key: 'cube-1',
|
|
405
|
+
otype: 'number.add',
|
|
406
|
+
path: 'score',
|
|
407
|
+
value: 10,
|
|
408
|
+
},
|
|
409
|
+
],
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
const msg8b: CRDTMessage = {
|
|
413
|
+
id: 'msg-008b',
|
|
414
|
+
sessionId: 'session-bob',
|
|
415
|
+
clock: { 'session-bob': 3 },
|
|
416
|
+
lamportTime: 9,
|
|
417
|
+
timestamp: Date.now() / 1000,
|
|
418
|
+
ops: [
|
|
419
|
+
{
|
|
420
|
+
key: 'cube-1',
|
|
421
|
+
otype: 'number.add',
|
|
422
|
+
path: 'score',
|
|
423
|
+
value: 5,
|
|
424
|
+
},
|
|
425
|
+
],
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
console.log('Message 8a (Alice):');
|
|
429
|
+
console.log(' ā %s: cube-1.score += 10', msg8a.ops[0].otype);
|
|
430
|
+
console.log('Message 8b (Bob - concurrent):');
|
|
431
|
+
console.log(' ā %s: cube-1.score += 5', msg8b.ops[0].otype);
|
|
432
|
+
console.log(' ā Final score: 15 (both additions applied!)');
|
|
433
|
+
|
|
434
|
+
// ========================================
|
|
435
|
+
// 9. REPARENT - Compound operation
|
|
436
|
+
// ========================================
|
|
437
|
+
console.log('\nš Step 9: Reparent Node\n');
|
|
438
|
+
|
|
439
|
+
const msg9: CRDTMessage = {
|
|
440
|
+
id: 'msg-009',
|
|
441
|
+
sessionId: 'session-alice',
|
|
442
|
+
clock: { 'session-alice': 6 },
|
|
443
|
+
lamportTime: 10,
|
|
444
|
+
timestamp: Date.now() / 1000,
|
|
445
|
+
ops: [
|
|
446
|
+
// Create new parent
|
|
447
|
+
{
|
|
448
|
+
key: 'group-1',
|
|
449
|
+
otype: 'node.insert',
|
|
450
|
+
path: 'group-1',
|
|
451
|
+
value: {
|
|
452
|
+
key: 'uuid-group-001',
|
|
453
|
+
tag: 'Group',
|
|
454
|
+
name: 'My Group',
|
|
455
|
+
'transform.position': [0, 0, 0],
|
|
456
|
+
'transform.rotation': [0, 0, 0, 1],
|
|
457
|
+
'transform.scale': [1, 1, 1],
|
|
458
|
+
},
|
|
459
|
+
},
|
|
460
|
+
// Remove from old parent
|
|
461
|
+
{
|
|
462
|
+
key: 'scene',
|
|
463
|
+
otype: 'array.remove',
|
|
464
|
+
path: 'children',
|
|
465
|
+
value: 'cube-1',
|
|
466
|
+
},
|
|
467
|
+
// Add to new parent
|
|
468
|
+
{
|
|
469
|
+
key: 'group-1',
|
|
470
|
+
otype: 'array.push',
|
|
471
|
+
path: 'children',
|
|
472
|
+
value: 'cube-1',
|
|
473
|
+
},
|
|
474
|
+
],
|
|
475
|
+
};
|
|
476
|
+
|
|
477
|
+
console.log('Message 9 (Reparent - Compound):');
|
|
478
|
+
console.log(' Operations: %d ops (atomic)', msg9.ops.length);
|
|
479
|
+
msg9.ops.forEach((op, i) => {
|
|
480
|
+
if (op.otype === 'node.insert') {
|
|
481
|
+
console.log(' %d. Create group: %s', i + 1, op.key);
|
|
482
|
+
} else if (op.otype === 'array.remove') {
|
|
483
|
+
console.log(' %d. Remove from %s.children: "%s"', i + 1, op.key, op.value);
|
|
484
|
+
} else if (op.otype === 'array.push') {
|
|
485
|
+
console.log(' %d. Add to %s.children: "%s"', i + 1, op.key, op.value);
|
|
486
|
+
}
|
|
487
|
+
});
|
|
488
|
+
console.log(' ā cube-1 moved from scene to group-1');
|
|
489
|
+
|
|
490
|
+
// ========================================
|
|
491
|
+
// 10. DELETE - Remove node
|
|
492
|
+
// ========================================
|
|
493
|
+
console.log('\nšļø Step 10: Delete Node\n');
|
|
494
|
+
|
|
495
|
+
const msg10: CRDTMessage = {
|
|
496
|
+
id: 'msg-010',
|
|
497
|
+
sessionId: 'session-bob',
|
|
498
|
+
clock: { 'session-bob': 4 },
|
|
499
|
+
lamportTime: 11,
|
|
500
|
+
timestamp: Date.now() / 1000,
|
|
501
|
+
ops: [
|
|
502
|
+
// Remove from parent
|
|
503
|
+
{
|
|
504
|
+
key: 'scene',
|
|
505
|
+
otype: 'array.remove',
|
|
506
|
+
path: 'children',
|
|
507
|
+
value: 'sphere-1',
|
|
508
|
+
},
|
|
509
|
+
// Delete node (tombstone)
|
|
510
|
+
{
|
|
511
|
+
key: 'sphere-1',
|
|
512
|
+
otype: 'node.remove',
|
|
513
|
+
path: 'sphere-1',
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
};
|
|
517
|
+
|
|
518
|
+
console.log('Message 10 (Delete):');
|
|
519
|
+
msg10.ops.forEach((op, i) => {
|
|
520
|
+
if (op.otype === 'array.remove') {
|
|
521
|
+
console.log(' %d. Remove from parent: %s', i + 1, op.value);
|
|
522
|
+
} else if (op.otype === 'node.remove') {
|
|
523
|
+
console.log(' %d. Delete node: %s (tombstone)', i + 1, op.key);
|
|
524
|
+
}
|
|
525
|
+
});
|
|
526
|
+
|
|
527
|
+
// ========================================
|
|
528
|
+
// SUMMARY
|
|
529
|
+
// ========================================
|
|
530
|
+
console.log('\nš Summary\n');
|
|
531
|
+
console.log('='.repeat(60));
|
|
532
|
+
console.log('\nTotal messages: 10');
|
|
533
|
+
console.log('Total operations: %d', [msg1, msg2, msg3, msg4, msg5, msg6, msg7, msg8a, msg8b, msg9, msg10].reduce((sum, msg) => sum + msg.ops.length, 0));
|
|
534
|
+
|
|
535
|
+
console.log('\n⨠Features Demonstrated:\n');
|
|
536
|
+
console.log(' ā
CRDT Wrapper (envelope) with metadata');
|
|
537
|
+
console.log(' ā
Operation batching (multiple ops per message)');
|
|
538
|
+
console.log(' ā
Compound updates (multiple nodes per message)');
|
|
539
|
+
console.log(' ā
Explicit dtypes: number.set, vector3.add, color.set, etc.');
|
|
540
|
+
console.log(' ā
Node keys (not UUIDs) for operations');
|
|
541
|
+
console.log(' ā
Additive transforms: vector3.add (position += delta)');
|
|
542
|
+
console.log(' ā
Absolute transforms: vector3.set (position = value)');
|
|
543
|
+
console.log(' ā
Multi-select drag (batch additive on multiple nodes)');
|
|
544
|
+
console.log(' ā
Additive counters: number.add (concurrent updates sum)');
|
|
545
|
+
console.log(' ā
Reparenting (compound: remove + insert + array ops)');
|
|
546
|
+
console.log(' ā
Node deletion (remove from parent + tombstone)');
|
|
547
|
+
|
|
548
|
+
console.log('\n' + '='.repeat(60));
|
|
549
|
+
console.log('\nš” Key Design Points:\n');
|
|
550
|
+
console.log(' ⢠CRDTMessage = envelope (metadata) + ops (batch)');
|
|
551
|
+
console.log(' ⢠Operations use `key` (human-friendly) not UUID');
|
|
552
|
+
console.log(' ⢠otype is explicit: "vector3.add" vs "vector3.set"');
|
|
553
|
+
console.log(' ⢠True batching: one message, many nodes, atomic');
|
|
554
|
+
console.log(' ⢠No nested "properties" - clean operation structure');
|
|
555
|
+
console.log('\n' + '='.repeat(60));
|