Skip to content

Different outcomes for Y.Text when artificially delaying text attribute updates #291

@raedle

Description

@raedle

Checklist

  • (UNSURE) Are you reporting a bug? Use github issues for bug reports and feature requests. For general questions, please use https://discuss.yjs.dev/
  • Try to report your issue in the correct repository. Yjs consists of many modules. When in doubt, report it to https://github.com/yjs/yjs/issues/

Describe the bug
I want to start off that I am unsure if what I am reporting is a bug or an expected behavior.

I am seeing different outcomes when remote updates for a Y.Text struct (specifically attribute updates) are applied locally but after another local change happened to text attributes.

The following code has three cases:

  1. Single text struct leading to the expected outcome
  2. Two text structs where remote updates are applied immediately and leading to the expected outcome
  3. Two text structs where remote updates are applied after another local change and leading to an unexpected outcome

To Reproduce
Steps to reproduce the behavior:

  1. Execute code or go to https://codesandbox.io/s/busy-cannon-59l6t?file=/src/index.ts
  2. Open console
  3. Check differences in logged text deltas

Example code:

import * as Y from "yjs";

// Create doc with text and initial text
const doc = new Y.Doc();
const text = doc.getText("text");
text.insert(0, "helloworld");
const initialState = Y.encodeStateAsUpdate(doc);

function getDoc(): Y.Doc {
  const doc = new Y.Doc();
  Y.applyUpdate(doc, initialState);
  return doc;
}

// 1. Single text struct
function oneText() {
  const doc1 = getDoc();
  const text1 = doc1.getText("text");

  text1.format(3, 3, { bold: true });
  text1.format(0, 4, { bold: true });

  console.log({
    test: "oneText",
    text1: text1.toDelta()
  });
}

// 2. Two text structs synced locally without and with a
// timeout.
//
// The timeout will delay applying the change on the other
// struct by putting it in a macro task. This means that
// both local changes will be applied before the update from
// the respective other struct.
//
// Without timeout
// text1 will apply format 3, 3, { bold: true }
// text2 will receive format 3, 3, { bold: true }
// text2 will apply format 0, 4, { bold: true }
// text1 will receive format 0, 4, { bold: true }
// 
// Expected output: [{"insert":"hellow","attributes":{"bold":true}},{"insert":"orld"}]
// Actual output: [{"insert":"hellow","attributes":{"bold":true}},{"insert":"orld"}]
//
// With timeout
// text1 will apply format 3, 3, { bold: true }
// text2 will apply format 0, 4, { bold: true }
// text2 will receive format 3, 3, { bold: true }
// text1 will receive format 0, 4, { bold: true }
//
// Expected output: [{"insert":"hellow","attributes":{"bold":true}},{"insert":"orld"}]
// Actual ouput: [{"insert":"hell","attributes":{"bold":true}},{"insert":"oworld"}]
function twoText(withTimeout: boolean = false) {
  const doc1 = getDoc();
  const text1 = doc1.getText("text");
  const doc2 = getDoc();
  const text2 = doc2.getText("text");

  doc1.on("update", (update: Uint8Array) => {
    if (withTimeout) {
      setTimeout(() => {
        Y.applyUpdate(doc2, update);
      }, 0);
    } else {
      Y.applyUpdate(doc2, update);
    }
  });
  doc2.on("update", (update: Uint8Array) => {
    if (withTimeout) {
      setTimeout(() => {
        Y.applyUpdate(doc1, update);
      }, 0);
    } else {
      Y.applyUpdate(doc1, update);
    }
  });

  doc1.transact(() => {
    text1.format(3, 3, { bold: true });
  });
  doc2.transact(() => {
    text2.format(0, 4, { bold: true });
  });

  if (withTimeout) {
    setTimeout(() => {
      console.log({
        test: "twoText",
        text1: text1.toDelta(),
        text2: text2.toDelta()
      });
    }, 0);
  } else {
    console.log({
      test: "twoText",
      text1: text1.toDelta(),
      text2: text2.toDelta()
    });
  }
}

oneText();
twoText();
twoText(true);

Screenshots
The screenshot shows the rendered console output. Compare the top text1/text2 combo with the bottom text1/text2 combo. The expected outcome is that the bottom combo would be the same as the top combo.

image

Environment Information

  • Browser Chrome Version 89.0.4389.114 (Official Build) (x86_64)
  • Node.js v12.16.3
  • Yjs 13.5.3

Metadata

Metadata

Assignees

Labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions