{- | Module : Hypermedia.Datastar.WAI Description : SSE streaming and request handling for WAI This module connects Datastar to WAI (Web Application Interface), Haskell's standard HTTP server interface. It provides: * 'sseResponse' — create a streaming SSE response with a 'ServerSentEventGenerator' callback * 'sendPatchElements', 'sendPatchSignals', 'sendExecuteScript' — send Datastar events through the open connection * 'readSignals' — decode signals sent by the browser (from query params on GET, or from the request body on POST) * 'isDatastarRequest' — distinguish Datastar SSE requests from normal page loads === Streaming architecture 'sseResponse' uses WAI's @responseStream@ to hold the HTTP connection open. You receive a 'ServerSentEventGenerator' and call @send*@ functions as many times as needed. The connection stays open until your callback returns (or the client disconnects). === Thread safety The 'ServerSentEventGenerator' uses an internal 'Control.Concurrent.MVar.MVar' lock, so it is safe to call @send*@ functions from multiple threads concurrently. === How signals flow from browser to server When the browser makes a Datastar request, signals are sent as JSON: * __GET requests__: signals are URL-encoded in the @datastar@ query parameter * __POST requests__: signals are in the request body as JSON Use 'readSignals' with any 'Data.Aeson.FromJSON' instance to decode them. -} module Hypermedia.Datastar.WAI where import Control.Concurrent.MVar import Control.Exception import Data.Text (Text) import Data.Text.Encoding qualified as TE import Data.Aeson (FromJSON) import Data.Aeson qualified as A import Data.ByteString.Builder qualified as BSB import Network.HTTP.Types qualified as WAI import Network.Wai qualified as WAI import Hypermedia.Datastar.Types import Hypermedia.Datastar.ExecuteScript qualified as ES import Hypermedia.Datastar.PatchElements qualified as PE import Hypermedia.Datastar.PatchSignals qualified as PS import Hypermedia.Datastar.Logger qualified as Logger import qualified Hypermedia.Datastar.Logger as Logger {- | An opaque handle for sending SSE events to the browser. Obtain one from the callback passed to 'sseResponse'. The handle is thread-safe — you can send events from multiple threads concurrently. You don't construct these directly; 'sseResponse' creates one for you. -} data ServerSentEventGenerator = ServerSentEventGenerator { sseWrite :: BSB.Builder -> IO () , sseFlush :: IO () , sseLock :: MVar () , sseLogger :: Logger.DatastarLogger } {- | Create a WAI 'WAI.Response' that streams SSE events. The callback receives a 'ServerSentEventGenerator' for sending events. The SSE connection stays open until the callback returns. @ app :: WAI.Request -> (WAI.Response -> IO b) -> IO b app req respond = respond $ sseResponse $ \\gen -> do sendPatchElements gen (patchElements "\