packages feed

pgqueuer-hs-0.0.1.0: src/PGQueuer/Listener.hs

{-# LANGUAGE OverloadedStrings #-}

module PGQueuer.Listener (
    listen,
    unlisten,
    unlistenAll,
    getNextNotification,
    notifyJobCancellation,
    notifyHealthCheck,
    parseEventPayload,
) where

import Control.Monad (void)
import Data.Aeson ((.=))
import qualified Data.Aeson as Aeson
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import Data.Time (getCurrentTime)
import Data.UUID.V4 (nextRandom)
import Database.PostgreSQL.Simple (Connection, Only (..), execute, execute_)
import Database.PostgreSQL.Simple.Notification (Notification, getNotification)
import Database.PostgreSQL.Simple.Types (Query (..))
import PGQueuer.Types (Channel (..), defaultChannel)

-- | Listen for notifications on a specific channel
listen :: Connection -> T.Text -> IO ()
listen conn channel =
    void $ execute_ conn (Query $ TE.encodeUtf8 ("LISTEN " <> channel))

-- | Stop listening on a specific channel
unlisten :: Connection -> T.Text -> IO ()
unlisten conn channel =
    void $ execute_ conn (Query $ TE.encodeUtf8 ("UNLISTEN " <> channel))

-- | Stop listening on all channels
unlistenAll :: Connection -> IO ()
unlistenAll conn =
    void $ execute_ conn (Query "UNLISTEN *;")

-- | Get the next notification, blocking until available
getNextNotification :: Connection -> IO (Maybe Notification)
getNextNotification conn = Just <$> getNotification conn

sendNotification :: Connection -> Aeson.Value -> IO ()
sendNotification conn payload =
    void $ execute conn "NOTIFY pgqueuer, ?" (Only payloadText)
  where
    payloadText = TE.decodeUtf8 . BL.toStrict $ Aeson.encode payload

notificationChannel :: T.Text
notificationChannel = channelText
  where
    Channel channelText = defaultChannel

-- | Send a job cancellation notification
notifyJobCancellation :: Connection -> Int -> IO ()
notifyJobCancellation conn jobId = do
    sentAt <- getCurrentTime
    sendNotification
        conn
        ( Aeson.object
            [ "channel" .= notificationChannel
            , "sent_at" .= sentAt
            , "type" .= ("cancellation_event" :: T.Text)
            , "ids" .= ([jobId] :: [Int])
            ]
        )

-- | Send a health check notification
notifyHealthCheck :: Connection -> IO ()
notifyHealthCheck conn = do
    sentAt <- getCurrentTime
    healthCheckId <- nextRandom
    sendNotification
        conn
        ( Aeson.object
            [ "channel" .= notificationChannel
            , "sent_at" .= sentAt
            , "type" .= ("health_check_event" :: T.Text)
            , "id" .= healthCheckId
            ]
        )

-- | Parse event payload from notification
parseEventPayload :: BS.ByteString -> Either String String
parseEventPayload payload =
    case Aeson.eitherDecodeStrict' payload :: Either String Aeson.Value of
        Left err -> Left err
        Right _ -> Right (BS.unpack payload)