Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Conversation

@javiergonzalez-synth
Copy link

@javiergonzalez-synth javiergonzalez-synth commented Nov 5, 2025

Fixes: #736

Note: I made the PR against v13 branch because the unit tests in main were failing.

Describe the bug

Undo-manager over arrays sometimes generates a wrong result when undoing.

To Reproduce
Run the following jest test:

import * as Y from "yjs";

it("undo should be ok", () => {
  const doc = new Y.Doc();
  const array = doc.getArray("array");
  const undoManager = new Y.UndoManager(array, { captureTimeout: 0 });

  doc.transact(() => {
    array.insert(0, [1, 2, 3]);
  });
  // 1,2,3
  expect(array.toJSON()).toEqual([1, 2, 3]);

  doc.transact(() => {
    array.insert(2, [6, 7]);
  });
  // 1,2,3
  // 1,2,6,7,3
  expect(array.toJSON()).toEqual([1, 2, 6, 7, 3]);

  doc.transact(() => {
    array.delete(1, 1);
    array.insert(1, [8]);
  });
  // 1,2,3
  // 1,2,6,7,3
  // 1,8,6,7,3
  expect(array.toJSON()).toEqual([1, 8, 6, 7, 3]);

  doc.transact(() => {
    array.delete(0, 1);
  });
  // 1,2,3
  // 1,2,6,7,3
  // 1,8,6,7,3
  // 8,6,7,3
  expect(array.toJSON()).toEqual([8, 6, 7, 3]);

  doc.transact(() => {
    array.delete(1, 2);
  });
  // 1,2,3
  // 1,2,6,7,3
  // 1,8,6,7,3
  // 8,6,7,3
  // 8,3
  expect(array.toJSON()).toEqual([8, 3]);

  undoManager.undo();
  // 1,2,3
  // 1,2,6,7,3
  // 1,8,6,7,3
  // 8,6,7,3
  expect(array.toJSON()).toEqual([8, 6, 7, 3]);

  doc.transact(() => {
    array.insert(2, [9]);
  });
  // 1,2,3
  // 1,2,6,7,3
  // 1,8,6,7,3
  // 8,6,7,3
  // 8,6,9,7,3
  expect(array.toJSON()).toEqual([8, 6, 9, 7, 3]);

  undoManager.undo();
  // 1,2,3
  // 1,2,6,7,3
  // 1,8,6,7,3
  // 8,6,7,3
  expect(array.toJSON()).toEqual([8, 6, 7, 3]);

  undoManager.undo();
  // 1,2,3
  // 1,2,6,7,3
  // 1,8,6,7,3
  expect(array.toJSON()).toEqual([1, 8, 6, 7, 3]);

  undoManager.undo();
  // 1,2,3
  // 1,2,6,7,3
  expect(array.toJSON()).toEqual([1, 2, 6, 7, 3]);

  undoManager.undo();
  // 1,2,3
  expect(array.toJSON()).toEqual([1, 2, 3]); // !! actually has 1,2,7,3
});

Expected behavior
Undo manager should be able to go back to the first initial state, 1,2,3, but it goest to a weird 1,2,7,3 state instead. This is the minimal amount of operations I could find that trigger this error.

Environment Information

  • Node 20.6.0
  • Yjs version 13.6.27 and 14.0.0-8

Additional context
The problem with this kind of broken undo steps is that you never know exactly when they are going to trigger and it leaves you in a potentially corrupted state

Problem

When undoing array operations, the UndoManager would sometimes produce incorrect results. Specifically, when an item that was originally inserted as a single multi-value item (e.g., [6, 7]) was later split due to other operations, only part of it would be deleted during undo, leaving the document in a corrupted state.

Cause

In /src/utils/UndoManager.js, the popStackItem function was incorrectly handling split items:

  • It would follow the redone chain to find the current version of an item
  • But when the item had been split, it would only process the first part
  • It tried to use the right pointer to find the rest, but this doesn't work when other items have been inserted between the split parts

Fix

Modified the code to iterate through each position in the original item's range and follow the redone chain for each position individually:

for (let i = 0; i < struct.length; i++) {
  const { item: currentItem } = followRedone(store, createID(struct.id.client, struct.id.clock + i))
  if (shouldDelete(currentItem)) {
    itemsToDelete.push(currentItem)
  }
}

@javiergonzalez-synth javiergonzalez-synth changed the base branch from main to v13 November 5, 2025 09:47
@javiergonzalez-synth javiergonzalez-synth marked this pull request as draft November 5, 2025 10:11
@javiergonzalez-synth javiergonzalez-synth marked this pull request as ready for review November 5, 2025 10:23
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

Successfully merging this pull request may close these issues.

Undo manager sometimes generates wrong states when undoing

1 participant