feat: initial commit

This commit is contained in:
2023-02-09 12:16:36 +01:00
commit 65a866efe1
113 changed files with 17880 additions and 0 deletions

51
pkg/http/file.go Normal file
View 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
View 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
View 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
View 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,
}
}