@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,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
+ }