feat: game lobby service

This commit is contained in:
Ilia Denisov
2026-04-25 23:20:55 +02:00
committed by GitHub
parent 32dc29359a
commit 48b0056b49
336 changed files with 57074 additions and 1418 deletions
+785
View File
@@ -0,0 +1,785 @@
package app
import (
"errors"
"fmt"
"log/slog"
"time"
"galaxy/lobby/internal/adapters/gmclient"
"galaxy/lobby/internal/adapters/idgen"
"galaxy/lobby/internal/adapters/metricsintentpub"
"galaxy/lobby/internal/adapters/metricsracenamedir"
"galaxy/lobby/internal/adapters/racenameintents"
"galaxy/lobby/internal/adapters/racenamestub"
"galaxy/lobby/internal/adapters/redisstate"
"galaxy/lobby/internal/adapters/runtimemanager"
"galaxy/lobby/internal/adapters/userlifecycle"
"galaxy/lobby/internal/adapters/userservice"
"galaxy/lobby/internal/config"
"galaxy/lobby/internal/domain/racename"
"galaxy/lobby/internal/ports"
"galaxy/lobby/internal/telemetry"
"galaxy/lobby/internal/service/approveapplication"
"galaxy/lobby/internal/service/blockmember"
"galaxy/lobby/internal/service/cancelgame"
"galaxy/lobby/internal/service/createinvite"
"galaxy/lobby/internal/service/creategame"
"galaxy/lobby/internal/service/declineinvite"
"galaxy/lobby/internal/service/getgame"
"galaxy/lobby/internal/service/listgames"
"galaxy/lobby/internal/service/listmemberships"
"galaxy/lobby/internal/service/listmyapplications"
"galaxy/lobby/internal/service/listmygames"
"galaxy/lobby/internal/service/listmyinvites"
"galaxy/lobby/internal/service/listmyracenames"
"galaxy/lobby/internal/service/manualreadytostart"
"galaxy/lobby/internal/service/openenrollment"
"galaxy/lobby/internal/service/pausegame"
"galaxy/lobby/internal/service/redeeminvite"
"galaxy/lobby/internal/service/registerracename"
"galaxy/lobby/internal/service/rejectapplication"
"galaxy/lobby/internal/service/removemember"
"galaxy/lobby/internal/service/resumegame"
"galaxy/lobby/internal/service/capabilityevaluation"
"galaxy/lobby/internal/service/retrystartgame"
"galaxy/lobby/internal/service/revokeinvite"
"galaxy/lobby/internal/service/startgame"
"galaxy/lobby/internal/service/submitapplication"
"galaxy/lobby/internal/service/updategame"
"galaxy/lobby/internal/worker/enrollmentautomation"
"galaxy/lobby/internal/worker/gmevents"
"galaxy/lobby/internal/worker/pendingregistration"
"galaxy/lobby/internal/worker/runtimejobresult"
userlifecycleworker "galaxy/lobby/internal/worker/userlifecycle"
"galaxy/notificationintent"
"github.com/redis/go-redis/v9"
)
// wiring owns the process-level singletons that downstream service
// constructors resolve through their port interfaces. It is the single
// place in the process where concrete adapter types are referenced, so
// service code always depends on ports rather than on specific adapters.
//
// extends this struct with the application/membership stores,
// the gap-activation store, the User Service client, the notification
// intent publisher, and the three application services.
type wiring struct {
// policy is the lobby-owned Race Name Directory canonical-key
// policy, shared between the RND adapter and any future service
// that needs to call Canonicalize directly.
policy *racename.Policy
// raceNameDirectory is the platform-wide in-game name uniqueness
// arbiter.
raceNameDirectory ports.RaceNameDirectory
// gameStore persists game records.
gameStore ports.GameStore
// applicationStore persists application records.
applicationStore ports.ApplicationStore
// inviteStore persists invite records.
inviteStore ports.InviteStore
// membershipStore persists membership records.
membershipStore ports.MembershipStore
// gapActivationStore records when a game's gap window opens
//.
gapActivationStore ports.GapActivationStore
// userService is the synchronous User Service eligibility client
//.
userService ports.UserService
// intentPublisher publishes notification intents to
// notification:intents.
intentPublisher ports.IntentPublisher
// idGenerator produces opaque identifiers for new records.
idGenerator ports.IDGenerator
// createGame handles `lobby.game.create`.
createGame *creategame.Service
// updateGame handles `lobby.game.update`.
updateGame *updategame.Service
// openEnrollment handles `lobby.game.open_enrollment`.
openEnrollment *openenrollment.Service
// cancelGame handles `lobby.game.cancel`.
cancelGame *cancelgame.Service
// manualReadyToStart handles `lobby.game.ready_to_start`.
manualReadyToStart *manualreadytostart.Service
// enrollmentAutomation drives the periodic auto-close worker
//.
enrollmentAutomation *enrollmentautomation.Worker
// submitApplication handles `lobby.application.submit`.
submitApplication *submitapplication.Service
// approveApplication handles `lobby.application.approve`.
approveApplication *approveapplication.Service
// rejectApplication handles `lobby.application.reject`.
rejectApplication *rejectapplication.Service
// createInvite handles `lobby.invite.create`.
createInvite *createinvite.Service
// redeemInvite handles `lobby.invite.redeem`.
redeemInvite *redeeminvite.Service
// declineInvite handles `lobby.invite.decline`.
declineInvite *declineinvite.Service
// revokeInvite handles `lobby.invite.revoke`.
revokeInvite *revokeinvite.Service
// runtimeManager publishes start and stop jobs to Runtime Manager
//.
runtimeManager ports.RuntimeManager
// gmClient registers running games with Game Master.
gmClient ports.GMClient
// streamOffsetStore persists Redis Streams consumer progress
//.
streamOffsetStore ports.StreamOffsetStore
// startGame handles `lobby.game.start`.
startGame *startgame.Service
// retryStartGame handles `lobby.game.retry_start`.
retryStartGame *retrystartgame.Service
// pauseGame handles `lobby.game.pause`.
pauseGame *pausegame.Service
// resumeGame handles `lobby.game.resume`.
resumeGame *resumegame.Service
// removeMember handles `lobby.membership.remove`.
removeMember *removemember.Service
// blockMember handles `lobby.membership.block`.
blockMember *blockmember.Service
// registerRaceName handles `lobby.race_name.register`.
registerRaceName *registerracename.Service
// listMyRaceNames handles `lobby.race_names.list`.
listMyRaceNames *listmyracenames.Service
// getGame handles `lobby.game.get`.
getGame *getgame.Service
// listGames handles `lobby.games.list`.
listGames *listgames.Service
// listMemberships handles `lobby.memberships.list`.
listMemberships *listmemberships.Service
// listMyGames handles `lobby.my_games.list`.
listMyGames *listmygames.Service
// listMyApplications handles `lobby.my_applications.list`.
listMyApplications *listmyapplications.Service
// listMyInvites handles `lobby.my_invites.list`.
listMyInvites *listmyinvites.Service
// runtimeJobResultConsumer consumes runtime:job_results and drives
// the post-start sequence.
runtimeJobResultConsumer *runtimejobresult.Consumer
// gameTurnStatsStore persists the per-game per-user stats aggregate
// fed by every runtime_snapshot_update event.
gameTurnStatsStore ports.GameTurnStatsStore
// evaluationGuardStore stores the per-game «already evaluated»
// marker that keeps replayed game_finished events safe.
evaluationGuardStore ports.EvaluationGuardStore
// capabilityEvaluation runs the capability evaluator at
// game finish.
capabilityEvaluation *capabilityevaluation.Service
// gmEventsConsumer consumes gm:lobby_events and applies snapshot
// updates plus capability evaluation handoff.
gmEventsConsumer *gmevents.Consumer
// pendingRegistration releases expired Race Name Directory
// pending_registration entries on a periodic tick.
pendingRegistration *pendingregistration.Worker
// userLifecycleConsumer reads `user:lifecycle_events` from User
// Service and dispatches each entry to userLifecycleWorker. The
// consumer is the long-lived Component registered with app.New;
// the worker has no goroutine of its own.
userLifecycleConsumer *userlifecycle.Consumer
// userLifecycleWorker runs the cascade triggered by each lifecycle
// event.
userLifecycleWorker *userlifecycleworker.Worker
}
// newWiring constructs the process-level dependency set.
func newWiring(
cfg config.Config,
redisClient *redis.Client,
clock func() time.Time,
logger *slog.Logger,
telemetryRuntime *telemetry.Runtime,
) (*wiring, error) {
policy, err := racename.NewPolicy()
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
if clock == nil {
clock = time.Now
}
if logger == nil {
logger = slog.Default()
}
rawDirectory, err := buildRaceNameDirectory(cfg, redisClient, policy, clock)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
directory := metricsracenamedir.New(rawDirectory, telemetryRuntime)
if redisClient == nil {
return nil, errors.New("new lobby wiring: nil redis client")
}
gameStore, err := redisstate.NewGameStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
applicationStore, err := redisstate.NewApplicationStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
inviteStore, err := redisstate.NewInviteStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
membershipStore, err := redisstate.NewMembershipStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
gapActivationStore, err := redisstate.NewGapActivationStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
userServiceClient, err := userservice.NewClient(userservice.Config{
BaseURL: cfg.UserService.BaseURL,
Timeout: cfg.UserService.Timeout,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
rawIntentPublisher, err := notificationintent.NewPublisher(notificationintent.PublisherConfig{
Client: redisClient,
Stream: cfg.Redis.NotificationIntentsStream,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
intentPublisher := metricsintentpub.New(rawIntentPublisher, telemetryRuntime)
ids := idgen.NewGenerator()
createSvc, err := creategame.NewService(creategame.Dependencies{
Games: gameStore,
IDs: ids,
Clock: clock,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
updateSvc, err := updategame.NewService(updategame.Dependencies{
Games: gameStore,
Clock: clock,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
openSvc, err := openenrollment.NewService(openenrollment.Dependencies{
Games: gameStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
cancelSvc, err := cancelgame.NewService(cancelgame.Dependencies{
Games: gameStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
manualReadySvc, err := manualreadytostart.NewService(manualreadytostart.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Invites: inviteStore,
Intents: intentPublisher,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
submitSvc, err := submitapplication.NewService(submitapplication.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Applications: applicationStore,
Users: userServiceClient,
Directory: directory,
Intents: intentPublisher,
IDs: ids,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
approveSvc, err := approveapplication.NewService(approveapplication.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Applications: applicationStore,
Directory: directory,
GapStore: gapActivationStore,
Intents: intentPublisher,
IDs: ids,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
rejectSvc, err := rejectapplication.NewService(rejectapplication.Dependencies{
Games: gameStore,
Applications: applicationStore,
Directory: directory,
Intents: intentPublisher,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
createInviteSvc, err := createinvite.NewService(createinvite.Dependencies{
Games: gameStore,
Invites: inviteStore,
Memberships: membershipStore,
Intents: intentPublisher,
IDs: ids,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
redeemInviteSvc, err := redeeminvite.NewService(redeeminvite.Dependencies{
Games: gameStore,
Invites: inviteStore,
Memberships: membershipStore,
Directory: directory,
Users: userServiceClient,
GapStore: gapActivationStore,
Intents: intentPublisher,
IDs: ids,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
declineInviteSvc, err := declineinvite.NewService(declineinvite.Dependencies{
Invites: inviteStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
revokeInviteSvc, err := revokeinvite.NewService(revokeinvite.Dependencies{
Games: gameStore,
Invites: inviteStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
enrollmentWorker, err := enrollmentautomation.NewWorker(enrollmentautomation.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Invites: inviteStore,
Intents: intentPublisher,
GapStore: gapActivationStore,
Interval: cfg.EnrollmentAutomation.Interval,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
gmClientImpl, err := gmclient.NewClient(gmclient.Config{
BaseURL: cfg.GM.BaseURL,
Timeout: cfg.GM.Timeout,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
runtimePublisher, err := runtimemanager.NewPublisher(runtimemanager.Config{
Client: redisClient,
StartJobsStream: cfg.Redis.RuntimeStartJobsStream,
StopJobsStream: cfg.Redis.RuntimeStopJobsStream,
Clock: clock,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
streamOffsets, err := redisstate.NewStreamOffsetStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
startSvc, err := startgame.NewService(startgame.Dependencies{
Games: gameStore,
RuntimeManager: runtimePublisher,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
retrySvc, err := retrystartgame.NewService(retrystartgame.Dependencies{
Games: gameStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
pauseSvc, err := pausegame.NewService(pausegame.Dependencies{
Games: gameStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
resumeSvc, err := resumegame.NewService(resumegame.Dependencies{
Games: gameStore,
GM: gmClientImpl,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
removeMemberSvc, err := removemember.NewService(removemember.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Directory: directory,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
blockMemberSvc, err := blockmember.NewService(blockmember.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
registerRaceNameSvc, err := registerracename.NewService(registerracename.Dependencies{
Directory: directory,
Users: userServiceClient,
Intents: intentPublisher,
Clock: clock,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
listMyRaceNamesSvc, err := listmyracenames.NewService(listmyracenames.Dependencies{
Directory: directory,
Games: gameStore,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
getGameSvc, err := getgame.NewService(getgame.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Invites: inviteStore,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
listGamesSvc, err := listgames.NewService(listgames.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
listMembershipsSvc, err := listmemberships.NewService(listmemberships.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
listMyGamesSvc, err := listmygames.NewService(listmygames.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
listMyApplicationsSvc, err := listmyapplications.NewService(listmyapplications.Dependencies{
Games: gameStore,
Applications: applicationStore,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
listMyInvitesSvc, err := listmyinvites.NewService(listmyinvites.Dependencies{
Games: gameStore,
Invites: inviteStore,
Memberships: membershipStore,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
runtimeConsumer, err := runtimejobresult.NewConsumer(runtimejobresult.Config{
Client: redisClient,
Stream: cfg.Redis.RuntimeJobResultsStream,
BlockTimeout: cfg.Redis.RuntimeJobResultsReadBlockTimeout,
Games: gameStore,
RuntimeManager: runtimePublisher,
GMClient: gmClientImpl,
Intents: intentPublisher,
OffsetStore: streamOffsets,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
gameTurnStatsStore, err := redisstate.NewGameTurnStatsStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
evaluationGuardStore, err := redisstate.NewEvaluationGuardStore(redisClient)
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
raceNameIntents, err := racenameintents.NewPublisher(racenameintents.Config{
Publisher: intentPublisher,
Clock: clock,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
capabilityService, err := capabilityevaluation.NewService(capabilityevaluation.Dependencies{
Games: gameStore,
Memberships: membershipStore,
Stats: gameTurnStatsStore,
Directory: directory,
Intents: raceNameIntents,
Guard: evaluationGuardStore,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
gmConsumer, err := gmevents.NewConsumer(gmevents.Config{
Client: redisClient,
Stream: cfg.Redis.GMEventsStream,
BlockTimeout: cfg.Redis.GMEventsReadBlockTimeout,
Games: gameStore,
Stats: gameTurnStatsStore,
Capability: capabilityService,
OffsetStore: streamOffsets,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
pendingRegistrationWorker, err := pendingregistration.NewWorker(pendingregistration.Dependencies{
Directory: directory,
Interval: cfg.PendingRegistration.Interval,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
userLifecycleWorker, err := userlifecycleworker.NewWorker(userlifecycleworker.Dependencies{
Directory: directory,
Memberships: membershipStore,
Applications: applicationStore,
Invites: inviteStore,
Games: gameStore,
RuntimeManager: runtimePublisher,
Intents: intentPublisher,
Clock: clock,
Logger: logger,
Telemetry: telemetryRuntime,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
userLifecycleConsumer, err := userlifecycle.NewConsumer(userlifecycle.Config{
Client: redisClient,
Stream: cfg.Redis.UserLifecycleStream,
BlockTimeout: cfg.Redis.UserLifecycleReadBlockTimeout,
OffsetStore: streamOffsets,
Clock: clock,
Logger: logger,
})
if err != nil {
return nil, fmt.Errorf("new lobby wiring: %w", err)
}
userLifecycleConsumer.OnEvent(userLifecycleWorker.Handle)
return &wiring{
policy: policy,
raceNameDirectory: directory,
gameStore: gameStore,
applicationStore: applicationStore,
inviteStore: inviteStore,
membershipStore: membershipStore,
gapActivationStore: gapActivationStore,
userService: userServiceClient,
intentPublisher: intentPublisher,
idGenerator: ids,
createGame: createSvc,
updateGame: updateSvc,
openEnrollment: openSvc,
cancelGame: cancelSvc,
manualReadyToStart: manualReadySvc,
submitApplication: submitSvc,
approveApplication: approveSvc,
rejectApplication: rejectSvc,
createInvite: createInviteSvc,
redeemInvite: redeemInviteSvc,
declineInvite: declineInviteSvc,
revokeInvite: revokeInviteSvc,
enrollmentAutomation: enrollmentWorker,
runtimeManager: runtimePublisher,
gmClient: gmClientImpl,
streamOffsetStore: streamOffsets,
startGame: startSvc,
retryStartGame: retrySvc,
pauseGame: pauseSvc,
resumeGame: resumeSvc,
removeMember: removeMemberSvc,
blockMember: blockMemberSvc,
registerRaceName: registerRaceNameSvc,
listMyRaceNames: listMyRaceNamesSvc,
getGame: getGameSvc,
listGames: listGamesSvc,
listMemberships: listMembershipsSvc,
listMyGames: listMyGamesSvc,
listMyApplications: listMyApplicationsSvc,
listMyInvites: listMyInvitesSvc,
runtimeJobResultConsumer: runtimeConsumer,
gameTurnStatsStore: gameTurnStatsStore,
evaluationGuardStore: evaluationGuardStore,
capabilityEvaluation: capabilityService,
gmEventsConsumer: gmConsumer,
pendingRegistration: pendingRegistrationWorker,
userLifecycleConsumer: userLifecycleConsumer,
userLifecycleWorker: userLifecycleWorker,
}, nil
}
// buildRaceNameDirectory instantiates the Race Name Directory adapter
// selected by cfg.RaceNameDirectory.Backend.
func buildRaceNameDirectory(
cfg config.Config,
redisClient *redis.Client,
policy *racename.Policy,
clock func() time.Time,
) (ports.RaceNameDirectory, error) {
switch cfg.RaceNameDirectory.Backend {
case config.RaceNameDirectoryBackendRedis:
if redisClient == nil {
return nil, errors.New("redis race name directory backend requires a Redis client")
}
return redisstate.NewRaceNameDirectory(
redisClient,
policy,
redisstate.WithRaceNameDirectoryClock(clock),
)
case config.RaceNameDirectoryBackendStub:
return racenamestub.NewDirectory(racenamestub.WithClock(clock))
default:
return nil, fmt.Errorf("unsupported race name directory backend %q", cfg.RaceNameDirectory.Backend)
}
}