packages feed

tricorder-0.1.0.0: src/Tricorder/Effects/SessionStore.hs

module Tricorder.Effects.SessionStore
    ( SessionStore (..)
    , SessionStoreReloaded (..)
    , get
    , rawReload
    , withSession
    , withSubSession
    , ActiveSession (..)
    , Reloader (..)
    , runSessionStore
    , runSessionStoreConst
    ) where

import Atelier.Config (LoadedConfig)
import Atelier.Effects.Chan (Chan)
import Atelier.Effects.Conc (Conc)
import Atelier.Effects.FileSystem (FileSystem)
import Atelier.Effects.Log (Log)
import Atelier.Effects.Publishing (Pub, Sub, publish)
import Effectful (Effect)
import Effectful.Concurrent (Concurrent)
import Effectful.Dispatch.Dynamic (interpret_, reinterpretWith_)
import Effectful.Reader.Static (Reader)
import Effectful.TH (makeEffect)
import Relude.Extra.Tuple (dup)

import Atelier.Effects.Conc qualified as Conc
import Atelier.Effects.Iterator qualified as Iter
import Atelier.Effects.Publishing qualified as Sub
import Effectful.State.Static.Shared qualified as State

import Tricorder.Runtime (ProjectRoot)
import Tricorder.Session (Session, loadSession)


data SessionStore :: Effect where
    Get :: SessionStore m Session
    RawReload :: SessionStore m ()


makeEffect ''SessionStore


data ActiveSession es = ActiveSession
    { session :: Session
    , reloader :: Reloader es
    }


newtype Reloader es = Reloader {reload :: Eff es ()}


-- | Operate on the most recent 'Session' stored. Whenever the session is
-- reloaded, the passed action is cancelled, and started again with the new
-- 'Session'.
withSession
    :: ( Conc :> es
       , SessionStore :> es
       , Sub SessionStoreReloaded :> es
       )
    => (ActiveSession es -> Eff es a)
    -> Eff es Void
withSession act =
    Conc.restartableForkWith (void $ Sub.listenOnce_ @SessionStoreReloaded) get \session ->
        act ActiveSession {session, reloader = Reloader rawReload}


-- | Tracks the parts of 'Session' that the caller cares about, and restarts
-- the passed function whenever those parts change based on the 'subSession's
-- 'Eq' instance.
withSubSession
    :: forall subSession es a
     . ( Chan :> es
       , Conc :> es
       , Concurrent :> es
       , Eq subSession
       , SessionStore :> es
       , Sub SessionStoreReloaded :> es
       )
    => (Session -> subSession)
    -> Session
    -> (Reloader es -> subSession -> Eff es a)
    -> Eff es Void
withSubSession mkSubSession initialSession action =
    Iter.fromEvents @SessionStoreReloaded \iter ->
        let initialSubSession = mkSubSession initialSession
            subIter =
                Iter.changes
                    initialSubSession
                    (fmap (\(SessionStoreReloaded s) -> mkSubSession s) iter)
        in  Conc.restartableForkLoop
                initialSubSession
                (Iter.next subIter)
                \cfg -> action (Reloader rawReload) cfg


data SessionStoreReloaded = SessionStoreReloaded Session


runSessionStore
    :: ( FileSystem :> es
       , Log :> es
       , Pub SessionStoreReloaded :> es
       , Reader LoadedConfig :> es
       , Reader ProjectRoot :> es
       )
    => Eff (SessionStore : es) a -> Eff es a
runSessionStore act = do
    initialSession <- loadSession
    reinterpretWith_ (State.evalState initialSession) act \case
        Get -> State.get
        RawReload -> do
            session <- State.stateM (\_ -> dup <$> loadSession)
            publish $ SessionStoreReloaded session


runSessionStoreConst :: Session -> Eff (SessionStore : es) a -> Eff es a
runSessionStoreConst session = interpret_ \case
    Get -> pure session
    RawReload -> pure ()