@vuer-ai/vuer-rtc 0.0.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 (169) hide show
  1. package/dist/client/EditBuffer.d.ts +43 -0
  2. package/dist/client/EditBuffer.d.ts.map +1 -0
  3. package/dist/client/EditBuffer.js +96 -0
  4. package/dist/client/EditBuffer.js.map +1 -0
  5. package/dist/client/actions.d.ts +66 -0
  6. package/dist/client/actions.d.ts.map +1 -0
  7. package/dist/client/actions.js +345 -0
  8. package/dist/client/actions.js.map +1 -0
  9. package/dist/client/createGraph.d.ts +30 -0
  10. package/dist/client/createGraph.d.ts.map +1 -0
  11. package/dist/client/createGraph.js +91 -0
  12. package/dist/client/createGraph.js.map +1 -0
  13. package/dist/client/hooks.d.ts +81 -0
  14. package/dist/client/hooks.d.ts.map +1 -0
  15. package/dist/client/hooks.js +161 -0
  16. package/dist/client/hooks.js.map +1 -0
  17. package/dist/client/index.d.ts +8 -0
  18. package/dist/client/index.d.ts.map +1 -0
  19. package/dist/client/index.js +10 -0
  20. package/dist/client/index.js.map +1 -0
  21. package/dist/client/types.d.ts +74 -0
  22. package/dist/client/types.d.ts.map +1 -0
  23. package/dist/client/types.js +11 -0
  24. package/dist/client/types.js.map +1 -0
  25. package/dist/hooks.d.ts +8 -0
  26. package/dist/hooks.d.ts.map +1 -0
  27. package/dist/hooks.js +7 -0
  28. package/dist/hooks.js.map +1 -0
  29. package/dist/index.d.ts +9 -0
  30. package/dist/index.d.ts.map +1 -0
  31. package/dist/index.js +12 -0
  32. package/dist/index.js.map +1 -0
  33. package/dist/operations/OperationTypes.d.ts +239 -0
  34. package/dist/operations/OperationTypes.d.ts.map +1 -0
  35. package/dist/operations/OperationTypes.js +10 -0
  36. package/dist/operations/OperationTypes.js.map +1 -0
  37. package/dist/operations/OperationValidator.d.ts +32 -0
  38. package/dist/operations/OperationValidator.d.ts.map +1 -0
  39. package/dist/operations/OperationValidator.js +208 -0
  40. package/dist/operations/OperationValidator.js.map +1 -0
  41. package/dist/operations/apply/array.d.ts +22 -0
  42. package/dist/operations/apply/array.d.ts.map +1 -0
  43. package/dist/operations/apply/array.js +64 -0
  44. package/dist/operations/apply/array.js.map +1 -0
  45. package/dist/operations/apply/boolean.d.ts +18 -0
  46. package/dist/operations/apply/boolean.d.ts.map +1 -0
  47. package/dist/operations/apply/boolean.js +34 -0
  48. package/dist/operations/apply/boolean.js.map +1 -0
  49. package/dist/operations/apply/color.d.ts +14 -0
  50. package/dist/operations/apply/color.d.ts.map +1 -0
  51. package/dist/operations/apply/color.js +46 -0
  52. package/dist/operations/apply/color.js.map +1 -0
  53. package/dist/operations/apply/index.d.ts +18 -0
  54. package/dist/operations/apply/index.d.ts.map +1 -0
  55. package/dist/operations/apply/index.js +26 -0
  56. package/dist/operations/apply/index.js.map +1 -0
  57. package/dist/operations/apply/node.d.ts +24 -0
  58. package/dist/operations/apply/node.d.ts.map +1 -0
  59. package/dist/operations/apply/node.js +77 -0
  60. package/dist/operations/apply/node.js.map +1 -0
  61. package/dist/operations/apply/number.d.ts +26 -0
  62. package/dist/operations/apply/number.d.ts.map +1 -0
  63. package/dist/operations/apply/number.js +54 -0
  64. package/dist/operations/apply/number.js.map +1 -0
  65. package/dist/operations/apply/object.d.ts +14 -0
  66. package/dist/operations/apply/object.d.ts.map +1 -0
  67. package/dist/operations/apply/object.js +47 -0
  68. package/dist/operations/apply/object.js.map +1 -0
  69. package/dist/operations/apply/quaternion.d.ts +15 -0
  70. package/dist/operations/apply/quaternion.d.ts.map +1 -0
  71. package/dist/operations/apply/quaternion.js +33 -0
  72. package/dist/operations/apply/quaternion.js.map +1 -0
  73. package/dist/operations/apply/string.d.ts +14 -0
  74. package/dist/operations/apply/string.d.ts.map +1 -0
  75. package/dist/operations/apply/string.js +26 -0
  76. package/dist/operations/apply/string.js.map +1 -0
  77. package/dist/operations/apply/types.d.ts +34 -0
  78. package/dist/operations/apply/types.d.ts.map +1 -0
  79. package/dist/operations/apply/types.js +32 -0
  80. package/dist/operations/apply/types.js.map +1 -0
  81. package/dist/operations/apply/vector3.d.ts +18 -0
  82. package/dist/operations/apply/vector3.d.ts.map +1 -0
  83. package/dist/operations/apply/vector3.js +44 -0
  84. package/dist/operations/apply/vector3.js.map +1 -0
  85. package/dist/operations/dispatcher.d.ts +35 -0
  86. package/dist/operations/dispatcher.d.ts.map +1 -0
  87. package/dist/operations/dispatcher.js +107 -0
  88. package/dist/operations/dispatcher.js.map +1 -0
  89. package/dist/operations/index.d.ts +10 -0
  90. package/dist/operations/index.d.ts.map +1 -0
  91. package/dist/operations/index.js +17 -0
  92. package/dist/operations/index.js.map +1 -0
  93. package/dist/state/ConflictResolver.d.ts +36 -0
  94. package/dist/state/ConflictResolver.d.ts.map +1 -0
  95. package/dist/state/ConflictResolver.js +167 -0
  96. package/dist/state/ConflictResolver.js.map +1 -0
  97. package/dist/state/DType.d.ts +160 -0
  98. package/dist/state/DType.d.ts.map +1 -0
  99. package/dist/state/DType.js +282 -0
  100. package/dist/state/DType.js.map +1 -0
  101. package/dist/state/Schema.d.ts +32 -0
  102. package/dist/state/Schema.d.ts.map +1 -0
  103. package/dist/state/Schema.js +175 -0
  104. package/dist/state/Schema.js.map +1 -0
  105. package/dist/state/VectorClock.d.ts +42 -0
  106. package/dist/state/VectorClock.d.ts.map +1 -0
  107. package/dist/state/VectorClock.js +84 -0
  108. package/dist/state/VectorClock.js.map +1 -0
  109. package/dist/state/index.d.ts +11 -0
  110. package/dist/state/index.d.ts.map +1 -0
  111. package/dist/state/index.js +13 -0
  112. package/dist/state/index.js.map +1 -0
  113. package/docs/OPERATION_HINTS.md +222 -0
  114. package/docs/SCENE_GRAPH.md +373 -0
  115. package/docs/TYPE_BEHAVIORS.md +348 -0
  116. package/examples/01-basic-usage.ts +139 -0
  117. package/examples/02-concurrent-edits.ts +208 -0
  118. package/examples/03-scene-building.ts +258 -0
  119. package/examples/04-conflict-resolution.ts +339 -0
  120. package/examples/README.md +86 -0
  121. package/jest.config.js +19 -0
  122. package/package.json +57 -0
  123. package/src/client/EditBuffer.ts +105 -0
  124. package/src/client/actions.ts +397 -0
  125. package/src/client/createGraph.ts +132 -0
  126. package/src/client/hooks.tsx +249 -0
  127. package/src/client/index.ts +35 -0
  128. package/src/client/types.ts +94 -0
  129. package/src/hooks.ts +20 -0
  130. package/src/index.ts +14 -0
  131. package/src/operations/OperationTypes.ts +340 -0
  132. package/src/operations/OperationValidator.ts +260 -0
  133. package/src/operations/apply/array.ts +84 -0
  134. package/src/operations/apply/boolean.ts +48 -0
  135. package/src/operations/apply/color.ts +65 -0
  136. package/src/operations/apply/index.ts +37 -0
  137. package/src/operations/apply/node.ts +98 -0
  138. package/src/operations/apply/number.ts +76 -0
  139. package/src/operations/apply/object.ts +63 -0
  140. package/src/operations/apply/quaternion.ts +47 -0
  141. package/src/operations/apply/string.ts +36 -0
  142. package/src/operations/apply/types.ts +66 -0
  143. package/src/operations/apply/vector3.ts +60 -0
  144. package/src/operations/dispatcher.ts +127 -0
  145. package/src/operations/index.ts +80 -0
  146. package/src/state/ConflictResolver.ts +205 -0
  147. package/src/state/DType.ts +333 -0
  148. package/src/state/Schema.ts +236 -0
  149. package/src/state/VectorClock.ts +98 -0
  150. package/src/state/index.ts +14 -0
  151. package/tests/client/actions.test.ts +371 -0
  152. package/tests/client/edit-buffer.test.ts +117 -0
  153. package/tests/fixtures/array-ops.jsonl +6 -0
  154. package/tests/fixtures/boolean-ops.jsonl +6 -0
  155. package/tests/fixtures/color-ops.jsonl +4 -0
  156. package/tests/fixtures/edit-buffer.jsonl +3 -0
  157. package/tests/fixtures/node-ops.jsonl +6 -0
  158. package/tests/fixtures/number-ops.jsonl +7 -0
  159. package/tests/fixtures/object-ops.jsonl +4 -0
  160. package/tests/fixtures/operations.jsonl +7 -0
  161. package/tests/fixtures/string-ops.jsonl +4 -0
  162. package/tests/fixtures/undo-redo.jsonl +3 -0
  163. package/tests/fixtures/vector-ops.jsonl +9 -0
  164. package/tests/operations/collections.test.ts +193 -0
  165. package/tests/operations/nodes.test.ts +228 -0
  166. package/tests/operations/primitives.test.ts +222 -0
  167. package/tests/operations/vectors.test.ts +150 -0
  168. package/tsconfig.json +21 -0
  169. package/tsconfig.test.json +9 -0
@@ -0,0 +1,258 @@
1
+ /**
2
+ * Example 03: Scene Building
3
+ *
4
+ * Demonstrates building a complete scene with batched operations,
5
+ * reparenting, and node deletion.
6
+ * Run with: npx tsx examples/03-scene-building.ts
7
+ */
8
+
9
+ import { createEmptyGraph, applyMessage, applyMessages } from '../src/operations/index.js';
10
+ import type { CRDTMessage } from '../src/operations/index.js';
11
+
12
+ console.log('šŸŽ¬ Example 03: Scene Building\n');
13
+
14
+ // ============================================
15
+ // Step 1: Create initial scene structure
16
+ // ============================================
17
+
18
+ console.log('--- Step 1: Create Scene with Multiple Objects ---\n');
19
+
20
+ const initScene: CRDTMessage = {
21
+ id: 'init',
22
+ sessionId: 'server',
23
+ clock: { server: 1 },
24
+ lamportTime: 1,
25
+ timestamp: Date.now(),
26
+ ops: [
27
+ // Scene root (no parent - it's the root)
28
+ {
29
+ key: 'scene',
30
+ otype: 'node.insert',
31
+ path: 'scene',
32
+ value: {
33
+ id: 'uuid-scene',
34
+ tag: 'Scene',
35
+ name: 'Game Level',
36
+ background: '#1a1a2e',
37
+ },
38
+ },
39
+ // Player (parent: scene)
40
+ {
41
+ key: 'player',
42
+ otype: 'node.insert',
43
+ path: 'player',
44
+ parent: 'scene',
45
+ value: {
46
+ id: 'uuid-player',
47
+ tag: 'Mesh',
48
+ name: 'Player',
49
+ color: '#00ff00',
50
+ health: 100,
51
+ 'transform.position': [0, 1, 0],
52
+ 'transform.scale': [1, 2, 1],
53
+ },
54
+ },
55
+ // Enemy 1 (parent: scene)
56
+ {
57
+ key: 'enemy-1',
58
+ otype: 'node.insert',
59
+ path: 'enemy-1',
60
+ parent: 'scene',
61
+ value: {
62
+ id: 'uuid-enemy-1',
63
+ tag: 'Mesh',
64
+ name: 'Goblin',
65
+ color: '#ff0000',
66
+ health: 50,
67
+ 'transform.position': [10, 1, 5],
68
+ },
69
+ },
70
+ // Enemy 2 (parent: scene)
71
+ {
72
+ key: 'enemy-2',
73
+ otype: 'node.insert',
74
+ path: 'enemy-2',
75
+ parent: 'scene',
76
+ value: {
77
+ id: 'uuid-enemy-2',
78
+ tag: 'Mesh',
79
+ name: 'Orc',
80
+ color: '#880000',
81
+ health: 100,
82
+ 'transform.position': [-10, 1, 5],
83
+ },
84
+ },
85
+ ],
86
+ };
87
+
88
+ let graph = applyMessage(createEmptyGraph(), initScene);
89
+
90
+ console.log('Created nodes:', Object.keys(graph.nodes));
91
+ console.log('Scene children:', graph.nodes['scene'].children);
92
+
93
+ // ============================================
94
+ // Step 2: Create a group and reparent enemies
95
+ // ============================================
96
+
97
+ console.log('\n--- Step 2: Reparent Enemies into Group ---\n');
98
+
99
+ const reparent: CRDTMessage = {
100
+ id: 'reparent',
101
+ sessionId: 'alice',
102
+ clock: { alice: 1 },
103
+ lamportTime: 2,
104
+ timestamp: Date.now(),
105
+ ops: [
106
+ // Create enemies group (parent: scene)
107
+ {
108
+ key: 'enemies',
109
+ otype: 'node.insert',
110
+ path: 'enemies',
111
+ parent: 'scene',
112
+ value: {
113
+ id: 'uuid-enemies',
114
+ tag: 'Group',
115
+ name: 'Enemies',
116
+ 'transform.position': [0, 0, 0],
117
+ },
118
+ },
119
+ // Remove enemies from scene and add to group
120
+ {
121
+ key: 'scene',
122
+ otype: 'array.remove',
123
+ path: 'children',
124
+ value: 'enemy-1',
125
+ },
126
+ {
127
+ key: 'scene',
128
+ otype: 'array.remove',
129
+ path: 'children',
130
+ value: 'enemy-2',
131
+ },
132
+ {
133
+ key: 'enemies',
134
+ otype: 'array.set',
135
+ path: 'children',
136
+ value: ['enemy-1', 'enemy-2'],
137
+ },
138
+ ],
139
+ };
140
+
141
+ graph = applyMessage(graph, reparent);
142
+
143
+ console.log('Scene children:', graph.nodes['scene'].children);
144
+ console.log('Enemies group children:', graph.nodes['enemies'].children);
145
+
146
+ // ============================================
147
+ // Step 3: Batch update - damage all enemies
148
+ // ============================================
149
+
150
+ console.log('\n--- Step 3: Batch Update - Damage All Enemies ---\n');
151
+
152
+ const damageAll: CRDTMessage = {
153
+ id: 'damage',
154
+ sessionId: 'player',
155
+ clock: { player: 1 },
156
+ lamportTime: 3,
157
+ timestamp: Date.now(),
158
+ ops: [
159
+ {
160
+ key: 'enemy-1',
161
+ otype: 'number.add',
162
+ path: 'health',
163
+ value: -30, // Take 30 damage
164
+ },
165
+ {
166
+ key: 'enemy-2',
167
+ otype: 'number.add',
168
+ path: 'health',
169
+ value: -30, // Take 30 damage
170
+ },
171
+ ],
172
+ };
173
+
174
+ graph = applyMessage(graph, damageAll);
175
+
176
+ console.log('Enemy 1 health:', graph.nodes['enemy-1'].health);
177
+ console.log('Enemy 2 health:', graph.nodes['enemy-2'].health);
178
+
179
+ // ============================================
180
+ // Step 4: Delete dead enemy
181
+ // ============================================
182
+
183
+ console.log('\n--- Step 4: Kill Enemy 1 ---\n');
184
+
185
+ // More damage to enemy-1
186
+ const killEnemy1: CRDTMessage = {
187
+ id: 'kill',
188
+ sessionId: 'player',
189
+ clock: { player: 2 },
190
+ lamportTime: 4,
191
+ timestamp: Date.now(),
192
+ ops: [
193
+ {
194
+ key: 'enemy-1',
195
+ otype: 'number.add',
196
+ path: 'health',
197
+ value: -20, // Now at 0
198
+ },
199
+ ],
200
+ };
201
+
202
+ graph = applyMessage(graph, killEnemy1);
203
+ console.log('Enemy 1 health after damage:', graph.nodes['enemy-1'].health);
204
+
205
+ // Remove dead enemy
206
+ const removeEnemy1: CRDTMessage = {
207
+ id: 'remove-enemy',
208
+ sessionId: 'server',
209
+ clock: { server: 2 },
210
+ lamportTime: 5,
211
+ timestamp: Date.now(),
212
+ ops: [
213
+ {
214
+ key: 'enemies',
215
+ otype: 'array.remove',
216
+ path: 'children',
217
+ value: 'enemy-1',
218
+ },
219
+ {
220
+ key: 'enemy-1',
221
+ otype: 'node.remove',
222
+ path: 'enemy-1',
223
+ },
224
+ ],
225
+ };
226
+
227
+ graph = applyMessage(graph, removeEnemy1);
228
+
229
+ console.log('Enemy 1 deleted:', graph.nodes['enemy-1'].deletedAt !== undefined);
230
+ console.log('Enemies group children:', graph.nodes['enemies'].children);
231
+
232
+ // ============================================
233
+ // Final Scene Tree
234
+ // ============================================
235
+
236
+ console.log('\nšŸ“Š Final Scene Tree:\n');
237
+
238
+ function printTree(key: string, indent = 0) {
239
+ const node = graph.nodes[key];
240
+ if (!node || node.deletedAt) return;
241
+
242
+ const prefix = ' '.repeat(indent);
243
+ const health = node.health !== undefined ? ` (health: ${node.health})` : '';
244
+ console.log(`${prefix}ā”œā”€ ${node.name} [${node.tag}]${health}`);
245
+
246
+ for (const childKey of node.children || []) {
247
+ printTree(childKey, indent + 1);
248
+ }
249
+ }
250
+
251
+ console.log('scene');
252
+ printTree('scene');
253
+
254
+ console.log('\nšŸ“Š All nodes (including tombstones):');
255
+ for (const [key, node] of Object.entries(graph.nodes)) {
256
+ const status = node.deletedAt ? 'āŒ DELETED' : 'āœ… ACTIVE';
257
+ console.log(` ${key}: ${status}`);
258
+ }
@@ -0,0 +1,339 @@
1
+ /**
2
+ * Example 04: Conflict Resolution
3
+ *
4
+ * Demonstrates how CRDT resolves conflicts when messages arrive out-of-order.
5
+ * Run with: npx tsx examples/04-conflict-resolution.ts
6
+ */
7
+
8
+ import { createEmptyGraph, applyMessage } from '../src/operations/index.js';
9
+ import type { CRDTMessage } from '../src/operations/index.js';
10
+
11
+ console.log('šŸŽ¬ Example 04: Conflict Resolution\n');
12
+
13
+ // ============================================
14
+ // Setup: Create a cube
15
+ // ============================================
16
+
17
+ let graph = applyMessage(createEmptyGraph(), {
18
+ id: 'setup',
19
+ sessionId: 'server',
20
+ clock: { server: 1 },
21
+ lamportTime: 0,
22
+ timestamp: Date.now(),
23
+ ops: [{
24
+ key: 'cube',
25
+ otype: 'node.insert',
26
+ path: 'cube',
27
+ value: {
28
+ id: 'uuid-cube',
29
+ tag: 'Mesh',
30
+ name: 'Cube',
31
+ color: '#ffffff',
32
+ 'transform.position': [0, 0, 0],
33
+ },
34
+ }],
35
+ });
36
+
37
+ console.log('Initial state:');
38
+ console.log(' color:', graph.nodes['cube'].color);
39
+ console.log(' position:', graph.nodes['cube']['transform.position']);
40
+
41
+ // ============================================
42
+ // Scenario 1: LWW - Out-of-order arrival
43
+ // ============================================
44
+
45
+ console.log('\n--- Scenario 1: LWW with Out-of-Order Arrival ---\n');
46
+ console.log('Bob sets color to blue (lamport: 11)');
47
+ console.log('Alice sets color to red (lamport: 10) - arrives AFTER Bob\n');
48
+
49
+ // Bob's message arrives first (higher lamport)
50
+ graph = applyMessage(graph, {
51
+ id: 'bob-color',
52
+ sessionId: 'bob',
53
+ clock: { bob: 1 },
54
+ lamportTime: 11,
55
+ timestamp: Date.now(),
56
+ ops: [{ key: 'cube', otype: 'color.set', path: 'color', value: '#0000ff' }],
57
+ });
58
+ console.log('After Bob (lamport 11):', graph.nodes['cube'].color);
59
+
60
+ // Alice's message arrives later (lower lamport - should be ignored)
61
+ graph = applyMessage(graph, {
62
+ id: 'alice-color',
63
+ sessionId: 'alice',
64
+ clock: { alice: 1 },
65
+ lamportTime: 10,
66
+ timestamp: Date.now(),
67
+ ops: [{ key: 'cube', otype: 'color.set', path: 'color', value: '#ff0000' }],
68
+ });
69
+ console.log('After Alice (lamport 10):', graph.nodes['cube'].color);
70
+
71
+ console.log('\nāœ… Result: Bob wins! (lamport 11 > 10)');
72
+ console.log(' Alice\'s late message was correctly ignored.');
73
+
74
+ // ============================================
75
+ // Scenario 2: Additive - Order doesn't matter
76
+ // ============================================
77
+
78
+ console.log('\n--- Scenario 2: Additive Operations ---\n');
79
+ console.log('Messages can arrive in any order, result is the same.\n');
80
+
81
+ // Reset position
82
+ graph = applyMessage(graph, {
83
+ id: 'reset',
84
+ sessionId: 'server',
85
+ clock: { server: 2 },
86
+ lamportTime: 20,
87
+ timestamp: Date.now(),
88
+ ops: [{ key: 'cube', otype: 'vector3.set', path: 'transform.position', value: [0, 0, 0] }],
89
+ });
90
+
91
+ console.log('Position reset to [0, 0, 0]');
92
+
93
+ // Apply in order: Alice, Bob, Charlie
94
+ let graph1 = applyMessage(graph, {
95
+ id: 'alice-move', sessionId: 'alice', clock: { alice: 2 }, lamportTime: 21, timestamp: Date.now(),
96
+ ops: [{ key: 'cube', otype: 'vector3.add', path: 'transform.position', value: [1, 0, 0] }],
97
+ });
98
+ graph1 = applyMessage(graph1, {
99
+ id: 'bob-move', sessionId: 'bob', clock: { bob: 2 }, lamportTime: 22, timestamp: Date.now(),
100
+ ops: [{ key: 'cube', otype: 'vector3.add', path: 'transform.position', value: [0, 2, 0] }],
101
+ });
102
+ graph1 = applyMessage(graph1, {
103
+ id: 'charlie-move', sessionId: 'charlie', clock: { charlie: 1 }, lamportTime: 23, timestamp: Date.now(),
104
+ ops: [{ key: 'cube', otype: 'vector3.add', path: 'transform.position', value: [0, 0, 3] }],
105
+ });
106
+
107
+ // Apply in reverse order: Charlie, Bob, Alice
108
+ let graph2 = applyMessage(graph, {
109
+ id: 'charlie-move', sessionId: 'charlie', clock: { charlie: 1 }, lamportTime: 23, timestamp: Date.now(),
110
+ ops: [{ key: 'cube', otype: 'vector3.add', path: 'transform.position', value: [0, 0, 3] }],
111
+ });
112
+ graph2 = applyMessage(graph2, {
113
+ id: 'bob-move', sessionId: 'bob', clock: { bob: 2 }, lamportTime: 22, timestamp: Date.now(),
114
+ ops: [{ key: 'cube', otype: 'vector3.add', path: 'transform.position', value: [0, 2, 0] }],
115
+ });
116
+ graph2 = applyMessage(graph2, {
117
+ id: 'alice-move', sessionId: 'alice', clock: { alice: 2 }, lamportTime: 21, timestamp: Date.now(),
118
+ ops: [{ key: 'cube', otype: 'vector3.add', path: 'transform.position', value: [1, 0, 0] }],
119
+ });
120
+
121
+ console.log('Order 1 (Alice → Bob → Charlie):', graph1.nodes['cube']['transform.position']);
122
+ console.log('Order 2 (Charlie → Bob → Alice):', graph2.nodes['cube']['transform.position']);
123
+
124
+ const pos1 = JSON.stringify(graph1.nodes['cube']['transform.position']);
125
+ const pos2 = JSON.stringify(graph2.nodes['cube']['transform.position']);
126
+ console.log('\nāœ… Result:', pos1 === pos2 ? 'Same! Order doesn\'t matter for additive ops.' : 'āŒ Different!');
127
+
128
+ // ============================================
129
+ // Scenario 3: Mixed operations
130
+ // ============================================
131
+
132
+ console.log('\n--- Scenario 3: Mixed Operations in Real-time ---\n');
133
+ console.log('Simulating two users editing simultaneously...\n');
134
+
135
+ // Start fresh
136
+ graph = applyMessage(createEmptyGraph(), {
137
+ id: 'init',
138
+ sessionId: 'server',
139
+ clock: { server: 1 },
140
+ lamportTime: 0,
141
+ timestamp: Date.now(),
142
+ ops: [{
143
+ key: 'box',
144
+ otype: 'node.insert',
145
+ path: 'box',
146
+ value: {
147
+ id: 'uuid-box',
148
+ tag: 'Mesh',
149
+ name: 'Box',
150
+ color: '#888888',
151
+ 'transform.position': [0, 0, 0],
152
+ },
153
+ }],
154
+ });
155
+
156
+ console.log('Initial: color=#888888, position=[0,0,0]');
157
+
158
+ // Alice drags and changes color
159
+ const aliceMsg: CRDTMessage = {
160
+ id: 'alice-edit',
161
+ sessionId: 'alice',
162
+ clock: { alice: 1 },
163
+ lamportTime: 5,
164
+ timestamp: Date.now(),
165
+ ops: [
166
+ { key: 'box', otype: 'vector3.add', path: 'transform.position', value: [10, 0, 0] },
167
+ { key: 'box', otype: 'color.set', path: 'color', value: '#ff0000' },
168
+ ],
169
+ };
170
+
171
+ // Bob drags and changes color (higher lamport)
172
+ const bobMsg: CRDTMessage = {
173
+ id: 'bob-edit',
174
+ sessionId: 'bob',
175
+ clock: { bob: 1 },
176
+ lamportTime: 6,
177
+ timestamp: Date.now(),
178
+ ops: [
179
+ { key: 'box', otype: 'vector3.add', path: 'transform.position', value: [0, 5, 0] },
180
+ { key: 'box', otype: 'color.set', path: 'color', value: '#00ff00' },
181
+ ],
182
+ };
183
+
184
+ // Apply Bob first, then Alice (out of order)
185
+ graph = applyMessage(graph, bobMsg);
186
+ console.log('After Bob: color=%s, position=%s', graph.nodes['box'].color, JSON.stringify(graph.nodes['box']['transform.position']));
187
+
188
+ graph = applyMessage(graph, aliceMsg);
189
+ console.log('After Alice: color=%s, position=%s', graph.nodes['box'].color, JSON.stringify(graph.nodes['box']['transform.position']));
190
+
191
+ console.log('\nāœ… Final result:');
192
+ console.log(' Position: [10, 5, 0] - Both drags applied (additive)');
193
+ console.log(' Color: #00ff00 - Bob wins (LWW, lamport 6 > 5)');
194
+
195
+ // ============================================
196
+ // Scenario 4: Complex interleaved out-of-order
197
+ // ============================================
198
+
199
+ console.log('\n--- Scenario 4: Complex Interleaved Out-of-Order ---\n');
200
+ console.log('This scenario breaks naive approaches:\n');
201
+ console.log(' āŒ Simple set: Always overwrites → last arrival wins (wrong)');
202
+ console.log(' āŒ 1-step lookahead: Compare with previous op only (wrong)');
203
+ console.log(' āœ… Full LWW: Compare with node\'s max lamport (correct)\n');
204
+
205
+ // Timeline of when operations were CREATED:
206
+ // t=1: Alice sets color to red (lamport 10)
207
+ // t=2: Bob sets color to blue (lamport 20)
208
+ // t=3: Alice sets color to green (lamport 30)
209
+ // t=4: Charlie sets color to yellow (lamport 40)
210
+ // t=5: Bob sets color to purple (lamport 25) - Bob was offline, clock behind
211
+
212
+ // But network delivers them in scrambled order:
213
+ const messages = [
214
+ { name: 'Bob (purple)', lamport: 25, color: '#800080' },
215
+ { name: 'Alice (green)', lamport: 30, color: '#00ff00' },
216
+ { name: 'Charlie (yellow)', lamport: 40, color: '#ffff00' },
217
+ { name: 'Alice (red)', lamport: 10, color: '#ff0000' },
218
+ { name: 'Bob (blue)', lamport: 20, color: '#0000ff' },
219
+ ];
220
+
221
+ console.log('Messages arrive in scrambled order:');
222
+ messages.forEach((m, i) => console.log(` ${i + 1}. ${m.name} (lamport: ${m.lamport})`));
223
+
224
+ // ========================================
225
+ // Architecture: State = { graph, journal }
226
+ // ========================================
227
+ // - graph: Current materialized state
228
+ // - journal: Log of all messages (for replay/debugging)
229
+ //
230
+ // On each message:
231
+ // 1. state.journal.push(msg)
232
+ // 2. state.graph = applyMessage(state.graph, msg)
233
+
234
+ interface State {
235
+ graph: ReturnType<typeof createEmptyGraph>;
236
+ journal: CRDTMessage[];
237
+ }
238
+
239
+ const state: State = {
240
+ graph: createEmptyGraph(),
241
+ journal: [],
242
+ };
243
+
244
+ // Helper: process a message (append + apply)
245
+ function processMessage(s: State, msg: CRDTMessage): State {
246
+ return {
247
+ journal: [...s.journal, msg],
248
+ graph: applyMessage(s.graph, msg),
249
+ };
250
+ }
251
+
252
+ // Init
253
+ state.graph = applyMessage(state.graph, {
254
+ id: 'init', sessionId: 'server', clock: { server: 1 }, lamportTime: 0, timestamp: Date.now(),
255
+ ops: [{ key: 'obj', otype: 'node.insert', path: 'obj', value: { key: 'uuid', tag: 'Mesh', name: 'Object', color: '#ffffff' }}],
256
+ });
257
+
258
+ console.log('\nšŸ“– State = { graph, journal }');
259
+ console.log('─'.repeat(65));
260
+ console.log(' # Message Lamport MaxLamport State Applied?');
261
+ console.log('─'.repeat(65));
262
+
263
+ for (let i = 0; i < messages.length; i++) {
264
+ const m = messages[i];
265
+ const prevColor = state.graph.nodes['obj'].color;
266
+
267
+ const msg: CRDTMessage = {
268
+ id: `msg-${m.lamport}`,
269
+ sessionId: m.name,
270
+ clock: { [m.name]: 1 },
271
+ lamportTime: m.lamport,
272
+ timestamp: Date.now(),
273
+ ops: [{ key: 'obj', otype: 'color.set', path: 'color', value: m.color }],
274
+ };
275
+
276
+ // Process: append to journal + apply to graph
277
+ Object.assign(state, processMessage(state, msg));
278
+
279
+ const applied = state.graph.nodes['obj'].color !== prevColor;
280
+ const maxLamport = state.graph.nodes['obj'].lamportTime;
281
+ const appliedStr = applied ? 'āœ“ applied' : 'āœ— ignored';
282
+
283
+ console.log(` ${i + 1} ${m.name.padEnd(16)} ${String(m.lamport).padStart(3)} ${String(maxLamport).padStart(3)} ${state.graph.nodes['obj'].color} ${appliedStr}`);
284
+ }
285
+
286
+ console.log('─'.repeat(65));
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'].lamportTime})`);
289
+
290
+ const isCorrect = state.graph.nodes['obj'].color === '#ffff00';
291
+ console.log(`\n${isCorrect ? 'āœ…' : 'āŒ'} Result: ${isCorrect ? 'CORRECT' : 'WRONG'}`);
292
+
293
+ // Verify convergence - apply same messages in different order
294
+ console.log('\n--- Verifying convergence with different arrival orders ---\n');
295
+
296
+ const orders = [
297
+ [0, 1, 2, 3, 4], // Original scrambled
298
+ [4, 3, 2, 1, 0], // Reverse
299
+ [2, 0, 4, 1, 3], // Random shuffle 1
300
+ [3, 1, 4, 0, 2], // Random shuffle 2
301
+ ];
302
+
303
+ const results: string[] = [];
304
+
305
+ for (let i = 0; i < orders.length; i++) {
306
+ let g = applyMessage(createEmptyGraph(), {
307
+ id: 'init', sessionId: 'server', clock: { server: 1 }, lamportTime: 0, timestamp: Date.now(),
308
+ ops: [{ key: 'obj', otype: 'node.insert', path: 'obj', value: { key: 'uuid', tag: 'Mesh', name: 'Object', color: '#ffffff' }}],
309
+ });
310
+
311
+ for (const idx of orders[i]) {
312
+ const msg = messages[idx];
313
+ g = applyMessage(g, {
314
+ id: `msg-${msg.lamport}`,
315
+ sessionId: msg.name,
316
+ clock: { [msg.name]: 1 },
317
+ lamportTime: msg.lamport,
318
+ timestamp: Date.now(),
319
+ ops: [{ key: 'obj', otype: 'color.set', path: 'color', value: msg.color }],
320
+ });
321
+ }
322
+
323
+ results.push(g.nodes['obj'].color);
324
+ console.log(`Order ${i + 1}: [${orders[i].join(', ')}] → ${g.nodes['obj'].color}`);
325
+ }
326
+
327
+ const allSame = results.every(r => r === results[0]);
328
+ console.log(`\n${allSame ? 'āœ…' : 'āŒ'} All orders converge to same result: ${allSame ? 'YES' : 'NO'}`);
329
+
330
+ // ============================================
331
+ // Summary
332
+ // ============================================
333
+
334
+ console.log('\nšŸ“Š Summary:');
335
+ console.log('─'.repeat(50));
336
+ console.log('LWW (*.set): Higher lamport wins, regardless of arrival order');
337
+ console.log('Additive (*.add): All values accumulate, order doesn\'t matter');
338
+ console.log('─'.repeat(50));
339
+ console.log('\nClients converge to the same state without coordination!');
@@ -0,0 +1,86 @@
1
+ # CRDT Examples
2
+
3
+ This folder contains examples demonstrating how the CRDT system works.
4
+
5
+ ## Running Examples
6
+
7
+ ```bash
8
+ cd packages/server
9
+ npx tsx examples/01-basic-usage.ts
10
+ npx tsx examples/02-concurrent-edits.ts
11
+ npx tsx examples/03-scene-building.ts
12
+ npx tsx examples/04-conflict-resolution.ts
13
+ ```
14
+
15
+ ## Examples
16
+
17
+ ### 01-basic-usage.ts
18
+ Basic usage of the CRDT system:
19
+ - Creating an empty scene graph
20
+ - Inserting nodes
21
+ - Applying operations
22
+ - Moving objects (additive)
23
+ - Changing properties
24
+
25
+ ### 02-concurrent-edits.ts
26
+ Demonstrates concurrent edit resolution:
27
+ - **Additive operations** (`vector3.add`, `number.add`): Values are summed
28
+ - **LWW operations** (`color.set`, `vector3.set`): Last write wins (by lamport time)
29
+
30
+ ### 03-scene-building.ts
31
+ Building a complete scene:
32
+ - Batch operations (multiple ops in one message)
33
+ - Reparenting nodes
34
+ - Node deletion (tombstones)
35
+
36
+ ### 04-conflict-resolution.ts
37
+ Demonstrates conflict resolution:
38
+ - LWW with out-of-order message arrival
39
+ - Additive operations are order-independent
40
+ - Mixed operations in real-time editing
41
+
42
+ ## Core Concepts
43
+
44
+ ### CRDTMessage Structure
45
+
46
+ ```typescript
47
+ interface CRDTMessage {
48
+ id: string; // Message ID
49
+ sessionId: string; // Who sent this
50
+ clock: VectorClock; // For causal ordering
51
+ lamportTime: number; // For total ordering (LWW)
52
+ timestamp: number; // Wall-clock time
53
+ ops: Operation[]; // Batch of operations
54
+ }
55
+ ```
56
+
57
+ ### Operation Types
58
+
59
+ | otype | Merge Behavior | Use Case |
60
+ |-------|---------------|----------|
61
+ | `node.insert` | Idempotent | Create node (use `parent` field to auto-add to parent's children) |
62
+ | `node.remove` | Tombstone | Delete node |
63
+ | `number.set` | LWW | Absolute number |
64
+ | `number.add` | Sum | Counter, delta |
65
+ | `vector3.set` | LWW | Absolute position |
66
+ | `vector3.add` | Component sum | Drag movement |
67
+ | `color.set` | LWW | Material color |
68
+ | `array.set` | LWW | Replace array |
69
+ | `array.push` | Append | Add child |
70
+ | `array.remove` | Remove | Remove child |
71
+
72
+ ### Additive vs LWW
73
+
74
+ **Additive** (`*.add`): Order doesn't matter, values accumulate
75
+ ```
76
+ Alice: position += [5, 0, 0]
77
+ Bob: position += [0, 3, 0]
78
+ Result: position += [5, 3, 0] āœ…
79
+ ```
80
+
81
+ **LWW** (`*.set`): Higher lamportTime wins
82
+ ```
83
+ Alice: color = red (lamport: 10)
84
+ Bob: color = blue (lamport: 11)
85
+ Result: color = blue āœ… (Bob's lamport is higher)
86
+ ```
package/jest.config.js ADDED
@@ -0,0 +1,19 @@
1
+ /** @type {import('jest').Config} */
2
+ export default {
3
+ testEnvironment: 'node',
4
+ extensionsToTreatAsEsm: ['.ts', '.tsx'],
5
+ moduleNameMapper: {
6
+ '^(\\.{1,2}/.*)\\.js$': '$1',
7
+ },
8
+ transform: {
9
+ '^.+\\.tsx?$': [
10
+ 'ts-jest',
11
+ {
12
+ useESM: true,
13
+ tsconfig: 'tsconfig.test.json',
14
+ },
15
+ ],
16
+ },
17
+ testMatch: ['**/tests/**/*.test.ts'],
18
+ moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
19
+ };