// Package server provides the HTTP server lifecycle, // including startup, routing, and graceful shutdown. The // process lifetime is owned by fx: shutdown is requested // through fx.Shutdowner so every component's OnStop hook runs // in dependency order. package server import ( "context" "log/slog" "net/http" "sneak.berlin/go/netwatch/internal/config" "sneak.berlin/go/netwatch/internal/globals" "sneak.berlin/go/netwatch/internal/handlers" "sneak.berlin/go/netwatch/internal/logger" "sneak.berlin/go/netwatch/internal/middleware" "github.com/go-chi/chi/v5" "go.uber.org/fx" ) // Params defines the dependencies for Server. type Params struct { fx.In Config *config.Config Globals *globals.Globals Handlers *handlers.Handlers Logger *logger.Logger Middleware *middleware.Middleware Shutdowner fx.Shutdowner } // Server is the top-level HTTP server orchestrator. type Server struct { h *handlers.Handlers httpServer *http.Server log *slog.Logger mw *middleware.Middleware params Params router *chi.Mux shutdowner fx.Shutdowner } // New creates a Server and registers lifecycle hooks for // starting and stopping it. func New( lc fx.Lifecycle, params Params, ) (*Server, error) { s := new(Server) s.params = params s.mw = params.Middleware s.h = params.Handlers s.log = params.Logger.Get() s.shutdowner = params.Shutdowner lc.Append(fx.Hook{ OnStart: func(_ context.Context) error { // Build the router and http.Server synchronously // here, before spawning the serving goroutine, so // httpServer is fully constructed by the time OnStop // (or an early signal) can read it. fx guarantees // OnStart returns before OnStop runs, so no // synchronization or nil check is needed at shutdown. s.SetupRoutes() s.httpServer = s.newHTTPServer() go s.listenAndServe() return nil }, OnStop: func(ctx context.Context) error { return s.shutdown(ctx) }, }) return s, nil } // ServeHTTP delegates to the chi router. func (s *Server) ServeHTTP( w http.ResponseWriter, r *http.Request, ) { s.router.ServeHTTP(w, r) } // shutdown gracefully stops the HTTP server within the // deadline of the context fx provides for OnStop. func (s *Server) shutdown(ctx context.Context) error { err := s.httpServer.Shutdown(ctx) if err != nil { s.log.Error("server clean shutdown failed", "error", err) return err } s.log.Info("server stopped") return nil }