Skip to content

Commit

Permalink
Sse enhancement (#39)
Browse files Browse the repository at this point in the history
[minor] added helper method to get count of active clients
[patch] renamed `GetClientMessageChan` to `ClientMessageChan`
[-] updated the sample app's HTML page to show details slightly better
[-] breaking change in function name, since previous version was released only 24hrs ago. I considered it ok to break it this soon
  • Loading branch information
bnkamalesh authored Mar 13, 2022
1 parent 49bc848 commit 75dd8ef
Show file tree
Hide file tree
Showing 8 changed files with 195 additions and 134 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
[![](https://godoc.org/github.com/nathany/looper?status.svg)](http://godoc.org/github.com/bnkamalesh/webgo)
[![](https://awesome.re/mentioned-badge.svg)](https://github.com/avelino/awesome-go#web-frameworks)

# WebGo v6.4.0
# WebGo v6.5.1

WebGo is a minimalistic framework for [Go](https://golang.org) to build web applications (server side) with no 3rd party dependencies. WebGo will always be Go standard library compliant; with the HTTP handlers having the same signature as [http.HandlerFunc](https://golang.org/pkg/net/http/#HandlerFunc).

Expand Down
6 changes: 3 additions & 3 deletions cmd/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

### Server Sent Events

![sse-demo](https://user-images.githubusercontent.com/1092882/158008892-b1a04fa7-cb5c-4bb9-9210-00cbdd163ef0.png)
![sse-demo](https://user-images.githubusercontent.com/1092882/158047065-447eb868-1efd-4a8d-b748-7caee2b3fcfd.png)

This picture shows the sample SSE implementation provided with this application.

Expand All @@ -18,7 +18,7 @@ $ mkdir -p github.com/bnkamalesh
$ cd github.com/bnkamalesh
$ git clone https://github.com/bnkamalesh/webgo.git
$ cd webgo
$ go run cmd/main.go
$ go run cmd/*.go

Info 2020/06/03 12:55:26 HTTP server, listening on :8080
```
Expand All @@ -32,7 +32,7 @@ $ docker run \
-p 8080:8080 \
-v ${PWD}:/go/src/github.com/bnkamalesh/webgo/ \
-w /go/src/github.com/bnkamalesh/webgo/cmd \
--rm -ti golang:latest go run main.go
--rm -ti golang:latest go run *.go

Info 2020/06/03 12:55:26 HTTP server, listening on :8080
```
Expand Down
84 changes: 84 additions & 0 deletions cmd/handlers.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package main

import (
"context"
"errors"
"fmt"
"log"
"net/http"
"os"
"strings"

"github.com/bnkamalesh/webgo/v6"
"github.com/bnkamalesh/webgo/v6/extensions/sse"
)

// StaticFilesHandler is used to serve static files
func StaticFilesHandler(rw http.ResponseWriter, r *http.Request) {
wctx := webgo.Context(r)
// '..' is replaced to prevent directory traversal which could go out of static directory
path := strings.ReplaceAll(wctx.Params()["w"], "..", "-")

rw.Header().Set("Last-Modified", lastModified)
http.ServeFile(rw, r, fmt.Sprintf("./cmd/static/%s", path))
}

func OriginalResponseWriterHandler(w http.ResponseWriter, r *http.Request) {
rw := webgo.OriginalResponseWriter(w)
if rw == nil {
webgo.Send(w, "text/html", "got nil", http.StatusPreconditionFailed)
return
}

webgo.Send(w, "text/html", "success", http.StatusOK)
}

func HomeHandler(w http.ResponseWriter, r *http.Request) {
out, err := os.ReadFile("./cmd/static/index.html")
if err != nil {
webgo.SendError(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = w.Write(out)
}

func SSEHandler(sse *sse.SSE) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := webgo.Context(r).Params()
r.Header.Set(sse.ClientIDHeader, params["clientID"])

err := sse.Handler(w, r)
if err != nil && !errors.Is(err, context.Canceled) {
log.Println("errorLogger:", err.Error())
return
}
}
}

func ErrorSetterHandler(w http.ResponseWriter, r *http.Request) {
err := errors.New("oh no, server error")
webgo.SetError(r, err)

webgo.R500(w, err.Error())
}

func ParamHandler(w http.ResponseWriter, r *http.Request) {
// WebGo context
wctx := webgo.Context(r)
// URI parameters, map[string]string
params := wctx.Params()
// route, the webgo.Route which is executing this request
route := wctx.Route
webgo.R200(
w,
map[string]interface{}{
"route": route.Name,
"params": params,
"chained": r.Header.Get("chained"),
},
)
}

func InvalidJSONHandler(w http.ResponseWriter, r *http.Request) {
webgo.R200(w, make(chan int))
}
97 changes: 15 additions & 82 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,6 @@
package main

import (
"context"
"errors"
"fmt"
"log"
"net/http"
Expand All @@ -20,69 +18,10 @@ var (
lastModified = time.Now().Format(http.TimeFormat)
)

func paramHandler(w http.ResponseWriter, r *http.Request) {
// WebGo context
wctx := webgo.Context(r)
// URI parameters, map[string]string
params := wctx.Params()
// route, the webgo.Route which is executing this request
route := wctx.Route
webgo.R200(
w,
map[string]interface{}{
"route": route.Name,
"params": params,
"chained": r.Header.Get("chained"),
},
)
}

func invalidJSON(w http.ResponseWriter, r *http.Request) {
webgo.R200(w, make(chan int))
}

func chain(w http.ResponseWriter, r *http.Request) {
r.Header.Set("chained", "true")
}

func homeHandler(w http.ResponseWriter, r *http.Request) {
out, err := os.ReadFile("./cmd/static/index.html")
if err != nil {
webgo.SendError(w, err.Error(), http.StatusInternalServerError)
return
}
_, _ = w.Write(out)
}

func SSEHandler(sse *sse.SSE) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
params := webgo.Context(r).Params()
r.Header.Set(sse.ClientIDHeader, params["clientID"])

err := sse.Handler(w, r)
if err != nil && !errors.Is(err, context.Canceled) {
log.Println("errorLogger:", err.Error())
return
}
}
}
func errorSetter(w http.ResponseWriter, r *http.Request) {
err := errors.New("oh no, server error")
webgo.SetError(r, err)

webgo.R500(w, err.Error())
}

func originalResponseWriter(w http.ResponseWriter, r *http.Request) {
rw := webgo.OriginalResponseWriter(w)
if rw == nil {
webgo.Send(w, "text/html", "got nil", http.StatusPreconditionFailed)
return
}

webgo.Send(w, "text/html", "success", http.StatusOK)
}

// errLogger is a middleware which will log all errors returned/set by a handler
func errLogger(w http.ResponseWriter, r *http.Request, next http.HandlerFunc) {
next(w, r)
Expand All @@ -101,66 +40,56 @@ func routegroupMiddleware(w http.ResponseWriter, r *http.Request, next http.Hand
next(w, r)
}

// StaticFiles is used to serve static files
func StaticFiles(rw http.ResponseWriter, r *http.Request) {
wctx := webgo.Context(r)
// '..' is replaced to prevent directory traversal which could go out of static directory
path := strings.ReplaceAll(wctx.Params()["w"], "..", "-")

rw.Header().Set("Last-Modified", lastModified)
http.ServeFile(rw, r, fmt.Sprintf("./cmd/static/%s", path))
}

func getRoutes(sse *sse.SSE) []*webgo.Route {
return []*webgo.Route{
{
Name: "root",
Method: http.MethodGet,
Pattern: "/",
Handlers: []http.HandlerFunc{homeHandler},
Handlers: []http.HandlerFunc{HomeHandler},
TrailingSlash: true,
},
{
Name: "matchall",
Method: http.MethodGet,
Pattern: "/matchall/:wildcard*",
Handlers: []http.HandlerFunc{paramHandler},
Handlers: []http.HandlerFunc{ParamHandler},
TrailingSlash: true,
},
{
Name: "api",
Method: http.MethodGet,
Pattern: "/api/:param",
Handlers: []http.HandlerFunc{chain, paramHandler},
Handlers: []http.HandlerFunc{chain, ParamHandler},
TrailingSlash: true,
FallThroughPostResponse: true,
},
{
Name: "invalidjson",
Method: http.MethodGet,
Pattern: "/invalidjson",
Handlers: []http.HandlerFunc{invalidJSON},
Handlers: []http.HandlerFunc{InvalidJSONHandler},
TrailingSlash: true,
},
{
Name: "error-setter",
Method: http.MethodGet,
Pattern: "/error-setter",
Handlers: []http.HandlerFunc{errorSetter},
Handlers: []http.HandlerFunc{ErrorSetterHandler},
TrailingSlash: true,
},
{
Name: "original-responsewriter",
Method: http.MethodGet,
Pattern: "/original-responsewriter",
Handlers: []http.HandlerFunc{originalResponseWriter},
Handlers: []http.HandlerFunc{OriginalResponseWriterHandler},
TrailingSlash: true,
},
{
Name: "static",
Method: http.MethodGet,
Pattern: "/static/:w*",
Handlers: []http.HandlerFunc{StaticFiles},
Handlers: []http.HandlerFunc{StaticFilesHandler},
TrailingSlash: true,
},
{
Expand All @@ -173,7 +102,7 @@ func getRoutes(sse *sse.SSE) []*webgo.Route {
}
}

func main() {
func setup() (*webgo.Router, *sse.SSE) {
port := strings.TrimSpace(os.Getenv("HTTP_PORT"))
if port == "" {
port = "8080"
Expand All @@ -195,7 +124,7 @@ func main() {
Name: "router-group-prefix-v6.2_api",
Method: http.MethodGet,
Pattern: "/api/:param",
Handlers: []http.HandlerFunc{chain, paramHandler},
Handlers: []http.HandlerFunc{chain, ParamHandler},
})
routeGroup.Use(routegroupMiddleware)

Expand All @@ -206,16 +135,20 @@ func main() {
router := webgo.NewRouter(cfg, routes...)
router.UseOnSpecialHandlers(accesslog.AccessLog)
router.Use(errLogger, accesslog.AccessLog, cors.CORS(nil))
return router, sseService
}

func main() {
router, sseService := setup()
// broadcast server time to all SSE listeners
go func() {
retry := time.Millisecond * 500
for {
now := time.Now().Format(http.TimeFormat)
now := time.Now().Format(time.RFC1123Z)
sseService.Clients.Range(func(key, value interface{}) bool {
msg, _ := value.(chan *sse.Message)
msg <- &sse.Message{
Data: now,
Data: now + fmt.Sprintf(" (%d)", sseService.ClientsCount()),
Retry: retry,
}
return true
Expand Down
20 changes: 15 additions & 5 deletions cmd/static/css/main.css
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,21 @@ section.main {
overflow: hidden;
}

#sse {
table {
width: 100%;
font-size: 12px;
line-height: 1.5em;
background: rgba(0,0,0,0.1);
padding: 0.5em;
border-radius: 4px;
margin-left: 0.5em;
border: 1px solid #eee;
border-collapse: collapse;
}
tr, td {
border: 1px solid #eee;
}
td {
padding: 0.25rem;
text-align: right;
}
td:nth-child(1) {
text-align: left;
background-color: rgba(0,0,0,0.02);
}
Loading

0 comments on commit 75dd8ef

Please sign in to comment.