Movable List#293
Merged
Merged
Conversation
Co-authored-by: Leon <leeeon233@gmail.com>
This "move" flag does not actually mean that the insertion is caused by the move op. 就算是 move 造成的它不一定就能是 true 它得是下游真的能在“前一个版本的 array 里找到“,才能是 true
The Movable List is currently flawed; an element may not exist on the movable list state, yet there are operations that revive its corresponding list item. In such cases, the diff calculation does not send back the corresponding element state (this occurs when tracing back, which fuzz testing currently does not cover. It might only be exposed by randomly switching to a version and then checking for consistency; otherwise, as long as all elements are in memory, this problem does not arise). Moreover, there is no need to store elements in the state that do not have a corresponding list item. They will be deleted during the Snapshot, and relying on "them still being in the state" is incorrect behavior. Such adjustments also eliminate the need to maintain the `pending_elements` field. By allowing the opgroup to record the mapping from pos id to state id, we can ensure that the events sent to the movable list state will include the corresponding state. Movable List 现在是有错的,elem 可能不存在 movable list state 上,但是又有操作把它对应的 list item 复活了,此时 diff calc 不会把对应 element 状态发送回来(往前回溯的时候会出现,fuzz 现在没覆盖到。得有随意切换一个版本然后 check consistency 才可能会暴露;否则现在大家 elements 都在内存,就没这个问题) 而且我们没有必要在状态中存储没有对应 list item 的 element。在 Snapshot 的时候它们都会被删掉,如果依赖了“它们还会在 state 内”就是错误的行为。这样的调整也让我们不需要去维护 pending_elements 这个 field 了 通过让 opgroup 记录了 pos id → state id 的映射,可以保证发给 movable list state 的事件中会带上对应的 state
Merged
zxch3n
marked this pull request as ready for review
April 24, 2024 09:19
…refine the type of subscribe
Leeeon233
approved these changes
Apr 25, 2024
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Background
Loro's
Listsupports insert and delete operations but lacks built-in methods for set and move. To simulate set and move, developers might combinedeleteandinsert. However, this approach can lead to issues during concurrent operations on the same element, often resulting in duplicate entries upon merging.For instance, consider a list
[0, 1, 2]. If user A moves the element '0' to position 1, while user B moves it to position 2, the ideal merged outcome should be either[1, 0, 2]or[1, 2, 0]. However, using the delete-insert method to simulate a move results in[1, 0, 2, 0], as both users delete '0' from its original position and insert it independently at new positions.To address this, we introduce a
MovableListcontainer. This new container type directly supports move and set operations, aligning more closely with user expectations and preventing the issues associated with simulated moves.Design
Algorithm Introduction
The algorithm is based on Moving Elements in List CRDTs by Martin Kleppmann.
It uses a List CRDT to provide "stable position representation." Then, it records the position and value with an additional LWW Map. When users move an item, it creates a new list item on the List CRDT and removes the old one, and update the LWW Map with the new position ID.
Thus, it's implemented by 3 CRDTs:
Element ID → List Item IDandElement ID → Valuewith two LWW Maps. When a Move occurs, it updates the mapping to List Item ID; when a set op occurs, it updates the mapping to Value.List Item and Element
We use three CRDTs in the algorithm: one CRDT maintains the order of the List, one maintains the mapping from ElementID to the List, and one maintains the mapping from ElementID to Value.
We refer to elements on the List as List Items; each item only has ID information without a specific value.
For the two LWW Maps, we store them together in an Element object.
Two Indexing Methods
In the case of concurrent moving, movable list items are not directly removed from the document, so there will be extra list Items on the document with no Elements pointing to them. These are imperceptible to the users. However, for the List Diff Calculator, whether an Element points to a List Item is imperceptible, so the Diff it produces cannot determine whether a List Item is needless. Therefore, in the following scenario,
We refer to the index from the user's perspective as
UserIndex, and the one stored internally for the DiffCalculator asOpIndex.Design of Internal Event & External Event
Internal Event is used by the DiffCalculator to pass state changes to the State, while External Event is used by the State to communicate its changes to the outside world.
The design of the Internal Event includes the following:
External Event continues to use a format similar to Quill Delta but adds Meta information to mark whether the creation is a result of Move behavior.
Implementation of DiffCalculator
The calculation of Internal Events is the task of the DiffCalculator.
Structure of MovableList State in Memory
Encoding of Operations
Storage of Snapshot
The Snapshot needs to encode all List Items, including those not visible to the user, into a separate vec
Implementation of Handler
Due to the "Two Indexing Method," the index information exposed to users is different from the index information used for the op. Because the Handler is where users directly interact with Loro, we let index conversions happen at the Handler.
Examples
Performance
Tested on M1. The duration is recorded in
ms.The cost of
MovableListis close toList.