feat: backend service
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user