ron-rdt-0.5: lib/RON/Data/ORSet.hs
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE TypeApplications #-}
-- | Observed-Remove Set (OR-Set)
module RON.Data.ORSet
( ORSet (..)
, ObjectORSet (..)
, ORSetRaw
, addNewRef
, addRef
, addValue
, removeRef
, removeValue
) where
import qualified Data.Map.Strict as Map
import RON.Data.Internal
import RON.Error (MonadE)
import RON.Event (ReplicaClock, getEventUuid)
import RON.Types (Atom, Object (Object, frame, id),
Op (Op, opId, payload, refId),
StateChunk (StateChunk, stateBody, stateType, stateVersion),
UUID)
import RON.UUID (pattern Zero)
import qualified RON.UUID as UUID
-- | Untyped OR-Set.
-- Implementation:
-- a map from the last change (creation or deletion) to the original op.
newtype ORSetRaw = ORSetRaw (Map UUID Op)
deriving (Eq, Show)
opKey :: Op -> UUID
opKey Op{opId, refId} = case refId of
Zero -> opId -- alive
_ -> refId -- tombstone
observedRemove :: Op -> Op -> Op
observedRemove = maxOn refId
instance Semigroup ORSetRaw where
ORSetRaw set1 <> ORSetRaw set2 =
ORSetRaw $ Map.unionWith observedRemove set1 set2
instance Monoid ORSetRaw where
mempty = ORSetRaw mempty
instance Reducible ORSetRaw where
reducibleOpType = setType
stateFromChunk ops =
ORSetRaw $ Map.fromListWith observedRemove [(opKey op, op) | op <- ops]
stateToChunk (ORSetRaw set) =
mkStateChunk setType . sortOn opId $ Map.elems set
-- | Name-UUID to use as OR-Set type marker.
setType :: UUID
setType = $(UUID.liftName "set")
-- | Type-directing wrapper for typed OR-Set of atomic values
newtype ORSet a = ORSet [a]
-- | Type-directing wrapper for typed OR-Set of objects
newtype ObjectORSet a = ObjectORSet [a]
instance ReplicatedAsPayload a => Replicated (ORSet a) where
encoding = objectEncoding
instance ReplicatedAsPayload a => ReplicatedAsObject (ORSet a) where
objectOpType = setType
newObject = commonNewObject pure
getObject = commonGetObject pure
instance ReplicatedAsObject a => Replicated (ObjectORSet a) where
encoding = objectEncoding
instance ReplicatedAsObject a => ReplicatedAsObject (ObjectORSet a) where
objectOpType = setType
newObject = commonNewObject $ fmap (\Object{id} -> id) . newObject
getObject obj@Object{frame} =
commonGetObject (\itemId -> getObject (Object itemId frame)) obj
commonNewObject
:: ( Coercible (orset item) [item]
, ReplicaClock m
, ReplicatedAsPayload itemRep
)
=> (item -> m itemRep) -> orset item -> m (Object (orset item))
commonNewObject newItem items = collectFrame $ do
ops <- for (coerce items) $ \item -> do
event <- lift getEventUuid
payload <- lift $ newItem item
pure . Op event Zero $ toPayload payload
oid <- lift getEventUuid
let stateVersion = maximumDef oid $ map opId ops
tell $
Map.singleton oid $
StateChunk{stateType = setType, stateVersion, stateBody = ops}
pure oid
commonGetObject
:: forall item m orset itemRep
. (Coercible (orset item) [item], MonadE m, ReplicatedAsPayload itemRep)
=> (itemRep -> m item) -> Object (orset item) -> m (orset item)
commonGetObject getItem obj@Object{..} = do
StateChunk{..} <- getObjectStateChunk obj
mItems <- for stateBody $ \Op{refId, payload} -> case refId of
Zero -> Just <$> (fromPayload payload >>= getItem)
_ -> pure Nothing
pure . coerce @[item] $ catMaybes mItems
-- | XXX Internal. Common implementation of 'addValue' and 'addRef'.
commonAdd :: (ReplicatedAsPayload b, ReplicaClock m, MonadE m)
=> b -> StateT (Object a) m ()
commonAdd item = do
obj@Object{id, frame} <- get
StateChunk{..} <- getObjectStateChunk obj
event <- getEventUuid
let payload = toPayload item
let newOp = Op event Zero payload
let chunk' = stateBody ++ [newOp]
let state' = StateChunk
{stateType = setType, stateVersion = event, stateBody = chunk'}
put obj{frame = Map.insert id state' frame}
-- | Add atomic value to the OR-Set
addValue
:: (ReplicatedAsPayload a, ReplicaClock m, MonadE m)
=> a -> StateT (Object (ORSet a)) m ()
addValue = commonAdd
-- | Add a reference to the object to the OR-Set
addRef
:: (ReplicaClock m, MonadE m)
=> Object a -> StateT (Object (ObjectORSet a)) m ()
addRef Object{id = itemId, frame = itemFrame} = do
modify' $ \Object{..} -> Object{frame = frame <> itemFrame, ..}
commonAdd itemId
-- | Encode an object and add a reference to it to the OR-Set
addNewRef
:: forall a m
. (ReplicatedAsObject a, ReplicaClock m, MonadE m)
=> a -> StateT (Object (ObjectORSet a)) m (Object a)
addNewRef item = do
itemObj@(Object _ itemFrame) <- lift $ newObject item
modify' $ \Object{..} -> Object{frame = frame <> itemFrame, ..}
addRef itemObj
pure itemObj
-- | XXX Internal. Common implementation of 'removeValue' and 'removeRef'.
commonRemove
:: (MonadE m, ReplicaClock m)
=> ([Atom] -> Bool) -> StateT (Object (orset a)) m ()
commonRemove isTarget = do
obj@Object{id, frame} <- get
StateChunk{..} <- getObjectStateChunk obj
let state0@(ORSetRaw opMap) = stateFromChunk stateBody
let targetEvents =
[ opId
| Op{opId, refId, payload} <- toList opMap
, refId == Zero -- is alive
, isTarget payload
]
case targetEvents of
[] -> pure ()
_ -> do
tombstone <- getEventUuid
let patch =
[ Op{opId = tombstone, refId, payload = []}
| refId <- targetEvents
]
let chunk' = state0 <> stateFromChunk patch
let state' = stateToChunk chunk'
put obj{frame = Map.insert id state' frame}
-- | Remove an atomic value from the OR-Set
removeValue
:: (ReplicatedAsPayload a, MonadE m, ReplicaClock m)
=> a -> StateT (Object (ORSet a)) m ()
removeValue = commonRemove . eqPayload
-- | Remove an object reference from the OR-Set
removeRef
:: (MonadE m, ReplicaClock m)
=> Object a -> StateT (Object (ObjectORSet a)) m ()
removeRef = commonRemove . eqRef