@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,340 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation Types - New design with CRDT wrapper and dtype operations
|
|
3
|
+
*
|
|
4
|
+
* Design:
|
|
5
|
+
* - CRDTMessage: Envelope with CRDT metadata + batch of operations
|
|
6
|
+
* - Operations: Explicit dtype operations (number.set, vector3.add, etc.)
|
|
7
|
+
* - Batching: One message can contain multiple operations on multiple nodes
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { VectorClock } from '../state/VectorClock.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* CRDTMessage - Envelope containing CRDT metadata + batch of operations
|
|
14
|
+
*/
|
|
15
|
+
export interface CRDTMessage {
|
|
16
|
+
// === CRDT Metadata (wrapper) ===
|
|
17
|
+
id: string; // Message ID (UUID)
|
|
18
|
+
sessionId: string; // Session that created this message
|
|
19
|
+
clock: VectorClock; // Vector clock for causal ordering
|
|
20
|
+
lamportTime: number; // Lamport timestamp for total ordering
|
|
21
|
+
timestamp: number; // Wall-clock time (milliseconds since epoch)
|
|
22
|
+
|
|
23
|
+
// === Operations (batch) ===
|
|
24
|
+
ops: Operation[]; // One or more operations
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Base Operation - All operations extend this
|
|
29
|
+
*/
|
|
30
|
+
export interface BaseOp {
|
|
31
|
+
key: string; // Node key (human-friendly, e.g., 'cube-1', 'player', 'scene')
|
|
32
|
+
otype: string; // dtype operation: 'number.set', 'vector3.add', etc.
|
|
33
|
+
path: string; // Property path: 'color', 'transform.position', etc.
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// ============================================
|
|
37
|
+
// NUMBER Operations
|
|
38
|
+
// ============================================
|
|
39
|
+
|
|
40
|
+
export interface NumberSetOp extends BaseOp {
|
|
41
|
+
key: string;
|
|
42
|
+
otype: 'number.set';
|
|
43
|
+
path: string;
|
|
44
|
+
value: number;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface NumberAddOp extends BaseOp {
|
|
48
|
+
key: string;
|
|
49
|
+
otype: 'number.add';
|
|
50
|
+
path: string;
|
|
51
|
+
value: number;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface NumberMultiplyOp extends BaseOp {
|
|
55
|
+
key: string;
|
|
56
|
+
otype: 'number.multiply';
|
|
57
|
+
path: string;
|
|
58
|
+
value: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export interface NumberMinOp extends BaseOp {
|
|
62
|
+
key: string;
|
|
63
|
+
otype: 'number.min';
|
|
64
|
+
path: string;
|
|
65
|
+
value: number;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface NumberMaxOp extends BaseOp {
|
|
69
|
+
key: string;
|
|
70
|
+
otype: 'number.max';
|
|
71
|
+
path: string;
|
|
72
|
+
value: number;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ============================================
|
|
76
|
+
// STRING Operations
|
|
77
|
+
// ============================================
|
|
78
|
+
|
|
79
|
+
export interface StringSetOp extends BaseOp {
|
|
80
|
+
key: string;
|
|
81
|
+
otype: 'string.set';
|
|
82
|
+
path: string;
|
|
83
|
+
value: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface StringConcatOp extends BaseOp {
|
|
87
|
+
key: string;
|
|
88
|
+
otype: 'string.concat';
|
|
89
|
+
path: string;
|
|
90
|
+
value: string;
|
|
91
|
+
separator?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// ============================================
|
|
95
|
+
// BOOLEAN Operations
|
|
96
|
+
// ============================================
|
|
97
|
+
|
|
98
|
+
export interface BooleanSetOp extends BaseOp {
|
|
99
|
+
key: string;
|
|
100
|
+
otype: 'boolean.set';
|
|
101
|
+
path: string;
|
|
102
|
+
value: boolean;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export interface BooleanOrOp extends BaseOp {
|
|
106
|
+
key: string;
|
|
107
|
+
otype: 'boolean.or';
|
|
108
|
+
path: string;
|
|
109
|
+
value: boolean;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface BooleanAndOp extends BaseOp {
|
|
113
|
+
key: string;
|
|
114
|
+
otype: 'boolean.and';
|
|
115
|
+
path: string;
|
|
116
|
+
value: boolean;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ============================================
|
|
120
|
+
// VECTOR3 Operations
|
|
121
|
+
// ============================================
|
|
122
|
+
|
|
123
|
+
export interface Vector3SetOp extends BaseOp {
|
|
124
|
+
key: string;
|
|
125
|
+
otype: 'vector3.set';
|
|
126
|
+
path: string;
|
|
127
|
+
value: [number, number, number];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export interface Vector3AddOp extends BaseOp {
|
|
131
|
+
key: string;
|
|
132
|
+
otype: 'vector3.add';
|
|
133
|
+
path: string;
|
|
134
|
+
value: [number, number, number];
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface Vector3MultiplyOp extends BaseOp {
|
|
138
|
+
key: string;
|
|
139
|
+
otype: 'vector3.multiply';
|
|
140
|
+
path: string;
|
|
141
|
+
value: [number, number, number];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================
|
|
145
|
+
// QUATERNION Operations
|
|
146
|
+
// ============================================
|
|
147
|
+
|
|
148
|
+
export interface QuaternionSetOp extends BaseOp {
|
|
149
|
+
key: string;
|
|
150
|
+
otype: 'quaternion.set';
|
|
151
|
+
path: string;
|
|
152
|
+
value: [number, number, number, number];
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface QuaternionMultiplyOp extends BaseOp {
|
|
156
|
+
key: string;
|
|
157
|
+
otype: 'quaternion.multiply';
|
|
158
|
+
path: string;
|
|
159
|
+
value: [number, number, number, number];
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ============================================
|
|
163
|
+
// COLOR Operations
|
|
164
|
+
// ============================================
|
|
165
|
+
|
|
166
|
+
export interface ColorSetOp extends BaseOp {
|
|
167
|
+
key: string;
|
|
168
|
+
otype: 'color.set';
|
|
169
|
+
path: string;
|
|
170
|
+
value: string; // Hex color
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface ColorBlendOp extends BaseOp {
|
|
174
|
+
key: string;
|
|
175
|
+
otype: 'color.blend';
|
|
176
|
+
path: string;
|
|
177
|
+
value: string;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// ============================================
|
|
181
|
+
// ARRAY Operations
|
|
182
|
+
// ============================================
|
|
183
|
+
|
|
184
|
+
export interface ArraySetOp extends BaseOp {
|
|
185
|
+
key: string;
|
|
186
|
+
otype: 'array.set';
|
|
187
|
+
path: string;
|
|
188
|
+
value: any[];
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export interface ArrayPushOp extends BaseOp {
|
|
192
|
+
key: string;
|
|
193
|
+
otype: 'array.push';
|
|
194
|
+
path: string;
|
|
195
|
+
value: any;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export interface ArrayUnionOp extends BaseOp {
|
|
199
|
+
key: string;
|
|
200
|
+
otype: 'array.union';
|
|
201
|
+
path: string;
|
|
202
|
+
value: any[];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export interface ArrayRemoveOp extends BaseOp {
|
|
206
|
+
key: string;
|
|
207
|
+
otype: 'array.remove';
|
|
208
|
+
path: string;
|
|
209
|
+
value: any;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// ============================================
|
|
213
|
+
// OBJECT Operations
|
|
214
|
+
// ============================================
|
|
215
|
+
|
|
216
|
+
export interface ObjectSetOp extends BaseOp {
|
|
217
|
+
key: string;
|
|
218
|
+
otype: 'object.set';
|
|
219
|
+
path: string;
|
|
220
|
+
value: Record<string, any>;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export interface ObjectMergeOp extends BaseOp {
|
|
224
|
+
key: string;
|
|
225
|
+
otype: 'object.merge';
|
|
226
|
+
path: string;
|
|
227
|
+
value: Record<string, any>;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ============================================
|
|
231
|
+
// SCENE GRAPH Operations
|
|
232
|
+
// ============================================
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* NODE.INSERT - Create new node as child of parent
|
|
236
|
+
*/
|
|
237
|
+
export interface NodeInsertOp extends BaseOp {
|
|
238
|
+
key: string; // Parent node's key
|
|
239
|
+
otype: 'node.insert';
|
|
240
|
+
path: 'children';
|
|
241
|
+
value: {
|
|
242
|
+
key: string; // New node's key
|
|
243
|
+
id: string; // Node's UUID (for CRDT)
|
|
244
|
+
tag: string; // 'Scene' | 'Mesh' | 'Group' | ...
|
|
245
|
+
name: string;
|
|
246
|
+
[key: string]: any; // Initial properties
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* NODE.REMOVE - Remove node from parent (tombstone)
|
|
252
|
+
*/
|
|
253
|
+
export interface NodeRemoveOp extends BaseOp {
|
|
254
|
+
key: string; // Parent node's key
|
|
255
|
+
otype: 'node.remove';
|
|
256
|
+
path: 'children';
|
|
257
|
+
value: string; // Node key to remove
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
// ============================================
|
|
261
|
+
// META Operations (undo/redo)
|
|
262
|
+
// ============================================
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* META.UNDO - Mark a message as undone
|
|
266
|
+
*/
|
|
267
|
+
export interface MetaUndoOp {
|
|
268
|
+
key: '_meta';
|
|
269
|
+
otype: 'meta.undo';
|
|
270
|
+
path: '_meta';
|
|
271
|
+
targetMsgId: string; // Message ID to undo
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* META.REDO - Unmark a message as undone
|
|
276
|
+
*/
|
|
277
|
+
export interface MetaRedoOp {
|
|
278
|
+
key: '_meta';
|
|
279
|
+
otype: 'meta.redo';
|
|
280
|
+
path: '_meta';
|
|
281
|
+
targetMsgId: string; // Message ID to redo
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ============================================
|
|
285
|
+
// Union of all operations
|
|
286
|
+
// ============================================
|
|
287
|
+
|
|
288
|
+
export type Operation =
|
|
289
|
+
| NumberSetOp | NumberAddOp | NumberMultiplyOp | NumberMinOp | NumberMaxOp
|
|
290
|
+
| StringSetOp | StringConcatOp
|
|
291
|
+
| BooleanSetOp | BooleanOrOp | BooleanAndOp
|
|
292
|
+
| Vector3SetOp | Vector3AddOp | Vector3MultiplyOp
|
|
293
|
+
| QuaternionSetOp | QuaternionMultiplyOp
|
|
294
|
+
| ColorSetOp | ColorBlendOp
|
|
295
|
+
| ArraySetOp | ArrayPushOp | ArrayUnionOp | ArrayRemoveOp
|
|
296
|
+
| ObjectSetOp | ObjectMergeOp
|
|
297
|
+
| NodeInsertOp | NodeRemoveOp
|
|
298
|
+
| MetaUndoOp | MetaRedoOp;
|
|
299
|
+
|
|
300
|
+
// ============================================
|
|
301
|
+
// Scene Graph (state representation)
|
|
302
|
+
// ============================================
|
|
303
|
+
|
|
304
|
+
/**
|
|
305
|
+
* SceneNode - Node in the flattened scene graph
|
|
306
|
+
*
|
|
307
|
+
* Properties are stored directly on the node with dot-notation paths:
|
|
308
|
+
* - 'transform.position': [0, 0, 0]
|
|
309
|
+
* - 'color': '#ff0000'
|
|
310
|
+
* - 'opacity': 0.5
|
|
311
|
+
*/
|
|
312
|
+
export interface SceneNode {
|
|
313
|
+
// Identity
|
|
314
|
+
id: string; // UUID (used for CRDT)
|
|
315
|
+
key: string; // Map key (human-friendly, unique in scene)
|
|
316
|
+
tag: string; // Node type
|
|
317
|
+
name: string; // Display name
|
|
318
|
+
|
|
319
|
+
// Tree structure
|
|
320
|
+
children: string[]; // Child node keys (not IDs!)
|
|
321
|
+
|
|
322
|
+
// CRDT metadata
|
|
323
|
+
clock: VectorClock;
|
|
324
|
+
lamportTime: number;
|
|
325
|
+
createdAt: number;
|
|
326
|
+
updatedAt: number;
|
|
327
|
+
deletedAt?: number; // Tombstone for soft delete
|
|
328
|
+
|
|
329
|
+
// Dynamic properties (flat with dot-notation paths)
|
|
330
|
+
// e.g., 'transform.position', 'color', 'opacity'
|
|
331
|
+
[path: string]: unknown;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* SceneGraph - Complete scene representation
|
|
336
|
+
*/
|
|
337
|
+
export interface SceneGraph {
|
|
338
|
+
nodes: Record<string, SceneNode>; // Flattened map by key
|
|
339
|
+
rootKey: string; // Root node key
|
|
340
|
+
}
|
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Operation Validator - Validates CRDTMessage and operations
|
|
3
|
+
*
|
|
4
|
+
* Validates:
|
|
5
|
+
* - CRDTMessage envelope (CRDT metadata)
|
|
6
|
+
* - Individual operations (dtype operations)
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { CRDTMessage, Operation } from './OperationTypes.js';
|
|
10
|
+
|
|
11
|
+
export interface ValidationResult {
|
|
12
|
+
valid: boolean;
|
|
13
|
+
errors?: string[];
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export class OperationValidator {
|
|
17
|
+
/**
|
|
18
|
+
* Validate a CRDTMessage (envelope + operations)
|
|
19
|
+
*/
|
|
20
|
+
validateMessage(msg: any): ValidationResult {
|
|
21
|
+
const errors: string[] = [];
|
|
22
|
+
|
|
23
|
+
// Validate envelope
|
|
24
|
+
if (!msg.id || typeof msg.id !== 'string') {
|
|
25
|
+
errors.push('id is required and must be a string');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!msg.sessionId || typeof msg.sessionId !== 'string') {
|
|
29
|
+
errors.push('sessionId is required and must be a string');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!msg.clock || typeof msg.clock !== 'object') {
|
|
33
|
+
errors.push('clock must be an object');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (typeof msg.lamportTime !== 'number') {
|
|
37
|
+
errors.push('lamportTime must be a number');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof msg.timestamp !== 'number') {
|
|
41
|
+
errors.push('timestamp must be a number');
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (!Array.isArray(msg.ops)) {
|
|
45
|
+
errors.push('ops must be an array');
|
|
46
|
+
return { valid: false, errors };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (msg.ops.length === 0) {
|
|
50
|
+
errors.push('ops array cannot be empty');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Validate each operation
|
|
54
|
+
msg.ops.forEach((op: any, index: number) => {
|
|
55
|
+
const opErrors = this.validateOperation(op);
|
|
56
|
+
if (!opErrors.valid) {
|
|
57
|
+
errors.push(`op[${index}]: ${opErrors.errors?.join(', ')}`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Validate a single operation
|
|
66
|
+
*/
|
|
67
|
+
validateOperation(op: any): ValidationResult {
|
|
68
|
+
const errors: string[] = [];
|
|
69
|
+
|
|
70
|
+
// Validate base fields
|
|
71
|
+
if (!op.key || typeof op.key !== 'string') {
|
|
72
|
+
errors.push('key is required and must be a string');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!op.otype || typeof op.otype !== 'string') {
|
|
76
|
+
errors.push('otype is required and must be a string');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (!op.path || typeof op.path !== 'string') {
|
|
80
|
+
errors.push('path is required and must be a string');
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Validate based on otype
|
|
84
|
+
if (op.otype) {
|
|
85
|
+
const [dtype, operation] = op.otype.split('.');
|
|
86
|
+
|
|
87
|
+
switch (dtype) {
|
|
88
|
+
case 'number':
|
|
89
|
+
this.validateNumberOp(op, operation, errors);
|
|
90
|
+
break;
|
|
91
|
+
case 'string':
|
|
92
|
+
this.validateStringOp(op, operation, errors);
|
|
93
|
+
break;
|
|
94
|
+
case 'boolean':
|
|
95
|
+
this.validateBooleanOp(op, operation, errors);
|
|
96
|
+
break;
|
|
97
|
+
case 'vector3':
|
|
98
|
+
this.validateVector3Op(op, operation, errors);
|
|
99
|
+
break;
|
|
100
|
+
case 'quaternion':
|
|
101
|
+
this.validateQuaternionOp(op, operation, errors);
|
|
102
|
+
break;
|
|
103
|
+
case 'color':
|
|
104
|
+
this.validateColorOp(op, operation, errors);
|
|
105
|
+
break;
|
|
106
|
+
case 'array':
|
|
107
|
+
this.validateArrayOp(op, operation, errors);
|
|
108
|
+
break;
|
|
109
|
+
case 'object':
|
|
110
|
+
this.validateObjectOp(op, operation, errors);
|
|
111
|
+
break;
|
|
112
|
+
case 'node':
|
|
113
|
+
this.validateNodeOp(op, operation, errors);
|
|
114
|
+
break;
|
|
115
|
+
case 'meta':
|
|
116
|
+
this.validateMetaOp(op, operation, errors);
|
|
117
|
+
break;
|
|
118
|
+
default:
|
|
119
|
+
errors.push(`Unknown dtype: ${dtype}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return errors.length > 0 ? { valid: false, errors } : { valid: true };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
private validateNumberOp(op: any, operation: string, errors: string[]): void {
|
|
127
|
+
if (!['set', 'add', 'multiply', 'min', 'max'].includes(operation)) {
|
|
128
|
+
errors.push(`Invalid number operation: ${operation}`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (typeof op.value !== 'number') {
|
|
132
|
+
errors.push('number operation requires value to be a number');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
private validateStringOp(op: any, operation: string, errors: string[]): void {
|
|
137
|
+
if (!['set', 'concat'].includes(operation)) {
|
|
138
|
+
errors.push(`Invalid string operation: ${operation}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (typeof op.value !== 'string') {
|
|
142
|
+
errors.push('string operation requires value to be a string');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private validateBooleanOp(op: any, operation: string, errors: string[]): void {
|
|
147
|
+
if (!['set', 'or', 'and'].includes(operation)) {
|
|
148
|
+
errors.push(`Invalid boolean operation: ${operation}`);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (typeof op.value !== 'boolean') {
|
|
152
|
+
errors.push('boolean operation requires value to be a boolean');
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private validateVector3Op(op: any, operation: string, errors: string[]): void {
|
|
157
|
+
if (!['set', 'add', 'multiply'].includes(operation)) {
|
|
158
|
+
errors.push(`Invalid vector3 operation: ${operation}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
!Array.isArray(op.value) ||
|
|
163
|
+
op.value.length !== 3 ||
|
|
164
|
+
!op.value.every((v: any) => typeof v === 'number')
|
|
165
|
+
) {
|
|
166
|
+
errors.push('vector3 operation requires value to be [number, number, number]');
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private validateQuaternionOp(op: any, operation: string, errors: string[]): void {
|
|
171
|
+
if (!['set', 'multiply'].includes(operation)) {
|
|
172
|
+
errors.push(`Invalid quaternion operation: ${operation}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (
|
|
176
|
+
!Array.isArray(op.value) ||
|
|
177
|
+
op.value.length !== 4 ||
|
|
178
|
+
!op.value.every((v: any) => typeof v === 'number')
|
|
179
|
+
) {
|
|
180
|
+
errors.push('quaternion operation requires value to be [number, number, number, number]');
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
private validateColorOp(op: any, operation: string, errors: string[]): void {
|
|
185
|
+
if (!['set', 'blend'].includes(operation)) {
|
|
186
|
+
errors.push(`Invalid color operation: ${operation}`);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (typeof op.value !== 'string') {
|
|
190
|
+
errors.push('color operation requires value to be a string');
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private validateArrayOp(op: any, operation: string, errors: string[]): void {
|
|
195
|
+
if (!['set', 'push', 'union', 'remove'].includes(operation)) {
|
|
196
|
+
errors.push(`Invalid array operation: ${operation}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (operation === 'set' || operation === 'union') {
|
|
200
|
+
if (!Array.isArray(op.value)) {
|
|
201
|
+
errors.push(`array.${operation} requires value to be an array`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
// push and remove can be any value
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
private validateObjectOp(op: any, operation: string, errors: string[]): void {
|
|
208
|
+
if (!['set', 'merge'].includes(operation)) {
|
|
209
|
+
errors.push(`Invalid object operation: ${operation}`);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (typeof op.value !== 'object' || Array.isArray(op.value)) {
|
|
213
|
+
errors.push('object operation requires value to be an object');
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private validateNodeOp(op: any, operation: string, errors: string[]): void {
|
|
218
|
+
if (!['insert', 'remove'].includes(operation)) {
|
|
219
|
+
errors.push(`Invalid node operation: ${operation}`);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (operation === 'insert') {
|
|
223
|
+
if (!op.value || typeof op.value !== 'object') {
|
|
224
|
+
errors.push('node.insert requires value to be an object');
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (!op.value.id || typeof op.value.id !== 'string') {
|
|
229
|
+
errors.push('node.insert requires value.id (string)');
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!op.value.tag || typeof op.value.tag !== 'string') {
|
|
233
|
+
errors.push('node.insert requires value.tag (string)');
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!op.value.name || typeof op.value.name !== 'string') {
|
|
237
|
+
errors.push('node.insert requires value.name (string)');
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
// node.remove doesn't have value
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
private validateMetaOp(op: any, operation: string, errors: string[]): void {
|
|
244
|
+
if (!['undo', 'redo'].includes(operation)) {
|
|
245
|
+
errors.push(`Invalid meta operation: ${operation}`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (op.key !== '_meta') {
|
|
249
|
+
errors.push('meta operation requires key to be "_meta"');
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (op.path !== '_meta') {
|
|
253
|
+
errors.push('meta operation requires path to be "_meta"');
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (!op.targetMsgId || typeof op.targetMsgId !== 'string') {
|
|
257
|
+
errors.push('meta operation requires targetMsgId (string)');
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Array operations: set, push, remove, union
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { SceneGraph, ArraySetOp, ArrayPushOp, ArrayRemoveOp, ArrayUnionOp } from '../OperationTypes.js';
|
|
6
|
+
import type { OpMeta } from './types.js';
|
|
7
|
+
import { getNodeProperty, setNodeProperty, setNodePropertyLWW } from './types.js';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* array.set - Last-Write-Wins (replace the entire array)
|
|
11
|
+
*/
|
|
12
|
+
export function ArraySet(
|
|
13
|
+
draft: SceneGraph,
|
|
14
|
+
op: ArraySetOp,
|
|
15
|
+
meta: OpMeta
|
|
16
|
+
): void {
|
|
17
|
+
const node = draft.nodes[op.key];
|
|
18
|
+
if (!node || node.deletedAt) return;
|
|
19
|
+
setNodePropertyLWW(node, op.path, [...op.value], meta);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* array.push - Append item to an array
|
|
24
|
+
*/
|
|
25
|
+
export function ArrayPush(
|
|
26
|
+
draft: SceneGraph,
|
|
27
|
+
op: ArrayPushOp,
|
|
28
|
+
meta: OpMeta
|
|
29
|
+
): void {
|
|
30
|
+
const node = draft.nodes[op.key];
|
|
31
|
+
if (!node || node.deletedAt) return;
|
|
32
|
+
const current = getNodeProperty<unknown[]>(node, op.path, []);
|
|
33
|
+
setNodeProperty(node, op.path, [...current, op.value], meta);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* array.remove - Remove item from array (by value equality)
|
|
38
|
+
*/
|
|
39
|
+
export function ArrayRemove(
|
|
40
|
+
draft: SceneGraph,
|
|
41
|
+
op: ArrayRemoveOp,
|
|
42
|
+
meta: OpMeta
|
|
43
|
+
): void {
|
|
44
|
+
const node = draft.nodes[op.key];
|
|
45
|
+
if (!node || node.deletedAt) return;
|
|
46
|
+
const current = getNodeProperty<unknown[]>(node, op.path, []);
|
|
47
|
+
const filtered = current.filter(item => {
|
|
48
|
+
// Handle primitive equality and object reference
|
|
49
|
+
if (typeof item === 'object' && typeof op.value === 'object') {
|
|
50
|
+
return JSON.stringify(item) !== JSON.stringify(op.value);
|
|
51
|
+
}
|
|
52
|
+
return item !== op.value;
|
|
53
|
+
});
|
|
54
|
+
setNodeProperty(node, op.path, filtered, meta);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* array.union - Union of arrays (set-like, no duplicates)
|
|
59
|
+
*/
|
|
60
|
+
export function ArrayUnion(
|
|
61
|
+
draft: SceneGraph,
|
|
62
|
+
op: ArrayUnionOp,
|
|
63
|
+
meta: OpMeta
|
|
64
|
+
): void {
|
|
65
|
+
const node = draft.nodes[op.key];
|
|
66
|
+
if (!node || node.deletedAt) return;
|
|
67
|
+
const current = getNodeProperty<unknown[]>(node, op.path, []);
|
|
68
|
+
|
|
69
|
+
// Use Set for primitives, manual check for objects
|
|
70
|
+
const result = [...current];
|
|
71
|
+
for (const item of op.value) {
|
|
72
|
+
const exists = result.some(existing => {
|
|
73
|
+
if (typeof existing === 'object' && typeof item === 'object') {
|
|
74
|
+
return JSON.stringify(existing) === JSON.stringify(item);
|
|
75
|
+
}
|
|
76
|
+
return existing === item;
|
|
77
|
+
});
|
|
78
|
+
if (!exists) {
|
|
79
|
+
result.push(item);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
setNodeProperty(node, op.path, result, meta);
|
|
84
|
+
}
|