packages feed

kiroku-store-0.1.0.0: src/Kiroku/Store/Subscription/Stream.hs

module Kiroku.Store.Subscription.Stream (
    subscriptionStream,
)
where

import Control.Concurrent.STM (TBQueue, atomically, newTBQueueIO, readTBQueue, writeTBQueue)
import Kiroku.Store.Connection (KirokuStore)
import Kiroku.Store.Subscription (subscribe)
import Kiroku.Store.Subscription.Types (
    SubscriptionConfig,
    SubscriptionConfigM (..),
    SubscriptionHandleM (..),
    SubscriptionResult (..),
 )
import Kiroku.Store.Types (RecordedEvent)
import Numeric.Natural (Natural)
import Streamly.Data.Stream (Stream)
import Streamly.Data.Stream qualified as Stream

{- | Create a pull-based 'Stream' from a kiroku subscription.

A bounded 'TBQueue' sits between kiroku's push-based handler and the
returned Streamly stream. The handler writes @Just event@ into the queue
and returns 'Continue'. A @Nothing@ sentinel signals end-of-stream.
The stream terminates when it reads @Nothing@.

The @handler@ field in the provided 'SubscriptionConfig' is ignored —
the bridge provides its own handler.

The returned cancel action cancels the underlying subscription and
writes the sentinel so any blocked reader wakes up and terminates.
-}
subscriptionStream ::
    KirokuStore ->
    SubscriptionConfig ->
    -- | TBQueue capacity (buffer size for backpressure)
    Natural ->
    IO (Stream IO RecordedEvent, IO ())
subscriptionStream store config bufferSize = do
    queue <- newTBQueueIO bufferSize

    let bridgeHandler :: RecordedEvent -> IO SubscriptionResult
        bridgeHandler event = do
            atomically $ writeTBQueue queue (Just event)
            pure Continue

    let bridgeConfig =
            config
                { handler = bridgeHandler
                }

    subHandle <- subscribe store bridgeConfig

    let cancelAction = do
            cancel subHandle
            atomically $ writeTBQueue queue Nothing

    let stream = Stream.unfoldrM (step queue) ()

    pure (stream, cancelAction)
  where
    step :: TBQueue (Maybe RecordedEvent) -> () -> IO (Maybe (RecordedEvent, ()))
    step queue () = do
        mEvent <- atomically $ readTBQueue queue
        case mEvent of
            Just event -> pure (Just (event, ()))
            Nothing -> pure Nothing