@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 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/state/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,2CAA2C;AAC3C,OAAO,EAAE,kBAAkB,EAAoB,MAAM,kBAAkB,CAAC;AAExE,4CAA4C;AAC5C,OAAO,EAAE,KAAK,EAAoC,MAAM,YAAY,CAAC;AACrE,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAuB,MAAM,aAAa,CAAC;AACnG,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC"}
@@ -0,0 +1,222 @@
1
+ # Explicit Operation Types: Additive vs Absolute Updates
2
+
3
+ ## The Problem
4
+
5
+ In a collaborative 3D editor, the frontend knows the user's **intent**:
6
+ - **Additive**: `position.x += 5` (relative movement)
7
+ - **Absolute**: `position.x = 10` (set to specific value)
8
+
9
+ ## The Solution: Explicit otype
10
+
11
+ Operations use explicit `otype` that encodes both dtype and operation:
12
+ - `vector3.add` = additive vector operation
13
+ - `vector3.set` = absolute vector operation
14
+ - `number.add` = additive number operation
15
+ - `number.set` = absolute number operation
16
+
17
+ ### CRDTMessage Structure
18
+
19
+ ```typescript
20
+ interface CRDTMessage {
21
+ // === CRDT Metadata (envelope) ===
22
+ id: string; // Message ID
23
+ sessionId: string; // Session that created this message
24
+ clock: VectorClock; // Vector clock for causal ordering
25
+ lamportTime: number; // Lamport timestamp for total ordering
26
+ timestamp: number; // Wall-clock time
27
+
28
+ // === Operations (batch) ===
29
+ ops: Operation[]; // One or more operations
30
+ }
31
+ ```
32
+
33
+ ### Operation Structure
34
+
35
+ ```typescript
36
+ interface BaseOp {
37
+ key: string; // Node key (e.g., 'cube-1', 'player')
38
+ otype: string; // Explicit dtype.operation (e.g., 'vector3.add')
39
+ path: string; // Property path (e.g., 'transform.position')
40
+ }
41
+ ```
42
+
43
+ ## Frontend Use Cases
44
+
45
+ ### Case 1: Additive Transform (Relative Movement)
46
+
47
+ User drags object by [5, 0, 0]:
48
+
49
+ ```typescript
50
+ const msg: CRDTMessage = {
51
+ id: 'msg-001',
52
+ sessionId: 'session-alice',
53
+ clock: { 'session-alice': 1 },
54
+ lamportTime: 1,
55
+ timestamp: Date.now(),
56
+ ops: [
57
+ {
58
+ key: 'cube-1',
59
+ otype: 'vector3.add', // Explicit additive!
60
+ path: 'transform.position',
61
+ value: [5, 0, 0]
62
+ }
63
+ ]
64
+ };
65
+ ```
66
+
67
+ **Result**: If two users drag simultaneously, both movements apply:
68
+ - Alice: `position += [5, 0, 0]`
69
+ - Bob: `position += [0, 3, 0]`
70
+ - Final: `position += [5, 3, 0]` ✅
71
+
72
+ ### Case 2: Absolute Transform (Set Position)
73
+
74
+ User enters exact coordinates in inspector:
75
+
76
+ ```typescript
77
+ const msg: CRDTMessage = {
78
+ id: 'msg-002',
79
+ sessionId: 'session-alice',
80
+ clock: { 'session-alice': 2 },
81
+ lamportTime: 2,
82
+ timestamp: Date.now(),
83
+ ops: [
84
+ {
85
+ key: 'sphere-1',
86
+ otype: 'vector3.set', // Explicit absolute!
87
+ path: 'transform.position',
88
+ value: [10, 5, 0]
89
+ }
90
+ ]
91
+ };
92
+ ```
93
+
94
+ **Result**: If two users set position simultaneously, last-write-wins:
95
+ - Alice: `position = [10, 5, 0]` (lamport: 100)
96
+ - Bob: `position = [0, 10, 0]` (lamport: 101)
97
+ - Final: `position = [0, 10, 0]` (Bob wins) ✅
98
+
99
+ ### Case 3: Score Counter (Always Additive)
100
+
101
+ User collects 10 points:
102
+
103
+ ```typescript
104
+ const msg: CRDTMessage = {
105
+ id: 'msg-003',
106
+ sessionId: 'session-alice',
107
+ clock: { 'session-alice': 3 },
108
+ lamportTime: 3,
109
+ timestamp: Date.now(),
110
+ ops: [
111
+ {
112
+ key: 'cube-1',
113
+ otype: 'number.add', // Additive counter
114
+ path: 'score',
115
+ value: 10
116
+ }
117
+ ]
118
+ };
119
+ ```
120
+
121
+ **Result**: Concurrent score updates sum:
122
+ - Alice: `score += 10`
123
+ - Bob: `score += 5`
124
+ - Final: `score = 15` ✅
125
+
126
+ ### Case 4: Color (Always LWW)
127
+
128
+ User changes material color:
129
+
130
+ ```typescript
131
+ const msg: CRDTMessage = {
132
+ id: 'msg-004',
133
+ sessionId: 'session-alice',
134
+ clock: { 'session-alice': 4 },
135
+ lamportTime: 4,
136
+ timestamp: Date.now(),
137
+ ops: [
138
+ {
139
+ key: 'cube-1',
140
+ otype: 'color.set',
141
+ path: 'color',
142
+ value: '#ff0000'
143
+ }
144
+ ]
145
+ };
146
+ ```
147
+
148
+ **Result**: Concurrent color changes resolve to latest:
149
+ - Alice: `color = '#ff0000'` (lamport: 100)
150
+ - Bob: `color = '#00ff00'` (lamport: 101)
151
+ - Final: `color = '#00ff00'` (Bob wins) ✅
152
+
153
+ ## Batching: Multiple Operations in One Message
154
+
155
+ ### Same Node, Multiple Properties
156
+
157
+ ```typescript
158
+ const msg: CRDTMessage = {
159
+ id: 'msg-005',
160
+ sessionId: 'session-alice',
161
+ clock: { 'session-alice': 5 },
162
+ lamportTime: 5,
163
+ timestamp: Date.now(),
164
+ ops: [
165
+ { key: 'cube-1', otype: 'color.set', path: 'color', value: '#00ff00' },
166
+ { key: 'cube-1', otype: 'number.set', path: 'opacity', value: 0.5 },
167
+ { key: 'cube-1', otype: 'vector3.add', path: 'transform.position', value: [0, 2, 0] }
168
+ ]
169
+ };
170
+ ```
171
+
172
+ ### Multi-Select Drag (Multiple Nodes)
173
+
174
+ ```typescript
175
+ const msg: CRDTMessage = {
176
+ id: 'msg-006',
177
+ sessionId: 'session-alice',
178
+ clock: { 'session-alice': 6 },
179
+ lamportTime: 6,
180
+ timestamp: Date.now(),
181
+ ops: [
182
+ { key: 'cube-1', otype: 'vector3.add', path: 'transform.position', value: [3, 0, 0] },
183
+ { key: 'sphere-1', otype: 'vector3.add', path: 'transform.position', value: [3, 0, 0] }
184
+ ]
185
+ };
186
+ ```
187
+
188
+ ## Available Operation Types
189
+
190
+ | otype | Description | Example |
191
+ |-------|-------------|---------|
192
+ | `number.set` | LWW number | `opacity = 0.5` |
193
+ | `number.add` | Additive | `score += 10` |
194
+ | `number.multiply` | Multiplicative | `scale *= 2` |
195
+ | `number.min` | Take minimum | `minScore = min(current, new)` |
196
+ | `number.max` | Take maximum | `maxScore = max(current, new)` |
197
+ | `vector3.set` | LWW vector | `position = [0, 5, 0]` |
198
+ | `vector3.add` | Additive | `position += [5, 0, 0]` |
199
+ | `color.set` | LWW color | `color = '#ff0000'` |
200
+ | `array.set` | Replace array | `children = ['a', 'b']` |
201
+ | `array.push` | Append item | `children.push('c')` |
202
+ | `array.remove` | Remove item | `children.remove('a')` |
203
+ | `node.insert` | Create node | New node in scene |
204
+ | `node.remove` | Delete node | Tombstone |
205
+
206
+ ## Example: Transform Gestures
207
+
208
+ | Gesture | otype | path | value |
209
+ |---------|-------|------|-------|
210
+ | Drag (translate) | `vector3.add` | `transform.position` | `[dx, dy, dz]` |
211
+ | Inspector input | `vector3.set` | `transform.position` | `[x, y, z]` |
212
+ | Scale gesture | `vector3.multiply` | `transform.scale` | `[sx, sy, sz]` |
213
+ | Snap to grid | `vector3.set` | `transform.position` | `[gx, gy, gz]` |
214
+
215
+ ## Key Design Points
216
+
217
+ 1. **Explicit otype**: `vector3.add` vs `vector3.set` - no ambiguity
218
+ 2. **Operations use `key`**: Human-friendly node keys, not UUIDs
219
+ 3. **True batching**: One message, multiple ops, atomic
220
+ 4. **Clean structure**: No nested "properties" or "operations" objects
221
+
222
+ See `docs/TYPE_BEHAVIORS.md` for complete merge semantics.
@@ -0,0 +1,373 @@
1
+ # Scene Graph Data Structure
2
+
3
+ Hierarchical tree structure for collaborative 3D editing with **instancing support** via flattened map format.
4
+
5
+ ## Table of Contents
6
+
7
+ - [Scene Graph Schema](#scene-graph-schema)
8
+ - [Scene Node Schema](#scene-node-schema)
9
+ - [CRDTMessage Structure](#crdtmessage-structure)
10
+ - [Operations](#operations)
11
+ - [node.insert](#nodeinsert)
12
+ - [node.remove](#noderemove)
13
+ - [Property Operations](#property-operations)
14
+ - [Array Operations](#array-operations)
15
+ - [Tree Operations](#tree-operations)
16
+ - [Reparent (Compound)](#reparent-compound)
17
+ - [Instancing](#instancing)
18
+ - [Query Patterns](#query-patterns)
19
+
20
+ ---
21
+
22
+ ## Scene Graph Schema
23
+
24
+ ```typescript
25
+ interface SceneGraph {
26
+ nodes: Record<string, SceneNode>; // Flattened map by key
27
+ rootKey: string; // Root node key
28
+ }
29
+ ```
30
+
31
+ **Design:** Flattened map (similar to glTF) for O(1) lookup and stable references.
32
+
33
+ **Example:**
34
+ ```typescript
35
+ {
36
+ nodes: {
37
+ 'scene': { key: 'scene', tag: 'Scene', children: ['forest', 'player'], ... },
38
+ 'forest': { key: 'forest', tag: 'Group', children: ['tree-1'], ... },
39
+ 'tree-1': { key: 'tree-1', tag: 'Mesh', children: [], ... },
40
+ 'player': { key: 'player', tag: 'Mesh', children: [], ... }
41
+ },
42
+ rootKey: 'scene'
43
+ }
44
+ ```
45
+
46
+ ---
47
+
48
+ ## Scene Node Schema
49
+
50
+ ```typescript
51
+ interface SceneNode {
52
+ // Identity
53
+ id: string; // UUID (for CRDT)
54
+ key: string; // Map key (human-friendly, unique in scene)
55
+ tag: string; // "Scene" | "Group" | "Mesh" | "Light" | "Camera"
56
+ name: string; // Display name
57
+
58
+ // Tree structure (children-only for instancing)
59
+ children: string[]; // Child keys (not IDs!)
60
+
61
+ // Properties (flat structure with dot-notation paths)
62
+ // e.g., 'transform.position': [0, 0, 0], 'color': '#ff0000'
63
+ [key: string]: any;
64
+
65
+ // CRDT metadata
66
+ clock: VectorClock;
67
+ lamportTime: number;
68
+ createdAt: number;
69
+ updatedAt: number;
70
+ deletedAt?: number; // Tombstone (soft delete)
71
+ }
72
+ ```
73
+
74
+ **Key Design Decisions:**
75
+ - **Flattened map** by `key` (not nested tree)
76
+ - **No `parentId`** → supports **instancing** (same node can have multiple parents)
77
+ - **Operations use `key`** → human-readable references
78
+ - **Children use keys** (not IDs) → human-readable tree structure
79
+
80
+ ---
81
+
82
+ ## CRDTMessage Structure
83
+
84
+ All operations are wrapped in a `CRDTMessage` envelope:
85
+
86
+ ```typescript
87
+ interface CRDTMessage {
88
+ // === CRDT Metadata (envelope) ===
89
+ id: string; // Message ID
90
+ sessionId: string; // Session that created this message
91
+ clock: VectorClock; // Vector clock for causal ordering
92
+ lamportTime: number; // Lamport timestamp for total ordering
93
+ timestamp: number; // Wall-clock time
94
+
95
+ // === Operations (batch) ===
96
+ ops: Operation[]; // One or more operations
97
+ }
98
+
99
+ interface BaseOp {
100
+ key: string; // Node key (e.g., 'cube-1', 'scene')
101
+ otype: string; // dtype.operation (e.g., 'vector3.add', 'node.insert')
102
+ path: string; // Property path (e.g., 'transform.position')
103
+ }
104
+ ```
105
+
106
+ ---
107
+
108
+ ## Operations
109
+
110
+ ### node.insert
111
+
112
+ Create a new node in the scene graph.
113
+
114
+ ```typescript
115
+ const msg: CRDTMessage = {
116
+ id: 'msg-001',
117
+ sessionId: 'session-alice',
118
+ clock: { 'session-alice': 1 },
119
+ lamportTime: 1,
120
+ timestamp: Date.now(),
121
+ ops: [
122
+ {
123
+ key: 'cube-1',
124
+ otype: 'node.insert',
125
+ path: 'cube-1',
126
+ value: {
127
+ id: 'uuid-cube-001',
128
+ tag: 'Mesh',
129
+ name: 'Red Cube',
130
+ color: '#ff0000',
131
+ 'transform.position': [0, 0, 0],
132
+ 'transform.rotation': [0, 0, 0, 1],
133
+ 'transform.scale': [1, 1, 1]
134
+ }
135
+ }
136
+ ]
137
+ };
138
+
139
+ // Result: Added to nodes map
140
+ // nodes['cube-1'] = { id: 'uuid-cube-001', key: 'cube-1', tag: 'Mesh', ... }
141
+ ```
142
+
143
+ **Behavior:**
144
+ - Adds node to `nodes` map: `nodes[key] = node`
145
+ - If `key` already exists → idempotent (no-op)
146
+ - Does NOT auto-add to parent (use separate `array.push` on parent's `children`)
147
+
148
+ **Conflicts:**
149
+ - Different keys → both succeed
150
+ - Same key → first wins (idempotent)
151
+
152
+ ---
153
+
154
+ ### node.remove
155
+
156
+ Delete node (tombstone) + remove from all parents' children.
157
+
158
+ ```typescript
159
+ const msg: CRDTMessage = {
160
+ id: 'msg-002',
161
+ sessionId: 'session-bob',
162
+ clock: { 'session-bob': 1 },
163
+ lamportTime: 2,
164
+ timestamp: Date.now(),
165
+ ops: [
166
+ {
167
+ key: 'cube-1',
168
+ otype: 'node.remove',
169
+ path: 'cube-1'
170
+ }
171
+ ]
172
+ };
173
+ ```
174
+
175
+ **Behavior:**
176
+ 1. Set `node.deletedAt = timestamp` (tombstone)
177
+ 2. Remove `cube-1` from all parents' `children` arrays
178
+
179
+ **Conflicts:**
180
+ - remove **wins** over concurrent property update
181
+ - Multiple removes → idempotent
182
+
183
+ ---
184
+
185
+ ### Property Operations
186
+
187
+ Update node properties using explicit otype:
188
+
189
+ ```typescript
190
+ const msg: CRDTMessage = {
191
+ id: 'msg-003',
192
+ sessionId: 'session-alice',
193
+ clock: { 'session-alice': 2 },
194
+ lamportTime: 3,
195
+ timestamp: Date.now(),
196
+ ops: [
197
+ // Additive transform (drag)
198
+ { key: 'cube-1', otype: 'vector3.add', path: 'transform.position', value: [5, 0, 0] },
199
+
200
+ // Absolute transform (inspector input)
201
+ { key: 'sphere-1', otype: 'vector3.set', path: 'transform.position', value: [0, 5, 0] },
202
+
203
+ // Color change (LWW)
204
+ { key: 'cube-1', otype: 'color.set', path: 'color', value: '#00ff00' },
205
+
206
+ // Additive counter
207
+ { key: 'cube-1', otype: 'number.add', path: 'score', value: 10 },
208
+
209
+ // LWW number
210
+ { key: 'cube-1', otype: 'number.set', path: 'opacity', value: 0.5 }
211
+ ]
212
+ };
213
+ ```
214
+
215
+ **Available Property otypes:**
216
+
217
+ | otype | Merge Behavior | Example |
218
+ |-------|---------------|---------|
219
+ | `number.set` | LWW | `opacity = 0.5` |
220
+ | `number.add` | Sum | `score += 10` |
221
+ | `number.multiply` | Product | `scale *= 2` |
222
+ | `number.min` | Minimum | `min(current, new)` |
223
+ | `number.max` | Maximum | `max(current, new)` |
224
+ | `vector3.set` | LWW | `position = [0,5,0]` |
225
+ | `vector3.add` | Component-wise sum | `position += [5,0,0]` |
226
+ | `quaternion.set` | LWW | `rotation = [0,0,0,1]` |
227
+ | `color.set` | LWW | `color = '#ff0000'` |
228
+ | `string.set` | LWW | `name = 'Cube'` |
229
+ | `boolean.set` | LWW | `visible = true` |
230
+
231
+ ---
232
+
233
+ ### Array Operations
234
+
235
+ Manipulate arrays (like `children`):
236
+
237
+ ```typescript
238
+ const msg: CRDTMessage = {
239
+ id: 'msg-004',
240
+ sessionId: 'session-alice',
241
+ clock: { 'session-alice': 3 },
242
+ lamportTime: 4,
243
+ timestamp: Date.now(),
244
+ ops: [
245
+ // Replace entire array
246
+ { key: 'scene', otype: 'array.set', path: 'children', value: ['cube-1', 'sphere-1'] },
247
+
248
+ // Append to array
249
+ { key: 'scene', otype: 'array.push', path: 'children', value: 'light-1' },
250
+
251
+ // Remove from array
252
+ { key: 'scene', otype: 'array.remove', path: 'children', value: 'cube-1' }
253
+ ]
254
+ };
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Tree Operations
260
+
261
+ ### Reparent (Compound)
262
+
263
+ Move node from one parent to another using batched operations:
264
+
265
+ ```typescript
266
+ const msg: CRDTMessage = {
267
+ id: 'msg-005',
268
+ sessionId: 'session-alice',
269
+ clock: { 'session-alice': 4 },
270
+ lamportTime: 5,
271
+ timestamp: Date.now(),
272
+ ops: [
273
+ // 1. Create new parent (optional)
274
+ {
275
+ key: 'group-1',
276
+ otype: 'node.insert',
277
+ path: 'group-1',
278
+ value: {
279
+ id: 'uuid-group-001',
280
+ tag: 'Group',
281
+ name: 'My Group',
282
+ 'transform.position': [0, 0, 0],
283
+ 'transform.rotation': [0, 0, 0, 1],
284
+ 'transform.scale': [1, 1, 1]
285
+ }
286
+ },
287
+ // 2. Remove from old parent
288
+ { key: 'scene', otype: 'array.remove', path: 'children', value: 'cube-1' },
289
+ // 3. Add to new parent
290
+ { key: 'group-1', otype: 'array.push', path: 'children', value: 'cube-1' }
291
+ ]
292
+ };
293
+ // → cube-1 moved from scene to group-1 (atomic)
294
+ ```
295
+
296
+ **Why compound operations?**
297
+ - Children-only model (no parentId to update)
298
+ - Supports instancing (node can have multiple parents)
299
+ - Atomic: all operations in same message
300
+
301
+ ---
302
+
303
+ ## Instancing
304
+
305
+ Same node can appear in multiple places (multiple parents reference it).
306
+
307
+ ```typescript
308
+ // Shared tree node
309
+ { key: 'tree-model', tag: 'Mesh', geometry: 'tree.glb', ... }
310
+
311
+ // Parent A references it
312
+ { key: 'forest-A', tag: 'Group', children: ['tree-model', ...] }
313
+
314
+ // Parent B also references it
315
+ { key: 'forest-B', tag: 'Group', children: ['tree-model', ...] }
316
+ ```
317
+
318
+ `tree-model` appears in **both** `forest-A` and `forest-B`.
319
+
320
+ **Transform:**
321
+ - Each parent's transform combines with `tree-model`'s local transform
322
+ - World transform = parent.transform × tree-model.transform
323
+
324
+ **Delete:**
325
+ - If `tree-model` is removed via `node.remove`, it's deleted from **all** parents' children arrays
326
+
327
+ ---
328
+
329
+ ## Query Patterns
330
+
331
+ ```typescript
332
+ // Get node by key
333
+ getNode(key: string): SceneNode | undefined
334
+
335
+ // Get active nodes (exclude tombstones)
336
+ getActiveNodes(): SceneNode[]
337
+
338
+ // Get children
339
+ getChildren(key: string): SceneNode[]
340
+
341
+ // Find parents (requires reverse index)
342
+ getParents(key: string): SceneNode[]
343
+
344
+ // Build reverse index
345
+ function buildParentIndex(nodes: Record<string, SceneNode>): Map<string, string[]> {
346
+ const index = new Map();
347
+ for (const node of Object.values(nodes)) {
348
+ for (const childKey of node.children) {
349
+ if (!index.has(childKey)) index.set(childKey, []);
350
+ index.get(childKey).push(node.key);
351
+ }
352
+ }
353
+ return index;
354
+ }
355
+
356
+ // Check cycles
357
+ isDescendant(key: string, ancestorKey: string): boolean
358
+
359
+ // Get world transform
360
+ getWorldTransform(key: string, throughParentKey: string): Transform3D
361
+ ```
362
+
363
+ ---
364
+
365
+ ## Design Rationale
366
+
367
+ - ✅ **Instancing support** (multiple parents)
368
+ - ✅ **Simpler model** (one-way references only)
369
+ - ✅ **CRDT-friendly** (no bidirectional constraints)
370
+ - ✅ **True batching** (multiple ops, atomic)
371
+ - ✅ **Explicit otype** (no ambiguity: `vector3.add` vs `vector3.set`)
372
+ - ✅ **Human-friendly keys** (not UUIDs)
373
+ - ❌ **Parent lookup requires reverse index** (not persisted)