packages feed

eventsource-api 1.3.1 → 1.5.0

raw patch · 9 files changed

+168/−964 lines, 9 filesdep +streamingPVP ok

version bump matches the API change (PVP)

Dependencies added: streaming

API changes (from Hackage documentation)

- EventSource.Aggregate: Action' :: (e -> s -> (s -> a -> m ()) -> m ()) -> Action' e s m a
- EventSource.Aggregate: AggEnv :: SomeStore -> Id a -> AggEnv a
- EventSource.Aggregate: AggState :: !ExpectedVersion -> !a -> AggState a
- EventSource.Aggregate: [aggEnvId] :: AggEnv a -> Id a
- EventSource.Aggregate: [aggEnvStore] :: AggEnv a -> SomeStore
- EventSource.Aggregate: [aggStateVersion] :: AggState a -> !ExpectedVersion
- EventSource.Aggregate: [aggState] :: AggState a -> !a
- EventSource.Aggregate: [runAction] :: Action' e s m a -> e -> s -> (s -> a -> m ()) -> m ()
- EventSource.Aggregate: aggId :: Agg a -> Id a
- EventSource.Aggregate: apply :: Aggregate a => a -> Evt a -> M a a
- EventSource.Aggregate: askEnv :: Action' e s m e
- EventSource.Aggregate: class Aggregate a where {
- EventSource.Aggregate: class StreamId a
- EventSource.Aggregate: class Aggregate a => Validate a where {
- EventSource.Aggregate: closeAgg :: Agg a -> M a ()
- EventSource.Aggregate: data Agg a
- EventSource.Aggregate: data AggEnv a
- EventSource.Aggregate: data AggState a
- EventSource.Aggregate: execute :: MonadBase IO (M a) => Agg a -> Action a r -> M a r
- EventSource.Aggregate: getState :: Action' e s m s
- EventSource.Aggregate: instance Control.Monad.Trans.Class.MonadTrans (EventSource.Aggregate.Action' e s)
- EventSource.Aggregate: instance GHC.Base.Applicative (EventSource.Aggregate.Action' e s m)
- EventSource.Aggregate: instance GHC.Base.Functor (EventSource.Aggregate.Action' e s m)
- EventSource.Aggregate: instance GHC.Base.Monad (EventSource.Aggregate.Action' e s m)
- EventSource.Aggregate: loadAgg :: (Aggregate a, StreamId (Id a), DecodeEvent (Evt a), MonadBaseControl IO (M a)) => SomeStore -> Id a -> a -> M a (Either ForEventFailure (Agg a))
- EventSource.Aggregate: loadOrCreateAgg :: (Aggregate a, StreamId (Id a), DecodeEvent (Evt a), MonadBaseControl IO (M a)) => SomeStore -> Id a -> a -> M a (Agg a)
- EventSource.Aggregate: newAgg :: (Aggregate a, MonadBaseControl IO (M a)) => SomeStore -> Id a -> a -> M a (Agg a)
- EventSource.Aggregate: newtype Action' e s m a
- EventSource.Aggregate: persist :: (StreamId id, EncodeEvent event, MonadBase IO m) => SomeStore -> id -> ExpectedVersion -> event -> m EventNumber
- EventSource.Aggregate: putState :: s -> Action' e s m ()
- EventSource.Aggregate: route :: MonadBase IO (M a) => Agg a -> (SomeStore -> a -> (a -> r -> M a ()) -> M a ()) -> M a r
- EventSource.Aggregate: runAgg :: MonadBase IO (M a) => Agg a -> Action a r -> (r -> M a ()) -> M a ()
- EventSource.Aggregate: snapshot :: MonadBase IO (M a) => Agg a -> M a a
- EventSource.Aggregate: submitCmd :: (Validate a, MonadBase IO (M a), StreamId (Id a), EncodeEvent (Evt a)) => Agg a -> Cmd a -> M a (Decision a)
- EventSource.Aggregate: submitEvt :: (Aggregate a, MonadBase IO (M a)) => Agg a -> Evt a -> M a ()
- EventSource.Aggregate: toStreamName :: StreamId a => a -> StreamName
- EventSource.Aggregate: type Action a r = Action' (AggEnv a) (AggState a) (M a) r
- EventSource.Aggregate: type Decision a = Either (Err a) (Evt a)
- EventSource.Aggregate: type family Err a :: *;
- EventSource.Aggregate: validate :: Validate a => a -> Cmd a -> M a (Decision a)
- EventSource.Aggregate: }
- EventSource.Aggregate.Simple: applyIO :: AggregateIO event state => state -> event -> IO state
- EventSource.Aggregate.Simple: class AggregateIO event state | event -> state
- EventSource.Aggregate.Simple: class AggregateIO event state => ValidateIO command event state | command -> state, command -> event
- EventSource.Aggregate.Simple: closeAgg :: AggIO id command event state -> IO ()
- EventSource.Aggregate.Simple: data Simple id command event state
- EventSource.Aggregate.Simple: instance EventSource.Aggregate.Simple.AggregateIO event state => EventSource.Aggregate.Aggregate (EventSource.Aggregate.Simple.Simple id command event state)
- EventSource.Aggregate.Simple: instance EventSource.Aggregate.Simple.ValidateIO command event state => EventSource.Aggregate.Validate (EventSource.Aggregate.Simple.Simple id command event state)
- EventSource.Aggregate.Simple: loadAgg :: (AggregateIO event state, StreamId id, DecodeEvent event) => SomeStore -> id -> state -> IO (Either ForEventFailure (AggIO id command event state))
- EventSource.Aggregate.Simple: loadOrCreateAgg :: (AggregateIO event state, StreamId id, DecodeEvent event) => SomeStore -> id -> state -> IO (AggIO id command event state)
- EventSource.Aggregate.Simple: newAgg :: AggregateIO event state => SomeStore -> id -> state -> IO (AggIO id command event state)
- EventSource.Aggregate.Simple: route :: AggIO id command event state -> (SomeStore -> state -> (state -> r -> IO ()) -> IO ()) -> IO r
- EventSource.Aggregate.Simple: snapshot :: AggIO id command event state -> IO state
- EventSource.Aggregate.Simple: submitCmd :: (ValidateIO command event state, StreamId id, EncodeEvent event) => AggIO id command event state -> command -> IO (Either SomeException event)
- EventSource.Aggregate.Simple: submitEvt :: AggregateIO event state => AggIO id command event state -> event -> IO ()
- EventSource.Aggregate.Simple: type AggIO id command event state = Agg (Simple id command event state)
- EventSource.Aggregate.Simple: validateIO :: ValidateIO command event state => state -> command -> IO (Either SomeException event)
- EventSource.Store: Batch :: EventNumber -> Int32 -> Batch
- EventSource.Store: ForEventDecodeFailure :: Text -> ForEventFailure
- EventSource.Store: ForEventReadFailure :: ReadFailure -> ForEventFailure
- EventSource.Store: [nextEvent] :: Subscription -> forall m. MonadBase IO m => m (Either SomeException SavedEvent)
- EventSource.Store: data Batch
- EventSource.Store: data ForEventFailure
- EventSource.Store: data StreamIterator
- EventSource.Store: foldEvents :: (MonadBase IO m, DecodeEvent a, Store store) => store -> StreamName -> (s -> a -> s) -> s -> ExceptT ForEventFailure m s
- EventSource.Store: foldEventsM :: (MonadBase IO m, DecodeEvent a, Store store) => store -> StreamName -> (s -> a -> m s) -> s -> ExceptT ForEventFailure m s
- EventSource.Store: foldEventsWithNumberM :: (MonadBase IO m, DecodeEvent a, Store store) => store -> StreamName -> (EventNumber -> s -> a -> m s) -> s -> ExceptT ForEventFailure m s
- EventSource.Store: foldSavedEvents :: (MonadBase IO m, Store store) => store -> StreamName -> (s -> SavedEvent -> s) -> s -> ExceptT ForEventFailure m s
- EventSource.Store: foldSavedEventsM :: (MonadBase IO m, Store store) => store -> StreamName -> (s -> SavedEvent -> m s) -> s -> ExceptT ForEventFailure m s
- EventSource.Store: foldSub :: (DecodeEvent a, MonadBase IO m) => Subscription -> (a -> m ()) -> (SomeException -> m ()) -> m ()
- EventSource.Store: foldSubAsync :: (MonadBaseControl IO m, DecodeEvent a) => Subscription -> (a -> m ()) -> (SomeException -> m ()) -> m (Async (StM m ()))
- EventSource.Store: foldSubSaved :: MonadBase IO m => Subscription -> (SavedEvent -> m ()) -> (SomeException -> m ()) -> m ()
- EventSource.Store: foldSubSavedAsync :: MonadBaseControl IO m => Subscription -> (SavedEvent -> m ()) -> (SomeException -> m ()) -> m (Async (StM m ()))
- EventSource.Store: forEvents :: (MonadBase IO m, DecodeEvent a, Store store) => store -> StreamName -> (a -> m ()) -> ExceptT ForEventFailure m ()
- EventSource.Store: forEventsWithNumber :: (MonadBase IO m, DecodeEvent a, Store store) => store -> StreamName -> (EventNumber -> a -> m ()) -> ExceptT ForEventFailure m ()
- EventSource.Store: forSavedEvents :: (MonadBase IO m, Store store) => store -> StreamName -> (SavedEvent -> m ()) -> ExceptT ForEventFailure m ()
- EventSource.Store: instance GHC.Exception.Exception EventSource.Store.ExpectedVersionException
- EventSource.Store: instance GHC.Exception.Exception EventSource.Store.ForEventFailure
- EventSource.Store: instance GHC.Show.Show EventSource.Store.ForEventFailure
- EventSource.Store: instance GHC.Show.Show EventSource.Store.StreamIterator
- EventSource.Store: iteratorNext :: StreamIterator -> forall m. MonadBase IO m => m (Maybe SavedEvent)
- EventSource.Store: iteratorNextEvent :: (DecodeEvent a, MonadBase IO m, MonadPlus m) => StreamIterator -> m (Maybe a)
- EventSource.Store: iteratorReadAll :: MonadBase IO m => StreamIterator -> m [SavedEvent]
- EventSource.Store: iteratorReadAllEvents :: (DecodeEvent a, MonadBase IO m, MonadPlus m) => StreamIterator -> m [a]
- EventSource.Store: nextEventAs :: (DecodeEvent a, MonadBase IO m) => Subscription -> m (Either SomeException a)
- EventSource.Store: readBatch :: (Store store, MonadBase IO m) => store -> StreamName -> Batch -> m (Async (ReadStatus Slice))
- EventSource.Store: streamIterator :: (Store store, MonadBase IO m) => store -> StreamName -> m (ReadStatus StreamIterator)
- EventSource.Types: Slice :: [SavedEvent] -> Bool -> EventNumber -> Slice
- EventSource.Types: [sliceNextEventNumber] :: Slice -> EventNumber
- EventSource.Types: data Slice
- EventSource.Types: instance Data.Semigroup.Semigroup EventSource.Types.Properties
- EventSource.Types: instance GHC.Exception.Exception EventSource.Types.DecodeEventException
- EventSource.Types: instance GHC.Show.Show EventSource.Types.Slice
+ EventSource.Store: Batch' :: !a -> !Int32 -> Batch' a
+ EventSource.Store: [subscriptionStream] :: Subscription -> forall m. MonadBase IO m => Stream (Of SavedEvent) m ()
+ EventSource.Store: data Batch' a
+ EventSource.Store: instance GHC.Exception.Type.Exception EventSource.Store.ExpectedVersionException
+ EventSource.Store: readStream :: (Store store, MonadBase IO m) => store -> StreamName -> Batch -> Stream (Of SavedEvent) (ExceptT ReadFailure m) ()
+ EventSource.Store: type Batch = Batch' EventNumber
+ EventSource.Store.Internal.Iterator: Batch' :: !a -> !Int32 -> Batch' a
+ EventSource.Store.Internal.Iterator: IteratorOverAvailable :: Slice' a -> IteratorOverState a
+ EventSource.Store.Internal.Iterator: IteratorOverClosed :: IteratorOverState a
+ EventSource.Store.Internal.Iterator: IteratorOverEndOfStream :: IteratorOverAction a
+ EventSource.Store.Internal.Iterator: IteratorOverEvent :: SavedEvent -> IteratorOverAction a
+ EventSource.Store.Internal.Iterator: IteratorOverNextBatch :: a -> IteratorOverAction a
+ EventSource.Store.Internal.Iterator: [batchFrom] :: Batch' a -> !a
+ EventSource.Store.Internal.Iterator: [batchSize] :: Batch' a -> !Int32
+ EventSource.Store.Internal.Iterator: data Batch' a
+ EventSource.Store.Internal.Iterator: data IteratorOverAction a
+ EventSource.Store.Internal.Iterator: data IteratorOverState a
+ EventSource.Store.Internal.Iterator: iterateOver :: MonadBase IO m => IORef (IteratorOverState a) -> (Batch' a -> m (Async (ReadStatus (Slice' a)))) -> m (Maybe SavedEvent)
+ EventSource.Store.Internal.Iterator: startFrom :: a -> Batch' a
+ EventSource.Types: Slice' :: ![SavedEvent] -> !Bool -> !a -> Slice' a
+ EventSource.Types: [sliceNext] :: Slice' a -> !a
+ EventSource.Types: data Slice' a
+ EventSource.Types: instance GHC.Base.Semigroup EventSource.Types.Properties
+ EventSource.Types: instance GHC.Exception.Type.Exception EventSource.Types.DecodeEventException
+ EventSource.Types: instance GHC.Exception.Type.Exception EventSource.Types.ReadFailure
+ EventSource.Types: instance GHC.Show.Show a => GHC.Show.Show (EventSource.Types.Slice' a)
+ EventSource.Types: sliceNextEventNumber :: Slice -> EventNumber
+ EventSource.Types: type Slice = Slice' EventNumber
- EventSource.Store: Subscription :: SubscriptionId -> (forall m. MonadBase IO m => m (Either SomeException SavedEvent)) -> Subscription
+ EventSource.Store: Subscription :: SubscriptionId -> (forall m. MonadBase IO m => Stream (Of SavedEvent) m ()) -> Subscription
- EventSource.Store: [batchFrom] :: Batch -> EventNumber
+ EventSource.Store: [batchFrom] :: Batch' a -> !a
- EventSource.Store: [batchSize] :: Batch -> Int32
+ EventSource.Store: [batchSize] :: Batch' a -> !Int32
- EventSource.Store: startFrom :: EventNumber -> Batch
+ EventSource.Store: startFrom :: a -> Batch' a
- EventSource.Store: unhandled :: (MonadBase IO m, Exception e) => ExceptT e m a -> m a
+ EventSource.Store: unhandled :: (MonadBase IO m, Exception e) => Stream (Of a) (ExceptT e m) () -> Stream (Of a) m ()
- EventSource.Types: Event :: EventType -> EventId -> Data -> Maybe Properties -> Event
+ EventSource.Types: Event :: !EventType -> !EventId -> !Data -> !Maybe Properties -> Event
- EventSource.Types: Properties :: (Map Text Text) -> Properties
+ EventSource.Types: Properties :: Map Text Text -> Properties
- EventSource.Types: ReadError :: (Maybe Text) -> ReadFailure
+ EventSource.Types: ReadError :: Maybe Text -> ReadFailure
- EventSource.Types: SavedEvent :: EventNumber -> Event -> Maybe Event -> SavedEvent
+ EventSource.Types: SavedEvent :: !EventNumber -> !Event -> !Maybe Event -> SavedEvent
- EventSource.Types: [eventId] :: Event -> EventId
+ EventSource.Types: [eventId] :: Event -> !EventId
- EventSource.Types: [eventMetadata] :: Event -> Maybe Properties
+ EventSource.Types: [eventMetadata] :: Event -> !Maybe Properties
- EventSource.Types: [eventNumber] :: SavedEvent -> EventNumber
+ EventSource.Types: [eventNumber] :: SavedEvent -> !EventNumber
- EventSource.Types: [eventPayload] :: Event -> Data
+ EventSource.Types: [eventPayload] :: Event -> !Data
- EventSource.Types: [eventType] :: Event -> EventType
+ EventSource.Types: [eventType] :: Event -> !EventType
- EventSource.Types: [linkEvent] :: SavedEvent -> Maybe Event
+ EventSource.Types: [linkEvent] :: SavedEvent -> !Maybe Event
- EventSource.Types: [savedEvent] :: SavedEvent -> Event
+ EventSource.Types: [savedEvent] :: SavedEvent -> !Event
- EventSource.Types: [sliceEndOfStream] :: Slice -> Bool
+ EventSource.Types: [sliceEndOfStream] :: Slice' a -> !Bool
- EventSource.Types: [sliceEvents] :: Slice -> [SavedEvent]
+ EventSource.Types: [sliceEvents] :: Slice' a -> ![SavedEvent]

Files

CHANGELOG.md view
@@ -1,3 +1,8 @@+1.5.0+=====+  * Introduce a streaming interface.+  * Remove aggregate interface.+ 1.3.1 =====   * Fix GHC 8.4 build.
README.md view
@@ -1,4 +1,4 @@-# [eventsource-api][]+# eventsource-api  This project provides what we think be a lean eventsourcing API. The goal is to set a common ground for eventsourcing-based application, yet doesn't force you@@ -11,30 +11,32 @@ --   an event store. class Store store where   -- | Appends a batch of events at the end of a stream.-  appendEvents :: (EncodeEvent a, MonadIO m)+  appendEvents :: (EncodeEvent a, MonadBase IO m)                => store                -> StreamName                -> ExpectedVersion                -> [a]                -> m (Async EventNumber) -  -- | Appends a batch of events at the end of a stream.-  readBatch :: MonadIO m-            => store-            -> StreamName-            -> Batch-            -> m (Async (ReadStatus Slice))+  -- | Reads a stream in a stream-processing fashion.+  readStream :: MonadBase IO m+             => store+             -> StreamName+             -> Batch+             -> Stream (Of SavedEvent) (ExceptT ReadFailure m) ()    -- | Subscribes to given stream.-  subscribe :: MonadIO m => store -> StreamName -> m Subscription+  subscribe :: MonadBase IO m => store -> StreamName -> m Subscription++ ```  The idea is to use any backend of your liking as long as it's able to derive that typeclass with its related implicit constraints.    1. `appendEvents` MUST add events at the end of a stream without doing any other alteration on the stream. The order of the event's array MUST be respected within the store. Most important, the implementation MUST comply to the given `ExpectedVersion` passed by the user.-  -  2. `readBatch` SHOULD retrieve a set of event given a starting position and a batch size (represented by the `Batch` parameter). The returned array MUST have its events ordered by their `EventNumber` property. +  2. `readStream` SHOULD retrieve a set of event given a starting position and a batch size (represented by the `Batch` parameter). The returned array MUST have its events ordered by their `EventNumber` property.+   3. `subscribe` SHOULD register a subscription to the given stream. An implementation MUST allow to subscribe to a stream that doesn't exist yet. A subscription allows the user to be notified of any event added to the stream. There is no mandatory timing to meet regarding how fast the subscription is notified by a change. We only suggest to dispatch new events as soon as possible.  #### `ExpectedVersion`@@ -56,5 +58,3 @@ ```  On a write, if the `ExpectedVersion` condition given by the user is not met within the store, the implementation should raise an exception. At the moment, the API doesn't capture this situation in the type system.--[eventsource-api]: https://github.com/YoEight/eventsource-api
eventsource-api.cabal view
@@ -1,33 +1,36 @@--- This file has been generated from package.yaml by hpack version 0.28.2.+cabal-version: 1.12++-- This file has been generated from package.yaml by hpack version 0.31.1. -- -- see: https://github.com/sol/hpack ----- hash: 25d3e7c905840b2773e327d1dc91e90a9be3ecf9ff4c72ad67d8f999f0f05b9b+-- hash: 139d55fed31480f82f48ab1b236c875dac7ae19b648a917872b898b4f8c5a00b  name:           eventsource-api-version:        1.3.1+version:        1.5.0 synopsis:       Provides an eventsourcing high level API. description:    A high-level eventsourcing library. category:       Eventsourcing-homepage:       https://github.com/YoEight/eventsource-api#readme-bug-reports:    https://github.com/YoEight/eventsource-api/issues+homepage:       https://gitlab.com/YoEight/eventsource-api-hs author:         Yorick Laupa maintainer:     yo.eight@gmail.com license:        BSD3 license-file:   LICENSE.md build-type:     Simple-cabal-version:  >= 1.10 extra-source-files:     CHANGELOG.md     LICENSE.md     package.yaml     README.md -source-repository head-  type: git-  location: https://github.com/YoEight/eventsource-api- library+  exposed-modules:+      EventSource+      EventSource.Store+      EventSource.Store.Internal.Iterator+      EventSource.Types+  other-modules:+      Paths_eventsource_api   hs-source-dirs:       library   ghc-options: -Wall@@ -44,17 +47,10 @@     , mtl     , stm     , stm-chans+    , streaming     , string-conversions     , text     , transformers-base     , unordered-containers     , uuid-  exposed-modules:-      EventSource-      EventSource.Aggregate-      EventSource.Aggregate.Simple-      EventSource.Store-      EventSource.Types-  other-modules:-      Paths_eventsource_api   default-language: Haskell2010
− library/EventSource/Aggregate.hs
@@ -1,399 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}-{-# LANGUAGE GADTs            #-}-{-# LANGUAGE Rank2Types       #-}-{-# LANGUAGE TypeFamilies     #-}------------------------------------------------------------------------------------ |--- Module    :  EventSource.Aggregate--- Copyright :  (C) 2017 Yorick Laupa--- License   :  (see the file LICENSE)--- Maintainer:  Yorick Laupa <yo.eight@gmail.com>--- Stability :  experimental--- Portability: non-portable------ Implementation of an aggregate abstraction.--- Link: https://en.wikipedia.org/wiki/Domain-driven_design.----------------------------------------------------------------------------------module EventSource.Aggregate-  ( StreamId(..)-  -- * Aggregate-  , Aggregate(..)-  , Validate(..)-  , Decision-  , Agg-  , aggId-  , runAgg-  , newAgg-  , loadAgg-  , loadOrCreateAgg-  -- * Interactions-  , submitCmd-  , submitEvt-  , snapshot-  , route-  , closeAgg-  , execute-  -- * Internal-  , Action'(..)-  , Action-  , askEnv-  , getState-  , putState-  , AggEnv(..)-  , AggState(..)-  , persist-  ) where-----------------------------------------------------------------------------------import Control.Exception (SomeException, throwIO)-import Control.Monad (ap)-import Data.Foldable (for_, traverse_)-----------------------------------------------------------------------------------import           Control.Concurrent.Async.Lifted (wait)-import qualified Control.Concurrent.Lifted as Concurrent-import           Control.Concurrent.STM (atomically)-import qualified Control.Concurrent.STM.TBMQueue as Queue-import           Control.Exception.Enclosed (tryAny)-import           Control.Monad.Base (MonadBase, liftBase)-import           Control.Monad.Except (runExceptT)-import           Control.Monad.Loops (whileJust_)-import           Control.Monad.Trans (MonadTrans(..))-import           Control.Monad.Trans.Control (MonadBaseControl)-import           Data.IORef.Lifted (IORef, newIORef, readIORef, atomicWriteIORef)-----------------------------------------------------------------------------------import EventSource------------------------------------------------------------------------------------- | Maps an id to a 'StreamName'.-class StreamId a where-  toStreamName :: a -> StreamName------------------------------------------------------------------------------------- | Represents a stream aggregate. An aggregate can rebuild its internal state---   by replaying all the stream's events that aggregate is responsible for.-class Aggregate a where-  -- | Type of the id associated to the aggregate.-  type Id a  :: *--  -- | Type of event handled by the aggregate.-  type Evt a :: *--  -- | Type of monad stack used by the aggregate.-  type M a :: * -> *--  -- | Given current aggregate state, updates it according to the event the-  --   aggregate receives.-  apply :: a -> Evt a -> M a a------------------------------------------------------------------------------------- | When validating a command, tells if the command was valid. If the command---   is valid, it returns an event. Otherwise, it returns an error.-type Decision a = Either (Err a) (Evt a)------------------------------------------------------------------------------------- | Represents an aggregate that support validation. An aggregate that supports---   validation can receive command and decide if it was valid or not. When the---   validation is successful, The aggregate emits an event that will be---   persisted and pass to 'apply' function.-class Aggregate a => Validate a where-  -- | Type of command supported by the aggregate.-  type Cmd a :: *--  -- | Type of error that aggregate can yield.-  type Err a :: *--  -- | Validates a command. If the command validation succeeds, it will emits-  --   an event. Otherwise, it will returns an error.-  validate :: a -> Cmd a -> M a (Decision a)------------------------------------------------------------------------------------- | Internal aggregate action. An action is executed by an aggregate. An action---   embodies fundamental operations like submitting event, validating command---   or returning the current snapshot of an aggregate. Action are CPS-ed---   encoded so the execution model can be flexible. An action can perform---   synchronously or asynchronously.-newtype Action' e s m a =-  Action' { runAction :: e-                      -> s-                      -> (s -> a -> m ())-                      -> m () }-----------------------------------------------------------------------------------instance Functor (Action' e s m) where-  fmap f (Action' k) = Action' $ \e s resp ->-    k e s (\s' a -> resp s' (f a))-----------------------------------------------------------------------------------instance Applicative (Action' e s m) where-  pure  = return-  (<*>) = ap-----------------------------------------------------------------------------------instance Monad (Action' e s m) where-  return a = Action' $ \_ s resp -> resp s a--  Action' k >>= f = Action' $ \e s resp ->-    k e s (\s' a -> runAction (f a) e s' resp)-----------------------------------------------------------------------------------instance MonadTrans (Action' e s) where-  lift m = Action' $ \_ s resp -> m >>= resp s------------------------------------------------------------------------------------- | Returns an action environment.-askEnv :: Action' e s m e-askEnv = Action' $ \e s resp -> resp s e------------------------------------------------------------------------------------- | Returns an action current state.-getState :: Action' e s m s-getState = Action' $ \_ s resp -> resp s s------------------------------------------------------------------------------------- | Set an action state.-putState :: s -> Action' e s m ()-putState s = Action' $ \_ _ resp -> resp s ()------------------------------------------------------------------------------------- | An action configured to aggregate internal types.-type Action a r = Action' (AggEnv a) (AggState a) (M a) r------------------------------------------------------------------------------------- | Aggregate internal environment.-data AggEnv a =-  AggEnv { aggEnvStore :: SomeStore-           -- ^ Handle to an eventstore.-         , aggEnvId :: Id a-           -- ^ Identification of the aggregate.-         }------------------------------------------------------------------------------------- | Aggregate internal state.-data AggState a =-  AggState { aggStateVersion :: !ExpectedVersion-             -- ^ Expected version of the next write. This isn't expose to-             --   user and it's updated automatically.-           , aggState :: !a-             -- ^ Aggregate current state.-           }------------------------------------------------------------------------------------- | A stream aggregate. An aggregate updates its internal based on the event---  it receives. You can read its current state by using 'snapshot'. If it---  supports validation, through 'Validated' typeclass, it can receive---  command and emits an event if the command was successful. Otherwise, it will---  yield an error. When receiving valid command, an aggregate will persist the---  resulting event. An aggregate is only responsible of its own stream.-data Agg a where-  Agg :: AggEnv a-      -> M a ()-      -> IORef (Maybe SomeException) -- Last exception ever captured.-      -> (forall r. Action a r -> (r -> M a ()) -> M a ())-      -> Agg a------------------------------------------------------------------------------------- | Returns an aggregate id.-aggId :: Agg a -> Id a-aggId (Agg env _ _ _) = aggEnvId env------------------------------------------------------------------------------------- | Executes an action on an aggregate.-runAgg :: MonadBase IO (M a) => Agg a -> Action a r -> (r -> M a ()) -> M a ()-runAgg (Agg _ _ errRef k) action resp = do-  errLast <- readIORef errRef-  traverse_ (liftBase . throwIO) errLast-  k action resp------------------------------------------------------------------------------------- | Holds an existantially quantified action so it can passed around easily---   to aggregate's internal concurrent channel.-data Msg a where-  Msg :: Action a r -> (r -> M a ()) -> Msg a------------------------------------------------------------------------------------- | Creates a new aggregate given an eventstore handle, an id and an initial---   state.-newAgg :: (Aggregate a, MonadBaseControl IO (M a))-       => SomeStore-       -> Id a-       -> a-       -> M a (Agg a)-newAgg store aId seed = do-  queue   <- liftBase $ Queue.newTBMQueueIO 500 -- Max parked messages.-  ref     <- newIORef (AggState AnyVersion seed)-  errLast <- newIORef Nothing--  let takeFromQueue  = liftBase $ atomically $ Queue.readTBMQueue queue-      closeAggThread = liftBase $ atomically $ Queue.closeTBMQueue queue-      env            = AggEnv store aId--  _ <- Concurrent.fork $ whileJust_ takeFromQueue $ \(Msg action k) ->-         do s   <- readIORef ref-            res <- tryAny $ runAction action env s $ \s' r -> do-              atomicWriteIORef ref s'-              k r--            case res of-              Left e ->-                do atomicWriteIORef errLast (Just e)-                   liftBase $ atomically $ Queue.closeTBMQueue queue--              _ -> pure ()--  pure $ Agg env closeAggThread errLast $ \action k ->-    liftBase $ atomically $ Queue.writeTBMQueue queue (Msg action k)------------------------------------------------------------------------------------- | Creates an aggregate and replays its entire stream to rebuild its---   internal state.-loadAgg :: (Aggregate a, StreamId (Id a), DecodeEvent (Evt a), MonadBaseControl IO (M a))-        => SomeStore-        -> Id a-        -> a-        -> M a (Either ForEventFailure (Agg a))-loadAgg store aId seed = do-  agg <- newAgg store aId seed-  res <- execute agg (loadEventsAction aId)--  -- We don't need to let a running thread for nothing if we couldn't load the-  -- aggregate properly.-  case res of-    Left _ -> closeAgg agg-    _      -> pure ()--  pure (agg <$ res)------------------------------------------------------------------------------------- | Like 'loadAgg' but call 'loadAgg' in case of 'ForEventFailure' error.-loadOrCreateAgg :: (Aggregate a, StreamId (Id a), DecodeEvent (Evt a), MonadBaseControl IO (M a))-                => SomeStore-                -> Id a-                -> a-                -> M a (Agg a)-loadOrCreateAgg store aId seed = do-  agg <- newAgg store aId seed-  _   <- execute agg (loadEventsAction aId)-  pure agg------------------------------------------------------------------------------------- | Submits a command to the aggregate. If the command was valid, it returns--- an event otherwise an error. In case of a valid command, the aggregate--- persist the resulting event to the eventstore. The aggregate will also--- update its internal state accordingly.-submitCmd :: (Validate a, MonadBase IO (M a), StreamId (Id a), EncodeEvent (Evt a))-          => Agg a-          -> Cmd a-          -> M a (Decision a)-submitCmd agg cmd = execute agg (submitCmdAction cmd)------------------------------------------------------------------------------------- | Submits an event. The aggregate will update its internal state accondingly.-submitEvt :: (Aggregate a, MonadBase IO (M a)) => Agg a -> Evt a -> M a ()-submitEvt agg evt = execute agg (submitEvtAction evt)------------------------------------------------------------------------------------- | Returns current aggregate state.-snapshot :: MonadBase IO (M a) => Agg a -> M a a-snapshot agg = execute agg snapshotAction------------------------------------------------------------------------------------- | Uses usually by Root aggregates which usually have unusual workflow and---  make great use of a CPS-ed computation.---   http://blog.sapiensworks.com/post/2016/07/14/DDD-Aggregate-Decoded-1-route :: MonadBase IO (M a)-      => Agg a-      -> (SomeStore -> a -> (a -> r -> M a ()) -> M a ())-      -> M a r-route agg k = execute agg (routeAction k)------------------------------------------------------------------------------------- | Executes an action.-execute :: MonadBase IO (M a) => Agg a -> Action a r -> M a r-execute agg action = do-  var <- Concurrent.newEmptyMVar-  runAgg agg action (Concurrent.putMVar var)-  Concurrent.takeMVar var------------------------------------------------------------------------------------- | Persists an event to the eventstore.-persist :: (StreamId id, EncodeEvent event, MonadBase IO m)-        => SomeStore-        -> id-        -> ExpectedVersion-        -> event-        -> m EventNumber-persist store aid ver event =-  liftBase (appendEvent store (toStreamName aid) ver event >>= wait)------------------------------------------------------------------------------------- | Closes internal aggregate state.-closeAgg :: Agg a -> M a ()-closeAgg (Agg _ action _ _) = action------------------------------------------------------------------------------------- // Internal commands.----------------------------------------------------------------------------------submitCmdAction :: (Validate a, MonadBase IO (M a), StreamId (Id a), EncodeEvent (Evt a))-                => Cmd a-                -> Action a (Decision a)-submitCmdAction cmd = do-  env    <- askEnv-  s      <- getState-  result <- lift $ validate (aggState s) cmd--  for_ result $ \event ->-    do next <- lift $ persist (aggEnvStore env)-                              (aggEnvId env)-                              (aggStateVersion s)-                              event--       let s' = s { aggStateVersion = ExactVersion next }--       putState s'-       submitEvtAction event--  pure result-----------------------------------------------------------------------------------submitEvtAction :: (Aggregate a, Monad (M a)) => Evt a -> Action a ()-submitEvtAction event = do-  s  <- getState-  a' <- lift $ apply (aggState s) event-  let s' = s { aggState = a' }-  putState s'-----------------------------------------------------------------------------------snapshotAction :: Monad (M a) => Action a a-snapshotAction = fmap aggState getState-----------------------------------------------------------------------------------loadEventsAction :: (Aggregate a, StreamId (Id a), DecodeEvent (Evt a), MonadBase IO (M a))-                 => Id a-                 -> Action a (Either ForEventFailure a)-loadEventsAction aId = do-  seed <- getState-  env  <- askEnv--  let go num s event =-        do a <- apply (aggState s) event-           pure s { aggState        = a-                  , aggStateVersion = ExactVersion num-                  }--  res <- lift $ runExceptT-       $ foldEventsWithNumberM (aggEnvStore env) (toStreamName aId) go seed--  traverse_ putState res-  pure (fmap aggState res)-----------------------------------------------------------------------------------routeAction :: (SomeStore -> a -> (a -> r -> M a ()) -> M a ())-            -> Action a r-routeAction k = Action' $ \env s resp -> do-  k (aggEnvStore env) (aggState s) $ \a r ->-    let s' = s { aggState = a } in-    resp s' r--
− library/EventSource/Aggregate/Simple.hs
@@ -1,153 +0,0 @@-{-# LANGUAGE FunctionalDependencies #-}-{-# LANGUAGE MultiParamTypeClasses  #-}-{-# LANGUAGE TypeFamilies           #-}------------------------------------------------------------------------------------ |--- Module    :  EventSource.Aggregate.Simple--- Copyright :  (C) 2017 Yorick Laupa--- License   :  (see the file LICENSE)--- Maintainer:  Yorick Laupa <yo.eight@gmail.com>--- Stability :  experimental--- Portability: non-portable-------------------------------------------------------------------------------------module EventSource.Aggregate.Simple-  ( AggIO-  , AggregateIO(..)-  , ValidateIO(..)-  , Simple-  , newAgg-  , loadAgg-  , loadOrCreateAgg-  , submitCmd-  , submitEvt-  , closeAgg-  , snapshot-  , route-  ) where-----------------------------------------------------------------------------------import Control.Exception (SomeException)-----------------------------------------------------------------------------------import qualified EventSource.Aggregate as Self-import           EventSource------------------------------------------------------------------------------------- | Simple aggregate abstraction.-newtype Simple id command event state = Simple state------------------------------------------------------------------------------------- | A stream aggregate. An aggregate updates its internal based on the event---   it receives. You can read its current state by using 'snapshot'. If it---   supports validation, through 'Validated' typeclass, it can receive---   command and emits an event if the command was successful. Otherwise, it will---   yield an error. When receiving valid command, an aggregate will persist the---   resulting event. An aggregate is only responsible of its own stream.-type AggIO id command event state = Self.Agg (Simple id command event state)------------------------------------------------------------------------------------- | Represents a stream aggregate. An aggregate can rebuild its internal state---   by replaying all the stream's events that aggregate is responsible for.-class AggregateIO event state | event -> state where--  -- | Given current aggregate state, updates it according to the event the-  --   aggregate receives.-  applyIO :: state -> event -> IO state------------------------------------------------------------------------------------- | Represents an aggregate that support validation. An aggregate that supports---   validation can receive command and decide if it was valid or not. When the---   validation is successful, The aggregate emits an event that will be---   persisted and pass to 'apply' function.-class AggregateIO event state => ValidateIO command event state | command -> state, command -> event where--  -- | Validates a command. If the command validation succeeds, it will emits-  --   an event. Otherwise, it will returns an error.-  validateIO :: state -> command -> IO (Either SomeException event)-----------------------------------------------------------------------------------instance AggregateIO event state => Self.Aggregate (Simple id command event state) where-  type Id  (Simple id command event state) = id-  type Evt (Simple id command event state) = event-  type M   (Simple id command event state) = IO--  apply (Simple a) e = fmap Simple (applyIO a e)-----------------------------------------------------------------------------------instance ValidateIO command event state => Self.Validate (Simple id command event state) where-  type Cmd (Simple id command event state) = command-  type Err (Simple id command event state) = SomeException--  validate (Simple a) cmd = validateIO a cmd------------------------------------------------------------------------------------- | Creates a new aggregate given an eventstore handle, an id and an initial---   state.-newAgg :: AggregateIO event state-       => SomeStore-       -> id-       -> state-       -> IO (AggIO id command event state)-newAgg store aId seed = Self.newAgg store aId (Simple seed)------------------------------------------------------------------------------------- | Creates an aggregate and replays its entire stream to rebuild its---   internal state.-loadAgg :: (AggregateIO event state, Self.StreamId id, DecodeEvent event)-        => SomeStore-        -> id-        -> state-        -> IO (Either ForEventFailure (AggIO id command event state))-loadAgg store aId seed = Self.loadAgg store aId (Simple seed)------------------------------------------------------------------------------------- | Like 'loadAgg' but call 'loadAgg' in case of 'ForEventFailure' error.-loadOrCreateAgg :: (AggregateIO event state, Self.StreamId id, DecodeEvent event)-                => SomeStore-                -> id-                -> state-                -> IO (AggIO id command event state)-loadOrCreateAgg store aId seed = Self.loadOrCreateAgg store aId (Simple seed)------------------------------------------------------------------------------------- | Submits a command to the aggregate. If the command was valid, it returns---   an event otherwise an error. In case of a valid command, the aggregate---   persist the resulting event to the eventstore. The aggregate will also---   update its internal state accordingly.-submitCmd :: (ValidateIO command event state, Self.StreamId id, EncodeEvent event)-          => AggIO id command event state-          -> command-          -> IO (Either SomeException event)-submitCmd agg cmd = Self.submitCmd agg cmd------------------------------------------------------------------------------------- | Submits an event. The aggregate will update its internal state accondingly.-submitEvt :: AggregateIO event state-          => AggIO id command event state-          -> event-          -> IO ()-submitEvt agg event = Self.submitEvt agg event------------------------------------------------------------------------------------- | Returns current aggregate state.-snapshot :: AggIO id command event state -> IO state-snapshot agg = do-  Simple a <- Self.snapshot agg-  pure a------------------------------------------------------------------------------------- | Closes internal aggregate state.-closeAgg :: AggIO id command event state -> IO ()-closeAgg agg = Self.closeAgg agg------------------------------------------------------------------------------------- | Uses usually by Root aggregates which usually have unusual workflow and---  make great use of a CPS-ed computation.---   http://blog.sapiensworks.com/post/2016/07/14/DDD-Aggregate-Decoded-1-route :: AggIO id command event state-      -> (SomeStore -> state -> (state -> r -> IO ()) -> IO ())-      -> IO r-route agg k = Self.route agg $ \store (Simple state) resp ->-  k store state (\state' -> resp (Simple state'))-
library/EventSource/Store.hs view
@@ -14,73 +14,37 @@ -- -------------------------------------------------------------------------------- module EventSource.Store-  ( Batch(..)+  ( Batch'(..)+  , Batch   , Subscription(..)   , SubscriptionId   , ExpectedVersionException(..)   , Store(..)   , SomeStore(..)-  , StreamIterator-  , iteratorNext-  , iteratorNextEvent-  , iteratorReadAll-  , iteratorReadAllEvents-  , streamIterator   , freshSubscriptionId   , startFrom-  , nextEventAs-  , foldSub-  , foldSubAsync   , appendEvent-  , forEvents-  , forEventsWithNumber-  , foldEventsWithNumberM-  , foldEventsM-  , foldEvents-  , forSavedEvents-  , foldSavedEventsM-  , foldSavedEvents-  , foldSubSaved-  , foldSubSavedAsync-  , ForEventFailure(..)   , unhandled   ) where  ---------------------------------------------------------------------------------import Control.Monad (MonadPlus, mzero)-import Control.Exception (Exception, SomeException, toException, throwIO)-import Data.Bifunctor (first)-import Data.Foldable (for_)-import Data.Int (Int32)-import Data.Traversable (for)+import Control.Exception (Exception, throwIO)  ---------------------------------------------------------------------------------import Control.Concurrent.Async.Lifted (Async, async, wait)+import Control.Concurrent.Async.Lifted (Async) import Control.Monad.Base (MonadBase, liftBase)-import Control.Monad.Except (ExceptT, runExceptT, mapExceptT, throwError)-import Control.Monad.State (get, put, evalStateT)-import Control.Monad.Trans (lift)-import Control.Monad.Trans.Control (MonadBaseControl, StM)-import Data.IORef.Lifted (IORef, newIORef, atomicModifyIORef')-import Data.Text (Text)+import Control.Monad.Except (ExceptT, runExceptT) import Data.UUID (UUID) import Data.UUID.V4 (nextRandom)+import Streaming (hoist)+import Streaming.Prelude (Stream, Of)  -------------------------------------------------------------------------------- import EventSource.Types------------------------------------------------------------------------------------- | Represents batch information needed to read a stream.-data Batch =-  Batch { batchFrom :: EventNumber-        , batchSize :: Int32-        }+import EventSource.Store.Internal.Iterator (Batch'(..), startFrom)  ----------------------------------------------------------------------------------- | Starts a 'Batch' from a given point. The batch size is set to default,---   which is 500.-startFrom :: EventNumber -> Batch-startFrom from = Batch from 500+type Batch = Batch' EventNumber  -------------------------------------------------------------------------------- -- | Represents a subscription id.@@ -96,81 +60,11 @@ data Subscription =   Subscription { subscriptionId :: SubscriptionId -               , nextEvent :: forall m. MonadBase IO m-                           => m (Either SomeException SavedEvent)+               , subscriptionStream+                    :: forall m. MonadBase IO m => Stream (Of SavedEvent) m ()                }  ----------------------------------------------------------------------------------- | Waits for the next event and deserializes it on the go.-nextEventAs :: (DecodeEvent a, MonadBase IO m)-            => Subscription-            -> m (Either SomeException a)-nextEventAs sub = do-  res <- nextEvent sub-  let action = do-        event <- res-        first (toException . DecodeEventException)-          $ decodeEvent-          $ savedEvent event--  return action------------------------------------------------------------------------------------- | Folds over every event coming from the subscription until the end of the---   universe, unless an 'Exception' raises from the subscription.---   'SomeException' is used because we let the underlying subscription model---   exposed its own 'Exception'. If the callback that handles incoming events---   throws an exception, it will not be catch by the error callback.-foldSub :: (DecodeEvent a, MonadBase IO m)-        => Subscription-        -> (a -> m ())-        -> (SomeException -> m ())-        -> m ()-foldSub sub onEvent onError = loop-  where-    loop = do-      res <- nextEventAs sub-      case res of-        Left e -> onError e-        Right a -> onEvent a >> loop------------------------------------------------------------------------------------- | Asynchronous version of 'foldSub'.-foldSubAsync :: (MonadBaseControl IO m, DecodeEvent a)-             => Subscription-             -> (a -> m ())-             -> (SomeException -> m ())-             -> m (Async (StM m ()))-foldSubAsync sub onEvent onError =-  async $ foldSub sub onEvent onError------------------------------------------------------------------------------------- | Similar to 'foldSub' but provides access to the 'SavedEvent' instead of---   decoded event.-foldSubSaved :: MonadBase IO m-             => Subscription-             -> (SavedEvent -> m ())-             -> (SomeException -> m ())-             -> m ()-foldSubSaved sub onEvent onError = loop-  where-    loop = do-      res <- nextEvent sub-      case res of-        Left e -> onError e-        Right a -> onEvent a >> loop------------------------------------------------------------------------------------- | Asynchronous version of 'foldSubSaved'.-foldSubSavedAsync :: MonadBaseControl IO m-                  => Subscription-                  -> (SavedEvent -> m ())-                  -> (SomeException -> m ())-                  -> m (Async (StM m ()))-foldSubSavedAsync sub onEvent onError =-  async $ foldSubSaved sub onEvent onError---------------------------------------------------------------------------------- data ExpectedVersionException   = ExpectedVersionException     { versionExceptionExpected :: ExpectedVersion@@ -192,12 +86,12 @@                -> [a]                -> m (Async EventNumber) -  -- | Appends a batch of events at the end of a stream.-  readBatch :: MonadBase IO m-            => store-            -> StreamName-            -> Batch-            -> m (Async (ReadStatus Slice))+  -- | Reads a stream in a stream-processing fashion.+  readStream :: MonadBase IO m+             => store+             -> StreamName+             -> Batch+             -> Stream (Of SavedEvent) (ExceptT ReadFailure m) ()    -- | Subscribes to given stream.   subscribe :: MonadBase IO m => store -> StreamName -> m Subscription@@ -213,7 +107,7 @@ -------------------------------------------------------------------------------- instance Store SomeStore where   appendEvents (SomeStore store) = appendEvents store-  readBatch (SomeStore store)    = readBatch store+  readStream (SomeStore store)   = readStream store   subscribe (SomeStore store)    = subscribe store  --------------------------------------------------------------------------------@@ -227,248 +121,12 @@ appendEvent store stream ver a = appendEvents store stream ver [a]  ----------------------------------------------------------------------------------- | Represents failures that can occurs when using 'forEvents'.-data ForEventFailure-  = ForEventReadFailure ReadFailure-  | ForEventDecodeFailure Text-  deriving Show-----------------------------------------------------------------------------------instance Exception ForEventFailure------------------------------------------------------------------------------------- | Iterates over all events of stream given a starting point and a batch size.-forEvents :: (MonadBase IO m, DecodeEvent a, Store store)-          => store-          -> StreamName-          -> (a -> m ())-          -> ExceptT ForEventFailure m ()-forEvents store name k = forEventsWithNumber store name (const k)------------------------------------------------------------------------------------- | Iterates over all events of stream given a starting point and a batch size.---   It also passes the 'EventNumber' at each recursion step.-forEventsWithNumber :: (MonadBase IO m, DecodeEvent a, Store store)-                    => store-                    -> StreamName-                    -> (EventNumber -> a -> m ())-                    -> ExceptT ForEventFailure m ()-forEventsWithNumber store name k = do-  res <- streamIterator store name-  case res of-    ReadSuccess i -> loop i-    ReadFailure e -> throwError $ ForEventReadFailure e-  where-    loop i = do-      opt <- iteratorNext i-      for_ opt $ \saved ->-        case decodeEvent $ savedEvent saved of-          Left e -> throwError $ ForEventDecodeFailure e-          Right a -> lift (k (eventNumber saved) a) >> loop i------------------------------------------------------------------------------------- | Like 'forEvents' but expose signature similar to 'foldM'.-foldEventsM :: (MonadBase IO m, DecodeEvent a, Store store)-            => store-            -> StreamName-            -> (s -> a -> m s)-            -> s-            -> ExceptT ForEventFailure m s-foldEventsM store stream k seed =-  foldEventsWithNumberM store stream (const k) seed------------------------------------------------------------------------------------- | Like 'forEvents' but expose signature similar to 'foldM' and also passes---   the 'EventNumber' at each recursion step.-foldEventsWithNumberM :: (MonadBase IO m, DecodeEvent a, Store store)-            => store-            -> StreamName-            -> (EventNumber -> s -> a -> m s)-            -> s-            -> ExceptT ForEventFailure m s-foldEventsWithNumberM store stream k seed = mapExceptT trans action-  where-    trans m = evalStateT m seed--    action = do-      forEventsWithNumber store stream $ \num a -> do-        s <- get-        s' <- lift $ k num s a-        put s'-      get-------------------------------------------------------------------------------------- | Like 'foldEventsM' but expose signature similar to 'foldl'.-foldEvents :: (MonadBase IO m, DecodeEvent a, Store store)-           => store-           -> StreamName-           -> (s -> a -> s)-           -> s-           -> ExceptT ForEventFailure m s-foldEvents store stream k seed =-  foldEventsM store stream (\s a -> return $ k s a) seed------------------------------------------------------------------------------------- | Like `forEvents` but provides access to 'SavedEvent' instead of---   decoded event.-forSavedEvents :: (MonadBase IO m, Store store)-               => store-               -> StreamName-               -> (SavedEvent -> m ())-               -> ExceptT ForEventFailure m ()-forSavedEvents store name k = do-  res <- streamIterator store name-  case res of-    ReadSuccess i -> loop i-    ReadFailure e -> throwError $ ForEventReadFailure e-  where-    loop i = do-      opt <- iteratorNext i-      for_ opt $ \saved -> lift (k saved) >> loop i------------------------------------------------------------------------------------- | Like 'forSavedEvents' but expose signature similar to 'foldM'.-foldSavedEventsM :: (MonadBase IO m, Store store)-                 => store-                 -> StreamName-                 -> (s -> SavedEvent -> m s)-                 -> s-                 -> ExceptT ForEventFailure m s-foldSavedEventsM store stream k seed = mapExceptT trans action-  where-    trans m = evalStateT m seed--    action = do-      forSavedEvents store stream $ \a -> do-        s <- get-        s' <- lift $ k s a-        put s'-      get------------------------------------------------------------------------------------- | Like 'foldSavedEventsM' but expose signature similar to 'foldl'.-foldSavedEvents :: (MonadBase IO m, Store store)-                => store-                -> StreamName-                -> (s -> SavedEvent -> s)-                -> s-                -> ExceptT ForEventFailure m s-foldSavedEvents store stream k seed =-  foldSavedEventsM store stream (\s a -> return $ k s a) seed---------------------------------------------------------------------------------- -- | Throws an exception in case 'ExceptT' was a 'Left'.-unhandled :: (MonadBase IO m, Exception e) => ExceptT e m a -> m a-unhandled m = runExceptT m >>= \case-  Left e  -> liftBase $ throwIO e-  Right a -> pure a------------------------------------------------------------------------------------- | Allows to easily iterate over a stream's events.-newtype StreamIterator =-  StreamIterator { iteratorNext :: forall m. MonadBase IO m => m (Maybe SavedEvent) }-----------------------------------------------------------------------------------instance Show StreamIterator where-  show _ = "StreamIterator"------------------------------------------------------------------------------------- | Reads the next available event from the 'StreamIterator' and try to---   deserialize it at the same time.-iteratorNextEvent :: (DecodeEvent a, MonadBase IO m, MonadPlus m)-                  => StreamIterator-                  -> m (Maybe a)-iteratorNextEvent i = do-  res <- iteratorNext i-  case res of-    Nothing -> return Nothing-    Just s ->-      case decodeEvent $ savedEvent s of-        Left _ -> mzero-        Right a -> return $ Just a------------------------------------------------------------------------------------- | Reads all events from the 'StreamIterator' until reaching end of stream.-iteratorReadAll :: MonadBase IO m => StreamIterator -> m [SavedEvent]-iteratorReadAll i = do-  res <- iteratorNext i-  case res of-    Nothing -> return []-    Just s -> fmap (s:) $ iteratorReadAll i------------------------------------------------------------------------------------- | Like 'iteratorReadAll' but try to deserialize the events at the same time.-iteratorReadAllEvents :: (DecodeEvent a, MonadBase IO m, MonadPlus m)-                      => StreamIterator-                      -> m [a]-iteratorReadAllEvents i = do-  res <- iteratorNextEvent i-  case res of-    Nothing -> return []-    Just a -> fmap (a:) $ iteratorReadAllEvents i------------------------------------------------------------------------------------- | Returns a 'StreamIterator' for the given stream name. The returned---   'StreamIterator' IS NOT THREADSAFE.-streamIterator :: (Store store, MonadBase IO m)-               => store-               -> StreamName-               -> m (ReadStatus StreamIterator)-streamIterator store name = do-  w <- readBatch store name (startFrom 0)-  res <- liftBase $ wait w-  for res $ \slice -> do-    ref <- newIORef $ IteratorOverAvailable slice-    return $ StreamIterator $ iterateOver store ref name-----------------------------------------------------------------------------------data IteratorOverState-  = IteratorOverAvailable Slice-  | IteratorOverClosed-----------------------------------------------------------------------------------data IteratorOverAction-  = IteratorOverEvent SavedEvent-  | IteratorOverNextBatch EventNumber-  | IteratorOverEndOfStream-----------------------------------------------------------------------------------iterateOver :: (Store store, MonadBase IO m)-            => store-            -> IORef IteratorOverState-            -> StreamName-            -> m (Maybe SavedEvent)-iterateOver store ref name = go+unhandled :: (MonadBase IO m, Exception e)+          => Stream (Of a) (ExceptT e m) ()+          -> Stream (Of a) m ()+unhandled = hoist go   where-    go = do-      action <- atomicModifyIORef' ref $ \st ->-        case st of-          IteratorOverAvailable slice ->-            case sliceEvents slice of-              e:es ->-                let nextSlice = slice { sliceEvents = es }-                    nxtSt = IteratorOverAvailable nextSlice in-                (nxtSt, IteratorOverEvent e)-              [] | sliceEndOfStream slice-                   -> (IteratorOverClosed, IteratorOverEndOfStream)-                 | otherwise-                   -> let resp = IteratorOverNextBatch $-                                 sliceNextEventNumber slice in-                     (st, resp)-          IteratorOverClosed -> (st, IteratorOverEndOfStream)--      case action of-        IteratorOverEvent e -> return $ Just e-        IteratorOverEndOfStream -> return Nothing-        IteratorOverNextBatch num -> do-          w <- readBatch store name (startFrom num)-          res <- liftBase $ wait w-          case res of-            ReadFailure _ -> do-              atomicModifyIORef' ref $ \_ -> (IteratorOverClosed, ())-              return Nothing-            ReadSuccess slice -> do-              let nxtSt = IteratorOverAvailable slice-              atomicModifyIORef' ref $ \_ -> (nxtSt, ())-              go+    go m = runExceptT m >>= \case+      Left e  -> liftBase $ throwIO e+      Right a -> pure a
+ library/EventSource/Store/Internal/Iterator.hs view
@@ -0,0 +1,86 @@+{-# LANGUAGE FlexibleContexts #-}+--------------------------------------------------------------------------------+-- |+-- Module    :  EventSource.Store.Iterator+-- Copyright :  (C) 2018 Yorick Laupa+-- License   :  (see the file LICENSE)+-- Maintainer:  Yorick Laupa <yo.eight@gmail.com>+-- Stability :  experimental+-- Portability: non-portable+--+--------------------------------------------------------------------------------+module EventSource.Store.Internal.Iterator where++--------------------------------------------------------------------------------+import Data.Int (Int32)++--------------------------------------------------------------------------------+import Control.Concurrent.Async.Lifted (Async, wait)+import Control.Monad.Base (MonadBase, liftBase)+import Data.IORef.Lifted (IORef, atomicModifyIORef')++--------------------------------------------------------------------------------+import EventSource.Types++--------------------------------------------------------------------------------+-- | Represents batch information needed to read a stream.+data Batch' a =+  Batch' { batchFrom :: !a+         , batchSize :: !Int32+         }++--------------------------------------------------------------------------------+-- | Starts a 'Batch' from a given point. The batch size is set to default,+--   which is 500.+startFrom :: a -> Batch' a+startFrom from = Batch' from 500++--------------------------------------------------------------------------------+data IteratorOverState a+  = IteratorOverAvailable (Slice' a)+  | IteratorOverClosed++--------------------------------------------------------------------------------+data IteratorOverAction a+  = IteratorOverEvent SavedEvent+  | IteratorOverNextBatch a+  | IteratorOverEndOfStream++--------------------------------------------------------------------------------+iterateOver :: MonadBase IO m+            => IORef (IteratorOverState a)+            -> (Batch' a -> m (Async (ReadStatus (Slice' a))))+            -> m (Maybe SavedEvent)+iterateOver ref puller = go+  where+    go = do+      action <- atomicModifyIORef' ref $ \st ->+        case st of+          IteratorOverAvailable slice ->+            case sliceEvents slice of+              e:es ->+                let nextSlice = slice { sliceEvents = es }+                    nxtSt = IteratorOverAvailable nextSlice in+                (nxtSt, IteratorOverEvent e)+              [] | sliceEndOfStream slice+                   -> (IteratorOverClosed, IteratorOverEndOfStream)+                 | otherwise+                   -> let resp = IteratorOverNextBatch $+                                 sliceNext slice in+                     (st, resp)+          IteratorOverClosed -> (st, IteratorOverEndOfStream)++      case action of+        IteratorOverEvent e -> return $ Just e+        IteratorOverEndOfStream -> return Nothing+        IteratorOverNextBatch num -> do+          w <- puller (startFrom num)+          res <- liftBase $ wait w+          case res of+            ReadFailure _ -> do+              atomicModifyIORef' ref $ \_ -> (IteratorOverClosed, ())+              return Nothing+            ReadSuccess slice -> do+              let nxtSt = IteratorOverAvailable slice+              atomicModifyIORef' ref $ \_ -> (nxtSt, ())+              go
library/EventSource/Types.hs view
@@ -224,10 +224,10 @@ -------------------------------------------------------------------------------- -- | Encapsulates an event which is about to be saved. data Event =-  Event { eventType :: EventType-        , eventId :: EventId-        , eventPayload :: Data-        , eventMetadata :: Maybe Properties+  Event { eventType :: !EventType+        , eventId :: !EventId+        , eventPayload :: !Data+        , eventMetadata :: !(Maybe Properties)         } deriving Show  --------------------------------------------------------------------------------@@ -238,9 +238,9 @@ -------------------------------------------------------------------------------- -- | Represents an event that's saved into the event store. data SavedEvent =-  SavedEvent { eventNumber :: EventNumber-             , savedEvent :: Event-             , linkEvent :: Maybe Event+  SavedEvent { eventNumber :: !EventNumber+             , savedEvent :: !Event+             , linkEvent :: !(Maybe Event)              } deriving Show  --------------------------------------------------------------------------------@@ -250,13 +250,20 @@  -------------------------------------------------------------------------------- -- | Represents batch of events read from a store.-data Slice =-  Slice { sliceEvents :: [SavedEvent]-        , sliceEndOfStream :: Bool-        , sliceNextEventNumber :: EventNumber-        } deriving Show+data Slice' a =+  Slice' { sliceEvents :: ![SavedEvent]+         , sliceEndOfStream :: !Bool+         , sliceNext :: !a+         } deriving Show  --------------------------------------------------------------------------------+type Slice = Slice' EventNumber++--------------------------------------------------------------------------------+sliceNextEventNumber :: Slice -> EventNumber+sliceNextEventNumber = sliceNext++-------------------------------------------------------------------------------- -- | Deserializes a 'Slice''s events. sliceEventsAs :: DecodeEvent a => Slice -> Either Text [a] sliceEventsAs = traverse savedEventAs . sliceEvents@@ -326,6 +333,9 @@   | ReadError (Maybe Text)   | AccessDenied StreamName   deriving Show++--------------------------------------------------------------------------------+instance Exception ReadFailure  -------------------------------------------------------------------------------- instance Functor ReadStatus where
package.yaml view
@@ -9,7 +9,7 @@ - package.yaml - README.md ghc-options: -Wall-github: YoEight/eventsource-api+homepage: https://gitlab.com/YoEight/eventsource-api-hs library:   dependencies:     - base >=4.9 && <5@@ -29,6 +29,7 @@     - stm     - monad-loops ==0.4.*     - enclosed-exceptions+    - streaming   source-dirs: library license: BSD3 license-file: LICENSE.md@@ -36,4 +37,4 @@ maintainer: yo.eight@gmail.com name: eventsource-api synopsis: Provides an eventsourcing high level API.-version: '1.3.1'+version: '1.5.0'