ron-rdt-0.11: lib/RON/Data/Internal.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DisambiguateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE OverloadedRecordDot #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PatternSynonyms #-}
{-# LANGUAGE Rank2Types #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE ViewPatterns #-}
module RON.Data.Internal (
Encoding (..),
ReducedChunk (..),
Reducer (..),
Reducible (..),
Replicated (..),
ReplicatedAsObject (..),
ReplicatedAsPayload (..),
Unapplied,
WireReducer,
advanceToObject,
eqPayload,
eqRef,
evalObjectState,
evalObjectState_,
getObjectState,
getObjectStateChunk,
modifyObjectStateChunk,
modifyObjectStateChunk_,
newObjectFrame,
reduceState,
reduceObjectStates,
stateFromWireChunk,
stateToWireChunk,
tryFromRon,
tryOptionFromRon,
wireStateChunk,
pattern Some,
--
objectEncoding,
rconcat,
payloadEncoding,
--
fromRon,
newRon,
--
ObjectStateT,
MonadObjectState,
) where
import RON.Prelude
import Data.List (minimum)
import Data.Map.Strict qualified as Map
import Data.Text qualified as Text
import RON.Error (
Error (Error),
MonadE,
correct,
errorContext,
liftMaybe,
)
import RON.Event (Event (..), ReplicaClock, advance, decodeEvent, timeValue)
import RON.Semilattice (BoundedSemilattice)
import RON.Types (
Atom (AInteger, AString, AUuid),
ObjectFrame (ObjectFrame, frame, uuid),
ObjectRef (ObjectRef),
Op (Op, opId, payload, refId),
Payload,
StateChunk (StateChunk),
StateFrame,
UUID (UUID),
WireChunk,
WireStateChunk (WireStateChunk, stateBody, stateType),
)
-- | Reduce all chunks of specific type and object in the frame
type WireReducer = UUID -> NonEmpty WireChunk -> [WireChunk]
data Reducer = Reducer
{ wireReducer :: WireReducer
, stateReducer :: WireStateChunk -> WireStateChunk -> WireStateChunk
}
-- | Unapplied patches and raw ops
type Unapplied = ([ReducedChunk], [Op])
{- | Untyped-reducible types.
Untyped means if this type is a container then the types of data contained in
it is not considered.
-}
class (BoundedSemilattice a, Eq a) => Reducible a where
-- | UUID of the type
reducibleOpType :: UUID
-- | Load a state from a state chunk.
stateFromChunk :: [Op] -> a
-- | Store a state to a state chunk
stateToChunk :: a -> [Op]
-- | Merge a state with patches and raw ops
applyPatches :: a -> Unapplied -> (a, Unapplied)
applyPatches a (patches, ops) =
( a
<> foldMap ((.patchValue) . patchFromChunk) patches
<> foldMap ((.patchValue) . patchFromRawOp) ops
, mempty
)
-- | Merge patches and raw ops into bigger patches or throw obsolete ops
reduceUnappliedPatches :: Unapplied -> Unapplied
reduceUnappliedPatches (patches, ops) =
( maybeToList
. fmap (patchToChunk @a . sconcat)
. nonEmpty
$ map patchFromChunk patches <> map patchFromRawOp ops
, []
)
stateToWireChunk :: forall rep. (Reducible rep) => rep -> WireStateChunk
stateToWireChunk rep = wireStateChunk @rep $ StateChunk $ stateToChunk @rep rep
stateFromWireChunk ::
forall a m. (MonadE m, Reducible a) => WireStateChunk -> m a
stateFromWireChunk WireStateChunk{stateType, stateBody} = do
unless (stateType == reducibleOpType @a) $
throwError $
Error
"bad type"
[ Error ("expected " <> show (reducibleOpType @a)) []
, Error ("got " <> show stateType) []
]
pure $ stateFromChunk stateBody
data ReducedChunk = ReducedChunk
{ rcRef :: UUID
, rcBody :: [Op]
}
deriving (Show)
data Patch a = Patch {patchRef :: UUID, patchValue :: a}
instance (Semigroup a) => Semigroup (Patch a) where
Patch ref1 a1 <> Patch ref2 a2 = Patch (min ref1 ref2) (a1 <> a2)
patchFromRawOp :: (Reducible a) => Op -> Patch a
patchFromRawOp op@Op{opId} =
Patch{patchRef = opId, patchValue = stateFromChunk [op]}
patchFromChunk :: (Reducible a) => ReducedChunk -> Patch a
patchFromChunk ReducedChunk{rcRef, rcBody} =
Patch{patchRef = rcRef, patchValue = stateFromChunk rcBody}
patchToChunk :: (Reducible a) => Patch a -> ReducedChunk
patchToChunk Patch{patchRef, patchValue} =
ReducedChunk{rcRef = patchRef, rcBody = stateBody}
where
WireStateChunk{stateBody} = stateToWireChunk patchValue
-- | Base class for typed encoding
class Replicated a where
{- | Instances SHOULD implement 'encoding' either as 'objectEncoding' or as
'payloadEncoding'
-}
encoding :: Encoding a
data Encoding a = Encoding
{ encodingNewRon ::
forall m.
(MonadState StateFrame m, ReplicaClock m) =>
a ->
m Payload
, encodingFromRon ::
forall m.
(MonadE m, MonadState StateFrame m) =>
Payload ->
m a
}
-- | Encode typed data to a payload with possible addition objects
newRon ::
(MonadState StateFrame m, ReplicaClock m, Replicated a) => a -> m Payload
newRon = encodingNewRon where Encoding{encodingNewRon} = encoding
{- | Decode typed data from a payload.
The implementation may use other objects in the frame to resolve references.
-}
fromRon :: (MonadE m, MonadState StateFrame m, Replicated a) => Payload -> m a
fromRon = encodingFromRon where Encoding{encodingFromRon} = encoding
-- | Standard implementation of 'Replicated' for 'ReplicatedAsObject' types.
objectEncoding :: (ReplicatedAsObject a) => Encoding a
objectEncoding =
Encoding
{ encodingNewRon = \a -> do
ObjectRef uuid <- newObject a
pure [AUuid uuid]
, encodingFromRon = objectFromRon $ runReaderT readObject
}
-- | Standard implementation of 'Replicated' for 'ReplicatedAsPayload' types.
payloadEncoding :: (ReplicatedAsPayload a) => Encoding a
payloadEncoding =
Encoding
{ encodingNewRon = pure . toPayload
, encodingFromRon = fromPayload
}
{- | Instances of this class are encoded as payload only.
Law: @'encoding' == 'payloadEncoding'@
-}
class (Replicated a) => ReplicatedAsPayload a where
-- | Encode data
toPayload :: a -> Payload
-- | Decode data
fromPayload :: (MonadE m) => Payload -> m a
instance Replicated Int64 where encoding = payloadEncoding
instance ReplicatedAsPayload Int64 where
toPayload int = [AInteger int]
fromPayload atoms = errorContext "Integer" $ case atoms of
[AInteger int] -> pure int
_ -> throwError "Expected Integer"
instance Replicated UUID where encoding = payloadEncoding
instance ReplicatedAsPayload UUID where
toPayload u = [AUuid u]
fromPayload atoms = errorContext "UUID" $ case atoms of
[AUuid u] -> pure u
_ -> throwError "Expected UUID"
instance Replicated Text where encoding = payloadEncoding
instance ReplicatedAsPayload Text where
toPayload t = [AString t]
fromPayload atoms = errorContext "String" $ case atoms of
[AString t] -> pure t
_ -> throwError "Expected String"
instance Replicated Char where encoding = payloadEncoding
instance ReplicatedAsPayload Char where
toPayload c = [AString $ Text.singleton c]
fromPayload atoms = errorContext "Char" $ case atoms of
[AString (Text.uncons -> Just (c, ""))] -> pure c
_ -> throwError "Expected one-character string"
{- | Instances of this class are encoded as objects.
An enclosing object's payload will be filled with this object's id.
Law: @'encoding' == 'objectEncoding'@
-}
class (Reducible (Rep a), Replicated a) => ReplicatedAsObject a where
-- | Untyped RON-RDT representation
type Rep a
-- | Encode data. Write frame and return id.
newObject ::
(MonadState StateFrame m, ReplicaClock m) => a -> m (ObjectRef a)
-- | Decode data
readObject :: (MonadE m, MonadObjectState a m) => m a
objectFromRon :: (MonadE m) => (ObjectRef a -> m a) -> Payload -> m a
objectFromRon handler atoms =
errorContext "objectFromRon" $
case atoms of
[AUuid uuid] -> handler $ ObjectRef uuid
_ -> throwError "Expected object UUID"
-- | Create new 'ObjectFrame' from a value
newObjectFrame ::
(ReplicaClock m, ReplicatedAsObject a) => a -> m (ObjectFrame a)
newObjectFrame a = do
(ObjectRef uuid, frame) <- runStateT (newObject a) mempty
pure ObjectFrame{uuid, frame}
getObjectStateChunk ::
forall a m. (MonadE m, MonadObjectState a m) => m (StateChunk (Rep a))
getObjectStateChunk = do
ObjectRef uuid <- ask
frame <- get
WireStateChunk{stateType, stateBody} <-
liftMaybe "no such object in chunk" $ Map.lookup uuid frame
unless (stateType == reducibleOpType @(Rep a)) $
throwError $
Error
"bad type"
[ Error ("expected " <> show (reducibleOpType @(Rep a))) []
, Error ("got " <> show stateType) []
]
pure $ StateChunk stateBody
modifyObjectStateChunk ::
forall a b m.
(MonadE m, MonadObjectState a m, ReplicaClock m) =>
(StateChunk (Rep a) -> m (b, StateChunk (Rep a))) ->
m b
modifyObjectStateChunk f = do
advanceToObject
ObjectRef uuid <- ask
chunk <- getObjectStateChunk
(a, StateChunk chunk') <- f chunk
modify' $
Map.insert
uuid
WireStateChunk
{ stateType = reducibleOpType @(Rep a)
, stateBody = chunk'
}
pure a
modifyObjectStateChunk_ ::
(MonadE m, MonadObjectState a m, ReplicaClock m) =>
(StateChunk (Rep a) -> m (StateChunk (Rep a))) ->
m ()
modifyObjectStateChunk_ f =
modifyObjectStateChunk \chunk -> do
chunk' <- f chunk
pure ((), chunk')
eqRef :: ObjectRef a -> Payload -> Bool
eqRef (ObjectRef uuid) = \case
[AUuid ref] -> uuid == ref
_ -> False
eqPayload :: (ReplicatedAsPayload a) => a -> Payload -> Bool
eqPayload a = (toPayload a ==)
pattern ATrue :: Atom
pattern ATrue = AUuid (UUID 0xe36e69000000000 0) -- true
pattern AFalse :: Atom
pattern AFalse = AUuid (UUID 0xaa5c37a40000000 0) -- false
instance Replicated Bool where encoding = payloadEncoding
instance ReplicatedAsPayload Bool where
toPayload b
| b = [ATrue]
| otherwise = [AFalse]
fromPayload =
errorContext "Bool" . \case
[ATrue] -> pure True
[AFalse] -> pure False
_ -> throwError "Expected single UUID `true` or `false`"
type ObjectStateT b m a = ReaderT (ObjectRef b) (StateT StateFrame m) a
type MonadObjectState a m =
(MonadReader (ObjectRef a) m, MonadState StateFrame m, Reducible (Rep a))
advanceToObject :: (MonadE m, MonadObjectState a m, ReplicaClock m) => m ()
advanceToObject = do
allObjectTimes <- getAllObjectTimes
for_ (maximumMay allObjectTimes) advance
where
getAllObjectTimes =
map (\Event{time} -> timeValue time) . mapMaybe decodeEvent
<$> getAllObjectUuids
getAllObjectUuids = do
ObjectRef objectId <- ask
StateChunk chunk <- getObjectStateChunk
pure $ objectId : foldMap allOpUuids chunk
allOpUuids Op{opId, refId, payload} =
opId : refId : [u | AUuid u <- payload]
reduceState ::
forall rep.
(Reducible rep) =>
StateChunk rep ->
StateChunk rep ->
StateChunk rep
reduceState (StateChunk s1) (StateChunk s2) =
StateChunk $ stateToChunk @rep $ stateFromChunk s1 <> stateFromChunk s2
reduceObjectStates ::
forall a m.
(MonadE m, MonadState StateFrame m, ReplicatedAsObject a) =>
NonEmpty (ObjectRef a) ->
m (ObjectRef a)
reduceObjectStates (obj :| objs) = do
c :| cs <- for (obj :| objs) $ runReaderT getObjectStateChunk
let chunk = foldl' (reduceState @(Rep a)) c cs
oid = minimum [i | ObjectRef i <- obj : objs]
modify' $ Map.insert oid $ wireStateChunk chunk
pure $ ObjectRef oid
rconcat ::
forall a m.
(MonadE m, MonadState StateFrame m, ReplicatedAsObject a) =>
NonEmpty UUID ->
m a
rconcat uuids =
errorContext "rconcat" do
ObjectRef ref <- reduceObjectStates @a $ ObjectRef <$> uuids
fromRon [AUuid ref]
tryFromRon ::
(MonadE m, MonadState StateFrame m, Replicated a) =>
Payload ->
m (Maybe a)
tryFromRon = correct Nothing . fmap Just . fromRon
tryOptionFromRon ::
(MonadE m, MonadState StateFrame m, Replicated a) =>
Payload ->
m (Maybe a)
tryOptionFromRon payload = case payload of
Some : payload' ->
correct Nothing $
(Just <$> fromRon payload)
`catchError` \e1 ->
(Just <$> fromRon payload')
`catchError` \e2 ->
throwError $ Error "tryOptionFromRon" [e1, e2]
_ -> tryFromRon payload
{- | TODO(2019-08-06, cblp) Remove a year after release
(the release is planned on 2019-08; removal is planned on 2020-08)
-}
pattern Some :: Atom
pattern Some = AUuid (UUID 0xdf3c69000000000 0)
{-# DEPRECATED Some "Will be removed soon" #-}
getObjectState :: (MonadE m, MonadObjectState a m) => m (Rep a)
getObjectState = do
StateChunk chunk <- getObjectStateChunk
pure $ stateFromChunk chunk
wireStateChunk ::
forall rep. (Reducible rep) => StateChunk rep -> WireStateChunk
wireStateChunk (StateChunk stateBody) =
WireStateChunk{stateType = reducibleOpType @rep, stateBody}
-- | Run ObjectFrame action
evalObjectState :: (Monad m) => ObjectFrame b -> ObjectStateT b m a -> m a
evalObjectState ObjectFrame{uuid, frame} action =
evalStateT (runReaderT action $ ObjectRef uuid) frame
-- | Run ObjectFrame action, starting with an empty frame
evalObjectState_ :: (Monad m) => StateT StateFrame m a -> m a
evalObjectState_ action = evalStateT action mempty
instance Replicated (ObjectRef a) where
encoding = payloadEncoding
instance ReplicatedAsPayload (ObjectRef a) where
toPayload (ObjectRef uuid) = toPayload uuid
fromPayload = fmap ObjectRef . fromPayload