Skip to content

Commit 4e1aa89

Browse files
authored
Added OT Client transform functions (#194)
* Added OT Client transform functions * minor cleanup
1 parent 65fafff commit 4e1aa89

File tree

1 file changed

+91
-4
lines changed

1 file changed

+91
-4
lines changed

backend/OTClient/operation.ts

Lines changed: 91 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,94 @@
1-
export interface Operation {
2-
operation: string;
3-
index: number;
1+
export type Operation = {
2+
// TODO: we may need to remodel our operations and re-think how exactly this should be structured
3+
path: number[];
4+
editType: "insert" | "delete" | "noop";
45
}
56

7+
// constant noop type
8+
export const noop: Operation = { path: [], editType: "noop" };
69

7-
export const transform = (a: Operation, b: Operation): [Operation, Operation] => [{ operation: 'add', index: 3}, { operation: 'add', index: 3}];
10+
// transform takes in two operations and returns their transformed ops
11+
export const transform = (a: Operation, b: Operation): [Operation, Operation] => {
12+
const transformedPaths = transformPaths(a, b);
13+
[a.path, b.path] = transformedPaths;
14+
15+
return [normalise(a), normalise(b)];
16+
}
17+
18+
// transformPaths takes in two operations and transforms them accordingly, note that it only returns the updated paths
19+
const transformPaths = (a: Operation, b: Operation): [number[], number[]] => {
20+
const tp = transformationPoint(a.path, b.path);
21+
if (!effectIndependent(a.path, b.path, tp)) {
22+
switch (true) {
23+
case a.editType === "insert" && b.editType === "insert": return transformInserts(a.path, b.path, tp);
24+
case a.editType === "delete" && b.editType === "delete": return transformDeletes(a.path, b.path, tp);
25+
case a.editType === "insert" && b.editType === "delete": return transformInsertDelete(a.path, b.path, tp);
26+
default:
27+
const result = transformInsertDelete(b.path, a.path, tp);
28+
result.reverse();
29+
return result;
30+
}
31+
}
32+
33+
return [a.path, b.path];
34+
}
35+
36+
// transformInserts takes 2 paths and their transformation point and transforms them as if they were insertion functions
37+
const transformInserts = (a: number[], b: number[], tp: number): [number[], number[]] => {
38+
switch (true) {
39+
case a[tp] > b[tp]: return [update(a, tp, 1), b];
40+
case a[tp] < b[tp]: return [a, update(b, tp, 1)];
41+
default:
42+
return a.length > b.length
43+
? [update(a, tp, 1), b]
44+
: (a.length < b.length ? [a, update(b, tp, 1)] : [a, b]);
45+
}
46+
}
47+
48+
// transformDeletes takes 2 paths and transforms them as if they were deletion operations
49+
const transformDeletes = (a: number[], b: number[], tp: number): [number[], number[]] => {
50+
switch (true) {
51+
case a[tp] > b[tp]: return [update(a, tp, -1), b];
52+
case a[tp] < b[tp]: return [a, update(b, tp, -1)];
53+
default:
54+
return a.length > b.length
55+
? [[], b]
56+
: (a.length < b.length ? [a, []] : [[], []]);
57+
}
58+
}
59+
60+
// transformInsertDelete takes an insertion operation and a deletion operation and transforms them
61+
const transformInsertDelete = (insertOp: number[], deleteOp: number[], tp: number): [number[], number[]] => {
62+
switch (true) {
63+
case insertOp[tp] > deleteOp[tp]: return [update(insertOp, tp, -1), deleteOp];
64+
case insertOp[tp] < deleteOp[tp]: return [insertOp, update(deleteOp, tp, 1)];
65+
default:
66+
return insertOp.length > deleteOp.length
67+
? [[], deleteOp]
68+
: [insertOp, update(deleteOp, tp, 1)]
69+
}
70+
}
71+
72+
// update updates a specific index in a path
73+
const update = (pos: number[], toChange: number, change: number) => {
74+
pos[toChange] += change;
75+
return pos;
76+
}
77+
78+
// transformationPoint takes in two paths and computes their transformation point
79+
const transformationPoint = (a: number[], b: number[]): number =>
80+
[...Array(Math.max(a.length, b.length)).keys()]
81+
.find(i => (a[i] ?? -1) != (b[i] ?? -1)) ?? (Math.max(a.length, b.length) - 1);
82+
83+
84+
// effectIndependence takes two paths and determines if their effect is independent or not
85+
const effectIndependent = (a: number[], b: number[], transformationPoint: number): boolean =>
86+
a.length > (transformationPoint + 1) && b.length > (transformationPoint + 1) ||
87+
(a[transformationPoint] > b[transformationPoint] && a.length < b.length) ||
88+
(a[transformationPoint] < b[transformationPoint] && a.length > b.length);
89+
90+
// normalise turns an empty operation into a noop
91+
const normalise = (a: Operation): Operation =>
92+
a.path.length === 0
93+
? noop
94+
: a;

0 commit comments

Comments
 (0)