ron-rdt-0.11: lib/RON/Data/RGA.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
-- | Replicated Growable Array (RGA)
module RON.Data.RGA (
RGA (..),
RgaRep,
RgaString,
edit,
editText,
getAliveIndices,
getList,
getText,
insert,
insertAfter,
insertAtBegin,
insertText,
insertTextAfter,
insertTextAtBegin,
newFromList,
newFromText,
remove,
rgaType,
RON.Data.RGA.toList,
toText,
)
where
import Data.Algorithm.Diff (PolyDiff (Both, First, Second), getGroupedDiffBy)
import Data.Bifunctor (second)
import Data.HashMap.Strict qualified as HashMap
import Data.Map.Strict ((!?))
import Data.Map.Strict qualified as Map
import Data.Text qualified as Text
import RON.Data.Internal (
MonadObjectState,
ReducedChunk (ReducedChunk, rcBody, rcRef),
Reducible,
Rep,
Replicated (encoding),
ReplicatedAsObject,
ReplicatedAsPayload,
Unapplied,
applyPatches,
fromRon,
getObjectStateChunk,
modifyObjectStateChunk_,
newObject,
newRon,
objectEncoding,
readObject,
reduceUnappliedPatches,
reducibleOpType,
stateFromChunk,
stateToChunk,
toPayload,
)
import RON.Error (MonadE, errorContext, throwErrorText)
import RON.Event (ReplicaClock, getEventUuid, getEventUuids)
import RON.Prelude
import RON.Semilattice (Semilattice)
import RON.Types (
ObjectRef (ObjectRef),
Op (Op, opId, payload, refId),
StateChunk (StateChunk),
StateFrame,
UUID,
WireStateChunk (WireStateChunk, stateBody, stateType),
)
import RON.UUID (pattern Zero)
import RON.UUID qualified as UUID
import RON.Util.Word (ls60, pattern B11)
{-# ANN module ("HLint: ignore Reduce duplication" :: String) #-}
{- | opId = vertex id
refId:
0 = value is alive,
_ = tombstone event, value is backup for undo
payload: the value
-}
type Vertex = Op
data VertexListItem
= VertexListItem
{ itemValue :: Vertex
, itemNext :: Maybe UUID
}
deriving (Eq, Show)
data VertexList
= VertexList
{ listHead :: UUID
, listItems :: HashMap UUID VertexListItem
}
deriving (Eq, Show)
instance Semigroup VertexList where
(<>) = merge
instance Semilattice VertexList
vertexListToOps :: VertexList -> [Vertex]
vertexListToOps v@VertexList{..} = go listHead listItems
where
go root items =
let VertexListItem{..} =
HashMap.lookupDefault
( error $
unlines $
["Cannot find vertex id", show root, "in array"]
++ map show (HashMap.toList items)
++ ["Original array is", show v]
)
root
items
rest = case itemNext of
Just next -> go next (HashMap.delete root items)
Nothing -> []
in itemValue : rest
vertexListFromOps :: [Vertex] -> Maybe VertexList
vertexListFromOps = foldr go mempty
where
go v@Op{opId} vlist = Just VertexList{listHead = opId, listItems = vlist'}
where
item itemNext = VertexListItem{itemValue = v, itemNext}
vlist' = case vlist of
Nothing -> HashMap.singleton opId (item Nothing)
Just VertexList{listHead, listItems} ->
HashMap.insert opId (item $ Just listHead) listItems
-- | Untyped RGA
newtype RgaRep = RgaRep (Maybe VertexList)
deriving (Eq, Monoid, Semigroup, Semilattice, Show)
data PatchSet
= PatchSet
{ psPatches :: Map UUID VertexList
-- ^ the key is the parent event, the value is a non-empty VertexList
, psRemovals :: Map UUID UUID
-- ^ the key is the target event, the value is the tombstone event
}
deriving (Eq, Show)
instance Semigroup PatchSet where
rga1 <> rga2 = reapplyPatchSet $ preMerge rga1 rga2
preMerge :: PatchSet -> PatchSet -> PatchSet
preMerge (PatchSet p1 r1) (PatchSet p2 r2) =
PatchSet
{ psPatches = Map.unionWith (<>) p1 p2
, psRemovals = Map.unionWith max r1 r2
}
instance Monoid PatchSet where
mempty = PatchSet{psPatches = mempty, psRemovals = mempty}
patchSetFromRawOp :: Op -> PatchSet
patchSetFromRawOp op@Op{opId, refId, payload} = case payload of
[] ->
-- remove op
mempty{psRemovals = Map.singleton refId opId}
_ : _ ->
-- append op
mempty
{ psPatches =
Map.singleton
refId
VertexList
{ listHead = opId
, listItems =
HashMap.singleton
opId
VertexListItem
{ itemValue = op{refId = Zero}
, itemNext = Nothing
}
}
}
patchSetFromChunk :: ReducedChunk -> PatchSet
patchSetFromChunk ReducedChunk{rcRef, rcBody} =
case (UUID.split rcRef).uuidVersion of
B11 ->
-- derived event -- rm-patch compatibility
foldMap patchSetFromRawOp rcBody
_ ->
-- patch
case vertexListFromOps rcBody of
Just patch -> mempty{psPatches = Map.singleton rcRef patch}
Nothing -> mempty
instance Reducible RgaRep where
reducibleOpType = rgaType
stateFromChunk = RgaRep . vertexListFromOps
stateToChunk (RgaRep rga) = maybe [] vertexListToOps rga
applyPatches rga (patches, ops) =
second patchSetToChunks . reapplyPatchSetToState rga $
foldMap patchSetFromChunk patches <> foldMap patchSetFromRawOp ops
reduceUnappliedPatches (patches, ops) =
patchSetToChunks . reapplyPatchSet $
foldMap patchSetFromChunk patches <> foldMap patchSetFromRawOp ops
patchSetToChunks :: PatchSet -> Unapplied
patchSetToChunks PatchSet{psPatches, psRemovals} =
( [ ReducedChunk{..}
| (rcRef, vertices) <- Map.assocs psPatches
, let rcBody = vertexListToOps vertices
]
, [ Op{opId = tombstone, refId, payload = []}
| (refId, tombstone) <- Map.assocs psRemovals
]
)
reapplyPatchSet :: PatchSet -> PatchSet
reapplyPatchSet ps =
continue ps [reapplyPatchesToOtherPatches, reapplyRemovalsToPatches]
reapplyPatchSetToState :: RgaRep -> PatchSet -> (RgaRep, PatchSet)
reapplyPatchSetToState rga ps =
continue (rga, ps) [reapplyPatchesToState, reapplyRemovalsToState]
continue :: x -> [x -> Maybe x] -> x
continue x fs = case asum $ map ($ x) fs of
Nothing -> x
Just x' -> continue x' fs
reapplyPatchesToState :: (RgaRep, PatchSet) -> Maybe (RgaRep, PatchSet)
reapplyPatchesToState (RgaRep rstate, ps@PatchSet{..}) = case rstate of
Just VertexList{listHead = targetHead, listItems = targetItems} ->
asum
[ do
targetItems' <- applyPatch parent patch targetItems
pure
( RgaRep . Just $ VertexList targetHead targetItems'
, ps{psPatches = Map.delete parent psPatches}
)
| (parent, patch) <- Map.assocs psPatches
]
Nothing -> do
-- rstate is empty => only virtual 0 node exists
-- => we can apply only 0 patch
patch <- psPatches !? Zero
pure (RgaRep $ Just patch, ps{psPatches = Map.delete Zero psPatches})
reapplyPatchesToOtherPatches :: PatchSet -> Maybe PatchSet
reapplyPatchesToOtherPatches ps@PatchSet{..} =
asum
[ do
targetItems' <- applyPatch parent patch targetItems
pure
ps
{ psPatches =
Map.insert
targetParent
(VertexList targetHead targetItems')
. Map.delete parent
$ psPatches
}
| (parent, patch) <- Map.assocs psPatches
, (targetParent, targetPatch) <- Map.assocs psPatches
, parent /= targetParent
, let VertexList targetHead targetItems = targetPatch
]
applyPatch ::
UUID ->
VertexList ->
HashMap UUID VertexListItem ->
Maybe (HashMap UUID VertexListItem)
applyPatch parent patch targetItems = case parent of
Zero ->
error "chunk with zero ref must be considered a state, not a patch"
_ -> do
item@VertexListItem{itemNext} <- HashMap.lookup parent targetItems
let VertexList next' newItems = case itemNext of
Nothing -> patch
Just next -> VertexList next targetItems <> patch
let item' = item{itemNext = Just next'}
pure $ HashMap.insert parent item' targetItems <> newItems
reapplyRemovalsToState :: (RgaRep, PatchSet) -> Maybe (RgaRep, PatchSet)
reapplyRemovalsToState (RgaRep rstate, ps@PatchSet{..}) = do
VertexList{listHead = targetHead, listItems = targetItems} <- rstate
asum
[ do
targetItems' <- applyRemoval parent tombstone targetItems
pure
( RgaRep . Just $ VertexList targetHead targetItems'
, ps{psRemovals = Map.delete parent psRemovals}
)
| (parent, tombstone) <- Map.assocs psRemovals
]
reapplyRemovalsToPatches :: PatchSet -> Maybe PatchSet
reapplyRemovalsToPatches PatchSet{..} =
asum
[ do
targetItems' <- applyRemoval parent tombstone targetItems
pure
PatchSet
{ psRemovals = Map.delete parent psRemovals
, psPatches =
Map.insert
targetParent
(VertexList targetHead targetItems')
psPatches
}
| (parent, tombstone) <- Map.assocs psRemovals
, (targetParent, targetPatch) <- Map.assocs psPatches
, let VertexList targetHead targetItems = targetPatch
]
applyRemoval ::
UUID ->
UUID ->
HashMap UUID VertexListItem ->
Maybe (HashMap UUID VertexListItem)
applyRemoval parent tombstone targetItems = do
item@VertexListItem{itemValue = v@Op{refId}} <-
HashMap.lookup parent targetItems
let item' = item{itemValue = v{refId = max refId tombstone}}
pure $ HashMap.insert parent item' targetItems
merge :: VertexList -> VertexList -> VertexList
merge v1 v2 =
fromMaybe (error "merge of non-empty lists cannot be empty") $
vertexListFromOps $
(merge' `on` vertexListToOps) v1 v2
merge' :: [Vertex] -> [Vertex] -> [Vertex]
merge' [] vs2 = vs2
merge' vs1 [] = vs1
merge' w1@(v1 : vs1) w2@(v2 : vs2) =
case compare e1 e2 of
LT -> v2 : merge' w1 vs2
GT -> v1 : merge' vs1 w2
EQ -> mergeVertices : merge' vs1 vs2
where
Op{opId = e1, refId = tombstone1, payload = p1} = v1
Op{opId = e2, refId = tombstone2, payload = p2} = v2
-- priority of deletion
mergeVertices =
Op
{ opId = e1
, refId = max tombstone1 tombstone2
, payload = maxOn length p1 p2
}
-- | Name-UUID to use as RGA type marker.
rgaType :: UUID
rgaType = $(UUID.liftName "rga")
-- | Typed RGA
newtype RGA a = RGA [a]
deriving (Eq, Show)
instance (Replicated a) => Replicated (RGA a) where encoding = objectEncoding
instance (Replicated a) => ReplicatedAsObject (RGA a) where
type Rep (RGA a) = RgaRep
newObject (RGA items) = do
oid <- getEventUuid
vertexIds <- getEventUuids $ ls60 $ genericLength items
ops <-
for (zip items vertexIds) $ \(item, vertexId) -> do
payload <- newRon item
pure $ Op vertexId Zero payload
modify' $ Map.insert oid $ wireStateChunk ops
pure $ ObjectRef oid
readObject = do
StateChunk stateBody <- getObjectStateChunk
mItems <-
for stateBody $ \Op{refId, payload} -> case refId of
Zero -> Just <$> fromRon payload
_ -> pure Nothing
pure . RGA $ catMaybes mItems
{- | Replace content of the RGA throug introducing changes detected by
'getGroupedDiffBy'.
-}
edit ::
( MonadE m
, MonadObjectState (RGA a) m
, ReplicaClock m
, ReplicatedAsPayload a
) =>
[a] ->
m ()
edit newItems =
modifyObjectStateChunk_ $ \chunk@(StateChunk stateBody) -> do
let newItems' = [Op Zero Zero $ toPayload item | item <- newItems]
-- TODO(2019-04-17, #59, cblp) replace 'toPayload' with 'newRon' and
-- relax constraint on 'a' from 'ReplicatedAsPayload' to
-- 'Replicated'
let diff = getGroupedDiffBy eqAliveOnPayload stateBody newItems'
(stateBody', Last lastEvent) <-
runWriterT $
fold <$> for diff \case
First removed -> do
tombstones <- getEventUuids $ genericLength removed
for (zip removed tombstones) \case
(op@Op{refId = Zero}, tombstone) -> do
-- not deleted yet
tell . Last $ Just tombstone
pure op{refId = tombstone}
(op, _) ->
-- deleted already
pure op
Both v _ -> pure v
Second added -> do
opIds <- getEventUuids $ genericLength added
for (zip added opIds) \(op, opId) -> do
tell . Last $ Just opId
pure op{opId}
pure $ case lastEvent of
Nothing -> chunk
Just _ -> StateChunk stateBody'
where
eqAliveOnPayload
Op{refId = Zero, payload = p1}
Op{refId = Zero, payload = p2} =
p1 == p2
eqAliveOnPayload _ _ = False
-- | Speciaization of 'edit' for 'Text'
editText ::
(MonadE m, MonadObjectState RgaString m, ReplicaClock m) => Text -> m ()
editText = edit . Text.unpack
{- | Speciaization of 'RGA' to 'Char'.
This is the recommended way to store a string.
-}
type RgaString = RGA Char
-- | Create an RGA from a list
newFromList ::
(MonadState StateFrame m, ReplicaClock m, Replicated a) =>
[a] ->
m (ObjectRef (RGA a))
newFromList = newObject . RGA
-- | Create an 'RgaString' from a text
newFromText ::
(MonadState StateFrame m, ReplicaClock m) =>
Text ->
m (ObjectRef RgaString)
newFromText = newFromList . Text.unpack
getAliveIndices :: (MonadE m, MonadObjectState (RGA a) m) => m [UUID]
getAliveIndices = do
StateChunk stateBody <- getObjectStateChunk
pure [opId | Op{opId, refId = Zero} <- stateBody]
-- | Read elements from RGA
getList :: (MonadE m, MonadObjectState (RGA a) m, Replicated a) => m [a]
getList = coerce <$> readObject
-- | Read characters from 'RgaString'
getText :: (MonadE m, MonadObjectState RgaString m) => m Text
getText = Text.pack <$> getList
{- | Insert a sequence of elements after the specified position.
Position is identified by 'UUID'. 'Nothing' means the beginning.
-}
insert ::
(MonadE m, MonadObjectState (RGA a) m, ReplicaClock m, Replicated a) =>
[a] ->
-- | position
Maybe UUID ->
m ()
insert [] _ = pure ()
insert items mPosition =
modifyObjectStateChunk_ $ \(StateChunk stateBody) -> do
vertexIds <- getEventUuids $ ls60 $ genericLength items
ops <-
for (zip items vertexIds) $ \(item, vertexId) -> do
payload <- newRon item
pure $ Op vertexId Zero payload
stateBody' <-
case mPosition of
Nothing -> pure $ ops <> stateBody
Just position -> findAndInsertAfter position ops stateBody
pure $ StateChunk stateBody'
where
findAndInsertAfter pos newOps = go
where
go = \case
[] -> throwErrorText "Position not found"
op@Op{opId} : ops
| opId == pos -> pure $ op : newOps ++ ops
| otherwise -> (op :) <$> go ops
insertAtBegin ::
(MonadE m, MonadObjectState (RGA a) m, ReplicaClock m, Replicated a) =>
[a] ->
m ()
insertAtBegin items = insert items Nothing
insertAfter ::
(MonadE m, MonadObjectState (RGA a) m, ReplicaClock m, Replicated a) =>
[a] ->
-- | position
UUID ->
m ()
insertAfter items = insert items . Just
{- | Insert a text after the specified position.
Position is identified by 'UUID'. 'Nothing' means the beginning.
-}
insertText ::
(MonadE m, MonadObjectState RgaString m, ReplicaClock m) =>
Text ->
-- | position
Maybe UUID ->
m ()
insertText = insert . Text.unpack
insertTextAtBegin ::
(MonadE m, MonadObjectState RgaString m, ReplicaClock m) => Text -> m ()
insertTextAtBegin text = insertText text Nothing
insertTextAfter ::
(MonadE m, MonadObjectState RgaString m, ReplicaClock m) =>
Text ->
-- | position
UUID ->
m ()
insertTextAfter text = insertText text . Just
-- | Record a removal of a specific item
remove ::
(MonadE m, MonadObjectState (RGA a) m, ReplicaClock m) =>
-- | position
UUID ->
m ()
remove position =
errorContext "RGA.remove" $
errorContext ("position = " <> show position) $
modifyObjectStateChunk_ $
\(StateChunk stateBody) -> do
event <- getEventUuid
stateBody' <- findAndTombstone event stateBody
pure $ StateChunk stateBody'
where
findAndTombstone event = go
where
go = \case
[] -> throwErrorText "Position not found"
op@Op{opId} : ops
| opId == position -> pure $ op{refId = event} : ops
| otherwise -> (op :) <$> go ops
wireStateChunk :: [Op] -> WireStateChunk
wireStateChunk stateBody = WireStateChunk{stateType = rgaType, stateBody}
toList :: RGA a -> [a]
toList (RGA xs) = xs
toText :: RgaString -> Text
toText (RGA s) = Text.pack s