-- | The \"cache\" splice ensures that its contents are cached and only -- evaluated periodically. The cached contents are returned every time the -- splice is referenced. -- -- Use the ttl attribute to set the amount of time between reloads. The ttl -- value should be a positive integer followed by a single character -- specifying the units. Valid units are seconds, minutes, hours, days, and -- weeks. If the ttl string is invalid or the ttl attribute is not specified, -- the cache is never refreshed unless explicitly cleared with -- clearCacheTagState. module Text.Templating.Heist.Splices.Cache ( CacheTagState , mkCacheTag , clearCacheTagState ) where ------------------------------------------------------------------------------ import Control.Concurrent import Control.Monad import Control.Monad.Trans import Data.IORef import qualified Data.HashMap.Strict as H import Data.HashMap.Strict (HashMap) import qualified Data.HashSet as Set import Data.Text (Text) import qualified Data.Text as T import Data.Text.Read import Data.Time.Clock import System.Random import Text.XmlHtml.Cursor import Text.XmlHtml hiding (Node) ------------------------------------------------------------------------------ import Text.Templating.Heist.Internal import Text.Templating.Heist.Types ------------------------------------------------------------------------------ cacheTagName :: Text cacheTagName = "cache" ------------------------------------------------------------------------------ -- | State for storing cache tag information newtype CacheTagState = CTS (MVar (HashMap Text (UTCTime, Template))) ------------------------------------------------------------------------------ -- | Clears the cache tag state. clearCacheTagState :: CacheTagState -> IO () clearCacheTagState (CTS cacheMVar) = modifyMVar_ cacheMVar (const $ return H.empty) ------------------------------------------------------------------------------ -- | Converts a TTL string into an integer number of seconds. parseTTL :: Text -> Int parseTTL s = value * multiplier where value = either (const 0) fst $ decimal s multiplier = case T.last s of 's' -> 1 'm' -> 60 'h' -> 3600 'd' -> 86400 'w' -> 604800 _ -> 0 ------------------------------------------------------------------------------ cacheImpl :: (MonadIO m) => CacheTagState -> HeistT m Template cacheImpl (CTS mv) = do tree <- getParamNode let err = error $ unwords ["cacheImpl is bound to a tag" ,"that didn't get an id attribute." ," This should never happen."] let i = maybe err id $ getAttribute "id" tree ttl = maybe 0 parseTTL $ getAttribute "ttl" tree mp <- liftIO $ readMVar mv (mp',ns) <- do cur <- liftIO getCurrentTime let mbn = H.lookup i mp reload = do nodes' <- runNodeList $ childNodes tree return $! (H.insert i (cur,nodes') mp, nodes') case mbn of Nothing -> reload (Just (lastUpdate,n)) -> do if ttl > 0 && tagName tree == Just cacheTagName && diffUTCTime cur lastUpdate > fromIntegral ttl then reload else do stopRecursion return $! (mp,n) liftIO $ modifyMVar_ mv (const $ return mp') return ns ------------------------------------------------------------------------------ -- | Returns a function that modifies a HeistState to include a \"cache\" -- tag. The cache tag is not bound automatically with the other default Heist -- tags. This is because this function also returns CacheTagState, so the -- user will be able to clear it with the 'clearCacheTagState' function. mkCacheTag :: MonadIO m => IO (HeistState m -> HeistState m, CacheTagState) mkCacheTag = do sr <- newIORef $ Set.empty mv <- liftM CTS $ newMVar H.empty return $ ( addOnLoadHook (assignIds sr) . -- The cache tag allows the ttl attribute. bindSplice cacheTagName (cacheImpl mv) . -- Like the old static tag...does not allow ttl bindSplice "static" (cacheImpl mv) , mv) where generateId :: IO Int generateId = getStdRandom random assignIds setref = mapM f where f node = g $ fromNode node getId = do i <- liftM (T.pack . show) generateId st <- readIORef setref if Set.member i st then getId else do writeIORef setref $ Set.insert i st return $ T.append "cache-id-" i g curs = do let node = current curs curs' <- if tagName node == Just cacheTagName || tagName node == Just "static" then do i <- getId return $ modifyNode (setAttribute "id" i) curs else return curs let mbc = nextDF curs' maybe (return $ topNode curs') g mbc