eventsourcing-0.9.0: src/Database/CQRS/Stream.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
-- | Events can be written to a stream and can be streamed from it.
module Database.CQRS.Stream
( Stream(..)
, WritableStream(..)
, EventWithContext(..)
, EventWithContext'
, MonadMetadata(..)
, ConsistencyCheck(..)
, writeEvent
, writeEventCc
, optimistically
, StreamBounds(..)
, StreamBounds'
, afterEvent
, untilEvent
) where
import GHC.Generics
import qualified Control.Monad.Except as Exc
import qualified Pipes
import Database.CQRS.Error
class Stream f stream where
-- | Type of the events contained in that stream.
type EventType stream :: *
-- | Type of unique identifiers for events in the stream.
--
-- There must be a total order on identifiers so they can be sorted.
type EventIdentifier stream :: *
-- | Depending on the store, this structure can contain the creation date, a
-- correlation ID, etc.
type EventMetadata stream :: *
-- | Stream all the events within some bounds in arbitrary batches.
--
-- Events must be streamed from lowest to greatest identifier. If the back-end
-- is fetching events in batches, they can be returned in the same way to
-- improve performace. If the event can't be decoded, a 'Left' should be
-- returned instead with the identifier and an error message.
streamEvents
:: stream
-> StreamBounds' stream
-> Pipes.Producer
[ Either
(EventIdentifier stream, String)
(EventWithContext' stream)
] f ()
-- | A condition to check before inserting a new event in a stream.
--
-- This can be used to enforce consistency by checking that no new events were
-- inserted since some validation has been performed and therefore that the
-- validations are still sound.
data ConsistencyCheck identifier
= NoConsistencyCheck -- ^ Always write the new event.
| CheckNoEvents -- ^ There are no events in that stream.
| CheckLastEvent identifier
-- ^ The latest event's identifier matches.
class Stream f stream => WritableStream f stream where
-- | Append the event to the stream and return the identifier.
--
-- The identifier must be greater than the previous events' identifiers.
--
-- The function must throw 'ConsistencyCheckError' if the check fails.
writeEventWithMetadata
:: stream
-> EventType stream
-> EventMetadata stream
-> ConsistencyCheck (EventIdentifier stream)
-> f (EventIdentifier stream)
-- | Once added to the stream, an event is adorned with an identifier and some
-- metadata.
data EventWithContext identifier metadata event = EventWithContext
{ identifier :: identifier
, metadata :: metadata
, event :: event
} deriving (Eq, Show, Generic)
type EventWithContext' stream
= EventWithContext
(EventIdentifier stream)
(EventMetadata stream)
(EventType stream)
-- | The event metadata come from the current "environment".
class MonadMetadata metadata m where
getMetadata :: m metadata
instance Monad m => MonadMetadata () m where
getMetadata = pure ()
-- | Get the metadata from the environment, append the event to the store and
-- return the identifier.
writeEvent
:: (Monad m, MonadMetadata (EventMetadata stream) m, WritableStream m stream)
=> stream
-> EventType stream
-> m (EventIdentifier stream)
writeEvent stream ev = do
md <- getMetadata
writeEventWithMetadata stream ev md NoConsistencyCheck
-- | Get the metadata from the environment, validate the consistency check,
-- append the event to the store and return its identifier.
writeEventCc
:: (Monad m, MonadMetadata (EventMetadata stream) m, WritableStream m stream)
=> stream
-> EventType stream
-> ConsistencyCheck (EventIdentifier stream)
-> m (EventIdentifier stream)
writeEventCc stream ev cc = do
md <- getMetadata
writeEventWithMetadata stream ev md cc
-- | Execute an action and retry indefinitely as long as it throws
-- 'ConsistencyCheckError'.
--
-- This makes it possible to have Optimistic Concurrency Control when writing
-- events by getting the aggregate and using 'writeEventCc' or
-- 'writeEventWithMetadata' inside the action passed to 'optimistically'.
--
-- /!\ It does NOT create a transaction when you can write several events. You
-- should only use this to write a single event!
optimistically :: Exc.MonadError Error m => m a -> m a
optimistically f = do
f `Exc.catchError` \case
ConsistencyCheckError _ -> optimistically f
e -> Exc.throwError e
-- | Lower/upper bounds of an event stream.
--
-- The 'Semigroup' instance returns bounds for the intersection of the two
-- streams.
data StreamBounds identifier = StreamBounds
{ _afterEvent :: Maybe identifier -- ^ Exclusive.
, _untilEvent :: Maybe identifier -- ^ Inclusive.
}
deriving (Functor, Foldable, Traversable)
type StreamBounds' stream = StreamBounds (EventIdentifier stream)
instance
forall identifier. Ord identifier
=> Semigroup (StreamBounds identifier) where
sb1 <> sb2 =
StreamBounds
{ _afterEvent = combine _afterEvent max
, _untilEvent = combine _untilEvent min
}
where
combine
:: (StreamBounds identifier -> Maybe b) -> (b -> b -> b) -> Maybe b
combine proj merge =
case (proj sb1, proj sb2) of
(mx, Nothing) -> mx
(Nothing, my) -> my
(Just x, Just y) -> Just $ merge x y
instance Ord identifier => Monoid (StreamBounds identifier) where
mempty = StreamBounds Nothing Nothing
-- | After the event with the given identifier, excluding it.
afterEvent :: Ord identifier => identifier -> StreamBounds identifier
afterEvent i = mempty { _afterEvent = Just i }
-- | Until the event with the given identifier, including it.
untilEvent :: Ord identifier => identifier -> StreamBounds identifier
untilEvent i = mempty { _untilEvent = Just i }