packages feed

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