- Reorder HandleGameEvents to create NATS subscriptions before SSE - Use chi's middleware.NewWrapResponseWriter for proper http.Flusher support - Add slog-zerolog adapter for unified logging - Add ErrorLog to HTTP server for better error visibility - Change session Cookie.Secure to false for HTTP support - Change heartbeat from 15s to 10s - Remove ConnectionIndicator patching (was causing PatchElementsNoTargetsFound) The key fix was using chi's response writer wrapper which properly implements http.Flusher, allowing SSE data to be flushed immediately instead of being buffered.
134 lines
3.3 KiB
Go
134 lines
3.3 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"embed"
|
|
"fmt"
|
|
"log/slog"
|
|
"net"
|
|
"net/http"
|
|
"os/signal"
|
|
"syscall"
|
|
"time"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/go-chi/chi/v5/middleware"
|
|
"github.com/rs/zerolog/log"
|
|
slogzerolog "github.com/samber/slog-zerolog/v2"
|
|
"golang.org/x/sync/errgroup"
|
|
|
|
"github.com/ryanhamamura/games/config"
|
|
"github.com/ryanhamamura/games/connect4"
|
|
"github.com/ryanhamamura/games/db"
|
|
"github.com/ryanhamamura/games/db/repository"
|
|
"github.com/ryanhamamura/games/logging"
|
|
appnats "github.com/ryanhamamura/games/nats"
|
|
"github.com/ryanhamamura/games/router"
|
|
"github.com/ryanhamamura/games/sessions"
|
|
"github.com/ryanhamamura/games/snake"
|
|
"github.com/ryanhamamura/games/version"
|
|
)
|
|
|
|
//go:embed assets
|
|
var assets embed.FS
|
|
|
|
func main() {
|
|
ctx, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
|
|
defer cancel()
|
|
|
|
cfg := config.Global
|
|
zerologLogger := logging.SetupLogger(cfg.Environment, cfg.LogLevel)
|
|
slog.SetDefault(slog.New(slogzerolog.Option{
|
|
Level: slogzerolog.ZeroLogLeveler{Logger: zerologLogger},
|
|
Logger: zerologLogger,
|
|
NoTimestamp: true,
|
|
}.NewZerologHandler()))
|
|
|
|
if err := run(ctx); err != nil && err != http.ErrServerClosed {
|
|
log.Fatal().Err(err).Msg("server error")
|
|
}
|
|
}
|
|
|
|
func run(ctx context.Context) error {
|
|
cfg := config.Global
|
|
addr := fmt.Sprintf("%s:%s", cfg.Host, cfg.Port)
|
|
slog.Info("server starting", "addr", addr, "version", version.Version, "commit", version.Commit)
|
|
defer slog.Info("server shutdown complete")
|
|
|
|
eg, egctx := errgroup.WithContext(ctx)
|
|
|
|
// Database
|
|
cleanupDB, err := db.Init(cfg.DBPath)
|
|
if err != nil {
|
|
return fmt.Errorf("initializing database: %w", err)
|
|
}
|
|
defer cleanupDB()
|
|
|
|
queries := repository.New(db.DB)
|
|
|
|
// Sessions
|
|
sessionManager, cleanupSessions := sessions.SetupSessionManager(db.DB)
|
|
defer cleanupSessions()
|
|
|
|
// NATS
|
|
nc, cleanupNATS, err := appnats.SetupNATS(egctx)
|
|
if err != nil {
|
|
return fmt.Errorf("setting up NATS: %w", err)
|
|
}
|
|
defer cleanupNATS()
|
|
|
|
// Game stores
|
|
store := connect4.NewStore(queries)
|
|
store.SetNotifyFunc(func(gameID string) {
|
|
nc.Publish(connect4.GameSubject(gameID), nil) //nolint:errcheck // best-effort notification
|
|
})
|
|
|
|
snakeStore := snake.NewSnakeStore(queries)
|
|
snakeStore.SetNotifyFunc(func(gameID string) {
|
|
nc.Publish(snake.GameSubject(gameID), nil) //nolint:errcheck // best-effort notification
|
|
})
|
|
|
|
// Router
|
|
logger := log.Logger
|
|
r := chi.NewMux()
|
|
r.Use(
|
|
logging.RequestLogger(&logger, cfg.Environment),
|
|
middleware.Recoverer,
|
|
sessionManager.LoadAndSave,
|
|
)
|
|
|
|
router.SetupRoutes(r, queries, sessionManager, nc, store, snakeStore, assets)
|
|
|
|
// HTTP server
|
|
srv := &http.Server{
|
|
Addr: addr,
|
|
Handler: r,
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
BaseContext: func(l net.Listener) context.Context {
|
|
return egctx
|
|
},
|
|
ErrorLog: slog.NewLogLogger(
|
|
slog.Default().Handler(),
|
|
slog.LevelError,
|
|
),
|
|
}
|
|
|
|
eg.Go(func() error {
|
|
err := srv.ListenAndServe()
|
|
if err != nil && err != http.ErrServerClosed {
|
|
return fmt.Errorf("server error: %w", err)
|
|
}
|
|
return nil
|
|
})
|
|
|
|
eg.Go(func() error {
|
|
<-egctx.Done()
|
|
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
|
defer cancel()
|
|
slog.Debug("shutting down server...")
|
|
return srv.Shutdown(shutdownCtx)
|
|
})
|
|
|
|
return eg.Wait()
|
|
}
|