packages feed

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

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}

-- | Observed-Remove Set (OR-Set)
module RON.Data.ORSet (
    ORSet (..),
    ORSetItem (..),
    ORSetMap,
    ORSetRep (..),
    addRef,
    addValue,
    findAnyAlive,
    findAnyAlive',
    removeObjectIf,
    removeRef,
    removeValue,
    setType,
    zoomItem,

    -- * struct_set
    addFieldValue,
    assignField,
    getFieldObject,
    newStruct,
    removeFieldValue,
    removeFieldValueIf,
    viewField,
    viewFieldLWW,
    viewFieldMax,
    viewFieldMin,
    viewFieldSet,
    zoomFieldObject,
) where

import RON.Prelude

import Data.Map.Strict qualified as Map

import RON.Data.Internal (
    MonadObjectState,
    ObjectStateT,
    Reducible,
    Rep,
    Replicated (encoding),
    ReplicatedAsObject,
    ReplicatedAsPayload (fromPayload),
    eqPayload,
    eqRef,
    evalObjectState,
    fromRon,
    getObjectState,
    getObjectStateChunk,
    modifyObjectStateChunk_,
    newObject,
    newRon,
    objectEncoding,
    rconcat,
    readObject,
    reduceObjectStates,
    reducibleOpType,
    stateFromChunk,
    stateToChunk,
 )
import RON.Error (MonadE, errorContext, throwErrorText)
import RON.Event (ReplicaClock, getEventUuid)
import RON.Semilattice (Semilattice)
import RON.Types (
    Atom (AUuid),
    ObjectFrame (..),
    ObjectRef (..),
    Op (..),
    Payload,
    StateChunk (..),
    StateFrame,
    UUID,
    WireStateChunk (..),
 )
import RON.UUID (pattern Zero)
import RON.UUID qualified as UUID
import RON.Util (Instance (..))

{- | Untyped OR-Set.
Implementation: a map from the itemKey to the original op.
Each time a value is added, a new item=op is created.
Deletion of a value replaces all its known items with tombstone ops.
-}
newtype ORSetRep = ORSetRep (Map UUID Op)
    deriving (Eq, Show)

itemKey :: Op -> UUID
itemKey Op{opId, refId} = case refId of
    Zero -> opId -- alive
    _ -> refId -- tombstone

preferTombstone :: Op -> Op -> Op
preferTombstone = maxOn (.refId)

instance Semigroup ORSetRep where
    ORSetRep set1 <> ORSetRep set2 =
        ORSetRep $ Map.unionWith preferTombstone set1 set2

instance Monoid ORSetRep where
    mempty = ORSetRep mempty

{- | Laws:
1. Idempotent because 'Map.unionWith' is idempotent.
2. Commutative because 'preferTombstone' is commutative.
-}
instance Semilattice ORSetRep

instance Reducible ORSetRep where
    reducibleOpType = setType

    stateFromChunk ops =
        ORSetRep $
            Map.fromListWith preferTombstone [(itemKey op, op) | op <- ops]

    stateToChunk (ORSetRep set) = sortOn (.opId) $ Map.elems set

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

-- | Name-UUID to use as OR-Set type marker.
setType :: UUID
setType = $(UUID.liftName "set")

{- | Type-directing wrapper for typed OR-Set.
'Eq' instance is purely technical, it doesn't use 'Ord', nor 'Hashable',
so its result may be confusing.
-}
newtype ORSet a = ORSet [a]
    deriving (Eq, Show)

{- TODO(2019-07-24, cblp, #102) data family ORSet c a where
    newtype instance ORSet Ord      a = ORSetOrd  (Set     a)
    newtype instance ORSet Hashable a = ORSetHash (HashSet a)
-}

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

instance (Replicated a) => ReplicatedAsObject (ORSet a) where
    type Rep (ORSet a) = ORSetRep

    newObject (ORSet items) = do
        ops <- for items $ \item -> do
            event <- getEventUuid
            payload <- newRon item
            pure $ Op event Zero payload
        oid <- getEventUuid
        modify' $ Map.insert oid $ wireStateChunk ops
        pure $ ObjectRef oid

    readObject = do
        StateChunk ops <- getObjectStateChunk
        mItems <- for ops $ \Op{refId, payload} -> case refId of
            Zero -> do
                item <- fromRon payload
                pure $ Just item
            _ -> pure Nothing
        pure . ORSet $ catMaybes mItems

-- | XXX Internal. Common implementation of 'addValue' and 'addRef'.
commonAdd :: (MonadE m, MonadObjectState a m, ReplicaClock m) => Payload -> m ()
commonAdd payload =
    modifyObjectStateChunk_ $ \(StateChunk stateBody) -> do
        event <- getEventUuid
        pure $ StateChunk $ stateBody ++ [Op event Zero payload]

-- | Encode a value and add a it to the OR-Set
addValue ::
    (MonadE m, MonadObjectState (ORSet a) m, ReplicaClock m, Replicated a) =>
    a ->
    m ()
addValue item = do
    payload <- newRon item
    commonAdd payload

addRef ::
    (MonadE m, MonadObjectState (ORSet a) m, ReplicaClock m) =>
    ObjectRef a ->
    m ()
addRef (ObjectRef itemUuid) = commonAdd [AUuid itemUuid]

-- | XXX Internal. Common implementation of 'removeValue' and 'removeRef'.
commonRemove ::
    (MonadE m, MonadObjectState (ORSet a) m, ReplicaClock m) =>
    (Payload -> m Bool) ->
    m ()
commonRemove isTarget =
    modifyObjectStateChunk_ $ \(StateChunk chunk) -> do
        let state0@(ORSetRep opMap) = stateFromChunk chunk
        targetEvents <-
            fmap catMaybes $
                for (toList opMap) $ \op@Op{refId, payload} ->
                    case refId of
                        Zero -> do
                            t <- isTarget payload
                            pure $ if t then Just op else Nothing
                        _ -> pure Nothing
        StateChunk
            <$> case targetEvents of
                [] -> pure chunk
                _ -> do
                    tombstone <- getEventUuid
                    let patch =
                            [ op{opId = tombstone, refId = observed}
                            | op@Op{opId = observed} <- targetEvents
                            ]
                    let state' = state0 <> stateFromChunk patch
                    pure $ stateToChunk state'

removeObjectIf ::
    (MonadE m, MonadObjectState (ORSet a) m, ReplicaClock m) =>
    ObjectStateT a m Bool ->
    m ()
removeObjectIf isTarget = commonRemove $ \case
    AUuid uuid' : _ -> do
        frame <- get
        evalObjectState ObjectFrame{uuid = uuid', frame} isTarget
    _ -> pure False

-- | Remove an atomic value from the OR-Set
removeValue ::
    ( MonadE m
    , MonadObjectState (ORSet a) m
    , ReplicaClock m
    , ReplicatedAsPayload a
    ) =>
    a ->
    m ()
removeValue v = commonRemove $ pure . eqPayload v

-- | Remove from a field all values that equal to the specified value.
removeFieldValue ::
    ( MonadE m
    , MonadObjectState struct m
    , ReplicaClock m
    , ReplicatedAsPayload a
    ) =>
    -- | Field name
    UUID ->
    -- | Value
    a ->
    m ()
removeFieldValue field value =
    removeFieldValueIfP field $ pure . eqPayload value

-- | Remove from a field all values that obey the predicate.
removeFieldValueIf ::
    ( MonadE m
    , MonadObjectState struct m
    , ReplicaClock m
    , ReplicatedAsPayload a
    ) =>
    -- | Field name
    UUID ->
    (a -> m Bool) ->
    m ()
removeFieldValueIf field isTarget =
    removeFieldValueIfP field $ \valuePayload -> do
        value <- fromPayload valuePayload
        isTarget value

-- | Remove from a field all payloads that obey the predicate.
removeFieldValueIfP ::
    (MonadE m, MonadObjectState struct m, ReplicaClock m) =>
    -- | Field name
    UUID ->
    (Payload -> m Bool) ->
    m ()
removeFieldValueIfP field isTarget =
    modifyObjectStateChunk_ $ \(StateChunk stateBody) -> do
        (observedOps, stateBody1) <-
            partitionM isFieldAliveAndSameValue stateBody
        removeOps <- for observedOps $ \op@Op{opId = observedEvent} -> do
            tombstone <- getEventUuid -- TODO(2019-07-10, cblp) sequential
            pure op{opId = tombstone, refId = observedEvent}
        let stateBody2 = stateBody1 ++ removeOps
        pure $ StateChunk stateBody2
  where
    isFieldAliveAndSameValue = \case
        Op{refId = Zero, payload = AUuid field' : valuePayload}
            | field' == field -> isTarget valuePayload
        _ -> pure False

-- | Remove an object reference from the OR-Set
removeRef ::
    (MonadE m, MonadObjectState (ORSet a) m, ReplicaClock m) =>
    ObjectRef a ->
    m ()
removeRef r = commonRemove $ pure . eqRef r

-- | Reference to an item inside an 'ORSet'.
newtype ORSetItem a = ORSetItem UUID
    deriving (Show)

{- | Go from modification of the whole set to the modification of an item
object.
-}
zoomItem ::
    (MonadE m) =>
    ORSetItem item ->
    ObjectStateT item m a ->
    ObjectStateT (ORSet item) m a
zoomItem (ORSetItem key) innerModifier = do
    ORSetRep opMap <- getObjectState
    itemValueRef <- case Map.lookup key opMap of
        Nothing ->
            -- TODO(2019-07-08, cblp) create empty object?
            throwErrorText "no such key in ORSet"
        Just Op{payload} -> case payload of
            [AUuid itemValueRef] -> pure itemValueRef
            _ -> throwErrorText "item payload is not an object ref"
    lift $ runReaderT innerModifier $ ObjectRef itemValueRef

-- | Find any alive item. If no alive item found, return 'Nothing'.
findAnyAlive ::
    (MonadE m, MonadObjectState (ORSet item) m) => m (Maybe (ORSetItem item))
findAnyAlive = do
    ORSetRep opMap <- getObjectState
    let aliveItems = [op | op@Op{refId = Zero} <- toList opMap]
    pure $ case listToMaybe aliveItems of
        Nothing -> Nothing
        Just Op{opId} -> Just $ ORSetItem opId

-- | Find any alive item. If no alive item found, report an error.
findAnyAlive' ::
    (MonadE m, MonadObjectState (ORSet item) m) => m (ORSetItem item)
findAnyAlive' = do
    mx <- findAnyAlive
    case mx of
        Just x -> pure x
        Nothing -> throwErrorText "empty set"

type ORSetMap k v = ORSet (k, v)

{- | Assign a value to a field, deleting all previous (observed) values.
Assignment of 'Nothing' just deletes all values.
-}
assignField ::
    (MonadE m, MonadObjectState struct m, ReplicaClock m, Replicated a) =>
    -- | Field name
    UUID ->
    -- | Value
    Maybe a ->
    m ()
assignField field mvalue =
    modifyObjectStateChunk_ $ \(StateChunk stateBody) -> do
        addOp <- case mvalue of
            Just value -> do
                event <- getEventUuid
                valuePayload <- newRon value
                pure $
                    Just
                        Op
                            { opId = event
                            , refId = Zero
                            , payload = AUuid field : valuePayload
                            }
            Nothing -> pure Nothing
        let (observedOps, stateBody1) = partition (isAliveField field) stateBody
        removeOps <- for observedOps $ \op@Op{opId = observedEvent} -> do
            tombstone <- getEventUuid -- TODO(2019-07-10, cblp) sequential
            pure op{opId = tombstone, refId = observedEvent}
        let stateBody2 = stateBody1 ++ toList addOp ++ removeOps
        pure $ StateChunk stateBody2

addFieldValue ::
    (MonadE m, MonadObjectState struct m, ReplicaClock m, Replicated a) =>
    -- | Field name
    UUID ->
    -- | Value
    a ->
    m ()
addFieldValue field value =
    modifyObjectStateChunk_ $ \(StateChunk stateBody) -> do
        event <- getEventUuid
        valuePayload <- newRon value
        let addOp =
                Op
                    { opId = event
                    , refId = Zero
                    , payload = AUuid field : valuePayload
                    }
        pure $ StateChunk $ stateBody ++ [addOp]

isAliveField :: UUID -> Op -> Bool
isAliveField field = \case
    Op{refId = Zero, payload = AUuid field' : _} -> field == field'
    _ -> False

filterAliveFieldPayloads ::
    -- | field
    UUID ->
    -- | state body
    [Op] ->
    -- | value payloads
    [Payload]
filterAliveFieldPayloads field ops =
    [ valuePayload
    | Op{refId = Zero, payload = AUuid field' : valuePayload} <- ops
    , field' == field
    ]

filterAliveFieldIdsAndPayloads ::
    -- | field
    UUID ->
    -- | state body
    [Op] ->
    -- | op ids and value payloads
    [(UUID, Payload)]
filterAliveFieldIdsAndPayloads field ops =
    [ (opId, valuePayload)
    | Op{opId, refId = Zero, payload = AUuid field' : valuePayload} <- ops
    , field' == field
    ]

-- | Find object field, merge all versions, return 'Nothing' if no versions
getFieldObject ::
    (MonadE m, MonadObjectState struct m, ReplicatedAsObject a) =>
    -- | Field name
    UUID ->
    m (Maybe (ObjectRef a))
getFieldObject field =
    errorContext "ORSet.getFieldObject" $ do
        StateChunk ops <- getObjectStateChunk
        let payloads = filterAliveFieldPayloads field ops
            refs = [ref | AUuid ref : _ <- payloads]
        case refs of
            [] -> pure Nothing
            p : ps -> fmap Just $ reduceObjectStates $ fmap ObjectRef $ p :| ps

-- | Decode field value, merge all versions, return 'Nothing' if no versions
viewField ::
    (MonadE m, MonadState StateFrame m, ReplicatedAsObject a) =>
    -- | Field name
    UUID ->
    -- | ORSet object chunk
    StateChunk ORSetRep ->
    m (Maybe a)
viewField field (StateChunk stateBody) =
    errorContext "ORSet.viewField" $ do
        let payloads = filterAliveFieldPayloads field stateBody
            refs = [ref | AUuid ref : _ <- payloads]
        case refs of
            [] -> pure Nothing
            p : ps -> fmap Just . rconcat $ p :| ps

-- | Decode field value, keep last version only
viewFieldLWW ::
    (MonadE m, MonadState StateFrame m, Replicated a) =>
    -- | Field name
    UUID ->
    -- | ORSet object chunk
    StateChunk ORSetRep ->
    m (Maybe a)
viewFieldLWW field (StateChunk stateBody) =
    errorContext "ORSet.viewFieldLWW" $ do
        let mPayload =
                fmap snd . maximumMayOn fst $
                    filterAliveFieldIdsAndPayloads field stateBody
        case mPayload of
            Nothing -> pure Nothing
            Just [] -> pure Nothing
            Just payload -> Just <$> fromRon payload

-- | Decode field value, keep max value only, only for Integer and Float
viewFieldMax ::
    (MonadE m, Ord a, ReplicatedAsPayload a) =>
    -- | Field name
    UUID ->
    -- | ORSet object chunk
    StateChunk ORSetRep ->
    m (Maybe a)
viewFieldMax field (StateChunk stateBody) =
    errorContext "ORSet.viewFieldMax" $
        fmap maximumMay $
            traverse fromPayload $
                filterAliveFieldPayloads field stateBody

-- | Decode field value, keep min value only, only for Integer and Float
viewFieldMin ::
    (MonadE m, Ord a, ReplicatedAsPayload a) =>
    -- | Field name
    UUID ->
    -- | ORSet object chunk
    StateChunk ORSetRep ->
    m (Maybe a)
viewFieldMin field (StateChunk stateBody) =
    errorContext "ORSet.viewFieldMin" $
        fmap minimumMay $
            traverse fromPayload $
                filterAliveFieldPayloads field stateBody

-- | Decode field value, keep all versions
viewFieldSet ::
    (MonadE m, MonadState StateFrame m, Replicated a) =>
    -- | Field name
    UUID ->
    -- | ORSet object chunk
    StateChunk ORSetRep ->
    m [a]
viewFieldSet field (StateChunk stateBody) =
    errorContext "ORSet.viewFieldSet" $
        traverse fromRon $
            filterAliveFieldPayloads field stateBody

-- | Pseudo-lens to an object inside a specified field
zoomFieldObject ::
    forall a field m struct.
    ( MonadE m
    , ReplicaClock m
    , ReplicatedAsObject field
    , ReplicatedAsObject struct
    ) =>
    -- | Field name
    UUID ->
    -- | Inner object modifier
    ObjectStateT field m a ->
    ObjectStateT struct m a
zoomFieldObject field innerModifier =
    errorContext ("ORSet.zoomFieldObject " <> show field) $ do
        (StateChunk stateBody) <- getObjectStateChunk
        let objectIds =
                [ objectId
                | AUuid objectId : _ <- filterAliveFieldPayloads field stateBody
                ]
        object <- case objectIds of
            [] -> ObjectRef <$> getEventUuid -- create empty object
            oid : oids ->
                reduceObjectStates @field $ fmap ObjectRef $ oid :| oids
        lift $ runReaderT innerModifier object

-- | Create an ORSet object from a list of named fields.
newStruct ::
    (MonadState StateFrame m, ReplicaClock m) =>
    [(UUID, Instance Replicated)] ->
    m UUID
newStruct fields = do
    objectId <- getEventUuid
    stateBody <-
        for fields $ \(name, Instance value) -> do
            opId <- getEventUuid -- TODO(2019-07-12, cblp, #15) sequential uuids
            valuePayload <- newRon value
            pure Op{opId, refId = Zero, payload = AUuid name : valuePayload}
    modify' $ Map.insert objectId $ wireStateChunk stateBody
    pure objectId

partitionM :: (Applicative m) => (a -> m Bool) -> [a] -> m ([a], [a])
partitionM f = \case
    [] -> pure ([], [])
    x : xs -> do
        res <- f x
        (as, bs) <- partitionM f xs
        pure ([x | res] ++ as, [x | not res] ++ bs)