packages feed

eventium-core-0.2.1: src/Eventium/ProjectionCache/Cache.hs

{-# LANGUAGE RankNTypes #-}

module Eventium.ProjectionCache.Cache
  ( runProjectionCacheUsing,
    codecProjectionCache,
    getLatestVersionedProjectionWithCache,
    getLatestGlobalProjectionWithCache,
    updateVersionedProjectionCache,
    updateGlobalProjectionCache,
    snapshotEventHandler,
    snapshotGlobalEventHandler,
    module Eventium.ProjectionCache.Types,
  )
where

import Eventium.Codec
import Eventium.EventHandler
import Eventium.Projection
import Eventium.ProjectionCache.Types
import Eventium.Store.Class

-- | Changes the monad a 'ProjectionCache' runs in. This is useful to run the
-- cache in another 'Monad' while forgetting the original 'Monad'.
runProjectionCacheUsing ::
  (forall a. mstore a -> m a) ->
  ProjectionCache key position encoded mstore ->
  ProjectionCache key position encoded m
runProjectionCacheUsing runCache pc =
  ProjectionCache
    { storeSnapshot = \uuid version st -> runCache $ pc.storeSnapshot uuid version st,
      loadSnapshot = runCache . pc.loadSnapshot
    }

-- | Wraps a 'ProjectionCache' and transparently encodes/decodes events for
-- you. Note that in this implementation decoding errors when using
-- 'getEvents' are simply ignored (the event is not returned).
codecProjectionCache ::
  (Monad m) =>
  Codec state encoded ->
  ProjectionCache key position encoded m ->
  ProjectionCache key position state m
codecProjectionCache codec pc =
  ProjectionCache storeSnapshot' loadSnapshot'
  where
    storeSnapshot' uuid version = pc.storeSnapshot uuid version . codec.encode
    loadSnapshot' uuid = do
      mState <- pc.loadSnapshot uuid
      return $ mState >>= traverse codec.decode

-- | 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 proj =
  getLatestProjectionWithCache' cache proj proj.key >>= getLatestStreamProjection store

-- | Like 'getLatestGlobalProjection', but uses a 'ProjectionCache' if it
-- contains more recent state.
getLatestGlobalProjectionWithCache ::
  (Monad m) =>
  GlobalEventStoreReader m event ->
  GlobalProjectionCache state m ->
  GlobalStreamProjection state event ->
  m (GlobalStreamProjection state event)
getLatestGlobalProjectionWithCache store cache proj =
  getLatestProjectionWithCache' cache proj () >>= 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 proj k = do
  mLatestState <- cache.loadSnapshot k
  let mkProjection' (pos, st) =
        if pos > proj.position
          then
            proj
              { position = pos,
                state = st
              }
          else proj
  return $ maybe proj mkProjection' mLatestState

-- | Loads the latest projection state from the cache/store and stores this
-- value back into the projection cache.
updateVersionedProjectionCache ::
  (Monad m) =>
  VersionedEventStoreReader m event ->
  VersionedProjectionCache state m ->
  VersionedStreamProjection state event ->
  m ()
updateVersionedProjectionCache reader cache proj = do
  sp <- getLatestVersionedProjectionWithCache reader cache proj
  cache.storeSnapshot sp.key sp.position sp.state

-- | Analog of 'updateVersionedProjectionCache' for a 'GlobalProjectionCache'.
updateGlobalProjectionCache ::
  (Monad m) =>
  GlobalEventStoreReader m event ->
  GlobalProjectionCache state m ->
  GlobalStreamProjection state event ->
  m ()
updateGlobalProjectionCache reader cache proj = do
  sp <- getLatestGlobalProjectionWithCache reader cache proj
  cache.storeSnapshot () sp.position sp.state

-- | Creates an 'EventHandler' that updates a 'VersionedProjectionCache'
-- whenever a versioned stream event is received. Compose with
-- @synchronousPublisher@ or @publishingEventStoreWriter@ to keep the cache
-- up to date as events are written.
snapshotEventHandler ::
  (Monad m) =>
  VersionedEventStoreReader m event ->
  VersionedProjectionCache state m ->
  Projection state event ->
  EventHandler m (VersionedStreamEvent event)
snapshotEventHandler reader cache proj =
  EventHandler $ \streamEvent -> do
    let uuid = streamEvent.key
    updateVersionedProjectionCache reader cache (versionedStreamProjection uuid proj)

-- | Creates an 'EventHandler' that updates a 'GlobalProjectionCache'
-- whenever a global stream event is received. Compose with
-- @synchronousPublisher@ or @publishingEventStoreWriter@ to keep the cache
-- up to date as events are written.
snapshotGlobalEventHandler ::
  (Monad m) =>
  GlobalEventStoreReader m event ->
  GlobalProjectionCache state m ->
  Projection state (VersionedStreamEvent event) ->
  EventHandler m (GlobalStreamEvent event)
snapshotGlobalEventHandler reader cache proj =
  EventHandler $ \_ ->
    updateGlobalProjectionCache reader cache (globalStreamProjection proj)