Best Practices for Undo/Redo Operations in a Microkernel Architecture with Cross-Plugin Transactions?

First of all, thank you very much for your help! I’d like to discuss a best practice for local undo / redo development I’m developing a 2d graphics system. Suppose there is a delete operation wrapped in a transaction, specifically targeting connected graphics (graphics which can be line connected). Upon execution of this delete command, a consequential data modification occurs. This modification is closely monitored by a line plugin (this plugin directly listens data change), which responds to the deletion event of the connected graphic. The plugin’s response involves a critical update to its internal data: it alters the ‘sourceId’ parameter, transitioning its value from the identifier of the deleted element to a null state. This adjustment in the plugin’s data is encapsulated within a transaction. Consequently, such design leads to an unwanted behavior in the undo functionality. Rather than reverting the deletion of the graphic itself, the undo operation primarily focuses on restoring the ‘sourceId’ data to its pre-deletion state. We set captureTimeOut to 0. Is it possible that we can merge multiple transactions locally in a single undo operation?

(One solution I have is to wrap an extra layer of transaction and abstract another changeTransaction that doesn’t immediately result in a transaction. this changeTransaction wraps the data changes that are used to respond to changes in the data layer. When processing the changeTransaction, we undo the most recent transaction change, get the cached data changes from the most recent transaction, merge the data in the changeTransaction with the cached changes, and then wrap the merged data in a real transaction.)

btw, the reason we decided to set captureTimeOut to 0 is that changes in the graphics system from the data layer to the rendering layer are not “what you see is what you get”. There is no guarantee that some intermediate state of data will result in a legitimate rendering.

Very much looking forward to your replies!

(Below is my rephrased thoughts on my question)
Hello Yjs Community,

I’m currently developing a 2D graphics system using a microkernel architecture and facing a unique challenge with our undo/redo functionality. Our system is structured such that connectable graphics are handled by a plugin-geometry module, while line connections are managed by a plugin-line module. This separation has led to a significant issue: when a graphic element is deleted and a line’s source/target ID is updated in response, these actions are processed in separate transactions by the respective plugins.

Key Challenges:

  1. Handling Cross-Plugin Transactions: Due to this architecture, coordinating changes across plugins within a single transaction is problematic. This affects our undo/redo functionality as it prevents grouping related changes (like graphic deletion and line ID updates) together atomically.
  2. Complex Undo/Redo Operations: In our current setup, the undo function primarily addresses the most recent change, which is often just the update of a ‘sourceId’ in the line plugin. This means it doesn’t fully revert the more impactful action of deleting a graphic element. We need an undo/redo system that considers both the deletion and line update as one cohesive operation.

To tackle this, I’m considering introducing a changeTransaction wrapper. This wrapper would aim to collect and merge changes from both plugins into a single, comprehensive transaction. This unified transaction could then be treated as a single operation in the undo/redo stack, maintaining consistency and reliability in representing state changes.

The challenge lies in ensuring that this combined transaction accurately reflects the state changes from both plugin-geometry and plugin-line, especially given our system’s non-WYSIWYG nature. We need to avoid any intermediate states that could lead to invalid renderings.

My Question:

What would be the best practice for implementing an effective undo/redo mechanism in a microkernel architecture where changes made by different plugins are interconnected but not naturally grouped into a single transaction? I’m eager to hear any insights or suggestions on how to approach this challenge.