@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
|
@@ -12,7 +12,7 @@ import type { OpMeta } from './apply/types.js';
|
|
|
12
12
|
import * as registry from './apply/index.js';
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
|
-
* Handler map:
|
|
15
|
+
* Handler map: ot -> apply function
|
|
16
16
|
*/
|
|
17
17
|
const handlers: Record<string, (graph: SceneGraph, op: Operation, meta: OpMeta) => void> = {
|
|
18
18
|
// Number operations
|
|
@@ -80,14 +80,50 @@ export function applyOperation(
|
|
|
80
80
|
op: Operation,
|
|
81
81
|
meta: OpMeta
|
|
82
82
|
): void {
|
|
83
|
-
const handler = handlers[op.
|
|
83
|
+
const handler = handlers[op.ot];
|
|
84
84
|
if (handler) {
|
|
85
85
|
handler(graph, op, meta);
|
|
86
86
|
} else {
|
|
87
|
-
console.warn(`Unknown
|
|
87
|
+
console.warn(`Unknown ot: ${op.ot}`);
|
|
88
88
|
}
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Compare operations for sorting by Lamport timestamp.
|
|
93
|
+
* CRDT invariant: operations must be applied in causal order (seq → ts → id).
|
|
94
|
+
*
|
|
95
|
+
* This ensures that:
|
|
96
|
+
* 1. Operations with lower Lamport clocks are applied first
|
|
97
|
+
* 2. If Lamport clocks are equal, wall-clock time breaks the tie
|
|
98
|
+
* 3. If both are equal, lexicographic ID order ensures determinism
|
|
99
|
+
*
|
|
100
|
+
* @param a - First operation
|
|
101
|
+
* @param b - Second operation
|
|
102
|
+
* @returns Negative if a < b, positive if a > b, 0 if equal
|
|
103
|
+
*/
|
|
104
|
+
function compareOperations(a: Operation, b: Operation): number {
|
|
105
|
+
const aOp = a as any;
|
|
106
|
+
const bOp = b as any;
|
|
107
|
+
|
|
108
|
+
// Compare by Lamport clock (seq)
|
|
109
|
+
if (aOp.seq !== undefined && bOp.seq !== undefined) {
|
|
110
|
+
if (aOp.seq !== bOp.seq) return aOp.seq - bOp.seq;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// If equal or missing, compare by wall-clock time (ts)
|
|
114
|
+
if (aOp.ts !== undefined && bOp.ts !== undefined) {
|
|
115
|
+
if (aOp.ts !== bOp.ts) return aOp.ts - bOp.ts;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Final fallback: compare by ID (lexicographic)
|
|
119
|
+
if (aOp.id && bOp.id) {
|
|
120
|
+
return String(aOp.id).localeCompare(String(bOp.id));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// If no metadata, preserve original order
|
|
124
|
+
return 0;
|
|
125
|
+
}
|
|
126
|
+
|
|
91
127
|
/**
|
|
92
128
|
* Shallow clone the graph and modified nodes
|
|
93
129
|
*/
|
|
@@ -112,24 +148,29 @@ export function applyMessage(graph: SceneGraph, msg: CRDTMessage): SceneGraph {
|
|
|
112
148
|
const newGraph = shallowCloneGraph(graph);
|
|
113
149
|
|
|
114
150
|
const meta: OpMeta = {
|
|
115
|
-
|
|
151
|
+
client: msg.client,
|
|
116
152
|
clock: msg.clock,
|
|
117
|
-
|
|
118
|
-
|
|
153
|
+
lt: msg.lt,
|
|
154
|
+
ts: msg.ts,
|
|
119
155
|
};
|
|
120
156
|
|
|
121
|
-
|
|
157
|
+
// Sort operations by Lamport timestamp to ensure causal order
|
|
158
|
+
// This is critical for CRDT correctness, especially for text operations
|
|
159
|
+
// where replace ops may reference IDs from insert ops
|
|
160
|
+
const sortedOps = [...msg.ops].sort(compareOperations);
|
|
161
|
+
|
|
162
|
+
for (const op of sortedOps) {
|
|
122
163
|
// Determine all node keys that this operation will mutate
|
|
123
164
|
const keysToClone: string[] = [];
|
|
124
165
|
if (op.key && newGraph.nodes[op.key]) {
|
|
125
166
|
keysToClone.push(op.key);
|
|
126
167
|
}
|
|
127
|
-
if (op.
|
|
168
|
+
if (op.ot === 'node.move') {
|
|
128
169
|
const { nodeKey, newParent } = (op as any).value;
|
|
129
170
|
if (nodeKey && newGraph.nodes[nodeKey]) keysToClone.push(nodeKey);
|
|
130
171
|
if (newParent && newGraph.nodes[newParent]) keysToClone.push(newParent);
|
|
131
172
|
}
|
|
132
|
-
if (op.
|
|
173
|
+
if (op.ot === 'node.remove') {
|
|
133
174
|
const nodeKey = (op as any).value;
|
|
134
175
|
if (typeof nodeKey === 'string' && newGraph.nodes[nodeKey]) keysToClone.push(nodeKey);
|
|
135
176
|
}
|
|
@@ -158,13 +199,16 @@ export function applyMessage(graph: SceneGraph, msg: CRDTMessage): SceneGraph {
|
|
|
158
199
|
*/
|
|
159
200
|
export function applyMessageMut(graph: SceneGraph, msg: CRDTMessage): void {
|
|
160
201
|
const meta: OpMeta = {
|
|
161
|
-
|
|
202
|
+
client: msg.client,
|
|
162
203
|
clock: msg.clock,
|
|
163
|
-
|
|
164
|
-
|
|
204
|
+
lt: msg.lt,
|
|
205
|
+
ts: msg.ts,
|
|
165
206
|
};
|
|
166
207
|
|
|
167
|
-
|
|
208
|
+
// Sort operations by Lamport timestamp to ensure causal order
|
|
209
|
+
const sortedOps = [...msg.ops].sort(compareOperations);
|
|
210
|
+
|
|
211
|
+
for (const op of sortedOps) {
|
|
168
212
|
applyOperation(graph, op, meta);
|
|
169
213
|
}
|
|
170
214
|
}
|
package/src/serdes.ts
CHANGED
|
@@ -22,7 +22,7 @@ export type WireMessage =
|
|
|
22
22
|
| { mtype: 'error'; msgId: string; error: string }
|
|
23
23
|
| { mtype: 'state'; snapshot: Snapshot; journal: CRDTMessage[] }
|
|
24
24
|
| { mtype: 'sync'; vectorClock?: VectorClock; filter: Uint8Array; count: number }
|
|
25
|
-
| { mtype: 'heartbeat';
|
|
25
|
+
| { mtype: 'heartbeat'; client: string; vectorClock: VectorClock }
|
|
26
26
|
| { mtype: 'room-reset' }
|
|
27
27
|
| { mtype: 'compaction-watermark'; watermark: VectorClock };
|
|
28
28
|
|
|
@@ -2,29 +2,29 @@
|
|
|
2
2
|
* ConflictResolver - Operation-based property conflict resolution
|
|
3
3
|
*
|
|
4
4
|
* Merges concurrent property updates based on:
|
|
5
|
-
* - Operation type (
|
|
5
|
+
* - Operation type (ot) which encodes dtype.operation
|
|
6
6
|
* - DType merge functions
|
|
7
7
|
*
|
|
8
|
-
* Schema-less: dtype and operation are derived from
|
|
8
|
+
* Schema-less: dtype and operation are derived from ot.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { DType, type ValueWithMeta } from './DType.js';
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Parse
|
|
14
|
+
* Parse ot string into dtype and operation
|
|
15
15
|
*
|
|
16
16
|
* @example
|
|
17
17
|
* parseOtype('vector3.add') // { dtype: 'vector3', operation: 'add' }
|
|
18
18
|
* parseOtype('number.set') // { dtype: 'number', operation: 'set' }
|
|
19
19
|
*/
|
|
20
|
-
export function parseOtype(
|
|
21
|
-
const dotIndex =
|
|
20
|
+
export function parseOtype(ot: string): { dtype: string; operation: string } {
|
|
21
|
+
const dotIndex = ot.indexOf('.');
|
|
22
22
|
if (dotIndex === -1) {
|
|
23
|
-
return { dtype:
|
|
23
|
+
return { dtype: ot, operation: 'set' };
|
|
24
24
|
}
|
|
25
25
|
return {
|
|
26
|
-
dtype:
|
|
27
|
-
operation:
|
|
26
|
+
dtype: ot.slice(0, dotIndex),
|
|
27
|
+
operation: ot.slice(dotIndex + 1),
|
|
28
28
|
};
|
|
29
29
|
}
|
|
30
30
|
|
|
@@ -32,23 +32,23 @@ export class ConflictResolver {
|
|
|
32
32
|
/**
|
|
33
33
|
* Merge a single property from multiple concurrent updates
|
|
34
34
|
*
|
|
35
|
-
* @param
|
|
35
|
+
* @param ot - Operation type (e.g., 'vector3.add', 'number.set')
|
|
36
36
|
* @param values - Array of values with metadata
|
|
37
37
|
* @returns Merged value
|
|
38
38
|
*/
|
|
39
39
|
mergeProperty<T = any>(
|
|
40
|
-
|
|
40
|
+
ot: string,
|
|
41
41
|
values: ValueWithMeta<T>[]
|
|
42
42
|
): T {
|
|
43
43
|
if (values.length === 0) {
|
|
44
|
-
throw new Error(`Cannot merge empty values for
|
|
44
|
+
throw new Error(`Cannot merge empty values for ot: ${ot}`);
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
if (values.length === 1) {
|
|
48
48
|
return values[0].value;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const { dtype, operation } = parseOtype(
|
|
51
|
+
const { dtype, operation } = parseOtype(ot);
|
|
52
52
|
|
|
53
53
|
// Get merge function based on dtype and operation
|
|
54
54
|
const mergeFn = this.getMergeFunction(dtype, operation);
|
|
@@ -72,14 +72,14 @@ export class ConflictResolver {
|
|
|
72
72
|
mergeProperties(
|
|
73
73
|
updates: Array<{
|
|
74
74
|
properties: Record<string, any>;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
ots: Record<string, string>; // ot per property
|
|
76
|
+
lt: number;
|
|
77
|
+
client: string;
|
|
78
78
|
}>
|
|
79
79
|
): Record<string, any> {
|
|
80
80
|
// Group values by property name
|
|
81
81
|
const valuesByProperty = new Map<string, ValueWithMeta[]>();
|
|
82
|
-
const
|
|
82
|
+
const otByProperty = new Map<string, string>();
|
|
83
83
|
|
|
84
84
|
for (const update of updates) {
|
|
85
85
|
for (const [key, value] of Object.entries(update.properties)) {
|
|
@@ -89,13 +89,13 @@ export class ConflictResolver {
|
|
|
89
89
|
|
|
90
90
|
valuesByProperty.get(key)!.push({
|
|
91
91
|
value,
|
|
92
|
-
|
|
93
|
-
|
|
92
|
+
lt: update.lt,
|
|
93
|
+
client: update.client,
|
|
94
94
|
});
|
|
95
95
|
|
|
96
|
-
// Store
|
|
97
|
-
if (update.
|
|
98
|
-
|
|
96
|
+
// Store ot for this property
|
|
97
|
+
if (update.ots?.[key]) {
|
|
98
|
+
otByProperty.set(key, update.ots[key]);
|
|
99
99
|
}
|
|
100
100
|
}
|
|
101
101
|
}
|
|
@@ -104,8 +104,8 @@ export class ConflictResolver {
|
|
|
104
104
|
const result: Record<string, any> = {};
|
|
105
105
|
|
|
106
106
|
for (const [propertyName, values] of valuesByProperty.entries()) {
|
|
107
|
-
const
|
|
108
|
-
result[propertyName] = this.mergeProperty(
|
|
107
|
+
const ot = otByProperty.get(propertyName) || 'any.set';
|
|
108
|
+
result[propertyName] = this.mergeProperty(ot, values);
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
return result;
|
package/src/state/DType.ts
CHANGED
|
@@ -12,8 +12,8 @@
|
|
|
12
12
|
*/
|
|
13
13
|
export interface ValueWithMeta<T = any> {
|
|
14
14
|
value: T;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
lt: number;
|
|
16
|
+
client?: string;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/**
|
|
@@ -37,7 +37,7 @@ export const DType = {
|
|
|
37
37
|
* SET - Last-Write-Wins
|
|
38
38
|
*/
|
|
39
39
|
set: ((values: ValueWithMeta<number>[]) => {
|
|
40
|
-
const sorted = [...values].sort((a, b) => b.
|
|
40
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
41
41
|
return sorted[0].value;
|
|
42
42
|
}) as MergeFn<number>,
|
|
43
43
|
|
|
@@ -78,7 +78,7 @@ export const DType = {
|
|
|
78
78
|
* SET - Last-Write-Wins
|
|
79
79
|
*/
|
|
80
80
|
set: ((values: ValueWithMeta<string>[]) => {
|
|
81
|
-
const sorted = [...values].sort((a, b) => b.
|
|
81
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
82
82
|
return sorted[0].value;
|
|
83
83
|
}) as MergeFn<string>,
|
|
84
84
|
|
|
@@ -86,7 +86,7 @@ export const DType = {
|
|
|
86
86
|
* CONCAT - Concatenate (ordered by lamport)
|
|
87
87
|
*/
|
|
88
88
|
concat: ((values: ValueWithMeta<string>[], separator = '\n') => {
|
|
89
|
-
const sorted = [...values].sort((a, b) => a.
|
|
89
|
+
const sorted = [...values].sort((a, b) => a.lt - b.lt);
|
|
90
90
|
return sorted.map((v) => v.value).join(separator);
|
|
91
91
|
}) as MergeFn<string>,
|
|
92
92
|
},
|
|
@@ -99,7 +99,7 @@ export const DType = {
|
|
|
99
99
|
* SET - Last-Write-Wins
|
|
100
100
|
*/
|
|
101
101
|
set: ((values: ValueWithMeta<boolean>[]) => {
|
|
102
|
-
const sorted = [...values].sort((a, b) => b.
|
|
102
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
103
103
|
return sorted[0].value;
|
|
104
104
|
}) as MergeFn<boolean>,
|
|
105
105
|
|
|
@@ -130,7 +130,7 @@ export const DType = {
|
|
|
130
130
|
* SET - Replace entire vector (preserves user intent)
|
|
131
131
|
*/
|
|
132
132
|
set: ((values: ValueWithMeta<[number, number, number]>[]) => {
|
|
133
|
-
const sorted = [...values].sort((a, b) => b.
|
|
133
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
134
134
|
return sorted[0].value;
|
|
135
135
|
}) as MergeFn<[number, number, number]>,
|
|
136
136
|
|
|
@@ -169,7 +169,7 @@ export const DType = {
|
|
|
169
169
|
* SET - Replace entire quaternion
|
|
170
170
|
*/
|
|
171
171
|
set: ((values: ValueWithMeta<[number, number, number, number]>[]) => {
|
|
172
|
-
const sorted = [...values].sort((a, b) => b.
|
|
172
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
173
173
|
return sorted[0].value;
|
|
174
174
|
}) as MergeFn<[number, number, number, number]>,
|
|
175
175
|
|
|
@@ -177,7 +177,7 @@ export const DType = {
|
|
|
177
177
|
* MULTIPLY - Quaternion multiplication (composition)
|
|
178
178
|
*/
|
|
179
179
|
multiply: ((values: ValueWithMeta<[number, number, number, number]>[]) => {
|
|
180
|
-
const sorted = [...values].sort((a, b) => a.
|
|
180
|
+
const sorted = [...values].sort((a, b) => a.lt - b.lt);
|
|
181
181
|
let result = sorted[0].value;
|
|
182
182
|
|
|
183
183
|
for (let i = 1; i < sorted.length; i++) {
|
|
@@ -196,7 +196,7 @@ export const DType = {
|
|
|
196
196
|
* SET - Replace color
|
|
197
197
|
*/
|
|
198
198
|
set: ((values: ValueWithMeta<string>[]) => {
|
|
199
|
-
const sorted = [...values].sort((a, b) => b.
|
|
199
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
200
200
|
return sorted[0].value;
|
|
201
201
|
}) as MergeFn<string>,
|
|
202
202
|
|
|
@@ -224,7 +224,7 @@ export const DType = {
|
|
|
224
224
|
* SET - Replace entire array
|
|
225
225
|
*/
|
|
226
226
|
set: ((values: ValueWithMeta<any[]>[]) => {
|
|
227
|
-
const sorted = [...values].sort((a, b) => b.
|
|
227
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
228
228
|
return sorted[0].value;
|
|
229
229
|
}) as MergeFn<any[]>,
|
|
230
230
|
|
|
@@ -240,7 +240,7 @@ export const DType = {
|
|
|
240
240
|
* APPEND - Append all (ordered by lamport)
|
|
241
241
|
*/
|
|
242
242
|
append: ((values: ValueWithMeta<any[]>[]) => {
|
|
243
|
-
const sorted = [...values].sort((a, b) => a.
|
|
243
|
+
const sorted = [...values].sort((a, b) => a.lt - b.lt);
|
|
244
244
|
return sorted.flatMap((v) => v.value);
|
|
245
245
|
}) as MergeFn<any[]>,
|
|
246
246
|
},
|
|
@@ -253,7 +253,7 @@ export const DType = {
|
|
|
253
253
|
* SET - Replace entire object
|
|
254
254
|
*/
|
|
255
255
|
set: ((values: ValueWithMeta<Record<string, any>>[]) => {
|
|
256
|
-
const sorted = [...values].sort((a, b) => b.
|
|
256
|
+
const sorted = [...values].sort((a, b) => b.lt - a.lt);
|
|
257
257
|
return sorted[0].value;
|
|
258
258
|
}) as MergeFn<Record<string, any>>,
|
|
259
259
|
|
|
@@ -266,9 +266,9 @@ export const DType = {
|
|
|
266
266
|
|
|
267
267
|
for (const v of values) {
|
|
268
268
|
for (const [key, value] of Object.entries(v.value)) {
|
|
269
|
-
if (!latestTimes[key] || v.
|
|
269
|
+
if (!latestTimes[key] || v.lt > latestTimes[key]) {
|
|
270
270
|
result[key] = value;
|
|
271
|
-
latestTimes[key] = v.
|
|
271
|
+
latestTimes[key] = v.lt;
|
|
272
272
|
}
|
|
273
273
|
}
|
|
274
274
|
}
|
|
@@ -287,7 +287,7 @@ export const DType = {
|
|
|
287
287
|
immutable: {
|
|
288
288
|
set: ((values: ValueWithMeta[]) => {
|
|
289
289
|
// Return first value (creation value)
|
|
290
|
-
const sorted = [...values].sort((a, b) => a.
|
|
290
|
+
const sorted = [...values].sort((a, b) => a.lt - b.lt);
|
|
291
291
|
return sorted[0].value;
|
|
292
292
|
}) as MergeFn,
|
|
293
293
|
},
|
package/src/state/VectorClock.ts
CHANGED
|
@@ -11,35 +11,35 @@ export type VectorClock = Record<string, number>;
|
|
|
11
11
|
|
|
12
12
|
export class VectorClockManager {
|
|
13
13
|
/**
|
|
14
|
-
* Create a new vector clock for a
|
|
15
|
-
* Initializes the
|
|
14
|
+
* Create a new vector clock for a client
|
|
15
|
+
* Initializes the client's counter to 0
|
|
16
16
|
*/
|
|
17
|
-
create(
|
|
18
|
-
return { [
|
|
17
|
+
create(client: string): VectorClock {
|
|
18
|
+
return { [client]: 0 };
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
/**
|
|
22
|
-
* Increment the counter for a
|
|
22
|
+
* Increment the counter for a client
|
|
23
23
|
* Returns a new clock (immutable)
|
|
24
24
|
*/
|
|
25
|
-
increment(clock: VectorClock,
|
|
26
|
-
const currentValue = clock[
|
|
25
|
+
increment(clock: VectorClock, client: string): VectorClock {
|
|
26
|
+
const currentValue = clock[client] || 0;
|
|
27
27
|
return {
|
|
28
28
|
...clock,
|
|
29
|
-
[
|
|
29
|
+
[client]: currentValue + 1,
|
|
30
30
|
};
|
|
31
31
|
}
|
|
32
32
|
|
|
33
33
|
/**
|
|
34
34
|
* Merge two vector clocks
|
|
35
|
-
* Takes the maximum value for each
|
|
35
|
+
* Takes the maximum value for each client
|
|
36
36
|
* Used when receiving remote operations
|
|
37
37
|
*/
|
|
38
38
|
merge(clock1: VectorClock, clock2: VectorClock): VectorClock {
|
|
39
39
|
const merged: VectorClock = { ...clock1 };
|
|
40
40
|
|
|
41
|
-
Object.entries(clock2).forEach(([
|
|
42
|
-
merged[
|
|
41
|
+
Object.entries(clock2).forEach(([client, count]) => {
|
|
42
|
+
merged[client] = Math.max(merged[client] || 0, count);
|
|
43
43
|
});
|
|
44
44
|
|
|
45
45
|
return merged;
|
|
@@ -62,9 +62,9 @@ export class VectorClockManager {
|
|
|
62
62
|
let clock1Greater = false;
|
|
63
63
|
let clock2Greater = false;
|
|
64
64
|
|
|
65
|
-
allSessionIds.forEach((
|
|
66
|
-
const val1 = clock1[
|
|
67
|
-
const val2 = clock2[
|
|
65
|
+
allSessionIds.forEach((client) => {
|
|
66
|
+
const val1 = clock1[client] || 0;
|
|
67
|
+
const val2 = clock2[client] || 0;
|
|
68
68
|
|
|
69
69
|
if (val1 > val2) {
|
|
70
70
|
clock1Greater = true;
|
package/src/state/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* - VectorClock: Used by client for causal ordering
|
|
5
5
|
* - DType, ConflictResolver: Server-side conflict resolution for concurrent updates
|
|
6
6
|
*
|
|
7
|
-
* Schema-less design: dtype and operation are derived from
|
|
7
|
+
* Schema-less design: dtype and operation are derived from ot (e.g., 'vector3.add').
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
// Vector clock (used by client and server)
|