content: {-# LANGUAGE RankNTypes #-}
  {-# LANGUAGE RecordWildCards #-}
  
  module Eventful.ProjectionCache.Types
    ( ProjectionCache (..)
    , VersionedProjectionCache
    , GlobalStreamProjectionCache
    , runProjectionCacheUsing
    , serializedProjectionCache
    , getLatestVersionedProjectionWithCache
    , getLatestGlobalProjectionWithCache
    , updateProjectionCache
    , updateGlobalProjectionCache
    ) where
  
  import Eventful.Projection
  import Eventful.Serializer
  import Eventful.Store.Class
  import Eventful.UUID
  
  -- | A 'ProjectionCache' caches snapshots of 'Projection's in event streams.
  -- This is useful if your event streams are very large. This cache operates on
  -- some 'Monad' @m@ and stores the 'Projection' state of type @serialized@.
  --
  -- At its core, this is essentially just a key-value store with knowledge of
  -- the stream 'UUID' and 'EventVersion'. It is recommended to use the other
  -- helper functions in this module to interpret the stored values using a
  -- 'Projection'.
  --
  -- The @key@ and @position@ type parameters are polymorphic so we can abstract
  -- over a cache for individual event streams, and a cache for globally ordered
  -- streams.
  data ProjectionCache key position serialized m
    = ProjectionCache
    { storeProjectionSnapshot :: key -> position -> serialized -> m ()
      -- ^ Stores the state for a projection at a given @key@ and @position@.
      -- This is pretty unsafe, because there is no guarantee what is stored is
      -- actually derived from the events in the stream. Consider using
      -- 'updateProjectionCache'.
    , loadProjectionSnapshot :: key -> m (Maybe (position, serialized))
      -- ^ Loads the latest projection state from the cache.
    }
  
  -- | Type synonym for a 'ProjectionCache' used on individual event streams.
  type VersionedProjectionCache serialized m = ProjectionCache UUID EventVersion serialized m
  
  -- | Type synonym for a 'ProjectionCache' that is used in conjunction with a
  -- 'GlobalStreamEventStore'.
  type GlobalStreamProjectionCache key serialized m = ProjectionCache key SequenceNumber serialized m
  
  -- | Changes the monad a 'ProjectionCache' runs in. This is useful to run the
  -- cache in another 'Monad' while forgetting the original 'Monad'.
  runProjectionCacheUsing
    :: (Monad m, Monad mstore)
    => (forall a. mstore a -> m a)
    -> ProjectionCache key position serialized mstore
    -> ProjectionCache key position serialized m
  runProjectionCacheUsing runCache ProjectionCache{..} =
    ProjectionCache
    { storeProjectionSnapshot = \uuid version state -> runCache $ storeProjectionSnapshot uuid version state
    , loadProjectionSnapshot = runCache . loadProjectionSnapshot
    }
  
  -- | Wraps a 'ProjectionCache' and transparently serializes/deserializes events for
  -- you. Note that in this implementation deserialization errors when using
  -- 'getEvents' are simply ignored (the event is not returned).
  serializedProjectionCache
    :: (Monad m)
    => Serializer state serialized
    -> ProjectionCache key position serialized m
    -> ProjectionCache key position state m
  serializedProjectionCache Serializer{..} ProjectionCache{..} =
    ProjectionCache storeProjectionSnapshot' loadProjectionSnapshot'
    where
      storeProjectionSnapshot' uuid version = storeProjectionSnapshot uuid version . serialize
      loadProjectionSnapshot' uuid = do
        mState <- loadProjectionSnapshot uuid
        return $ mState >>= traverse deserialize
  
  -- | Like 'getLatestVersionedProjection', but uses a 'ProjectionCache' if it contains
  -- more recent state.
  getLatestVersionedProjectionWithCache
    :: (Monad m)
    => VersionedEventStoreReader m event
    -> VersionedProjectionCache state m
    -> VersionedStreamProjection state event
    -> m (VersionedStreamProjection state event)
  getLatestVersionedProjectionWithCache store cache projection =
    getLatestProjectionWithCache' cache projection (streamProjectionKey projection) >>= getLatestStreamProjection store
  
  -- | Like 'getLatestGlobalProjection', but uses a 'ProjectionCache' if it
  -- contains more recent state.
  getLatestGlobalProjectionWithCache
    :: (Monad m)
    => GlobalEventStoreReader m event
    -> GlobalStreamProjectionCache key state m
    -> GlobalStreamProjection state event
    -> key
    -> m (GlobalStreamProjection state event)
  getLatestGlobalProjectionWithCache store cache projection key =
    getLatestProjectionWithCache' cache projection key >>= getLatestStreamProjection store
  
  getLatestProjectionWithCache'
    :: (Monad m, Ord position)
    => ProjectionCache key position state m
    -> StreamProjection projKey position state event
    -> key
    -> m (StreamProjection projKey position state event)
  getLatestProjectionWithCache' cache projection key = do
    mLatestState <- loadProjectionSnapshot cache key
    let
      mkProjection' (position, state) =
        if position > streamProjectionPosition projection
        then
          projection
          { streamProjectionPosition = position
          , streamProjectionState = state
          }
        else projection
    return $ maybe projection mkProjection' mLatestState
  
  -- | Loads the latest projection state from the cache/store and stores this
  -- value back into the projection cache.
  updateProjectionCache
    :: (Monad m)
    => VersionedEventStoreReader m event
    -> VersionedProjectionCache state m
    -> VersionedStreamProjection state event
    -> m ()
  updateProjectionCache reader cache projection = do
    StreamProjection{..} <- getLatestVersionedProjectionWithCache reader cache projection
    storeProjectionSnapshot cache streamProjectionKey streamProjectionPosition streamProjectionState
  
  -- | Analog of 'updateProjectionCache' for a 'GlobalStreamProjectionCache'.
  updateGlobalProjectionCache
    :: (Monad m)
    => GlobalEventStoreReader m event
    -> GlobalStreamProjectionCache key state m
    -> GlobalStreamProjection state event
    -> key
    -> m ()
  updateGlobalProjectionCache reader cache projection key = do
    StreamProjection{..} <- getLatestGlobalProjectionWithCache reader cache projection key
    storeProjectionSnapshot cache key streamProjectionPosition streamProjectionState
  