packages feed

ron-rdt-0.11: lib/RON/Data/CT.hs

{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DerivingStrategies #-}
{-# 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 #-}

-- | Causal Tree (CT)
module RON.Data.CT (
    CT (..),
    CTRep,
    CTString,
    edit,
    editText,
    getAliveWithIndices,
    aliveWithIndices,
    getList,
    getText,
    insert,
    insertAtBegin,
    insertText,
    insertTextAtBegin,
    newFromList,
    newFromText,
    remove,
    ctType,
    RON.Data.CT.toList,
    toText,
)
where

import RON.Prelude

import Control.Monad.State (state)
import Data.Algorithm.Diff (PolyDiff (Both, First, Second), getGroupedDiffBy)
import Data.Foldable qualified as Foldable
import Data.Map.Strict ((!?))
import Data.Map.Strict qualified as Map
import Data.Set qualified as Set
import Data.Text qualified as Text

import RON.Data.Internal (
    MonadObjectState,
    Reducible,
    Rep,
    Replicated (encoding),
    ReplicatedAsObject,
    ReplicatedAsPayload,
    fromRon,
    getObjectStateChunk,
    modifyObjectStateChunk_,
    newObject,
    newRon,
    objectEncoding,
    readObject,
    reducibleOpType,
    stateFromChunk,
    stateToChunk,
    toPayload,
 )
import RON.Error (MonadE, errorContext)
import RON.Event (ReplicaClock, getEventUuid, getEventUuids)
import RON.Semilattice (Semilattice)
import RON.Types (
    ObjectRef (ObjectRef),
    Op (Op, opId, payload, refId),
    Payload,
    StateChunk (StateChunk),
    StateFrame,
    UUID,
    WireStateChunk (WireStateChunk, stateBody, stateType),
 )
import RON.UUID (zero)
import RON.UUID qualified as UUID
import RON.Util.Word (ls60)

data CTRepItem = CTRepItem
    { op :: Op
    , next :: Maybe UUID
    -- ^ Nothing is the end
    , isDeleted :: Bool
    -- ^ is tombstone or has tombstone among children
    }
    deriving (Eq, Show)

-- | Untyped Causal Tree
data CTRep = CTRep
    { ops :: Map UUID CTRepItem
    -- ^ indexed by opId
    , start :: Maybe UUID
    }
    deriving stock (Eq, Show)
    deriving anyclass (Semilattice)

instance Semigroup CTRep where
    c <> d = CTRep{ops = c.ops <> d.ops, start = min c.start d.start}

instance Monoid CTRep where
    mempty = CTRep{ops = mempty, start = Nothing}

instance Reducible CTRep where
    reducibleOpType = ctType
    stateFromChunk = ctRepFromChunk
    stateToChunk CTRep{ops} = map (.op) $ Foldable.toList ops

ctRepFromChunk :: [Op] -> CTRep
ctRepFromChunk ops = CTRep{ops = items, start}
  where
    opIndex = Map.fromList [(op.opId, op) | op <- ops]

    -- from parent to children
    childrenIndex =
        Map.fromListWith (<>) [(op.refId, Set.singleton op.opId) | op <- ops]

    roots = childrenIndex !? zero ?: Set.empty

    traversed = traverseFromNodes roots
      where
        traverseFromNodes = foldMap traverseFromNode . reverse . Foldable.toList
        traverseFromNode p =
            p : traverseFromNodes (childrenIndex !? p ?: Set.empty)

    items =
        Map.fromList
            [ (opId, CTRepItem{op, next, isDeleted})
            | (opId, next) <-
                zip traversed (map Just (drop 1 traversed) ++ [Nothing])
            , let op =
                    opIndex
                        !? opId
                        ?: error
                            ( "CTRep: op "
                                <> show opId
                                <> " not found in "
                                <> show opIndex
                            )
            , let children = childrenIndex !? opId ?: Set.empty
            , let isDeleted =
                    null op.payload
                        || any
                            ( \c ->
                                maybe False (\d -> null d.payload) $
                                    opIndex !? c
                            )
                            children
            ]

    start = Set.lookupMax roots

-- | Name-UUID to use as CT type marker.
ctType :: UUID
ctType = $(UUID.liftName "ct")

-- | Typed CT
newtype CT a = CT [a]
    deriving (Eq, Show)

instance (Replicated a) => Replicated (CT a) where encoding = objectEncoding

instance (Replicated a) => ReplicatedAsObject (CT a) where
    type Rep (CT a) = CTRep

    newObject (CT items) = do
        oid <- getEventUuid
        opIds <- getEventUuids $ ls60 $ genericLength items
        ops <-
            for (zip3 items opIds (zero : opIds)) \(item, opId, refId) -> do
                payload <- newRon item
                pure Op{opId, refId, payload}
        modify' $ Map.insert oid $ wireStateChunk ops
        pure $ ObjectRef oid

    readObject = do
        StateChunk stateBody <- getObjectStateChunk
        fmap CT $
            traverse (fromRon . snd) $
                aliveWithIndices $
                    stateFromChunk stateBody

{- | Replace content of the CT through introducing changes detected by
'getGroupedDiffBy'.
-}
edit ::
    ( MonadE m
    , MonadObjectState (CT a) m
    , ReplicaClock m
    , ReplicatedAsPayload a
    ) =>
    [a] ->
    m ()
edit newItems =
    modifyObjectStateChunk_ \(StateChunk stateBody) -> do
        let oldItems' = aliveWithIndices $ stateFromChunk stateBody
        -- TODO(2019-04-17, #59, cblp) replace 'toPayload' with 'newRon' and
        -- relax constraint on 'a' from 'ReplicatedAsPayload' to
        -- 'Replicated'
        let newItems' = map toPayload newItems
        let diff =
                getGroupedDiffBy
                    (\(_, p1) p2 -> p1 == p2)
                    oldItems'
                    newItems'
        newOps <-
            (`evalStateT` zero {- prevous op id -}) $
                fold <$> for diff \case
                    First removed -> do
                        opIds <- getEventUuids $ genericLength removed
                        for (zip removed opIds) \((refId, _), opId) -> do
                            put opId
                            pure Op{opId, refId, payload = []}
                    Both xs _ -> do
                        for_ (lastMay xs) \(opId, _payload) -> put opId
                        pure []
                    Second added -> do
                        opIds <- getEventUuids $ genericLength added
                        for (zip added opIds) \(item, opId) -> do
                            refId <- state (,opId)
                            pure Op{opId, refId, payload = item}
        pure $ StateChunk $ stateBody <> newOps

-- | Speciaization of 'edit' for 'Text'
editText ::
    (MonadE m, MonadObjectState CTString m, ReplicaClock m) => Text -> m ()
editText = edit . Text.unpack

{- | Speciaization of 'CT' to 'Char'.
This is the recommended way to store a string.
-}
type CTString = CT Char

-- | Create a CT from a list
newFromList ::
    (MonadState StateFrame m, ReplicaClock m, Replicated a) =>
    [a] ->
    m (ObjectRef (CT a))
newFromList = newObject . CT

-- | Create a 'CTString' from a text
newFromText ::
    (MonadState StateFrame m, ReplicaClock m) =>
    Text ->
    m (ObjectRef CTString)
newFromText = newFromList . Text.unpack

-- | Read elements from CT
getList :: (MonadE m, MonadObjectState (CT a) m, Replicated a) => m [a]
getList = coerce <$> readObject

-- | Read characters from 'CTString'
getText :: (MonadE m, MonadObjectState CTString 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 (CT a) m, ReplicaClock m, Replicated a) =>
    [a] ->
    -- | position
    UUID ->
    m ()
insert [] _ = pure ()
insert items startRef =
    modifyObjectStateChunk_ \(StateChunk stateBody) -> do
        opIds <- getEventUuids $ ls60 $ genericLength items
        ops <-
            for (zip3 items opIds (startRef : opIds)) \(item, opId, refId) -> do
                payload <- newRon item
                pure Op{opId, refId, payload}
        pure $ StateChunk $ stateBody <> ops

insertAtBegin ::
    (MonadE m, MonadObjectState (CT a) m, ReplicaClock m, Replicated a) =>
    [a] ->
    m ()
insertAtBegin items = insert items zero

{- | Insert a text after the specified position.
Position is identified by 'UUID'. 'zero' means the beginning.
-}
insertText ::
    (MonadE m, MonadObjectState CTString m, ReplicaClock m) =>
    Text ->
    -- | position
    UUID ->
    m ()
insertText = insert . Text.unpack

insertTextAtBegin ::
    (MonadE m, MonadObjectState CTString m, ReplicaClock m) => Text -> m ()
insertTextAtBegin text = insertText text zero

-- | Record a removal of a specific item
remove ::
    (MonadE m, MonadObjectState (CT a) m, ReplicaClock m) =>
    -- | reference to the item to remove
    UUID ->
    m ()
remove refId =
    errorContext "CT.remove" . errorContext ("refId = " <> show refId) $
        modifyObjectStateChunk_ \(StateChunk stateBody) -> do
            opId <- getEventUuid
            let op = Op{opId, refId, payload = []}
            pure $ StateChunk $ stateBody <> [op]

wireStateChunk :: [Op] -> WireStateChunk
wireStateChunk stateBody = WireStateChunk{stateType = ctType, stateBody}

toList :: CT a -> [a]
toList (CT xs) = xs

toText :: CTString -> Text
toText (CT s) = Text.pack s

aliveWithIndices :: CTRep -> [(UUID, Payload)]
aliveWithIndices CTRep{ops, start} = go start
  where
    go = \case
        Nothing -> []
        Just p ->
            case ops !? p of
                Nothing -> []
                Just CTRepItem{op, next, isDeleted} ->
                    [(op.opId, op.payload) | not isDeleted] ++ go next

getAliveWithIndices ::
    (MonadE m, MonadObjectState (CT a) m) =>
    m [(UUID, Payload)]
getAliveWithIndices = do
    StateChunk stateBody <- getObjectStateChunk
    pure $ aliveWithIndices $ stateFromChunk stateBody