cqrs-example-0.10.0: src/CQRSExample/Main.hs
module Main ( main ) where
import Control.Concurrent (forkIO, threadDelay)
import Control.Concurrent.Async (concurrently)
import Control.Concurrent.STM (atomically, TChan)
import qualified Control.Concurrent.STM.TChan as C
import Control.Concurrent.STM.TVar (TVar, newTVarIO)
import Control.Monad (void, forever, when)
import Data.CQRS.Memory (newEventStore, newArchiveStore, newStorage)
import Data.CQRS.SnapshotStore (nullSnapshotStore)
import qualified Data.CQRS.Repository as R
import Data.CQRS.Types.PersistedEvent
import Data.CQRS.Types.ArchiveStore (ArchiveStore, enumerateAllEvents)
import Data.UUID.Types (UUID)
import Network.Wai.EventSource (ServerEvent(..))
import qualified System.IO.Streams as Streams
import System.IO.Streams (InputStream)
import System.Random (randomIO)
import Web.Scotty (scotty)
import CQRSExample.AggregateAction (aggregateAction)
import CQRSExample.Events
import CQRSExample.Notifications
import CQRSExample.Query
import CQRSExample.Routing
-- Drain an input stream, applying an IO action to each element.
drain :: InputStream a -> (a -> IO ()) -> IO ()
drain inputStream f = go
where
go = do
a <- Streams.read inputStream
case a of
Just a' -> f a' >> go
Nothing -> return ()
-- Source of refresh events to the browser. Never returns.
eventSourcingThread :: TVar QueryState -> ArchiveStore Event -> TChan ServerEvent -> TChan (UUID, [PersistedEvent Event]) -> IO ()
eventSourcingThread qs archiveStore serverEvents publishedEvents = do
-- Start two threads. One thread just periodically traverses the
-- whole archive set once in a while. For production you would want
-- to track which archives have already been completely processed by
-- the Query state and stop traversal at that point. The other
-- thread just processes events published by the repository. This
-- latter thread never terminates unless an exception is thrown.
void $ concurrently traverseArchives processPublishedEvents
where
traverseArchives :: IO ()
traverseArchives = do
putStrLn "Traversing archives..."
-- Traverse and process all the events.
enumerateAllEvents archiveStore $ \inputStream -> do
drain inputStream processEvent
-- Go again after a while.
threadDelay 30000000
traverseArchives
processEvent :: (UUID, PersistedEvent Event) -> IO ()
processEvent (aggregateId, ev) = do
processEvents aggregateId [ev]
processPublishedEvents :: IO ()
processPublishedEvents = do
-- Main event-sourcing loop; never terminates
forever $ do
-- Wait for events to be published.
(aggregateId, evs) <- atomically $ C.readTChan publishedEvents
processEvents aggregateId evs
processEvents :: UUID -> [PersistedEvent Event] -> IO ()
processEvents aggregateId evs = do
-- Supply to Query to update its state.
runQuery qs $ reactToEvents aggregateId evs
-- Send out a server event if we have any notifications.
let notifications = updateNotifications (aggregateId, evs) mempty
when (notifications /= mempty) $ do
atomically $ C.writeTChan serverEvents $! toServerEvent $ notifications
-- Start serving the application.
startServing :: IO ()
startServing = do
qState <- newTVarIO newQueryState
-- Queue of json events to send to browser.
serverEvents <- atomically $ C.newBroadcastTChan
-- Events published by query module.
publishedEvents <- atomically $ C.newTChan
-- Publish events
let publishEvents events = atomically $ C.writeTChan publishedEvents events
-- Create memory CQRS backing storage
storage <- newStorage
eventStore <- newEventStore storage
archiveStore <- newArchiveStore randomIO storage
-- Create the resository
let repositorySettings = R.setSnapshotFrequency 10 $ R.defaultSettings
let repository = R.newRepository repositorySettings aggregateAction eventStore nullSnapshotStore publishEvents randomIO
-- Start sourcing events.
void $ forkIO $ do
eventSourcingThread qState archiveStore serverEvents publishedEvents
-- Web serving thread.
void $ forkIO $ do
scotty 8000 $ routes qState repository serverEvents
main :: IO ()
main = do
putStrLn "Starting..."
startServing
putStrLn "Press <Enter> to quit"
_ <- getLine
return ()