feat: game lobby service
This commit is contained in:
@@ -0,0 +1,89 @@
|
||||
package ports
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"galaxy/lobby/internal/domain/common"
|
||||
"galaxy/lobby/internal/domain/membership"
|
||||
)
|
||||
|
||||
// MembershipStore stores membership records and their secondary indexes.
|
||||
// Adapters are responsible for maintaining the per-game set and per-user
|
||||
// set together with the record.
|
||||
type MembershipStore interface {
|
||||
// Save persists a new active membership record. Save rejects records
|
||||
// whose status is not active and is create-only: re-saving an
|
||||
// existing membership id returns membership.ErrConflict.
|
||||
Save(ctx context.Context, record membership.Membership) error
|
||||
|
||||
// Get returns the record identified by membershipID. It returns
|
||||
// membership.ErrNotFound when no record exists.
|
||||
Get(ctx context.Context, membershipID common.MembershipID) (membership.Membership, error)
|
||||
|
||||
// GetByGame returns every membership attached to gameID. The order
|
||||
// is adapter-defined; callers may reorder as needed.
|
||||
GetByGame(ctx context.Context, gameID common.GameID) ([]membership.Membership, error)
|
||||
|
||||
// GetByUser returns every membership held by userID. The order is
|
||||
// adapter-defined; callers may reorder as needed.
|
||||
GetByUser(ctx context.Context, userID string) ([]membership.Membership, error)
|
||||
|
||||
// UpdateStatus applies one status transition in a compare-and-swap
|
||||
// fashion. The adapter must first call membership.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, and set RemovedAt to input.At when
|
||||
// transitioning out of active.
|
||||
UpdateStatus(ctx context.Context, input UpdateMembershipStatusInput) error
|
||||
|
||||
// Delete removes the membership record identified by membershipID
|
||||
// from the primary store and from the per-game and per-user
|
||||
// secondary index sets in one operation. Delete is the pre-start
|
||||
// path of removemember; the post-start path uses
|
||||
// UpdateStatus(active → removed). Delete returns
|
||||
// membership.ErrNotFound when no record exists for the id.
|
||||
Delete(ctx context.Context, membershipID common.MembershipID) error
|
||||
}
|
||||
|
||||
// UpdateMembershipStatusInput stores the arguments required to apply one
|
||||
// status transition through a MembershipStore.
|
||||
type UpdateMembershipStatusInput struct {
|
||||
// MembershipID identifies the record to mutate.
|
||||
MembershipID common.MembershipID
|
||||
|
||||
// ExpectedFrom stores the status the caller believes the record
|
||||
// currently has. A mismatch results in membership.ErrConflict.
|
||||
ExpectedFrom membership.Status
|
||||
|
||||
// To stores the destination status.
|
||||
To membership.Status
|
||||
|
||||
// At stores the wall-clock used for RemovedAt.
|
||||
At time.Time
|
||||
}
|
||||
|
||||
// Validate reports whether input contains a structurally valid status
|
||||
// transition request.
|
||||
func (input UpdateMembershipStatusInput) Validate() error {
|
||||
if err := input.MembershipID.Validate(); err != nil {
|
||||
return fmt.Errorf("update membership status: membership id: %w", err)
|
||||
}
|
||||
if !input.ExpectedFrom.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update membership status: expected from status %q is unsupported",
|
||||
input.ExpectedFrom,
|
||||
)
|
||||
}
|
||||
if !input.To.IsKnown() {
|
||||
return fmt.Errorf(
|
||||
"update membership status: to status %q is unsupported",
|
||||
input.To,
|
||||
)
|
||||
}
|
||||
if input.At.IsZero() {
|
||||
return fmt.Errorf("update membership status: at must not be zero")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user