@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.
Files changed (114) hide show
  1. package/.env +1 -0
  2. package/PHASE1_SUMMARY.md +94 -0
  3. package/README.md +423 -0
  4. package/dist/broker/InMemoryBroker.d.ts +24 -0
  5. package/dist/broker/InMemoryBroker.d.ts.map +1 -0
  6. package/dist/broker/InMemoryBroker.js +65 -0
  7. package/dist/broker/InMemoryBroker.js.map +1 -0
  8. package/dist/broker/index.d.ts +3 -0
  9. package/dist/broker/index.d.ts.map +1 -0
  10. package/dist/broker/index.js +2 -0
  11. package/dist/broker/index.js.map +1 -0
  12. package/dist/broker/types.d.ts +47 -0
  13. package/dist/broker/types.d.ts.map +1 -0
  14. package/dist/broker/types.js +9 -0
  15. package/dist/broker/types.js.map +1 -0
  16. package/dist/index.d.ts +13 -0
  17. package/dist/index.d.ts.map +1 -0
  18. package/dist/index.js +18 -0
  19. package/dist/index.js.map +1 -0
  20. package/dist/journal/JournalRepository.d.ts +39 -0
  21. package/dist/journal/JournalRepository.d.ts.map +1 -0
  22. package/dist/journal/JournalRepository.js +102 -0
  23. package/dist/journal/JournalRepository.js.map +1 -0
  24. package/dist/journal/JournalService.d.ts +69 -0
  25. package/dist/journal/JournalService.d.ts.map +1 -0
  26. package/dist/journal/JournalService.js +224 -0
  27. package/dist/journal/JournalService.js.map +1 -0
  28. package/dist/journal/index.d.ts +6 -0
  29. package/dist/journal/index.d.ts.map +1 -0
  30. package/dist/journal/index.js +6 -0
  31. package/dist/journal/index.js.map +1 -0
  32. package/dist/persistence/DocumentRepository.d.ts +22 -0
  33. package/dist/persistence/DocumentRepository.d.ts.map +1 -0
  34. package/dist/persistence/DocumentRepository.js +66 -0
  35. package/dist/persistence/DocumentRepository.js.map +1 -0
  36. package/dist/persistence/PrismaClient.d.ts +8 -0
  37. package/dist/persistence/PrismaClient.d.ts.map +1 -0
  38. package/dist/persistence/PrismaClient.js +21 -0
  39. package/dist/persistence/PrismaClient.js.map +1 -0
  40. package/dist/persistence/SessionRepository.d.ts +22 -0
  41. package/dist/persistence/SessionRepository.d.ts.map +1 -0
  42. package/dist/persistence/SessionRepository.js +103 -0
  43. package/dist/persistence/SessionRepository.js.map +1 -0
  44. package/dist/persistence/index.d.ts +7 -0
  45. package/dist/persistence/index.d.ts.map +1 -0
  46. package/dist/persistence/index.js +7 -0
  47. package/dist/persistence/index.js.map +1 -0
  48. package/dist/serve.d.ts +18 -0
  49. package/dist/serve.d.ts.map +1 -0
  50. package/dist/serve.js +211 -0
  51. package/dist/serve.js.map +1 -0
  52. package/dist/transport/RTCServer.d.ts +92 -0
  53. package/dist/transport/RTCServer.d.ts.map +1 -0
  54. package/dist/transport/RTCServer.js +273 -0
  55. package/dist/transport/RTCServer.js.map +1 -0
  56. package/dist/transport/index.d.ts +2 -0
  57. package/dist/transport/index.d.ts.map +1 -0
  58. package/dist/transport/index.js +2 -0
  59. package/dist/transport/index.js.map +1 -0
  60. package/dist/version.d.ts +2 -0
  61. package/dist/version.d.ts.map +1 -0
  62. package/dist/version.js +2 -0
  63. package/dist/version.js.map +1 -0
  64. package/jest.config.js +36 -0
  65. package/package.json +56 -0
  66. package/prisma/schema.prisma +121 -0
  67. package/src/broker/InMemoryBroker.ts +81 -0
  68. package/src/broker/index.ts +2 -0
  69. package/src/broker/types.ts +60 -0
  70. package/src/index.ts +23 -0
  71. package/src/journal/JournalRepository.ts +119 -0
  72. package/src/journal/JournalService.ts +291 -0
  73. package/src/journal/index.ts +10 -0
  74. package/src/persistence/DocumentRepository.ts +76 -0
  75. package/src/persistence/PrismaClient.ts +24 -0
  76. package/src/persistence/SessionRepository.ts +114 -0
  77. package/src/persistence/index.ts +7 -0
  78. package/src/serve.ts +240 -0
  79. package/src/transport/RTCServer.ts +327 -0
  80. package/src/transport/index.ts +1 -0
  81. package/src/version.ts +1 -0
  82. package/tests/README.md +112 -0
  83. package/tests/demo.ts +555 -0
  84. package/tests/e2e/convergence.test.ts +221 -0
  85. package/tests/e2e/helpers/assertions.ts +158 -0
  86. package/tests/e2e/helpers/createTestServer.ts +220 -0
  87. package/tests/e2e/latency.test.ts +512 -0
  88. package/tests/e2e/packet-loss.test.ts +229 -0
  89. package/tests/e2e/relay.test.ts +255 -0
  90. package/tests/e2e/sync-perf.test.ts +365 -0
  91. package/tests/e2e/sync-reconciliation.test.ts +237 -0
  92. package/tests/e2e/text-sync.test.ts +199 -0
  93. package/tests/e2e/tombstone-convergence.test.ts +356 -0
  94. package/tests/fixtures/array-ops.jsonl +6 -0
  95. package/tests/fixtures/boolean-ops.jsonl +6 -0
  96. package/tests/fixtures/color-ops.jsonl +4 -0
  97. package/tests/fixtures/edit-buffer.jsonl +3 -0
  98. package/tests/fixtures/messages.jsonl +4 -0
  99. package/tests/fixtures/node-ops.jsonl +6 -0
  100. package/tests/fixtures/number-ops.jsonl +7 -0
  101. package/tests/fixtures/object-ops.jsonl +4 -0
  102. package/tests/fixtures/operations.jsonl +7 -0
  103. package/tests/fixtures/string-ops.jsonl +4 -0
  104. package/tests/fixtures/undo-redo.jsonl +3 -0
  105. package/tests/fixtures/vector-ops.jsonl +9 -0
  106. package/tests/integration/repositories.test.ts +320 -0
  107. package/tests/journal/journal-service.test.ts +185 -0
  108. package/tests/test-data/datatypes.ts +677 -0
  109. package/tests/test-data/operations-example.ts +306 -0
  110. package/tests/test-data/scene-example.ts +247 -0
  111. package/tests/unit/operations.test.ts +310 -0
  112. package/tests/unit/vectorClock.test.ts +281 -0
  113. package/tsconfig.json +19 -0
  114. package/tsconfig.test.json +8 -0
@@ -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));