Skip to content

lowbit-dev/sse

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

SSE

Go Report Card Go Reference License: MIT

SSE is a minimal, zero-magic Server-Sent Events library for Go. It separates the protocol formatting from the HTTP transport, ensuring zero-allocation writes and explicit connection lifecycles. Built strictly on the standard library, it favors mechanical sympathy and composability over framework-like conveniences.

  • Zero-allocation writes — formats directly into the HTTP buffer without intermediate strings
  • Explicit concurrency — background heartbeats are context-aware and shut down cleanly
  • Standard library native — composes directly with io.Reader and http.ResponseWriter
  • Zero dependencies

Install

go get lowbit.dev/sse

An Event

An Event is a plain data struct:

type Event struct {
    Name       string
    ID         string
    Retry      time.Duration
    Data       string
    Extensions map[string]string
}

It holds pure data. The library does not assume your payload is JSON or any other format. If you need to send structured data, marshal it before assignment.

Usage

Server

Wrap an http.ResponseWriter with an Emitter to safely manage concurrent writes and flushes. Pass the request context to the background heartbeat so it cleans up automatically when the client disconnects.

func StreamHandler(w http.ResponseWriter, r *http.Request) {
    emitter, err := sse.NewEmitter(w)
    if err != nil {
        http.Error(w, "Streaming unsupported", http.StatusInternalServerError)
        return
    }

    // Keep the connection alive; automatically stops when r.Context() is canceled
    go emitter.ServeHeartbeats(r.Context(), 15*time.Second)

    ticker := time.NewTicker(1 * time.Second)
    defer ticker.Stop()

    for {
        select {
        case <-r.Context().Done():
            return // Client disconnected
            
        case <-ticker.C:
            _, err := emitter.Emit(sse.Event{
                Name: "ping",
                Data: "pong",
            })
            if err != nil {
                return // Socket dead
            }
        }
    }
}

Client

Pass any io.Reader (like an http.Response.Body) to the Reader. The parsing loop is entirely in your control, allowing you to handle timeouts and cancellations via standard HTTP client contexts.

req, _ := http.NewRequestWithContext(ctx, "GET", "http://example.com/stream", nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
    log.Fatal(err)
}
defer resp.Body.Close()

reader := sse.NewReader(resp.Body)

for {
    event, err := reader.Read()
    if err != nil {
        if errors.Is(err, io.EOF) {
            // Clean disconnect
            break
        }
        // Handle error
        break
    }
    
    fmt.Printf("Event: %s, Data: %s\n", event.Name, event.Data)
}

Core Types

sse splits responsibilities to avoid massive heap allocations and to keep connection state explicitly separated from pure data.

Type Role
Emitter Safely manages concurrent writes, flushes, and background heartbeats for an http.ResponseWriter.
Writer Handles the zero-allocation protocol formatting to any underlying io.Writer.
Reader Wraps an io.Reader for allocation-efficient event parsing.
Event The raw data container mapping directly to the SSE specification.

Errors

Error Effect
ErrStreamingUnsupported Returned by NewEmitter if the provided http.ResponseWriter does not implement http.Flusher.
io.EOF Returned by Reader.Read() when the server sends a clean termination (FIN packet).

Both errors behave predictably. If Emitter.Emit() fails, it will return the standard library's network error (e.g., broken pipe), indicating a dead connection that should be dropped.

About

Minimal Server-Sent Events primitives for Go

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages