{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE LambdaCase #-} {-# LANGUAGE NamedFieldPuns #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE PatternSynonyms #-} {-# LANGUAGE RecordWildCards #-} -- | Replicated Growable Array (RGA) module RON.Data.RGA ( RGA (..) , RgaRaw , RgaString , edit , editText , getList , getText , newFromList , newFromText , rgaType ) where import RON.Internal.Prelude import Control.Monad.Except (MonadError, liftEither) import Control.Monad.State.Strict (MonadState, get, put) import Control.Monad.Writer.Strict (lift, runWriterT, tell) import Data.Algorithm.Diff (Diff (Both, First, Second), getGroupedDiffBy) import Data.Bifunctor (bimap) import qualified Data.HashMap.Strict as HashMap import Data.List (genericLength) import qualified Data.Map.Strict as Map import Data.Monoid (Last (..)) import qualified Data.Text as Text import RON.Data.Internal import RON.Event (ReplicaClock, advanceToUuid, getEventUuid, getEventUuids) import RON.Types (Object (..), Op (..), StateChunk (..), UUID) import RON.Util.Word (pattern B11, ls60) import RON.UUID (pattern Zero, uuidVersion) import qualified RON.UUID as UUID -- | opEvent = vertex id -- opRef: -- 0 = value is alive, -- _ = tombstone event, value is backup for undo -- opPayload: 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 vertexListToOps :: Maybe VertexList -> [Vertex] vertexListToOps mv = case mv of Nothing -> [] Just 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 $ fromJust mv]) 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{opEvent = vid} vlist = Just $ VertexList{listHead = vid, listItems = vlist'} where item itemNext = VertexListItem{itemValue = v, itemNext} vlist' = case vlist of Nothing -> HashMap.singleton vid (item Nothing) Just VertexList{listHead, listItems} -> HashMap.insert vid (item $ Just listHead) listItems -- | Untyped RGA newtype RgaRaw = RgaRaw (Maybe VertexList) deriving (Eq, Monoid, Semigroup, 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{opEvent, opRef, opPayload} = case opPayload of [] -> -- remove op mempty{psRemovals = Map.singleton opRef opEvent} _:_ -> -- append op mempty { psPatches = Map.singleton opRef VertexList { listHead = opEvent , listItems = HashMap.singleton opEvent VertexListItem { itemValue = op{opRef = Zero} , itemNext = Nothing } } } patchSetFromChunk :: ReducedChunk -> PatchSet patchSetFromChunk ReducedChunk{rcRef, rcBody} = case uuidVersion $ UUID.split rcRef 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 RgaRaw where reducibleOpType = rgaType stateFromChunk = RgaRaw . vertexListFromOps stateToChunk (RgaRaw rga) = StateChunk (chunkVersion ops) ops where ops = vertexListToOps rga applyPatches rga (patches, ops) = bimap id 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{..} = ( [ ReducedChunk{rcVersion = chunkVersion rcBody, ..} | (rcRef, vertices) <- Map.assocs psPatches , let rcBody = vertexListToOps $ Just vertices ] , [ Op{opEvent = tombstone, opRef = vid, opPayload = []} | (vid, tombstone) <- Map.assocs psRemovals ] ) chunkVersion :: [Op] -> UUID chunkVersion ops = maximumDef Zero [ max vertexId tombstone | Op{opEvent = vertexId, opRef = tombstone} <- ops ] reapplyPatchSet :: PatchSet -> PatchSet reapplyPatchSet ps = continue ps [reapplyPatchesToOtherPatches, reapplyRemovalsToPatches] reapplyPatchSetToState :: RgaRaw -> PatchSet -> (RgaRaw, 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 :: (RgaRaw, PatchSet) -> Maybe (RgaRaw, PatchSet) reapplyPatchesToState (RgaRaw state, ps@PatchSet{..}) = case state of Just VertexList{listHead = targetHead, listItems = targetItems} -> asum [ do targetItems' <- applyPatch parent patch targetItems pure ( RgaRaw . Just $ VertexList targetHead targetItems' , ps{psPatches = Map.delete parent psPatches} ) | (parent, patch) <- Map.assocs psPatches ] Nothing -> do -- state is empty => only virtual 0 node exists -- => we can apply only 0 patch patch <- Map.lookup Zero psPatches pure (RgaRaw $ 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 -> undefined _ -> 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 :: (RgaRaw, PatchSet) -> Maybe (RgaRaw, PatchSet) reapplyRemovalsToState (RgaRaw state, ps@PatchSet{..}) = do VertexList{listHead = targetHead, listItems = targetItems} <- state asum [ do targetItems' <- applyRemoval parent tombstone targetItems pure ( RgaRaw . 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{opRef}} <- HashMap.lookup parent targetItems let item' = item{itemValue = v{opRef = max opRef tombstone}} pure $ HashMap.insert parent item' targetItems merge :: VertexList -> VertexList -> VertexList merge v1 v2 = fromMaybe undefined . vertexListFromOps $ (merge' `on` vertexListToOps . Just) 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{opEvent = e1, opRef = tombstone1, opPayload = p1} = v1 Op{opEvent = e2, opRef = tombstone2, opPayload = p2} = v2 -- priority of deletion mergeVertices = Op { opEvent = e1 , opRef = max tombstone1 tombstone2 , opPayload = maxOn length p1 p2 } -- | Name-UUID to use as RGA type marker. rgaType :: UUID rgaType = fromJust $ UUID.mkName "rga" -- | Typed RGA newtype RGA a = RGA [a] deriving (Eq) instance Replicated a => Replicated (RGA a) where encoding = objectEncoding instance Replicated a => ReplicatedAsObject (RGA a) where objectOpType = rgaType newObject (RGA items) = collectFrame $ do vertexIds <- lift $ getEventUuids $ ls60 $ genericLength items ops <- for (zip items vertexIds) $ \(item, vertexId) -> do payload <- newRon item pure $ Op vertexId Zero payload oid <- lift getEventUuid let version = maximumDef oid $ map opEvent ops tell $ Map.singleton (rgaType, oid) $ StateChunk version ops pure oid getObject obj@Object{..} = do StateChunk{..} <- getObjectStateChunk obj mItems <- for stateBody $ \Op{..} -> case opRef of Zero -> Just <$> fromRon opPayload objectFrame _ -> pure Nothing pure . RGA $ catMaybes mItems -- | Replace content of the RGA throug introducing changes detected by -- 'getGroupedDiffBy'. edit :: ( Replicated a, ReplicatedAsPayload a , ReplicaClock m, MonadError String m, MonadState (Object (RGA a)) m ) => [a] -> m () edit newItems = do obj@Object{..} <- get StateChunk{..} <- liftEither $ getObjectStateChunk obj advanceToUuid stateVersion let newItems' = [Op Zero Zero $ toPayload item | item <- newItems] let diff = getGroupedDiffBy ((==) `on` opPayload) stateBody newItems' (stateBody', Last lastEvent) <- runWriterT . fmap concat . for diff $ \case First removed -> for removed $ \case op@Op{opRef = Zero} -> do -- not deleted yet -- TODO(2018-11-03, #15, cblp) get sequential ids tombstone <- lift getEventUuid tell . Last $ Just tombstone pure op{opRef = tombstone} op -> -- deleted already pure op Both v _ -> pure v Second added -> for added $ \op -> do -- TODO(2018-11-03, #15, cblp) get sequential ids opEvent <- lift getEventUuid tell . Last $ Just opEvent pure op{opEvent} case lastEvent of Nothing -> pure () Just stateVersion' -> do let state' = StateChunk stateVersion' stateBody' put Object { objectFrame = Map.insert (rgaType, objectId) state' objectFrame , .. } -- | Speciaization of 'edit' for 'Text' editText :: (ReplicaClock m, MonadError String m, MonadState (Object RgaString) 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 :: (Replicated a, ReplicaClock m) => [a] -> m (Object (RGA a)) newFromList = newObject . RGA -- | Create an 'RgaString' from a text newFromText :: ReplicaClock m => Text -> m (Object RgaString) newFromText = newFromList . Text.unpack -- | Read elements from RGA getList :: Replicated a => Object (RGA a) -> Either String [a] getList = coerce . getObject -- | Read characters from 'RgaString' getText :: Object RgaString -> Either String Text getText = fmap Text.pack . getList