Skip to content

Commit

Permalink
Initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
softwarespot committed Oct 20, 2024
0 parents commit c5de052
Show file tree
Hide file tree
Showing 15 changed files with 826 additions and 0 deletions.
23 changes: 23 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This workflow will build a golang project
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go

name: Go
on: [push, pull_request]

jobs:
test:
strategy:
matrix:
go-version: [1.23.x]

runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}

- name: Test
run: go test -cover -v ./...
26 changes: 26 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Taken from URL: https://github.com/github/gitignore/blob/main/Go.gitignore
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, built with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out

# Dependency directories (remove the comment below to include it)
# vendor/

# Go workspace file
go.work
go.work.sum

# env file
.env
20 changes: 20 additions & 0 deletions LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
The MIT License (MIT)

Copyright (c) 2024 SoftwareSpot Apps

Permission is hereby granted, free of charge, to any person obtaining a copy of
this software and associated documentation files (the "Software"), to deal in
the Software without restriction, including without limitation the rights to
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
4 changes: 4 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
test:
go test -cover -v ./...

.PHONY: test
55 changes: 55 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Server-Sent Events (SSE) handler

[![Go Reference](https://pkg.go.dev/badge/github.com/softwarespot/sse.svg)](https://pkg.go.dev/github.com/softwarespot/sse) ![Go Tests](https://github.com/softwarespot/replay/actions/workflows/go.yml/badge.svg)

**Server-Sent Events (SSE) handler** is a generic and [http.Handler](https://pkg.go.dev/net/http#Handler) compliant module, that implements real-time event streaming from a server to web clients using the [Server-Sent Events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) protocol. It's a robust module for managing multiple client connections, broadcasting events, and handling client registrations and unregistrations efficiently.

Examples of using this module can be found from the [./examples](./examples/) directory.

## Prerequisites

- Go 1.23.0 or above

## Installation

```bash
go get -u github.com/softwarespot/sse
```

## Usage

A basic example of using **SSE**.

```Go
package main

import (
"fmt"
"net/http"
"time"

"github.com/softwarespot/sse"
)

func main() {
// Use the default configuration
h := sse.New[int](nil)
defer h.Close()

go func() {
var evt int
for {
fmt.Println("sse handler: broadcast event", h.Broadcast(evt))
time.Sleep(64 * time.Millisecond)
evt++
}
}()

http.Handle("/events", h)
http.ListenAndServe(":3000", nil)
}
```

## License

The code has been licensed under the [MIT](https://opensource.org/license/mit) license.
42 changes: 42 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package sse

import "time"

// Config defines the configuration settings for the Server-Sent Events (SSE) handler.
type Config[T any] struct {
// How often to flush the events to the connected clients. Default is 256ms
FlushFrequency time.Duration

// How long to wait for all connected client to gracefully close. Default is 30s
CloseTimeout time.Duration

// Replay events when a client connects
Replay struct {
// How many events to send in chunks, when a client connects. Default is 256 events
Initial int

// How many events to keep in memory. Default is 2048 events
Maximum int

// How long an event should be kept in memory for. Default is 30s
Expiry time.Duration
}

// Events encoder function, which returns a slice of bytes that will then be converted to a string. Default is json.Marshal()
Encoder func([]T) ([]byte, error)
}

// NewConfig initializes a configuration instance with reasonable defaults.
func NewConfig[T any]() *Config[T] {
cfg := &Config[T]{}
cfg.FlushFrequency = 256 * time.Millisecond
cfg.CloseTimeout = 30 * time.Second
cfg.Replay.Initial = 256
cfg.Replay.Maximum = 2048
cfg.Replay.Expiry = 30 * time.Second

// Use the default encoder
cfg.Encoder = nil

return cfg
}
83 changes: 83 additions & 0 deletions examples/custom_event_server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
package main

import (
"context"
"crypto/rand"
"fmt"
"net/http"
"os"
"os/signal"
"syscall"
"time"

"github.com/softwarespot/sse"
)

type CustomEvent struct {
ID string `json:"id"`
}

func main() {
ctx, _ := signalTrap(context.Background(), os.Interrupt, syscall.SIGTERM)
ctx, cancel := context.WithTimeout(ctx, 5*time.Minute)
defer cancel()

// Use the default configuration
h := sse.New[CustomEvent](nil)
go func() {
for {
id := must(generateID[string]())
evt := CustomEvent{
ID: id,
}
fmt.Println("sse handler: broadcast event", h.Broadcast(evt))
time.Sleep(64 * time.Millisecond)
}
}()

// Start the server on port "3000" as non-blocking
go func() {
http.Handle("/events", h)
http.ListenAndServe(":3000", nil)
}()

// Wait for either a termination signal or timeout of the context
<-ctx.Done()

if err := h.Close(); err != nil {
fmt.Println("sse handler: server shutdown with error:", err)
os.Exit(1)
}
fmt.Println("sse handler: server shutdown gracefully")
}

// Helpers

func generateID[T ~string]() (T, error) {
b := make([]byte, 8)
if _, err := rand.Read(b); err != nil {
return "", fmt.Errorf("creating a new ID: %w", err)
}
return T(fmt.Sprintf("%x-%d", b, time.Now().UnixMilli())), nil
}

func must[T any](res T, err error) T {
if err != nil {
panic(err)
}
return res
}

func signalTrap(ctx context.Context, sig ...os.Signal) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithCancel(ctx)
go func() {
signals := make(chan os.Signal, 1)
signal.Notify(signals, sig...)
select {
case <-ctx.Done():
case <-signals:
}
cancel()
}()
return ctx, cancel
}
27 changes: 27 additions & 0 deletions examples/event_server/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package main

import (
"fmt"
"math/rand/v2"
"net/http"
"time"

"github.com/softwarespot/sse"
)

func main() {
// Use the default configuration
h := sse.New[int64](nil)
defer h.Close()

go func() {
for {
evt := rand.Int64N(1000)
fmt.Println("sse handler: broadcast event", h.Broadcast(evt))
time.Sleep(64 * time.Millisecond)
}
}()

http.Handle("/events", h)
http.ListenAndServe(":3000", nil)
}
Loading

0 comments on commit c5de052

Please sign in to comment.