feat(ws): release generic WebSocket client and server v0.31.0-preview
This commit is contained in:
44
ws/omni/handler.go
Normal file
44
ws/omni/handler.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Package omni connects managed WebSocket sessions to a GoOmni application.
|
||||
package omni
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/app"
|
||||
"git.yuntuo.hk/k2safe/go-omni/config"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/ws"
|
||||
)
|
||||
|
||||
// Handler preserves the HTTP middleware chain and records the actual upgrade
|
||||
// status. The server owns all session callbacks after this handler returns.
|
||||
// Register Server.Stop/Drain on App or svc Runtime before accepting connections.
|
||||
func Handler(server *ws.Server, handlers ws.Handlers) httpx.Handler {
|
||||
if server == nil {
|
||||
panic("ws/omni: nil server")
|
||||
}
|
||||
return func(c *httpx.Context) error {
|
||||
_, err := server.Accept(c.Raw(), c.Request(), handlers)
|
||||
if err == nil {
|
||||
c.MarkUpgraded()
|
||||
}
|
||||
// Accept has already written any handshake failure response.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
// Mount registers a GET endpoint and its shutdown barriers on a standalone App.
|
||||
// Use Handler with svc.OnStop/OnDrain when the service Runtime owns dependencies.
|
||||
// Mount once per Server. Configure the App before calling it.
|
||||
func Mount(a *app.App, path string, server *ws.Server, handlers ws.Handlers) error {
|
||||
if a == nil || server == nil {
|
||||
return errors.New("ws/omni: app and server required")
|
||||
}
|
||||
if a.Config().Transport != config.TransportNetHTTP && a.Config().Transport != "" {
|
||||
return ws.ErrUnsupportedTransport
|
||||
}
|
||||
a.GET(path, Handler(server, handlers))
|
||||
a.OnStop("ws:"+path, server.Stop)
|
||||
a.OnDrain("ws:"+path, server.Drain)
|
||||
return nil
|
||||
}
|
||||
92
ws/omni/handler_test.go
Normal file
92
ws/omni/handler_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package omni_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
omni "git.yuntuo.hk/k2safe/go-omni"
|
||||
"git.yuntuo.hk/k2safe/go-omni/httpx"
|
||||
"git.yuntuo.hk/k2safe/go-omni/lifecycle"
|
||||
"git.yuntuo.hk/k2safe/go-omni/ws"
|
||||
wsomni "git.yuntuo.hk/k2safe/go-omni/ws/omni"
|
||||
"github.com/gorilla/websocket"
|
||||
)
|
||||
|
||||
func TestMountLifetimeAndStatus(t *testing.T) {
|
||||
a := omni.New(omni.WithLogger(false))
|
||||
var observed atomic.Int64
|
||||
a.Use(func(next httpx.Handler) httpx.Handler {
|
||||
return func(c *httpx.Context) error { err := next(c); observed.Store(int64(c.Status())); return err }
|
||||
})
|
||||
s, err := ws.NewServer(ws.ServerConfig{Session: ws.SessionConfig{CloseTimeout: 30 * time.Millisecond}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err = wsomni.Mount(a, "/ws", s, ws.Handlers{OnMessage: func(ctx context.Context, s *ws.Session, m ws.Message) error { return s.Send(ctx, m) }}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
h := httptest.NewServer(a)
|
||||
defer h.Close()
|
||||
defer a.Close()
|
||||
c, _, err := websocket.DefaultDialer.Dial("ws"+strings.TrimPrefix(h.URL, "http")+"/ws", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer c.Close()
|
||||
for i := 0; i < 10; i++ {
|
||||
if err = c.WriteMessage(websocket.TextMessage, []byte("hello")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
c.SetReadDeadline(time.Now().Add(time.Second))
|
||||
_, v, e := c.ReadMessage()
|
||||
if e != nil || string(v) != "hello" {
|
||||
t.Fatal(e)
|
||||
}
|
||||
}
|
||||
if observed.Load() != 101 {
|
||||
t.Fatal("upgrade logged as", observed.Load())
|
||||
}
|
||||
var cleanup atomic.Bool
|
||||
a.OnShutdown(func() error {
|
||||
if s.Stats().ActiveConnections != 0 {
|
||||
t.Error("resource closed before sessions")
|
||||
}
|
||||
cleanup.Store(true)
|
||||
return nil
|
||||
})
|
||||
if err = a.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !cleanup.Load() {
|
||||
t.Fatal("missing cleanup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRetainsResourcesForBlockedComponent(t *testing.T) {
|
||||
a := omni.New(omni.WithLogger(false))
|
||||
closed := false
|
||||
a.OnStop("blocked", func(context.Context) error { return nil })
|
||||
a.OnDrain("blocked", func(context.Context) error { return context.DeadlineExceeded })
|
||||
a.OnShutdown(func() error { closed = true; return nil })
|
||||
if err := a.Close(); !errors.Is(err, lifecycle.ErrResourcesRetained) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if closed {
|
||||
t.Fatal("unsafe cleanup")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMountRejectsNative(t *testing.T) {
|
||||
a := omni.New(omni.WithTransport(omni.TransportNative))
|
||||
defer a.Close()
|
||||
s, _ := ws.NewServer(ws.ServerConfig{})
|
||||
defer s.Shutdown(context.Background())
|
||||
if err := wsomni.Mount(a, "/ws", s, ws.Handlers{}); !errors.Is(err, ws.ErrUnsupportedTransport) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user