Closed Legioth closed 2 years ago
These are the potential outcomes for the different operations that we might want to support.
set
fails in case the provided key/entry no longer exists.replace
fails in case the key doesn't exist, or if the value for that key is different than the expected value.insertBefore
and insertAfter
fail if the reference key no longer exists. These operations generate a key for the new entry and will thus need to return both success/failure and the generated key.moveBefore
and moveBefore
fail if the reference key or the key of the item to move no longer exist.remove
fails if the key no longer existsinsertFirst
and insertLast
will not fail because from concurrent modifications. These operations generate a key for the new entry.One potential way out of the problem with return values is to observe that only the various insert*
operations may need to return multiple values (key and asynchronous status), whereas all the other operations only need to return an asynchronous status. We could thus let all the other operations return a CompletableFuture<T>
where T
is either a Boolean
or an enum that helps clarify which of the different failure conditions were encountered. The insert operations would instead return a wrapper class (could be a record if we would use Java 16) with getters for the generated key and an appropriate CompletableFuture
.
When it comes to CollaborationListKey
versus CollaborationListEntry
, it would seem that it's better to just use a key. With an entry, it would leave the room for ambiguity about whether the value in the entry is the latest live value or the value that was present when the entry was created. Using a key instead would help make that aspect clearer.
The event delivered to subscribers would also need to be enhanced to be able to distinguish all the cases.
type
field with an enum value to distinguish the type of operation:
INSERT
for insertBefore
, insertFirst
, insertAfter
and insertLast
SET
for set
, replace
and remove
.MOVE
for moveBefore
and moveAfter
.value
and oldValue
are always populated even if the value didn't change (i.e. for MOVE
). For inserts, oldValue
is null and for removals, value
is null.before
and oldBefore
are always populated even if the structure is not changed. before
is null
it the entry is the first item in the list and for removals. oldBefore
is null
for inserts or if the entry was previously the first item in the list.after
and oldAfter
are always populate even if the structure is not changed. after
is null
if the entry is the last item in the list and for removals. oldAfter
is null
for inserts or if the entry was previously the last item in the list.
CollaborationList
does currently only support appending values to the end of the list, but it doesn't support inserting values into other positions, replacing values or removing values.All those missing operations need a way of referencing the list entry that is targeted. This cannot use the regular list index since any concurrent operation might cause the list to be reindexed. Instead, each list entry would need to have its own key that can be used to reference that entry in subsequent operations (there's also an option to use the value itself as a key, but that alternative doesn't allow having duplicates in the list). The entry key would be returned from insertion operations and also be available for iteration (based on a snapshot taken when iteration starts) as an alternative to iterating over only the values.
Technically, the key could be implemented e.g. as a randomly generated UUID or through fractional indexing. This is an opaque internal value that we should encapsulate in a value class to avoid leaking irrelevant implementation details. We could also consider a design where the value object is representing the whole entry so that e.g. update operations would be in the entry class instead of in the list class itself.
With the key as a value holder, the various operations would be like this:
All data manipulation operations should give a
CompletableFuture
that is used to signal when the operation is completed. All operations except inserting at the beginning or the end can fail in case the referenced key is no longer present in the list. The insertion operations should also return the key that is associated with the new entry, which leads to multiple bad API alternatives:CompletableFuture<CollaborationListKey>
which means that an async step is needed to do anything further operations with the key. There's also the problem with how to signal failure (i.e. because of the reference key is no longer present): we would either have to resolve asnull
, as a special "invalid"CollaborationListKey
value, or fail the future. Either option makes usage inconvenient in different ways.CompletableFuture<Boolean>
for tracking success.CollaborationListKey key = list.insertBefore(otherKey, "8", success -> System.out.println(success))
. This usage pattern might be optimal for avoiding boilerplate but it's not consistent with existing APIs (which we may still change if we want).As code, the various alternatives would thus look like this:
With using the key as an entry with further operations, usage would instead be like this: