Skip to content

Path related issue with inverted operations #1

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
manufacturist opened this issue Aug 4, 2017 · 3 comments
Open

Path related issue with inverted operations #1

manufacturist opened this issue Aug 4, 2017 · 3 comments

Comments

@manufacturist
Copy link

manufacturist commented Aug 4, 2017

Hey @kbadk,

The issue I am having is with lines 87-90. The code works fine for every other scenario I've tested, however when trying to apply inverted operations it simply does not work because of the final path value change done at those lines.

Let's look at the following scenario. I want to bold the 'b' letter in the following HTML:

<body>
  <p>abc</p>
</body>

Obtaining:

<body>
  <p>a<strong>b</strong>c</p>
</body>

I'm doing this by running the json0-ot-diff code in the browser and retrieving the diffs between an older version of the JsonML (obtained from the body element) and the newer one (after the bold operation).

The following operations are obtained:

  1. {p:[2,2], ld:"abc", li:"a"} - Ok
  2. {p:[2,3], li: ["STRONG", {}, "b"]} - Ok
  3. {p:[2,5], li: "c"} - Why?

The inverted ops are the following (applied in this exact order):

  1. {p:[2,5], ld: "c"} - Will not apply correctly using the json0.apply(snapshot, op) function because there is nothing at path [2,5]
  2. {p:[2,3], ld: ["STRONG", {}, "b"]} - Ok
  3. {p:[2,2], ld:"a", ld:"abc"} - Ok

What is the reason for increasing / decreasing the final element of the path array by 1 for 'li' / 'ld' operations when if (equal(path, opParentPath))?

EDIT 1:
I've just understood the logic of the code, however I still think that there's a bug regarding the scenario above.

@kbadk
Copy link
Owner

kbadk commented Aug 4, 2017

Hi there,

I appreciate your feedback – thank you! You're right, that does seem like a bug, and I can't really explain why I wrote

if (equal(path, opParentPath)) {
    if ("li" in op) offset++;
    if ("ld" in op) offset--;
}

back then. After all, it's been more than a year since I last looked at the code.

However, when removing that, tests begin to fail, so that's something I'll have to look into before submitting a fix. Of course, if you already have a fix at hand – or if you're willing to spend a little time on it – pull requests are always welcome.

@manufacturist
Copy link
Author

manufacturist commented Aug 4, 2017

I'll give it a shot. The offset is used to update the path of operations after 'li' or 'ld' operations occur.

For example:

<body>
  <p id='1'></p>
  <p id='2'></p>
  <p id='3'></p>
</body>

Deleting the second and third 'p' elements would result in the following operations:

  1. {p:[3], ld:{"P", {id: '2'}} - The 'p' with id 2
  2. {p:[3], ld:{"P", {id: '3'}} - The 'p' with id 3 because it's now on the position of the 'p' with id 2

This happens because you wrote the code using a top-down approach when handling the generation of the operations. No need for offset if a bottom-up approach would have been chosen for calculating the diffs but there would have probably been other issues :)

EDIT 1: Added 'id' to the 2 operations used in the example (forgot to add them initially)

@manufacturist
Copy link
Author

I came up with 2 (+1) solutions:

For this repository, building the ops using a bottom-up approach would fix the issue. This means dropping the offset variable and creating the ops from the last element to the first:

for (var i = l; i >= 0; --i) {
	var newOps = diff(input[i], output[i], [...path, i]);
	ops.concat(newOps);
}
return ops;

This solution was great, however it does not mix well with inverted 'sd/si' operations (Fixable, however I do not have the time to do it).

The second solution (The one I actually used) was not applying the offset if there was no list delete operation within the generated ops. The solution works great for me, however there are scenarios which will render it useless (e.g. ld, li, li, li - the third li would have a bad path).

For this, I've kept a path map which was reset after before every optimizeDiff function call. The pathMap remembers the offset and the total number of ops for a certain parent path. It looks like this:

var pathMap = {};

[...]

// within the 'li' OR 'ld' op creation code (no reason to do it in the list replace op code)
var offsetKey = JSON.stringify(op.p.slice(0, -1));
var totalOpsKey = offsetKey + "_ops";

// for 'li' - TODO: optimize
pathMap[offsetKey] = typeof pathMap[offsetKey]  == "number" ? pathMap[offsetKey] + 1 : 1;
pathMap[totalOpsKey] = typeof pathMap[totalOpsKey]  == "number" ? pathMap[totalOpsKey] + 1 : 1;

[...]

// for 'ld' - TODO: optimize
pathMap[offsetKey] = typeof pathMap[offsetKey]  == "number" ? pathMap[offsetKey] - 1 : -1;
pathMap[totalOpsKey] = typeof pathMap[totalOpsKey]  == "number" ? pathMap[totalOpsKey] + 1 : 1;

[...]

// when creating the 'li' op
if(pathMap[offsetKey] == pathMap[totalOpsKey]) {
	op.p[op.p.length - 1] -= pathMap[offsetKey] - 1;
}

This solution works great for my use case and as far as I've seen, for this repository as well. However there are these rare scenarios which can cause some mess.

The third solution is pretty straight forward. It only adds one function to the code:

[...]
{
	[...]
	for (var i=0; i < l; ++i) {
		var newOps = diff(input[i], output[i], [...path, i + offset]);
		newOps.forEach(function(op) {
			var opParentPath = op.p.slice(0, -1);
			if (equal(path, opParentPath)) {
				if ("li" in op && mustIncrementOffset(path, op, input, output, ...)) {
					offset++;
				}

				if ("ld" in op) offset--;
			}
			ops.push(op);
		});
	}
	return ops;
}
[...]
function mustIncrementOffset(/*metadata params*/) {
	[...]
	return /*boolean*/;
}

This is the best solution, however after spending a lot of time on it trying to 'calculate' when to increment the offset with no success, I've decided to go with the second one (I mean, the second solution could be a deciding factor for the mustIncrementOffset function - if there's no list delete op generated, don't increment the offset).

I will not be doing a PR since I did not offer any viable solution (100% working for every scenario, including future 'si' / 'sd' operation support (loominc's fork)).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants