{-# LANGUAGE FlexibleContexts #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE TypeApplications #-} -- | LWW-per-field RDT module RON.Data.LWW ( LwwPerField (..) , assignField , lwwType , newObject , readField , viewField , zoomField ) where import RON.Internal.Prelude import Control.Error (fmapL) import Control.Monad.Except (MonadError, liftEither) import Control.Monad.State.Strict (MonadState, StateT, get, put, runStateT) import Control.Monad.Writer.Strict (lift, runWriterT, tell) import qualified Data.Map.Strict as Map import RON.Data.Internal (Reducible, Replicated, ReplicatedAsObject, collectFrame, fromRon, getObjectStateChunk, mkStateChunk, newRon, objectOpType, reducibleOpType, stateFromChunk, stateToChunk) import RON.Event (ReplicaClock, advanceToUuid, getEventUuid) import RON.Types (Atom (AUuid), Object (..), Op (..), StateChunk (..), StateFrame, UUID) import qualified RON.UUID as UUID -- | Last-Write-Wins: select an op with latter event lww :: Op -> Op -> Op lww = maxOn opEvent -- | Untyped LWW. Implementation: a map from 'opRef' to the original op. newtype LwwPerField = LwwPerField (Map UUID Op) deriving (Eq, Monoid, Show) instance Semigroup LwwPerField where LwwPerField fields1 <> LwwPerField fields2 = LwwPerField $ Map.unionWith lww fields1 fields2 instance Reducible LwwPerField where reducibleOpType = lwwType stateFromChunk ops = LwwPerField $ Map.fromListWith lww [(opRef op, op) | op <- ops] stateToChunk (LwwPerField fields) = mkStateChunk $ Map.elems fields -- | Name-UUID to use as LWW type marker. lwwType :: UUID lwwType = fromJust $ UUID.mkName "lww" -- | Create LWW object from a list of named fields. newObject :: ReplicaClock m => [(UUID, I Replicated)] -> m (Object a) newObject fields = collectFrame $ do payloads <- for fields $ \(_, I value) -> newRon value e <- lift getEventUuid tell $ Map.singleton (lwwType, e) $ StateChunk e [Op e name p | ((name, _), p) <- zip fields payloads] pure e -- | Decode field value viewField :: Replicated a => UUID -- ^ Field name -> StateChunk -- ^ LWW object chunk -> StateFrame -> Either String a viewField field StateChunk{..} frame = fmapL (("LWW.viewField " <> show field <> ":\n") <>) $ do let ops = filter ((field ==) . opRef) stateBody Op{..} <- case ops of [] -> Left $ unwords ["no field", show field, "in lww chunk"] [op] -> pure op _ -> Left "unreduced state" fromRon opPayload frame -- | Decode field value readField :: ( MonadError String m , MonadState (Object a) m , ReplicatedAsObject a , Replicated b ) => UUID -- ^ Field name -> m b readField field = do obj@Object{..} <- get liftEither $ do stateChunk <- getObjectStateChunk obj viewField field stateChunk objectFrame -- | Assign a value to a field assignField :: forall a b m . ( ReplicatedAsObject a , Replicated b , ReplicaClock m, MonadError String m, MonadState (Object a) m ) => UUID -- ^ Field name -> b -- ^ Value (from untyped world) -> m () assignField field value = do obj@Object{..} <- get StateChunk{..} <- liftEither $ getObjectStateChunk obj advanceToUuid stateVersion let chunk = filter ((field /=) . opRef) stateBody e <- getEventUuid (p, frame') <- runWriterT $ newRon value let newOp = Op e field p let chunk' = sortOn opRef $ newOp : chunk let state' = StateChunk e chunk' put Object { objectFrame = Map.insert (objectOpType @a, objectId) state' objectFrame <> frame' , .. } -- | Anti-lens to an object inside a specified field zoomField :: (ReplicatedAsObject outer, MonadError String m) => UUID -- ^ Field name -> StateT (Object inner) m a -- ^ Nested object modifier -> StateT (Object outer) m a zoomField field innerModifier = do obj@Object{..} <- get StateChunk{..} <- liftEither $ getObjectStateChunk obj let ops = filter ((field ==) . opRef) stateBody Op{..} <- case ops of [] -> throwError $ unwords ["no field", show field, "in lww chunk"] [op] -> pure op _ -> throwError "unreduced state" innerObjectId <- case opPayload of [AUuid oid] -> pure oid _ -> throwError "bad payload" let innerObject = Object innerObjectId objectFrame (a, Object{objectFrame = objectFrame'}) <- lift $ runStateT innerModifier innerObject put Object{objectFrame = objectFrame', ..} pure a