feat: game lobby service
This commit is contained in:
@@ -0,0 +1,106 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
"galaxy/lobby/internal/domain/invite"
|
||||
)
|
||||
|
||||
// InviteStore stores invite records and their secondary indexes. Adapters
|
||||
// are responsible for maintaining the per-game set and per-invitee set
|
||||
// together with the record.
|
||||
type InviteStore interface {
|
||||
// Save persists a new created invite record. Save rejects records
|
||||
// whose status is not created and is create-only: re-saving an
|
||||
// existing invite id returns invite.ErrConflict.
|
||||
Save(ctx context.Context, record invite.Invite) error
|
||||
|
||||
// Get returns the record identified by inviteID. It returns
|
||||
// invite.ErrNotFound when no record exists.
|
||||
Get(ctx context.Context, inviteID common.InviteID) (invite.Invite, error)
|
||||
|
||||
// GetByGame returns every invite attached to gameID. The order is
|
||||
// adapter-defined; callers may reorder as needed.
|
||||
GetByGame(ctx context.Context, gameID common.GameID) ([]invite.Invite, error)
|
||||
|
||||
// GetByUser returns every invite addressed to inviteeUserID. The
|
||||
// order is adapter-defined; callers may reorder as needed.
|
||||
GetByUser(ctx context.Context, inviteeUserID string) ([]invite.Invite, error)
|
||||
|
||||
// GetByInviter returns every invite created by inviterUserID. The
|
||||
// order is adapter-defined; callers may reorder as needed. The
|
||||
// secondary index is maintained alongside the per-game and per-user
|
||||
// indexes; cascade-release callers read it without touching
|
||||
// the per-game listings.
|
||||
GetByInviter(ctx context.Context, inviterUserID string) ([]invite.Invite, error)
|
||||
|
||||
// UpdateStatus applies one status transition in a compare-and-swap
|
||||
// fashion. The adapter must first call invite.Transition to reject
|
||||
// invalid pairs without touching the store; on success it must
|
||||
// verify that the current status equals input.ExpectedFrom, update
|
||||
// the primary record, set DecidedAt to input.At, and apply
|
||||
// input.RaceName when transitioning to redeemed. input.RaceName must
|
||||
// be non-empty when To is redeemed and empty otherwise.
|
||||
UpdateStatus(ctx context.Context, input UpdateInviteStatusInput) error
|
||||
}
|
||||
|
||||
// UpdateInviteStatusInput stores the arguments required to apply one
|
||||
// status transition through an InviteStore.
|
||||
type UpdateInviteStatusInput struct {
|
||||
// InviteID identifies the record to mutate.
|
||||
InviteID common.InviteID
|
||||
|
||||
// ExpectedFrom stores the status the caller believes the record
|
||||
// currently has. A mismatch results in invite.ErrConflict.
|
||||
ExpectedFrom invite.Status
|
||||
|
||||
// To stores the destination status.
|
||||
To invite.Status
|
||||
|
||||
// At stores the wall-clock used for DecidedAt.
|
||||
At time.Time
|
||||
|
||||
// RaceName carries the invitee's confirmed in-game name. It is
|
||||
// required when To is redeemed and must be empty otherwise.
|
||||
RaceName string
|
||||
}
|
||||
|
||||
// Validate reports whether input contains a structurally valid status
|
||||
// transition request.
|
||||
func (input UpdateInviteStatusInput) Validate() error {
|
||||
if err := input.InviteID.Validate(); err != nil {
|
||||
return fmt.Errorf("update invite status: invite id: %w", err)
|
||||
}
|
||||
if !input.ExpectedFrom.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update invite status: expected from status %q is unsupported",
|
||||
input.ExpectedFrom,
|
||||
)
|
||||
}
|
||||
if !input.To.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update invite status: to status %q is unsupported",
|
||||
input.To,
|
||||
)
|
||||
}
|
||||
if input.At.IsZero() {
|
||||
return fmt.Errorf("update invite status: at must not be zero")
|
||||
}
|
||||
if input.To == invite.StatusRedeemed {
|
||||
if strings.TrimSpace(input.RaceName) == "" {
|
||||
return fmt.Errorf(
|
||||
"update invite status: race name must not be empty when redeeming",
|
||||
)
|
||||
}
|
||||
} else if input.RaceName != "" {
|
||||
return fmt.Errorf(
|
||||
"update invite status: race name must be empty when transitioning to %q",
|
||||
input.To,
|
||||
)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user