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

Command manager is saving the entire attrs object on each attr() call (unexpected behaviour?) #2687

Open
alexandernst opened this issue Jun 8, 2024 · 4 comments
Labels
jointjs+ The paid commercial extension to the JointJS

Comments

@alexandernst
Copy link
Contributor

alexandernst commented Jun 8, 2024

Current versus expected behaviour

Consider the following code:

const a1 = new ShapeV4();
a1.position(20, 20);
graph.addCells([a1]);

a1.attr({ title: { textWrap: { text: "foo" } } });
console.log(cm.undoStack[1].data.next);

a1.position(21, 21);
console.log(cm.undoStack[2].data.next);

I get the following results in the console:

image

The second result is totally expected. It contains an object with the modified property (position), but the first object contains unexpected data. It contains a title object with all the properties of the title element, but only the textWrap/text attribute was modified by the .attr() call. It also contains the pathBody object, which wasn't modified.

In a real world scenario, when working on nodes with lots of attrs and/or embedded data, this behaviour results in quite a big (in size) undo and redo stacks.

Is this expected behaviour? Is there a way to make the command manager apply a diffing algorithm to the data that is being saved in the undo/redo stacks?

Steps to reproduce

  1. Open the POC at https://codesandbox.io/p/sandbox/jj4-textwrap-bug-y9l8kp
  2. Look at the console

Version

4.0.1+

What browsers are you seeing the problem on?

Chrome, Safari

What operating system are you seeing the problem on?

Mac

@kumilingus
Copy link
Contributor

This is expected.

The CommandManager makes a snapshot of an attribute value before and after the change (using shallow clone for performance). The snapshot is then applied simply with set() method i.e. without merging.

Even if we would provide a callback, we would have to somehow record the property removal. It would not appear in the diff (and if we record it as undefined it would get lost in the JSON export).

I am not convinced these extra data is an issue. How many data are we talking about?

If this size is an issue, you can also introduce top-level properties such as color or borderWidth (with data-binding to attrs) and record these instead of the attrs object (using cmdBeforeAdd: (eventName) => eventName !== 'change:attrs')).

@kumilingus kumilingus added jointjs+ The paid commercial extension to the JointJS and removed bug labels Jun 14, 2024
@alexandernst
Copy link
Contributor Author

alexandernst commented Jul 6, 2024

Hi Roman! Sorry for not replying earlier. It has been a crazy month nearing the launch.

Its a difficult problem and the solutions won't be easy (if you were to actually accept this as a bug that needs to be fixed).

A possible solution would be to use JSON Patch Notation Objects (RFC 6902) (ref implementation in js) with a diffing algorithm that could produce sequential patches (very much like a cvs).

Consider the following props / data and the possible patch:

Original props:

{
    prop1: "foo",
    prop2: ["a", "b", "c"],
    prop3: {
        prop3sub1: "bar",
        prop3sub2: 99,
    }
}

New props:

{
    prop1: "foo",
    prop2: ["a", "b", "d"],
    prop3: {
        prop3sub1: "bar",
    },
    prop4: "xyz"
}

Generated diff:

[
  {
    op: "remove",
    path: "/prop3/prop3sub2",
  }, {
    op: "replace",
    path: "/prop2/2",
    value: "d",
  }, {
    op: "add",
    path: "/prop4",
    value: "xyz",
  }
]

The command manager could hold a set of patches instead of the entire snapshot of all props.

Pros to this solution:

  • reduced command manager size (both in-memory and when exported to a file)
  • faster undo / redo operations, since the command manager would need to apply changes only to the props that were actually changed
  • the patches can be represented as json (they are, in fact, json)

Cons:

  • this is a breaking change

About our use case and why I created this issue:

We do actually have quite a big undo/redo CM size because of this behavior. Each cell contains quite some elements, and each element might contain complex svg paths. This means that changing any prop will create a new snapshot of all the paths in the cell. We could workaround the problem with something like what you suggested (I had a couple similar ideas), but it feels hacky and imho this should be handled by the command manager.

@alexandernst
Copy link
Contributor Author

alexandernst commented Jul 6, 2024

Actually, this can be implemented without it being a breaking change! A new option can be added to the constructor of the command manager. Such an option could let users specify which method do they want to use. Eg:

const commandManager = new dia.CommandManager({
    diffing_algorithm: "full_snapshots",  //  "diffs_rfc6902",   
});

If the option isn't provided, "full_snapshots" would be assumed, thus keeping backwards compatibility.

@alexandernst
Copy link
Contributor Author

Hi @kumilingus ! I just wanted to let you know that I implemented a command manager with JSON Patch Notation Objects. Feel free to take a look at it and grab whatever might fit in JJ+! 🙏

https://github.com/Develatio/nebulant-builder/blob/develop/src/core/CommandManager.js

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
jointjs+ The paid commercial extension to the JointJS
Projects
None yet
Development

No branches or pull requests

2 participants