Skip to content
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

Issue with attribute changes lost during merge between docs #521

Open
ArsnealX opened this issue Apr 10, 2023 · 0 comments
Open

Issue with attribute changes lost during merge between docs #521

ArsnealX opened this issue Apr 10, 2023 · 0 comments
Assignees
Labels

Comments

@ArsnealX
Copy link

ArsnealX commented Apr 10, 2023

Describe the bug
I have encountered an issue while merging updates between two docs. To reproduce the issue, I created two blank docs, A and B, and inserted two paragraphs of text in A, each with their own attribute. I then made changes to the attributes of the second and first paragraphs in A and B respectively. I then attempted to merge the updates from A and B. Unfortunately, I noticed that B's changes were lost during the merge.

Additionally, in cases where there were nested attributes, the changes made in B not only were lost, but also some of the untouched key values in B were erroneously changed. For example, in the case of nested attributes down below, the ID of the second paragraph in B originally had an ID of 2. However, after merging with A it was changed to 1.

I would appreciate any assistance in resolving this issue. Thank you for your time and attention to this matter.

To Reproduce
Flat Attribute Case

import * as Y from 'yjs'
import * as t from 'lib0/testing.js'

export const testFlatAttribute = tc => {
    const doc1 = new Y.Doc()
    const doc2 = new Y.Doc()

    // When using random ID, 
    // if the ID of doc2 is smaller than doc1, this test case will not encounter error.
    // However, if the ID of doc2 is larger than doc1's ID, unexpected result will occur.
    doc1.clientID = 1
    doc2.clientID = 2

    let ytext1 = doc1.getText('text');
    let ytext2 = doc2.getText('text');

    ytext1.applyDelta([
        {
            insert: 'a',
            attributes: { id: 1, c: false }
        },
        {
            insert: 'b',
            attributes: { id: 2, c: false }
        }
    ])

    Y.applyUpdate(doc1, Y.encodeStateAsUpdate(doc2))
    Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))

    ytext1.applyDelta([
        { retain: 1 },
        {
          retain: 1,
          attributes: { id: 2, c: true },
        }
    ])
    ytext2.applyDelta([
        {
          retain: 1,
          attributes: { id: 1, c: true }
        }
    ])

    Y.applyUpdate(doc1, Y.encodeStateAsUpdate(doc2))
    Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))

    let expect = [
        {
            insert: 'a',
            attributes: { id: 1, c: true },
        },
        {
            insert: 'b',
            // This result will be { id: 2, c: false } instead of { id: 2, c: true }.
            attributes: { id: 2, c: true },
        }
    ]

    // If there is a field named "id" or not in attributes, the { c: false } of 'b' will always be produced.
    t.compare(ytext1.toDelta(), expect)
    t.compare(ytext2.toDelta(), expect)
}

Nested Attribute Case

import * as Y from 'yjs'
import * as t from 'lib0/testing.js'

export const testNestedAttribute = tc => {
    const doc1 = new Y.Doc()
    const doc2 = new Y.Doc()

    doc1.clientID = 1
    doc2.clientID = 2

    let ytext1 = doc1.getText('text');
    let ytext2 = doc2.getText('text');

    ytext1.applyDelta([
        {
            insert: 'a',
            attributes: { a: { id: 1, c: false } }
        },
        {
            insert: 'b',
            attributes: { a: { id: 2, c: false } }
        }
    ])

    Y.applyUpdate(doc1, Y.encodeStateAsUpdate(doc2))
    Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))

    ytext1.applyDelta([
        { retain: 1 },
        {
          retain: 1,
          attributes: { a: { id: 2, c: true } },
        }
    ])
    ytext2.applyDelta([
        {
          retain: 1,
          attributes: { a: { id: 1, c: true } },
        }
    ])

    Y.applyUpdate(doc1, Y.encodeStateAsUpdate(doc2))
    Y.applyUpdate(doc2, Y.encodeStateAsUpdate(doc1))

    let expect = [
        {
            insert: 'a',
            attributes: { a: { id: 1, c: true } },
        },
        {
            insert: 'b',
            // The result here will be { a: { id: 1, c: false} }, 
            // which means that not only is the id changed from 2 to 1, but the modification of c is also lost.
            attributes: { a: { id: 2, c: true } },
        }
    ]

    t.compare(ytext1.toDelta(), expect)
    t.compare(ytext2.toDelta(), expect)
}

Environment Information

  • Node.js v16.16.0
  • Yjs v13.5.52

Additional context
I noticed this issue in v13.5.39, and it still reproduces in v13.5.52. In the old version, there is no need to manually set the clientID, and the incorrect result will be consistently obtained. In the new version, if the clientID is not set, random results will be obtained, sometimes correct and sometimes wrong.

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

No branches or pull requests

2 participants