package app import ( "database/sql" "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" pgapplicationstore "galaxy/lobby/internal/adapters/postgres/applicationstore" pggamestore "galaxy/lobby/internal/adapters/postgres/gamestore" pginvitestore "galaxy/lobby/internal/adapters/postgres/invitestore" pgmembershipstore "galaxy/lobby/internal/adapters/postgres/membershipstore" pgracenamedir "galaxy/lobby/internal/adapters/postgres/racenamedir" "galaxy/lobby/internal/adapters/racenameinmem" "galaxy/lobby/internal/adapters/racenameintents" "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/engineimage" "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, pgPool *sql.DB, 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() } if redisClient == nil { return nil, errors.New("new lobby wiring: nil redis client") } if pgPool == nil { return nil, errors.New("new lobby wiring: nil postgres pool") } rawDirectory, err := buildRaceNameDirectory(cfg, pgPool, policy, clock) if err != nil { return nil, fmt.Errorf("new lobby wiring: %w", err) } directory := metricsracenamedir.New(rawDirectory, telemetryRuntime) pgStoreCfg := struct { DB *sql.DB OperationTimeout time.Duration }{ DB: pgPool, OperationTimeout: cfg.Postgres.Conn.OperationTimeout, } gameStore, err := pggamestore.New(pggamestore.Config{ DB: pgStoreCfg.DB, OperationTimeout: pgStoreCfg.OperationTimeout, }) if err != nil { return nil, fmt.Errorf("new lobby wiring: %w", err) } applicationStore, err := pgapplicationstore.New(pgapplicationstore.Config{ DB: pgStoreCfg.DB, OperationTimeout: pgStoreCfg.OperationTimeout, }) if err != nil { return nil, fmt.Errorf("new lobby wiring: %w", err) } inviteStore, err := pginvitestore.New(pginvitestore.Config{ DB: pgStoreCfg.DB, OperationTimeout: pgStoreCfg.OperationTimeout, }) if err != nil { return nil, fmt.Errorf("new lobby wiring: %w", err) } membershipStore, err := pgmembershipstore.New(pgmembershipstore.Config{ DB: pgStoreCfg.DB, OperationTimeout: pgStoreCfg.OperationTimeout, }) 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) } engineImageResolver, err := engineimage.NewResolver(cfg.RuntimeManager.EngineImageTemplate) 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, ImageResolver: engineImageResolver, 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, pgPool *sql.DB, policy *racename.Policy, clock func() time.Time, ) (ports.RaceNameDirectory, error) { switch cfg.RaceNameDirectory.Backend { case config.RaceNameDirectoryBackendPostgres: if pgPool == nil { return nil, errors.New("postgres race name directory backend requires a Postgres pool") } return pgracenamedir.New(pgracenamedir.Config{ DB: pgPool, OperationTimeout: cfg.Postgres.Conn.OperationTimeout, Policy: policy, Clock: clock, }) case config.RaceNameDirectoryBackendStub: return racenameinmem.NewDirectory(racenameinmem.WithClock(clock)) default: return nil, fmt.Errorf("unsupported race name directory backend %q", cfg.RaceNameDirectory.Backend) } }