@vuer-ai/vuer-rtc 0.7.0 → 0.8.2
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/CLAUDE.md +3 -2
- package/dist/client/EditBuffer.d.ts +4 -4
- package/dist/client/EditBuffer.d.ts.map +1 -1
- package/dist/client/EditBuffer.js +26 -25
- package/dist/client/EditBuffer.js.map +1 -1
- package/dist/client/actions.d.ts +3 -3
- package/dist/client/actions.d.ts.map +1 -1
- package/dist/client/actions.js +71 -70
- package/dist/client/actions.js.map +1 -1
- package/dist/client/coalesceGraphOps.d.ts +4 -4
- package/dist/client/coalesceGraphOps.js +4 -4
- package/dist/client/coalesceTextOperations.d.ts.map +1 -1
- package/dist/client/coalesceTextOperations.js +23 -20
- package/dist/client/coalesceTextOperations.js.map +1 -1
- package/dist/client/coalescence/lwwOperations.js +3 -3
- package/dist/client/coalescence/lwwOperations.js.map +1 -1
- package/dist/client/coalescence/numberOperations.js +2 -2
- package/dist/client/coalescence/numberOperations.js.map +1 -1
- package/dist/client/coalescence/registry.d.ts +3 -3
- package/dist/client/coalescence/registry.d.ts.map +1 -1
- package/dist/client/coalescence/registry.js +11 -11
- package/dist/client/coalescence/registry.js.map +1 -1
- package/dist/client/coalescence/textDeletes.d.ts +8 -7
- package/dist/client/coalescence/textDeletes.d.ts.map +1 -1
- package/dist/client/coalescence/textDeletes.js +11 -11
- package/dist/client/coalescence/textDeletes.js.map +1 -1
- package/dist/client/coalescence/textInserts.d.ts +8 -5
- package/dist/client/coalescence/textInserts.d.ts.map +1 -1
- package/dist/client/coalescence/textInserts.js +32 -12
- package/dist/client/coalescence/textInserts.js.map +1 -1
- package/dist/client/coalescence/utils.d.ts +3 -9
- package/dist/client/coalescence/utils.d.ts.map +1 -1
- package/dist/client/coalescence/utils.js +10 -8
- package/dist/client/coalescence/utils.js.map +1 -1
- package/dist/client/coalescence/vector3Operations.js +2 -2
- package/dist/client/coalescence/vector3Operations.js.map +1 -1
- package/dist/client/createGraph.d.ts +2 -2
- package/dist/client/createGraph.js +4 -4
- package/dist/client/createGraph.js.map +1 -1
- package/dist/client/createTextDocument.d.ts +1 -1
- package/dist/client/createTextDocument.js +3 -3
- package/dist/client/createTextDocument.js.map +1 -1
- package/dist/client/hooks.d.ts +3 -3
- package/dist/client/hooks.d.ts.map +1 -1
- package/dist/client/hooks.js +4 -4
- package/dist/client/hooks.js.map +1 -1
- package/dist/client/textActions.d.ts +2 -2
- package/dist/client/textActions.d.ts.map +1 -1
- package/dist/client/textActions.js +47 -47
- package/dist/client/textActions.js.map +1 -1
- package/dist/client/textTypes.d.ts +8 -8
- package/dist/client/textTypes.d.ts.map +1 -1
- package/dist/client/types.d.ts +4 -4
- package/dist/client/types.d.ts.map +1 -1
- package/dist/crdt/GraphTextCRDT.d.ts +2 -2
- package/dist/crdt/GraphTextCRDT.d.ts.map +1 -1
- package/dist/crdt/GraphTextCRDT.js +6 -6
- package/dist/crdt/GraphTextCRDT.js.map +1 -1
- package/dist/crdt/Rope.d.ts +13 -14
- package/dist/crdt/Rope.d.ts.map +1 -1
- package/dist/crdt/Rope.js +130 -59
- package/dist/crdt/Rope.js.map +1 -1
- package/dist/crdt/index.d.ts +1 -1
- package/dist/crdt/index.d.ts.map +1 -1
- package/dist/crdt/index.js +1 -1
- package/dist/crdt/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/operations/OperationTypes.d.ts +45 -48
- package/dist/operations/OperationTypes.d.ts.map +1 -1
- package/dist/operations/OperationValidator.js +11 -11
- package/dist/operations/OperationValidator.js.map +1 -1
- package/dist/operations/apply/node.js +3 -3
- package/dist/operations/apply/node.js.map +1 -1
- package/dist/operations/apply/text.d.ts.map +1 -1
- package/dist/operations/apply/text.js +35 -32
- package/dist/operations/apply/text.js.map +1 -1
- package/dist/operations/apply/types.d.ts +4 -4
- package/dist/operations/apply/types.d.ts.map +1 -1
- package/dist/operations/apply/types.js +8 -8
- package/dist/operations/apply/types.js.map +1 -1
- package/dist/operations/dispatcher.d.ts.map +1 -1
- package/dist/operations/dispatcher.js +52 -13
- package/dist/operations/dispatcher.js.map +1 -1
- package/dist/serdes.d.ts +1 -1
- package/dist/serdes.d.ts.map +1 -1
- package/dist/state/ConflictResolver.d.ts +9 -9
- package/dist/state/ConflictResolver.d.ts.map +1 -1
- package/dist/state/ConflictResolver.js +20 -20
- package/dist/state/ConflictResolver.js.map +1 -1
- package/dist/state/DType.d.ts +2 -2
- package/dist/state/DType.d.ts.map +1 -1
- package/dist/state/DType.js +14 -14
- package/dist/state/DType.js.map +1 -1
- package/dist/state/VectorClock.d.ts +6 -6
- package/dist/state/VectorClock.d.ts.map +1 -1
- package/dist/state/VectorClock.js +14 -14
- package/dist/state/VectorClock.js.map +1 -1
- package/dist/state/index.d.ts +1 -1
- package/dist/state/index.js +1 -1
- package/examples/01-basic-usage.ts +16 -16
- package/examples/02-concurrent-edits.ts +29 -29
- package/examples/03-scene-building.ts +28 -28
- package/examples/04-conflict-resolution.ts +56 -56
- package/examples/05-coalescence-usage.ts +23 -23
- package/examples/README.md +12 -12
- package/package.json +1 -1
- package/src/client/EditBuffer.ts +28 -27
- package/src/client/TEXT_DOCUMENT_API.md +9 -9
- package/src/client/actions.ts +74 -70
- package/src/client/coalesceGraphOps.ts +4 -4
- package/src/client/coalesceTextOperations.ts +26 -22
- package/src/client/coalescence/lwwOperations.ts +3 -3
- package/src/client/coalescence/numberOperations.ts +2 -2
- package/src/client/coalescence/registry.ts +13 -12
- package/src/client/coalescence/textDeletes.ts +22 -18
- package/src/client/coalescence/textInserts.ts +49 -25
- package/src/client/coalescence/utils.ts +14 -11
- package/src/client/coalescence/vector3Operations.ts +2 -2
- package/src/client/createGraph.ts +4 -4
- package/src/client/createTextDocument.ts +3 -3
- package/src/client/hooks.tsx +5 -5
- package/src/client/textActions.ts +47 -47
- package/src/client/textTypes.ts +8 -8
- package/src/client/types.ts +4 -4
- package/src/crdt/GraphTextCRDT.ts +6 -6
- package/src/crdt/Rope.ts +156 -71
- package/src/crdt/index.ts +2 -0
- package/src/index.ts +2 -0
- package/src/operations/OperationTypes.ts +47 -47
- package/src/operations/OperationValidator.ts +11 -11
- package/src/operations/apply/node.ts +3 -3
- package/src/operations/apply/text.ts +38 -32
- package/src/operations/apply/types.ts +11 -11
- package/src/operations/dispatcher.ts +57 -13
- package/src/serdes.ts +1 -1
- package/src/state/ConflictResolver.ts +23 -23
- package/src/state/DType.ts +16 -16
- package/src/state/VectorClock.ts +14 -14
- package/src/state/index.ts +1 -1
- package/tests/client/actions.test.ts +76 -76
- package/tests/client/coalesce-graph-operations.test.ts +84 -84
- package/tests/client/coalesce-text-operations.test.ts +91 -114
- package/tests/client/compaction.test.ts +18 -18
- package/tests/client/delete-coalescence-bug.test.ts +34 -34
- package/tests/client/edit-buffer.test.ts +27 -30
- package/tests/client/graph-coalescence-phase1.test.ts +66 -66
- package/tests/client/graph-coalescence.test.ts +50 -50
- package/tests/client/journal-benchmark.test.ts +5 -5
- package/tests/crdt/graph-text-crdt.test.ts +60 -64
- package/tests/crdt/rope.test.ts +9 -8
- package/tests/crdt/text-operations.test.ts +28 -28
- package/tests/fixtures/array-ops.jsonl +6 -6
- package/tests/fixtures/boolean-ops.jsonl +6 -6
- package/tests/fixtures/color-ops.jsonl +4 -4
- package/tests/fixtures/edit-buffer.jsonl +3 -3
- package/tests/fixtures/node-ops.jsonl +6 -6
- package/tests/fixtures/number-ops.jsonl +7 -7
- package/tests/fixtures/object-ops.jsonl +4 -4
- package/tests/fixtures/operations.jsonl +7 -7
- package/tests/fixtures/string-ops.jsonl +4 -4
- package/tests/fixtures/undo-redo.jsonl +3 -3
- package/tests/fixtures/vector-ops.jsonl +17 -17
- package/tests/operations/collections.test.ts +4 -4
- package/tests/operations/nodes.test.ts +5 -5
- package/tests/operations/operation-ordering.test.ts +406 -0
- package/tests/operations/primitives.test.ts +4 -4
- package/tests/operations/unified-schema.test.ts +27 -27
- package/tests/operations/vectors.test.ts +4 -4
- package/tests/sync/digest.test.ts +5 -5
|
@@ -15,10 +15,10 @@ import type { VectorClock } from '../state/VectorClock.js';
|
|
|
15
15
|
export interface CRDTMessage {
|
|
16
16
|
// === CRDT Metadata (wrapper) ===
|
|
17
17
|
id: string; // Message ID (UUID)
|
|
18
|
-
|
|
18
|
+
client: string; // Session that created this message
|
|
19
19
|
clock: VectorClock; // Vector clock for causal ordering
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
lt: number; // Lamport timestamp for total ordering
|
|
21
|
+
ts: number; // Wall-clock time (seconds since epoch)
|
|
22
22
|
|
|
23
23
|
// === Operations (batch) ===
|
|
24
24
|
ops: Operation[]; // One or more operations
|
|
@@ -29,7 +29,7 @@ export interface CRDTMessage {
|
|
|
29
29
|
*/
|
|
30
30
|
export interface BaseOp {
|
|
31
31
|
key: string; // Node key (human-friendly, e.g., 'cube-1', 'player', 'scene')
|
|
32
|
-
|
|
32
|
+
ot: string; // dtype operation: 'number.set', 'vector3.add', etc.
|
|
33
33
|
path: string; // Property path: 'color', 'transform.position', etc.
|
|
34
34
|
}
|
|
35
35
|
|
|
@@ -39,35 +39,35 @@ export interface BaseOp {
|
|
|
39
39
|
|
|
40
40
|
export interface NumberSetOp extends BaseOp {
|
|
41
41
|
key: string;
|
|
42
|
-
|
|
42
|
+
ot: 'number.set';
|
|
43
43
|
path: string;
|
|
44
44
|
value: number;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export interface NumberAddOp extends BaseOp {
|
|
48
48
|
key: string;
|
|
49
|
-
|
|
49
|
+
ot: 'number.add';
|
|
50
50
|
path: string;
|
|
51
51
|
value: number;
|
|
52
52
|
}
|
|
53
53
|
|
|
54
54
|
export interface NumberMultiplyOp extends BaseOp {
|
|
55
55
|
key: string;
|
|
56
|
-
|
|
56
|
+
ot: 'number.multiply';
|
|
57
57
|
path: string;
|
|
58
58
|
value: number;
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
export interface NumberMinOp extends BaseOp {
|
|
62
62
|
key: string;
|
|
63
|
-
|
|
63
|
+
ot: 'number.min';
|
|
64
64
|
path: string;
|
|
65
65
|
value: number;
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export interface NumberMaxOp extends BaseOp {
|
|
69
69
|
key: string;
|
|
70
|
-
|
|
70
|
+
ot: 'number.max';
|
|
71
71
|
path: string;
|
|
72
72
|
value: number;
|
|
73
73
|
}
|
|
@@ -78,14 +78,14 @@ export interface NumberMaxOp extends BaseOp {
|
|
|
78
78
|
|
|
79
79
|
export interface StringSetOp extends BaseOp {
|
|
80
80
|
key: string;
|
|
81
|
-
|
|
81
|
+
ot: 'string.set';
|
|
82
82
|
path: string;
|
|
83
83
|
value: string;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
86
|
export interface StringConcatOp extends BaseOp {
|
|
87
87
|
key: string;
|
|
88
|
-
|
|
88
|
+
ot: 'string.concat';
|
|
89
89
|
path: string;
|
|
90
90
|
value: string;
|
|
91
91
|
separator?: string;
|
|
@@ -101,7 +101,7 @@ export interface StringConcatOp extends BaseOp {
|
|
|
101
101
|
*/
|
|
102
102
|
export interface TextInitOp extends BaseOp {
|
|
103
103
|
key: string;
|
|
104
|
-
|
|
104
|
+
ot: 'text.init';
|
|
105
105
|
path: string;
|
|
106
106
|
value?: string; // Optional initial content
|
|
107
107
|
}
|
|
@@ -113,7 +113,7 @@ export interface TextInitOp extends BaseOp {
|
|
|
113
113
|
*/
|
|
114
114
|
export interface TextInsertOp extends BaseOp {
|
|
115
115
|
key: string;
|
|
116
|
-
|
|
116
|
+
ot: 'text.insert';
|
|
117
117
|
path: string;
|
|
118
118
|
// Position-based format (local convenience)
|
|
119
119
|
position?: number;
|
|
@@ -133,13 +133,13 @@ export interface TextInsertOp extends BaseOp {
|
|
|
133
133
|
*/
|
|
134
134
|
export interface TextDeleteOp extends BaseOp {
|
|
135
135
|
key: string;
|
|
136
|
-
|
|
136
|
+
ot: 'text.delete';
|
|
137
137
|
path: string;
|
|
138
138
|
// Position-based format (local convenience)
|
|
139
139
|
position?: number;
|
|
140
140
|
length?: number;
|
|
141
|
-
// CRDT metadata format (network sync)
|
|
142
|
-
deletions?: Array<
|
|
141
|
+
// CRDT metadata format (network sync) - compressed tuple format
|
|
142
|
+
deletions?: Array<[string, number]>; // [[itemId, length], ...]
|
|
143
143
|
}
|
|
144
144
|
|
|
145
145
|
/**
|
|
@@ -149,7 +149,7 @@ export interface TextDeleteOp extends BaseOp {
|
|
|
149
149
|
*/
|
|
150
150
|
export interface TextReplaceOp extends BaseOp {
|
|
151
151
|
key: string;
|
|
152
|
-
|
|
152
|
+
ot: 'text.replace';
|
|
153
153
|
path: string;
|
|
154
154
|
// Position-based format (local convenience)
|
|
155
155
|
position?: number;
|
|
@@ -170,21 +170,21 @@ export interface TextReplaceOp extends BaseOp {
|
|
|
170
170
|
|
|
171
171
|
export interface BooleanSetOp extends BaseOp {
|
|
172
172
|
key: string;
|
|
173
|
-
|
|
173
|
+
ot: 'boolean.set';
|
|
174
174
|
path: string;
|
|
175
175
|
value: boolean;
|
|
176
176
|
}
|
|
177
177
|
|
|
178
178
|
export interface BooleanOrOp extends BaseOp {
|
|
179
179
|
key: string;
|
|
180
|
-
|
|
180
|
+
ot: 'boolean.or';
|
|
181
181
|
path: string;
|
|
182
182
|
value: boolean;
|
|
183
183
|
}
|
|
184
184
|
|
|
185
185
|
export interface BooleanAndOp extends BaseOp {
|
|
186
186
|
key: string;
|
|
187
|
-
|
|
187
|
+
ot: 'boolean.and';
|
|
188
188
|
path: string;
|
|
189
189
|
value: boolean;
|
|
190
190
|
}
|
|
@@ -195,21 +195,21 @@ export interface BooleanAndOp extends BaseOp {
|
|
|
195
195
|
|
|
196
196
|
export interface Vector3SetOp extends BaseOp {
|
|
197
197
|
key: string;
|
|
198
|
-
|
|
198
|
+
ot: 'vector3.set';
|
|
199
199
|
path: string;
|
|
200
200
|
value: [number, number, number];
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
export interface Vector3AddOp extends BaseOp {
|
|
204
204
|
key: string;
|
|
205
|
-
|
|
205
|
+
ot: 'vector3.add';
|
|
206
206
|
path: string;
|
|
207
207
|
value: [number, number, number];
|
|
208
208
|
}
|
|
209
209
|
|
|
210
210
|
export interface Vector3MultiplyOp extends BaseOp {
|
|
211
211
|
key: string;
|
|
212
|
-
|
|
212
|
+
ot: 'vector3.multiply';
|
|
213
213
|
path: string;
|
|
214
214
|
value: [number, number, number];
|
|
215
215
|
}
|
|
@@ -232,15 +232,15 @@ export type EulerOrder = 'XYZ' | 'XZY' | 'YXZ' | 'YZX' | 'ZXY' | 'ZYX';
|
|
|
232
232
|
*
|
|
233
233
|
* @example
|
|
234
234
|
* // Intrinsic XYZ rotation (default): rotate around body's X, then Y, then Z
|
|
235
|
-
* {
|
|
235
|
+
* { ot: 'vector3.applyEuler', key: 'arm', path: 'direction', value: [0, Math.PI/2, 0] }
|
|
236
236
|
*
|
|
237
237
|
* // Extrinsic XYZ rotation: rotate around fixed X, then Y, then Z
|
|
238
238
|
* // Note: Extrinsic XYZ is equivalent to Intrinsic ZYX
|
|
239
|
-
* {
|
|
239
|
+
* { ot: 'vector3.applyEuler', key: 'arm', path: 'direction', value: [0, Math.PI/2, 0], intrinsic: false }
|
|
240
240
|
*/
|
|
241
241
|
export interface Vector3ApplyEulerOp extends BaseOp {
|
|
242
242
|
key: string;
|
|
243
|
-
|
|
243
|
+
ot: 'vector3.applyEuler';
|
|
244
244
|
path: string;
|
|
245
245
|
value: [number, number, number];
|
|
246
246
|
order?: EulerOrder;
|
|
@@ -249,7 +249,7 @@ export interface Vector3ApplyEulerOp extends BaseOp {
|
|
|
249
249
|
|
|
250
250
|
export interface Vector3ApplyQuaternionOp extends BaseOp {
|
|
251
251
|
key: string;
|
|
252
|
-
|
|
252
|
+
ot: 'vector3.applyQuaternion';
|
|
253
253
|
path: string;
|
|
254
254
|
value: [number, number, number, number]; // [x, y, z, w]
|
|
255
255
|
}
|
|
@@ -260,14 +260,14 @@ export interface Vector3ApplyQuaternionOp extends BaseOp {
|
|
|
260
260
|
|
|
261
261
|
export interface EulerSetOp extends BaseOp {
|
|
262
262
|
key: string;
|
|
263
|
-
|
|
263
|
+
ot: 'euler.set';
|
|
264
264
|
path: string;
|
|
265
265
|
value: [number, number, number]; // [x, y, z] in radians
|
|
266
266
|
}
|
|
267
267
|
|
|
268
268
|
export interface EulerAddOp extends BaseOp {
|
|
269
269
|
key: string;
|
|
270
|
-
|
|
270
|
+
ot: 'euler.add';
|
|
271
271
|
path: string;
|
|
272
272
|
value: [number, number, number]; // [x, y, z] in radians
|
|
273
273
|
}
|
|
@@ -278,14 +278,14 @@ export interface EulerAddOp extends BaseOp {
|
|
|
278
278
|
|
|
279
279
|
export interface QuaternionSetOp extends BaseOp {
|
|
280
280
|
key: string;
|
|
281
|
-
|
|
281
|
+
ot: 'quaternion.set';
|
|
282
282
|
path: string;
|
|
283
283
|
value: [number, number, number, number];
|
|
284
284
|
}
|
|
285
285
|
|
|
286
286
|
export interface QuaternionMultiplyOp extends BaseOp {
|
|
287
287
|
key: string;
|
|
288
|
-
|
|
288
|
+
ot: 'quaternion.multiply';
|
|
289
289
|
path: string;
|
|
290
290
|
value: [number, number, number, number];
|
|
291
291
|
}
|
|
@@ -296,14 +296,14 @@ export interface QuaternionMultiplyOp extends BaseOp {
|
|
|
296
296
|
|
|
297
297
|
export interface ColorSetOp extends BaseOp {
|
|
298
298
|
key: string;
|
|
299
|
-
|
|
299
|
+
ot: 'color.set';
|
|
300
300
|
path: string;
|
|
301
301
|
value: string; // Hex color
|
|
302
302
|
}
|
|
303
303
|
|
|
304
304
|
export interface ColorBlendOp extends BaseOp {
|
|
305
305
|
key: string;
|
|
306
|
-
|
|
306
|
+
ot: 'color.blend';
|
|
307
307
|
path: string;
|
|
308
308
|
value: string;
|
|
309
309
|
}
|
|
@@ -314,28 +314,28 @@ export interface ColorBlendOp extends BaseOp {
|
|
|
314
314
|
|
|
315
315
|
export interface ArraySetOp extends BaseOp {
|
|
316
316
|
key: string;
|
|
317
|
-
|
|
317
|
+
ot: 'array.set';
|
|
318
318
|
path: string;
|
|
319
319
|
value: any[];
|
|
320
320
|
}
|
|
321
321
|
|
|
322
322
|
export interface ArrayPushOp extends BaseOp {
|
|
323
323
|
key: string;
|
|
324
|
-
|
|
324
|
+
ot: 'array.push';
|
|
325
325
|
path: string;
|
|
326
326
|
value: any;
|
|
327
327
|
}
|
|
328
328
|
|
|
329
329
|
export interface ArrayUnionOp extends BaseOp {
|
|
330
330
|
key: string;
|
|
331
|
-
|
|
331
|
+
ot: 'array.union';
|
|
332
332
|
path: string;
|
|
333
333
|
value: any[];
|
|
334
334
|
}
|
|
335
335
|
|
|
336
336
|
export interface ArrayRemoveOp extends BaseOp {
|
|
337
337
|
key: string;
|
|
338
|
-
|
|
338
|
+
ot: 'array.remove';
|
|
339
339
|
path: string;
|
|
340
340
|
value: any;
|
|
341
341
|
}
|
|
@@ -346,14 +346,14 @@ export interface ArrayRemoveOp extends BaseOp {
|
|
|
346
346
|
|
|
347
347
|
export interface ObjectSetOp extends BaseOp {
|
|
348
348
|
key: string;
|
|
349
|
-
|
|
349
|
+
ot: 'object.set';
|
|
350
350
|
path: string;
|
|
351
351
|
value: Record<string, any>;
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
export interface ObjectMergeOp extends BaseOp {
|
|
355
355
|
key: string;
|
|
356
|
-
|
|
356
|
+
ot: 'object.merge';
|
|
357
357
|
path: string;
|
|
358
358
|
value: Record<string, any>;
|
|
359
359
|
}
|
|
@@ -367,7 +367,7 @@ export interface ObjectMergeOp extends BaseOp {
|
|
|
367
367
|
*/
|
|
368
368
|
export interface NodeInsertOp extends BaseOp {
|
|
369
369
|
key: string; // Parent node's key
|
|
370
|
-
|
|
370
|
+
ot: 'node.insert';
|
|
371
371
|
path: 'children';
|
|
372
372
|
value: {
|
|
373
373
|
key: string; // New node's key (also used as id)
|
|
@@ -382,7 +382,7 @@ export interface NodeInsertOp extends BaseOp {
|
|
|
382
382
|
*/
|
|
383
383
|
export interface NodeRemoveOp extends BaseOp {
|
|
384
384
|
key: string; // Parent node's key
|
|
385
|
-
|
|
385
|
+
ot: 'node.remove';
|
|
386
386
|
path: 'children';
|
|
387
387
|
value: string; // Node key to remove
|
|
388
388
|
}
|
|
@@ -392,7 +392,7 @@ export interface NodeRemoveOp extends BaseOp {
|
|
|
392
392
|
*/
|
|
393
393
|
export interface NodeMoveOp extends BaseOp {
|
|
394
394
|
key: string; // Old parent node's key
|
|
395
|
-
|
|
395
|
+
ot: 'node.move';
|
|
396
396
|
path: 'children';
|
|
397
397
|
value: {
|
|
398
398
|
nodeKey: string; // Node key to move
|
|
@@ -409,7 +409,7 @@ export interface NodeMoveOp extends BaseOp {
|
|
|
409
409
|
*/
|
|
410
410
|
export interface MetaUndoOp {
|
|
411
411
|
key: '_meta';
|
|
412
|
-
|
|
412
|
+
ot: 'meta.undo';
|
|
413
413
|
path: '_meta';
|
|
414
414
|
targetMsgId: string; // Message ID to undo
|
|
415
415
|
}
|
|
@@ -419,7 +419,7 @@ export interface MetaUndoOp {
|
|
|
419
419
|
*/
|
|
420
420
|
export interface MetaRedoOp {
|
|
421
421
|
key: '_meta';
|
|
422
|
-
|
|
422
|
+
ot: 'meta.redo';
|
|
423
423
|
path: '_meta';
|
|
424
424
|
targetMsgId: string; // Message ID to redo
|
|
425
425
|
}
|
|
@@ -450,8 +450,8 @@ export type Operation =
|
|
|
450
450
|
* Per-property LWW metadata
|
|
451
451
|
*/
|
|
452
452
|
export interface PropertyLWW {
|
|
453
|
-
|
|
454
|
-
|
|
453
|
+
lt: number;
|
|
454
|
+
client: string;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
457
|
/**
|
|
@@ -484,6 +484,6 @@ export interface SceneNode {
|
|
|
484
484
|
export interface SceneGraph {
|
|
485
485
|
nodes: Record<string, SceneNode>; // Flattened map by key
|
|
486
486
|
rootKey: string; // Root node key
|
|
487
|
-
lww: Record<string, PropertyLWW>; // Global LWW map: "nodeKey.path" → {lamportTime,
|
|
487
|
+
lww: Record<string, PropertyLWW>; // Global LWW map: "nodeKey.path" → {lamportTime, client}
|
|
488
488
|
tombstones: Record<string, number>; // Deleted nodes: nodeKey → deletedAt timestamp
|
|
489
489
|
}
|
|
@@ -25,20 +25,20 @@ export class OperationValidator {
|
|
|
25
25
|
errors.push('id is required and must be a string');
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
if (!msg.
|
|
29
|
-
errors.push('
|
|
28
|
+
if (!msg.client || typeof msg.client !== 'string') {
|
|
29
|
+
errors.push('client is required and must be a string');
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
if (!msg.clock || typeof msg.clock !== 'object') {
|
|
33
33
|
errors.push('clock must be an object');
|
|
34
34
|
}
|
|
35
35
|
|
|
36
|
-
if (typeof msg.
|
|
37
|
-
errors.push('
|
|
36
|
+
if (typeof msg.lt !== 'number') {
|
|
37
|
+
errors.push('lt must be a number');
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
-
if (typeof msg.
|
|
41
|
-
errors.push('
|
|
40
|
+
if (typeof msg.ts !== 'number') {
|
|
41
|
+
errors.push('ts must be a number');
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
if (!Array.isArray(msg.ops)) {
|
|
@@ -72,17 +72,17 @@ export class OperationValidator {
|
|
|
72
72
|
errors.push('key must be a string');
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
-
if (!op.
|
|
76
|
-
errors.push('
|
|
75
|
+
if (!op.ot || typeof op.ot !== 'string') {
|
|
76
|
+
errors.push('ot is required and must be a string');
|
|
77
77
|
}
|
|
78
78
|
|
|
79
79
|
if (!op.path || typeof op.path !== 'string') {
|
|
80
80
|
errors.push('path is required and must be a string');
|
|
81
81
|
}
|
|
82
82
|
|
|
83
|
-
// Validate based on
|
|
84
|
-
if (op.
|
|
85
|
-
const [dtype, operation] = op.
|
|
83
|
+
// Validate based on ot
|
|
84
|
+
if (op.ot) {
|
|
85
|
+
const [dtype, operation] = op.ot.split('.');
|
|
86
86
|
|
|
87
87
|
switch (dtype) {
|
|
88
88
|
case 'number':
|
|
@@ -50,8 +50,8 @@ export function NodeInsert(
|
|
|
50
50
|
// Register in LWW map
|
|
51
51
|
const lwwKey = `${nodeKey}.${propKey}`;
|
|
52
52
|
draft.lww[lwwKey] = {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
lt: meta.lt,
|
|
54
|
+
client: meta.client,
|
|
55
55
|
};
|
|
56
56
|
}
|
|
57
57
|
}
|
|
@@ -89,7 +89,7 @@ export function NodeRemove(
|
|
|
89
89
|
}
|
|
90
90
|
|
|
91
91
|
// Tombstone: mark as deleted at root level
|
|
92
|
-
draft.tombstones[nodeKey] = meta.
|
|
92
|
+
draft.tombstones[nodeKey] = meta.ts;
|
|
93
93
|
|
|
94
94
|
// Remove from parent's children
|
|
95
95
|
const parentNode = draft.nodes[op.key];
|
|
@@ -101,7 +101,7 @@ export function TextInit(
|
|
|
101
101
|
const node = draft.nodes[op.key];
|
|
102
102
|
if (!node) return;
|
|
103
103
|
|
|
104
|
-
const rope = getOrCreateRope(node, op.path, meta.
|
|
104
|
+
const rope = getOrCreateRope(node, op.path, meta.client);
|
|
105
105
|
|
|
106
106
|
if (op.value && op.value.length > 0) {
|
|
107
107
|
insert(rope, 0, op.value);
|
|
@@ -121,25 +121,26 @@ export function TextInsert(
|
|
|
121
121
|
const node = draft.nodes[op.key];
|
|
122
122
|
if (!node) return;
|
|
123
123
|
|
|
124
|
-
const rope = getOrCreateRope(node, op.path, meta.
|
|
124
|
+
const rope = getOrCreateRope(node, op.path, meta.client);
|
|
125
125
|
|
|
126
|
-
|
|
126
|
+
const value = (op as any).value;
|
|
127
|
+
|
|
128
|
+
if (op.id !== undefined && value !== undefined && op.seq !== undefined) {
|
|
127
129
|
// CRDT metadata format (replay from journal)
|
|
130
|
+
// Value is already [anchor, content] tuple
|
|
128
131
|
apply(rope, {
|
|
129
|
-
|
|
132
|
+
ot: 'insert',
|
|
130
133
|
id: op.id,
|
|
131
|
-
|
|
132
|
-
parentId: op.parentId ?? null,
|
|
134
|
+
value, // Pass tuple as-is
|
|
133
135
|
seq: op.seq,
|
|
134
136
|
ts: op.ts ?? Date.now() / 1000,
|
|
135
137
|
});
|
|
136
138
|
} else if (op.position !== undefined && op.value !== undefined) {
|
|
137
139
|
// Position-based format (local edit) → convert to CRDT metadata in-place
|
|
138
|
-
|
|
139
|
-
const crdtOp = insertWithSplit(rope, op.position, op.value, meta.sessionId);
|
|
140
|
+
const crdtOp = insertWithSplit(rope, op.position, op.value, meta.client);
|
|
140
141
|
op.id = crdtOp.id;
|
|
141
|
-
|
|
142
|
-
op.
|
|
142
|
+
// Pack tuple: [anchor, content] - crdtOp.value is already [anchor, content]
|
|
143
|
+
(op as any).value = crdtOp.value;
|
|
143
144
|
op.seq = crdtOp.seq;
|
|
144
145
|
op.ts = crdtOp.ts;
|
|
145
146
|
}
|
|
@@ -161,13 +162,17 @@ export function TextDelete(
|
|
|
161
162
|
const rope = getRope(node, op.path);
|
|
162
163
|
if (!rope) return;
|
|
163
164
|
|
|
164
|
-
|
|
165
|
+
const deletionsArray = (op as any).rm;
|
|
166
|
+
|
|
167
|
+
if (deletionsArray !== undefined) {
|
|
165
168
|
// CRDT metadata format (replay from journal)
|
|
166
|
-
|
|
169
|
+
// rm is already in tuple format: [[id, len], ...]
|
|
170
|
+
applyDelete(rope, { ot: 'delete', rm: deletionsArray });
|
|
167
171
|
} else if (op.position !== undefined && op.length !== undefined) {
|
|
168
172
|
// Position-based format (local edit) → convert to CRDT metadata in-place
|
|
169
173
|
const crdtOp = remove(rope, op.position, op.length);
|
|
170
|
-
|
|
174
|
+
// rm is already in tuple format from remove()
|
|
175
|
+
(op as any).rm = crdtOp.rm;
|
|
171
176
|
}
|
|
172
177
|
|
|
173
178
|
// node[path] is the mutated TextRope
|
|
@@ -184,31 +189,32 @@ export function TextReplace(
|
|
|
184
189
|
const node = draft.nodes[op.key];
|
|
185
190
|
if (!node) return;
|
|
186
191
|
|
|
187
|
-
const rope = getOrCreateRope(node, op.path, meta.
|
|
192
|
+
const rope = getOrCreateRope(node, op.path, meta.client);
|
|
193
|
+
|
|
194
|
+
const deletionsArray = (op as any).rm;
|
|
195
|
+
const value = (op as any).value;
|
|
188
196
|
|
|
189
|
-
if (
|
|
197
|
+
if (deletionsArray !== undefined && op.id !== undefined && op.seq !== undefined) {
|
|
190
198
|
// CRDT metadata format (replay from journal)
|
|
199
|
+
// Value is already [anchor, content] tuple
|
|
191
200
|
applyReplace(rope, {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
parentId: op.parentId ?? null,
|
|
199
|
-
seq: op.seq,
|
|
200
|
-
ts: op.ts ?? Date.now() / 1000,
|
|
201
|
-
},
|
|
201
|
+
ot: 'replace',
|
|
202
|
+
rm: deletionsArray,
|
|
203
|
+
id: op.id,
|
|
204
|
+
value: value ?? [null, ''], // Pass tuple as-is
|
|
205
|
+
seq: op.seq,
|
|
206
|
+
ts: op.ts ?? Date.now() / 1000,
|
|
202
207
|
});
|
|
203
208
|
} else if (op.position !== undefined && op.length !== undefined && op.value !== undefined) {
|
|
204
209
|
// Position-based format (local edit) → convert to CRDT metadata in-place
|
|
205
|
-
const crdtOp = replace(rope, op.position, op.length, op.value, meta.
|
|
206
|
-
|
|
207
|
-
op.
|
|
208
|
-
op.
|
|
209
|
-
|
|
210
|
-
op.
|
|
211
|
-
op.
|
|
210
|
+
const crdtOp = replace(rope, op.position, op.length, op.value, meta.client);
|
|
211
|
+
// rm is already in tuple format from replace()
|
|
212
|
+
(op as any).rm = crdtOp.rm;
|
|
213
|
+
op.id = crdtOp.id;
|
|
214
|
+
// Pack tuple: [anchor, content] - crdtOp.value is already [anchor, content]
|
|
215
|
+
(op as any).value = crdtOp.value;
|
|
216
|
+
op.seq = crdtOp.seq;
|
|
217
|
+
op.ts = crdtOp.ts;
|
|
212
218
|
}
|
|
213
219
|
|
|
214
220
|
// node[path] is the mutated TextRope
|
|
@@ -9,10 +9,10 @@ import type { SceneGraph, SceneNode } from '../OperationTypes.js';
|
|
|
9
9
|
* Operation metadata from the CRDTMessage envelope
|
|
10
10
|
*/
|
|
11
11
|
export interface OpMeta {
|
|
12
|
-
|
|
12
|
+
client: string;
|
|
13
13
|
clock: VectorClock;
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
lt: number;
|
|
15
|
+
ts: number;
|
|
16
16
|
}
|
|
17
17
|
|
|
18
18
|
/**
|
|
@@ -59,8 +59,8 @@ function getLWW(graph: SceneGraph, nodeKey: string, path: string): PropertyLWW |
|
|
|
59
59
|
function setLWW(graph: SceneGraph, nodeKey: string, path: string, meta: OpMeta): void {
|
|
60
60
|
const key = `${nodeKey}.${path}`;
|
|
61
61
|
graph.lww[key] = {
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
lt: meta.lt,
|
|
63
|
+
client: meta.client,
|
|
64
64
|
};
|
|
65
65
|
}
|
|
66
66
|
|
|
@@ -69,7 +69,7 @@ function setLWW(graph: SceneGraph, nodeKey: string, path: string, meta: OpMeta):
|
|
|
69
69
|
*
|
|
70
70
|
* Resolution is **per-property**: concurrent writes to different paths
|
|
71
71
|
* on the same node never conflict. For two writes to the same path,
|
|
72
|
-
* higher lamportTime wins. On tie, higher
|
|
72
|
+
* higher lamportTime wins. On tie, higher client ID wins (lexicographic).
|
|
73
73
|
* This ensures deterministic conflict resolution regardless of
|
|
74
74
|
* message arrival order.
|
|
75
75
|
*/
|
|
@@ -89,16 +89,16 @@ export function setNodePropertyLWW(
|
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
-
if (meta.
|
|
92
|
+
if (meta.lt > current.lt) {
|
|
93
93
|
// Strictly higher — always wins
|
|
94
94
|
node[path] = value;
|
|
95
95
|
setLWW(graph, node.key, path, meta);
|
|
96
|
-
} else if (meta.
|
|
97
|
-
// Tie: higher
|
|
98
|
-
if (meta.
|
|
96
|
+
} else if (meta.lt === current.lt) {
|
|
97
|
+
// Tie: higher client wins, or same client (last-write-wins within message)
|
|
98
|
+
if (meta.client >= current.client) {
|
|
99
99
|
node[path] = value;
|
|
100
100
|
setLWW(graph, node.key, path, meta);
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
// meta.
|
|
103
|
+
// meta.lt < current.lt → skip (stale write)
|
|
104
104
|
}
|