package httpx import ( "context" "fmt" "net" "net/http" "sync" "sync/atomic" "time" "github.com/gin-gonic/gin" "go.uber.org/zap" adminlogic "spark-server/internal/module/admin/logic" authlogic "spark-server/internal/module/auth/logic" clientlogic "spark-server/internal/module/client/logic" energylogic "spark-server/internal/module/energy/logic" favlogic "spark-server/internal/module/favorite/logic" friendlogic "spark-server/internal/module/friend/logic" grouplogic "spark-server/internal/module/group/logic" msglogic "spark-server/internal/module/message/logic" modlogic "spark-server/internal/module/moderation/logic" momentlogic "spark-server/internal/module/moment/logic" musiclogic "spark-server/internal/module/music/logic" smslogic "spark-server/internal/module/sms/logic" squarelogic "spark-server/internal/module/square/logic" synclogic "spark-server/internal/module/sync/logic" uploadlogic "spark-server/internal/module/upload/logic" userlogic "spark-server/internal/module/user/logic" "spark-server/internal/pkg/ratelimit" ) type Server struct { log *zap.Logger http *http.Server ready chan struct{} readyOnce sync.Once listening atomic.Bool } // Health is the readiness checker. Nil means tests: /readyz is always ready. type Health interface { Ready(ctx context.Context) (ok bool, data any) } // EngineDeps wires Application services into Gin. Auth nil disables Bearer checks (tests). type EngineDeps struct { Auth authlogic.Service Login *authlogic.LoginService SMS *smslogic.Service Users *userlogic.Service Friends *friendlogic.Service Groups *grouplogic.Service Files *uploadlogic.Service Favorites *favlogic.Service Moments *momentlogic.Service Square *squarelogic.Service Messages *msglogic.Service Sync *synclogic.Service Admin *adminlogic.Service AdminTokens *adminlogic.TokenService Music *musiclogic.Service Client *clientlogic.Service Moderation *modlogic.Service Energy *energylogic.Service Health Health AuthLimiter *ratelimit.Limiter } func New(log *zap.Logger, addr string, deps *EngineDeps) *Server { if log == nil { log = zap.NewNop() } if deps == nil { deps = &EngineDeps{} } r := NewEngine(log, deps) return &Server{ log: log, ready: make(chan struct{}), http: &http.Server{ Addr: addr, Handler: r, ReadHeaderTimeout: 5 * time.Second, }, } } func NewEngine(log *zap.Logger, deps *EngineDeps) *gin.Engine { gin.SetMode(gin.ReleaseMode) r := gin.New() _ = r.SetTrustedProxies([]string{"127.0.0.1", "::1"}) if deps == nil { deps = &EngineDeps{} } r.Use(Trace(log), Recovery(log), AccessLog(), AuthRateLimit(deps.AuthLimiter), Auth(deps.Auth)) registerRoutes(r, deps) return r } func (s *Server) Start() error { ln, err := net.Listen("tcp", s.http.Addr) if err != nil { s.signalReady() return err } s.listening.Store(true) s.signalReady() s.log.Info("http listening", zap.String("addr", ln.Addr().String())) err = s.http.Serve(ln) if err == http.ErrServerClosed { return nil } return err } func (s *Server) WaitReady(ctx context.Context) error { select { case <-ctx.Done(): return ctx.Err() case <-s.ready: if s.listening.Load() { return nil } return fmt.Errorf("http not listening") } } func (s *Server) signalReady() { s.readyOnce.Do(func() { close(s.ready) }) } func (s *Server) Stop(ctx context.Context) error { return s.http.Shutdown(ctx) }