@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.
Files changed (172) hide show
  1. package/CLAUDE.md +3 -2
  2. package/dist/client/EditBuffer.d.ts +4 -4
  3. package/dist/client/EditBuffer.d.ts.map +1 -1
  4. package/dist/client/EditBuffer.js +26 -25
  5. package/dist/client/EditBuffer.js.map +1 -1
  6. package/dist/client/actions.d.ts +3 -3
  7. package/dist/client/actions.d.ts.map +1 -1
  8. package/dist/client/actions.js +71 -70
  9. package/dist/client/actions.js.map +1 -1
  10. package/dist/client/coalesceGraphOps.d.ts +4 -4
  11. package/dist/client/coalesceGraphOps.js +4 -4
  12. package/dist/client/coalesceTextOperations.d.ts.map +1 -1
  13. package/dist/client/coalesceTextOperations.js +23 -20
  14. package/dist/client/coalesceTextOperations.js.map +1 -1
  15. package/dist/client/coalescence/lwwOperations.js +3 -3
  16. package/dist/client/coalescence/lwwOperations.js.map +1 -1
  17. package/dist/client/coalescence/numberOperations.js +2 -2
  18. package/dist/client/coalescence/numberOperations.js.map +1 -1
  19. package/dist/client/coalescence/registry.d.ts +3 -3
  20. package/dist/client/coalescence/registry.d.ts.map +1 -1
  21. package/dist/client/coalescence/registry.js +11 -11
  22. package/dist/client/coalescence/registry.js.map +1 -1
  23. package/dist/client/coalescence/textDeletes.d.ts +8 -7
  24. package/dist/client/coalescence/textDeletes.d.ts.map +1 -1
  25. package/dist/client/coalescence/textDeletes.js +11 -11
  26. package/dist/client/coalescence/textDeletes.js.map +1 -1
  27. package/dist/client/coalescence/textInserts.d.ts +8 -5
  28. package/dist/client/coalescence/textInserts.d.ts.map +1 -1
  29. package/dist/client/coalescence/textInserts.js +32 -12
  30. package/dist/client/coalescence/textInserts.js.map +1 -1
  31. package/dist/client/coalescence/utils.d.ts +3 -9
  32. package/dist/client/coalescence/utils.d.ts.map +1 -1
  33. package/dist/client/coalescence/utils.js +10 -8
  34. package/dist/client/coalescence/utils.js.map +1 -1
  35. package/dist/client/coalescence/vector3Operations.js +2 -2
  36. package/dist/client/coalescence/vector3Operations.js.map +1 -1
  37. package/dist/client/createGraph.d.ts +2 -2
  38. package/dist/client/createGraph.js +4 -4
  39. package/dist/client/createGraph.js.map +1 -1
  40. package/dist/client/createTextDocument.d.ts +1 -1
  41. package/dist/client/createTextDocument.js +3 -3
  42. package/dist/client/createTextDocument.js.map +1 -1
  43. package/dist/client/hooks.d.ts +3 -3
  44. package/dist/client/hooks.d.ts.map +1 -1
  45. package/dist/client/hooks.js +4 -4
  46. package/dist/client/hooks.js.map +1 -1
  47. package/dist/client/textActions.d.ts +2 -2
  48. package/dist/client/textActions.d.ts.map +1 -1
  49. package/dist/client/textActions.js +47 -47
  50. package/dist/client/textActions.js.map +1 -1
  51. package/dist/client/textTypes.d.ts +8 -8
  52. package/dist/client/textTypes.d.ts.map +1 -1
  53. package/dist/client/types.d.ts +4 -4
  54. package/dist/client/types.d.ts.map +1 -1
  55. package/dist/crdt/GraphTextCRDT.d.ts +2 -2
  56. package/dist/crdt/GraphTextCRDT.d.ts.map +1 -1
  57. package/dist/crdt/GraphTextCRDT.js +6 -6
  58. package/dist/crdt/GraphTextCRDT.js.map +1 -1
  59. package/dist/crdt/Rope.d.ts +13 -14
  60. package/dist/crdt/Rope.d.ts.map +1 -1
  61. package/dist/crdt/Rope.js +130 -59
  62. package/dist/crdt/Rope.js.map +1 -1
  63. package/dist/crdt/index.d.ts +1 -1
  64. package/dist/crdt/index.d.ts.map +1 -1
  65. package/dist/crdt/index.js +1 -1
  66. package/dist/crdt/index.js.map +1 -1
  67. package/dist/index.d.ts +1 -1
  68. package/dist/index.d.ts.map +1 -1
  69. package/dist/index.js +1 -1
  70. package/dist/index.js.map +1 -1
  71. package/dist/operations/OperationTypes.d.ts +45 -48
  72. package/dist/operations/OperationTypes.d.ts.map +1 -1
  73. package/dist/operations/OperationValidator.js +11 -11
  74. package/dist/operations/OperationValidator.js.map +1 -1
  75. package/dist/operations/apply/node.js +3 -3
  76. package/dist/operations/apply/node.js.map +1 -1
  77. package/dist/operations/apply/text.d.ts.map +1 -1
  78. package/dist/operations/apply/text.js +35 -32
  79. package/dist/operations/apply/text.js.map +1 -1
  80. package/dist/operations/apply/types.d.ts +4 -4
  81. package/dist/operations/apply/types.d.ts.map +1 -1
  82. package/dist/operations/apply/types.js +8 -8
  83. package/dist/operations/apply/types.js.map +1 -1
  84. package/dist/operations/dispatcher.d.ts.map +1 -1
  85. package/dist/operations/dispatcher.js +52 -13
  86. package/dist/operations/dispatcher.js.map +1 -1
  87. package/dist/serdes.d.ts +1 -1
  88. package/dist/serdes.d.ts.map +1 -1
  89. package/dist/state/ConflictResolver.d.ts +9 -9
  90. package/dist/state/ConflictResolver.d.ts.map +1 -1
  91. package/dist/state/ConflictResolver.js +20 -20
  92. package/dist/state/ConflictResolver.js.map +1 -1
  93. package/dist/state/DType.d.ts +2 -2
  94. package/dist/state/DType.d.ts.map +1 -1
  95. package/dist/state/DType.js +14 -14
  96. package/dist/state/DType.js.map +1 -1
  97. package/dist/state/VectorClock.d.ts +6 -6
  98. package/dist/state/VectorClock.d.ts.map +1 -1
  99. package/dist/state/VectorClock.js +14 -14
  100. package/dist/state/VectorClock.js.map +1 -1
  101. package/dist/state/index.d.ts +1 -1
  102. package/dist/state/index.js +1 -1
  103. package/examples/01-basic-usage.ts +16 -16
  104. package/examples/02-concurrent-edits.ts +29 -29
  105. package/examples/03-scene-building.ts +28 -28
  106. package/examples/04-conflict-resolution.ts +56 -56
  107. package/examples/05-coalescence-usage.ts +23 -23
  108. package/examples/README.md +12 -12
  109. package/package.json +1 -1
  110. package/src/client/EditBuffer.ts +28 -27
  111. package/src/client/TEXT_DOCUMENT_API.md +9 -9
  112. package/src/client/actions.ts +74 -70
  113. package/src/client/coalesceGraphOps.ts +4 -4
  114. package/src/client/coalesceTextOperations.ts +26 -22
  115. package/src/client/coalescence/lwwOperations.ts +3 -3
  116. package/src/client/coalescence/numberOperations.ts +2 -2
  117. package/src/client/coalescence/registry.ts +13 -12
  118. package/src/client/coalescence/textDeletes.ts +22 -18
  119. package/src/client/coalescence/textInserts.ts +49 -25
  120. package/src/client/coalescence/utils.ts +14 -11
  121. package/src/client/coalescence/vector3Operations.ts +2 -2
  122. package/src/client/createGraph.ts +4 -4
  123. package/src/client/createTextDocument.ts +3 -3
  124. package/src/client/hooks.tsx +5 -5
  125. package/src/client/textActions.ts +47 -47
  126. package/src/client/textTypes.ts +8 -8
  127. package/src/client/types.ts +4 -4
  128. package/src/crdt/GraphTextCRDT.ts +6 -6
  129. package/src/crdt/Rope.ts +156 -71
  130. package/src/crdt/index.ts +2 -0
  131. package/src/index.ts +2 -0
  132. package/src/operations/OperationTypes.ts +47 -47
  133. package/src/operations/OperationValidator.ts +11 -11
  134. package/src/operations/apply/node.ts +3 -3
  135. package/src/operations/apply/text.ts +38 -32
  136. package/src/operations/apply/types.ts +11 -11
  137. package/src/operations/dispatcher.ts +57 -13
  138. package/src/serdes.ts +1 -1
  139. package/src/state/ConflictResolver.ts +23 -23
  140. package/src/state/DType.ts +16 -16
  141. package/src/state/VectorClock.ts +14 -14
  142. package/src/state/index.ts +1 -1
  143. package/tests/client/actions.test.ts +76 -76
  144. package/tests/client/coalesce-graph-operations.test.ts +84 -84
  145. package/tests/client/coalesce-text-operations.test.ts +91 -114
  146. package/tests/client/compaction.test.ts +18 -18
  147. package/tests/client/delete-coalescence-bug.test.ts +34 -34
  148. package/tests/client/edit-buffer.test.ts +27 -30
  149. package/tests/client/graph-coalescence-phase1.test.ts +66 -66
  150. package/tests/client/graph-coalescence.test.ts +50 -50
  151. package/tests/client/journal-benchmark.test.ts +5 -5
  152. package/tests/crdt/graph-text-crdt.test.ts +60 -64
  153. package/tests/crdt/rope.test.ts +9 -8
  154. package/tests/crdt/text-operations.test.ts +28 -28
  155. package/tests/fixtures/array-ops.jsonl +6 -6
  156. package/tests/fixtures/boolean-ops.jsonl +6 -6
  157. package/tests/fixtures/color-ops.jsonl +4 -4
  158. package/tests/fixtures/edit-buffer.jsonl +3 -3
  159. package/tests/fixtures/node-ops.jsonl +6 -6
  160. package/tests/fixtures/number-ops.jsonl +7 -7
  161. package/tests/fixtures/object-ops.jsonl +4 -4
  162. package/tests/fixtures/operations.jsonl +7 -7
  163. package/tests/fixtures/string-ops.jsonl +4 -4
  164. package/tests/fixtures/undo-redo.jsonl +3 -3
  165. package/tests/fixtures/vector-ops.jsonl +17 -17
  166. package/tests/operations/collections.test.ts +4 -4
  167. package/tests/operations/nodes.test.ts +5 -5
  168. package/tests/operations/operation-ordering.test.ts +406 -0
  169. package/tests/operations/primitives.test.ts +4 -4
  170. package/tests/operations/unified-schema.test.ts +27 -27
  171. package/tests/operations/vectors.test.ts +4 -4
  172. package/tests/sync/digest.test.ts +5 -5
@@ -1,4 +1,4 @@
1
- {"name": "object_set", "op": {"key": "node-1", "otype": "object.set", "path": "metadata", "value": {"author": "user-1", "version": 1}}}
2
- {"name": "object_set_empty", "op": {"key": "node-1", "otype": "object.set", "path": "metadata", "value": {}}}
3
- {"name": "object_merge", "op": {"key": "node-1", "otype": "object.merge", "path": "metadata", "value": {"tags": ["a", "b"]}}}
4
- {"name": "object_merge_nested", "op": {"key": "node-1", "otype": "object.merge", "path": "config", "value": {"settings": {"enabled": true}}}}
1
+ {"name": "object_set", "op": {"key": "node-1", "ot": "object.set", "path": "metadata", "value": {"author": "user-1", "version": 1}}}
2
+ {"name": "object_set_empty", "op": {"key": "node-1", "ot": "object.set", "path": "metadata", "value": {}}}
3
+ {"name": "object_merge", "op": {"key": "node-1", "ot": "object.merge", "path": "metadata", "value": {"tags": ["a", "b"]}}}
4
+ {"name": "object_merge_nested", "op": {"key": "node-1", "ot": "object.merge", "path": "config", "value": {"settings": {"enabled": true}}}}
@@ -1,7 +1,7 @@
1
- {"name": "number.set", "op": {"key": "cube-1", "otype": "number.set", "path": "opacity", "value": 0.5}, "expected": {"opacity": 0.5}}
2
- {"name": "number.add", "op": {"key": "cube-1", "otype": "number.add", "path": "score", "value": 10}, "expected": {"score": 10}}
3
- {"name": "vector3.set", "op": {"key": "cube-1", "otype": "vector3.set", "path": "position", "value": [1, 2, 3]}, "expected": {"position": [1, 2, 3]}}
4
- {"name": "vector3.add", "op": {"key": "cube-1", "otype": "vector3.add", "path": "position", "value": [0.1, 0.2, 0.3]}, "initial": {"position": [1, 2, 3]}, "expected": {"position": [1.1, 2.2, 3.3]}}
5
- {"name": "boolean.set", "op": {"key": "cube-1", "otype": "boolean.set", "path": "visible", "value": false}, "expected": {"visible": false}}
6
- {"name": "string.set", "op": {"key": "cube-1", "otype": "string.set", "path": "name", "value": "MyCube"}, "expected": {"name": "MyCube"}}
7
- {"name": "node.insert", "op": {"key": "sphere-1", "otype": "node.insert", "path": "sphere-1", "value": {"id": "uuid-sphere", "tag": "Mesh", "name": "Sphere"}}, "expected": {"nodes": {"sphere-1": {"id": "uuid-sphere", "tag": "Mesh", "name": "Sphere"}}}}
1
+ {"name": "number.set", "op": {"key": "cube-1", "ot": "number.set", "path": "opacity", "value": 0.5}, "expected": {"opacity": 0.5}}
2
+ {"name": "number.add", "op": {"key": "cube-1", "ot": "number.add", "path": "score", "value": 10}, "expected": {"score": 10}}
3
+ {"name": "vector3.set", "op": {"key": "cube-1", "ot": "vector3.set", "path": "position", "value": [1, 2, 3]}, "expected": {"position": [1, 2, 3]}}
4
+ {"name": "vector3.add", "op": {"key": "cube-1", "ot": "vector3.add", "path": "position", "value": [0.1, 0.2, 0.3]}, "initial": {"position": [1, 2, 3]}, "expected": {"position": [1.1, 2.2, 3.3]}}
5
+ {"name": "boolean.set", "op": {"key": "cube-1", "ot": "boolean.set", "path": "visible", "value": false}, "expected": {"visible": false}}
6
+ {"name": "string.set", "op": {"key": "cube-1", "ot": "string.set", "path": "name", "value": "MyCube"}, "expected": {"name": "MyCube"}}
7
+ {"name": "node.insert", "op": {"key": "sphere-1", "ot": "node.insert", "path": "sphere-1", "value": {"id": "uuid-sphere", "tag": "Mesh", "name": "Sphere"}}, "expected": {"nodes": {"sphere-1": {"id": "uuid-sphere", "tag": "Mesh", "name": "Sphere"}}}}
@@ -1,4 +1,4 @@
1
- {"name": "string_set", "op": {"key": "node-1", "otype": "string.set", "path": "name", "value": "Player"}}
2
- {"name": "string_set_empty", "op": {"key": "node-1", "otype": "string.set", "path": "name", "value": ""}}
3
- {"name": "string_concat", "op": {"key": "node-1", "otype": "string.concat", "path": "tags", "value": "active"}}
4
- {"name": "string_concat_separator", "op": {"key": "node-1", "otype": "string.concat", "path": "tags", "value": "enemy", "separator": ","}}
1
+ {"name": "string_set", "op": {"key": "node-1", "ot": "string.set", "path": "name", "value": "Player"}}
2
+ {"name": "string_set_empty", "op": {"key": "node-1", "ot": "string.set", "path": "name", "value": ""}}
3
+ {"name": "string_concat", "op": {"key": "node-1", "ot": "string.concat", "path": "tags", "value": "active"}}
4
+ {"name": "string_concat_separator", "op": {"key": "node-1", "ot": "string.concat", "path": "tags", "value": "enemy", "separator": ","}}
@@ -1,3 +1,3 @@
1
- {"name": "undo_single_edit", "steps": [{"action": "edit", "op": {"key": "cube-1", "otype": "vector3.set", "path": "position", "value": [5, 5, 5]}}, {"action": "commit"}, {"action": "undo"}], "expected": {"position": [0, 0, 0]}}
2
- {"name": "redo_after_undo", "steps": [{"action": "edit", "op": {"key": "cube-1", "otype": "vector3.set", "path": "position", "value": [5, 5, 5]}}, {"action": "commit"}, {"action": "undo"}, {"action": "redo"}], "expected": {"position": [5, 5, 5]}}
3
- {"name": "undo_uncommitted", "steps": [{"action": "edit", "op": {"key": "cube-1", "otype": "vector3.set", "path": "position", "value": [5, 5, 5]}}, {"action": "undo"}], "expected": {"position": [0, 0, 0]}}
1
+ {"name": "undo_single_edit", "steps": [{"action": "edit", "op": {"key": "cube-1", "ot": "vector3.set", "path": "position", "value": [5, 5, 5]}}, {"action": "commit"}, {"action": "undo"}], "expected": {"position": [0, 0, 0]}}
2
+ {"name": "redo_after_undo", "steps": [{"action": "edit", "op": {"key": "cube-1", "ot": "vector3.set", "path": "position", "value": [5, 5, 5]}}, {"action": "commit"}, {"action": "undo"}, {"action": "redo"}], "expected": {"position": [5, 5, 5]}}
3
+ {"name": "undo_uncommitted", "steps": [{"action": "edit", "op": {"key": "cube-1", "ot": "vector3.set", "path": "position", "value": [5, 5, 5]}}, {"action": "undo"}], "expected": {"position": [0, 0, 0]}}
@@ -1,17 +1,17 @@
1
- {"name": "vector3_set", "op": {"key": "node-1", "otype": "vector3.set", "path": "position", "value": [1, 2, 3]}}
2
- {"name": "vector3_set_zero", "op": {"key": "node-1", "otype": "vector3.set", "path": "position", "value": [0, 0, 0]}}
3
- {"name": "vector3_add", "op": {"key": "node-1", "otype": "vector3.add", "path": "position", "value": [0.5, 0.5, 0.5]}}
4
- {"name": "vector3_add_negative", "op": {"key": "node-1", "otype": "vector3.add", "path": "position", "value": [-1, -1, -1]}}
5
- {"name": "vector3_multiply", "op": {"key": "node-1", "otype": "vector3.multiply", "path": "scale", "value": [2, 2, 2]}}
6
- {"name": "vector3_multiply_non_uniform", "op": {"key": "node-1", "otype": "vector3.multiply", "path": "scale", "value": [1, 2, 3]}}
7
- {"name": "quaternion_set", "op": {"key": "node-1", "otype": "quaternion.set", "path": "rotation", "value": [0, 0, 0, 1]}}
8
- {"name": "quaternion_set_rotated", "op": {"key": "node-1", "otype": "quaternion.set", "path": "rotation", "value": [0, 0.7071, 0, 0.7071]}}
9
- {"name": "quaternion_multiply", "op": {"key": "node-1", "otype": "quaternion.multiply", "path": "rotation", "value": [0, 0.3827, 0, 0.9239]}}
10
- {"name": "vector3_apply_euler", "op": {"key": "node-1", "otype": "vector3.applyEuler", "path": "direction", "value": [0, 1.5708, 0]}}
11
- {"name": "vector3_apply_euler_zero", "op": {"key": "node-1", "otype": "vector3.applyEuler", "path": "direction", "value": [0, 0, 0]}}
12
- {"name": "vector3_apply_quaternion", "op": {"key": "node-1", "otype": "vector3.applyQuaternion", "path": "direction", "value": [0, 0.7071, 0, 0.7071]}}
13
- {"name": "vector3_apply_quaternion_identity", "op": {"key": "node-1", "otype": "vector3.applyQuaternion", "path": "direction", "value": [0, 0, 0, 1]}}
14
- {"name": "euler_set", "op": {"key": "node-1", "otype": "euler.set", "path": "eulerRotation", "value": [0, 1.5708, 0]}}
15
- {"name": "euler_set_zero", "op": {"key": "node-1", "otype": "euler.set", "path": "eulerRotation", "value": [0, 0, 0]}}
16
- {"name": "euler_add", "op": {"key": "node-1", "otype": "euler.add", "path": "eulerRotation", "value": [0.1, 0.2, 0.3]}}
17
- {"name": "euler_add_negative", "op": {"key": "node-1", "otype": "euler.add", "path": "eulerRotation", "value": [-0.5, -0.5, -0.5]}}
1
+ {"name": "vector3_set", "op": {"key": "node-1", "ot": "vector3.set", "path": "position", "value": [1, 2, 3]}}
2
+ {"name": "vector3_set_zero", "op": {"key": "node-1", "ot": "vector3.set", "path": "position", "value": [0, 0, 0]}}
3
+ {"name": "vector3_add", "op": {"key": "node-1", "ot": "vector3.add", "path": "position", "value": [0.5, 0.5, 0.5]}}
4
+ {"name": "vector3_add_negative", "op": {"key": "node-1", "ot": "vector3.add", "path": "position", "value": [-1, -1, -1]}}
5
+ {"name": "vector3_multiply", "op": {"key": "node-1", "ot": "vector3.multiply", "path": "scale", "value": [2, 2, 2]}}
6
+ {"name": "vector3_multiply_non_uniform", "op": {"key": "node-1", "ot": "vector3.multiply", "path": "scale", "value": [1, 2, 3]}}
7
+ {"name": "quaternion_set", "op": {"key": "node-1", "ot": "quaternion.set", "path": "rotation", "value": [0, 0, 0, 1]}}
8
+ {"name": "quaternion_set_rotated", "op": {"key": "node-1", "ot": "quaternion.set", "path": "rotation", "value": [0, 0.7071, 0, 0.7071]}}
9
+ {"name": "quaternion_multiply", "op": {"key": "node-1", "ot": "quaternion.multiply", "path": "rotation", "value": [0, 0.3827, 0, 0.9239]}}
10
+ {"name": "vector3_apply_euler", "op": {"key": "node-1", "ot": "vector3.applyEuler", "path": "direction", "value": [0, 1.5708, 0]}}
11
+ {"name": "vector3_apply_euler_zero", "op": {"key": "node-1", "ot": "vector3.applyEuler", "path": "direction", "value": [0, 0, 0]}}
12
+ {"name": "vector3_apply_quaternion", "op": {"key": "node-1", "ot": "vector3.applyQuaternion", "path": "direction", "value": [0, 0.7071, 0, 0.7071]}}
13
+ {"name": "vector3_apply_quaternion_identity", "op": {"key": "node-1", "ot": "vector3.applyQuaternion", "path": "direction", "value": [0, 0, 0, 1]}}
14
+ {"name": "euler_set", "op": {"key": "node-1", "ot": "euler.set", "path": "eulerRotation", "value": [0, 1.5708, 0]}}
15
+ {"name": "euler_set_zero", "op": {"key": "node-1", "ot": "euler.set", "path": "eulerRotation", "value": [0, 0, 0]}}
16
+ {"name": "euler_add", "op": {"key": "node-1", "ot": "euler.add", "path": "eulerRotation", "value": [0.1, 0.2, 0.3]}}
17
+ {"name": "euler_add_negative", "op": {"key": "node-1", "ot": "euler.add", "path": "eulerRotation", "value": [-0.5, -0.5, -0.5]}}
@@ -28,10 +28,10 @@ function loadOps(filename: string): OpFixture[] {
28
28
  function createMsg(op: Operation, lamport = 1): CRDTMessage {
29
29
  return {
30
30
  id: `msg-${Math.random().toString(36).slice(2)}`,
31
- sessionId: 'test-session',
31
+ client: 'test-session',
32
32
  clock: { 'test-session': lamport },
33
- lamportTime: lamport,
34
- timestamp: Date.now() / 1000,
33
+ lt: lamport,
34
+ ts: Date.now() / 1000,
35
35
  ops: [op],
36
36
  };
37
37
  }
@@ -43,7 +43,7 @@ function createNodeWithProps(
43
43
  ): SceneGraph {
44
44
  const nodeOp: Operation = {
45
45
  key: '',
46
- otype: 'node.insert',
46
+ ot: 'node.insert',
47
47
  path: 'children',
48
48
  value: {
49
49
  key,
@@ -28,10 +28,10 @@ function loadOps(filename: string): OpFixture[] {
28
28
  function createMsg(op: Operation, lamport = 1): CRDTMessage {
29
29
  return {
30
30
  id: `msg-${Math.random().toString(36).slice(2)}`,
31
- sessionId: 'test-session',
31
+ client: 'test-session',
32
32
  clock: { 'test-session': lamport },
33
- lamportTime: lamport,
34
- timestamp: Date.now() / 1000,
33
+ lt: lamport,
34
+ ts: Date.now() / 1000,
35
35
  ops: [op],
36
36
  };
37
37
  }
@@ -119,7 +119,7 @@ describe('Node Operations', () => {
119
119
  // Insert same mesh again with different name
120
120
  const duplicateOp: Operation = {
121
121
  key: 'scene',
122
- otype: 'node.insert',
122
+ ot: 'node.insert',
123
123
  path: 'children',
124
124
  value: {
125
125
  key: 'cube-1',
@@ -193,7 +193,7 @@ describe('Node Operations', () => {
193
193
 
194
194
  const removeOp: Operation = {
195
195
  key: 'scene',
196
- otype: 'node.remove',
196
+ ot: 'node.remove',
197
197
  path: 'children',
198
198
  value: 'non-existent',
199
199
  };
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Test: Operation ordering bug that caused IME divergence
3
+ *
4
+ * This test reproduces the exact scenario from message-log-1772398431392.json
5
+ * where operations arrived with non-monotonic sequence numbers:
6
+ * seq: 40, 50, 59, 62, 46, 56, 64, 60, 63
7
+ *
8
+ * The fix ensures operations are sorted by Lamport timestamp before applying.
9
+ */
10
+
11
+ import { describe, it, expect } from '@jest/globals';
12
+ import { applyMessage, createEmptyGraph } from '../../src/operations/dispatcher.js';
13
+ import type { CRDTMessage } from '../../src/operations/OperationTypes.js';
14
+
15
+ describe('Operation Ordering - IME Divergence Bug', () => {
16
+ it('should apply operations in Lamport order when array has non-monotonic seq', () => {
17
+ // Create initial graph with text node
18
+ let graph = createEmptyGraph();
19
+
20
+ // Initialize root and text node
21
+ const initMsg: CRDTMessage = {
22
+ id: 'msg-init',
23
+ client: 'Misaka-08330-hy6',
24
+ clock: { 'Misaka-08330-hy6': 1 },
25
+ lt: 1,
26
+ ts: 1000.0,
27
+ ops: [
28
+ {
29
+ ot: 'node.insert',
30
+ key: '',
31
+ path: 'children',
32
+ value: { key: 'default-scene', tag: 'Scene', name: 'Scene' },
33
+ },
34
+ {
35
+ ot: 'node.insert',
36
+ key: 'default-scene',
37
+ path: 'children',
38
+ value: { key: 'text-doc', tag: 'Text', name: 'Text' },
39
+ },
40
+ {
41
+ ot: 'text.init',
42
+ key: 'text-doc',
43
+ path: 'content',
44
+ value: '',
45
+ } as any,
46
+ ],
47
+ };
48
+ graph = applyMessage(graph, initMsg);
49
+
50
+ // Simulate operations arriving in non-monotonic seq order
51
+ // This reproduces entry #21 from the divergence log
52
+ const msg: CRDTMessage = {
53
+ id: 'msg-ime',
54
+ client: 'Misaka-08330-hy6',
55
+ clock: { 'Misaka-08330-hy6': 2 },
56
+ lt: 64,
57
+ ts: 1001.0,
58
+ ops: [
59
+ // Op #1: seq:40
60
+ {
61
+ ot: 'text.insert',
62
+ key: 'text-doc',
63
+ path: 'content',
64
+ id: 'Misaka-08330-hy6:7',
65
+ value: [null, 'wo z'],
66
+ seq: 40,
67
+ ts: 1001.0,
68
+ } as any,
69
+ // Op #2: seq:50
70
+ {
71
+ ot: 'text.insert',
72
+ key: 'text-doc',
73
+ path: 'content',
74
+ id: 'Misaka-08330-hy6:24',
75
+ value: ['Misaka-08330-hy6:7', 'ch'],
76
+ seq: 50,
77
+ ts: 1001.1,
78
+ } as any,
79
+ // Op #3: seq:59 - creates ID that will be deleted
80
+ {
81
+ ot: 'text.insert',
82
+ key: 'text-doc',
83
+ path: 'content',
84
+ id: 'Misaka-08330-hy6:37',
85
+ value: ['Misaka-08330-hy6:24', 's'],
86
+ seq: 59,
87
+ ts: 1001.2,
88
+ } as any,
89
+ // Op #4: seq:62
90
+ {
91
+ ot: 'text.insert',
92
+ key: 'text-doc',
93
+ path: 'content',
94
+ id: 'Misaka-08330-hy6:39',
95
+ value: ['Misaka-08330-hy6:37', 'u r'],
96
+ seq: 62,
97
+ ts: 1001.3,
98
+ } as any,
99
+ // Op #5: seq:46 - GOES BACKWARDS!
100
+ {
101
+ ot: 'text.insert',
102
+ key: 'text-doc',
103
+ path: 'content',
104
+ id: 'Misaka-08330-hy6:17',
105
+ value: ['Misaka-08330-hy6:7', ' l'],
106
+ seq: 46,
107
+ ts: 1001.05,
108
+ } as any,
109
+ // Op #6: seq:56
110
+ {
111
+ ot: 'text.insert',
112
+ key: 'text-doc',
113
+ path: 'content',
114
+ id: 'Misaka-08330-hy6:33',
115
+ value: ['Misaka-08330-hy6:24', 'i'],
116
+ seq: 56,
117
+ ts: 1001.15,
118
+ } as any,
119
+ // Op #7: seq:64 - replace that deletes id:37 from op #3
120
+ {
121
+ ot: 'text.replace',
122
+ key: 'text-doc',
123
+ path: 'content',
124
+ position: 0,
125
+ length: 6,
126
+ value: [null, '输入'],
127
+ rm: [['Misaka-08330-hy6:37', 6]],
128
+ id: 'Misaka-08330-hy6:43',
129
+ seq: 64,
130
+ ts: 1001.4,
131
+ } as any,
132
+ // Op #8: seq:60
133
+ {
134
+ ot: 'text.insert',
135
+ key: 'text-doc',
136
+ path: 'content',
137
+ id: 'Misaka-08330-hy6:38',
138
+ value: ['Misaka-08330-hy6:37', 'h'],
139
+ seq: 60,
140
+ ts: 1001.25,
141
+ } as any,
142
+ // Op #9: delete (no seq shown in log, using 65)
143
+ {
144
+ ot: 'text.delete',
145
+ key: 'text-doc',
146
+ path: 'content',
147
+ position: 0,
148
+ length: 1,
149
+ rm: [['Misaka-08330-hy6:36', 1]],
150
+ seq: 65,
151
+ ts: 1001.45,
152
+ } as any,
153
+ // Op #10: seq:63
154
+ {
155
+ ot: 'text.insert',
156
+ key: 'text-doc',
157
+ path: 'content',
158
+ id: 'Misaka-08330-hy6:42',
159
+ value: ['Misaka-08330-hy6:38', 'u'],
160
+ seq: 63,
161
+ ts: 1001.35,
162
+ } as any,
163
+ ],
164
+ };
165
+
166
+ // Apply the message - operations should be sorted before applying
167
+ const result = applyMessage(graph, msg);
168
+
169
+ // The key test: operations should have been applied in seq order
170
+ // (40, 46, 50, 56, 59, 60, 62, 63, 64, 65) NOT array order
171
+ // This should produce consistent text without the CRDT throwing errors
172
+ expect(result.nodes['text-doc']).toBeDefined();
173
+ expect(result.nodes['text-doc'].content).toBeDefined();
174
+
175
+ // The exact text content depends on the CRDT merge logic,
176
+ // but it should not throw errors and should be deterministic
177
+ const text = String(result.nodes['text-doc'].content);
178
+ expect(text).toBeTruthy();
179
+
180
+ // Most importantly: this should NOT throw an error about missing IDs
181
+ // The replace operation (seq:64) that deletes id:37 should be applied
182
+ // AFTER the insert operation (seq:59) that creates id:37
183
+ });
184
+
185
+ it('should sort operations with only seq metadata', () => {
186
+ let graph = createEmptyGraph();
187
+
188
+ // Create nodes first
189
+ const initMsg: CRDTMessage = {
190
+ id: 'msg-init',
191
+ client: 'alice',
192
+ clock: { alice: 0 },
193
+ lt: 0,
194
+ ts: 999.0,
195
+ ops: [
196
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-1' } },
197
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-2' } },
198
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-3' } },
199
+ ],
200
+ };
201
+ graph = applyMessage(graph, initMsg);
202
+
203
+ const msg: CRDTMessage = {
204
+ id: 'msg-1',
205
+ client: 'alice',
206
+ clock: { alice: 1 },
207
+ lt: 50,
208
+ ts: 1000.0,
209
+ ops: [
210
+ {
211
+ ot: 'number.set',
212
+ key: 'node-1',
213
+ path: 'value',
214
+ value: 40,
215
+ seq: 40,
216
+ } as any,
217
+ {
218
+ ot: 'number.set',
219
+ key: 'node-2',
220
+ path: 'value',
221
+ value: 30,
222
+ seq: 30,
223
+ } as any,
224
+ {
225
+ ot: 'number.set',
226
+ key: 'node-3',
227
+ path: 'value',
228
+ value: 50,
229
+ seq: 50,
230
+ } as any,
231
+ ],
232
+ };
233
+
234
+ // Operations should be applied in seq order: 30, 40, 50
235
+ const result = applyMessage(graph, msg);
236
+ expect(result.nodes['node-1'].value).toBe(40);
237
+ expect(result.nodes['node-2'].value).toBe(30);
238
+ expect(result.nodes['node-3'].value).toBe(50);
239
+ });
240
+
241
+ it('should handle operations without seq/ts/id gracefully', () => {
242
+ let graph = createEmptyGraph();
243
+
244
+ // Create nodes first
245
+ const initMsg: CRDTMessage = {
246
+ id: 'msg-init',
247
+ client: 'alice',
248
+ clock: { alice: 0 },
249
+ lt: 0,
250
+ ts: 999.0,
251
+ ops: [
252
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-1' } },
253
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-2' } },
254
+ ],
255
+ };
256
+ graph = applyMessage(graph, initMsg);
257
+
258
+ const msg: CRDTMessage = {
259
+ id: 'msg-1',
260
+ client: 'alice',
261
+ clock: { alice: 1 },
262
+ lt: 10,
263
+ ts: 1000.0,
264
+ ops: [
265
+ {
266
+ ot: 'number.set',
267
+ key: 'node-1',
268
+ path: 'value',
269
+ value: 1,
270
+ } as any,
271
+ {
272
+ ot: 'number.set',
273
+ key: 'node-2',
274
+ path: 'value',
275
+ value: 2,
276
+ } as any,
277
+ ],
278
+ };
279
+
280
+ // Should not throw even without seq/ts/id
281
+ const result = applyMessage(graph, msg);
282
+ expect(result.nodes['node-1'].value).toBe(1);
283
+ expect(result.nodes['node-2'].value).toBe(2);
284
+ });
285
+
286
+ it('should use ts as tiebreaker when seq is equal', () => {
287
+ let graph = createEmptyGraph();
288
+
289
+ // Create nodes first
290
+ const initMsg: CRDTMessage = {
291
+ id: 'msg-init',
292
+ client: 'alice',
293
+ clock: { alice: 0 },
294
+ lt: 0,
295
+ ts: 999.0,
296
+ ops: [
297
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-1' } },
298
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-2' } },
299
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-3' } },
300
+ ],
301
+ };
302
+ graph = applyMessage(graph, initMsg);
303
+
304
+ const msg: CRDTMessage = {
305
+ id: 'msg-1',
306
+ client: 'alice',
307
+ clock: { alice: 1 },
308
+ lt: 40,
309
+ ts: 1000.0,
310
+ ops: [
311
+ {
312
+ ot: 'number.set',
313
+ key: 'node-1',
314
+ path: 'value',
315
+ value: 1,
316
+ seq: 40,
317
+ ts: 1000.2,
318
+ } as any,
319
+ {
320
+ ot: 'number.set',
321
+ key: 'node-2',
322
+ path: 'value',
323
+ value: 2,
324
+ seq: 40,
325
+ ts: 1000.1,
326
+ } as any,
327
+ {
328
+ ot: 'number.set',
329
+ key: 'node-3',
330
+ path: 'value',
331
+ value: 3,
332
+ seq: 40,
333
+ ts: 1000.3,
334
+ } as any,
335
+ ],
336
+ };
337
+
338
+ // Should sort by ts when seq is equal: 1000.1, 1000.2, 1000.3
339
+ const result = applyMessage(graph, msg);
340
+ expect(result.nodes['node-1'].value).toBe(1);
341
+ expect(result.nodes['node-2'].value).toBe(2);
342
+ expect(result.nodes['node-3'].value).toBe(3);
343
+ });
344
+
345
+ it('should use id as final tiebreaker', () => {
346
+ let graph = createEmptyGraph();
347
+
348
+ // Create nodes first
349
+ const initMsg: CRDTMessage = {
350
+ id: 'msg-init',
351
+ client: 'alice',
352
+ clock: { alice: 0 },
353
+ lt: 0,
354
+ ts: 999.0,
355
+ ops: [
356
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-1' } },
357
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-2' } },
358
+ { ot: 'node.insert', key: '', path: 'children', value: { key: 'node-3' } },
359
+ ],
360
+ };
361
+ graph = applyMessage(graph, initMsg);
362
+
363
+ const msg: CRDTMessage = {
364
+ id: 'msg-1',
365
+ client: 'alice',
366
+ clock: { alice: 1 },
367
+ lt: 40,
368
+ ts: 1000.0,
369
+ ops: [
370
+ {
371
+ ot: 'number.set',
372
+ key: 'node-1',
373
+ path: 'value',
374
+ value: 1,
375
+ seq: 40,
376
+ ts: 1000.0,
377
+ id: 'charlie:1',
378
+ } as any,
379
+ {
380
+ ot: 'number.set',
381
+ key: 'node-2',
382
+ path: 'value',
383
+ value: 2,
384
+ seq: 40,
385
+ ts: 1000.0,
386
+ id: 'alice:1',
387
+ } as any,
388
+ {
389
+ ot: 'number.set',
390
+ key: 'node-3',
391
+ path: 'value',
392
+ value: 3,
393
+ seq: 40,
394
+ ts: 1000.0,
395
+ id: 'bob:1',
396
+ } as any,
397
+ ],
398
+ };
399
+
400
+ // Should sort by id when seq and ts are equal: alice, bob, charlie
401
+ const result = applyMessage(graph, msg);
402
+ expect(result.nodes['node-1'].value).toBe(1);
403
+ expect(result.nodes['node-2'].value).toBe(2);
404
+ expect(result.nodes['node-3'].value).toBe(3);
405
+ });
406
+ });
@@ -28,10 +28,10 @@ function loadOps(filename: string): OpFixture[] {
28
28
  function createMsg(op: Operation, lamport = 1): CRDTMessage {
29
29
  return {
30
30
  id: `msg-${Math.random().toString(36).slice(2)}`,
31
- sessionId: 'test-session',
31
+ client: 'test-session',
32
32
  clock: { 'test-session': lamport },
33
- lamportTime: lamport,
34
- timestamp: Date.now() / 1000,
33
+ lt: lamport,
34
+ ts: Date.now() / 1000,
35
35
  ops: [op],
36
36
  };
37
37
  }
@@ -43,7 +43,7 @@ function createNodeWithProps(
43
43
  ): SceneGraph {
44
44
  const nodeOp: Operation = {
45
45
  key: '',
46
- otype: 'node.insert',
46
+ ot: 'node.insert',
47
47
  path: 'children',
48
48
  value: {
49
49
  key,