yjs / yjs

Shared data types for building collaborative software
https://docs.yjs.dev
Other
16.99k stars 616 forks source link

Merged document randomly picks up different values after merge #510

Closed vineetdigit closed 1 year ago

vineetdigit commented 1 year ago

Describe the bug

I am recreating a document by merging updates from two documents which have common ancestor as shown below.

But the merged document randomly picks the value from either document.

I was hoping that the merged document consitently picks one value between iterations of the loop below.

To Reproduce

const Y = require("yjs");

const Y = require("yjs");

for (let i = 0; i < 100; i++) {
  const yDocOrigin = new Y.Doc();
  yDocOrigin.getMap("map").set("key", "acme");
  originUpdate = Y.encodeStateAsUpdate(yDocOrigin);
  yDocOrigin.destroy();

  const yOurDoc = new Y.Doc();
  Y.applyUpdate(yOurDoc, originUpdate);
  yOurDoc.getMap("map").set("key", "acme-our");
  ourUpdate = Y.encodeStateAsUpdate(yOurDoc);
  yOurDoc.destroy();

  const yTheirDoc = new Y.Doc();
  Y.applyUpdate(yTheirDoc, originUpdate);
  yTheirDoc.getMap("map").set("key", "acme-their");
  theirUpdate = Y.encodeStateAsUpdate(yTheirDoc);
  yTheirDoc.destroy();

  const yMergedDoc = new Y.Doc();
  Y.applyUpdate(yMergedDoc, originUpdate);
  Y.applyUpdate(yMergedDoc, ourUpdate);
  Y.applyUpdate(yMergedDoc, theirUpdate);
  console.log(i, yMergedDoc.getMap("map").get("key")); // <- randomly picks 'acme-our' or 'acme-their'
  yMergedDoc.destroy();
}

Output

0 acme-their
1 acme-their
2 acme-our
3 acme-their
4 acme-their
5 acme-our
6 acme-their
7 acme-their
8 acme-our
9 acme-their
10 acme-their
11 acme-their
12 acme-our
13 acme-our
14 acme-their
15 acme-our
.... so on

Expected behavior

I am expecting the key to ALWAYS have the same value for all iterations of the loop

Environment Information

Huly®: YJS-340

dmonad commented 1 year ago

Hi @vineetdigit,

This is the expected behavior. The "winner" is decided by the ydoc.clientID of the document (which is a generated number). The higher clientID wins.

The reason is that this prevents a lot of other potential issues. E.g. assume that userA sets ymap.set('x', 0); ymap.set('y', 1000) and userB sets ymap.set('x', 1000); ymap.set('y', 0). The result should be consistently either { x: 0, y: 1000 } or { x: 1000, y: 0 } - which result shouldn't matter.

Comparing by clientID (=integer) is also A LOT more efficient than comparing arbitrary values which requires a lot more checks.

For testing, you can set the clientID manually - so you can reproduce potential issues. Try setting yOurDoc.clientID = 0; yTheirDoc = 1; .. Otherwise, please don't touch the clientID ;)

Also, thanks for your sponsorship! :heart: I would have sent you a thank-you mail, but couldn't find it in your account.