NOT-OMEGA commited on
Commit
bc35c16
·
verified ·
1 Parent(s): 2d7267d

Rename ot_engine.py to ot_engine.hpp

Browse files
Files changed (2) hide show
  1. ot_engine.hpp +164 -0
  2. ot_engine.py +0 -190
ot_engine.hpp ADDED
@@ -0,0 +1,164 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #pragma once
2
+ /*
3
+ * CollabDocs C++ — Operational Transformation Engine
4
+ *
5
+ * Implements:
6
+ * insert-insert, insert-delete, delete-insert, delete-delete
7
+ * with cursor sync and transform_against_log.
8
+ *
9
+ * All operations are VALUE types (copyable, no heap allocation).
10
+ */
11
+
12
+ #include <string>
13
+ #include <vector>
14
+ #include <algorithm>
15
+ #include <stdexcept>
16
+
17
+ namespace collab {
18
+
19
+ // ─── Operation ───────────────────────────────────────────────────────────────
20
+
21
+ enum class OpType { INSERT, DELETE };
22
+
23
+ struct Operation {
24
+ OpType type;
25
+ int position = 0;
26
+ std::string value; // meaningful for INSERT
27
+ int length = 0; // meaningful for DELETE (insert: derived)
28
+ int base_version = 0;
29
+ std::string user_id;
30
+ std::string op_id;
31
+
32
+ // Normalize: insert.length always == value.size()
33
+ void normalize() {
34
+ if (type == OpType::INSERT)
35
+ length = static_cast<int>(value.size());
36
+ }
37
+ };
38
+
39
+ // ─── Pairwise transform functions ────────────────────────────────────────────
40
+
41
+ // insert vs insert
42
+ inline Operation transform_ii(Operation op, const Operation& against) {
43
+ if (against.position < op.position) {
44
+ op.position += static_cast<int>(against.value.size());
45
+ } else if (against.position == op.position) {
46
+ // Tie-break: lower user_id wins (goes first)
47
+ if (against.user_id <= op.user_id)
48
+ op.position += static_cast<int>(against.value.size());
49
+ }
50
+ return op;
51
+ }
52
+
53
+ // delete vs insert (delete incoming, insert already applied)
54
+ // "delete wins" — absorbs chars inserted inside its range
55
+ inline Operation transform_di(Operation op, const Operation& against) {
56
+ int ins_pos = against.position;
57
+ int ins_len = static_cast<int>(against.value.size());
58
+ int del_end = op.position + op.length;
59
+
60
+ if (ins_pos < op.position) {
61
+ op.position += ins_len;
62
+ } else if (ins_pos <= del_end) {
63
+ op.length += ins_len;
64
+ }
65
+ return op;
66
+ }
67
+
68
+ // insert vs delete (insert incoming, delete already applied)
69
+ // if insert fell inside deleted range → collapse to del_start
70
+ inline Operation transform_id(Operation op, const Operation& against) {
71
+ int del_start = against.position;
72
+ int del_end = against.position + against.length;
73
+
74
+ if (del_end <= op.position) {
75
+ op.position -= against.length;
76
+ } else if (del_start < op.position) {
77
+ op.position = del_start;
78
+ }
79
+ return op;
80
+ }
81
+
82
+ // delete vs delete
83
+ inline Operation transform_dd(Operation op, const Operation& against) {
84
+ int op_start = op.position;
85
+ int op_end = op.position + op.length;
86
+ int ag_start = against.position;
87
+ int ag_end = against.position + against.length;
88
+
89
+ if (ag_end <= op_start) {
90
+ op.position -= against.length;
91
+ } else if (ag_start >= op_end) {
92
+ // no change
93
+ } else {
94
+ int overlap_start = std::max(op_start, ag_start);
95
+ int overlap_end = std::min(op_end, ag_end);
96
+ int overlap = overlap_end - overlap_start;
97
+
98
+ if (ag_start < op_start)
99
+ op.position = ag_start;
100
+
101
+ op.length = std::max(0, op.length - overlap);
102
+ }
103
+ return op;
104
+ }
105
+
106
+ // ─── Dispatch ────────────────────────────────────────────────────────────────
107
+
108
+ inline Operation transform_operation(Operation incoming, const Operation& applied) {
109
+ if (incoming.type == OpType::INSERT) {
110
+ if (applied.type == OpType::INSERT) return transform_ii(incoming, applied);
111
+ else return transform_id(incoming, applied);
112
+ } else {
113
+ if (applied.type == OpType::INSERT) return transform_di(incoming, applied);
114
+ else return transform_dd(incoming, applied);
115
+ }
116
+ }
117
+
118
+ // Transform `incoming` (based at `from_version`) against log entries
119
+ // with version in range (from_version, to_version].
120
+ inline Operation transform_against_log(
121
+ Operation incoming,
122
+ const std::vector<std::pair<int, Operation>>& op_log,
123
+ int from_version,
124
+ int to_version)
125
+ {
126
+ for (auto& [ver, applied_op] : op_log) {
127
+ if (ver > from_version && ver <= to_version)
128
+ incoming = transform_operation(incoming, applied_op);
129
+ }
130
+ return incoming;
131
+ }
132
+
133
+ // ─── Apply ───────────────────────────────────────────────────────────────────
134
+
135
+ inline std::string apply_operation(const std::string& content, const Operation& op) {
136
+ int len = static_cast<int>(content.size());
137
+ if (op.type == OpType::INSERT) {
138
+ int pos = std::max(0, std::min(op.position, len));
139
+ return content.substr(0, pos) + op.value + content.substr(pos);
140
+ } else {
141
+ if (op.length <= 0) return content;
142
+ int pos = std::max(0, std::min(op.position, len));
143
+ int end = std::max(0, std::min(pos + op.length, len));
144
+ return content.substr(0, pos) + content.substr(end);
145
+ }
146
+ }
147
+
148
+ // ─── Cursor sync ─────────────────────────────────────────────────────────────
149
+
150
+ inline int transform_cursor(int cursor, const Operation& op) {
151
+ if (op.type == OpType::INSERT) {
152
+ if (op.position <= cursor)
153
+ cursor += static_cast<int>(op.value.size());
154
+ } else {
155
+ int del_end = op.position + op.length;
156
+ if (del_end <= cursor)
157
+ cursor -= op.length;
158
+ else if (op.position <= cursor)
159
+ cursor = op.position;
160
+ }
161
+ return cursor;
162
+ }
163
+
164
+ } // namespace collab
ot_engine.py DELETED
@@ -1,190 +0,0 @@
1
- """
2
- Operational Transformation Engine
3
- Implements insert-insert, insert-delete, delete-insert, delete-delete
4
- transformations with correct overlap handling.
5
-
6
- Key fixes over original:
7
- - delete-delete overlap now correctly handles all 4 positional cases
8
- - transform_against_log is O(n) in missed ops, not re-scanned
9
- - apply_operation is side-effect free (returns new string)
10
- - All operations are immutable (copy.deepcopy on transform)
11
- """
12
-
13
- from __future__ import annotations
14
-
15
- import copy
16
- from dataclasses import dataclass, field
17
- from typing import List, Literal, Tuple
18
-
19
-
20
- @dataclass
21
- class Operation:
22
- op_type: Literal["insert", "delete"]
23
- position: int
24
- value: str = "" # meaningful only for insert
25
- length: int = 1 # meaningful only for delete
26
- base_version: int = 0
27
- user_id: str = ""
28
- op_id: str = ""
29
-
30
- def __post_init__(self):
31
- # Normalize: insert length always derived from value
32
- if self.op_type == "insert":
33
- self.length = len(self.value)
34
-
35
-
36
- # ─── Pairwise transforms ──────────────────────────────────────────────────────
37
-
38
- def _transform_ii(op: Operation, against: Operation) -> Operation:
39
- """insert vs insert"""
40
- r = copy.deepcopy(op)
41
- if against.position < op.position:
42
- r.position += len(against.value)
43
- elif against.position == op.position:
44
- # Deterministic tie-break: lower user_id wins (goes first)
45
- if against.user_id <= op.user_id:
46
- r.position += len(against.value)
47
- return r
48
-
49
-
50
- def _transform_di(op: Operation, against: Operation) -> Operation:
51
- """
52
- delete vs insert (against=insert already applied on server).
53
-
54
- Policy: "delete wins" — a concurrent insert inside the delete range is
55
- absorbed by the delete (the delete expands to cover new chars).
56
- This is the dOPT/Jupiter convention: deletions are authoritative over
57
- concurrent inserts within their range.
58
- """
59
- r = copy.deepcopy(op)
60
- ins_len = len(against.value)
61
- ins_pos = against.position
62
- del_end = op.position + op.length
63
-
64
- if ins_pos < op.position:
65
- # Insert before delete start — shift delete right
66
- r.position += ins_len
67
- elif ins_pos <= del_end:
68
- # Insert at or inside delete range (including right boundary) —
69
- # expand delete to absorb the inserted chars
70
- r.length += ins_len
71
- # ins_pos > del_end: insert after delete — no change
72
- return r
73
-
74
-
75
- def _transform_id(op: Operation, against: Operation) -> Operation:
76
- """
77
- insert vs delete (against=delete already applied on server).
78
-
79
- Policy: "delete wins" — if the insert position falls inside the deleted
80
- range, collapse the insert to del_start (the insert still happens, but
81
- the content it was anchored to is gone).
82
- """
83
- r = copy.deepcopy(op)
84
- del_start = against.position
85
- del_end = against.position + against.length
86
-
87
- if del_end <= op.position:
88
- # Delete entirely before insert — shift left by deleted amount
89
- r.position -= against.length
90
- elif del_start < op.position:
91
- # Insert was inside deleted range — collapse to del_start
92
- r.position = del_start
93
- # del_start >= op.position: delete is at or after insert — no change
94
- return r
95
-
96
-
97
- def _transform_dd(op: Operation, against: Operation) -> Operation:
98
- """delete vs delete"""
99
- r = copy.deepcopy(op)
100
-
101
- op_start = op.position
102
- op_end = op.position + op.length
103
- ag_start = against.position
104
- ag_end = against.position + against.length
105
-
106
- if ag_end <= op_start:
107
- # Against entirely before op — shift op left
108
- r.position -= against.length
109
-
110
- elif ag_start >= op_end:
111
- # Against entirely after op — no change
112
- pass
113
-
114
- else:
115
- # Overlapping. Four sub-cases:
116
- overlap_start = max(op_start, ag_start)
117
- overlap_end = min(op_end, ag_end)
118
- overlap = overlap_end - overlap_start
119
-
120
- # Shift position if against starts before op
121
- if ag_start < op_start:
122
- # Characters before op that were deleted shift our start left
123
- r.position = ag_start
124
- # else: op starts before against, position unchanged
125
-
126
- # Reduce length by the overlap (already deleted by `against`)
127
- r.length = max(0, op.length - overlap)
128
-
129
- return r
130
-
131
-
132
- def transform_operation(incoming: Operation, applied: Operation) -> Operation:
133
- """Transform `incoming` against an already-applied `applied`."""
134
- if incoming.op_type == "insert":
135
- if applied.op_type == "insert":
136
- return _transform_ii(incoming, applied)
137
- else:
138
- return _transform_id(incoming, applied)
139
- else:
140
- if applied.op_type == "insert":
141
- return _transform_di(incoming, applied)
142
- else:
143
- return _transform_dd(incoming, applied)
144
-
145
-
146
- def transform_against_log(
147
- incoming: Operation,
148
- op_log: List[Tuple[int, Operation]],
149
- from_version: int,
150
- to_version: int,
151
- ) -> Operation:
152
- """
153
- Transform `incoming` (based at `from_version`) against every operation
154
- in op_log with version in range (from_version, to_version].
155
- """
156
- result = copy.deepcopy(incoming)
157
- for ver, applied_op in op_log:
158
- if from_version < ver <= to_version:
159
- result = transform_operation(result, applied_op)
160
- return result
161
-
162
-
163
- # ─── Apply ───────────────────────────────────────────────────────────────────
164
-
165
- def apply_operation(content: str, op: Operation) -> str:
166
- """Apply a single operation to content. Pure function."""
167
- if op.op_type == "insert":
168
- pos = max(0, min(op.position, len(content)))
169
- return content[:pos] + op.value + content[pos:]
170
- elif op.op_type == "delete":
171
- if op.length <= 0:
172
- return content
173
- pos = max(0, min(op.position, len(content)))
174
- end = max(0, min(pos + op.length, len(content)))
175
- return content[:pos] + content[end:]
176
- return content
177
-
178
-
179
- def transform_cursor(cursor_pos: int, op: Operation) -> int:
180
- """Adjust a cursor position given an applied operation."""
181
- if op.op_type == "insert":
182
- if op.position <= cursor_pos:
183
- return cursor_pos + len(op.value)
184
- elif op.op_type == "delete":
185
- del_end = op.position + op.length
186
- if del_end <= cursor_pos:
187
- return cursor_pos - op.length
188
- elif op.position <= cursor_pos:
189
- return op.position
190
- return cursor_pos