flaw-editor/Flaw/Editor/Entity.hs
{-|
Module: Flaw.Editor.Entity
Description: Entity level of Oil.
License: MIT
Entity is a special typeclass of objects which can be stored in Oil repo, edited in Oil editor,
and transmitted via Oil protocol.
Entity can be serialized into series of key-value records, and deserialized from them.
Each entity instance is identified by 'EntityId', and this entity id is a prefix for every
database record of entity. In other words, all records related to a given entity are key-prefixed
with entity id. Part of record's key after entity id is called /key suffix/ of record.
Every entity instance has a main record: that's a record with key equal to entity id
(in other words, with null key suffix). Existence of entity instance is determined by
correctness of main record. If there's no main record or its value is incorrect,
entity instance doesn't exist.
Value of valid main record always starts with fixed-length /entity type id/, which determines
Haskell type of entity. After entity type id there could be other data, depending on type.
Values of all entity's records constitute entity's data. The only exception is main record:
entity's data includes only part of the value after entity type id.
Entities can be accessed through means of an additional layer before low-level Oil protocols, namely 'EntityManager'.
'EntityManager' performs conversion between raw records and 'Entity' values and provides caching via 'EntityVar's.
'EntityVar' is a container for entity value. 'EntityManager' ensures that all alive 'EntityVar's contains up-to-date values
(using weak references to vars). 'EntityVar' also allows for changing a value of an entity's record,
and automatically transmits that change into repo.
Due to various implementation constraints, not every operation can be done in 'STM' monad. Specifically:
* Creation of new 'EntityVar' (which includes generating new 'EntityId') or getting an entity var for an existing entity
runs in 'IO' monad.
* Reading entity value from 'EntityVar' or writing record value runs in 'STM' monad.
-}
{-# LANGUAGE ConstraintKinds, DefaultSignatures, FlexibleContexts, FlexibleInstances, GADTs, GeneralizedNewtypeDeriving, PatternSynonyms, PolyKinds, TemplateHaskell, TypeFamilies, TypeOperators, UndecidableInstances, ViewPatterns #-}
{-# OPTIONS_GHC -fno-warn-missing-pattern-synonym-signatures #-}
module Flaw.Editor.Entity
(
-- * Ids
EntityId(..)
, pattern ENTITY_ID_SIZE
, EntityTypeId(..)
, pattern ENTITY_TYPE_ID_SIZE
-- * Entity pointers and vars
, SomeEntityPtr(..)
, EntityPtr(..)
, InterfacedEntityPtr(..)
, SomeEntityVar(..)
, EntityVar(..)
, entityVarEntityId
-- * Entity classes
, Entity(..)
, BasicEntity(..)
, processBasicEntityChange
, applyBasicEntityChange
-- * Entity interfaces
, EntityInterfaceId(..)
, EntityInterface(..)
, EntityInterfaced(..)
-- * Entity containers
, SomeEntity(..)
, SomeBasicEntity(..)
, SomeBasicOrdEntity(..)
, SomeInterfacedEntity(..)
, SomeEntityInterface(..)
-- * Null things
, NullEntity(..)
, nullEntityId
, nullEntityTypeId
, nullEntityInterfaceId
, nullInterfacedEntityPtr
-- * Deserializers
, getRootBaseEntity
, getRootBaseBasicEntity
, getRootBaseBasicOrdEntity
, deserializeEntityInterface
-- * Entity manager
, EntityManager(..)
, GetEntity
, newEntityManager
, unsafePullEntityManager
-- ** Registration
, registerEntityType
, registerBasicEntityType
, registerBasicOrdEntityType
, registerEntityInterface
-- * Entity operations
, getSomeEntityVar
, getEntityVar
, newEntityVar
, readEntityVar
, readSomeEntityVar
, readSomeEntityWithRevisionVar
, writeEntityVarRecord
, writeBasicEntityVar
-- * Entity history
, EntityHistoryChan(..)
, entityVarHistory
, readEntityHistoryChan
-- * Miscellaneous
, EntityException(..)
, GenericEntityChange
, GenericEntityDatatype(..)
, GenericEntityConstructor(..)
, GenericEntitySelector(..)
, GenericEntityValue(..)
-- * TH helpers
, hashTextToEntityId
, hashTextToEntityTypeId
, hashTextToEntityInterfaceId
, interfaceEntityExp
, EntityRegistration(..)
, registerEntitiesAndInterfacesExp
-- * Re-exports for convenience
, Revision
, Proxy(..)
) where
import Control.Concurrent.STM
import Control.Exception
import Control.Monad
import Control.Monad.Reader
import qualified Crypto.Random.EntropyPool as C
import qualified Data.ByteArray.Encoding as BA
import qualified Data.ByteString as B
import qualified Data.ByteString.Short as BS
import Data.Default
import Data.IORef
import qualified Data.Map.Strict as M
import qualified Data.Serialize as S
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import Data.Typeable
import Data.Word
import GHC.Exts(Constraint)
import qualified GHC.Generics as G
import Language.Haskell.TH
import System.Mem.Weak
import Flaw.Flow
import Flaw.Oil.ClientRepo
import Flaw.Editor.Entity.Internal
import Flaw.Oil.Repo
-- | Entity id.
newtype EntityId = EntityId BS.ShortByteString deriving (Eq, Ord)
instance Show EntityId where
show (EntityId entityIdBytes) = "EntityId \"" <> T.unpack (T.decodeUtf8 $ BA.convertToBase BA.Base64 $ BS.fromShort entityIdBytes) <> "\""
instance Default EntityId where
def = nullEntityId
-- | Entity id length in bytes.
pattern ENTITY_ID_SIZE = 20
-- | Null entity id.
{-# NOINLINE nullEntityId #-}
nullEntityId :: EntityId
nullEntityId = EntityId $ BS.toShort $ B.replicate ENTITY_ID_SIZE 0
instance S.Serialize EntityId where
put (EntityId bytes) = S.putShortByteString bytes
get = EntityId <$> S.getShortByteString ENTITY_ID_SIZE
-- | Entity type id.
newtype EntityTypeId = EntityTypeId BS.ShortByteString deriving (Eq, Ord, Semigroup, Monoid, Show)
-- | Entity type id length in bytes.
pattern ENTITY_TYPE_ID_SIZE = 20
-- | Null entity type id.
{-# NOINLINE nullEntityTypeId #-}
nullEntityTypeId :: EntityTypeId
nullEntityTypeId = EntityTypeId BS.empty
instance S.Serialize EntityTypeId where
put (EntityTypeId bytes) = S.putShortByteString bytes
get = EntityTypeId <$> S.getShortByteString ENTITY_ID_SIZE
-- | Entity "pointer" is a typed entity id.
-- Doesn't keep a reference to cached entity.
-- You can read or write entity by entity pointer in IO monad.
newtype EntityPtr (a :: *) = EntityPtr EntityId deriving (Eq, Ord, S.Serialize, Default, Show)
-- | Untyped entity var, stores cached entity.
-- Entity manager keeps updating the var until it's GC'ed.
data SomeEntityVar = SomeEntityVar
{ someEntityVarEntityManager :: !EntityManager
, someEntityVarEntityId :: !EntityId
, someEntityVarValueVar :: {-# UNPACK #-} !(TVar EntityValue)
}
-- | Typed entity var.
newtype EntityVar (a :: *) = EntityVar SomeEntityVar
-- | Get entity id from entity var.
entityVarEntityId :: EntityVar a -> EntityId
entityVarEntityId (EntityVar SomeEntityVar
{ someEntityVarEntityId = entityId
}) = entityId
-- | Entity value, stored in entity var.
data EntityValue = EntityValue
{ entityValueEntity :: !SomeEntity
, entityValueRevision :: {-# UNPACK #-} !Revision
, entityValueHistoryVar :: {-# UNPACK #-} !(TVar EntityHistory)
}
-- | This is basically own implementation for broadcast `TChan`.
data EntityHistory where
EntityHistoryEnd :: EntityHistory
EntityHistoryChange :: Entity a =>
{ entityHistoryRevision :: {-# UNPACK #-} !Revision
, entityHistoryEntity :: !a
, entityHistoryChange :: !(EntityChange a)
, entityHistoryNextVar :: {-# UNPACK #-} !(TVar EntityHistory)
} -> EntityHistory
EntityHistoryTypeChange :: Entity a =>
{ entityHistoryRevision :: {-# UNPACK #-} !Revision
, entityHistoryEntity :: !a
, entityHistoryNextVar :: {-# UNPACK #-} !(TVar EntityHistory)
} -> EntityHistory
writeEntityChange :: Entity a => TVar EntityValue -> Revision -> a -> EntityChange a -> STM ()
writeEntityChange valueVar revision entity change = do
nextHistoryVar <- newTVar EntityHistoryEnd
entityValue@EntityValue
{ entityValueHistoryVar = historyVar
} <- readTVar valueVar
writeTVar historyVar EntityHistoryChange
{ entityHistoryRevision = revision
, entityHistoryEntity = entity
, entityHistoryChange = change
, entityHistoryNextVar = nextHistoryVar
}
writeTVar valueVar entityValue
{ entityValueEntity = SomeEntity entity
, entityValueRevision = revision
, entityValueHistoryVar = nextHistoryVar
}
writeEntityTypeChange :: Entity a => TVar EntityValue -> Revision -> a -> STM ()
writeEntityTypeChange valueVar revision entity = do
nextHistoryVar <- newTVar EntityHistoryEnd
entityValue@EntityValue
{ entityValueHistoryVar = historyVar
} <- readTVar valueVar
writeTVar historyVar EntityHistoryTypeChange
{ entityHistoryRevision = revision
, entityHistoryEntity = entity
, entityHistoryNextVar = nextHistoryVar
}
writeTVar valueVar entityValue
{ entityValueEntity = SomeEntity entity
, entityValueRevision = revision
, entityValueHistoryVar = nextHistoryVar
}
-- | Untyped entity pointer.
newtype SomeEntityPtr a = SomeEntityPtr EntityId deriving S.Serialize
-- | Class of repo entity.
-- Entity should be able to deserialize from any data.
-- It must not throw exceptions. It's free to ignore invalid data
-- and/or provide some default values instead.
class Typeable a => Entity (a :: *) where
{-# MINIMAL getEntityTypeId #-}
-- | Type representing change to entity.
type EntityChange a :: *
type EntityChange a = GenericEntityChange a
-- | Return type id of entity.
-- Parameter is not used and can be 'undefined'.
getEntityTypeId :: a -> EntityTypeId
-- | Process change in entity's data.
-- Changes of one specific record (with fixed key) constitute a group.
-- Changes to different records are commutative.
-- Must not throw exceptions.
-- Invalid values must be processed as well as valid,
-- keeping all the laws above, possibly recording some error state into entity.
processEntityChange
:: a -- ^ Current entity value.
-> B.ByteString -- ^ Key suffix of changed record.
-> B.ByteString -- ^ New value of changed record.
-> Maybe (a, EntityChange a)
default processEntityChange :: (G.Generic a, GenericEntityDatatype (G.Rep a), EntityChange a ~ GenericEntityChange a) => a -> B.ByteString -> B.ByteString -> Maybe (a, EntityChange a)
processEntityChange oldEntity keySuffix newValue = do -- Maybe monad
(newEntity, change) <- processGenericEntityDatatypeChange (G.from oldEntity) keySuffix newValue
return (G.to newEntity, change)
-- | Apply change to get new entity and write changes to entity var.
applyEntityChange
:: EntityVar a -- ^ Entity var.
-> EntityChange a -- ^ Entity change.
-> STM ()
default applyEntityChange :: (G.Generic a, GenericEntityDatatype (G.Rep a), EntityChange a ~ GenericEntityChange a) => EntityVar a -> EntityChange a -> STM ()
applyEntityChange = applyGenericEntityDatatypeChange
-- | Return witness for entity that it supports specified entity interface.
interfaceEntity :: EntityInterface i => Proxy i -> a -> EntityInterfaced i a
interfaceEntity _ _ = EntityNotInterfaced
-- | Return some short human-friendly textual representation of entity.
entityToText :: a -> T.Text
default entityToText :: Show a => a -> T.Text
entityToText = T.pack . show
-- | Basic entity is an entity consisting of main record only.
class (Entity a, EntityChange a ~ a) => BasicEntity a where
-- | Serialize basic entity into main record's value.
serializeBasicEntity :: a -> B.ByteString
default serializeBasicEntity :: S.Serialize a => a -> B.ByteString
serializeBasicEntity = S.encode
-- | Deserialize basic entity from main record's value.
deserializeBasicEntity :: B.ByteString -> a
default deserializeBasicEntity :: (S.Serialize a, Default a) => B.ByteString -> a
deserializeBasicEntity bytes = case S.decode bytes of
Left _e -> def
Right r -> r
-- | Implementation of 'processEntityChange' for 'BasicEntity'es.
processBasicEntityChange :: BasicEntity a => a -> B.ByteString -> B.ByteString -> Maybe (a, a)
processBasicEntityChange _oldEntity changedKeySuffix newValue =
if B.null changedKeySuffix then
let newEntity = deserializeBasicEntity newValue in Just (newEntity, newEntity)
else Nothing
-- | Implementation of 'applyEntityChange' for 'BasicEntity'es.
applyBasicEntityChange :: BasicEntity a => EntityVar a -> a -> STM ()
applyBasicEntityChange = writeBasicEntityVar
-- | Container for any entity.
data SomeEntity where
SomeEntity :: Entity a => a -> SomeEntity
-- | Container for basic entity.
data SomeBasicEntity where
SomeBasicEntity :: BasicEntity a => a -> SomeBasicEntity
-- | Container for basic ordered entity.
data SomeBasicOrdEntity where
SomeBasicOrdEntity :: (BasicEntity a, Ord a) => a -> SomeBasicOrdEntity
-- | Null entity, used when no entity could be deserialized.
data NullEntity = NullEntity deriving (Typeable, Show)
instance Entity NullEntity where
getEntityTypeId _ = nullEntityTypeId
processEntityChange NullEntity _ _ = Nothing
applyEntityChange _ _ = return ()
instance Default NullEntity where
def = NullEntity
-- | Entity interface id.
newtype EntityInterfaceId = EntityInterfaceId BS.ShortByteString deriving (Eq, Ord)
instance Show EntityInterfaceId where
show (EntityInterfaceId entityInterfaceIdBytes) = "EntityInterfaceId \"" <> T.unpack (T.decodeUtf8 $ BA.convertToBase BA.Base64 $ BS.fromShort entityInterfaceIdBytes) <> "\""
instance Default EntityInterfaceId where
def = nullEntityInterfaceId
-- | Entity interface id length in bytes.
pattern ENTITY_INTERFACE_ID_SIZE = 20
-- | Null entity interface id.
{-# NOINLINE nullEntityInterfaceId #-}
nullEntityInterfaceId :: EntityInterfaceId
nullEntityInterfaceId = EntityInterfaceId $ BS.toShort $ B.replicate ENTITY_INTERFACE_ID_SIZE 0
instance S.Serialize EntityInterfaceId where
put (EntityInterfaceId bytes) = S.putShortByteString bytes
get = EntityInterfaceId <$> S.getShortByteString ENTITY_INTERFACE_ID_SIZE
-- | Class of entity interface.
-- Entity interface is itself a class.
class Typeable i => EntityInterface (i :: * -> Constraint) where
-- | Get id of entity interface.
getEntityInterfaceId :: p i -> EntityInterfaceId
-- | Witness for entity having an interface.
data EntityInterfaced i a where
EntityInterfaced :: (EntityInterface i, Entity a, i a) => EntityInterfaced i a
EntityNotInterfaced :: EntityInterfaced i a
-- | Some entity supporting specified interface.
data SomeInterfacedEntity i where
SomeInterfacedEntity :: (EntityInterface i, Entity a, i a) => a -> SomeInterfacedEntity i
-- | Entity pointer pointing at some entity which supports specified interface.
newtype InterfacedEntityPtr (i :: * -> Constraint) = InterfacedEntityPtr EntityId deriving (Eq, Ord, S.Serialize, Default, Show)
-- | Some entity interface.
data SomeEntityInterface where
SomeEntityInterface :: EntityInterface i => Proxy i -> SomeEntityInterface
nullInterfacedEntityPtr :: InterfacedEntityPtr a
nullInterfacedEntityPtr = InterfacedEntityPtr nullEntityId
-- | Entity manager based on client repo.
data EntityManager = EntityManager
{
-- | Every client repo operation must be performed in this flow,
-- even not performed by entity manager itself.
entityManagerFlow :: !Flow
, entityManagerClientRepo :: !ClientRepo
-- | Push action, signal that client repo has client-side changes.
-- Called by entity manager in the flow.
, entityManagerPushAction :: !(IO ())
-- | Entropy pool to generate new entity ids.
, entityManagerEntropyPool :: !C.EntropyPool
, entityManagerNextTagRef :: {-# UNPACK #-} !(IORef Int)
, entityManagerCacheRef :: {-# UNPACK #-} !(IORef (M.Map EntityId CachedEntity))
-- | Entity deserialization functions.
, entityManagerDeserializatorsRef :: {-# UNPACK #-} !(IORef (M.Map EntityTypeId (GetEntity SomeEntity)))
-- | Basic entity deserialization functions.
, entityManagerBasicDeserializatorsRef :: {-# UNPACK #-} !(IORef (M.Map EntityTypeId (GetEntity SomeBasicEntity)))
-- | Basic ordered entity deserialization functions.
, entityManagerBasicOrdDeserializatorsRef :: {-# UNPACK #-} !(IORef (M.Map EntityTypeId (GetEntity SomeBasicOrdEntity)))
-- | Entity interfaces.
, entityManagerEntityInterfacesRef :: {-# UNPACK #-} !(IORef (M.Map EntityInterfaceId (GetEntity SomeEntityInterface)))
-- | Dirty entities.
, entityManagerDirtyRecordsVar :: {-# UNPACK #-} !(TVar (M.Map B.ByteString B.ByteString))
-- | Is push scheduled?
, entityManagerPushScheduledVar :: {-# UNPACK #-} !(TVar Bool)
}
-- | Monad for deserializing entities.
type GetEntity = ReaderT GetEntityState S.Get
data GetEntityState = GetEntityState
{ getEntityStateGetter :: !(S.Get SomeEntity)
, getEntityStateBasicGetter :: !(S.Get SomeBasicEntity)
, getEntityStateBasicOrdGetter :: !(S.Get SomeBasicOrdEntity)
, getEntityStateInterfaceGetter :: !(S.Get SomeEntityInterface)
}
-- | Deserialize entity type and get base entity for this type.
getRootBaseEntity :: GetEntity SomeEntity
getRootBaseEntity = lift . getEntityStateGetter =<< ask
-- | Deserialize entity type and get basic base entity for this type.
getRootBaseBasicEntity :: GetEntity SomeBasicEntity
getRootBaseBasicEntity = lift . getEntityStateBasicGetter =<< ask
-- | Deserialize entity type and get basic ordered base entity for this type.
getRootBaseBasicOrdEntity :: GetEntity SomeBasicOrdEntity
getRootBaseBasicOrdEntity = lift . getEntityStateBasicOrdGetter =<< ask
-- | Deserialize entity interface.
deserializeEntityInterface :: GetEntity SomeEntityInterface
deserializeEntityInterface = lift . getEntityStateInterfaceGetter =<< ask
-- | Entity in cache.
data CachedEntity = CachedEntity
{ cachedEntityTag :: {-# UNPACK #-} !Int
, cachedEntityWeak :: {-# UNPACK #-} !(Weak (TVar EntityValue))
}
-- | Initialize entity manager.
newEntityManager
:: Flow -- ^ Flow to run operations in.
-> ClientRepo -- ^ Underlying client repo.
-> IO () -- ^ Push action.
-> IO EntityManager
newEntityManager flow clientRepo pushAction = do
entropyPool <- C.createEntropyPool
nextTagRef <- newIORef 0
cacheRef <- newIORef M.empty
deserializatorsRef <- newIORef M.empty
basicDeserializatorsRef <- newIORef M.empty
basicOrdDeserializatorsRef <- newIORef M.empty
entityInterfacesRef <- newIORef M.empty
dirtyRecordsVar <- newTVarIO M.empty
pushScheduledVar <- newTVarIO False
return EntityManager
{ entityManagerFlow = flow
, entityManagerClientRepo = clientRepo
, entityManagerPushAction = pushAction
, entityManagerEntropyPool = entropyPool
, entityManagerNextTagRef = nextTagRef
, entityManagerCacheRef = cacheRef
, entityManagerDeserializatorsRef = deserializatorsRef
, entityManagerBasicDeserializatorsRef = basicDeserializatorsRef
, entityManagerBasicOrdDeserializatorsRef = basicOrdDeserializatorsRef
, entityManagerEntityInterfacesRef = entityInterfacesRef
, entityManagerDirtyRecordsVar = dirtyRecordsVar
, entityManagerPushScheduledVar = pushScheduledVar
}
-- | Register entity type.
registerEntityType :: EntityManager -> EntityTypeId -> (GetEntity SomeEntity) -> IO ()
registerEntityType EntityManager
{ entityManagerDeserializatorsRef = deserializatorsRef
} entityTypeId = modifyIORef' deserializatorsRef . M.insert entityTypeId
-- | Register basic entity type.
registerBasicEntityType :: EntityManager -> EntityTypeId -> (GetEntity SomeBasicEntity) -> IO ()
registerBasicEntityType entityManager@EntityManager
{ entityManagerBasicDeserializatorsRef = basicDeserializatorsRef
} entityTypeId deserializator = do
modifyIORef' basicDeserializatorsRef $ M.insert entityTypeId deserializator
registerEntityType entityManager entityTypeId $ do
SomeBasicEntity entity <- deserializator
return $ SomeEntity entity
-- | Register basic ordered entity type.
registerBasicOrdEntityType :: EntityManager -> EntityTypeId -> (GetEntity SomeBasicOrdEntity) -> IO ()
registerBasicOrdEntityType entityManager@EntityManager
{ entityManagerBasicOrdDeserializatorsRef = basicOrdDeserializatorsRef
} entityTypeId deserializator = do
modifyIORef' basicOrdDeserializatorsRef $ M.insert entityTypeId deserializator
registerBasicEntityType entityManager entityTypeId $ do
SomeBasicOrdEntity entity <- deserializator
return $ SomeBasicEntity entity
-- | Register entity interface.
registerEntityInterface :: EntityManager -> EntityInterfaceId -> (GetEntity SomeEntityInterface) -> IO ()
registerEntityInterface EntityManager
{ entityManagerEntityInterfacesRef = entityInterfacesRef
} entityInterfaceId = modifyIORef' entityInterfacesRef . M.insert entityInterfaceId
internalGetEntityState :: EntityManager -> IO GetEntityState
internalGetEntityState EntityManager
{ entityManagerDeserializatorsRef = deserializatorsRef
, entityManagerBasicDeserializatorsRef = basicDeserializatorsRef
, entityManagerBasicOrdDeserializatorsRef = basicOrdDeserializatorsRef
, entityManagerEntityInterfacesRef = entityInterfacesRef
} = do
deserializators <- readIORef deserializatorsRef
basicDeserializators <- readIORef basicDeserializatorsRef
basicOrdDeserializators <- readIORef basicOrdDeserializatorsRef
entityInterfaces <- readIORef entityInterfacesRef
let
f :: M.Map EntityTypeId (GetEntity a) -> S.Get a
f ds = do
firstEntityTypeId <- S.get
case M.lookup firstEntityTypeId ds of
Just deserializator -> runReaderT deserializator s
Nothing -> fail "unknown entity type id"
s = GetEntityState
{ getEntityStateGetter = f deserializators
, getEntityStateBasicGetter = f basicDeserializators
, getEntityStateBasicOrdGetter = f basicOrdDeserializators
, getEntityStateInterfaceGetter = do
entityInterfaceId <- S.get
case M.lookup entityInterfaceId entityInterfaces of
Just deserializator -> runReaderT deserializator s
Nothing -> fail "unknown entity interface id"
}
return s
-- | Combine revisions of entity's records to get entity's revision.
combineRevisions :: Revision -> Revision -> Revision
combineRevisions a b = if a > 0 && b > 0 then max a b else 0
-- | Deserialize entity.
deserializeSomeEntity :: EntityManager -> EntityId -> IO (Revision, SomeEntity)
deserializeSomeEntity entityManager@EntityManager
{ entityManagerClientRepo = clientRepo
} (EntityId (BS.fromShort -> entityIdBytes)) = do
(mainRecordRevision, mainValue) <- clientRepoGetRevisionValue clientRepo entityIdBytes
getEntity <- getEntityStateGetter <$> internalGetEntityState entityManager
let
eitherReturnResult = flip S.runGet mainValue $ do
SomeEntity baseEntity <- getEntity
mainValueSuffix <- S.getBytes =<< S.remaining
return $ do
let
f (revision, entity) key = do
(recordRevision, value) <-
if key == entityIdBytes then return (mainRecordRevision, mainValueSuffix)
else clientRepoGetRevisionValue clientRepo key
return
( combineRevisions revision recordRevision
, maybe entity fst $ processEntityChange entity (B.drop (B.length entityIdBytes) key) value
)
(revision, entity) <- foldM f (mainRecordRevision, baseEntity) =<< clientRepoGetKeysPrefixed clientRepo entityIdBytes
return (revision, SomeEntity entity)
case eitherReturnResult of
Left _ -> return (0, SomeEntity NullEntity)
Right returnResult -> returnResult
-- | Provide entity manager with changes pulled from remote repo.
-- Must be called in entity manager's flow, in the same task as pulling changes into client repo.
unsafePullEntityManager :: EntityManager -> [(Revision, B.ByteString, B.ByteString)] -> IO ()
unsafePullEntityManager entityManager@EntityManager
{ entityManagerClientRepo = clientRepo
, entityManagerCacheRef = cacheRef
, entityManagerDirtyRecordsVar = dirtyRecordsVar
} changes = do
getEntity <- getEntityStateGetter <$> internalGetEntityState entityManager
forM_ (filter ((>= ENTITY_ID_SIZE) . B.length) $ map (\(_revision, key, _value) -> key) changes) $ \recordKey -> do
-- get entity id
let
(entityIdBytes, recordKeySuffix) = B.splitAt ENTITY_ID_SIZE recordKey
entityId = EntityId $ BS.toShort entityIdBytes
-- get cached entity
cache <- readIORef cacheRef
case M.lookup entityId cache of
Just CachedEntity
{ cachedEntityWeak = weak
} -> do
maybeEntityVar <- deRefWeak weak
case maybeEntityVar of
Just entityVar -> do
-- note that changes from pull info contain server value,
-- i.e. it doesn't include non-pushed-yet changes on client side
-- so we need to read real value from client repo
(recordRevision, recordValue) <- clientRepoGetRevisionValue clientRepo recordKey
join $ atomically $ do
-- update entity only if record is not dirty
dirtyRecords <- readTVar dirtyRecordsVar
if M.member recordKey dirtyRecords then return $ return ()
else do
entityValue@EntityValue
{ entityValueEntity = SomeEntity entity
} <- readTVar entityVar
-- if it's main record
if B.null recordKeySuffix then let
-- get new entity type id
getter = do
SomeEntity baseEntity <- getEntity
-- if entity type id hasn't changed, simply process change
if getEntityTypeId entity == getEntityTypeId baseEntity then do
newValueSuffix <- S.getBytes =<< S.remaining
return $ do
case processEntityChange entity B.empty newValueSuffix of
Just (newEntity, entityChange) -> writeEntityChange entityVar recordRevision newEntity entityChange
Nothing -> return ()
return $ return ()
else return $ return $ do
-- re-deserialize it completely
-- we have to do it via two STM transactions. between these transactions
-- the only thing which can happen is user will write something into entity var (not changing a type)
-- it will be useless anyway, and typed entity var will have to be re-typed at least
-- so hopefully it's ok to do two transactions
(newRevision, SomeEntity newEntity) <- deserializeSomeEntity entityManager entityId
atomically $ writeEntityTypeChange entityVar newRevision newEntity
in case S.runGet getter recordValue of
Right stm -> stm
Left _e -> do
writeTVar entityVar entityValue
{ entityValueEntity = SomeEntity NullEntity
}
return $ return ()
-- else it's non-main record
else do
-- simply process change
case processEntityChange entity recordKeySuffix recordValue of
Just (newEntity, entityChange) -> writeEntityChange entityVar recordRevision newEntity entityChange
Nothing -> return ()
return $ return ()
Nothing ->
-- expired cached entity, remove it
writeIORef cacheRef $ M.delete entityId cache
Nothing -> return ()
scheduleEntityManagerPush :: EntityManager -> STM ()
scheduleEntityManagerPush EntityManager
{ entityManagerFlow = flow
, entityManagerClientRepo = clientRepo
, entityManagerPushAction = pushAction
, entityManagerDirtyRecordsVar = dirtyRecordsVar
, entityManagerPushScheduledVar = pushScheduledVar
} = do
-- only one push must be scheduled at all times
pushScheduled <- readTVar pushScheduledVar
unless pushScheduled $ do
writeTVar pushScheduledVar True
asyncRunInFlow flow $ do
-- atomically get dirty records
dirtyRecords <- atomically $ do
-- get dirty entities and clear them
dirtyRecords <- readTVar dirtyRecordsVar
writeTVar dirtyRecordsVar $ M.empty
-- reset scheduled state in the same transaction
writeTVar pushScheduledVar False
return dirtyRecords
-- write dirty records
forM_ (M.toList dirtyRecords) $ \(key, value) -> clientRepoChange clientRepo key value
-- run push action
pushAction
cacheEntity :: EntityManager -> EntityId -> IO SomeEntityVar
cacheEntity entityManager@EntityManager
{ entityManagerFlow = flow
, entityManagerNextTagRef = nextTagRef
, entityManagerCacheRef = cacheRef
} entityId = do
-- get initial value of entity
(revision, entity) <- deserializeSomeEntity entityManager entityId
-- create new var
tag <- atomicModifyIORef' nextTagRef $ \nextVarId -> (nextVarId + 1, nextVarId)
historyVar <- newTVarIO EntityHistoryEnd
entityVar <- newTVarIO EntityValue
{ entityValueEntity = entity
, entityValueRevision = revision
, entityValueHistoryVar = historyVar
}
-- put it into cache
weak <- mkWeakTVar entityVar $ weakFinalizer tag
modifyIORef' cacheRef $ M.insert entityId CachedEntity
{ cachedEntityTag = tag
, cachedEntityWeak = weak
}
return SomeEntityVar
{ someEntityVarEntityManager = entityManager
, someEntityVarEntityId = entityId
, someEntityVarValueVar = entityVar
}
where
-- finalizer for weak references
weakFinalizer tag = atomically $ asyncRunInFlow flow $ do
cache <- readIORef cacheRef
case M.lookup entityId cache of
Just CachedEntity
{ cachedEntityTag = t
} -> when (tag == t) $ writeIORef cacheRef $ M.delete entityId cache
Nothing -> return ()
-- | Get untyped entity var for given entity id.
getSomeEntityVar :: EntityManager -> EntityId -> IO SomeEntityVar
getSomeEntityVar entityManager@EntityManager
{ entityManagerFlow = flow
, entityManagerCacheRef = cacheRef
} entityId = runInFlow flow $ do
cache <- readIORef cacheRef
-- function to create new cached entity var
let cacheExistingEntityVar = cacheEntity entityManager entityId
-- check if there's cached entity
case M.lookup entityId cache of
-- if there's a cached var
Just CachedEntity
{ cachedEntityWeak = weak
} -> do
-- check if it's alive
maybeEntityVar <- deRefWeak weak
case maybeEntityVar of
-- if it's still alive, return it
Just entityVar -> return SomeEntityVar
{ someEntityVarEntityManager = entityManager
, someEntityVarEntityId = entityId
, someEntityVarValueVar = entityVar
}
-- otherwise cached var has been garbage collected
Nothing -> cacheExistingEntityVar
-- otherwise there's no cached var
Nothing -> cacheExistingEntityVar
-- | Get typed entity var for given entity id.
getEntityVar :: EntityManager -> EntityId -> IO (EntityVar a)
getEntityVar entityManager entityId = EntityVar <$> getSomeEntityVar entityManager entityId
-- | Generate entity id and create new entity var.
-- Generated entity is "empty", i.e. contains NullEntity.
newEntityVar :: Entity a => EntityManager -> IO (EntityVar a)
newEntityVar entityManager@EntityManager
{ entityManagerFlow = flow
, entityManagerClientRepo = clientRepo
, entityManagerPushAction = pushAction
, entityManagerEntropyPool = entropyPool
} = runInFlow flow $ f undefined where
f :: Entity a => a -> IO (EntityVar a)
f u = do
-- generate entity id
entityIdBytes <- C.getEntropyFrom entropyPool ENTITY_ID_SIZE
-- write entity type id
let EntityTypeId entityTypeIdBytes = getEntityTypeId u
clientRepoChange clientRepo entityIdBytes $ BS.fromShort entityTypeIdBytes
pushAction
-- create cached entity var
EntityVar <$> cacheEntity entityManager (EntityId $ BS.toShort entityIdBytes)
-- | Read entity var type safely.
-- If entity var contains entity of wrong type, throws EntityWrongTypeException.
readEntityVar :: Entity a => EntityVar a -> STM a
readEntityVar (EntityVar someEntityVar) = do
SomeEntity entity <- readSomeEntityVar someEntityVar
case cast entity of
Just correctEntity -> return correctEntity
Nothing -> throwSTM EntityWrongTypeException
-- | Read untyped entity from var.
readSomeEntityVar :: SomeEntityVar -> STM SomeEntity
readSomeEntityVar SomeEntityVar
{ someEntityVarValueVar = valueVar
} = entityValueEntity <$> readTVar valueVar
-- | Read untyped entity and revision from var.
readSomeEntityWithRevisionVar :: SomeEntityVar -> STM (SomeEntity, Revision)
readSomeEntityWithRevisionVar SomeEntityVar
{ someEntityVarValueVar = valueVar
} = do
EntityValue
{ entityValueEntity = someEntity
, entityValueRevision = revision
} <- readTVar valueVar
return (someEntity, revision)
-- | Write record for entity var.
-- Current entity in the var must be of the correct type.
writeEntityVarRecord :: Entity a => EntityVar a -> B.ByteString -> B.ByteString -> STM ()
writeEntityVarRecord entityVar@(EntityVar SomeEntityVar
{ someEntityVarEntityManager = entityManager@EntityManager
{ entityManagerDirtyRecordsVar = dirtyRecordsVar
}
, someEntityVarEntityId = EntityId entityIdBytes
, someEntityVarValueVar = entityValueVar
}) recordKeySuffix recordNewValue = do
-- get entity
EntityValue
{ entityValueEntity = SomeEntity entity
} <- readTVar entityValueVar
-- check that current entity is the same as entity var underlying type
let
castEntityForVar :: (Typeable a, Typeable b) => EntityVar a -> b -> Maybe a
castEntityForVar _ = cast
case castEntityForVar entityVar entity of
-- if entity is of the same type, simply apply the change
Just entityOfVarType -> case processEntityChange entityOfVarType recordKeySuffix recordNewValue of
Just (newEntity, entityChange) -> do
-- write to var
writeEntityChange entityValueVar 0 newEntity entityChange
-- write record
let
newRecordValue =
if B.null recordKeySuffix then let
EntityTypeId entityTypeIdBytes = getEntityTypeId newEntity
in BS.fromShort entityTypeIdBytes <> recordNewValue
else recordNewValue
modifyTVar' dirtyRecordsVar $ M.insert (BS.fromShort entityIdBytes <> recordKeySuffix) newRecordValue
Nothing -> return ()
-- else entity is of another type
Nothing -> throwSTM EntityWrongTypeException
-- schedule push
scheduleEntityManagerPush entityManager
-- | Write basic entity into entity var.
-- Entity is replaced completely. Var may contain entity of wrong type prior to operation.
writeBasicEntityVar :: BasicEntity a => EntityVar a -> a -> STM ()
writeBasicEntityVar (EntityVar SomeEntityVar
{ someEntityVarEntityManager = entityManager@EntityManager
{ entityManagerDirtyRecordsVar = dirtyRecordsVar
}
, someEntityVarEntityId = EntityId entityIdBytes
, someEntityVarValueVar = entityValueVar
}) newEntity = do
-- modify var
writeEntityChange entityValueVar 0 newEntity newEntity
-- write record
let EntityTypeId entityTypeIdBytes = getEntityTypeId newEntity
modifyTVar' dirtyRecordsVar $ M.insert (BS.fromShort entityIdBytes) $ BS.fromShort entityTypeIdBytes <> serializeBasicEntity newEntity
-- schedule push
scheduleEntityManagerPush entityManager
-- | Entity history chan is a stream of changes to entity.
newtype EntityHistoryChan a = EntityHistoryChan (TVar (TVar EntityHistory))
-- | Get entity's history chan.
entityVarHistory :: Typeable a => EntityVar a -> STM (EntityHistoryChan a)
entityVarHistory (EntityVar SomeEntityVar
{ someEntityVarValueVar = valueVar
}) = do
EntityValue
{ entityValueEntity = SomeEntity entity
, entityValueHistoryVar = historyVar
} <- readTVar valueVar
-- check that entity is of correct type
let
f :: a -> STM (EntityHistoryChan a)
f _ = EntityHistoryChan <$> newTVar historyVar
case cast entity of
Just castedEntity -> f castedEntity
Nothing -> throwSTM EntityWrongTypeException
-- | Get history event.
-- Retries if no history event available.
-- Throws `EntityWrongTypeException` on type change events,
-- or if entity is of wrong type.
readEntityHistoryChan :: Typeable a => EntityHistoryChan a -> STM (a, EntityChange a)
readEntityHistoryChan (EntityHistoryChan chanVar) = do
historyVar <- readTVar chanVar
history <- readTVar historyVar
case history of
EntityHistoryEnd -> retry
EntityHistoryChange
{ entityHistoryEntity = entity
, entityHistoryChange = change
, entityHistoryNextVar = nextHistoryVar
} -> let
f :: Maybe (a :~: b) -> a -> EntityChange a -> STM (b, EntityChange b)
f q e ec = case q of
Just Refl -> do
writeTVar chanVar nextHistoryVar
return (e, ec)
Nothing -> throwSTM EntityWrongTypeException
in f eqT entity change
EntityHistoryTypeChange {} -> throwSTM EntityWrongTypeException
data EntityException
= EntityWrongTypeException
deriving (Eq, Show)
instance Exception EntityException
type GenericEntityChange a = GenericEntityDatatypeChange (G.Rep a)
-- | Type of index of a field.
-- We support up to 256 fields.
type FieldIndex = Word8
-- | Serialize field index.
encodeFieldIndex :: FieldIndex -> B.ByteString
encodeFieldIndex = B.singleton
-- | Deserialize field index.
decodeFieldIndex :: B.ByteString -> Maybe FieldIndex
decodeFieldIndex bytes = if B.length bytes == 1 then Just $ B.head bytes else Nothing
class GenericEntityDatatype f where
type GenericEntityDatatypeChange f :: *
processGenericEntityDatatypeChange :: f p -> B.ByteString -> B.ByteString -> Maybe (f p, GenericEntityDatatypeChange f)
applyGenericEntityDatatypeChange :: (Entity a, G.Generic a, G.Rep a ~ f, EntityChange a ~ GenericEntityDatatypeChange f) => EntityVar a -> EntityChange a -> STM ()
class GenericEntityConstructor f where
type GenericEntityConstructorChange f :: *
processGenericEntityConstructorChange :: f p -> FieldIndex -> B.ByteString -> Maybe (f p, GenericEntityConstructorChange f)
applyGenericEntityConstructorChange :: f p -> GenericEntityConstructorChange f -> (B.ByteString, B.ByteString)
class GenericEntitySelector f where
type GenericEntitySelectorChange f :: *
processGenericEntitySelectorChange :: f p -> FieldIndex -> B.ByteString -> Maybe (f p, GenericEntitySelectorChange f)
applyGenericEntitySelectorChange :: f p -> FieldIndex -> GenericEntitySelectorChange f -> (B.ByteString, B.ByteString)
genericEntitySelectorFieldsCount :: f p -> Word8
class GenericEntityValue f where
type GenericEntityValueChange f :: *
processGenericEntityValueChange :: f p -> B.ByteString -> Maybe (f p, GenericEntityValueChange f)
applyGenericEntityValueChange :: f p -> GenericEntityValueChange f -> B.ByteString
instance GenericEntityConstructor f => GenericEntityDatatype (G.M1 G.D c f) where
type GenericEntityDatatypeChange (G.M1 G.D c f) = GenericEntityConstructorChange f
processGenericEntityDatatypeChange oldEntity keySuffix newValue = do -- Maybe monad
fieldIndex <- decodeFieldIndex keySuffix
(newEntity, change) <- processGenericEntityConstructorChange (G.unM1 oldEntity) fieldIndex newValue
return (G.M1 newEntity, change)
applyGenericEntityDatatypeChange var change = do
entity <- readEntityVar var
let (keySuffix, newValue) = applyGenericEntityConstructorChange (G.unM1 $ G.from entity) change
writeEntityVarRecord var keySuffix newValue
{-# INLINEABLE processGenericEntityDatatypeChange #-}
{-# INLINEABLE applyGenericEntityDatatypeChange #-}
instance GenericEntitySelector f => GenericEntityConstructor (G.M1 G.C c f) where
type GenericEntityConstructorChange (G.M1 G.C c f) = GenericEntitySelectorChange f
processGenericEntityConstructorChange oldEntity fieldIndex newValue = do -- Maybe monad
(newEntity, change) <- processGenericEntitySelectorChange (G.unM1 oldEntity) fieldIndex newValue
return (G.M1 newEntity, change)
applyGenericEntityConstructorChange oldEntity = applyGenericEntitySelectorChange (G.unM1 oldEntity) 0
{-# INLINEABLE processGenericEntityConstructorChange #-}
{-# INLINEABLE applyGenericEntityConstructorChange #-}
instance GenericEntityValue f => GenericEntitySelector (G.M1 G.S c f) where
type GenericEntitySelectorChange (G.M1 G.S c f) = GenericEntityValueChange f
-- field index must be zero here
processGenericEntitySelectorChange oldEntity 0 newValue = do -- Maybe monad
(newEntity, change) <- processGenericEntityValueChange (G.unM1 oldEntity) newValue
return (G.M1 newEntity, change)
processGenericEntitySelectorChange _oldEntity _fieldIndex _newValue = Nothing
genericEntitySelectorFieldsCount _ = 1
applyGenericEntitySelectorChange oldEntity fieldIndex change = (encodeFieldIndex fieldIndex, applyGenericEntityValueChange (G.unM1 oldEntity) change)
{-# INLINEABLE processGenericEntitySelectorChange #-}
{-# INLINEABLE genericEntitySelectorFieldsCount #-}
{-# INLINEABLE applyGenericEntitySelectorChange #-}
instance (GenericEntitySelector a, GenericEntitySelector b) => GenericEntitySelector (a G.:*: b) where
type GenericEntitySelectorChange (a G.:*: b) = Either (GenericEntitySelectorChange a) (GenericEntitySelectorChange b)
processGenericEntitySelectorChange (a G.:*: b) fieldIndex newValue = do -- Maybe monad
let aFieldCount = genericEntitySelectorFieldsCount a
if fieldIndex < aFieldCount then do
(newEntity, change) <- processGenericEntitySelectorChange a fieldIndex newValue
return (newEntity G.:*: b, Left change)
else do
(newEntity, change) <- processGenericEntitySelectorChange b (fieldIndex - aFieldCount) newValue
return (a G.:*: newEntity, Right change)
genericEntitySelectorFieldsCount (a G.:*: b) = genericEntitySelectorFieldsCount a + genericEntitySelectorFieldsCount b
applyGenericEntitySelectorChange (a G.:*: b) fieldIndex change = case change of
Left l -> applyGenericEntitySelectorChange a fieldIndex l
Right r -> applyGenericEntitySelectorChange b (fieldIndex + genericEntitySelectorFieldsCount a) r
{-# INLINEABLE processGenericEntitySelectorChange #-}
{-# INLINEABLE genericEntitySelectorFieldsCount #-}
{-# INLINEABLE applyGenericEntitySelectorChange #-}
-- value
instance BasicEntity a => GenericEntityValue (G.K1 G.R a) where
type GenericEntityValueChange (G.K1 G.R a) = EntityChange a
processGenericEntityValueChange _oldEntity newValue = Just (G.K1 newEntity, newEntity) where
newEntity = deserializeBasicEntity newValue
applyGenericEntityValueChange _oldEntity = serializeBasicEntity
{-# INLINEABLE processGenericEntityValueChange #-}
{-# INLINEABLE applyGenericEntityValueChange #-}
-- | Handy function to generate compile-time entity id out of text.
hashTextToEntityId :: T.Text -> Q Exp
hashTextToEntityId = hashTextDecl "entityIdHash_" [t| EntityId |] $ \e -> [| EntityId (BS.toShort $e) |]
-- | Handy function to generate compile-time entity type id out of text.
hashTextToEntityTypeId :: T.Text -> Q Exp
hashTextToEntityTypeId = hashTextDecl "entityTypeIdHash_" [t| EntityTypeId |] $ \e -> [| EntityTypeId (BS.toShort $e) |]
-- | Handy function to generate compile-time entity interface id out of text.
hashTextToEntityInterfaceId :: T.Text -> Q Exp
hashTextToEntityInterfaceId = hashTextDecl "entityInterfaceIdHash_" [t| EntityInterfaceId |] $ \e -> [| EntityInterfaceId (BS.toShort $e) |]
-- | Helper method for comparing interfaces' types.
-- Helps solving problems with kinds.
{-# INLINE eqEntityInterfaces #-}
eqEntityInterfaces :: (EntityInterface a, EntityInterface b) => Proxy a -> Proxy b -> Maybe ((Proxy a) :~: (Proxy b))
eqEntityInterfaces _ _ = eqT
-- | Handy 'interfaceEntity' implementation generator.
interfaceEntityExp :: [Name] -> ExpQ
interfaceEntityExp interfaceNames = do
p <- newName "p"
let
f n q = caseE [| eqEntityInterfaces $(varE p) (Proxy :: Proxy $(conT n)) |]
[ match [p| Just Refl |] (normalB [| EntityInterfaced |]) []
, match [p| Nothing |] (normalB q) []
]
lamE [varP p, wildP] $ foldr f (conE 'EntityNotInterfaced) interfaceNames
-- | Typeclass for registration of complex entities.
class EntityRegistration (a :: k) where
performEntityRegistration :: EntityManager -> Proxy a -> IO ()
-- | Register all simple instances of 'Entity' and 'EntityInterface',
-- perform registration from any 'EntityRegistration' instances in scope.
-- Type is :: EntityManager -> IO ()
-- Beware of TH declaration groups!
registerEntitiesAndInterfacesExp :: ExpQ
registerEntitiesAndInterfacesExp = do
em <- newName "entityManager"
-- simple Entity instances
entityInstancesStmts <- do
ClassI _ decs <- reify ''Entity
(concat <$>) . forM decs $ \(InstanceD Nothing context (AppT _ t) _) ->
if null context then do
supportDefault <- isInstance ''Default [t]
if supportDefault then do
supportBasicEntity <- isInstance ''BasicEntity [t]
if supportBasicEntity then do
supportOrd <- isInstance ''Ord [t]
if supportOrd then return [noBindS [| let a = def :: $(return t) in registerBasicOrdEntityType $(varE em) (getEntityTypeId a) $ return $ SomeBasicOrdEntity a |] ]
else return [noBindS [| let a = def :: $(return t) in registerBasicEntityType $(varE em) (getEntityTypeId a) $ return $ SomeBasicEntity a |] ]
else return [noBindS [| let a = def :: $(return t) in registerEntityType $(varE em) (getEntityTypeId a) $ return $ SomeEntity a |] ]
else do
reportWarning $ shows t " doesn't have Default instance. Cannot automatically register its Entity."
return []
else return []
-- EntityInterface instances
entityInterfaceInstancesStmts <- do
ClassI _ decs <- reify ''EntityInterface
(concat <$>) . forM decs $ \(InstanceD Nothing context (AppT _ t) _) ->
if null context then return [noBindS [| registerEntityInterface $(varE em) (getEntityInterfaceId (Proxy :: Proxy $(return t))) $ return $ SomeEntityInterface (Proxy :: Proxy $(return t)) |] ]
else return []
-- EntityRegistration instances
entityRegistrationInstancesStmts <- do
ClassI _ decs <- reify ''EntityRegistration
(concat <$>) . forM decs $ \(InstanceD Nothing context (AppT _ t) _) ->
if null context then return [noBindS [| performEntityRegistration $(varE em) (Proxy :: Proxy $(return t)) |] ]
else do
reportError $ shows t " has non-empty context for EntityRegistration instance"
return []
lamE [varP em] $ doE $ entityInstancesStmts ++ entityInterfaceInstancesStmts ++ entityRegistrationInstancesStmts