Compare commits
	
		
			3 Commits
		
	
	
		
			da9b96de84
			...
			539b20f419
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					539b20f419 | ||
| 
						 | 
					bd38a29b6d | ||
| 
						 | 
					ed6d30ec42 | 
@ -25,53 +25,33 @@ trait Auth[F[_]] {
 | 
				
			|||||||
    * check that room exists, password is valid call to add user to the players create session
 | 
					    * check that room exists, password is valid call to add user to the players create session
 | 
				
			||||||
    * mapping and return cookie
 | 
					    * mapping and return cookie
 | 
				
			||||||
    */
 | 
					    */
 | 
				
			||||||
  def joinRoom(
 | 
					  def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie]
 | 
				
			||||||
      roomName: String,
 | 
					 | 
				
			||||||
      roomPassword: String,
 | 
					 | 
				
			||||||
      nickName: String,
 | 
					 | 
				
			||||||
      nickPassword: String
 | 
					 | 
				
			||||||
  ): F[Either[String, ResponseCookie]]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def deleteSession(
 | 
					  def deleteSession(sessionId: Long): F[Unit]
 | 
				
			||||||
      sessionId: Long
 | 
					 | 
				
			||||||
  ): F[Unit]
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object Auth {
 | 
					object Auth {
 | 
				
			||||||
  type SessionsMap = Map[Long, (RoomID, PlayerID)]
 | 
					  type SessionsMap = Map[Long, (RoomID, PlayerID)]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  class SimpleAuth[F[_]: Sync](
 | 
					  class SimpleAuth[F[_]: Sync](sessions: Ref[F, SessionsMap]) extends Auth[F] {
 | 
				
			||||||
      sessions: Ref[F, SessionsMap],
 | 
					 | 
				
			||||||
      roomService: RoomService[F]
 | 
					 | 
				
			||||||
  ) extends Auth[F] {
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val authcookieName = "authcookie"
 | 
					    val authcookieName = "authcookie"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override def joinRoom(
 | 
					    override def joinRoom(roomId: RoomID, playerId: PlayerID): F[ResponseCookie] = {
 | 
				
			||||||
        roomName: String,
 | 
					 | 
				
			||||||
        roomPassword: String,
 | 
					 | 
				
			||||||
        nickName: String,
 | 
					 | 
				
			||||||
        nickPassword: String
 | 
					 | 
				
			||||||
    ): F[Either[String, ResponseCookie]] = {
 | 
					 | 
				
			||||||
      // TODO check for existing session for same room
 | 
					      // TODO check for existing session for same room
 | 
				
			||||||
      // and do i want to logout if existing session for another room? ugh
 | 
					      // and do i want to logout if existing session for another room? ugh
 | 
				
			||||||
      val roomId = RoomID(roomName)
 | 
					 | 
				
			||||||
      val result = for {
 | 
					 | 
				
			||||||
        playerId <- EitherT(
 | 
					 | 
				
			||||||
          roomService.joinRoom(roomId, nickName, nickPassword, roomPassword)
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
          .leftMap(_.toString())
 | 
					 | 
				
			||||||
      // newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
 | 
					      // newSessionId = Random.nextLong() // TODO return after i stop mocking RoomService
 | 
				
			||||||
        newSessionId = TestModels.testSessionId
 | 
					      val newSessionId = TestModels.testSessionId
 | 
				
			||||||
        _ <- EitherT.liftF(sessions.update(_.updated(newSessionId, (roomId, playerId))))
 | 
					      sessions
 | 
				
			||||||
      } yield ResponseCookie(
 | 
					        .update(_.updated(newSessionId, (roomId, playerId)))
 | 
				
			||||||
 | 
					        .as(
 | 
				
			||||||
 | 
					          ResponseCookie(
 | 
				
			||||||
            name = authcookieName,
 | 
					            name = authcookieName,
 | 
				
			||||||
            content = newSessionId.toString(),
 | 
					            content = newSessionId.toString(),
 | 
				
			||||||
            secure = true
 | 
					            secure = true
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
      result.value
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    override def authUser
 | 
					    override def authUser
 | 
				
			||||||
@ -92,8 +72,8 @@ object Auth {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  def make[F[_]: Sync](roomsService: RoomService[F]): F[Auth[F]] =
 | 
					  def make[F[_]: Sync](): F[Auth[F]] =
 | 
				
			||||||
    for {
 | 
					    for {
 | 
				
			||||||
      sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions)
 | 
					      sessionsMap <- Ref.of[F, SessionsMap](TestModels.testSessions)
 | 
				
			||||||
    } yield new SimpleAuth(sessionsMap, roomsService)
 | 
					    } yield new SimpleAuth(sessionsMap)
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -14,8 +14,8 @@ object BackendApp extends IOApp {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    val wiring = for {
 | 
					    val wiring = for {
 | 
				
			||||||
      roomService <- Resource.eval(RoomService.make[IO])
 | 
					      roomService <- Resource.eval(RoomService.make[IO])
 | 
				
			||||||
      auth <- Resource.eval(Auth.make(roomService))
 | 
					      auth <- Resource.eval(Auth.make[IO]())
 | 
				
			||||||
      httpService = MyHttpService.create(auth)
 | 
					      httpService = MyHttpService.create(auth, roomService)
 | 
				
			||||||
      server <- EmberServerBuilder
 | 
					      server <- EmberServerBuilder
 | 
				
			||||||
        .default[IO]
 | 
					        .default[IO]
 | 
				
			||||||
        .withHost(host)
 | 
					        .withHost(host)
 | 
				
			||||||
@ -25,9 +25,7 @@ object BackendApp extends IOApp {
 | 
				
			|||||||
    } yield server
 | 
					    } yield server
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    wiring.use(server =>
 | 
					    wiring.use(server =>
 | 
				
			||||||
      IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never
 | 
					      IO.delay(println(s"Server Has Started at ${server.address}")) >> IO.never.as(ExitCode.Success))
 | 
				
			||||||
        .as(ExitCode.Success)
 | 
					 | 
				
			||||||
    )
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -4,7 +4,6 @@ import cats.effect._
 | 
				
			|||||||
import cats.syntax.all._
 | 
					import cats.syntax.all._
 | 
				
			||||||
import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
 | 
					import org.http4s._, org.http4s.dsl.io._, org.http4s.implicits._
 | 
				
			||||||
import org.http4s.websocket.WebSocketFrame
 | 
					import org.http4s.websocket.WebSocketFrame
 | 
				
			||||||
// import io.circe.generic.auto._
 | 
					 | 
				
			||||||
import io.circe.syntax._
 | 
					import io.circe.syntax._
 | 
				
			||||||
import org.http4s.circe.CirceEntityDecoder._
 | 
					import org.http4s.circe.CirceEntityDecoder._
 | 
				
			||||||
import scala.concurrent.duration._
 | 
					import scala.concurrent.duration._
 | 
				
			||||||
@ -16,8 +15,8 @@ import org.http4s.server.AuthMiddleware.apply
 | 
				
			|||||||
import org.http4s.server.AuthMiddleware
 | 
					import org.http4s.server.AuthMiddleware
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object MyHttpService {
 | 
					object MyHttpService {
 | 
				
			||||||
  def create(auth: Auth[IO])(
 | 
					  def create(auth: Auth[IO], roomService: RoomService[IO])(
 | 
				
			||||||
      wsb: WebSocketBuilder[cats.effect.IO]
 | 
					      wsb: WebSocketBuilder[IO]
 | 
				
			||||||
  ): HttpApp[cats.effect.IO] = {
 | 
					  ): HttpApp[cats.effect.IO] = {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] =
 | 
					    val authedRoomRoutes: AuthedRoutes[(PlayerID, RoomID), IO] =
 | 
				
			||||||
@ -56,26 +55,23 @@ object MyHttpService {
 | 
				
			|||||||
    val authMiddleware = AuthMiddleware(auth.authUser)
 | 
					    val authMiddleware = AuthMiddleware(auth.authUser)
 | 
				
			||||||
    val aa = authMiddleware(authedRoomRoutes)
 | 
					    val aa = authMiddleware(authedRoomRoutes)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    import cats.data.EitherT
 | 
				
			||||||
    val authenticationRoute = HttpRoutes
 | 
					    val authenticationRoute = HttpRoutes
 | 
				
			||||||
      .of[IO] {
 | 
					      .of[IO] {
 | 
				
			||||||
        case req @ POST -> Root / "login" => {
 | 
					        case req @ POST -> Root / "login" => {
 | 
				
			||||||
          for {
 | 
					          val responseOrError = for {
 | 
				
			||||||
            data <- req.as[Requests.LogIn]
 | 
					            data <- EitherT.right(req.as[Requests.LogIn])
 | 
				
			||||||
            authResult <- auth.joinRoom(
 | 
					            Requests.LogIn(roomName, nickName, roomPassword, nickPassword) = data
 | 
				
			||||||
              data.roomName,
 | 
					            roomId = RoomID(roomName)
 | 
				
			||||||
              data.password,
 | 
					            playerId <- EitherT(roomService.joinRoom(roomId, nickName, nickPassword, roomPassword))
 | 
				
			||||||
              data.nickname,
 | 
					            authCookie <- EitherT.liftF(auth.joinRoom(roomId, playerId))
 | 
				
			||||||
              data.nickPassword
 | 
					            _ <- EitherT.liftF(IO(println(s"> logging in $nickName to $roomName")))
 | 
				
			||||||
            )
 | 
					            resp <- EitherT.liftF(Ok().flatTap(resp => IO(resp.addCookie(authCookie))))
 | 
				
			||||||
            resp <- authResult match {
 | 
					 | 
				
			||||||
              case Left(error) =>
 | 
					 | 
				
			||||||
                Forbidden(error)
 | 
					 | 
				
			||||||
              case Right(authCookie) => {
 | 
					 | 
				
			||||||
                IO(println(s"> logging in ${data.nickname} to ${data.roomName}")) >>
 | 
					 | 
				
			||||||
                  Ok().map(_.addCookie(authCookie))
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
          } yield resp
 | 
					          } yield resp
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          val response = responseOrError.leftSemiflatMap(error => Forbidden(error.toString())).merge
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          response
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
				
			|||||||
@ -1,8 +1,10 @@
 | 
				
			|||||||
package industries.sunshine.planningpoker
 | 
					package industries.sunshine.planningpoker
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import industries.sunshine.planningpoker.common.Models.*
 | 
					import industries.sunshine.planningpoker.common.Models.*
 | 
				
			||||||
import cats.effect.{Ref, Sync}
 | 
					import cats.effect.{Ref, Concurrent}
 | 
				
			||||||
import cats.syntax.all._
 | 
					import cats.syntax.all._
 | 
				
			||||||
 | 
					import fs2.Stream
 | 
				
			||||||
 | 
					import fs2.concurrent.Topic
 | 
				
			||||||
import cats.data.EitherT
 | 
					import cats.data.EitherT
 | 
				
			||||||
 | 
					
 | 
				
			||||||
enum RoomError {
 | 
					enum RoomError {
 | 
				
			||||||
@ -23,38 +25,69 @@ trait RoomService[F[_]] {
 | 
				
			|||||||
  ): F[Either[RoomError, PlayerID]]
 | 
					  ): F[Either[RoomError, PlayerID]]
 | 
				
			||||||
  def deleteRoom(roomID: RoomID): F[Unit]
 | 
					  def deleteRoom(roomID: RoomID): F[Unit]
 | 
				
			||||||
  def getRoom(roomID: RoomID): F[Option[Room]]
 | 
					  def getRoom(roomID: RoomID): F[Option[Room]]
 | 
				
			||||||
 | 
					  def subscribe(roomID: RoomID): Stream[F, Room]
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) extends RoomService[F] {
 | 
					class InMemoryRoomService[F[_]: Concurrent](stateRef: Ref[F, Map[RoomID, (Room, Topic[F, Room])]])
 | 
				
			||||||
 | 
					    extends RoomService[F] {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = {
 | 
					  override def createRoom(newRoom: Room): F[Either[RoomError, Room]] = {
 | 
				
			||||||
    stateRef.modify { rooms =>
 | 
					    for {
 | 
				
			||||||
 | 
					      updatesTopic <- Topic[F, Room]
 | 
				
			||||||
 | 
					      room <- stateRef.modify { rooms =>
 | 
				
			||||||
        rooms.get(newRoom.id) match {
 | 
					        rooms.get(newRoom.id) match {
 | 
				
			||||||
          case Some(_) =>
 | 
					          case Some(_) =>
 | 
				
			||||||
            rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
 | 
					            rooms -> RoomError.RoomAlreadyExists(newRoom.id.name).asLeft[Room]
 | 
				
			||||||
          case None =>
 | 
					          case None =>
 | 
				
			||||||
          (rooms.updated(newRoom.id, newRoom)) -> newRoom.asRight[RoomError]
 | 
					            (rooms.updated(newRoom.id, (newRoom, updatesTopic))) -> newRoom.asRight[RoomError]
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					    } yield room
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  override def updateRoom(room: Room): F[Unit] = stateRef.update { state =>
 | 
					  override def updateRoom(room: Room): F[Unit] = {
 | 
				
			||||||
    state.get(room.id).fold(state)(oldRoom => state.updated(room.id, room))
 | 
					    for {
 | 
				
			||||||
 | 
					      // modify is function to update state and compute auxillary value to return, here - topic
 | 
				
			||||||
 | 
					      topic <- stateRef.modify[Topic[F, Room]] { state =>
 | 
				
			||||||
 | 
					        state.get(room.id) match {
 | 
				
			||||||
 | 
					          case Some((oldRoom, topic)) => state.updated(room.id, (room, topic)) -> topic
 | 
				
			||||||
 | 
					          case None =>
 | 
				
			||||||
 | 
					            throw new IllegalStateException(s"updateRoom with ${room.id} on nonexistent room")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      _ <- topic.publish1(room) // update and publish are not atomic, sadly races can happen
 | 
				
			||||||
 | 
					    } yield ()
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def deleteRoom(roomID: RoomID): F[Unit] = stateRef.update(_.removed(roomID))
 | 
					  override def deleteRoom(roomID: RoomID): F[Unit] = {
 | 
				
			||||||
 | 
					    for {
 | 
				
			||||||
 | 
					      topic <- stateRef.modify[Topic[F, Room]](state =>
 | 
				
			||||||
 | 
					        state.get(roomID) match {
 | 
				
			||||||
 | 
					          case Some((oldRoom, topic)) => state.removed(roomID) -> topic
 | 
				
			||||||
 | 
					          case None =>
 | 
				
			||||||
 | 
					            throw new IllegalStateException(s"call to delete with $roomID on nonexistent room")
 | 
				
			||||||
 | 
					          // TODO - i'd prefer to swallow these errors
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					      _ <- topic.close
 | 
				
			||||||
 | 
					    } yield ()
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def getRoom(roomID: RoomID): F[Option[Room]] = stateRef.get.map(_.get(roomID))
 | 
					  override def getRoom(roomID: RoomID): F[Option[Room]] = {
 | 
				
			||||||
 | 
					    stateRef.get.map(_.get(roomID).map(_._1))
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  override def joinRoom(
 | 
					  override def joinRoom(
 | 
				
			||||||
      id: RoomID,
 | 
					      id: RoomID,
 | 
				
			||||||
      nickName: String,
 | 
					      nickName: String,
 | 
				
			||||||
      nickPassword: String,
 | 
					      nickPassword: String,
 | 
				
			||||||
      roomPassword: String
 | 
					      roomPassword: String
 | 
				
			||||||
  ): F[Either[RoomError, PlayerID]] = stateRef.modify { rooms =>
 | 
					  ): F[Either[RoomError, PlayerID]] = {
 | 
				
			||||||
    // need to cover cases:
 | 
					 | 
				
			||||||
    // - player already present, then return as is, i guess
 | 
					 | 
				
			||||||
    // - nick not known - add new player and new nick-password mapping
 | 
					 | 
				
			||||||
    // - nick known - add new player
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /** pure function that adds the player to the room need to cover cases:
 | 
				
			||||||
 | 
					      *   - player already present, then return as is, i guess
 | 
				
			||||||
 | 
					      *   - nick not known - add new player and new nick-password mapping
 | 
				
			||||||
 | 
					      *   - nick known - add new player
 | 
				
			||||||
 | 
					      */
 | 
				
			||||||
    def addPlayer(room: Room): (PlayerID, Room) = {
 | 
					    def addPlayer(room: Room): (PlayerID, Room) = {
 | 
				
			||||||
      room.players.find(_.name == nickName) match {
 | 
					      room.players.find(_.name == nickName) match {
 | 
				
			||||||
        case Some(player) => player.id -> room
 | 
					        case Some(player) => player.id -> room
 | 
				
			||||||
@ -71,8 +104,19 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    val joiningWithChecks = for {
 | 
					    /** to be executed under Ref.modify (i.e with state acquired) checks of whether player can be
 | 
				
			||||||
      room <- rooms.get(id).toRight(RoomError.RoomMissing(id.name))
 | 
					      * added to the room:
 | 
				
			||||||
 | 
					      *   - room password is correct
 | 
				
			||||||
 | 
					      *   - nickname is either not taken, or correct password was provided
 | 
				
			||||||
 | 
					      * @returns
 | 
				
			||||||
 | 
					      *   playerId (new or existing), updatedRoom (to be put into state), topic (to send the udpdate
 | 
				
			||||||
 | 
					      *   notification)
 | 
				
			||||||
 | 
					      */
 | 
				
			||||||
 | 
					    def getWithChecks(
 | 
				
			||||||
 | 
					        rooms: Map[RoomID, (Room, Topic[F, Room])]
 | 
				
			||||||
 | 
					    ): Either[RoomError, (PlayerID, Room, Topic[F, Room])] = for {
 | 
				
			||||||
 | 
					      roomAndTopic <- rooms.get(id).toRight(RoomError.RoomMissing(id.name))
 | 
				
			||||||
 | 
					      (room, topic) = roomAndTopic
 | 
				
			||||||
      _ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect)
 | 
					      _ <- Either.cond(room.password == roomPassword, (), RoomError.RoomPassIncorrect)
 | 
				
			||||||
      isNickPassCorrect = room.playersPasswords
 | 
					      isNickPassCorrect = room.playersPasswords
 | 
				
			||||||
        .get(nickName)
 | 
					        .get(nickName)
 | 
				
			||||||
@ -83,14 +127,54 @@ class InMemoryRoomService[F[_]: Sync](stateRef: Ref[F, Map[RoomID, Room]]) exten
 | 
				
			|||||||
        RoomError.NickPassIncorrect
 | 
					        RoomError.NickPassIncorrect
 | 
				
			||||||
      )
 | 
					      )
 | 
				
			||||||
      (playerId, updatedRoom) = addPlayer(room)
 | 
					      (playerId, updatedRoom) = addPlayer(room)
 | 
				
			||||||
    } yield playerId
 | 
					    } yield (playerId, updatedRoom, topic)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    rooms -> joiningWithChecks
 | 
					    // modify returns tuple (updatedState, valueToReturn)
 | 
				
			||||||
 | 
					    // this particular update either keeps room as is, or adds the player
 | 
				
			||||||
 | 
					    // and returns playerId and topic to be used outside
 | 
				
			||||||
 | 
					    //
 | 
				
			||||||
 | 
					    // NOTE here i have a lot of handwaving to pass topic outside
 | 
				
			||||||
 | 
					    // because it's not possible to send F[Unit] update
 | 
				
			||||||
 | 
					    // inside of the stateRef update (which works with pure functions?),
 | 
				
			||||||
 | 
					    // so room notification change has to be returned to outside
 | 
				
			||||||
 | 
					    val maybeUpdatedStateAndNotification = stateRef.modify { rooms =>
 | 
				
			||||||
 | 
					      val maybeAddedUser = getWithChecks(rooms)
 | 
				
			||||||
 | 
					      val updatedState =
 | 
				
			||||||
 | 
					        maybeAddedUser.fold(
 | 
				
			||||||
 | 
					          _ => rooms,
 | 
				
			||||||
 | 
					          { case (playerId, updatedRoom, topic) => rooms.updated(id, (updatedRoom, topic)) }
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					      val toReturn = maybeAddedUser.map { case (id, updatedRoom, topic) =>
 | 
				
			||||||
 | 
					        (id, topic.publish1(updatedRoom).void)
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      updatedState -> toReturn
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // now combining the effects : getting (updatedState & notificationEffect) or error
 | 
				
			||||||
 | 
					    // executing notification
 | 
				
			||||||
 | 
					    // returning only playerId
 | 
				
			||||||
 | 
					    val result = for {
 | 
				
			||||||
 | 
					      updatedState <- EitherT(maybeUpdatedStateAndNotification)
 | 
				
			||||||
 | 
					      (playerId, notification) = updatedState
 | 
				
			||||||
 | 
					      _ <- EitherT.liftF(notification)
 | 
				
			||||||
 | 
					    } yield (playerId)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    result.value
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  override def subscribe(roomID: RoomID): Stream[F, Room] =
 | 
				
			||||||
 | 
					    Stream
 | 
				
			||||||
 | 
					      .eval(stateRef.get)
 | 
				
			||||||
 | 
					      .flatMap(rooms =>
 | 
				
			||||||
 | 
					        rooms.get(roomID) match {
 | 
				
			||||||
 | 
					          case Some((room, topic)) => topic.subscribe(10)
 | 
				
			||||||
 | 
					          case None                => Stream.empty
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
object RoomService {
 | 
					object RoomService {
 | 
				
			||||||
  def make[F[_]: Sync]: F[RoomService[F]] = {
 | 
					  def make[F[_]: Concurrent]: F[RoomService[F]] = {
 | 
				
			||||||
    Ref.of[F, Map[RoomID, Room]](TestModels.testRooms).map(new InMemoryRoomService[F](_))
 | 
					    Ref.of[F, Map[RoomID, (Room, Topic[F, Room])]](Map.empty).map(new InMemoryRoomService[F](_))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -20,30 +20,27 @@ object Models {
 | 
				
			|||||||
      players: List[Player],
 | 
					      players: List[Player],
 | 
				
			||||||
      me: PlayerID,
 | 
					      me: PlayerID,
 | 
				
			||||||
      allowedCards: List[String],
 | 
					      allowedCards: List[String],
 | 
				
			||||||
      round: RoundState,
 | 
					      round: RoundStateView,
 | 
				
			||||||
      canCloseRound: Boolean = false
 | 
					      canCloseRound: Boolean = false
 | 
				
			||||||
  ) derives Codec.AsObject {
 | 
					  ) derives Codec.AsObject
 | 
				
			||||||
    def playersCount: Int = players.size
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  object RoomStateView {
 | 
					  object RoomStateView {
 | 
				
			||||||
    val empty = RoomStateView(
 | 
					    val empty = RoomStateView(
 | 
				
			||||||
      List.empty,
 | 
					      List.empty,
 | 
				
			||||||
      PlayerID(0),
 | 
					      PlayerID(0),
 | 
				
			||||||
      List.empty,
 | 
					      List.empty,
 | 
				
			||||||
      RoundState.Voting(None, List.empty),
 | 
					      RoundStateView.Voting(None, List.empty),
 | 
				
			||||||
      false
 | 
					      false
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  enum RoundState derives Codec.AsObject:
 | 
					  enum RoundStateView derives Codec.AsObject:
 | 
				
			||||||
 | 
					 | 
				
			||||||
    /** view state for round before votes are open player can know their vote and who of the other
 | 
					    /** view state for round before votes are open player can know their vote and who of the other
 | 
				
			||||||
      * players have voted
 | 
					      * players have voted
 | 
				
			||||||
      */
 | 
					      */
 | 
				
			||||||
    case Voting(
 | 
					    case Voting(
 | 
				
			||||||
        myCard: Option[String],
 | 
					        myCard: Option[String],
 | 
				
			||||||
        alreadyVoted: List[Player]
 | 
					        alreadyVoted: List[PlayerID]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /** view state for round after opening the votes
 | 
					    /** view state for round after opening the votes
 | 
				
			||||||
@ -51,15 +48,14 @@ object Models {
 | 
				
			|||||||
    case Viewing(
 | 
					    case Viewing(
 | 
				
			||||||
        votes: List[(PlayerID, String)]
 | 
					        votes: List[(PlayerID, String)]
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  final case class PlayerID(id: Long) derives Codec.AsObject
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  final case class PlayerID(id: Long) derives Codec.AsObject
 | 
				
			||||||
  final case class Player(name: String, id: PlayerID) derives Codec.AsObject
 | 
					  final case class Player(name: String, id: PlayerID) derives Codec.AsObject
 | 
				
			||||||
  object Player {
 | 
					  object Player {
 | 
				
			||||||
    def create(name: String) = Player(name, PlayerID(Random.nextLong))
 | 
					    def create(name: String) = Player(name, PlayerID(Random.nextLong))
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  final case class RoomID(name: String) derives Codec.AsObject
 | 
					  final case class RoomID(name: String) derives Codec.AsObject
 | 
				
			||||||
 | 
					 | 
				
			||||||
  final case class Room(
 | 
					  final case class Room(
 | 
				
			||||||
      id: RoomID,
 | 
					      id: RoomID,
 | 
				
			||||||
      players: List[Player],
 | 
					      players: List[Player],
 | 
				
			||||||
@ -67,9 +63,9 @@ object Models {
 | 
				
			|||||||
      password: String,
 | 
					      password: String,
 | 
				
			||||||
      allowedCards: List[String],
 | 
					      allowedCards: List[String],
 | 
				
			||||||
      round: RoundState,
 | 
					      round: RoundState,
 | 
				
			||||||
      playersPasswords: Map[String, String] = Map.empty // nickname into password
 | 
					      playersPasswords: Map[String, String] = Map.empty, // nickname into password
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    def toViewFor(playerId: PlayerID): RoomStateView = {
 | 
					    def getViewFor(playerId: PlayerID): RoomStateView = {
 | 
				
			||||||
      players
 | 
					      players
 | 
				
			||||||
        .find(_.id == playerId)
 | 
					        .find(_.id == playerId)
 | 
				
			||||||
        .fold(ifEmpty = RoomStateView.empty)((me: Player) =>
 | 
					        .fold(ifEmpty = RoomStateView.empty)((me: Player) =>
 | 
				
			||||||
@ -77,27 +73,28 @@ object Models {
 | 
				
			|||||||
            players,
 | 
					            players,
 | 
				
			||||||
            me.id,
 | 
					            me.id,
 | 
				
			||||||
            allowedCards,
 | 
					            allowedCards,
 | 
				
			||||||
            round,
 | 
					            round.toViewFor(playerId),
 | 
				
			||||||
            playerId == owner
 | 
					            playerId == owner
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  object Room {
 | 
					
 | 
				
			||||||
    val testRoom = Room(
 | 
					  // no need to send to the front end, no deed to derive codec, cool
 | 
				
			||||||
      id = RoomID("testroom"),
 | 
					  sealed trait RoundState {
 | 
				
			||||||
      players = List(
 | 
					    def toViewFor(player: PlayerID): RoundStateView
 | 
				
			||||||
        Player("me", PlayerID(1L)),
 | 
					  }
 | 
				
			||||||
        Player("horsey", PlayerID(444L)),
 | 
					  object RoundState {
 | 
				
			||||||
        Player("froggy", PlayerID(555L)),
 | 
					    final case class Voting(votes: Map[PlayerID, String]) extends RoundState {
 | 
				
			||||||
        Player("owley", PlayerID(777L))
 | 
					      def toViewFor(playerId: PlayerID): RoundStateView.Voting = RoundStateView.Voting(
 | 
				
			||||||
      ),
 | 
					          myCard = votes.get(playerId),
 | 
				
			||||||
      owner = PlayerID(1L),
 | 
					          alreadyVoted = votes.filterKeys(id => id != playerId).keys.toList
 | 
				
			||||||
      password = "password",
 | 
					 | 
				
			||||||
      allowedCards = List("S", "M", "L"),
 | 
					 | 
				
			||||||
      // TODO - this needs to be a different hting
 | 
					 | 
				
			||||||
      round = RoundState.Voting(None, List.empty)
 | 
					 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    final case class Viewing(votes: Map[PlayerID, String]) extends RoundState {
 | 
				
			||||||
 | 
					      def toViewFor(player: PlayerID): RoundStateView.Viewing =
 | 
				
			||||||
 | 
					        RoundStateView.Viewing(votes.toList)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
				
			|||||||
@ -15,7 +15,7 @@ object TestModels {
 | 
				
			|||||||
    password = "password",
 | 
					    password = "password",
 | 
				
			||||||
    allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					    allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
    round = RoundState.Viewing(
 | 
					    round = RoundState.Viewing(
 | 
				
			||||||
        List(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
 | 
					      Map(me.id -> "xs", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    playersPasswords = Map("me" -> "nickpassword") // nickname into password
 | 
					    playersPasswords = Map("me" -> "nickpassword") // nickname into password
 | 
				
			||||||
  )
 | 
					  )
 | 
				
			||||||
@ -29,70 +29,71 @@ object TestModels {
 | 
				
			|||||||
      players = List(me),
 | 
					      players = List(me),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = None, alreadyVoted = List.empty),
 | 
					      round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, pony),
 | 
					      players = List(me, pony),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = None, alreadyVoted = List.empty),
 | 
					      round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony),
 | 
					      players = List(me, birdy, pony),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = None, alreadyVoted = List.empty),
 | 
					      round = RoundStateView.Voting(myCard = None, alreadyVoted = List.empty),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony),
 | 
					      players = List(me, birdy, pony),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = None, alreadyVoted = List(birdy)),
 | 
					      round = RoundStateView.Voting(myCard = None, alreadyVoted = List(birdy.id)),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony),
 | 
					      players = List(me, birdy, pony),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)),
 | 
					      round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony),
 | 
					      players = List(me, birdy, pony),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)),
 | 
					      round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony, horsey),
 | 
					      players = List(me, birdy, pony, horsey),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy)),
 | 
					      round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id)),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony, horsey),
 | 
					      players = List(me, birdy, pony, horsey),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy, horsey)),
 | 
					      round = RoundStateView.Voting(myCard = Some("m"), alreadyVoted = List(birdy.id, horsey.id)),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony, horsey),
 | 
					      players = List(me, birdy, pony, horsey),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Voting(myCard = Some("m"), alreadyVoted = List(birdy, horsey, pony)),
 | 
					      round = RoundStateView
 | 
				
			||||||
 | 
					        .Voting(myCard = Some("m"), alreadyVoted = List(birdy.id, horsey.id, pony.id)),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
    ),
 | 
					    ),
 | 
				
			||||||
    RoomStateView(
 | 
					    RoomStateView(
 | 
				
			||||||
      players = List(me, birdy, pony, horsey),
 | 
					      players = List(me, birdy, pony, horsey),
 | 
				
			||||||
      me = me.id,
 | 
					      me = me.id,
 | 
				
			||||||
      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
					      allowedCards = List("xs", "s", "m", "l", "xl"),
 | 
				
			||||||
      round = RoundState.Viewing(
 | 
					      round = RoundStateView.Viewing(
 | 
				
			||||||
        List(me.id -> "m", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
 | 
					        List(me.id -> "m", pony.id -> "l", birdy.id -> "s", horsey.id -> "m")
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      canCloseRound = true
 | 
					      canCloseRound = true
 | 
				
			||||||
 | 
				
			|||||||
@ -32,9 +32,9 @@ object OwnHandControls {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private def myUnselectedCards(state: RoomStateView): List[String] = {
 | 
					  private def myUnselectedCards(state: RoomStateView): List[String] = {
 | 
				
			||||||
    state.round match {
 | 
					    state.round match {
 | 
				
			||||||
      case RoundState.Voting(myCard, _) =>
 | 
					      case RoundStateView.Voting(myCard, _) =>
 | 
				
			||||||
        state.allowedCards.filterNot(value => myCard.contains(value))
 | 
					        state.allowedCards.filterNot(value => myCard.contains(value))
 | 
				
			||||||
      case RoundState.Viewing(votes) =>
 | 
					      case RoundStateView.Viewing(votes) =>
 | 
				
			||||||
        state.allowedCards.filterNot(value => votes.toMap.get(state.me).contains(value))
 | 
					        state.allowedCards.filterNot(value => votes.toMap.get(state.me).contains(value))
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -2,21 +2,19 @@ package industries.sunshine.planningpoker
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
import scala.scalajs.js
 | 
					import scala.scalajs.js
 | 
				
			||||||
import com.raquo.laminar.api.L.{*, given}
 | 
					import com.raquo.laminar.api.L.{*, given}
 | 
				
			||||||
import industries.sunshine.planningpoker.common.Models.RoundState
 | 
					import industries.sunshine.planningpoker.common.Models.RoundStateView
 | 
				
			||||||
import com.raquo.airstream.core.Signal
 | 
					import com.raquo.airstream.core.Signal
 | 
				
			||||||
import industries.sunshine.planningpoker.common.Models.RoundState
 | 
					 | 
				
			||||||
import industries.sunshine.planningpoker.common.Models.RoundState
 | 
					 | 
				
			||||||
import io.laminext.fetch.Fetch
 | 
					import io.laminext.fetch.Fetch
 | 
				
			||||||
import scala.scalajs.js.Dynamic.{global => g}
 | 
					import scala.scalajs.js.Dynamic.{global => g}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import concurrent.ExecutionContext.Implicits.global
 | 
					import concurrent.ExecutionContext.Implicits.global
 | 
				
			||||||
 | 
					
 | 
				
			||||||
object TableControls {
 | 
					object TableControls {
 | 
				
			||||||
  def render(roundSignal: Signal[RoundState]): Element = {
 | 
					  def render(roundSignal: Signal[RoundStateView]): Element = {
 | 
				
			||||||
    div(
 | 
					    div(
 | 
				
			||||||
      child <-- roundSignal.map {
 | 
					      child <-- roundSignal.map {
 | 
				
			||||||
        case RoundState.Viewing(_)   => newPollButton()
 | 
					        case RoundStateView.Viewing(_)   => newPollButton()
 | 
				
			||||||
        case RoundState.Voting(_, _) => endPollButton()
 | 
					        case RoundStateView.Voting(_, _) => endPollButton()
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
				
			|||||||
@ -39,16 +39,16 @@ object TableView {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  def getPlayerCardType(
 | 
					  def getPlayerCardType(
 | 
				
			||||||
      id: PlayerID,
 | 
					      id: PlayerID,
 | 
				
			||||||
      state: RoundState,
 | 
					      state: RoundStateView,
 | 
				
			||||||
      name: String,
 | 
					      name: String,
 | 
				
			||||||
      myId: PlayerID
 | 
					      myId: PlayerID
 | 
				
			||||||
  ): CardState = {
 | 
					  ): CardState = {
 | 
				
			||||||
    state match {
 | 
					    state match {
 | 
				
			||||||
      case isOpen: RoundState.Voting =>
 | 
					      case isOpen: RoundStateView.Voting =>
 | 
				
			||||||
        if (myId == id) {
 | 
					        if (myId == id) {
 | 
				
			||||||
          isOpen.myCard.fold(NoCard(name))(vote => Open(vote))
 | 
					          isOpen.myCard.fold(NoCard(name))(vote => Open(vote))
 | 
				
			||||||
        } else isOpen.alreadyVoted.find(_.id == id).fold(NoCard(name))(_ => CardBack)
 | 
					        } else isOpen.alreadyVoted.find(_ == id).fold(NoCard(name))(_ => CardBack)
 | 
				
			||||||
      case isClosed: RoundState.Viewing =>
 | 
					      case isClosed: RoundStateView.Viewing =>
 | 
				
			||||||
        isClosed.votes
 | 
					        isClosed.votes
 | 
				
			||||||
          .find(_._1 == id)
 | 
					          .find(_._1 == id)
 | 
				
			||||||
          .fold {
 | 
					          .fold {
 | 
				
			||||||
 | 
				
			|||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user