package server import ( "context" "errors" "fmt" "net" "net/http" "github.com/go-chi/chi/v5" "code.nochebuena.dev/einherjar/contracts/logging" "code.nochebuena.dev/einherjar/contracts/observability" "code.nochebuena.dev/einherjar/core/xerrors" ) var _ Server = (*impl)(nil) var _ observability.Identifiable = (*impl)(nil) type impl struct { chi.Router logger logging.Logger cfg Config opts serverOpts srv *http.Server } // New creates a [Server]. No middleware is applied by default; use [WithMiddleware] // to compose the middleware stack before passing it to [launcher.New]. func New(logger logging.Logger, cfg Config, opts ...Option) Server { o := serverOpts{} for _, opt := range opts { opt(&o) } return &impl{ Router: chi.NewRouter(), logger: logger, cfg: cfg, opts: o, } } // OnInit applies registered middleware to the router. func (s *impl) OnInit() error { for _, mw := range s.opts.middleware { s.Router.Use(mw) } return nil } // OnStart binds the TCP listener synchronously so that a port conflict surfaces // immediately — the launcher can trigger a clean shutdown instead of running // silently without an HTTP server. Requests are served in a background goroutine. func (s *impl) OnStart() error { host := s.cfg.Host if host == "" { host = "0.0.0.0" } port := s.cfg.Port if port == 0 { port = 8080 } addr := fmt.Sprintf("%s:%d", host, port) ln, err := net.Listen("tcp", addr) if err != nil { return xerrors.Unavailable("server: bind %s", addr).WithError(err) } s.srv = &http.Server{ Addr: addr, Handler: s.Router, ReadTimeout: s.cfg.ReadTimeout, WriteTimeout: s.cfg.WriteTimeout, IdleTimeout: s.cfg.IdleTimeout, } go func() { if err := s.srv.Serve(ln); err != nil && !errors.Is(err, http.ErrServerClosed) { s.logger.Error("server: fatal", err) } }() s.logger.Info("server: listening", "addr", addr) return nil } // OnStop performs a graceful shutdown, waiting up to ShutdownTimeout for // in-flight requests to complete. No-op if OnStart was never called. func (s *impl) OnStop() error { if s.srv == nil { return nil } s.logger.Info("server: shutting down") timeout := s.cfg.ShutdownTimeout if timeout == 0 { timeout = defaultShutdownTimeout } ctx, cancel := context.WithTimeout(context.Background(), timeout) defer cancel() if err := s.srv.Shutdown(ctx); err != nil { return xerrors.Internal("server: shutdown").WithError(err) } return nil }