90 lines
3.2 KiB
Go
90 lines
3.2 KiB
Go
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
|
|
}
|