@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.
- package/dist/client/EditBuffer.d.ts +43 -0
- package/dist/client/EditBuffer.d.ts.map +1 -0
- package/dist/client/EditBuffer.js +96 -0
- package/dist/client/EditBuffer.js.map +1 -0
- package/dist/client/actions.d.ts +66 -0
- package/dist/client/actions.d.ts.map +1 -0
- package/dist/client/actions.js +345 -0
- package/dist/client/actions.js.map +1 -0
- package/dist/client/createGraph.d.ts +30 -0
- package/dist/client/createGraph.d.ts.map +1 -0
- package/dist/client/createGraph.js +91 -0
- package/dist/client/createGraph.js.map +1 -0
- package/dist/client/hooks.d.ts +81 -0
- package/dist/client/hooks.d.ts.map +1 -0
- package/dist/client/hooks.js +161 -0
- package/dist/client/hooks.js.map +1 -0
- package/dist/client/index.d.ts +8 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +10 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/types.d.ts +74 -0
- package/dist/client/types.d.ts.map +1 -0
- package/dist/client/types.js +11 -0
- package/dist/client/types.js.map +1 -0
- package/dist/hooks.d.ts +8 -0
- package/dist/hooks.d.ts.map +1 -0
- package/dist/hooks.js +7 -0
- package/dist/hooks.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +12 -0
- package/dist/index.js.map +1 -0
- package/dist/operations/OperationTypes.d.ts +239 -0
- package/dist/operations/OperationTypes.d.ts.map +1 -0
- package/dist/operations/OperationTypes.js +10 -0
- package/dist/operations/OperationTypes.js.map +1 -0
- package/dist/operations/OperationValidator.d.ts +32 -0
- package/dist/operations/OperationValidator.d.ts.map +1 -0
- package/dist/operations/OperationValidator.js +208 -0
- package/dist/operations/OperationValidator.js.map +1 -0
- package/dist/operations/apply/array.d.ts +22 -0
- package/dist/operations/apply/array.d.ts.map +1 -0
- package/dist/operations/apply/array.js +64 -0
- package/dist/operations/apply/array.js.map +1 -0
- package/dist/operations/apply/boolean.d.ts +18 -0
- package/dist/operations/apply/boolean.d.ts.map +1 -0
- package/dist/operations/apply/boolean.js +34 -0
- package/dist/operations/apply/boolean.js.map +1 -0
- package/dist/operations/apply/color.d.ts +14 -0
- package/dist/operations/apply/color.d.ts.map +1 -0
- package/dist/operations/apply/color.js +46 -0
- package/dist/operations/apply/color.js.map +1 -0
- package/dist/operations/apply/index.d.ts +18 -0
- package/dist/operations/apply/index.d.ts.map +1 -0
- package/dist/operations/apply/index.js +26 -0
- package/dist/operations/apply/index.js.map +1 -0
- package/dist/operations/apply/node.d.ts +24 -0
- package/dist/operations/apply/node.d.ts.map +1 -0
- package/dist/operations/apply/node.js +77 -0
- package/dist/operations/apply/node.js.map +1 -0
- package/dist/operations/apply/number.d.ts +26 -0
- package/dist/operations/apply/number.d.ts.map +1 -0
- package/dist/operations/apply/number.js +54 -0
- package/dist/operations/apply/number.js.map +1 -0
- package/dist/operations/apply/object.d.ts +14 -0
- package/dist/operations/apply/object.d.ts.map +1 -0
- package/dist/operations/apply/object.js +47 -0
- package/dist/operations/apply/object.js.map +1 -0
- package/dist/operations/apply/quaternion.d.ts +15 -0
- package/dist/operations/apply/quaternion.d.ts.map +1 -0
- package/dist/operations/apply/quaternion.js +33 -0
- package/dist/operations/apply/quaternion.js.map +1 -0
- package/dist/operations/apply/string.d.ts +14 -0
- package/dist/operations/apply/string.d.ts.map +1 -0
- package/dist/operations/apply/string.js +26 -0
- package/dist/operations/apply/string.js.map +1 -0
- package/dist/operations/apply/types.d.ts +34 -0
- package/dist/operations/apply/types.d.ts.map +1 -0
- package/dist/operations/apply/types.js +32 -0
- package/dist/operations/apply/types.js.map +1 -0
- package/dist/operations/apply/vector3.d.ts +18 -0
- package/dist/operations/apply/vector3.d.ts.map +1 -0
- package/dist/operations/apply/vector3.js +44 -0
- package/dist/operations/apply/vector3.js.map +1 -0
- package/dist/operations/dispatcher.d.ts +35 -0
- package/dist/operations/dispatcher.d.ts.map +1 -0
- package/dist/operations/dispatcher.js +107 -0
- package/dist/operations/dispatcher.js.map +1 -0
- package/dist/operations/index.d.ts +10 -0
- package/dist/operations/index.d.ts.map +1 -0
- package/dist/operations/index.js +17 -0
- package/dist/operations/index.js.map +1 -0
- package/dist/state/ConflictResolver.d.ts +36 -0
- package/dist/state/ConflictResolver.d.ts.map +1 -0
- package/dist/state/ConflictResolver.js +167 -0
- package/dist/state/ConflictResolver.js.map +1 -0
- package/dist/state/DType.d.ts +160 -0
- package/dist/state/DType.d.ts.map +1 -0
- package/dist/state/DType.js +282 -0
- package/dist/state/DType.js.map +1 -0
- package/dist/state/Schema.d.ts +32 -0
- package/dist/state/Schema.d.ts.map +1 -0
- package/dist/state/Schema.js +175 -0
- package/dist/state/Schema.js.map +1 -0
- package/dist/state/VectorClock.d.ts +42 -0
- package/dist/state/VectorClock.d.ts.map +1 -0
- package/dist/state/VectorClock.js +84 -0
- package/dist/state/VectorClock.js.map +1 -0
- package/dist/state/index.d.ts +11 -0
- package/dist/state/index.d.ts.map +1 -0
- package/dist/state/index.js +13 -0
- package/dist/state/index.js.map +1 -0
- package/docs/OPERATION_HINTS.md +222 -0
- package/docs/SCENE_GRAPH.md +373 -0
- package/docs/TYPE_BEHAVIORS.md +348 -0
- package/examples/01-basic-usage.ts +139 -0
- package/examples/02-concurrent-edits.ts +208 -0
- package/examples/03-scene-building.ts +258 -0
- package/examples/04-conflict-resolution.ts +339 -0
- package/examples/README.md +86 -0
- package/jest.config.js +19 -0
- package/package.json +57 -0
- package/src/client/EditBuffer.ts +105 -0
- package/src/client/actions.ts +397 -0
- package/src/client/createGraph.ts +132 -0
- package/src/client/hooks.tsx +249 -0
- package/src/client/index.ts +35 -0
- package/src/client/types.ts +94 -0
- package/src/hooks.ts +20 -0
- package/src/index.ts +14 -0
- package/src/operations/OperationTypes.ts +340 -0
- package/src/operations/OperationValidator.ts +260 -0
- package/src/operations/apply/array.ts +84 -0
- package/src/operations/apply/boolean.ts +48 -0
- package/src/operations/apply/color.ts +65 -0
- package/src/operations/apply/index.ts +37 -0
- package/src/operations/apply/node.ts +98 -0
- package/src/operations/apply/number.ts +76 -0
- package/src/operations/apply/object.ts +63 -0
- package/src/operations/apply/quaternion.ts +47 -0
- package/src/operations/apply/string.ts +36 -0
- package/src/operations/apply/types.ts +66 -0
- package/src/operations/apply/vector3.ts +60 -0
- package/src/operations/dispatcher.ts +127 -0
- package/src/operations/index.ts +80 -0
- package/src/state/ConflictResolver.ts +205 -0
- package/src/state/DType.ts +333 -0
- package/src/state/Schema.ts +236 -0
- package/src/state/VectorClock.ts +98 -0
- package/src/state/index.ts +14 -0
- package/tests/client/actions.test.ts +371 -0
- package/tests/client/edit-buffer.test.ts +117 -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/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/operations/collections.test.ts +193 -0
- package/tests/operations/nodes.test.ts +228 -0
- package/tests/operations/primitives.test.ts +222 -0
- package/tests/operations/vectors.test.ts +150 -0
- package/tsconfig.json +21 -0
- package/tsconfig.test.json +9 -0
|
@@ -0,0 +1,348 @@
|
|
|
1
|
+
# CRDT Type Behaviors: Comprehensive Reference
|
|
2
|
+
|
|
3
|
+
Different data types have different natural merge semantics. This document defines the behavior of each type when concurrent operations occur.
|
|
4
|
+
|
|
5
|
+
## Operation Type Format
|
|
6
|
+
|
|
7
|
+
Operations use explicit `otype` that encodes both dtype and operation:
|
|
8
|
+
- `number.set` / `number.add` / `number.multiply` / `number.min` / `number.max`
|
|
9
|
+
- `vector3.set` / `vector3.add` / `vector3.multiply`
|
|
10
|
+
- `color.set` / `color.blend`
|
|
11
|
+
- `array.set` / `array.push` / `array.remove` / `array.union`
|
|
12
|
+
- `node.insert` / `node.remove`
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Summary Table
|
|
17
|
+
|
|
18
|
+
| otype | Merge Behavior | Use Case |
|
|
19
|
+
|-------|---------------|----------|
|
|
20
|
+
| `number.set` | LWW | Properties, measurements |
|
|
21
|
+
| `number.add` | Sum | Counters, scores, deltas |
|
|
22
|
+
| `number.multiply` | Product | Scale factors |
|
|
23
|
+
| `number.min` | Minimum | Quality settings |
|
|
24
|
+
| `number.max` | Maximum | High scores |
|
|
25
|
+
| `string.set` | LWW | Names, labels |
|
|
26
|
+
| `string.concat` | Concatenate | Logs (ordered by lamport) |
|
|
27
|
+
| `boolean.set` | LWW | Flags, toggles |
|
|
28
|
+
| `boolean.or` | OR | Feature flags (enable wins) |
|
|
29
|
+
| `boolean.and` | AND | Permissions (disable wins) |
|
|
30
|
+
| `vector3.set` | LWW | Absolute position/scale |
|
|
31
|
+
| `vector3.add` | Component sum | Delta movement |
|
|
32
|
+
| `vector3.multiply` | Component product | Scale gestures |
|
|
33
|
+
| `quaternion.set` | LWW | Rotation |
|
|
34
|
+
| `color.set` | LWW | Material color |
|
|
35
|
+
| `array.set` | LWW | Replace array |
|
|
36
|
+
| `array.push` | Append | Add item |
|
|
37
|
+
| `array.remove` | Remove | Remove item |
|
|
38
|
+
| `array.union` | Union | Merge sets |
|
|
39
|
+
| `object.set` | LWW | Replace object |
|
|
40
|
+
| `object.merge` | Deep merge | Merge properties |
|
|
41
|
+
| `node.insert` | Idempotent | Create node |
|
|
42
|
+
| `node.remove` | Tombstone | Delete node |
|
|
43
|
+
|
|
44
|
+
## Table of Contents
|
|
45
|
+
|
|
46
|
+
- [Primitive Types](#primitive-types)
|
|
47
|
+
- [Number](#number) - `set`, `add`, `multiply`, `min`, `max`
|
|
48
|
+
- [String](#string) - `set`, `concat`
|
|
49
|
+
- [Boolean](#boolean) - `set`, `or`, `and`
|
|
50
|
+
- [Collection Types](#collection-types)
|
|
51
|
+
- [Array](#array) - `set`, `push`, `remove`, `union`
|
|
52
|
+
- [Object](#object) - `set`, `merge`
|
|
53
|
+
- [Compound Types (3D Graphics)](#compound-types-3d-graphics)
|
|
54
|
+
- [Vector3](#vector3-position-scale) - `set`, `add`, `multiply`
|
|
55
|
+
- [Quaternion](#quaternion-rotation) - `set`, `multiply`
|
|
56
|
+
- [Color](#color) - `set`, `blend`
|
|
57
|
+
- [Scene Graph Operations](#scene-graph-operations)
|
|
58
|
+
- [node.insert](#nodeinsert)
|
|
59
|
+
- [node.remove](#noderemove)
|
|
60
|
+
- [Implementation Strategy](#implementation-strategy)
|
|
61
|
+
|
|
62
|
+
---
|
|
63
|
+
|
|
64
|
+
## Primitive Types
|
|
65
|
+
|
|
66
|
+
### Number
|
|
67
|
+
|
|
68
|
+
| otype | Merge | Example |
|
|
69
|
+
|-------|-------|---------|
|
|
70
|
+
| `number.set` | LWW | `{ otype: 'number.set', path: 'opacity', value: 0.5 }` |
|
|
71
|
+
| `number.add` | Sum | `{ otype: 'number.add', path: 'score', value: 10 }` |
|
|
72
|
+
| `number.multiply` | Product | `{ otype: 'number.multiply', path: 'scale', value: 2 }` |
|
|
73
|
+
| `number.min` | Minimum | `{ otype: 'number.min', path: 'minScore', value: 100 }` |
|
|
74
|
+
| `number.max` | Maximum | `{ otype: 'number.max', path: 'maxScore', value: 100 }` |
|
|
75
|
+
|
|
76
|
+
**Examples:**
|
|
77
|
+
```typescript
|
|
78
|
+
// Additive counter (concurrent updates sum)
|
|
79
|
+
{ key: 'cube-1', otype: 'number.add', path: 'score', value: 10 }
|
|
80
|
+
// Alice: score += 10, Bob: score += 5 ā Result: 15 ā
|
|
81
|
+
|
|
82
|
+
// LWW property (last write wins)
|
|
83
|
+
{ key: 'cube-1', otype: 'number.set', path: 'opacity', value: 0.5 }
|
|
84
|
+
// Alice: opacity = 0.3 (lamport: 100), Bob: opacity = 0.8 (lamport: 101)
|
|
85
|
+
// Result: 0.8 ā
(Bob wins)
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
### String
|
|
91
|
+
|
|
92
|
+
| otype | Merge | Example |
|
|
93
|
+
|-------|-------|---------|
|
|
94
|
+
| `string.set` | LWW | `{ otype: 'string.set', path: 'name', value: 'Cube' }` |
|
|
95
|
+
| `string.concat` | Concatenate | `{ otype: 'string.concat', path: 'log', value: 'entry' }` |
|
|
96
|
+
|
|
97
|
+
**Examples:**
|
|
98
|
+
```typescript
|
|
99
|
+
// LWW name
|
|
100
|
+
{ key: 'cube-1', otype: 'string.set', path: 'name', value: 'Red Cube' }
|
|
101
|
+
// Alice: "Red Cube" (lamport: 100), Bob: "Blue Sphere" (lamport: 101)
|
|
102
|
+
// Result: "Blue Sphere" ā
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
### Boolean
|
|
108
|
+
|
|
109
|
+
| otype | Merge | Example |
|
|
110
|
+
|-------|-------|---------|
|
|
111
|
+
| `boolean.set` | LWW | `{ otype: 'boolean.set', path: 'visible', value: true }` |
|
|
112
|
+
| `boolean.or` | OR (enable wins) | `{ otype: 'boolean.or', path: 'enabled', value: true }` |
|
|
113
|
+
| `boolean.and` | AND (disable wins) | `{ otype: 'boolean.and', path: 'allowed', value: false }` |
|
|
114
|
+
|
|
115
|
+
**Examples:**
|
|
116
|
+
```typescript
|
|
117
|
+
// LWW visibility
|
|
118
|
+
{ key: 'cube-1', otype: 'boolean.set', path: 'visible', value: false }
|
|
119
|
+
|
|
120
|
+
// OR bias (enable wins regardless of lamport)
|
|
121
|
+
{ key: 'cube-1', otype: 'boolean.or', path: 'enabled', value: true }
|
|
122
|
+
// Alice: true, Bob: false ā Result: true ā
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
## Collection Types
|
|
128
|
+
|
|
129
|
+
### Array
|
|
130
|
+
|
|
131
|
+
| otype | Merge | Example |
|
|
132
|
+
|-------|-------|---------|
|
|
133
|
+
| `array.set` | LWW (replace) | `{ otype: 'array.set', path: 'children', value: ['a', 'b'] }` |
|
|
134
|
+
| `array.push` | Append | `{ otype: 'array.push', path: 'children', value: 'c' }` |
|
|
135
|
+
| `array.remove` | Remove item | `{ otype: 'array.remove', path: 'children', value: 'a' }` |
|
|
136
|
+
| `array.union` | Union | `{ otype: 'array.union', path: 'tags', value: ['new'] }` |
|
|
137
|
+
|
|
138
|
+
**Examples:**
|
|
139
|
+
```typescript
|
|
140
|
+
// Replace entire array
|
|
141
|
+
{ key: 'scene', otype: 'array.set', path: 'children', value: ['cube-1', 'sphere-1'] }
|
|
142
|
+
|
|
143
|
+
// Append to array
|
|
144
|
+
{ key: 'scene', otype: 'array.push', path: 'children', value: 'light-1' }
|
|
145
|
+
|
|
146
|
+
// Remove from array
|
|
147
|
+
{ key: 'scene', otype: 'array.remove', path: 'children', value: 'cube-1' }
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Object
|
|
153
|
+
|
|
154
|
+
| otype | Merge | Example |
|
|
155
|
+
|-------|-------|---------|
|
|
156
|
+
| `object.set` | LWW (replace) | `{ otype: 'object.set', path: 'metadata', value: {...} }` |
|
|
157
|
+
| `object.merge` | Deep merge | `{ otype: 'object.merge', path: 'metadata', value: {...} }` |
|
|
158
|
+
|
|
159
|
+
---
|
|
160
|
+
|
|
161
|
+
## Compound Types (3D Graphics)
|
|
162
|
+
|
|
163
|
+
### Vector3 (Position, Scale)
|
|
164
|
+
|
|
165
|
+
| otype | Merge | Example |
|
|
166
|
+
|-------|-------|---------|
|
|
167
|
+
| `vector3.set` | LWW | `{ otype: 'vector3.set', path: 'transform.position', value: [0,5,0] }` |
|
|
168
|
+
| `vector3.add` | Component sum | `{ otype: 'vector3.add', path: 'transform.position', value: [5,0,0] }` |
|
|
169
|
+
| `vector3.multiply` | Component product | `{ otype: 'vector3.multiply', path: 'transform.scale', value: [2,2,2] }` |
|
|
170
|
+
|
|
171
|
+
**Examples:**
|
|
172
|
+
```typescript
|
|
173
|
+
// Absolute position (inspector input)
|
|
174
|
+
{ key: 'cube-1', otype: 'vector3.set', path: 'transform.position', value: [10, 5, 0] }
|
|
175
|
+
// Alice: [10, 5, 0] (lamport: 100), Bob: [0, 10, 0] (lamport: 101)
|
|
176
|
+
// Result: [0, 10, 0] ā
(Bob wins)
|
|
177
|
+
|
|
178
|
+
// Additive movement (drag)
|
|
179
|
+
{ key: 'cube-1', otype: 'vector3.add', path: 'transform.position', value: [5, 0, 0] }
|
|
180
|
+
// Alice: += [5, 0, 0], Bob: += [0, 3, 0]
|
|
181
|
+
// Result: += [5, 3, 0] ā
(both movements apply)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
### Quaternion (Rotation)
|
|
187
|
+
|
|
188
|
+
| otype | Merge | Example |
|
|
189
|
+
|-------|-------|---------|
|
|
190
|
+
| `quaternion.set` | LWW | `{ otype: 'quaternion.set', path: 'transform.rotation', value: [0,0,0,1] }` |
|
|
191
|
+
| `quaternion.multiply` | Composition | `{ otype: 'quaternion.multiply', path: 'transform.rotation', value: [...] }` |
|
|
192
|
+
|
|
193
|
+
**Note:** Use `quaternion.set` (LWW) for predictable UX. Quaternion composition is unpredictable for users.
|
|
194
|
+
|
|
195
|
+
---
|
|
196
|
+
|
|
197
|
+
### Color
|
|
198
|
+
|
|
199
|
+
| otype | Merge | Example |
|
|
200
|
+
|-------|-------|---------|
|
|
201
|
+
| `color.set` | LWW | `{ otype: 'color.set', path: 'color', value: '#ff0000' }` |
|
|
202
|
+
| `color.blend` | Average | `{ otype: 'color.blend', path: 'color', value: '#00ff00' }` |
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
## Scene Graph Operations
|
|
207
|
+
|
|
208
|
+
### node.insert
|
|
209
|
+
|
|
210
|
+
Create a new node in the scene graph.
|
|
211
|
+
|
|
212
|
+
```typescript
|
|
213
|
+
{
|
|
214
|
+
key: 'cube-1',
|
|
215
|
+
otype: 'node.insert',
|
|
216
|
+
path: 'cube-1',
|
|
217
|
+
value: {
|
|
218
|
+
id: 'uuid-cube-001',
|
|
219
|
+
tag: 'Mesh',
|
|
220
|
+
name: 'Red Cube',
|
|
221
|
+
color: '#ff0000',
|
|
222
|
+
'transform.position': [0, 0, 0],
|
|
223
|
+
'transform.rotation': [0, 0, 0, 1],
|
|
224
|
+
'transform.scale': [1, 1, 1]
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
**Behavior:**
|
|
230
|
+
- Adds node to `nodes` map: `nodes[key] = node`
|
|
231
|
+
- If key exists ā idempotent (no-op)
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### node.remove
|
|
236
|
+
|
|
237
|
+
Delete node (tombstone).
|
|
238
|
+
|
|
239
|
+
```typescript
|
|
240
|
+
{
|
|
241
|
+
key: 'cube-1',
|
|
242
|
+
otype: 'node.remove',
|
|
243
|
+
path: 'cube-1'
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
**Behavior:**
|
|
248
|
+
1. Set `node.deletedAt = timestamp` (tombstone)
|
|
249
|
+
2. Remove key from all parents' `children` arrays
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## Implementation Strategy
|
|
254
|
+
|
|
255
|
+
### Operation Registry Pattern
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
// operations/registry.ts
|
|
259
|
+
export { applyNumberSet } from './number.js';
|
|
260
|
+
export { applyNumberAdd } from './number.js';
|
|
261
|
+
export { applyVector3Set } from './vector3.js';
|
|
262
|
+
export { applyVector3Add } from './vector3.js';
|
|
263
|
+
export { applyArraySet } from './array.js';
|
|
264
|
+
export { applyArrayPush } from './array.js';
|
|
265
|
+
export { applyArrayRemove } from './array.js';
|
|
266
|
+
export { applyNodeInsert } from './node.js';
|
|
267
|
+
export { applyNodeRemove } from './node.js';
|
|
268
|
+
// ...
|
|
269
|
+
|
|
270
|
+
// index.ts
|
|
271
|
+
import * as registry from './operations/registry.js';
|
|
272
|
+
|
|
273
|
+
export type NumberSetOp = { otype: 'number.set'; ... };
|
|
274
|
+
export type NumberAddOp = { otype: 'number.add'; ... };
|
|
275
|
+
// ...
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### Apply Function Signature
|
|
279
|
+
|
|
280
|
+
```typescript
|
|
281
|
+
import { produce } from 'immer';
|
|
282
|
+
|
|
283
|
+
// Each apply function uses immer for immutable updates
|
|
284
|
+
export function applyNumberSet(
|
|
285
|
+
draft: SceneGraph,
|
|
286
|
+
op: NumberSetOp,
|
|
287
|
+
meta: OpMeta
|
|
288
|
+
): void {
|
|
289
|
+
const node = draft.nodes[op.key];
|
|
290
|
+
if (!node || node.deletedAt) return;
|
|
291
|
+
node[op.path] = op.value;
|
|
292
|
+
node.lamportTime = meta.lamportTime;
|
|
293
|
+
node.updatedAt = meta.timestamp;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export function applyNumberAdd(
|
|
297
|
+
draft: SceneGraph,
|
|
298
|
+
op: NumberAddOp,
|
|
299
|
+
meta: OpMeta
|
|
300
|
+
): void {
|
|
301
|
+
const node = draft.nodes[op.key];
|
|
302
|
+
if (!node || node.deletedAt) return;
|
|
303
|
+
const current = (node[op.path] as number) ?? 0;
|
|
304
|
+
node[op.path] = current + op.value;
|
|
305
|
+
node.lamportTime = meta.lamportTime;
|
|
306
|
+
node.updatedAt = meta.timestamp;
|
|
307
|
+
}
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
### Dispatcher
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { produce } from 'immer';
|
|
314
|
+
import * as registry from './operations/registry.js';
|
|
315
|
+
|
|
316
|
+
const handlers: Record<string, (draft: SceneGraph, op: Operation, meta: OpMeta) => void> = {
|
|
317
|
+
'number.set': registry.applyNumberSet,
|
|
318
|
+
'number.add': registry.applyNumberAdd,
|
|
319
|
+
'vector3.set': registry.applyVector3Set,
|
|
320
|
+
'vector3.add': registry.applyVector3Add,
|
|
321
|
+
'array.set': registry.applyArraySet,
|
|
322
|
+
'array.push': registry.applyArrayPush,
|
|
323
|
+
'array.remove': registry.applyArrayRemove,
|
|
324
|
+
'node.insert': registry.applyNodeInsert,
|
|
325
|
+
'node.remove': registry.applyNodeRemove,
|
|
326
|
+
// ...
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
export function applyMessage(graph: SceneGraph, msg: CRDTMessage): SceneGraph {
|
|
330
|
+
return produce(graph, (draft) => {
|
|
331
|
+
const meta = {
|
|
332
|
+
sessionId: msg.sessionId,
|
|
333
|
+
clock: msg.clock,
|
|
334
|
+
lamportTime: msg.lamportTime,
|
|
335
|
+
timestamp: msg.timestamp,
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
for (const op of msg.ops) {
|
|
339
|
+
const handler = handlers[op.otype];
|
|
340
|
+
if (handler) {
|
|
341
|
+
handler(draft, op as any, meta);
|
|
342
|
+
} else {
|
|
343
|
+
console.warn(`Unknown otype: ${op.otype}`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
```
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 01: Basic Usage
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how to create a scene and apply operations.
|
|
5
|
+
* Run with: npx tsx examples/01-basic-usage.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 01: Basic Usage\n');
|
|
12
|
+
|
|
13
|
+
// Start with an empty scene graph
|
|
14
|
+
let graph = createEmptyGraph();
|
|
15
|
+
console.log('Initial graph:', graph);
|
|
16
|
+
|
|
17
|
+
// ============================================
|
|
18
|
+
// Step 1: Create a scene node
|
|
19
|
+
// ============================================
|
|
20
|
+
|
|
21
|
+
const msg1: CRDTMessage = {
|
|
22
|
+
id: 'msg-001',
|
|
23
|
+
sessionId: 'session-server',
|
|
24
|
+
clock: { 'session-server': 1 },
|
|
25
|
+
lamportTime: 1,
|
|
26
|
+
timestamp: Date.now(),
|
|
27
|
+
ops: [
|
|
28
|
+
{
|
|
29
|
+
key: 'scene',
|
|
30
|
+
otype: 'node.insert',
|
|
31
|
+
path: 'scene',
|
|
32
|
+
value: {
|
|
33
|
+
id: 'uuid-scene-001',
|
|
34
|
+
tag: 'Scene',
|
|
35
|
+
name: 'My Scene',
|
|
36
|
+
background: '#87CEEB',
|
|
37
|
+
'transform.position': [0, 0, 0],
|
|
38
|
+
'transform.rotation': [0, 0, 0, 1],
|
|
39
|
+
'transform.scale': [1, 1, 1],
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
],
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
graph = applyMessage(graph, msg1);
|
|
46
|
+
console.log('\nā
After inserting scene:');
|
|
47
|
+
console.log(' Root key:', graph.rootKey);
|
|
48
|
+
console.log(' Scene node:', graph.nodes['scene']);
|
|
49
|
+
|
|
50
|
+
// ============================================
|
|
51
|
+
// Step 2: Add a cube to the scene
|
|
52
|
+
// ============================================
|
|
53
|
+
|
|
54
|
+
const msg2: CRDTMessage = {
|
|
55
|
+
id: 'msg-002',
|
|
56
|
+
sessionId: 'session-alice',
|
|
57
|
+
clock: { 'session-alice': 1 },
|
|
58
|
+
lamportTime: 2,
|
|
59
|
+
timestamp: Date.now(),
|
|
60
|
+
ops: [
|
|
61
|
+
// Insert the cube node and add to scene's children in one operation
|
|
62
|
+
{
|
|
63
|
+
key: 'cube-1',
|
|
64
|
+
otype: 'node.insert',
|
|
65
|
+
path: 'cube-1',
|
|
66
|
+
parent: 'scene', // Automatically adds to scene's children
|
|
67
|
+
value: {
|
|
68
|
+
id: 'uuid-cube-001',
|
|
69
|
+
tag: 'Mesh',
|
|
70
|
+
name: 'Red Cube',
|
|
71
|
+
color: '#ff0000',
|
|
72
|
+
'transform.position': [2, 1, 0],
|
|
73
|
+
'transform.rotation': [0, 0, 0, 1],
|
|
74
|
+
'transform.scale': [1, 1, 1],
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
],
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
graph = applyMessage(graph, msg2);
|
|
81
|
+
console.log('\nā
After adding cube:');
|
|
82
|
+
console.log(' Cube node:', graph.nodes['cube-1']);
|
|
83
|
+
console.log(' Scene children:', graph.nodes['scene'].children);
|
|
84
|
+
|
|
85
|
+
// ============================================
|
|
86
|
+
// Step 3: Move the cube (additive)
|
|
87
|
+
// ============================================
|
|
88
|
+
|
|
89
|
+
const msg3: CRDTMessage = {
|
|
90
|
+
id: 'msg-003',
|
|
91
|
+
sessionId: 'session-alice',
|
|
92
|
+
clock: { 'session-alice': 2 },
|
|
93
|
+
lamportTime: 3,
|
|
94
|
+
timestamp: Date.now(),
|
|
95
|
+
ops: [
|
|
96
|
+
{
|
|
97
|
+
key: 'cube-1',
|
|
98
|
+
otype: 'vector3.add',
|
|
99
|
+
path: 'transform.position',
|
|
100
|
+
value: [5, 0, 0], // Move right by 5
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
graph = applyMessage(graph, msg3);
|
|
106
|
+
console.log('\nā
After moving cube (additive):');
|
|
107
|
+
console.log(' Cube position:', graph.nodes['cube-1']['transform.position']);
|
|
108
|
+
// Expected: [7, 1, 0] (original [2,1,0] + delta [5,0,0])
|
|
109
|
+
|
|
110
|
+
// ============================================
|
|
111
|
+
// Step 4: Change cube color
|
|
112
|
+
// ============================================
|
|
113
|
+
|
|
114
|
+
const msg4: CRDTMessage = {
|
|
115
|
+
id: 'msg-004',
|
|
116
|
+
sessionId: 'session-alice',
|
|
117
|
+
clock: { 'session-alice': 3 },
|
|
118
|
+
lamportTime: 4,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
ops: [
|
|
121
|
+
{
|
|
122
|
+
key: 'cube-1',
|
|
123
|
+
otype: 'color.set',
|
|
124
|
+
path: 'color',
|
|
125
|
+
value: '#00ff00', // Change to green
|
|
126
|
+
},
|
|
127
|
+
],
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
graph = applyMessage(graph, msg4);
|
|
131
|
+
console.log('\nā
After changing color:');
|
|
132
|
+
console.log(' Cube color:', graph.nodes['cube-1'].color);
|
|
133
|
+
|
|
134
|
+
// ============================================
|
|
135
|
+
// Final state
|
|
136
|
+
// ============================================
|
|
137
|
+
|
|
138
|
+
console.log('\nš Final Scene Graph:');
|
|
139
|
+
console.log(JSON.stringify(graph, null, 2));
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Example 02: Concurrent Edits
|
|
3
|
+
*
|
|
4
|
+
* Demonstrates how CRDT handles concurrent edits from multiple users.
|
|
5
|
+
* Run with: npx tsx examples/02-concurrent-edits.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 02: Concurrent Edits\n');
|
|
12
|
+
console.log('Scenario: Alice and Bob both edit the same cube at the same time.\n');
|
|
13
|
+
|
|
14
|
+
// Setup: Create a scene with a cube
|
|
15
|
+
let graph = createEmptyGraph();
|
|
16
|
+
|
|
17
|
+
const setupMsg: CRDTMessage = {
|
|
18
|
+
id: 'setup',
|
|
19
|
+
sessionId: 'server',
|
|
20
|
+
clock: { server: 1 },
|
|
21
|
+
lamportTime: 0,
|
|
22
|
+
timestamp: Date.now(),
|
|
23
|
+
ops: [
|
|
24
|
+
{
|
|
25
|
+
key: 'cube',
|
|
26
|
+
otype: 'node.insert',
|
|
27
|
+
path: 'cube',
|
|
28
|
+
value: {
|
|
29
|
+
key: 'uuid-cube',
|
|
30
|
+
tag: 'Mesh',
|
|
31
|
+
name: 'Shared Cube',
|
|
32
|
+
color: '#ffffff',
|
|
33
|
+
score: 0,
|
|
34
|
+
'transform.position': [0, 0, 0],
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
graph = applyMessage(graph, setupMsg);
|
|
41
|
+
console.log('Initial cube position:', graph.nodes['cube']['transform.position']);
|
|
42
|
+
console.log('Initial cube score:', graph.nodes['cube'].score);
|
|
43
|
+
|
|
44
|
+
// ============================================
|
|
45
|
+
// Scenario 1: Concurrent Drag (Additive)
|
|
46
|
+
// ============================================
|
|
47
|
+
|
|
48
|
+
console.log('\n--- Scenario 1: Concurrent Drag (vector3.add) ---\n');
|
|
49
|
+
|
|
50
|
+
// Alice drags the cube right by [5, 0, 0]
|
|
51
|
+
const aliceDrag: CRDTMessage = {
|
|
52
|
+
id: 'alice-drag',
|
|
53
|
+
sessionId: 'alice',
|
|
54
|
+
clock: { alice: 1 },
|
|
55
|
+
lamportTime: 10,
|
|
56
|
+
timestamp: Date.now(),
|
|
57
|
+
ops: [
|
|
58
|
+
{
|
|
59
|
+
key: 'cube',
|
|
60
|
+
otype: 'vector3.add', // Additive!
|
|
61
|
+
path: 'transform.position',
|
|
62
|
+
value: [5, 0, 0],
|
|
63
|
+
},
|
|
64
|
+
],
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
// Bob drags the cube up by [0, 3, 0] (concurrent with Alice)
|
|
68
|
+
const bobDrag: CRDTMessage = {
|
|
69
|
+
id: 'bob-drag',
|
|
70
|
+
sessionId: 'bob',
|
|
71
|
+
clock: { bob: 1 },
|
|
72
|
+
lamportTime: 11,
|
|
73
|
+
timestamp: Date.now(),
|
|
74
|
+
ops: [
|
|
75
|
+
{
|
|
76
|
+
key: 'cube',
|
|
77
|
+
otype: 'vector3.add', // Additive!
|
|
78
|
+
path: 'transform.position',
|
|
79
|
+
value: [0, 3, 0],
|
|
80
|
+
},
|
|
81
|
+
],
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
console.log('Alice drags: += [5, 0, 0]');
|
|
85
|
+
console.log('Bob drags: += [0, 3, 0]');
|
|
86
|
+
|
|
87
|
+
// Apply both (order doesn't matter for additive!)
|
|
88
|
+
graph = applyMessage(graph, aliceDrag);
|
|
89
|
+
graph = applyMessage(graph, bobDrag);
|
|
90
|
+
|
|
91
|
+
console.log('\nā
Result: Both movements applied!');
|
|
92
|
+
console.log('Final position:', graph.nodes['cube']['transform.position']);
|
|
93
|
+
console.log('Expected: [5, 3, 0]');
|
|
94
|
+
|
|
95
|
+
// ============================================
|
|
96
|
+
// Scenario 2: Concurrent Score Update (Additive)
|
|
97
|
+
// ============================================
|
|
98
|
+
|
|
99
|
+
console.log('\n--- Scenario 2: Concurrent Score Update (number.add) ---\n');
|
|
100
|
+
|
|
101
|
+
// Alice adds 10 points
|
|
102
|
+
const aliceScore: CRDTMessage = {
|
|
103
|
+
id: 'alice-score',
|
|
104
|
+
sessionId: 'alice',
|
|
105
|
+
clock: { alice: 2 },
|
|
106
|
+
lamportTime: 20,
|
|
107
|
+
timestamp: Date.now(),
|
|
108
|
+
ops: [
|
|
109
|
+
{
|
|
110
|
+
key: 'cube',
|
|
111
|
+
otype: 'number.add',
|
|
112
|
+
path: 'score',
|
|
113
|
+
value: 10,
|
|
114
|
+
},
|
|
115
|
+
],
|
|
116
|
+
};
|
|
117
|
+
|
|
118
|
+
// Bob adds 5 points (concurrent)
|
|
119
|
+
const bobScore: CRDTMessage = {
|
|
120
|
+
id: 'bob-score',
|
|
121
|
+
sessionId: 'bob',
|
|
122
|
+
clock: { bob: 2 },
|
|
123
|
+
lamportTime: 21,
|
|
124
|
+
timestamp: Date.now(),
|
|
125
|
+
ops: [
|
|
126
|
+
{
|
|
127
|
+
key: 'cube',
|
|
128
|
+
otype: 'number.add',
|
|
129
|
+
path: 'score',
|
|
130
|
+
value: 5,
|
|
131
|
+
},
|
|
132
|
+
],
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
console.log('Alice: score += 10');
|
|
136
|
+
console.log('Bob: score += 5');
|
|
137
|
+
|
|
138
|
+
graph = applyMessage(graph, aliceScore);
|
|
139
|
+
graph = applyMessage(graph, bobScore);
|
|
140
|
+
|
|
141
|
+
console.log('\nā
Result: Both scores accumulated!');
|
|
142
|
+
console.log('Final score:', graph.nodes['cube'].score);
|
|
143
|
+
console.log('Expected: 15');
|
|
144
|
+
|
|
145
|
+
// ============================================
|
|
146
|
+
// Scenario 3: Concurrent Color Change (LWW)
|
|
147
|
+
// ============================================
|
|
148
|
+
|
|
149
|
+
console.log('\n--- Scenario 3: Concurrent Color Change (color.set - LWW) ---\n');
|
|
150
|
+
|
|
151
|
+
// Alice sets color to red (lamport: 30)
|
|
152
|
+
const aliceColor: CRDTMessage = {
|
|
153
|
+
id: 'alice-color',
|
|
154
|
+
sessionId: 'alice',
|
|
155
|
+
clock: { alice: 3 },
|
|
156
|
+
lamportTime: 30,
|
|
157
|
+
timestamp: Date.now(),
|
|
158
|
+
ops: [
|
|
159
|
+
{
|
|
160
|
+
key: 'cube',
|
|
161
|
+
otype: 'color.set', // LWW!
|
|
162
|
+
path: 'color',
|
|
163
|
+
value: '#ff0000',
|
|
164
|
+
},
|
|
165
|
+
],
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Bob sets color to blue (lamport: 31 - higher, so Bob wins)
|
|
169
|
+
const bobColor: CRDTMessage = {
|
|
170
|
+
id: 'bob-color',
|
|
171
|
+
sessionId: 'bob',
|
|
172
|
+
clock: { bob: 3 },
|
|
173
|
+
lamportTime: 31, // Higher lamport time
|
|
174
|
+
timestamp: Date.now(),
|
|
175
|
+
ops: [
|
|
176
|
+
{
|
|
177
|
+
key: 'cube',
|
|
178
|
+
otype: 'color.set', // LWW!
|
|
179
|
+
path: 'color',
|
|
180
|
+
value: '#0000ff',
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
console.log('Alice: color = #ff0000 (red) [lamport: 30]');
|
|
186
|
+
console.log('Bob: color = #0000ff (blue) [lamport: 31]');
|
|
187
|
+
|
|
188
|
+
graph = applyMessage(graph, aliceColor);
|
|
189
|
+
graph = applyMessage(graph, bobColor);
|
|
190
|
+
|
|
191
|
+
console.log('\nā
Result: Bob wins (higher lamport time)!');
|
|
192
|
+
console.log('Final color:', graph.nodes['cube'].color);
|
|
193
|
+
console.log('Expected: #0000ff (blue)');
|
|
194
|
+
|
|
195
|
+
// ============================================
|
|
196
|
+
// Summary
|
|
197
|
+
// ============================================
|
|
198
|
+
|
|
199
|
+
console.log('\nš Summary:');
|
|
200
|
+
console.log('ā'.repeat(50));
|
|
201
|
+
console.log('Additive operations (*.add): Values are SUMMED');
|
|
202
|
+
console.log(' ā Great for: drag, counters, deltas');
|
|
203
|
+
console.log(' ā Order doesn\'t matter!');
|
|
204
|
+
console.log('');
|
|
205
|
+
console.log('LWW operations (*.set): Last write wins');
|
|
206
|
+
console.log(' ā Great for: colors, absolute positions, names');
|
|
207
|
+
console.log(' ā Higher lamportTime wins');
|
|
208
|
+
console.log('ā'.repeat(50));
|