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 }