feat: initial commit
This commit is contained in:
51
pkg/http/file.go
Normal file
51
pkg/http/file.go
Normal file
@ -0,0 +1,51 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
func serveFile(w http.ResponseWriter, r *http.Request, fs fs.FS, path string) {
|
||||
ctx := logger.With(r.Context(), logger.F("path", path))
|
||||
|
||||
file, err := fs.Open(path)
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
http.Error(w, http.StatusText(http.StatusNotFound), http.StatusNotFound)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Error(ctx, "error while opening fs file", logger.E(errors.WithStack(err)))
|
||||
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err := file.Close(); err != nil {
|
||||
logger.Error(ctx, "error while closing fs file", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
logger.Error(ctx, "error while retrieving fs file stat", logger.E(errors.WithStack(err)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
reader, ok := file.(io.ReadSeeker)
|
||||
if !ok {
|
||||
logger.Error(ctx, "could not convert file to readseeker", logger.E(errors.WithStack(err)))
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
http.ServeContent(w, r, path, info.ModTime(), reader)
|
||||
}
|
106
pkg/http/handler.go
Normal file
106
pkg/http/handler.go
Normal file
@ -0,0 +1,106 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bundle"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/sdk"
|
||||
"github.com/igm/sockjs-go/v3/sockjs"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
sockJSPathPrefix = "/sock"
|
||||
clientJSPath = "/client.js"
|
||||
backendMainScript = "backend/main.js"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
bundle bundle.Bundle
|
||||
public http.Handler
|
||||
|
||||
sockjs http.Handler
|
||||
bus bus.Bus
|
||||
sockjsOpts sockjs.Options
|
||||
|
||||
backend *app.Backend
|
||||
backendModuleFactories []app.BackendModuleFactory
|
||||
|
||||
mutex sync.RWMutex
|
||||
}
|
||||
|
||||
func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
h.mutex.RLock()
|
||||
defer h.mutex.RUnlock()
|
||||
|
||||
switch {
|
||||
case r.URL.Path == clientJSPath:
|
||||
serveFile(w, r, &sdk.FS, "client/dist/client.js")
|
||||
case r.URL.Path == clientJSPath+".map":
|
||||
serveFile(w, r, &sdk.FS, "client/dist/client.js.map")
|
||||
case strings.HasPrefix(r.URL.Path, sockJSPathPrefix):
|
||||
h.sockjs.ServeHTTP(w, r)
|
||||
default:
|
||||
h.public.ServeHTTP(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) Load(bdle bundle.Bundle) error {
|
||||
h.mutex.Lock()
|
||||
defer h.mutex.Unlock()
|
||||
|
||||
file, _, err := bdle.File(backendMainScript)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not open backend main script")
|
||||
}
|
||||
|
||||
mainScript, err := ioutil.ReadAll(file)
|
||||
if err != nil {
|
||||
return errors.Wrap(err, "could not read backend main script")
|
||||
}
|
||||
|
||||
backend := app.NewBackend(h.backendModuleFactories...)
|
||||
|
||||
if err := backend.Load(backendMainScript, string(mainScript)); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
fs := bundle.NewFileSystem("public", bdle)
|
||||
public := http.FileServer(fs)
|
||||
sockjs := sockjs.NewHandler(sockJSPathPrefix, h.sockjsOpts, h.handleSockJSSession)
|
||||
|
||||
if h.backend != nil {
|
||||
h.backend.Stop()
|
||||
}
|
||||
|
||||
if err := backend.Start(); err != nil {
|
||||
return errors.WithStack(err)
|
||||
}
|
||||
|
||||
h.bundle = bdle
|
||||
h.backend = backend
|
||||
h.public = public
|
||||
h.sockjs = sockjs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewHandler(funcs ...HandlerOptionFunc) *Handler {
|
||||
opts := defaultHandlerOptions()
|
||||
for _, fn := range funcs {
|
||||
fn(opts)
|
||||
}
|
||||
|
||||
handler := &Handler{
|
||||
sockjsOpts: opts.SockJS,
|
||||
backendModuleFactories: opts.BackendModuleFactories,
|
||||
bus: opts.Bus,
|
||||
}
|
||||
|
||||
return handler
|
||||
}
|
49
pkg/http/options.go
Normal file
49
pkg/http/options.go
Normal file
@ -0,0 +1,49 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/app"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus"
|
||||
"forge.cadoles.com/arcad/edge/pkg/bus/memory"
|
||||
"github.com/igm/sockjs-go/v3/sockjs"
|
||||
)
|
||||
|
||||
type HandlerOptions struct {
|
||||
Bus bus.Bus
|
||||
SockJS sockjs.Options
|
||||
BackendModuleFactories []app.BackendModuleFactory
|
||||
}
|
||||
|
||||
func defaultHandlerOptions() *HandlerOptions {
|
||||
sockjsOptions := func() sockjs.Options {
|
||||
return sockjs.DefaultOptions
|
||||
}()
|
||||
sockjsOptions.DisconnectDelay = 10 * time.Second
|
||||
|
||||
return &HandlerOptions{
|
||||
Bus: memory.NewBus(),
|
||||
SockJS: sockjsOptions,
|
||||
BackendModuleFactories: make([]app.BackendModuleFactory, 0),
|
||||
}
|
||||
}
|
||||
|
||||
type HandlerOptionFunc func(*HandlerOptions)
|
||||
|
||||
func WithBackendModules(factories ...app.BackendModuleFactory) HandlerOptionFunc {
|
||||
return func(opts *HandlerOptions) {
|
||||
opts.BackendModuleFactories = factories
|
||||
}
|
||||
}
|
||||
|
||||
func WithSockJS(options sockjs.Options) HandlerOptionFunc {
|
||||
return func(opts *HandlerOptions) {
|
||||
opts.SockJS = options
|
||||
}
|
||||
}
|
||||
|
||||
func WithBus(bus bus.Bus) HandlerOptionFunc {
|
||||
return func(opts *HandlerOptions) {
|
||||
opts.Bus = bus
|
||||
}
|
||||
}
|
214
pkg/http/sockjs.go
Normal file
214
pkg/http/sockjs.go
Normal file
@ -0,0 +1,214 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
|
||||
"forge.cadoles.com/arcad/edge/pkg/module"
|
||||
"github.com/igm/sockjs-go/v3/sockjs"
|
||||
"github.com/pkg/errors"
|
||||
"gitlab.com/wpetit/goweb/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
statusChannelClosed = iota
|
||||
)
|
||||
|
||||
func (h *Handler) handleSockJSSession(sess sockjs.Session) {
|
||||
ctx := logger.With(sess.Request().Context(),
|
||||
logger.F("sessionID", sess.ID()),
|
||||
)
|
||||
|
||||
logger.Debug(ctx, "new sockjs session")
|
||||
|
||||
defer func() {
|
||||
if sess.GetSessionState() == sockjs.SessionActive {
|
||||
if err := sess.Close(statusChannelClosed, "channel closed"); err != nil {
|
||||
logger.Error(ctx, "could not close sockjs session", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
go h.handleBackendMessages(ctx, sess)
|
||||
h.handleClientMessages(ctx, sess)
|
||||
}
|
||||
|
||||
func (h *Handler) handleBackendMessages(ctx context.Context, sess sockjs.Session) {
|
||||
messages, err := h.bus.Subscribe(ctx, module.MessageNamespaceBackend)
|
||||
if err != nil {
|
||||
panic(errors.WithStack(err))
|
||||
}
|
||||
|
||||
defer func() {
|
||||
// Close messages subscriber
|
||||
h.bus.Unsubscribe(ctx, module.MessageNamespaceBackend, messages)
|
||||
|
||||
logger.Debug(ctx, "unsubscribed")
|
||||
|
||||
if sess.GetSessionState() != sockjs.SessionActive {
|
||||
return
|
||||
}
|
||||
|
||||
if err := sess.Close(statusChannelClosed, "channel closed"); err != nil {
|
||||
logger.Error(ctx, "could not close sockjs session", logger.E(errors.WithStack(err)))
|
||||
}
|
||||
}()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case msg := <-messages:
|
||||
backendMessage, ok := msg.(*module.BackendMessage)
|
||||
if !ok {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"unexpected backend message",
|
||||
logger.F("message", msg),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
payload, err := json.Marshal(backendMessage.Data)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"could not encode message",
|
||||
logger.E(err),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
message := NewWebsocketMessage(
|
||||
WebsocketMessageTypeMessage,
|
||||
json.RawMessage(payload),
|
||||
)
|
||||
|
||||
data, err := json.Marshal(message)
|
||||
if err != nil {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"could not encode message",
|
||||
logger.E(err),
|
||||
)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "sending message")
|
||||
|
||||
// Send message
|
||||
if err := sess.Send(string(data)); err != nil {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"could not send message",
|
||||
logger.E(err),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) handleClientMessages(ctx context.Context, sess sockjs.Session) {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
logger.Debug(ctx, "context done")
|
||||
|
||||
return
|
||||
|
||||
default:
|
||||
logger.Debug(ctx, "waiting for websocket data")
|
||||
|
||||
data, err := sess.RecvCtx(ctx)
|
||||
if err != nil {
|
||||
if errors.Is(err, sockjs.ErrSessionNotOpen) {
|
||||
break
|
||||
}
|
||||
|
||||
logger.Error(
|
||||
ctx,
|
||||
"could not read message",
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "websocket data received", logger.F("data", data))
|
||||
|
||||
message := &WebsocketMessage{}
|
||||
if err := json.Unmarshal([]byte(data), message); err != nil {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"could not decode message",
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
switch {
|
||||
|
||||
case message.Type == WebsocketMessageTypeMessage:
|
||||
var payload map[string]interface{}
|
||||
if err := json.Unmarshal(message.Payload, &payload); err != nil {
|
||||
logger.Error(
|
||||
ctx,
|
||||
"could not decode payload",
|
||||
logger.E(errors.WithStack(err)),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
ctx := logger.With(ctx, logger.F("payload", payload))
|
||||
|
||||
frontendMessage := module.NewFrontendMessage(payload)
|
||||
|
||||
logger.Debug(ctx, "publishing new frontend message", logger.F("message", frontendMessage))
|
||||
|
||||
if err := h.bus.Publish(ctx, frontendMessage); err != nil {
|
||||
logger.Error(ctx, "could not publish message",
|
||||
logger.E(errors.WithStack(err)),
|
||||
logger.F("message", frontendMessage),
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logger.Debug(ctx, "new frontend message published", logger.F("message", frontendMessage))
|
||||
|
||||
default:
|
||||
logger.Error(
|
||||
ctx,
|
||||
"unsupported message type",
|
||||
logger.F("messageType", message.Type),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const (
|
||||
WebsocketMessageTypeMessage = "message"
|
||||
)
|
||||
|
||||
type WebsocketMessage struct {
|
||||
Type string `json:"t"`
|
||||
Payload json.RawMessage `json:"p"`
|
||||
}
|
||||
|
||||
type WebsocketMessagePayload struct {
|
||||
Data map[string]interface{} `json:"d"`
|
||||
}
|
||||
|
||||
func NewWebsocketMessage(dataType string, payload json.RawMessage) *WebsocketMessage {
|
||||
return &WebsocketMessage{
|
||||
Type: dataType,
|
||||
Payload: payload,
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user