slaveOftime
webssehtmxblazorfsharpdotnet

Explorer Server Sent Event (SSE)

2024-11-22

I never heard about Server Sent Event (SSE) before, but in dotnet 9 there is an official support for SSE, so I want to explore this technology.

WIKI: Server-Sent Events (SSE) is a server push technology enabling a client to receive automatic updates from a server via an HTTP connection, and describes how servers can initiate data transmission towards clients once an initial client connection has been established. They are commonly used to send message updates or continuous data streams to a browser client and designed to enhance native, cross-browser streaming through a JavaScript API called EventSource, through which a client requests a particular URL in order to receive an event stream. The EventSource API is standardized as part of HTML Living Standard[1] by the WHATWG. The media type for SSE is text/event-stream.

Looks like it can be very useful for some real-time use cases. And I remember when I built Fun.Htmx, I build an extension for it hx-sse. And it is quit raw, I never used it.

I am wandering, if I can build a more simpler model so I can grab and use it with no brain.

The SSE has couple of field for streaming: Event, Data, Id etc. based on the spec. I can build a live counter, when the network is broken, it will automatically retry and continue the counter after reconnected.

As a library consumer, I would like to write it like:

  1. Write a loop for streaming the UI node one by one.
  2. Get the of Last-Event-Id, so I can restore the counter after reconnected.
  3. Can send UI node with different event name, so from the client, I can share the same http request but render the UI node differently.

Below is the live demo:

    HtmxSseLiveCounter

    type HtmxSseLiveCounter() as this =
        inherit HxSseComponent()
    
        static member CountEventName = "count"
    
        // 1. Has a loop for streaming the UI node one by one with taskSeq, so the UI node is in a async stream
        override _.GetNodes() = taskSeq {
            let endIndex = 10000
    
            let startIndex =
                // 2. Get the benefit of Last-Event-Id, so I can restore the counter after reconnected.
                match this.LastEventId with
                | null -> 0
                | INT32 x -> if x >= endIndex then 0 else x + 1
                | _ -> 0
    
    
            p {
                class' "text-warning"
                "Counter started. Max value will be "; endIndex; ". Start count from "; startIndex
            }
    
            
            for i in startIndex .. endIndex do
                // 3. Can send UI node with different event name, so from the client, I can share the same http request but render the UI node differently.
                this.EventId <- string i
                this.EventName <- HtmxSseLiveCounter.CountEventName
                li { "count="; i; " at "; DateTime.Now.ToString() }
                do! Async.Sleep 1000
    
    
            // Set back to the default event name
            this.EventName <- HxSseComponent.NewNodeEventName
            p { "Counter ended" }
        }
    

    Below is code for consuming the counter

    Consume HtmxSseLiveCounter

    div {
        class' "p-3 mb-4 shadow-sm bg-slate-500 rounded-md"
        hxSseConnectComp (QueryBuilder<HtmxSseLiveCounter>())
        hxSseCloseOnComp
        // Get the UI node with default event name
        div { hxSseSwapOnComp }
        // Get the UI node with a different event name: count. And insert the latest at top.
        ul {
            class' "max-h-52 overflow-y-auto"
            hxSseSwap HtmxSseLiveCounter.CountEventName
            hxSwap_afterbegin
        }
    }
    

    Summary

    Fun.Htmx is very flexible for building related model/API to achieve the goal, the result is very satisfying.

    Regarding to SSE, one obvious thing is it is very light and efficient and can automatically retry. It is just a normal http but with enhanced features from various clients, like browsers, dotnet's HttpClient etc. It can be so useful like streaming some data which need a period of time, like ask ChatGPT a question, the dialog will start a stream and send back with token or blocks continually untill it is done. Even the network is broken, the server can still finish the token generation and put them into the cache and after the network is back, it can send the cached value.

    The Last-Event-Id is just a string of data, so we can put a lot of state into it, like the datetime, id etc. So we can check if we should continue based on the id, or based on the datetime, if we should expire the continuation etc.

    Do you like this post?