eventful-core 0.1.2 → 0.1.3
raw patch · 9 files changed
+372/−75 lines, 9 filesdep +contravariantPVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: contravariant
API changes (from Hackage documentation)
- Eventful.Serializer: serializedEventStore :: (Monad m) => Serializer event serialized -> EventStore serialized m -> EventStore event m
- Eventful.Serializer: serializedGloballyOrderedEventStore :: (Monad m) => Serializer event serialized -> GloballyOrderedEventStore serialized m -> GloballyOrderedEventStore event m
+ Eventful.Projection: GloballyOrderedProjection :: !(Projection state (GloballyOrderedEvent serialized)) -> !SequenceNumber -> !state -> GloballyOrderedProjection state serialized
+ Eventful.Projection: StreamProjection :: Projection state event -> !UUID -> EventVersion -> !state -> StreamProjection state event
+ Eventful.Projection: [globallyOrderedProjectionProjection] :: GloballyOrderedProjection state serialized -> !(Projection state (GloballyOrderedEvent serialized))
+ Eventful.Projection: [globallyOrderedProjectionSequenceNumber] :: GloballyOrderedProjection state serialized -> !SequenceNumber
+ Eventful.Projection: [globallyOrderedProjectionState] :: GloballyOrderedProjection state serialized -> !state
+ Eventful.Projection: [streamProjectionProjection] :: StreamProjection state event -> Projection state event
+ Eventful.Projection: [streamProjectionState] :: StreamProjection state event -> !state
+ Eventful.Projection: [streamProjectionUuid] :: StreamProjection state event -> !UUID
+ Eventful.Projection: [streamProjectionVersion] :: StreamProjection state event -> EventVersion
+ Eventful.Projection: data GloballyOrderedProjection state serialized
+ Eventful.Projection: data StreamProjection state event
+ Eventful.Projection: globallyOrderedProjection :: Projection state (GloballyOrderedEvent serialized) -> GloballyOrderedProjection state serialized
+ Eventful.Projection: globallyOrderedProjectionEventHandler :: GloballyOrderedProjection state serialized -> GloballyOrderedEvent serialized -> GloballyOrderedProjection state serialized
+ Eventful.Projection: instance Data.Functor.Contravariant.Contravariant (Eventful.Projection.Projection state)
+ Eventful.Projection: projectionMapMaybe :: (eventB -> Maybe eventA) -> Projection state eventA -> Projection state eventB
+ Eventful.Projection: streamProjection :: Projection state event -> UUID -> StreamProjection state event
+ Eventful.ProjectionCache.Types: ProjectionCache :: (key -> orderKey -> serialized -> m ()) -> (key -> m (Maybe (orderKey, serialized))) -> ProjectionCache key orderKey serialized m
+ Eventful.ProjectionCache.Types: [loadProjectionSnapshot] :: ProjectionCache key orderKey serialized m -> key -> m (Maybe (orderKey, serialized))
+ Eventful.ProjectionCache.Types: [storeProjectionSnapshot] :: ProjectionCache key orderKey serialized m -> key -> orderKey -> serialized -> m ()
+ Eventful.ProjectionCache.Types: data ProjectionCache key orderKey serialized m
+ Eventful.ProjectionCache.Types: getLatestGlobalProjectionWithCache :: (Monad m) => GloballyOrderedEventStore event m -> GloballyOrderedProjectionCache key state m -> GloballyOrderedProjection state event -> key -> m (GloballyOrderedProjection state event)
+ Eventful.ProjectionCache.Types: getLatestProjectionWithCache :: (Monad m) => EventStore event m -> StreamProjectionCache state m -> StreamProjection state event -> m (StreamProjection state event)
+ Eventful.ProjectionCache.Types: runProjectionCacheUsing :: (Monad m, Monad mstore) => (forall a. mstore a -> m a) -> ProjectionCache key orderKey serialized mstore -> ProjectionCache key orderKey serialized m
+ Eventful.ProjectionCache.Types: serializedProjectionCache :: (Monad m) => Serializer state serialized -> ProjectionCache key orderKey serialized m -> ProjectionCache key orderKey state m
+ Eventful.ProjectionCache.Types: type GloballyOrderedProjectionCache key serialized m = ProjectionCache key SequenceNumber serialized m
+ Eventful.ProjectionCache.Types: type StreamProjectionCache serialized m = ProjectionCache UUID EventVersion serialized m
+ Eventful.ProjectionCache.Types: updateGlobalProjectionCache :: (Monad m) => GloballyOrderedEventStore event m -> GloballyOrderedProjectionCache key state m -> GloballyOrderedProjection state event -> key -> m ()
+ Eventful.ProjectionCache.Types: updateProjectionCache :: (Monad m) => EventStore event m -> StreamProjectionCache state m -> StreamProjection state event -> m ()
+ Eventful.Store.Class: runEventStoreUsing :: (Monad m, Monad mstore) => (forall a. mstore a -> m a) -> EventStore serialized mstore -> EventStore serialized m
+ Eventful.Store.Class: runGloballyOrderedEventStoreUsing :: (Monad m, Monad mstore) => (forall a. mstore a -> m a) -> GloballyOrderedEventStore serialized mstore -> GloballyOrderedEventStore serialized m
+ Eventful.Store.Class: serializedEventStore :: (Monad m) => Serializer event serialized -> EventStore serialized m -> EventStore event m
+ Eventful.Store.Class: serializedGloballyOrderedEventStore :: (Monad m) => Serializer event serialized -> GloballyOrderedEventStore serialized m -> GloballyOrderedEventStore event m
+ Eventful.Store.Queries: EventStoreQueryRange :: EventStoreQueryStart a -> EventStoreQueryLimit a -> EventStoreQueryRange a
+ Eventful.Store.Queries: MaxNumberOfEvents :: Int -> EventStoreQueryLimit a
+ Eventful.Store.Queries: NoQueryLimit :: EventStoreQueryLimit a
+ Eventful.Store.Queries: StartFromBeginning :: EventStoreQueryStart a
+ Eventful.Store.Queries: StartQueryAt :: a -> EventStoreQueryStart a
+ Eventful.Store.Queries: StopQueryAt :: a -> EventStoreQueryLimit a
+ Eventful.Store.Queries: [eventStoreQueryRangeLimit] :: EventStoreQueryRange a -> EventStoreQueryLimit a
+ Eventful.Store.Queries: [eventStoreQueryRangeStart] :: EventStoreQueryRange a -> EventStoreQueryStart a
+ Eventful.Store.Queries: allEvents :: EventStoreQueryRange a
+ Eventful.Store.Queries: data EventStoreQueryLimit a
+ Eventful.Store.Queries: data EventStoreQueryRange a
+ Eventful.Store.Queries: data EventStoreQueryStart a
+ Eventful.Store.Queries: eventsStartingAt :: a -> EventStoreQueryRange a
+ Eventful.Store.Queries: eventsStartingAtTakeLimit :: a -> Int -> EventStoreQueryRange a
+ Eventful.Store.Queries: eventsStartingAtUntil :: a -> a -> EventStoreQueryRange a
+ Eventful.Store.Queries: eventsUntil :: a -> EventStoreQueryRange a
+ Eventful.Store.Queries: instance GHC.Base.Functor Eventful.Store.Queries.EventStoreQueryLimit
+ Eventful.Store.Queries: instance GHC.Base.Functor Eventful.Store.Queries.EventStoreQueryRange
+ Eventful.Store.Queries: instance GHC.Base.Functor Eventful.Store.Queries.EventStoreQueryStart
+ Eventful.Store.Queries: instance GHC.Classes.Eq a => GHC.Classes.Eq (Eventful.Store.Queries.EventStoreQueryLimit a)
+ Eventful.Store.Queries: instance GHC.Classes.Eq a => GHC.Classes.Eq (Eventful.Store.Queries.EventStoreQueryStart a)
+ Eventful.Store.Queries: instance GHC.Show.Show a => GHC.Show.Show (Eventful.Store.Queries.EventStoreQueryLimit a)
+ Eventful.Store.Queries: instance GHC.Show.Show a => GHC.Show.Show (Eventful.Store.Queries.EventStoreQueryStart a)
- Eventful.Projection: getLatestGlobalProjection :: (Monad m) => GloballyOrderedEventStore serialized m -> Projection proj (ProjectionEvent serialized) -> Maybe (proj, SequenceNumber) -> m (proj, SequenceNumber)
+ Eventful.Projection: getLatestGlobalProjection :: (Monad m) => GloballyOrderedEventStore serialized m -> GloballyOrderedProjection state serialized -> m (GloballyOrderedProjection state serialized)
- Eventful.Projection: getLatestProjection :: (Monad m) => EventStore serialized m -> Projection proj serialized -> UUID -> m (proj, EventVersion)
+ Eventful.Projection: getLatestProjection :: (Monad m) => EventStore event m -> StreamProjection state event -> m (StreamProjection state event)
- Eventful.Store.Class: EventStore :: (UUID -> m EventVersion) -> (UUID -> Maybe EventVersion -> m [StoredEvent serialized]) -> (ExpectedVersion -> UUID -> [serialized] -> m (Maybe EventWriteError)) -> EventStore serialized m
+ Eventful.Store.Class: EventStore :: (UUID -> m EventVersion) -> (UUID -> EventStoreQueryRange EventVersion -> m [StoredEvent serialized]) -> (ExpectedVersion -> UUID -> [serialized] -> m (Maybe EventWriteError)) -> EventStore serialized m
- Eventful.Store.Class: GloballyOrderedEventStore :: (SequenceNumber -> m [GloballyOrderedEvent serialized]) -> GloballyOrderedEventStore serialized m
+ Eventful.Store.Class: GloballyOrderedEventStore :: (EventStoreQueryRange SequenceNumber -> m [GloballyOrderedEvent serialized]) -> GloballyOrderedEventStore serialized m
- Eventful.Store.Class: [getEvents] :: EventStore serialized m -> UUID -> Maybe EventVersion -> m [StoredEvent serialized]
+ Eventful.Store.Class: [getEvents] :: EventStore serialized m -> UUID -> EventStoreQueryRange EventVersion -> m [StoredEvent serialized]
- Eventful.Store.Class: [getSequencedEvents] :: GloballyOrderedEventStore serialized m -> SequenceNumber -> m [GloballyOrderedEvent serialized]
+ Eventful.Store.Class: [getSequencedEvents] :: GloballyOrderedEventStore serialized m -> EventStoreQueryRange SequenceNumber -> m [GloballyOrderedEvent serialized]
Files
- eventful-core.cabal +8/−1
- src/Eventful.hs +1/−0
- src/Eventful/Aggregate.hs +5/−3
- src/Eventful/Projection.hs +95/−33
- src/Eventful/ProjectionCache/Types.hs +147/−0
- src/Eventful/ReadModel/Class.hs +1/−1
- src/Eventful/Serializer.hs +1/−35
- src/Eventful/Store/Class.hs +66/−2
- src/Eventful/Store/Queries.hs +48/−0
eventful-core.cabal view
@@ -3,7 +3,7 @@ -- see: https://github.com/sol/hpack name: eventful-core-version: 0.1.2+version: 0.1.3 synopsis: Core module for eventful description: Core module for eventful category: Database,Eventsourcing@@ -32,6 +32,7 @@ base >= 4.9 && < 5 , aeson , containers+ , contravariant , http-api-data , path-pieces , sum-type-boilerplate@@ -45,9 +46,11 @@ Eventful.EventBus Eventful.ProcessManager Eventful.Projection+ Eventful.ProjectionCache.Types Eventful.ReadModel.Class Eventful.Serializer Eventful.Store.Class+ Eventful.Store.Queries Eventful.TH Eventful.TH.Projection Eventful.TH.SumTypeSerializer@@ -65,6 +68,7 @@ base >= 4.9 && < 5 , aeson , containers+ , contravariant , http-api-data , path-pieces , sum-type-boilerplate@@ -82,9 +86,11 @@ Eventful.EventBus Eventful.ProcessManager Eventful.Projection+ Eventful.ProjectionCache.Types Eventful.ReadModel.Class Eventful.Serializer Eventful.Store.Class+ Eventful.Store.Queries Eventful.TH Eventful.TH.Projection Eventful.TH.SumTypeSerializer@@ -101,6 +107,7 @@ base >= 4.9 && < 5 , aeson , containers+ , contravariant , http-api-data , path-pieces , sum-type-boilerplate
src/Eventful.hs view
@@ -5,6 +5,7 @@ import Eventful.Aggregate as X import Eventful.EventBus as X import Eventful.Projection as X+import Eventful.ProjectionCache.Types as X import Eventful.ProcessManager as X import Eventful.ReadModel.Class as X import Eventful.Serializer as X
src/Eventful/Aggregate.hs view
@@ -1,3 +1,5 @@+{-# LANGUAGE RecordWildCards #-}+ -- | Defines an Aggregate type-class from DDD parlance. module Eventful.Aggregate@@ -48,9 +50,9 @@ -> command -> m [serialized] commandStoredAggregate store (Aggregate handler proj) uuid command = do- (latest, vers) <- getLatestProjection store proj uuid- let events = handler latest command- mError <- storeEvents store (ExactVersion vers) uuid events+ StreamProjection{..} <- getLatestProjection store (streamProjection proj uuid)+ let events = handler streamProjectionState command+ mError <- storeEvents store (ExactVersion streamProjectionVersion) uuid events case mError of (Just err) -> error $ "TODO: Create aggregate restart logic. " ++ show err Nothing -> return events
src/Eventful/Projection.hs view
@@ -4,15 +4,21 @@ ( Projection (..) , latestProjection , allProjections+ , StreamProjection (..)+ , streamProjection , getLatestProjection+ , GloballyOrderedProjection (..)+ , globallyOrderedProjection+ , globallyOrderedProjectionEventHandler , getLatestGlobalProjection , serializedProjection+ , projectionMapMaybe ) where import Data.Foldable (foldl')+import Data.Functor.Contravariant import Data.List (scanl')-import Data.Maybe (fromMaybe) import Eventful.Serializer import Eventful.Store.Class@@ -33,6 +39,11 @@ -- new state. } +instance Contravariant (Projection state) where+ contramap f (Projection seed handler) = Projection seed handler'+ where+ handler' state event = handler state (f event)+ -- | Computes the latest state of a 'Projection' from some events. latestProjection :: (Foldable t) => Projection state event -> t event -> state latestProjection (Projection seed handler) = foldl' handler seed@@ -44,46 +55,92 @@ allProjections :: Projection state event -> [event] -> [state] allProjections (Projection seed handler) = scanl' handler seed +-- | A 'StreamProjection' is a 'Projection' that has been constructed from+-- events from a particular event stream. This is mostly useful so we can+-- associate an 'EventVersion' with some state.+data StreamProjection state event+ = StreamProjection+ { streamProjectionProjection :: Projection state event+ , streamProjectionUuid :: !UUID+ , streamProjectionVersion :: EventVersion+ , streamProjectionState :: !state+ }++-- | Initialize a 'StreamProjection' with a 'Projection'.+streamProjection+ :: Projection state event+ -> UUID+ -> StreamProjection state event+streamProjection projection@Projection{..} uuid =+ StreamProjection projection uuid (-1) projectionSeed+ -- | Gets the latest projection from a store by using 'getEvents' and then -- applying the events using the Projection's event handler. getLatestProjection :: (Monad m)- => EventStore serialized m- -> Projection proj serialized- -> UUID- -> m (proj, EventVersion)-getLatestProjection store proj uuid = do- events <- getEvents store uuid Nothing+ => EventStore event m+ -> StreamProjection state event+ -> m (StreamProjection state event)+getLatestProjection store projection@StreamProjection{..} = do+ events <- getEvents store streamProjectionUuid (eventsStartingAt $ streamProjectionVersion + 1) let- latestVersion = maxEventVersion events- latestProj = latestProjection proj $ storedEventEvent <$> events- return (latestProj, latestVersion)+ latestVersion = newEventVersion events+ latestState = foldl' (projectionEventHandler streamProjectionProjection) streamProjectionState $ storedEventEvent <$> events+ return $+ projection+ { streamProjectionVersion = latestVersion+ , streamProjectionState = latestState+ } where- maxEventVersion [] = -1- maxEventVersion es = maximum $ storedEventVersion <$> es+ newEventVersion [] = streamProjectionVersion+ newEventVersion es = maximum $ storedEventVersion <$> es +-- | This is a combination of a 'Projection' and the latest projection state+-- with respect to some 'SequenceNumber'. This is useful for in-memory read+-- models, and for querying the latest state starting from some previous state+-- at a lower 'SequenceNumber'.+data GloballyOrderedProjection state serialized+ = GloballyOrderedProjection+ { globallyOrderedProjectionProjection :: !(Projection state (GloballyOrderedEvent serialized))+ , globallyOrderedProjectionSequenceNumber :: !SequenceNumber+ , globallyOrderedProjectionState :: !state+ }++-- | Initialize a 'GloballyOrderedProjection' at 'SequenceNumber' 0 and with+-- the projection's seed value.+globallyOrderedProjection+ :: Projection state (GloballyOrderedEvent serialized)+ -> GloballyOrderedProjection state serialized+globallyOrderedProjection projection@Projection{..} =+ GloballyOrderedProjection projection 0 projectionSeed++-- | This applies an event to a 'GloballyOrderedProjection'. NOTE: There is no+-- guarantee that the 'SequenceNumber' for the event is the previous+-- 'SequenceNumber' plus one (in fact, that isn't even a guarantee that some+-- stores can provide). This function will update the+-- 'GloballyOrderedProjetion' to use the sequence number of the event.+globallyOrderedProjectionEventHandler+ :: GloballyOrderedProjection state serialized+ -> GloballyOrderedEvent serialized+ -> GloballyOrderedProjection state serialized+globallyOrderedProjectionEventHandler GloballyOrderedProjection{..} event@GloballyOrderedEvent{..} =+ let+ Projection{..} = globallyOrderedProjectionProjection+ seqNum = globallyOrderedEventSequenceNumber+ state' = projectionEventHandler globallyOrderedProjectionState event+ in GloballyOrderedProjection globallyOrderedProjectionProjection seqNum state'+ -- | Gets globally ordered events from the event store and builds a -- 'Projection' based on 'ProjectionEvent'. Optionally accepts the current -- projection state as an argument. getLatestGlobalProjection :: (Monad m) => GloballyOrderedEventStore serialized m- -> Projection proj (ProjectionEvent serialized)- -> Maybe (proj, SequenceNumber)- -> m (proj, SequenceNumber)-getLatestGlobalProjection store proj mCurrentState = do- let- currentState = fromMaybe (projectionSeed proj) $ fst <$> mCurrentState- startingSequenceNumber = maybe 0 (+1) $ snd <$> mCurrentState- events <- getSequencedEvents store startingSequenceNumber- let- projectionEvents = globallyOrderedEventToProjectionEvent <$> events- latestState = foldl' (projectionEventHandler proj) currentState projectionEvents- latestSeq =- case events of- [] -> startingSequenceNumber- _ -> globallyOrderedEventSequenceNumber $ last events- return (latestState, latestSeq)+ -> GloballyOrderedProjection state serialized+ -> m (GloballyOrderedProjection state serialized)+getLatestGlobalProjection store globalProjection@GloballyOrderedProjection{..} = do+ events <- getSequencedEvents store (eventsStartingAt $ globallyOrderedProjectionSequenceNumber + 1)+ return $ foldl' globallyOrderedProjectionEventHandler globalProjection events -- | Use a 'Serializer' to wrap a 'Projection' with event type @event@ so it -- uses the @serialized@ type.@@ -91,9 +148,14 @@ :: Projection state event -> Serializer event serialized -> Projection state serialized-serializedProjection (Projection seed eventHandler) Serializer{..} =- Projection seed serializedHandler+serializedProjection proj Serializer{..} = projectionMapMaybe deserialize proj++-- | Transform a 'Projection' when you only have a partial relationship between+-- the source event type and the target event type.+projectionMapMaybe+ :: (eventB -> Maybe eventA)+ -> Projection state eventA+ -> Projection state eventB+projectionMapMaybe f (Projection seed handler) = Projection seed handler' where- -- Try to deserialize the event and apply the handler. If we can't- -- deserialize, then just return the state.- serializedHandler state = maybe state (eventHandler state) . deserialize+ handler' state = maybe state (handler state) . f
+ src/Eventful/ProjectionCache/Types.hs view
@@ -0,0 +1,147 @@+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE RecordWildCards #-}++module Eventful.ProjectionCache.Types+ ( ProjectionCache (..)+ , StreamProjectionCache+ , GloballyOrderedProjectionCache+ , runProjectionCacheUsing+ , serializedProjectionCache+ , getLatestProjectionWithCache+ , 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 @orderKey@ 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 orderKey serialized m+ = ProjectionCache+ { storeProjectionSnapshot :: key -> orderKey -> serialized -> m ()+ -- ^ Stores the state for a projection at a given @key@ and @orderKey@.+ -- 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 (orderKey, serialized))+ -- ^ Loads the latest projection state from the cache.+ }++-- | Type synonym for a 'ProjectionCache' used on individual event streams.+type StreamProjectionCache serialized m = ProjectionCache UUID EventVersion serialized m++-- | Type synonym for a 'ProjectionCache' that is used in conjunction with a+-- 'GloballyOrderedEventStore'.+type GloballyOrderedProjectionCache 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 orderKey serialized mstore+ -> ProjectionCache key orderKey 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 orderKey serialized m+ -> ProjectionCache key orderKey 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 'getLatestProjection', but uses a 'ProjectionCache' if it contains+-- more recent state.+getLatestProjectionWithCache+ :: (Monad m)+ => EventStore event m+ -> StreamProjectionCache state m+ -> StreamProjection state event+ -> m (StreamProjection state event)+getLatestProjectionWithCache store cache originalProj = do+ mLatestState <- loadProjectionSnapshot cache (streamProjectionUuid originalProj)+ let+ mkProjection' (version, state) =+ if version > streamProjectionVersion originalProj+ then+ originalProj+ { streamProjectionVersion = version+ , streamProjectionState = state+ }+ else originalProj+ projection' = maybe originalProj mkProjection' mLatestState+ getLatestProjection store projection'++-- | Like 'getLatestGlobalProjection', but uses a 'ProjectionCache' if it+-- contains more recent state.+getLatestGlobalProjectionWithCache+ :: (Monad m)+ => GloballyOrderedEventStore event m+ -> GloballyOrderedProjectionCache key state m+ -> GloballyOrderedProjection state event+ -> key+ -> m (GloballyOrderedProjection state event)+getLatestGlobalProjectionWithCache store cache originalProj key = do+ mLatestState <- loadProjectionSnapshot cache key+ let+ mkProjection' (seqNum, state) =+ if seqNum > globallyOrderedProjectionSequenceNumber originalProj+ then+ originalProj+ { globallyOrderedProjectionSequenceNumber = seqNum+ , globallyOrderedProjectionState = state+ }+ else originalProj+ projection' = maybe originalProj mkProjection' mLatestState+ getLatestGlobalProjection store projection'++-- | Loads the latest projection state from the cache/store and stores this+-- value back into the projection cache.+updateProjectionCache+ :: (Monad m)+ => EventStore event m+ -> StreamProjectionCache state m+ -> StreamProjection state event+ -> m ()+updateProjectionCache store cache projection = do+ StreamProjection{..} <- getLatestProjectionWithCache store cache projection+ storeProjectionSnapshot cache streamProjectionUuid streamProjectionVersion streamProjectionState++-- | Analog of 'updateProjectionCache' for a 'GloballyOrderedProjectionCache'.+updateGlobalProjectionCache+ :: (Monad m)+ => GloballyOrderedEventStore event m+ -> GloballyOrderedProjectionCache key state m+ -> GloballyOrderedProjection state event+ -> key+ -> m ()+updateGlobalProjectionCache store cache projection key = do+ GloballyOrderedProjection{..} <- getLatestGlobalProjectionWithCache store cache projection key+ storeProjectionSnapshot cache key globallyOrderedProjectionSequenceNumber globallyOrderedProjectionState
src/Eventful/ReadModel/Class.hs view
@@ -31,7 +31,7 @@ runPollingReadModel ReadModel{..} getGloballyOrderedEvents runStore waitSeconds = forever $ do -- Get new events starting from latest applied sequence number latestSeq <- readModelLatestAppliedSequence readModelModel- newEvents <- runStore $ getSequencedEvents getGloballyOrderedEvents (latestSeq + 1)+ newEvents <- runStore $ getSequencedEvents getGloballyOrderedEvents (eventsStartingAt $ latestSeq + 1) -- Handle the new events readModelHandleEvents readModelModel newEvents
src/Eventful/Serializer.hs view
@@ -14,9 +14,6 @@ , jsonSerializer , jsonTextSerializer , dynamicSerializer- -- * Serialized event store- , serializedEventStore- , serializedGloballyOrderedEventStore -- * Sum types , EventSumType (..) , eventSumTypeSerializer@@ -25,13 +22,11 @@ import Control.Applicative ((<|>)) import Data.Aeson import Data.Dynamic-import Data.Maybe (fromMaybe, mapMaybe)+import Data.Maybe (fromMaybe) import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TLE import GHC.Generics -import Eventful.Store.Class- -- | A 'Serializer' describes the injective conversion between types @a@ and -- @b@. In plain English, this means that you can go from @a@ to @b@, and you -- can 'Maybe' go from @b@ back to @a@. This is often used to serialize events@@ -112,35 +107,6 @@ (error $ "Failure in eventSumTypeSerializer. Can't serialize " ++ show (typeOf event)) (eventFromDyn $ eventToDyn event) deserialize' = eventFromDyn . eventToDyn---- | Wraps an 'EventStore' 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).-serializedEventStore- :: (Monad m)- => Serializer event serialized- -> EventStore serialized m- -> EventStore event m-serializedEventStore Serializer{..} store =- EventStore- (getLatestVersion store)- getEvents'- storeEvents'- where- getEvents' uuid mVersion = mapMaybe (traverse deserialize) <$> getEvents store uuid mVersion- storeEvents' expectedVersion uuid events = storeEvents store expectedVersion uuid (serialize <$> events)---- | Like 'serializedEventStore' except for 'GloballyOrderedEventStore'.-serializedGloballyOrderedEventStore- :: (Monad m)- => Serializer event serialized- -> GloballyOrderedEventStore serialized m- -> GloballyOrderedEventStore event m-serializedGloballyOrderedEventStore Serializer{..} store =- GloballyOrderedEventStore getSequencedEvents'- where- getSequencedEvents' sequenceNumber =- mapMaybe (traverse deserialize) <$> getSequencedEvents store sequenceNumber -- | This is a type class for serializing sum types of events to 'Dynamic' -- without the associated constructor. This is useful when transforming between
src/Eventful/Store/Class.hs view
@@ -2,6 +2,7 @@ {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveTraversable #-} {-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE RankNTypes #-} {-# LANGUAGE RecordWildCards #-} module Eventful.Store.Class@@ -10,6 +11,12 @@ , GloballyOrderedEventStore (..) , ExpectedVersion (..) , EventWriteError (..)+ , runEventStoreUsing+ , runGloballyOrderedEventStoreUsing+ , module Eventful.Store.Queries+ -- * Serialization+ , serializedEventStore+ , serializedGloballyOrderedEventStore -- * Utility types , ProjectionEvent (..) , StoredEvent (..)@@ -25,9 +32,12 @@ ) where import Data.Aeson+import Data.Maybe (mapMaybe) import Web.HttpApiData import Web.PathPieces +import Eventful.Serializer+import Eventful.Store.Queries import Eventful.UUID -- | The 'EventStore' is the core type of eventful. A store operates in some@@ -36,7 +46,7 @@ = EventStore { getLatestVersion :: UUID -> m EventVersion -- ^ Gets the latest 'EventVersion' for a given 'Projection'.- , getEvents :: UUID -> Maybe EventVersion -> m [StoredEvent serialized]+ , getEvents :: UUID -> EventStoreQueryRange EventVersion -> m [StoredEvent serialized] -- ^ Retrieves all the events for a given 'Projection' using that -- projection's UUID. If an event version is provided then all events with -- a version greater than or equal to that version are returned.@@ -50,7 +60,7 @@ -- store. newtype GloballyOrderedEventStore serialized m = GloballyOrderedEventStore- { getSequencedEvents :: SequenceNumber -> m [GloballyOrderedEvent serialized]+ { getSequencedEvents :: EventStoreQueryRange SequenceNumber -> m [GloballyOrderedEvent serialized] } -- | ExpectedVersion is used to assert the event stream is at a certain version@@ -100,6 +110,60 @@ if f latestVersion then storeEvents' uuid events >> return Nothing else return $ Just $ EventStreamNotAtExpectedVersion latestVersion++-- | Changes the monad an 'EventStore' runs in. This is useful to run event+-- stores in another 'Monad' while forgetting the original 'Monad'.+runEventStoreUsing+ :: (Monad m, Monad mstore)+ => (forall a. mstore a -> m a)+ -> EventStore serialized mstore+ -> EventStore serialized m+runEventStoreUsing runStore EventStore{..} =+ EventStore+ { getLatestVersion = runStore . getLatestVersion+ , getEvents = \uuid range -> runStore $ getEvents uuid range+ , storeEvents = \vers uuid events -> runStore $ storeEvents vers uuid events+ }++-- | Analog of 'runEventStoreUsing' for a 'GloballyOrderedEventStore'.+runGloballyOrderedEventStoreUsing+ :: (Monad m, Monad mstore)+ => (forall a. mstore a -> m a)+ -> GloballyOrderedEventStore serialized mstore+ -> GloballyOrderedEventStore serialized m+runGloballyOrderedEventStoreUsing runStore GloballyOrderedEventStore{..} =+ GloballyOrderedEventStore+ { getSequencedEvents = runStore . getSequencedEvents+ }++-- | Wraps an 'EventStore' 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).+serializedEventStore+ :: (Monad m)+ => Serializer event serialized+ -> EventStore serialized m+ -> EventStore event m+serializedEventStore Serializer{..} store =+ EventStore+ (getLatestVersion store)+ getEvents'+ storeEvents'+ where+ getEvents' uuid mVersion = mapMaybe (traverse deserialize) <$> getEvents store uuid mVersion+ storeEvents' expectedVersion uuid events = storeEvents store expectedVersion uuid (serialize <$> events)++-- | Like 'serializedEventStore' except for 'GloballyOrderedEventStore'.+serializedGloballyOrderedEventStore+ :: (Monad m)+ => Serializer event serialized+ -> GloballyOrderedEventStore serialized m+ -> GloballyOrderedEventStore event m+serializedGloballyOrderedEventStore Serializer{..} store =+ GloballyOrderedEventStore getSequencedEvents'+ where+ getSequencedEvents' sequenceNumber =+ mapMaybe (traverse deserialize) <$> getSequencedEvents store sequenceNumber -- | A 'ProjectionEvent' is an event that is associated with a 'Projection' via -- the projection's 'UUID'.
+ src/Eventful/Store/Queries.hs view
@@ -0,0 +1,48 @@+{-# LANGUAGE DeriveFunctor #-}++module Eventful.Store.Queries+ ( EventStoreQueryRange (..)+ , EventStoreQueryStart (..)+ , EventStoreQueryLimit (..)+ , allEvents+ , eventsUntil+ , eventsStartingAt+ , eventsStartingAtUntil+ , eventsStartingAtTakeLimit+ ) where++-- | This type defines how to query an event stream. It defines both where to+-- start and where to stop in the stream.+data EventStoreQueryRange a+ = EventStoreQueryRange+ { eventStoreQueryRangeStart :: EventStoreQueryStart a+ , eventStoreQueryRangeLimit :: EventStoreQueryLimit a+ } deriving (Functor)++-- | This type defines where an event store query starts.+data EventStoreQueryStart a+ = StartFromBeginning+ | StartQueryAt a+ deriving (Show, Eq, Functor)++-- | This type is used to limit the results of a query from an event store.+data EventStoreQueryLimit a+ = NoQueryLimit+ | MaxNumberOfEvents Int+ | StopQueryAt a+ deriving (Show, Eq, Functor)++allEvents :: EventStoreQueryRange a+allEvents = EventStoreQueryRange StartFromBeginning NoQueryLimit++eventsUntil :: a -> EventStoreQueryRange a+eventsUntil end = EventStoreQueryRange StartFromBeginning (StopQueryAt end)++eventsStartingAt :: a -> EventStoreQueryRange a+eventsStartingAt start = EventStoreQueryRange (StartQueryAt start) NoQueryLimit++eventsStartingAtUntil :: a -> a -> EventStoreQueryRange a+eventsStartingAtUntil start end = EventStoreQueryRange (StartQueryAt start) (StopQueryAt end)++eventsStartingAtTakeLimit :: a -> Int -> EventStoreQueryRange a+eventsStartingAtTakeLimit start maxNum = EventStoreQueryRange (StartQueryAt start) (MaxNumberOfEvents maxNum)