{-# LANGUAGE NamedFieldPuns  #-}
{-# LANGUAGE RecordWildCards #-}
{-|
Module: Internal.TCloseQ
Description: Transactional queues with a close operation.

This module provides a thin layer over 'TQueue', which affords "closing"
queues. Reading from a closed queue returns 'Nothing'.
-}
module Internal.TCloseQ
    ( Q
    , ErrClosed(..)
    , new
    , read
    , write
    , close
    ) where

import Prelude hiding (read)

import Control.Concurrent.STM

import Control.Exception (Exception)
import Control.Monad     (unless, when)
import Data.Maybe        (isNothing)

-- | A Queue with a close operation, with element type @a@.
data Q a = Q
    { q        :: TQueue (Maybe a)
    , isClosed :: TVar Bool
    }

-- | An exception which is thrown if a caller tries to write to a closed queue.
data ErrClosed = ErrClosed
    deriving(Show)
instance Exception ErrClosed

-- | Create a new empty queue.
new :: STM (Q a)
new = do
    q <- newTQueue
    isClosed <- newTVar False
    pure Q{..}

-- | Read a value from the queue. Returns Nothing if the queue is closed.
read :: Q a -> STM (Maybe a)
read Q{q} = do
    ret <- readTQueue q
    when (isNothing ret) $
        -- put it back in, so future reads also return nothing:
        writeTQueue q ret
    pure ret

-- | Write a value to the queue, which must not be closed. If it is closed,
-- this will throw 'ErrClosed'.
write :: Q a -> a -> STM ()
write Q{q, isClosed} v = do
    c <- readTVar isClosed
    when c $ throwSTM ErrClosed
    writeTQueue q (Just v)

-- | Close a queue. It is safe to close a queue more than once; subsequent
-- closes will have no effect.
close :: Q a -> STM ()
close Q{q, isClosed} = do
    c <- readTVar isClosed
    unless c $ do
        writeTVar isClosed True
        writeTQueue q Nothing