content: {-# LANGUAGE FlexibleContexts    #-}
  
  module Guide.Session
  (
    newAcidSessionStore
  )
  where
  
  import Imports
  
  -- Spock
  import Web.Spock.Config
  -- acid-state
  import Data.Acid as Acid
  
  import Guide.State
  import Guide.Types.Session
  
  -- |Queries for all user sessions and then removes sessions unless predicate matches.
  filterSessions :: AcidState GlobalState -> (SpockSession conn st -> Bool) -> IO ()
  filterSessions db p = do
    sessions <- Acid.query db GetSessions
    for_ sessions $ \sess -> do
      unless (p $ unwrapSession sess) $
        Acid.update db $ DeleteSession (sess ^. sess_id)
  
  -- |Queries for all user sessions and then performs an operation over all.
  mapSessions :: MonadIO m => AcidState GlobalState -> (SpockSession conn st -> m (SpockSession conn st)) -> m ()
  mapSessions db f = do
    sessions <- liftIO $ Acid.query db GetSessions
    for_ sessions $ \sess -> do
      newSess <- f (unwrapSession sess)
      liftIO $ Acid.update db $ StoreSession (wrapSession newSess)
  
  -- |Wraps an STM session store and periodically commits session to the database.
  newAcidSessionStore' :: AcidState GlobalState -> IO (SessionStore (SpockSession conn st) IO)
  newAcidSessionStore' db = do
    -- See Note [Session Linearizability]
    lock <- newMVar ()
    return $ SessionStore {
      ss_runTx = withMVar lock . const,
      ss_loadSession = \sessId -> do
        sess <- Acid.query db $ LoadSession sessId
        return $ unwrapSession <$> sess,
      ss_deleteSession = Acid.update db . DeleteSession,
      ss_storeSession = Acid.update db . StoreSession . wrapSession,
      ss_toList = do
        sessions <- Acid.query db GetSessions
        return $ map unwrapSession sessions,
      ss_filterSessions = filterSessions db,
      ss_mapSessions = mapSessions db
      }
  
  newAcidSessionStore :: AcidState GlobalState -> IO (SessionStoreInstance (SpockSession conn st))
  newAcidSessionStore db = SessionStoreInstance <$> newAcidSessionStore' db
  
  {- Note [Session Linearizability]
  
  Acid-State transactions are, I believe, serializable by default.
  Updates can be issued in parallel, and the temporal ordering of each update
  can vary, but each atomic update can be executed in arbitrary order.
  
  Acid-state may also be sequentially consistent, not sure. It's definitely
  not linearizable, which is a property we really want for session data
  types. In other words, we can have data races.
  
  Consider two actions taken by an administrator:
  
  * Administrator updates user profiles to remove access rights,
    running GetSession and then StoreSession, via filterSessions or mapSessions.
  
  * Eve at the same time updates their user profile to change their user name,
    running LoadSession and then StoreSession.
  
  Since filterSession is not atomic, this sequence could occur:
  
  |   Process   |     Command   | Context
  |    Admin    |  GetSessions  | mapSessions runs GetSessions, obtaining a list of all sessions
  |     Eve     |  LoadSession  | user profile page view
  |    Admin    |  StoreSession | mapSessions runs StoreSession for Eve, removing permissions
  |     Eve     |  StoreSession | Eve clicks "save profile" which refreshes her session
  
  This is a classic race condition. So we use a lock on the Session Store.
  
  -}
  