feat: game lobby service
This commit is contained in:
@@ -0,0 +1,169 @@
|
||||
// Package ports defines the stable interfaces that connect Game Lobby
|
||||
// Service use cases to external state and external services.
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
"galaxy/lobby/internal/domain/game"
|
||||
)
|
||||
|
||||
// GameStore stores game records and their secondary indexes. Adapters are
|
||||
// responsible for maintaining the status index together with the record.
|
||||
type GameStore interface {
|
||||
// Save upserts record. It is used for draft creation and for
|
||||
// field-only edits. The adapter must rewrite the status secondary
|
||||
// index when the status field changes. Save does not apply the
|
||||
// domain transition gate; callers that intend a status transition
|
||||
// must use UpdateStatus instead.
|
||||
Save(ctx context.Context, record game.Game) error
|
||||
|
||||
// Get returns the record identified by gameID. It returns
|
||||
// game.ErrNotFound when no record exists.
|
||||
Get(ctx context.Context, gameID common.GameID) (game.Game, error)
|
||||
|
||||
// GetByStatus returns every record currently indexed under status.
|
||||
// The slice is ordered by the created-at score ascending; callers
|
||||
// may reorder as needed.
|
||||
GetByStatus(ctx context.Context, status game.Status) ([]game.Game, error)
|
||||
|
||||
// CountByStatus returns the number of game records indexed under
|
||||
// each known status. The map carries one entry per game.Status from
|
||||
// game.AllStatuses, with zero counts for empty buckets. Telemetry
|
||||
// uses the result to emit the `lobby.active_games` observable gauge
|
||||
// without scanning record payloads.
|
||||
CountByStatus(ctx context.Context) (map[game.Status]int, error)
|
||||
|
||||
// GetByOwner returns every record whose OwnerUserID equals userID.
|
||||
// The order is adapter-defined; callers may reorder as needed. The
|
||||
// secondary index is maintained alongside the per-status index;
|
||||
// cascade-release callers consume it without touching the
|
||||
// status listings.
|
||||
GetByOwner(ctx context.Context, userID string) ([]game.Game, error)
|
||||
|
||||
// UpdateStatus applies one status transition in a compare-and-swap
|
||||
// fashion. The adapter must first call game.Transition to reject
|
||||
// invalid triplets without touching the store; on success it must
|
||||
// verify that the current status equals input.ExpectedFrom, update
|
||||
// the primary record, and rewrite the status secondary index.
|
||||
// Adapters set StartedAt when transitioning to running and
|
||||
// FinishedAt when transitioning to finished.
|
||||
UpdateStatus(ctx context.Context, input UpdateStatusInput) error
|
||||
|
||||
// UpdateRuntimeSnapshot overwrites the denormalized runtime snapshot
|
||||
// fields on the record identified by input.GameID. It does not
|
||||
// mutate the status field or the status secondary index.
|
||||
UpdateRuntimeSnapshot(ctx context.Context, input UpdateRuntimeSnapshotInput) error
|
||||
|
||||
// UpdateRuntimeBinding overwrites the runtime binding metadata on the
|
||||
// record identified by input.GameID. It does not mutate the status
|
||||
// field or the status secondary index. The binding must satisfy the
|
||||
// domain invariants (see game.RuntimeBinding.Validate). The adapter
|
||||
// returns game.ErrNotFound when no record exists.
|
||||
UpdateRuntimeBinding(ctx context.Context, input UpdateRuntimeBindingInput) error
|
||||
}
|
||||
|
||||
// UpdateStatusInput stores the arguments required to apply one status
|
||||
// transition through a GameStore.
|
||||
type UpdateStatusInput struct {
|
||||
// GameID identifies the record to mutate.
|
||||
GameID common.GameID
|
||||
|
||||
// ExpectedFrom stores the status the caller believes the record
|
||||
// currently has. A mismatch results in game.ErrConflict.
|
||||
ExpectedFrom game.Status
|
||||
|
||||
// To stores the destination status.
|
||||
To game.Status
|
||||
|
||||
// Trigger stores the transition trigger used by the domain gate.
|
||||
Trigger game.Trigger
|
||||
|
||||
// At stores the wall-clock used for UpdatedAt, and for StartedAt or
|
||||
// FinishedAt when the destination status requires it.
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input contains a structurally valid status
|
||||
// transition request.
|
||||
func (input UpdateStatusInput) Validate() error {
|
||||
if err := input.GameID.Validate(); err != nil {
|
||||
return fmt.Errorf("update status: game id: %w", err)
|
||||
}
|
||||
if !input.ExpectedFrom.IsKnown() {
|
||||
return fmt.Errorf("update status: expected from status %q is unsupported", input.ExpectedFrom)
|
||||
}
|
||||
if !input.To.IsKnown() {
|
||||
return fmt.Errorf("update status: to status %q is unsupported", input.To)
|
||||
}
|
||||
if !input.Trigger.IsKnown() {
|
||||
return fmt.Errorf("update status: trigger %q is unsupported", input.Trigger)
|
||||
}
|
||||
if input.At.IsZero() {
|
||||
return fmt.Errorf("update status: at must not be zero")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRuntimeSnapshotInput stores the arguments required to update the
|
||||
// denormalized runtime snapshot on one game record.
|
||||
type UpdateRuntimeSnapshotInput struct {
|
||||
// GameID identifies the record to mutate.
|
||||
GameID common.GameID
|
||||
|
||||
// Snapshot stores the new snapshot values to persist.
|
||||
Snapshot game.RuntimeSnapshot
|
||||
|
||||
// At stores the wall-clock used for UpdatedAt.
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input contains a structurally valid runtime
|
||||
// snapshot update request.
|
||||
func (input UpdateRuntimeSnapshotInput) Validate() error {
|
||||
if err := input.GameID.Validate(); err != nil {
|
||||
return fmt.Errorf("update runtime snapshot: game id: %w", err)
|
||||
}
|
||||
if input.Snapshot.CurrentTurn < 0 {
|
||||
return fmt.Errorf("update runtime snapshot: current turn must not be negative")
|
||||
}
|
||||
if input.At.IsZero() {
|
||||
return fmt.Errorf("update runtime snapshot: at must not be zero")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateRuntimeBindingInput stores the arguments required to persist
|
||||
// runtime binding metadata on one game record.
|
||||
type UpdateRuntimeBindingInput struct {
|
||||
// GameID identifies the record to mutate.
|
||||
GameID common.GameID
|
||||
|
||||
// Binding stores the runtime binding values to persist. The adapter
|
||||
// validates the binding before writing.
|
||||
Binding game.RuntimeBinding
|
||||
|
||||
// At stores the wall-clock used for UpdatedAt.
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input contains a structurally valid runtime
|
||||
// binding update request.
|
||||
func (input UpdateRuntimeBindingInput) Validate() error {
|
||||
if err := input.GameID.Validate(); err != nil {
|
||||
return fmt.Errorf("update runtime binding: game id: %w", err)
|
||||
}
|
||||
if err := input.Binding.Validate(); err != nil {
|
||||
return fmt.Errorf("update runtime binding: %w", err)
|
||||
}
|
||||
if input.At.IsZero() {
|
||||
return fmt.Errorf("update runtime binding: at must not be zero")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user