Files
galaxy-game/lobby/internal/ports/gamestore.go
T
2026-04-25 23:20:55 +02:00

170 lines
6.2 KiB
Go

// 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
}