package lobby import ( "context" "github.com/google/uuid" "go.uber.org/zap" ) // EntitlementProvider is the read-only view the lobby needs over the // user-domain entitlement snapshot. The canonical implementation is // `*user.Service` exposing `GetEntitlement(ctx, userID)`; tests substitute // a fake. // // `MaxRegisteredRaceNames` is the only field consumed by when // the caller attempts to register a `pending_registration` row the lobby // counts already-`registered` rows for that user against this limit. type EntitlementProvider interface { GetMaxRegisteredRaceNames(ctx context.Context, userID uuid.UUID) (int32, error) } // RuntimeGateway is the outbound surface the lobby uses to ask the runtime // module to start, pause, resume, or stop an engine container. The real // implementation lives in `backend/internal/runtime` ; until // then `NewNoopRuntimeGateway` ships a logger-only stub that pretends the // request was accepted so the lobby state machine stays exercisable // end-to-end. type RuntimeGateway interface { StartGame(ctx context.Context, gameID uuid.UUID) error StopGame(ctx context.Context, gameID uuid.UUID) error PauseGame(ctx context.Context, gameID uuid.UUID) error ResumeGame(ctx context.Context, gameID uuid.UUID) error } // RuntimeJobResult is the inbound shape used by the runtime reconciler // when a labelled container that lobby believes is alive has // disappeared. The wiring connects `Service.OnRuntimeJobResult` against // this type; the no-op consumer logs the event at debug level. type RuntimeJobResult struct { Op string Status string Message string } // NotificationPublisher is the outbound surface the lobby uses to fan out // notification intents (invite received, application submitted, race name // promoted, etc.). The real implementation lives in // `backend/internal/notification` ; until then // `NewNoopNotificationPublisher` ships a logger-only stub. type NotificationPublisher interface { PublishLobbyEvent(ctx context.Context, intent LobbyNotification) error } // LobbyNotification is the open shape carried by a notification intent. // The implementation emits a small set of `Kind` values matching the catalog in // `backend/README.md` §10. The `Payload` map is the kind-specific data // blob; recipients are the user_ids the intent should reach. // // The struct lives in the lobby package on purpose: it is the producer // vocabulary. The implementation will reuse it as the notification.Submit input // (or wrap it in a domain-side type, if more channels show up). type LobbyNotification struct { Kind string IdempotencyKey string Recipients []uuid.UUID Payload map[string]any } // NewNoopRuntimeGateway returns a RuntimeGateway that logs every call at // debug level and returns nil. The lobby state machine treats the no-op // as "request was accepted asynchronously" — the game stays in `starting` // until the canonical implementation wires real `runtime` / `OnRuntimeSnapshot` interactions. func NewNoopRuntimeGateway(logger *zap.Logger) RuntimeGateway { if logger == nil { logger = zap.NewNop() } return &noopRuntimeGateway{logger: logger.Named("lobby.runtime.noop")} } type noopRuntimeGateway struct { logger *zap.Logger } func (g *noopRuntimeGateway) StartGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop start-game", zap.String("game_id", gameID.String())) return nil } func (g *noopRuntimeGateway) StopGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop stop-game", zap.String("game_id", gameID.String())) return nil } func (g *noopRuntimeGateway) PauseGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop pause-game", zap.String("game_id", gameID.String())) return nil } func (g *noopRuntimeGateway) ResumeGame(_ context.Context, gameID uuid.UUID) error { g.logger.Debug("noop resume-game", zap.String("game_id", gameID.String())) return nil } // NewNoopNotificationPublisher returns a NotificationPublisher that logs // every call at debug level and returns nil. The implementation will swap in a // real publisher backed by `notification.Submit`. func NewNoopNotificationPublisher(logger *zap.Logger) NotificationPublisher { if logger == nil { logger = zap.NewNop() } return &noopNotificationPublisher{logger: logger.Named("lobby.notify.noop")} } type noopNotificationPublisher struct { logger *zap.Logger } func (p *noopNotificationPublisher) PublishLobbyEvent(_ context.Context, intent LobbyNotification) error { p.logger.Debug("noop notification", zap.String("kind", intent.Kind), zap.String("idempotency_key", intent.IdempotencyKey), zap.Int("recipients", len(intent.Recipients)), ) return nil }