Closed mweidner037 closed 3 years ago
Implementation idea: append (push)-only version, e.g., for chat messages. Would just be an optimization - you can of course use any CList in this fashion.
Although the list interface doesn't specify them, it would be nice if each implementation had "range" (bulk) versions of the insertion methods (insert, push, unshift, splice). E.g.:
/**
* Insert values starting at the given index.
* Afterwards, values[0] will be at startIndex, values[1]
* will be at startIndex + 1, etc.
*/
insertRange(startIndex: number, ...values: T[]): this;
The interface leaves these out because they properly should have different signatures on different implementations (...values vs ...args[] vs count).
Draft Move interface; a Set version is todo:
export interface ListMoveEvent extends CrdtEvent {
// TODO: make clear exactly how to simulate the op
// locally. I.e., give equivalent sequential ops
// (insert, delete). Might be clearest if the args
// do end up being the same as the original args on the
// sending replica.
/**
* The index where the moved element started
* on this replica. In general, except on the
* sending replica, this can be different from fromIndex.
*/
startIndex: number;
/**
* The index where the moved element ended (is currently).
* Might not equal toIndex even on the sender,
* in case fromIndex < toIndex (then it is toIndex - 1).
* TODO: during a bulk move, can we guarantee this index
* is the final destination, or will it only hold until
* the next entry is moved?
*/
endIndex: number;
count: number;
}
export interface MovableCListEventsRecord<T> extends CListEventsRecord<T> {
/**
* Emitted when a move operation
* moves a value or range of values.
*/
Move: ListMoveEvent;
}
export interface MovableCList<
T,
InsertArgs extends any[],
Events extends CListEventsRecord<T> = CListEventsRecord<T>
> extends CList<T, InsertArgs, Events> {
/**
* Move count values starting at fromIndex to toIndex.
* Concurrent moves of the same value will move the
* value to a single destination chosen
* by some arbitration rule.
*
* toIndex is evaluated before removing fromIndex
* and follows the same rules as insertion indices.
* So the element will end up at toIndex - 1 if fromIndex < toIndex,
* else toIndex.
*
* count defaults to one (move a single element).
*/
move(fromIndex: number, toIndex: number, count?: number): void;
}
// TODO: analogous SettableCList interface + event, if
// set appears as an operation in more than one implementation?
Interfaces
https://github.com/composablesys/compoventuals/blob/master/client/src/crdt/list/interfaces.ts
Questions
set
is a natural list/array operation. I've left it in for now since non-supporting implementations can implement it as delete(index) followed by insert(index, value).move
be included in PlainList and CrdtList, or in a separate sub-interface? I've included it for now since a PlainList can always choose to implement it using a delete + insert, and all of my planned CrdtList implementations are movable. But I am not sure about those decisions.Planned implementations
These are essentially the same as for sets. For each list, though, we can also ask whether it should be movable, not movable, or both (provide both implementations). Movable lists would use Martin's movable list semantics: if a value / valueCrdt is moved multiple times concurrently, it picks only one of the destinations. Generally, making lists movable increases memory usage, even if you don't use move operations, since you have to add an LwwRegister for each value that controls its location.
PlainList
set
operations by using a list of registers. So if multiple replicas set a location concurrently, it will pick one, instead of inserting both. Movable by default, since you are already adding a register for each value, so adding another one isn't too much overhead proportionally?CrdtList
Same possibilities as CrdtSet. In fact, Martin's construction can be interpreted as generically making a movable CrdtList out of any CrdtSet, which is how I've implemented a draft MovableList (https://github.com/composablesys/compoventuals/blob/master/client/src/crdt/list/movable_list.ts).
We could offer non-movable versions if we think users might want them as an optimization (this gets rid of the LwwRegister-per-value-Crdt memory overhead, and may shorten network messages slightly). It seems less important than for PrimitivePlainList, though, since the overhead is proportionally smaller, given that the values are themselves Crdts instead of just single characters.
Riak-style CrdtLists (movable or not) pick up an extra con: every message from a value Crdt must include that value Crdt's sequence identifier, and those identifiers can become long in long lists. This is because another replica receiving the message might have GC'd the target Crdt, and then when it resurrects the Crdt, it needs to know where to put the Crdt in the list. Also, getting this to work for a movable Riak-style CrdtList seems a bit tricky implementation-wise, but I think it should be possible and no less efficient than the non-movable version, except for the usual memory cost.