diff --git a/.github/workflows/dependency-graph.yml b/.github/workflows/dependency-graph.yml index 8ddb151..14760f0 100644 --- a/.github/workflows/dependency-graph.yml +++ b/.github/workflows/dependency-graph.yml @@ -9,4 +9,5 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 + - uses: sbt/setup-sbt@v1 - uses: scalacenter/sbt-dependency-submission@v2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index b9cdbc1..f9c5773 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -11,12 +11,14 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up JDK 21 uses: actions/setup-java@v4 with: - java-version: '17' + java-version: '21' distribution: 'corretto' cache: 'sbt' + - name: Install sbt + uses: sbt/setup-sbt@v1 - name: Run tests run: sbt test lambda/Universal/packageBin @@ -25,11 +27,11 @@ jobs: steps: - uses: actions/checkout@v4 - name: Set up frontend toolchain - uses: actions/setup-node@v4.0.1 + uses: actions/setup-node@v4 with: - node-version: 20.x + node-version: 22.x - name: Install project dependencies - run: npm install + run: npm ci working-directory: frontend - name: Test frontend run: npm run test diff --git a/.scalafmt.conf b/.scalafmt.conf new file mode 100644 index 0000000..4e1598f --- /dev/null +++ b/.scalafmt.conf @@ -0,0 +1,2 @@ +version = 3.8.3 +runner.dialect = scala3 \ No newline at end of file diff --git a/README.md b/README.md index 5841a89..d3d8655 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ $ npm run start #### API -You will need a JDK installed (pokerdot uses 17, but others should +You will need a JDK installed (pokerdot uses 21, but others should work fine) and `sbt`. ```bash diff --git a/build.sbt b/build.sbt index 4942c40..fa69172 100644 --- a/build.sbt +++ b/build.sbt @@ -1,30 +1,38 @@ import scala.concurrent.duration.DurationInt -ThisBuild / scalaVersion := "2.13.12" -ThisBuild / version := "0.1.0-SNAPSHOT" -ThisBuild / organization := "io.adamnfish" +ThisBuild / scalaVersion := "3.3.4" +ThisBuild / version := "0.1.0-SNAPSHOT" +ThisBuild / organization := "io.adamnfish" ThisBuild / organizationName := "adamnfish" ThisBuild / scalacOptions ++= Seq( "-Xfatal-warnings", - "-encoding", "UTF-8", - "-Ywarn-dead-code", + "-encoding", + "UTF-8", "-deprecation", - "-explaintypes", + "-java-output-version", + "21", + // avoid a scanamo derivation error message + "-Xmax-inlines", + "64" ) - -val circeVersion = "0.14.5" -val scanamoVersion = "1.0-M14" -val awsJavaSdkVersion = "2.20.68" +val circeVersion = "0.14.10" +val scanamoVersion = "3.0.0" +val awsJavaSdkVersion = "2.29.43" val commonDeps = Seq( - "org.scalatest" %% "scalatest" % "3.2.15" % Test, - "org.scalacheck" %% "scalacheck" % "1.17.0" % Test, - "org.scalatestplus" %% "scalacheck-1-14" % "3.2.2.0" % Test, + "org.scalatest" %% "scalatest" % "3.2.19" % Test, + "org.scalameta" %% "munit" % "1.0.3" % Test, + "org.scalameta" %% "munit-scalacheck" % "1.0.0" % Test, + "org.typelevel" %% "scalacheck-effect-munit" % "1.0.4" % Test, + "org.typelevel" %% "munit-cats-effect" % "2.0.0" % Test, + "org.scalacheck" %% "scalacheck" % "1.18.1" % Test, + "org.scalatestplus" %% "scalacheck-1-18" % "3.2.19.0" % Test ) val loggingDeps = Seq( - "ch.qos.logback" % "logback-classic" % "1.4.7", - "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", + "org.typelevel" %% "log4cats-slf4j" % "2.7.0", + "ch.qos.logback" % "logback-classic" % "1.5.15", + "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5" ) // https://aws.amazon.com/blogs/developer/tuning-the-aws-java-sdk-2-x-to-reduce-startup-time/ @@ -32,13 +40,13 @@ val loggingDeps = Seq( // some other jars are also filtered out of the Lambda in its native packager settings ThisBuild / excludeDependencies ++= Seq( ExclusionRule("software.amazon.awssdk", "netty-nio-client"), - ExclusionRule("software.amazon.awssdk", "apache-client"), + ExclusionRule("software.amazon.awssdk", "apache-client") ) lazy val root = (project in file(".")) .settings( name := "pokerdot", - libraryDependencies ++= commonDeps, + libraryDependencies ++= commonDeps ) .aggregate(core, lambda, devServer, integration) @@ -46,13 +54,15 @@ lazy val core = (project in file("core")) .settings( name := "core", libraryDependencies ++= Seq( - "dev.zio" %% "zio" % "2.0.13", + "org.typelevel" %% "cats-core" % "2.12.0", + "org.typelevel" %% "cats-effect" % "3.5.7", "io.circe" %% "circe-core" % circeVersion, "io.circe" %% "circe-generic" % circeVersion, "io.circe" %% "circe-parser" % circeVersion, - "org.scanamo" %% "scanamo" % scanamoVersion, "software.amazon.awssdk" % "dynamodb" % awsJavaSdkVersion, - ) ++ commonDeps, + "org.scanamo" %% "scanamo" % scanamoVersion, + "org.scanamo" %% "scanamo-cats-effect" % scanamoVersion + ) ++ commonDeps ) lazy val lambda = (project in file("lambda")) @@ -60,12 +70,13 @@ lazy val lambda = (project in file("lambda")) .settings( name := "lambda", libraryDependencies ++= Seq( - "com.typesafe.scala-logging" %% "scala-logging" % "3.9.5", - "com.amazonaws" % "aws-lambda-java-core" % "1.2.2", - "com.amazonaws" % "aws-lambda-java-events" % "3.11.1", - "com.amazonaws" % "aws-xray-recorder-sdk-core" % "2.15.0", + "com.amazonaws" % "aws-lambda-java-core" % "1.2.3", + "com.amazonaws" % "aws-lambda-java-events" % "3.14.0", + "com.amazonaws" % "aws-xray-recorder-sdk-core" % "2.18.2", "software.amazon.awssdk" % "apigatewaymanagementapi" % awsJavaSdkVersion, + // TODO: use the async crt version for everything "software.amazon.awssdk" % "url-connection-client" % awsJavaSdkVersion, + "software.amazon.awssdk" % "aws-crt-client" % awsJavaSdkVersion ) ++ commonDeps ++ loggingDeps, // native-packager Universal / topLevelDirectory := None, @@ -76,8 +87,8 @@ lazy val lambda = (project in file("lambda")) // these are only used at compile time to generate code, I think? // !path.contains("org.scala-lang.scala-compiler") && // required :-( // !path.contains("org.scala-lang.scala-reflect") && // required :-( - !path.contains("net.java.dev.jna.jna") && - !path.contains("org.jline.jline") + !path.contains("net.java.dev.jna.jna") && + !path.contains("org.jline.jline") } ) .dependsOn(core) @@ -86,18 +97,27 @@ lazy val integration = (project in file("integration")) .settings( name := "integration", libraryDependencies ++= Seq( - "org.scanamo" %% "scanamo-testkit" % scanamoVersion % Test, + "org.typelevel" %% "cats-effect-testing-scalatest" % "1.6.0" % Test, + // TODO: use the async crt version for everything "software.amazon.awssdk" % "url-connection-client" % awsJavaSdkVersion % Test, + "software.amazon.awssdk" % "aws-crt-client" % awsJavaSdkVersion % Test, "software.amazon.awssdk" % "dynamodb" % awsJavaSdkVersion % Test, + "org.scanamo" %% "scanamo-testkit" % scanamoVersion % Test ) ++ commonDeps ++ loggingDeps, + scalacOptions ++= Seq( + "-source", + "future" + ), // start DynamoDB for tests dynamoDBLocalDownloadDir := file(".dynamodb-local"), dynamoDBLocalPort := 8042, dynamoDBLocalDownloadIfOlderThan := 14.days, startDynamoDBLocal := startDynamoDBLocal.dependsOn(Test / compile).value, Test / test := (Test / test).dependsOn(startDynamoDBLocal).value, - Test / testOnly := (Test / testOnly).dependsOn(startDynamoDBLocal).evaluated, - Test / testOptions += dynamoDBLocalTestCleanup.value, + Test / testOnly := (Test / testOnly) + .dependsOn(startDynamoDBLocal) + .evaluated, + Test / testOptions += dynamoDBLocalTestCleanup.value ) .dependsOn(core % "compile->compile;test->test") @@ -105,10 +125,12 @@ lazy val devServer = (project in file("devserver")) .settings( name := "devserver", libraryDependencies ++= Seq( - "io.javalin" % "javalin" % "5.6.3", - "org.scanamo" %% "scanamo-testkit" % scanamoVersion, + "io.javalin" % "javalin" % "5.6.3", // TODO: v6 is now out "software.amazon.awssdk" % "dynamodb" % awsJavaSdkVersion, + // TODO: use the async crt version for everything "software.amazon.awssdk" % "url-connection-client" % awsJavaSdkVersion, + "software.amazon.awssdk" % "aws-crt-client" % awsJavaSdkVersion, + "org.scanamo" %% "scanamo-testkit" % scanamoVersion ) ++ commonDeps ++ loggingDeps, // console logging and ctrl-c to kill support run / fork := true, @@ -121,6 +143,6 @@ lazy val devServer = (project in file("devserver")) startDynamoDBLocal := startDynamoDBLocal.dependsOn(Compile / compile).value, Compile / run := (Compile / run).dependsOn(startDynamoDBLocal).evaluated, // allows browsing DB from http://localhost:8042/shell/ - dynamoDBLocalSharedDB := true, + dynamoDBLocalSharedDB := true ) .dependsOn(core) diff --git a/buildspec.yml b/buildspec.yml index 5973d26..4113420 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -7,8 +7,8 @@ env: phases: install: runtime-versions: - nodejs: 20 - java: corretto17 + nodejs: 22 + java: corretto21 pre_build: commands: @@ -27,7 +27,7 @@ phases: # Install software for frontend build - cd frontend - - npm install + - npm ci - cd .. - echo "[STATUS] pre build step finished" diff --git a/cloudformation/pokerdot.template.yaml b/cloudformation/pokerdot.template.yaml index 91f45ca..a92d59e 100644 --- a/cloudformation/pokerdot.template.yaml +++ b/cloudformation/pokerdot.template.yaml @@ -154,7 +154,7 @@ Resources: Handler: "io.adamnfish.pokerdot.Lambda::handleRequest" Timeout: 20 MemorySize: 1024 - Runtime: java17 + Runtime: java21 Tracing: Active Environment: Variables: diff --git a/core/src/main/scala/io/adamnfish/pokerdot/PokerDot.scala b/core/src/main/scala/io/adamnfish/pokerdot/PokerDot.scala index f438626..58af738 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/PokerDot.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/PokerDot.scala @@ -1,80 +1,87 @@ package io.adamnfish.pokerdot -import io.adamnfish.pokerdot.logic.Utils.{Attempt, RichEither, RichList} +import cats.* +import cats.effect.kernel.{Clock, Sync, Async} +import cats.implicits.* +import cats.syntax.all.* import io.adamnfish.pokerdot.logic.{Games, PlayerActions, Representations, Responses} -import io.adamnfish.pokerdot.models._ +import io.adamnfish.pokerdot.models.* import io.adamnfish.pokerdot.services.Database -import io.adamnfish.pokerdot.validation.Validation.{extractAdvancePhase, extractBet, extractCheck, extractCreateGame, extractFold, extractJoinGame, extractPing, extractStartGame, extractUpdateBlind} +import io.adamnfish.pokerdot.validation.Validation.* import io.circe.Json -import zio._ object PokerDot { - def pokerdot(requestBody: String, appContext: AppContext): Attempt[String] = { + def pokerdot[F[_] : MonadThrow](requestBody: String, appContext: AppContext[F]): F[String] = { (for { - requestJson <- Serialisation.parse(requestBody, "could not understand the request", None).attempt - operationJson <- ZIO.fromOption(requestJson.hcursor.downField("operation").focus).mapError(_ => + requestJson <- Serialisation.parse(requestBody, "could not understand the request", None) + operationJson <- MonadThrow[F].fromOption( + requestJson.hcursor.downField("operation").focus, Failures("Request did not include operation field", "could not understand the request") ) - operation <- Serialisation.extractJson[String](operationJson, "unexpected operation").attempt - response <- operation match { + operation <- Serialisation.extractJson[F, String](operationJson, "unexpected operation") + response: Response[Message] <- operation match { case "create-game" => - createGame(requestJson, appContext, initialSeed = appContext.rng.randomState()) + appContext.rng.randomState.flatMap { initialSeed => + createGame(requestJson, appContext, initialSeed).widen[Response[Message]] + } case "join-game" => - joinGame(requestJson, appContext) + joinGame(requestJson, appContext).widen[Response[Message]] case "start-game" => - startGame(requestJson, appContext) + startGame(requestJson, appContext).widen[Response[Message]] case "bet" => - bet(requestJson, appContext) + bet(requestJson, appContext).widen[Response[Message]] case "check" => - check(requestJson, appContext) + check(requestJson, appContext).widen[Response[Message]] case "fold" => - fold(requestJson, appContext) + fold(requestJson, appContext).widen[Response[Message]] case "advance-phase" => advancePhase(requestJson, appContext) case "update-blind" => - updateBlind(requestJson, appContext) + updateBlind(requestJson, appContext).widen[Response[Message]] // TODO: include admin endpoint to allow manual correction of game state case "ping" => - ping(requestJson, appContext) + ping(requestJson, appContext).widen[Response[Message]] case "wake" => - wake(appContext) + wake(appContext).widen[Response[Message]] case _ => - Attempt.failAs[Response[GameStatus]]( + MonadThrow[F].raiseError[Response[Message]] { Failures( s"Unexpected operation: $operation", "the request wasn't something I understand" ) - ) + } } // send messages allMessages = response.messages.toList ++ response.statuses.toList - _ <- allMessages.ioTraverse { case (address, msg: Message) => + _ <- allMessages.traverse { case (address, msg: Message) => appContext.messaging.sendMessage(address, msg) } } yield operation) - .tapError { failures => - // There are some failure messages that we don't want to send to clients (e.g. failed message delivery). - // It's not urgent, but prevents cluttering a user's experience with irrelevant failure information. - failures.externalFailures match { - case Nil => - // if all the messages were 'internal' then there's no need to send a failure message - ZIO.unit - case externalFailures => - appContext.messaging.sendError(appContext.playerAddress, failures.copy(failures = externalFailures)) - } + .onError { + case failures: Failures => + // There are some failure messages that we don't want to send to clients (e.g. failed message delivery). + // It's not urgent, but prevents cluttering a user's experience with irrelevant failure information. + failures.externalFailures match { + case Nil => + // if all the messages were 'internal' then there's no need to send a failure message + MonadThrow[F].unit + case externalFailures => + appContext.messaging.sendError(appContext.playerAddress, failures.externalOnly) + } } } // OPERATIONS - def createGame(requestJson: Json, appContext: AppContext, initialSeed: Long): Attempt[Response[Welcome]] = { + def createGame[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F], initialSeed: Long): F[Response[Welcome]] = { for { - createGame <- extractCreateGame(requestJson).attempt - rawGame = Games.newGame(createGame.gameName, trackStacks = false, appContext.clock, initialSeed) + createGame <- extractCreateGame(requestJson) + now <- appContext.time.now + rawGame = Games.newGame(createGame.gameName, trackStacks = false, now, initialSeed) uniqueGameCode <- Games.makeUniquePrefix(rawGame.gameId, appContext.db, Database.checkUniquePrefix) game = rawGame.copy(gameCode = uniqueGameCode) - host = Games.newPlayer(game.gameId, createGame.screenName, isHost = true, appContext.playerAddress, appContext.clock) + host = Games.newPlayer(game.gameId, createGame.screenName, isHost = true, appContext.playerAddress, now) gameWithHost = Games.addPlayer(game, host) gameDb = Representations.gameToDb(gameWithHost) hostDb = Representations.playerToDb(host) @@ -89,25 +96,29 @@ object PokerDot { * * TODO: this or another operation should allow spectators */ - def joinGame(requestJson: Json, appContext: AppContext): Attempt[Response[Welcome]] = { + def joinGame[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[Welcome]] = { for { - rawJoinGame <- extractJoinGame(requestJson).attempt + rawJoinGame <- extractJoinGame(requestJson) joinGame = Games.normaliseGameCode(rawJoinGame) maybeGame <- appContext.db.lookupGame(joinGame.gameCode) - rawGameDb <- Attempt.fromOption(maybeGame, Failures( - s"Game not found for code ${joinGame.gameCode}", - "could not find game to join, is the code correct?", - )) + rawGameDb <- MonadThrow[F].fromOption( + maybeGame, + Failures( + s"Game not found for code ${joinGame.gameCode}", + "could not find game to join, is the code correct?", + ) + ) playerDbs <- appContext.db.getPlayers(GameId(rawGameDb.gameId)) // player/spectator IDs aren't persisted in the game's DB record until the game starts // so we patch them in here so we can re-use existing functionality gameDb = Games.addPlayerIds(rawGameDb, playerDbs) - game <- Representations.gameFromDb(gameDb, playerDbs).attempt - _ <- Games.ensureNotStarted(game).attempt - _ <- Games.ensureNotAlreadyPlaying(game.players, appContext.playerAddress).attempt - _ <- Games.ensureNoDuplicateScreenName(game, joinGame.screenName).attempt - _ <- Games.ensurePlayerCount(game.players.length).attempt - player = Games.newPlayer(game.gameId, joinGame.screenName, false, appContext.playerAddress, appContext.clock) + game <- Representations.gameFromDb(gameDb, playerDbs) + _ <- Games.ensureNotStarted(game) + _ <- Games.ensureNotAlreadyPlaying(game.players, appContext.playerAddress) + _ <- Games.ensureNoDuplicateScreenName(game, joinGame.screenName) + _ <- Games.ensurePlayerCount(game.players.length) + now <- appContext.time.now + player = Games.newPlayer(game.gameId, joinGame.screenName, false, appContext.playerAddress, now) newGame = Games.addPlayer(game, player) response = Responses.welcome(newGame, player, appContext.playerAddress) playerDb = Representations.playerToDb(player) @@ -124,95 +135,107 @@ object PokerDot { * Players can no longer join after this point, but this might want some thought (especially * for spectators). */ - def startGame(requestJson: Json, appContext: AppContext): Attempt[Response[GameStatus]] = { + def startGame[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[GameStatus]] = { for { - startGame <- extractStartGame(requestJson).attempt + startGame <- extractStartGame(requestJson) maybeGame <- appContext.db.getGame(startGame.gameId) - rawGameDb <- Attempt.fromOption(maybeGame, Failures( - s"Cannot start game, game ID not found", "couldn't find game to start", - )) + rawGameDb <- MonadThrow[F].fromOption( + maybeGame, + Failures( + s"Cannot start game, game ID not found", "couldn't find game to start", + ) + ) playerDbs <- appContext.db.getPlayers(GameId(rawGameDb.gameId)) gameDb = Games.addPlayerIds(rawGameDb, playerDbs) - rawGame <- Representations.gameFromDb(gameDb, playerDbs).attempt - _ <- Games.ensureNotStarted(rawGame).attempt - _ <- Games.ensureHost(rawGame.players, startGame.playerKey).attempt - _ <- Games.ensureStartingPlayerCount(rawGame.players.length).attempt - now = appContext.clock.now() + rawGame <- Representations.gameFromDb(gameDb, playerDbs) + _ <- Games.ensureNotStarted(rawGame) + _ <- Games.ensureHost(rawGame.players, startGame.playerKey) + _ <- Games.ensureStartingPlayerCount(rawGame.players.length) + now <- appContext.time.now startedGame = Games.start(rawGame, now, startGame.initialSmallBlind, startGame.timerConfig, startGame.startingStack, startGame.playerOrder) startedGameDb = Representations.gameToDb(startedGame) playerDbs = Representations.allPlayerDbs(startedGame.players) // update all players with dealt cards, stack size etc - _ <- playerDbs.ioTraverse(appContext.db.writePlayer) + _ <- playerDbs.traverse(appContext.db.writePlayer) // persist started game _ <- appContext.db.writeGame(startedGameDb) } yield Responses.gameStatuses(startedGame, GameStartedSummary(), startGame.playerId, appContext.playerAddress) } - def bet(requestJson: Json, appContext: AppContext): Attempt[Response[GameStatus]] = { + def bet[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[GameStatus]] = { for { - bet <- extractBet(requestJson).attempt + bet <- extractBet(requestJson) maybeGame <- appContext.db.getGame(bet.gameId) - gameDb <- Attempt.fromOption(maybeGame, Failures( - s"Cannot bet, game ID not found", "couldn't find the game", - )) + gameDb <- MonadThrow[F].fromOption( + maybeGame, + Failures( + s"Cannot bet, game ID not found", "couldn't find the game", + ) + ) playerDbs <- appContext.db.getPlayers(GameId(gameDb.gameId)) - rawGame <- Representations.gameFromDb(gameDb, playerDbs).attempt - _ <- Games.ensureStarted(rawGame).attempt - rawPlayer <- Games.ensurePlayerKey(rawGame.players, bet.playerId, bet.playerKey).attempt - _ <- Games.ensureActive(rawGame.inTurn, bet.playerId).attempt - betResult <- PlayerActions.bet(rawGame, bet.betAmount, rawPlayer).attempt + rawGame <- Representations.gameFromDb(gameDb, playerDbs) + _ <- Games.ensureStarted(rawGame) + rawPlayer <- Games.ensurePlayerKey(rawGame.players, bet.playerId, bet.playerKey) + _ <- Games.ensureActive(rawGame.inTurn, bet.playerId) + betResult <- PlayerActions.bet(rawGame, bet.betAmount, rawPlayer) (newGame, action) = betResult // obtain DB representations for persistence updatedPlayerDbs = Representations.activePlayerDbs(newGame.players) newGameDb = Representations.gameToDb(newGame) // save this player - _ <- updatedPlayerDbs.ioTraverse(appContext.db.writePlayer) + _ <- updatedPlayerDbs.traverse(appContext.db.writePlayer) // save game _ <- appContext.db.writeGame(newGameDb) } yield Responses.gameStatuses(newGame, action, bet.playerId, appContext.playerAddress) } - def check(requestJson: Json, appContext: AppContext): Attempt[Response[GameStatus]] = { + def check[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[GameStatus]] = { for { - check <- extractCheck(requestJson).attempt + check <- extractCheck(requestJson) maybeGame <- appContext.db.getGame(check.gameId) - gameDb <- Attempt.fromOption(maybeGame, Failures( - s"Cannot check, game ID not found", "couldn't find the game", - )) + gameDb <- MonadThrow[F].fromOption( + maybeGame, + Failures( + s"Cannot check, game ID not found", "couldn't find the game", + ) + ) playerDbs <- appContext.db.getPlayers(GameId(gameDb.gameId)) - rawGame <- Representations.gameFromDb(gameDb, playerDbs).attempt - _ <- Games.ensureStarted(rawGame).attempt - player <- Games.ensurePlayerKey(rawGame.players, check.playerId, check.playerKey).attempt - _ <- Games.ensureActive(rawGame.inTurn, check.playerId).attempt // TODO: allow off-turn checks? - newGame <- PlayerActions.check(rawGame, player).attempt + rawGame <- Representations.gameFromDb(gameDb, playerDbs) + _ <- Games.ensureStarted(rawGame) + player <- Games.ensurePlayerKey(rawGame.players, check.playerId, check.playerKey) + _ <- Games.ensureActive(rawGame.inTurn, check.playerId) // TODO: allow off-turn checks? + newGame <- PlayerActions.check(rawGame, player) // obtain DB representations for persistence - updatedPlayerDbs <- Representations.filteredPlayerDbs(newGame.players, Set(check.playerId)).attempt + updatedPlayerDbs <- Representations.filteredPlayerDbs(newGame.players, Set(check.playerId)) newGameDb = Representations.gameToDb(newGame) // save this player - _ <- updatedPlayerDbs.ioTraverse(appContext.db.writePlayer) + _ <- updatedPlayerDbs.traverse(appContext.db.writePlayer) // save game _ <- appContext.db.writeGame(newGameDb) } yield Responses.gameStatuses(newGame, CheckSummary(check.playerId), check.playerId, appContext.playerAddress) } - def fold(requestJson: Json, appContext: AppContext): Attempt[Response[GameStatus]] = { + def fold[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[GameStatus]] = { for { - fold <- extractFold(requestJson).attempt + fold <- extractFold(requestJson) maybeGame <- appContext.db.getGame(fold.gameId) - gameDb <- Attempt.fromOption(maybeGame, Failures( - s"Cannot fold, game ID not found", "couldn't find the game", - )) + gameDb <- MonadThrow[F].fromOption( + maybeGame, + Failures( + s"Cannot fold, game ID not found", "couldn't find the game", + ) + ) playerDbs <- appContext.db.getPlayers(GameId(gameDb.gameId)) - rawGame <- Representations.gameFromDb(gameDb, playerDbs).attempt - _ <- Games.ensureStarted(rawGame).attempt - player <- Games.ensurePlayerKey(rawGame.players, fold.playerId, fold.playerKey).attempt - _ <- Games.ensureActive(rawGame.inTurn, fold.playerId).attempt // TODO: allow off-turn folds? + rawGame <- Representations.gameFromDb(gameDb, playerDbs) + _ <- Games.ensureStarted(rawGame) + player <- Games.ensurePlayerKey(rawGame.players, fold.playerId, fold.playerKey) + _ <- Games.ensureActive(rawGame.inTurn, fold.playerId) // TODO: allow off-turn folds? newGame = PlayerActions.fold(rawGame, player) // obtain DB representations for persistence - updatedPlayerDbs <- Representations.filteredPlayerDbs(newGame.players, Set(fold.playerId)).attempt + updatedPlayerDbs <- Representations.filteredPlayerDbs(newGame.players, Set(fold.playerId)) newGameDb = Representations.gameToDb(newGame) // save this player - _ <- updatedPlayerDbs.ioTraverse(appContext.db.writePlayer) + _ <- updatedPlayerDbs.traverse(appContext.db.writePlayer) // save game _ <- appContext.db.writeGame(newGameDb) } yield Responses.gameStatuses(newGame, FoldSummary(fold.playerId), fold.playerId, appContext.playerAddress) @@ -230,24 +253,29 @@ object PokerDot { * TODO: game setting for auto-advance? * perhaps separate settings for auto-advancing phase / showdown / round */ - def advancePhase(requestJson: Json, appContext: AppContext): Attempt[Response[Message]] = { + def advancePhase[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[Message]] = { for { - advancePhase <- extractAdvancePhase(requestJson).attempt + advancePhase <- extractAdvancePhase(requestJson) maybeGame <- appContext.db.getGame(advancePhase.gameId) - rawGameDb <- Attempt.fromOption(maybeGame, Failures( - s"Cannot advance phase, game ID not found", "couldn't find the game", - )) + rawGameDb <- MonadThrow[F].fromOption( + maybeGame, + Failures( + s"Cannot advance phase, game ID not found", "couldn't find the game", + ) + ) playerDbs <- appContext.db.getPlayers(GameId(rawGameDb.gameId)) - game <- Representations.gameFromDb(rawGameDb, playerDbs).attempt - _ <- Games.ensureStarted(game).attempt - _ <- Games.ensureAdmin(game.players, advancePhase.playerKey).attempt + game <- Representations.gameFromDb(rawGameDb, playerDbs) + _ <- Games.ensureStarted(game) + _ <- Games.ensureAdmin(game.players, advancePhase.playerKey) + now <- appContext.time.now // TODO: recursively call this operation if we are auto-advancing? - advanceResult <- PlayerActions.advancePhase(game, appContext.clock, appContext.rng).attempt + // TODO: move rng's application here - get next state and pass it into pure functions. + advanceResult <- PlayerActions.advancePhase(game, now, appContext.rng) (updatedGame, updatedPlayers, winnings) = advanceResult newGameDb = Representations.gameToDb(updatedGame) // only do DB updates for players that have changed - updatedPlayerDbs <- Representations.filteredPlayerDbs(updatedGame.players, updatedPlayers).attempt - _ <- updatedPlayerDbs.ioTraverse(appContext.db.writePlayer) + updatedPlayerDbs <- Representations.filteredPlayerDbs(updatedGame.players, updatedPlayers) + _ <- updatedPlayerDbs.traverse(appContext.db.writePlayer) _ <- appContext.db.writeGame(newGameDb) } yield { // TODO: this is too much logic for the controller @@ -268,21 +296,24 @@ object PokerDot { * * Pausing / playing is done by setting the optional pauseTime and by faking the start time, respectively. */ - def updateBlind(requestJson: Json, appContext: AppContext): Attempt[Response[GameStatus]] = { + def updateBlind[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[GameStatus]] = { for { - updateBlind <- extractUpdateBlind(requestJson).attempt + updateBlind <- extractUpdateBlind(requestJson) maybeGame <- appContext.db.getGame(updateBlind.gameId) - rawGameDb <- Attempt.fromOption(maybeGame, Failures( - s"Cannot update blind, game ID not found", "couldn't find the game", - )) + rawGameDb <- MonadThrow[F].fromOption( + maybeGame, + Failures( + s"Cannot update blind, game ID not found", "couldn't find the game", + ) + ) playerDbs <- appContext.db.getPlayers(GameId(rawGameDb.gameId)) - game <- Representations.gameFromDb(rawGameDb, playerDbs).attempt - _ <- Games.ensureStarted(game).attempt - _ <- Games.ensureAdmin(game.players, updateBlind.playerKey).attempt - now = appContext.clock.now() - updatedGame <- PlayerActions.updateBlind(game, updateBlind, now).attempt + game <- Representations.gameFromDb(rawGameDb, playerDbs) + _ <- Games.ensureStarted(game) + _ <- Games.ensureAdmin(game.players, updateBlind.playerKey) + now <- appContext.time.now + updatedGame <- PlayerActions.updateBlind(game, updateBlind, now) newGameDb = Representations.gameToDb(updatedGame) - action <- Games.updateBlindAction(updateBlind).attempt + action <- Games.updateBlindAction(updateBlind) _ <- appContext.db.writeGame(newGameDb) // this endpoint won't update players so there's no need to save them } yield Responses.gameStatuses(updatedGame, action, updateBlind.playerId, appContext.playerAddress) @@ -294,23 +325,23 @@ object PokerDot { * * Only available for valid connected players. */ - def ping(requestJson: Json, appContext: AppContext): Attempt[Response[GameStatus]] = { + def ping[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[Response[GameStatus]] = { for { - pingRequest <- extractPing(requestJson).attempt + pingRequest <- extractPing(requestJson) // fetch player / game data gameDbOpt <- appContext.db.getGame(pingRequest.gameId) - rawGameDb <- Games.requireGame(gameDbOpt, pingRequest.gameId.gid).attempt + rawGameDb <- Games.requireGame(gameDbOpt, pingRequest.gameId.gid) playerDbs <- appContext.db.getPlayers(pingRequest.gameId) // we remove duplicates, so it is safe to re-add playerDbs here // this addresses pings when the game has not yet started (and players have not been added) gameDb = Games.addPlayerIds(rawGameDb, playerDbs) - game <- Representations.gameFromDb(gameDb, playerDbs).attempt + game <- Representations.gameFromDb(gameDb, playerDbs) // TODO: handle players or spectators here // maybe check if requester is a player / spectator and delegate accordingly? - player <- Games.ensurePlayerKey(game.players, pingRequest.playerId, pingRequest.playerKey).attempt + player <- Games.ensurePlayerKey(game.players, pingRequest.playerId, pingRequest.playerKey) // update the player's address, if it has changed updatedPlayerOpt = Games.updatePlayerAddress(player, appContext.playerAddress) - updatedPlayer <- updatedPlayerOpt.fold[Attempt[Player]](ZIO.succeed(player)) { updatedPlayer => + updatedPlayer <- updatedPlayerOpt.fold[F[Player]](MonadThrow[F].pure(player)) { updatedPlayer => // if player's address has changed, persist change to DB val updatedPlayerDb = Representations.playerToDb(updatedPlayer) appContext.db.writePlayer(updatedPlayerDb).map(_ => updatedPlayer) @@ -321,11 +352,11 @@ object PokerDot { // TODO: split logic for players / spectators - def playerPing(requestJson: Json, appContext: AppContext): Attempt[(Message, PlayerDb)] = { + def playerPing[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[(Message, PlayerDb)] = { ??? } - def spectatorPing(requestJson: Json, appContext: AppContext): Attempt[(Message, PlayerDb)] = { + def spectatorPing[F[_] : MonadThrow](requestJson: Json, appContext: AppContext[F]): F[(Message, PlayerDb)] = { ??? } @@ -333,8 +364,8 @@ object PokerDot { * This endpoint does nothing here, but executing this function * wakes the container so that subsequent requests load quickly. */ - def wake(appContext: AppContext): Attempt[Response[Status]] = { - ZIO.succeed { + def wake[F[_] : MonadThrow](appContext: AppContext[F]): F[Response[Status]] = { + MonadThrow[F].pure { Responses.ok(appContext.playerAddress) } } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/logic/Cards.scala b/core/src/main/scala/io/adamnfish/pokerdot/logic/Cards.scala index 0657db6..06ca2f7 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/logic/Cards.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/logic/Cards.scala @@ -6,7 +6,7 @@ import io.adamnfish.pokerdot.models.{Ace, Card, Clubs, Diamonds, Eight, Five, Fo object Cards { implicit class RichRank(rank: Rank) { - def of(suit: Suit): Card = { + infix def of(suit: Suit): Card = { Card(rank, suit) } } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/logic/Games.scala b/core/src/main/scala/io/adamnfish/pokerdot/logic/Games.scala index d04f49a..54f8326 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/logic/Games.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/logic/Games.scala @@ -1,10 +1,13 @@ package io.adamnfish.pokerdot.logic +import cats.MonadThrow import io.adamnfish.pokerdot.logic.Play.dealHoles import io.adamnfish.pokerdot.logic.Utils.orderFromList -import io.adamnfish.pokerdot.models._ -import io.adamnfish.pokerdot.services.{Database, Clock} -import zio.ZIO +import io.adamnfish.pokerdot.models.* +import io.adamnfish.pokerdot.services.{Time, Database} +import cats.* +import cats.syntax.* +import cats.implicits.* import java.time.{Duration, Instant} import java.util.UUID @@ -14,13 +17,13 @@ import java.util.UUID * Game implementation functionality. */ object Games { - def newGame(gameName: String, trackStacks: Boolean, clock: Clock, initialState: Long): Game = { + def newGame(gameName: String, trackStacks: Boolean, now: Long, initialState: Long): Game = { val round = Play.generateRound(PreFlop, 0, initialState) val gameId = GameId(UUID.randomUUID().toString) Game( gameId = gameId, gameCode = gameCode(gameId), // try this, we can replace it with a longer unique prefix if required - expiry = expiryTime(clock.now()), + expiry = expiryTime(now), gameName = gameName, players = Nil, spectators = Nil, @@ -29,19 +32,19 @@ object Games { inTurn = None, button = 0, started = false, - startTime = clock.now(), + startTime = now, trackStacks = trackStacks, timer = None, ) } - def newPlayer(gameId: GameId, screenName: String, isHost: Boolean, playerAddress: PlayerAddress, clock: Clock): Player = { + def newPlayer(gameId: GameId, screenName: String, isHost: Boolean, playerAddress: PlayerAddress, now: Long): Player = { val playerId = PlayerId(UUID.randomUUID().toString) val playerKey = PlayerKey(UUID.randomUUID().toString) Player( gameId = gameId, playerId = playerId, - expiry = expiryTime(clock.now()), + expiry = expiryTime(now), screenName = screenName, playerAddress = playerAddress, playerKey = playerKey, @@ -59,13 +62,13 @@ object Games { ) } - def newSpectator(gameId: GameId, screenName: String, isHost: Boolean, playerAddress: PlayerAddress, clock: Clock): Spectator = { + def newSpectator(gameId: GameId, screenName: String, isHost: Boolean, playerAddress: PlayerAddress, now: Long): Spectator = { val playerId = PlayerId(UUID.randomUUID().toString) val playerKey = PlayerKey(UUID.randomUUID().toString) Spectator( gameId = gameId, playerId = playerId, - expiry = expiryTime(clock.now()), + expiry = expiryTime(now), playerAddress = playerAddress, playerKey = playerKey, screenName = screenName, @@ -105,6 +108,10 @@ object Games { ) } + /** + * TODO: this limits pokerdot to `16^4` concurrent games, and less than that in practice. + * Instead, the game code needs to be persisted as a unique prefix, however long that needs to be + */ def gameCode(gameId: GameId): String = { gameId.gid.take(4) } @@ -118,17 +125,17 @@ object Games { ) } - def makeUniquePrefix(gameId: GameId, persistence: Database, fn: (GameId, Int, Database) => Attempt[Boolean]): Attempt[String] = { + def makeUniquePrefix[F[_] : MonadThrow](gameId: GameId, persistence: Database[F], fn: (GameId, Int, Database[F]) => F[Boolean]): F[String] = { val min = 4 val max = 10 - def loop(prefixLength: Int): Attempt[String] = { + def loop(prefixLength: Int): F[String] = { fn(gameId, prefixLength, persistence).flatMap { case true => - ZIO.succeed(gameId.gid.take(prefixLength)) + MonadThrow[F].pure(gameId.gid.take(prefixLength)) case false if prefixLength < max => loop(prefixLength + 1) case _ => - ZIO.fail( + MonadThrow[F].raiseError( Failures("Couldn't create unique prefix of GameID", "couldn't set up game with a join code") ) } @@ -203,15 +210,15 @@ object Games { ) } - def updateBlindAction(updateBlind: UpdateBlind): Either[Failures, ActionSummary] = { + def updateBlindAction[F[_] : MonadThrow](updateBlind: UpdateBlind): F[ActionSummary] = { if (updateBlind.timerLevels.isDefined || updateBlind.progress.isDefined) { - Right(EditTimerSummary()) + MonadThrow[F].pure(EditTimerSummary()) } else if (updateBlind.playing.isDefined) { - Right(TimerStatusSummary(updateBlind.playing.contains(true))) + MonadThrow[F].pure(TimerStatusSummary(updateBlind.playing.contains(true))) } else if (updateBlind.smallBlind.isDefined) { - Right(EditBlindSummary()) + MonadThrow[F].pure(EditBlindSummary()) } else { - Left(Failures("Couldn't determine action from update blind request", "couldn't update the blinds.")) + MonadThrow[F].raiseError(Failures("Couldn't determine action from update blind request", "couldn't update the blinds.")) } } @@ -264,12 +271,12 @@ object Games { } else resetPlayer } - def requireGame(gameDbOpt: Option[GameDb], gid: String): Either[Failures, GameDb] = { + def requireGame[F[_] : MonadThrow](gameDbOpt: Option[GameDb], gid: String): F[GameDb] = { gameDbOpt match { case Some(gameDb) => - Right(gameDb) + MonadThrow[F].pure(gameDb) case None => - Left { + MonadThrow[F].raiseError { Failures( s"Game not found for lookup $gid", "couldn't find game, it may have been automatically deleted if it is old?", @@ -278,19 +285,19 @@ object Games { } } - def ensureNotStarted(game: Game): Either[Failures, Unit] = { - if (game.started) Left { + def ensureNotStarted[F[_] : MonadThrow](game: Game): F[Unit] = { + if (game.started) MonadThrow[F].raiseError { Failures( "game has already started", "the game has already started.", ) } - else Right(()) + else MonadThrow[F].pure(()) } - def ensureStarted(game: Game): Either[Failures, Unit] = { - if (game.started) Right(()) - else Left { + def ensureStarted[F[_] : MonadThrow](game: Game): F[Unit] = { + if (game.started) MonadThrow[F].pure(()) + else MonadThrow[F].raiseError { Failures( "game has not started", "the game has not started.", @@ -298,9 +305,9 @@ object Games { } } - def ensureNoDuplicateScreenName(game: Game, screenName: String): Either[Failures, Unit] = { + def ensureNoDuplicateScreenName[F[_] : MonadThrow](game: Game, screenName: String): F[Unit] = { if (game.players.exists(_.screenName == screenName)) - Left { + MonadThrow[F].raiseError { Failures( "Duplicate screen name, joining game failed", "someone else already has the same name.", @@ -308,27 +315,27 @@ object Games { ) } else - Right(()) + MonadThrow[F].pure(()) } - def ensurePlayerCount(n: Int): Either[Failures, Unit] = { + def ensurePlayerCount[F[_] : MonadThrow](n: Int): F[Unit] = { if (n >= 20) { - Left { + MonadThrow[F].raiseError { Failures( "Max player count exceeded", "there are already 20 players in this game, which is the maximum number.", ) } } else { - Right(()) + MonadThrow[F].pure(()) } } - def ensureStartingPlayerCount(n: Int): Either[Failures, Unit] = { + def ensureStartingPlayerCount[F[_] : MonadThrow](n: Int): F[Unit] = { if (n > 1) { - Right(()) + MonadThrow[F].pure(()) } else { - Left { + MonadThrow[F].raiseError { Failures( "Cannot start with one player", "a game requires at least 2 players.", @@ -337,31 +344,31 @@ object Games { } } - def ensureNotAlreadyPlaying(players: List[Player], playerAddress: PlayerAddress): Either[Failures, Unit] = { + def ensureNotAlreadyPlaying[F[_] : MonadThrow](players: List[Player], playerAddress: PlayerAddress): F[Unit] = { if (players.exists(_.playerAddress == playerAddress)) - Left { + MonadThrow[F].raiseError { Failures( "Duplicate player address, joining game failed", "you can't join the same game twice.", ) } else - Right(()) + MonadThrow[F].pure(()) } - def ensurePlayerKey(players: List[Player], playerId: PlayerId, playerKey: PlayerKey): Either[Failures, Player] = { + def ensurePlayerKey[F[_] : MonadThrow](players: List[Player], playerId: PlayerId, playerKey: PlayerKey): F[Player] = { players.find(_.playerId == playerId) match { case None => - Left { + MonadThrow[F].raiseError { Failures( "Couldn't validate key for player that does not exist", "couldn't find you in the game.", ) } case Some(player) if player.playerKey == playerKey => - Right(player) + MonadThrow[F].pure(player) case _ => - Left { + MonadThrow[F].raiseError { Failures( "Invalid player key", "couldn't authenticate you for this game.", @@ -370,19 +377,19 @@ object Games { } } - def ensureSpectatorKey(spectators: List[Spectator], playerId: PlayerId, playerKey: PlayerKey): Either[Failures, Spectator] = { + def ensureSpectatorKey[F[_] : MonadThrow](spectators: List[Spectator], playerId: PlayerId, playerKey: PlayerKey): F[Spectator] = { spectators.find(_.playerId == playerId) match { case None => - Left { + MonadThrow[F].raiseError { Failures( "Couldn't validate key for spectator that does not exist", "couldn't find you in the game.", ) } case Some(spectator) if spectator.playerKey == playerKey => - Right(spectator) + MonadThrow[F].pure(spectator) case _ => - Left { + MonadThrow[F].raiseError { Failures( "Invalid spectator key", "couldn't authenticate you for this game.", @@ -391,19 +398,19 @@ object Games { } } - def ensureHost(players: List[Player], playerKey: PlayerKey): Either[Failures, Player] = { + def ensureHost[F[_] : MonadThrow](players: List[Player], playerKey: PlayerKey): F[Player] = { players.find(_.playerKey == playerKey) match { case None => - Left { + MonadThrow[F].raiseError { Failures( "Couldn't validate host key for player that does not exist", "couldn't find you in the game.", ) } case Some(player) if player.isHost => - Right(player) + MonadThrow[F].pure(player) case _ => - Left { + MonadThrow[F].raiseError { Failures( "Invalid player key, not the host", "you are not the game's host." @@ -412,19 +419,19 @@ object Games { } } - def ensureAdmin(players: List[Player], playerKey: PlayerKey): Either[Failures, Player] = { + def ensureAdmin[F[_] : MonadThrow](players: List[Player], playerKey: PlayerKey): F[Player] = { players.find(_.playerKey == playerKey) match { case None => - Left { + MonadThrow[F].raiseError { Failures( "Couldn't validate host key for player that does not exist", "couldn't find you in the game.", ) } case Some(player) if player.isHost => - Right(player) + MonadThrow[F].pure(player) case _ => - Left { + MonadThrow[F].raiseError { Failures( "Invalid player key, not an admin", "you are not a game admin." @@ -433,11 +440,11 @@ object Games { } } - def ensureActive(inTurn: Option[PlayerId], playerId: PlayerId): Either[Failures, Unit] = { + def ensureActive[F[_] : MonadThrow](inTurn: Option[PlayerId], playerId: PlayerId): F[Unit] = { if (inTurn.contains(playerId)) { - Right(()) + MonadThrow[F].pure(()) } else { - Left { + MonadThrow[F].raiseError { Failures( "Active player check failed", "it is not your turn to act.", diff --git a/core/src/main/scala/io/adamnfish/pokerdot/logic/Play.scala b/core/src/main/scala/io/adamnfish/pokerdot/logic/Play.scala index 7c34b05..2b43860 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/logic/Play.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/logic/Play.scala @@ -1,10 +1,14 @@ package io.adamnfish.pokerdot.logic -import io.adamnfish.pokerdot.models._ +import cats.MonadThrow +import io.adamnfish.pokerdot.models.* import scala.util.Random import io.adamnfish.pokerdot.logic.Utils.RichList -import io.adamnfish.pokerdot.services.Clock +import io.adamnfish.pokerdot.services.Time +import cats.* +import cats.syntax.* +import cats.implicits.* import scala.annotation.tailrec @@ -260,18 +264,18 @@ object Play { * If the timer is not present, the existing blind is re-used. * If we're on a break, advancing to the next round should fail. */ - def blindForNextRound(currentSmallBlind: Int, now: Long, maybeTimerStatus: Option[TimerStatus]): Either[Failures, Int] = { + def blindForNextRound[F[_] : MonadThrow](currentSmallBlind: Int, now: Long, maybeTimerStatus: Option[TimerStatus]): F[Int] = { maybeTimerStatus match { case None => - Right(currentSmallBlind) + MonadThrow[F].pure(currentSmallBlind) case Some(TimerStatus(_, Some(_), _)) => // cannot advance to new round while game is paused - Left(Failures("Cannot advance paused game", "you can't start a new round while the game is paused")) + MonadThrow[F].raiseError(Failures("Cannot advance paused game", "you can't start a new round while the game is paused")) case Some(timerStatus) => timerSmallBlind(timerStatus, now).flatMap { case (nextSmallBlind, onABreak) => if (onABreak) - Left(Failures("Cannot advance while on a break","wait for the break to end before starting a new round", None, None)) - else Right(nextSmallBlind) + MonadThrow[F].raiseError(Failures("Cannot advance while on a break","wait for the break to end before starting a new round", None, None)) + else MonadThrow[F].pure(nextSmallBlind) } } } @@ -281,7 +285,7 @@ object Play { * * This is used when rounds advance, and whenever the timer is edited. */ - def timerSmallBlind(timerStatus: TimerStatus, now: Long): Either[Failures, (Int, Boolean)] = { + def timerSmallBlind[F[_] : MonadThrow](timerStatus: TimerStatus, now: Long): F[(Int, Boolean)] = { // this is important to take care of paused timers val adjustedTimerStartTime = timerStatus.pausedTime match { @@ -333,9 +337,11 @@ object Play { ) } } - mAnswer.orElse(mFallback) - .map(rl => (rl.smallBlind, break)) - .toRight(Failures("No valid timer level, empty timer?", "the timer is broken so we can't move to the next round")) + MonadThrow[F].fromEither( + mAnswer.orElse(mFallback) + .map(rl => (rl.smallBlind, break)) + .toRight(Failures("No valid timer level, empty timer?", "the timer is broken so we can't move to the next round")) + ) } private[logic] def nextActiveFromIndex(players: List[Player], index: Int): Option[PlayerId] = { diff --git a/core/src/main/scala/io/adamnfish/pokerdot/logic/PlayerActions.scala b/core/src/main/scala/io/adamnfish/pokerdot/logic/PlayerActions.scala index 6f6b7cd..363444f 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/logic/PlayerActions.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/logic/PlayerActions.scala @@ -1,16 +1,20 @@ package io.adamnfish.pokerdot.logic -import io.adamnfish.pokerdot.logic.Games._ +import cats.MonadThrow +import io.adamnfish.pokerdot.logic.Games.* import io.adamnfish.pokerdot.logic.Play.{dealHoles, playerIsActive, playerIsInvolved} -import io.adamnfish.pokerdot.models._ -import io.adamnfish.pokerdot.services.{Clock, Rng} +import io.adamnfish.pokerdot.models.* +import io.adamnfish.pokerdot.services.{Time, Rng} +import cats.* +import cats.syntax.* +import cats.implicits.* /** * This logic is quite complex so it gets its own object and tests. */ object PlayerActions { - def bet(game: Game, bet: Int, player: Player): Either[Failures, (Game, ActionSummary)] = { + def bet[F[_] : MonadThrow](game: Game, bet: Int, player: Player): F[(Game, ActionSummary)] = { val allIn = bet == player.stack val betTotal = player.bet + bet val currentBetAmount = Play.currentBetAmount(game.players) @@ -19,15 +23,15 @@ object PlayerActions { for { // ensure bet amount does not exceed stack _ <- - if (bet > player.stack) Left { + if (bet > player.stack) MonadThrow[F].raiseError { Failures( "Bet cannot exceed player stack", "You can't afford that bet.", ) - } else Right(()) + } else MonadThrow[F].unit // ensure bet matches other players' contributions this round _ <- - if (!allIn && betTotal < currentBetAmount) Left { + if (!allIn && betTotal < currentBetAmount) MonadThrow[F].raiseError { if (currentBetAmount > player.stack) { Failures( "Player needs to go all-in to bet", @@ -39,23 +43,23 @@ object PlayerActions { "your bet must be at least as much as the other players have paid.", ) } - } else Right(()) + } else MonadThrow[F].unit // ensure raise amount matches previous raise _ <- - if (!allIn && isRaise && (betTotal - currentBetAmount) < Play.currentRaiseAmount(game.players)) Left { + if (!allIn && isRaise && (betTotal - currentBetAmount) < Play.currentRaiseAmount(game.players)) MonadThrow[F].raiseError { Failures( "Raise amount does not meet previous raises", "you must raise by at least as much as the last bet or raise.", ) - } else Right(()) + } else MonadThrow[F].unit // ensure raise amount matches minimum raise (big blind) _ <- - if (!allIn && isRaise && (betTotal - currentBetAmount) < game.round.smallBlind * 2) Left { + if (!allIn && isRaise && (betTotal - currentBetAmount) < game.round.smallBlind * 2) MonadThrow[F].raiseError { Failures( "Player needs to raise by at least the Big Blind", "the minimum raise is the Big Blind.", ) - } else Right(()) + } else MonadThrow[F].unit updatedPlayers = game.players.map { case thisPlayer if thisPlayer.playerId == player.playerId => // use updated active player in game @@ -83,24 +87,24 @@ object PlayerActions { ) } - def check(game: Game, player: Player): Either[Failures, Game] = { + def check[F[_] : MonadThrow](game: Game, player: Player): F[Game] = { val currentBetAmount = Play.currentBetAmount(game.players) for { // ensure player is allowed to check _ <- - if (player.bet < currentBetAmount) Left { + if (player.bet < currentBetAmount) MonadThrow[F].raiseError { Failures( "Player cannot check until they have called other players", "you have to at least call other players before checking.", ) - } else Right(()) + } else MonadThrow[F].unit _ <- - if (player.checked) Left { + if (player.checked) MonadThrow[F].raiseError { Failures( "Player is already checked", "you have already checked.", ) - } else Right(()) + } else MonadThrow[F].unit updatedPlayers = game.players.map { case p if p.playerId == player.playerId => // use updated active player in game @@ -142,7 +146,7 @@ object PlayerActions { * Checks the round is ready to be advanced, then delegates * to the current round's advancement logic. */ - def advancePhase(game: Game, clock: Clock, rng: Rng): Either[Failures, (Game, Set[PlayerId], Option[(List[PlayerWinnings], List[PotWinnings])])] = { + def advancePhase[F[_] : MonadThrow](game: Game, now: Long, rng: Rng[F]): F[(Game, Set[PlayerId], Option[(List[PlayerWinnings], List[PotWinnings])])] = { for { _ <- ensurePlayersHaveFinishedActing(game) nonBustedPlayerIds = game.players.filterNot(_.busted).map(_.playerId).toSet @@ -152,48 +156,48 @@ object PlayerActions { // only progress through standard phases while players are still playing case (false, PreFlop) => val newGame = advanceFromPreFlop(game) - Right((newGame, nonBustedPlayerIds, None)) + MonadThrow[F].pure((newGame, nonBustedPlayerIds, None)) case (false, Flop) => val newGame = advanceFromFlop(game) - Right((newGame, nonBustedPlayerIds, None)) + MonadThrow[F].pure((newGame, nonBustedPlayerIds, None)) case (false, Turn) => val newGame = advanceFromTurn(game) - Right((newGame, nonBustedPlayerIds, None)) + MonadThrow[F].pure((newGame, nonBustedPlayerIds, None)) case (false, River) => val (newGame, playerWinnings, potWinnings) = advanceFromRiver(game) - Right((newGame, nonBustedPlayerIds, Some(playerWinnings, potWinnings))) + MonadThrow[F].pure((newGame, nonBustedPlayerIds, Some(playerWinnings, potWinnings))) case (_, Showdown) => // we can proceed from showdown whenever 2 or more players are still in the game - startNewRound(game, clock, rng).map { newGame => + startNewRound(game, now, rng).map { newGame => val allPlayerIds = game.players.map(_.playerId).toSet (newGame, allPlayerIds, None) } case (true, _) => // skip straight to showdown from any phase if everyone else has folded val (newGame, playerWinning, potWinning) = advanceFromFoldedFinish(game) - Right((newGame, nonBustedPlayerIds, Some(List(playerWinning), List(potWinning)))) + MonadThrow[F].pure((newGame, nonBustedPlayerIds, Some(List(playerWinning), List(potWinning)))) } } yield result } - def updateBlind(game: Game, updateBlind: UpdateBlind, now: Long): Either[Failures, Game] = { + def updateBlind[F[_] : MonadThrow](game: Game, updateBlind: UpdateBlind, now: Long): F[Game] = { for { newGame <- (updateBlind.smallBlind, updateBlind.timerLevels, updateBlind.playing, updateBlind.progress, game.timer) match { case (Some(_), _, Some(playing), _, _) => val status = if (playing) "start" else "pause" - Left(Failures(s"Cannot $status a timer when using manual blinds", s"you can't $status a timer if you're using manual blinds")) + MonadThrow[F].raiseError(Failures(s"Cannot $status a timer when using manual blinds", s"you can't $status a timer if you're using manual blinds")) case (Some(_), Some(_), _, _, _) => - Left(Failures("Cannot set timer levels when using manual blinds", "you can't create a timer if you're using manual blinds")) + MonadThrow[F].raiseError(Failures("Cannot set timer levels when using manual blinds", "you can't create a timer if you're using manual blinds")) case (Some(_), _, _, Some(_), _) => - Left(Failures("Cannot set timer progress when using manual blinds", "you can't update a timer if you're using manual blinds")) + MonadThrow[F].raiseError(Failures("Cannot set timer progress when using manual blinds", "you can't update a timer if you're using manual blinds")) case (None, None, Some(playing), None, None) => val status = if (playing) "start" else "pause" - Left(Failures(s"Cannot $status timer that does not exist", s"there's no timer running so we can't $status it")) + MonadThrow[F].raiseError(Failures(s"Cannot $status timer that does not exist", s"there's no timer running so we can't $status it")) case (None, None, _, Some(_), None) => - Left(Failures(s"Cannot update progress on a timer that does not exist", s"there's no timer running so we can't update it")) + MonadThrow[F].raiseError(Failures(s"Cannot update progress on a timer that does not exist", s"there's no timer running so we can't update it")) case (Some(manualSmallBlind), None, None, None, _) => // use manual blinds - Right { + MonadThrow[F].pure { game.copy( round = game.round.copy(smallBlind = manualSmallBlind), timer = None, @@ -210,7 +214,7 @@ object PlayerActions { timerLevels .collectFirst { case RoundLevel(_, smallBlind) => smallBlind } .getOrElse(0) // this should be excluded by validation - Right { + MonadThrow[F].pure { game.copy( round = game.round.copy(smallBlind = initialBlind), timer = Some(TimerStatus(startTime, pausedTime, timerLevels)), @@ -224,7 +228,7 @@ object PlayerActions { case None => existingTimer.copy(timerStartTime = now - (newProgress * 1000)) } - Right { + MonadThrow[F].pure { game.copy( timer = Some(newTimer), ) @@ -249,22 +253,22 @@ object PlayerActions { ) case (None, None, None, None, None) => // ?? should be excluded by validation of the update blind request - Right(game) + MonadThrow[F].pure(game) } } yield newGame } - private def updateBlindTimer(currentTimerStatus: TimerStatus, now: Long, maybeSetPlayingStatus: Option[Boolean], maybeProgress: Option[Int], newLevels: Option[List[TimerLevel]]): Either[Failures, TimerStatus] = { + private def updateBlindTimer[F[_] : MonadThrow](currentTimerStatus: TimerStatus, now: Long, maybeSetPlayingStatus: Option[Boolean], maybeProgress: Option[Int], newLevels: Option[List[TimerLevel]]): F[TimerStatus] = { (currentTimerStatus.pausedTime, maybeSetPlayingStatus) match { case (Some(_), Some(false)) => // already paused - Left(Failures("Cannot pause timer when it's already paused", "the timer is already paused.")) + MonadThrow[F].raiseError(Failures("Cannot pause timer when it's already paused", "the timer is already paused.")) case (None, Some(true)) => // already playing - Left(Failures("Cannot start timer when it's already running", "the timer is already running.")) + MonadThrow[F].raiseError(Failures("Cannot start timer when it's already running", "the timer is already running.")) case (Some(currentPausedTime), Some(true)) => // unpause, which adjusts the start time to put the timer in the right place - Right { + MonadThrow[F].pure { TimerStatus( timerStartTime = maybeProgress match { case Some(progress) => @@ -278,7 +282,7 @@ object PlayerActions { } case (None, Some(false)) => // pause - Right { + MonadThrow[F].pure { TimerStatus( timerStartTime = maybeProgress match { case Some(progress) => @@ -292,7 +296,7 @@ object PlayerActions { } case (_, None) => // not setting the play/pause status, so we can just go ahead and update the progress and levels - Right { + MonadThrow[F].pure { TimerStatus( timerStartTime = maybeProgress match { case Some(progress) => @@ -307,7 +311,7 @@ object PlayerActions { case _ => // to get here implies a contradiction between the current and desired play/pause states // this should be excluded earlier in the request lifecycle, so nothing to do here - Left { + MonadThrow[F].raiseError { Failures( s"Unexpected application state. is playing: ${currentTimerStatus.pausedTime.isEmpty}, desired playing state ${maybeSetPlayingStatus}", "couldn't understand the timer update, maybe try refreshing?" @@ -316,7 +320,7 @@ object PlayerActions { } } - private[logic] def ensurePlayersHaveFinishedActing(game: Game): Either[Failures, Unit] = { + private[logic] def ensurePlayersHaveFinishedActing[F[_] : MonadThrow](game: Game): F[Unit] = { val betAmount = Play.currentBetAmount(game.players) val playersYetToAct = game.players.filter(Play.playerIsYetToAct(betAmount, game.players)) if (playersYetToAct.nonEmpty) { @@ -327,14 +331,14 @@ object PlayerActions { case _ => s"${playersYetToAct.size} players still need to act" } - Left( + MonadThrow[F].raiseError( Failures( s"Cannot advance phase when players have not yet acted: ${playersYetToAct.map(_.playerId.pid)}", message, ) ) } else { - Right(()) + MonadThrow[F].unit } } @@ -445,31 +449,32 @@ object PlayerActions { * * If fewer than 2 players remain, the game is finished and we should not proceed. */ - private[logic] def startNewRound(game: Game, clock: Clock, rng: Rng): Either[Failures, Game] = { + private[logic] def startNewRound[F[_] : MonadThrow](game: Game, now: Long, rng: Rng[F]): F[Game] = { // finalise player payments, reset (and bust) players // shuffle, deal new cards, set up new round - val nextState = rng.nextState(game.seed) - val nextDeck = Play.deckOrder(nextState) - val updatedPlayers = game.players.map(resetPlayerForNextRound) - if (updatedPlayers.count(!_.busted) < 2) { - Left(Failures( - "Cannot advance from finished game showdown", - "you can't start a new round because the game has finished", - )) - } else { - // TODO: check whether blind amounts should change based on timer - Play.blindForNextRound(game.round.smallBlind, clock.now(), game.timer).map { newSmallBlind => - val (newButton, blindUpdatedPlayers) = Play.nextDealerAndBlinds(updatedPlayers, game.button, newSmallBlind) - game.copy( - round = game.round.copy( - phase = PreFlop, - smallBlind = newSmallBlind, - ), - button = newButton, // dealer advances - inTurn = Play.nextPlayer(blindUpdatedPlayers, None, newButton), - players = dealHoles(blindUpdatedPlayers, nextDeck), - seed = nextState - ) + rng.nextState(game.seed).flatMap { nextState => + val nextDeck = Play.deckOrder(nextState) + val updatedPlayers = game.players.map(resetPlayerForNextRound) + if (updatedPlayers.count(!_.busted) < 2) { + MonadThrow[F].raiseError(Failures( + "Cannot advance from finished game showdown", + "you can't start a new round because the game has finished", + )) + } else { + // TODO: check whether blind amounts should change based on timer + Play.blindForNextRound(game.round.smallBlind, now, game.timer).map { newSmallBlind => + val (newButton, blindUpdatedPlayers) = Play.nextDealerAndBlinds(updatedPlayers, game.button, newSmallBlind) + game.copy( + round = game.round.copy( + phase = PreFlop, + smallBlind = newSmallBlind, + ), + button = newButton, // dealer advances + inTurn = Play.nextPlayer(blindUpdatedPlayers, None, newButton), + players = dealHoles(blindUpdatedPlayers, nextDeck), + seed = nextState + ) + } } } } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/logic/Representations.scala b/core/src/main/scala/io/adamnfish/pokerdot/logic/Representations.scala index 75e3c33..290d2b1 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/logic/Representations.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/logic/Representations.scala @@ -1,7 +1,11 @@ package io.adamnfish.pokerdot.logic +import cats.MonadThrow import io.adamnfish.pokerdot.logic.Utils.RichList import io.adamnfish.pokerdot.models.{ActionSummary, BigBlind, Failures, Flop, FlopSummary, Game, GameDb, GameId, GameStatus, GameSummary, NoBlind, Player, PlayerAddress, PlayerDb, PlayerId, PlayerKey, PlayerSummary, PlayerWinnings, PotWinnings, PreFlop, PreFlopSummary, River, RiverSummary, Round, RoundSummary, RoundWinnings, SelfSummary, Showdown, ShowdownSummary, SmallBlind, Spectator, SpectatorSummary, Turn, TurnSummary} +import cats.* +import cats.implicits.* +import cats.syntax.* object Representations { @@ -85,28 +89,28 @@ object Representations { } } - def filteredPlayerDbs(players: List[Player], allowlist: Set[PlayerId]): Either[Failures, List[PlayerDb]] = { + def filteredPlayerDbs[F[_] : MonadThrow](players: List[Player], allowlist: Set[PlayerId]): F[List[PlayerDb]] = { val filtered = allPlayerDbs(players.filter(p => allowlist.contains(p.playerId))) if (filtered.isEmpty) { - Left { + MonadThrow[F].raiseError { Failures( "Trying to get playerDb for player ID that does not exist", "there was a problem trying to save a user that could not be found.", ) } } else { - Right(filtered) + MonadThrow[F].pure(filtered) } } - def gameFromDb(gameDb: GameDb, playerDbs: List[PlayerDb]): Either[Failures, Game] = { + def gameFromDb[F[_] : MonadThrow](gameDb: GameDb, playerDbs: List[PlayerDb]): F[Game] = { for { // checks we have a player db for each player / spectator ID in the game - playerDbs <- gameDb.playerIds.eTraverse(lookupPlayerDb(gameDb.gameId, playerDbs)) - spectatorDbs <- gameDb.spectatorIds.eTraverse(lookupPlayerDb(gameDb.gameId, playerDbs)) + playerDbs <- gameDb.playerIds.traverse(lookupPlayerDb(gameDb.gameId, playerDbs)) + spectatorDbs <- gameDb.spectatorIds.traverse(lookupPlayerDb(gameDb.gameId, playerDbs)) // make sure the current player exists inTurn <- gameDb.inTurn - .fold[Either[Failures, Option[PlayerDb]]](Right(None)) { playerId => + .fold[F[Option[PlayerDb]]](MonadThrow[F].pure(None)) { playerId => lookupPlayerDb(gameDb.gameId, playerDbs)(playerId).map(Some(_)) } round = Play.generateRound(gameDb.phase, gameDb.smallBlind, gameDb.seed) @@ -169,8 +173,8 @@ object Representations { ) } - private def lookupPlayerDb(gameId: String, playerDbs: List[PlayerDb])(playerId: String): Either[Failures, PlayerDb] = { - playerDbs + private def lookupPlayerDb[F[_] : MonadThrow](gameId: String, playerDbs: List[PlayerDb])(playerId: String): F[PlayerDb] = { + MonadThrow[F].fromEither(playerDbs .find(_.playerId == playerId) .toRight { Failures( @@ -178,6 +182,7 @@ object Representations { s"could not load all players", ) } + ) } def gameStatus(game: Game, player: Player, actionSummary: ActionSummary): GameStatus = { diff --git a/core/src/main/scala/io/adamnfish/pokerdot/logic/Utils.scala b/core/src/main/scala/io/adamnfish/pokerdot/logic/Utils.scala index 0a8882a..b1b6e83 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/logic/Utils.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/logic/Utils.scala @@ -1,27 +1,10 @@ package io.adamnfish.pokerdot.logic -import io.adamnfish.pokerdot.models.{Attempt, Failure, Failures} -import zio.ZIO +import io.adamnfish.pokerdot.models.{Failure, Failures} object Utils { implicit class RichList[A](val as: List[A]) extends AnyVal { - def eTraverse[L, R](f: A => Either[L, R]): Either[L, List[R]] = { - as.foldRight[Either[L, List[R]]](Right(Nil)) { (a, eAcc) => - for { - r <- f(a) - acc <- eAcc - } yield r :: acc - } - } - - def ioTraverse[B](f: A => Attempt[B]): Attempt[List[B]] = { - ZIO.validatePar(as)(f).mapError { fss => - // collect failure instances from multiple failures into a single Failures instance - Failures(fss.flatMap(_.failures)) - } - } - /** * Converts from stdlib's `-1 = empty` to an Option */ @@ -32,62 +15,6 @@ object Utils { } } - implicit class RichAttempt[A](val attempt: Attempt[A]) extends AnyVal { - def |!|(attempt2: Attempt[A]): Attempt[Unit] = { - ZIO.partition(List(attempt, attempt2))(identity).flatMap { case (failedResults, _) => - if (failedResults.isEmpty) { - ZIO.unit - } else { - val allFailures = failedResults.foldLeft[List[Failure]](Nil) { case (acc, failures) => - acc ++ failures.failures - } - ZIO.fail(Failures(allFailures)) - } - } - } - } - - implicit class RichFailureList(val failures: List[Failure]) extends AnyVal { - def |!|(otherFailures: List[Failure]): List[Failure] = { - failures ++ otherFailures - } - } - - implicit class RichEither[A](val efa: Either[Failures, A]) extends AnyVal { - def attempt: Attempt[A] = { - ZIO.fromEither(efa) - } - } - - object EitherUtils { - def sequence[A](aes: List[Either[Failures, A]]): Either[Failures, List[A]] = { - aes.foldRight[Either[Failures, List[A]]](Right(Nil)) { (ae, acc) => - acc match { - case Left(accFailures) => - ae match { - case Left(fs) => - Left(Failures(fs.failures ++ accFailures.failures)) - case Right(_) => - Left(accFailures) - } - case Right(as) => - ae.map(_ :: as) - } - } - } - } - - object Attempt { - def failAs[A](failures: Failures): Attempt[A] = { - val failed: Attempt[A] = ZIO.fail(failures) - failed - } - - def fromOption[A](ao: Option[A], ifEmpty: Failures): Attempt[A] = { - ao.fold[Attempt[A]](ZIO.fail(ifEmpty))(a => ZIO.succeed(a)) - } - } - def orderFromList[A, B](original: List[A], order: List[B])(identify: A => B): List[A] = { original.sortBy { a => val aId = identify(a) diff --git a/core/src/main/scala/io/adamnfish/pokerdot/models/Clients.scala b/core/src/main/scala/io/adamnfish/pokerdot/models/Clients.scala index 38eb9df..9cbf927 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/models/Clients.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/models/Clients.scala @@ -177,7 +177,7 @@ case class Wake( ) extends Request // Variance is required because advancePhase returns different messages depending on the phase -// this my be a sign that the advancePhase endpoint should be split up. +// this may be a sign that the advancePhase endpoint should be split up. // However, with features like "auto-advance" this may be required in the future, so it isn't // worth refactoring around this requirement for now. case class Response[+M <: Message]( diff --git a/core/src/main/scala/io/adamnfish/pokerdot/models/Failures.scala b/core/src/main/scala/io/adamnfish/pokerdot/models/Failures.scala index 535e341..a33dec7 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/models/Failures.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/models/Failures.scala @@ -1,30 +1,39 @@ package io.adamnfish.pokerdot.models -import zio.{IO, ZIO} - - -case class Failures(failures: List[Failure]) { - def logString: String = failures.map { failure => - List( - Some(failure.logMessage), - failure.context.map(c => s"context: $c"), - failure.exception.map(e => "err: " + e.getStackTrace.mkString("\n")), - failure.exception.flatMap(e => Option(e.getCause).map(c => "caused by: " + c.getStackTrace.mkString("\n"))) - ).flatten.mkString(" | ") - }.mkString(", ") +case class Failures private( + failures: List[Failure], logString: String, exception: Option[Throwable] +) extends Throwable(logString, exception.orNull) { val externalFailures: List[Failure] = failures.filterNot(_.internal) - - def exception: Option[Throwable] = - failures.find(_.exception.isDefined).flatMap(_.exception) + + def externalOnly: Failures = Failures(externalFailures) } object Failures { + private def findException(failures: List[Failure]): Option[Throwable] = + failures.find(_.exception.isDefined).flatMap(_.exception) + + private def generateLogString(failures: List[Failure]): String = + failures.map { failure => + List( + Some(failure.logMessage), + failure.context.map(c => s"context: $c"), + failure.exception.map(e => "err: " + e.getStackTrace.mkString("\n")), + failure.exception.flatMap(e => Option(e.getCause).map(c => "caused by: " + c.getStackTrace.mkString("\n"))) + ).flatten.mkString(" | ") + }.mkString(", ") + def apply(error: Failure): Failures = { - Failures(List(error)) + val failures = List(error) + Failures(failures, generateLogString(failures), findException(failures)) + } + + def apply(failures: Failure*): Failures = { + val lFailures = failures.toList + new Failures(lFailures, generateLogString(lFailures), findException(lFailures)) } - def apply(errors: Seq[Failure]): Failures = { - Failures(errors.toList) + def apply(failures: List[Failure]): Failures = { + Failures(failures, generateLogString(failures), findException(failures)) } def apply( @@ -34,7 +43,7 @@ object Failures { exception: Option[Throwable] = None, internal: Boolean = false, ): Failures = { - Failures(List(Failure(logMessage, userMessage, context, exception, internal))) + Failures(List(Failure(logMessage, userMessage, context, exception, internal)), logMessage, exception) } } @@ -46,6 +55,5 @@ case class Failure( exception: Option[Throwable] = None, internal: Boolean = false ) { - def asIO: IO[Failures, Nothing] = ZIO.fail(Failures(this)) def asFailures: Failures = Failures(this) } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/models/Game.scala b/core/src/main/scala/io/adamnfish/pokerdot/models/Game.scala index 29dad96..0bed47f 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/models/Game.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/models/Game.scala @@ -1,6 +1,6 @@ package io.adamnfish.pokerdot.models -import io.adamnfish.pokerdot.services.{Database, Clock, Messaging, Rng} +import io.adamnfish.pokerdot.services.{Database, Time, Messaging, Rng} case class Game( @@ -72,11 +72,11 @@ case class PlayerKey(key: String) extends AnyVal case class TraceId(tid: String) extends AnyVal -case class AppContext( +case class AppContext[F[_]]( playerAddress: PlayerAddress, traceId: TraceId, - db: Database, - messaging: Messaging, - clock: Clock, - rng: Rng, + db: Database[F], + messaging: Messaging[F], + time: Time[F], + rng: Rng[F], ) diff --git a/core/src/main/scala/io/adamnfish/pokerdot/models/Persistence.scala b/core/src/main/scala/io/adamnfish/pokerdot/models/Persistence.scala index 708b50e..ef0b448 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/models/Persistence.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/models/Persistence.scala @@ -1,6 +1,9 @@ package io.adamnfish.pokerdot.models +/** + * TODO: should we use a secondary index for gameCode, to support initial joining? + */ case class GameDb( gameCode: String, // partition gameId: String, // sort diff --git a/core/src/main/scala/io/adamnfish/pokerdot/models/Serialisation.scala b/core/src/main/scala/io/adamnfish/pokerdot/models/Serialisation.scala index 20b2b0a..1cc44dd 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/models/Serialisation.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/models/Serialisation.scala @@ -1,11 +1,11 @@ package io.adamnfish.pokerdot.models -import cats.syntax.functor._ +import cats.syntax.functor.* import io.circe.Json.JString import io.circe.generic.semiauto.{deriveDecoder, deriveEncoder} -import io.circe.{Decoder, Encoder, Json, JsonObject, KeyEncoder, parser} -import io.circe.syntax._ -import zio.IO +import io.circe.{Codec, Decoder, Encoder, Json, JsonObject, KeyEncoder, parser} +import io.circe.syntax.* +import cats.MonadThrow object Serialisation { @@ -17,64 +17,68 @@ object Serialisation { failures.asJson.noSpaces } - def parse(jsonStr: String, userMessage: String, context: Option[String]): Either[Failures, Json] = { - parser.parse(jsonStr).left.map { parsingFailure => - Failures( - s"Failed to parse request body JSON: ${parsingFailure.message}", - userMessage, - context, - Some(parsingFailure) - ) + def parse[F[_] : MonadThrow](jsonStr: String, userMessage: String, context: Option[String]): F[Json] = { + MonadThrow[F].fromEither { + parser.parse(jsonStr).left.map { parsingFailure => + Failures( + s"Failed to parse request body JSON: ${parsingFailure.message}", + userMessage, + context, + Some(parsingFailure) + ) + } } } - def extractJson[A](json: Json, userMessage: String)(implicit decoder: Decoder[A]): Either[Failures, A] = { - json.as[A].left.map { decodingFailure => - Failures( - s"Failed to parse JSON as expected type: ${decodingFailure.message}", - userMessage, - Some(decodingFailure.history.mkString("|")), - Some(decodingFailure) - ) + def extractJson[F[_] : MonadThrow, A](json: Json, userMessage: String)(implicit decoder: Decoder[A]): F[A] = { + MonadThrow[F].fromEither { + json.as[A].left.map { decodingFailure => + Failures( + s"Failed to parse JSON as expected type: ${decodingFailure.message}", + userMessage, + Some(decodingFailure.history.mkString("|")), + Some(decodingFailure) + ) + } } } // REQUEST PARSERS - def parseCreateGameRequest(json: Json): Either[Failures, CreateGame] = { - extractJson[CreateGame](json, "Could not understand the create game request") + def parseCreateGameRequest[F[_] : MonadThrow](json: Json): F[CreateGame] = { + extractJson[F, CreateGame](json, "Could not understand the create game request") } - def parseJoinGameRequest(json: Json): Either[Failures, JoinGame] = { - extractJson[JoinGame](json, "Could not understand the join game request") + def parseJoinGameRequest[F[_] : MonadThrow](json: Json): F[JoinGame] = { + extractJson[F, JoinGame](json, "Could not understand the join game request") } - def parseStartGameRequest(json: Json): Either[Failures, StartGame] = { - extractJson[StartGame](json, "Could not understand the start game request") + def parseStartGameRequest[F[_] : MonadThrow](json: Json): F[StartGame] = { + extractJson[F, StartGame](json, "Could not understand the start game request") } - def parseUpdateBlindRequest(json: Json): Either[Failures, UpdateBlind] = { - extractJson[UpdateBlind](json, "Could not understand the update blind request") + def parseUpdateBlindRequest[F[_] : MonadThrow](json: Json): F[UpdateBlind] = { + extractJson[F, UpdateBlind](json, "Could not understand the update blind request") } - def parseBetRequest(json: Json): Either[Failures, Bet] = { - extractJson[Bet](json, "Could not understand the bet request") + def parseBetRequest[F[_] : MonadThrow](json: Json): F[Bet] = { + extractJson[F, Bet](json, "Could not understand the bet request") } - def parseCheckRequest(json: Json): Either[Failures, Check] = { - extractJson[Check](json, "Could not understand the check request") + def parseCheckRequest[F[_] : MonadThrow](json: Json): F[Check] = { + extractJson[F, Check](json, "Could not understand the check request") } - def parseFoldRequest(json: Json): Either[Failures, Fold] = { - extractJson[Fold](json, "Could not understand the fold request") + def parseFoldRequest[F[_] : MonadThrow](json: Json): F[Fold] = { + extractJson[F, Fold](json, "Could not understand the fold request") } - def parseAdvancePhaseRequest(json: Json): Either[Failures, AdvancePhase] = { - extractJson[AdvancePhase](json, "Could not understand the advance phase request") + def parseAdvancePhaseRequest[F[_] : MonadThrow](json: Json): F[AdvancePhase] = { + extractJson[F, AdvancePhase](json, "Could not understand the advance phase request") } - def parsePingRequest(json: Json): Either[Failures, Ping] = { - extractJson[Ping](json, "Could not understand the ping request") + def parsePingRequest[F[_] : MonadThrow](json: Json): F[Ping] = { + extractJson[F, Ping](json, "Could not understand the ping request") } @@ -219,8 +223,8 @@ object Serialisation { .mapObject(o => o.add("hand", Json.fromString("straight-flush"))) } - private implicit val TimerStatusEncoder: Encoder[TimerStatus] = deriveEncoder[TimerStatus] - private implicit val TimerStatusDecoder: Decoder[TimerStatus] = deriveDecoder[TimerStatus] + private implicit val timerStatusEncoder: Encoder[TimerStatus] = deriveEncoder[TimerStatus] + private implicit val timerStatusDecoder: Decoder[TimerStatus] = deriveDecoder[TimerStatus] private implicit val roundPhaseEncoder: Encoder[RoundLevel] = deriveEncoder[RoundLevel] private implicit val roundPhaseDecoder: Decoder[RoundLevel] = deriveDecoder[RoundLevel] private implicit val breakEncoder: Encoder[BreakLevel] = deriveEncoder[BreakLevel] @@ -366,7 +370,11 @@ object Serialisation { ) } } - private implicit val failuresEncoder: Encoder[Failures] = deriveEncoder + private implicit val failuresEncoder: Encoder[Failures] = Encoder.instance { failures => + Json.obj( + "failures" -> Json.fromValues(failures.failures.map(failureEncoder.apply)), + ) + } object RequestEncoders { private implicit val createGameEncoder: Encoder[CreateGame] = deriveEncoder[CreateGame] @@ -417,4 +425,40 @@ object Serialisation { request.asJson } } + + object db { + private given encodePhase: Encoder[Phase] = Encoder[String].contramap { + case PreFlop => + "pre-flop" + case Flop => + "flop" + case Turn => + "turn" + case River => + "river" + case Showdown => + "showdown" + } + private given decodePhase: Decoder[Phase] = Decoder[String].emap { + case "pre-flop" => + Right(PreFlop) + case "flop" => + Right(Flop) + case "turn" => + Right(Turn) + case "river" => + Right(River) + case "showdown" => + Right(Showdown) + case other => + Left(s"Invalid phase: $other") + } + // AttributeCodec derivation requires Codec rather than encoder / decoder + private given Codec[Phase] = Codec.from(decodePhase, encodePhase) + private given Codec[TimerStatus] = Codec.from(timerStatusDecoder, timerStatusEncoder) + private given Codec[Hole] = Codec.from(holeDecoder, holeEncoder) + +// val gameDbCodec = summon[ItemCodec[GameDb]] +// val playerDbCodec = summon[ItemCodec[PlayerDb]] + } } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/models/package.scala b/core/src/main/scala/io/adamnfish/pokerdot/models/package.scala deleted file mode 100644 index 4ec29ef..0000000 --- a/core/src/main/scala/io/adamnfish/pokerdot/models/package.scala +++ /dev/null @@ -1,8 +0,0 @@ -package io.adamnfish.pokerdot - -import zio.IO - - -package object models { - type Attempt[A] = IO[Failures, A] -} diff --git a/core/src/main/scala/io/adamnfish/pokerdot/persistence/DynamoDbDatabase.scala b/core/src/main/scala/io/adamnfish/pokerdot/persistence/DynamoDbDatabase.scala index 6d0977d..0e95ca4 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/persistence/DynamoDbDatabase.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/persistence/DynamoDbDatabase.scala @@ -1,17 +1,26 @@ package io.adamnfish.pokerdot.persistence -import software.amazon.awssdk.services.dynamodb.DynamoDbClient +import cats.* +import cats.effect.Async +import cats.implicits.* +import cats.syntax.all.* import io.adamnfish.pokerdot.logic.Games -import io.adamnfish.pokerdot.logic.Utils.{EitherUtils, RichList} -import io.adamnfish.pokerdot.models.{Attempt, Failure, Failures, GameDb, GameId, PlayerDb} +import io.adamnfish.pokerdot.models.* import io.adamnfish.pokerdot.services.Database -import org.scanamo._ -import org.scanamo.syntax._ -import org.scanamo.generic.auto._ -import zio.ZIO +import org.scanamo.* +import org.scanamo.generic.auto.* +import org.scanamo.syntax.* +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import scala.jdk.CollectionConverters.* +import scala.util.control.NonFatal -class DynamoDbDatabase(client: DynamoDbClient, gameTableName: String, playerTableName: String) extends Database { +class DynamoDbDatabase[F[_]: Async]( + client: DynamoDbAsyncClient, + gameTableName: String, + playerTableName: String +) extends Database[F] { + private val scanamo = ScanamoCats[F](client) // TODO: switch DB models to use PlayerId? // provide implicit to allow Scanamo to use those wrapper types @@ -19,76 +28,127 @@ class DynamoDbDatabase(client: DynamoDbClient, gameTableName: String, playerTabl private val players = Table[PlayerDb](playerTableName) // TODO: consider whether this should just derive a gameCode and call lookup - override def getGame(gameId: GameId): Attempt[Option[GameDb]] = { + override def getGame(gameId: GameId): F[Option[GameDb]] = { val gameCode = Games.gameCode(gameId) for { - maybeResult <- execAsAttempt(games.get("gameCode" === gameCode and "gameId" === gameId.gid)) - maybeGameDb <- maybeResult.fold[Attempt[Option[GameDb]]](ZIO.succeed(None)) { result => - resultToAttempt(result).map(Some(_)) + maybeResult <- handleDbErr( + scanamo.exec[Option[Either[DynamoReadError, GameDb]]]( + games.get("gameCode" === gameCode and "gameId" === gameId.gid) + ) + ) + maybeGameDb <- maybeResult.fold[F[Option[GameDb]]](Async[F].pure(None)) { + result => + handleDbReadErr(result).map(Some(_)) } } yield maybeGameDb - } - override def lookupGame(gameCode: String): Attempt[Option[GameDb]] = { - for { - results <- execAsAttempt(games.query("gameCode" === gameCode and ("gameId" beginsWith gameCode))) - maybeResult <- results match { - case Nil => - ZIO.succeed(None) - case result :: Nil => - ZIO.succeed(Some(result)) - case _ => - Failure( - s"Multiple games found for code `$gameCode`", - "couldn't find a game for that code", - ).asIO - } - maybeGameDb <- maybeResult.fold[Attempt[Option[GameDb]]](ZIO.succeed(None)) { result => - resultToAttempt(result).map(Some(_)) - } - } yield maybeGameDb } + override def lookupGame(gameCode: String): F[Option[GameDb]] = { + if (gameCode.isEmpty) + Async[F].raiseError( + Failures( + "empty gameCode provided to searchGameCode", + "error fetching saved data", + exception = None + ) + ) + else { + for { + results <- handleDbErr( + scanamo.exec( + games.query( + "gameCode" === gameCode and ("gameId" beginsWith gameCode) + ) + ) + ) + maybeResult <- results match { + case Nil => + Async[F].pure(None) + case result :: Nil => + Async[F].pure(Some(result)) + case _ => + Async[F].raiseError( + Failure( + s"Multiple games found for code `$gameCode`", + "couldn't find a game for that code" + ).asFailures + ) + } + maybeGameDb <- maybeResult.fold[F[Option[GameDb]]]( + Async[F].pure(None) + ) { result => + handleDbReadErr(result).map(Some(_)) + } + } yield maybeGameDb + } + } - override def searchGameCode(gameCode: String): Attempt[List[GameDb]] = { - for { - results <- execAsAttempt(games.query("gameCode" === gameCode and ("gameId" beginsWith gameCode))) - gameDbs <- results.ioTraverse(resultToAttempt) - } yield gameDbs + override def searchGameCode(gameCode: String): F[List[GameDb]] = { + if (gameCode.isEmpty) + Async[F].raiseError( + Failures( + "empty gameCode provided to searchGameCode", + "error fetching saved data", + exception = None + ) + ) + else { + for { + results <- handleDbErr( + scanamo.exec( + games.query( + "gameCode" === gameCode and ("gameId" beginsWith gameCode) + ) + ) + ) + gameDbs <- results.traverse(handleDbReadErr) + } yield gameDbs + } } - override def getPlayers(gameId: GameId): Attempt[List[PlayerDb]] = { + override def getPlayers(gameId: GameId): F[List[PlayerDb]] = { for { - results <- execAsAttempt(players.query("gameId" === gameId.gid)) - players <- results.ioTraverse(resultToAttempt) + results <- handleDbErr( + scanamo.exec(players.query("gameId" === gameId.gid)) + ) + players <- results.traverse(handleDbReadErr) } yield players } - override def writeGame(gameDB: GameDb): Attempt[Unit] = { + override def writeGame(gameDB: GameDb): F[Unit] = { for { - result <- execAsAttempt(games.put(gameDB)) + result <- handleDbErr(scanamo.exec(games.put(gameDB))) } yield result } - override def writePlayer(playerDB: PlayerDb): Attempt[Unit] = { + override def writePlayer(playerDB: PlayerDb): F[Unit] = { for { - result <- execAsAttempt(players.put(playerDB)) + result <- handleDbErr(scanamo.exec(players.put(playerDB))) } yield result } - def execAsAttempt[A](op: ops.ScanamoOps[A]): Attempt[A] = { - ZIO.attempt { - Scanamo(client).exec(op) - }.mapError { err => - Failures("Uncaught DB error", "I had a problem saving the game", None, Some(err)) - } - } - - private def resultToAttempt[A](result: Either[DynamoReadError, A]): Attempt[A] = { - ZIO.fromEither { + private def handleDbReadErr[A]( + result: Either[DynamoReadError, A] + ): F[A] = { + Async[F].fromEither { result.left.map { dre => - Failures(s"DynamoReadError: $dre", "error with saved data", None, None) + Failures( + s"DynamoReadError: $dre", + "error reading saved data", + None, + None + ) } } } + + private def handleDbErr[A](fa: F[A]): F[A] = + Async[F].adaptError(fa) { case NonFatal(err) => + Failures( + "unhandled DynamoDB error", + "error fetching saved data", + exception = Some(err) + ) + } } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/services/Clock.scala b/core/src/main/scala/io/adamnfish/pokerdot/services/Clock.scala deleted file mode 100644 index eafd32d..0000000 --- a/core/src/main/scala/io/adamnfish/pokerdot/services/Clock.scala +++ /dev/null @@ -1,12 +0,0 @@ -package io.adamnfish.pokerdot.services - -import java.time.ZonedDateTime - - -trait Clock { - val now: () => Long -} - -object Clock extends Clock { - override val now: () => Long = () => ZonedDateTime.now().toInstant.toEpochMilli -} diff --git a/core/src/main/scala/io/adamnfish/pokerdot/services/Database.scala b/core/src/main/scala/io/adamnfish/pokerdot/services/Database.scala index 78e7949..09e579d 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/services/Database.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/services/Database.scala @@ -1,23 +1,28 @@ package io.adamnfish.pokerdot.services -import io.adamnfish.pokerdot.models.{Attempt, GameDb, GameId, PlayerDb} +import io.adamnfish.pokerdot.models.{GameDb, GameId, PlayerDb} +import cats.Monad +import cats._ +import cats.data._ +import cats.syntax.all._ -trait Database { - def getGame(gameId: GameId): Attempt[Option[GameDb]] - def lookupGame(gameCode: String): Attempt[Option[GameDb]] +trait Database[F[_]] { + def getGame(gameId: GameId): F[Option[GameDb]] - def searchGameCode(gameCode: String): Attempt[List[GameDb]] + def lookupGame(gameCode: String): F[Option[GameDb]] - def getPlayers(gameId: GameId): Attempt[List[PlayerDb]] + def searchGameCode(gameCode: String): F[List[GameDb]] - def writeGame(gameDB: GameDb): Attempt[Unit] + def getPlayers(gameId: GameId): F[List[PlayerDb]] - def writePlayer(playerDB: PlayerDb): Attempt[Unit] + def writeGame(gameDB: GameDb): F[Unit] + + def writePlayer(playerDB: PlayerDb): F[Unit] } object Database { - def checkUniquePrefix(gameId: GameId, prefixLength: Int, persistence: Database): Attempt[Boolean] = { + def checkUniquePrefix[F[_] : Monad](gameId: GameId, prefixLength: Int, persistence: Database[F]): F[Boolean] = { val gameCode = gameId.gid.take(prefixLength) for { gameDbs <- persistence.searchGameCode(gameCode) diff --git a/core/src/main/scala/io/adamnfish/pokerdot/services/Messaging.scala b/core/src/main/scala/io/adamnfish/pokerdot/services/Messaging.scala index c4808be..1de9e28 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/services/Messaging.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/services/Messaging.scala @@ -1,9 +1,9 @@ package io.adamnfish.pokerdot.services -import io.adamnfish.pokerdot.models.{Attempt, Failures, Message, PlayerAddress} +import io.adamnfish.pokerdot.models.{Failures, Message, PlayerAddress} -trait Messaging { - def sendMessage(playerAddress: PlayerAddress, message: Message): Attempt[Unit] +trait Messaging[F[_]] { + def sendMessage(playerAddress: PlayerAddress, message: Message): F[Unit] - def sendError(playerAddress: PlayerAddress, message: Failures): Attempt[Unit] + def sendError(playerAddress: PlayerAddress, message: Failures): F[Unit] } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/services/Rng.scala b/core/src/main/scala/io/adamnfish/pokerdot/services/Rng.scala index dd2d7a8..bdf550c 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/services/Rng.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/services/Rng.scala @@ -1,21 +1,24 @@ package io.adamnfish.pokerdot.services import java.security.SecureRandom +import cats.effect.IO +import cats.effect.kernel.Sync -trait Rng { - def randomState(): Long +trait Rng[F[_]] { + def randomState: F[Long] - def nextState(state: Long): Long + def nextState(state: Long): F[Long] } -class RandomRng extends Rng { - override def randomState(): Long = { - new SecureRandom().nextLong() +// TODO: switch to cats-effect's own Random +class RandomRng[F[_] : Sync] extends Rng[F] { + override def randomState: F[Long] = { + Sync[F].delay(new SecureRandom().nextLong()) } // True random in PROD, for each round - override def nextState(state: Long): Long = { - new SecureRandom().nextLong() + override def nextState(state: Long): F[Long] = { + Sync[F].delay(new SecureRandom().nextLong()) } } diff --git a/core/src/main/scala/io/adamnfish/pokerdot/services/Time.scala b/core/src/main/scala/io/adamnfish/pokerdot/services/Time.scala new file mode 100644 index 0000000..f6c7fe5 --- /dev/null +++ b/core/src/main/scala/io/adamnfish/pokerdot/services/Time.scala @@ -0,0 +1,17 @@ +package io.adamnfish.pokerdot.services + +import java.time.ZonedDateTime +import cats.effect.IO +import cats.effect.kernel.Clock +import cats.Functor +import cats.syntax.all.* + + +trait Time[F[_]] { + val now: F[Long] +} + +class RealTime[F[_] : Clock : Functor] extends Time[F] { + override val now: F[Long] = + Clock[F].realTimeInstant.map(_.toEpochMilli) +} diff --git a/core/src/main/scala/io/adamnfish/pokerdot/validation/Validation.scala b/core/src/main/scala/io/adamnfish/pokerdot/validation/Validation.scala index b2546ac..858f142 100644 --- a/core/src/main/scala/io/adamnfish/pokerdot/validation/Validation.scala +++ b/core/src/main/scala/io/adamnfish/pokerdot/validation/Validation.scala @@ -1,9 +1,13 @@ package io.adamnfish.pokerdot.validation +import cats.MonadThrow import io.adamnfish.pokerdot.models.Serialisation.{parseAdvancePhaseRequest, parseBetRequest, parseCheckRequest, parseCreateGameRequest, parseFoldRequest, parseJoinGameRequest, parsePingRequest, parseStartGameRequest, parseUpdateBlindRequest} -import io.adamnfish.pokerdot.models._ -import io.adamnfish.pokerdot.validation.Validators._ +import io.adamnfish.pokerdot.models.* +import io.adamnfish.pokerdot.validation.Validators.* import io.circe.Json +import cats.* +import cats.syntax.* +import cats.implicits.* object Validation { @@ -11,42 +15,42 @@ object Validation { validator(a, context, friendlyContext) } - private[validation] def asResult[A](a: A, failures: List[Failure]): Either[Failures, A] = { + private[validation] def asResult[A, F[_] : MonadThrow](a: A, failures: List[Failure]): F[A] = { failures match { - case Nil => Right(a) - case fs => Left(Failures(fs)) + case Nil => MonadThrow[F].pure(a) + case fs => MonadThrow[F].raiseError(Failures(fs)) } } - def validate(createGame: CreateGame): Either[Failures, CreateGame] = { + def validate[F[_] : MonadThrow](createGame: CreateGame): F[CreateGame] = { asResult(createGame, validate(createGame.gameName, "gameName", "game name", sensibleLength) ++ validate(createGame.screenName, "screenName", "player name", sensibleLength) ) } - def extractCreateGame(json: Json): Either[Failures, CreateGame] = { + def extractCreateGame[F[_] : MonadThrow](json: Json): F[CreateGame] = { for { raw <- parseCreateGameRequest(json) validated <- validate(raw) } yield validated } - def validate(joinGame: JoinGame): Either[Failures, JoinGame] = { + def validate[F[_] : MonadThrow](joinGame: JoinGame): F[JoinGame] = { asResult(joinGame, validate(joinGame.gameCode, "gameCode", "game code", gameCode) ++ validate(joinGame.screenName, "screenName", "player name", sensibleLength) ) } - def extractJoinGame(json: Json): Either[Failures, JoinGame] = { + def extractJoinGame[F[_] : MonadThrow](json: Json): F[JoinGame] = { for { raw <- parseJoinGameRequest(json) validated <- validate(raw) } yield validated } - def validate(startGame: StartGame): Either[Failures, StartGame] = { + def validate[F[_] : MonadThrow](startGame: StartGame): F[StartGame] = { asResult(startGame, validate(startGame.gameId.gid, "gameId", "game's id", isUUID) ++ validate(startGame.playerId.pid, "playerId", "player's id", isUUID) ++ @@ -87,14 +91,14 @@ object Validation { ) } - def extractStartGame(json: Json): Either[Failures, StartGame] = { + def extractStartGame[F[_] : MonadThrow](json: Json): F[StartGame] = { for { raw <- parseStartGameRequest(json) validated <- validate(raw) } yield validated } - def validate(bet: Bet): Either[Failures, Bet] = { + def validate[F[_] : MonadThrow](bet: Bet): F[Bet] = { asResult(bet, validate(bet.gameId.gid, "gameId", "game's id", isUUID) ++ validate(bet.playerId.pid, "playerId", "player's id", isUUID) ++ @@ -103,14 +107,14 @@ object Validation { ) } - def extractBet(json: Json): Either[Failures, Bet] = { + def extractBet[F[_] : MonadThrow](json: Json): F[Bet] = { for { raw <- parseBetRequest(json) validated <- validate(raw) } yield validated } - def validate(check: Check): Either[Failures, Check] = { + def validate[F[_] : MonadThrow](check: Check): F[Check] = { asResult(check, validate(check.gameId.gid, "gameId", "game's id", isUUID) ++ validate(check.playerId.pid, "playerId", "player's id", isUUID) ++ @@ -118,14 +122,14 @@ object Validation { ) } - def extractCheck(json: Json): Either[Failures, Check] = { + def extractCheck[F[_] : MonadThrow](json: Json): F[Check] = { for { raw <- parseCheckRequest(json) validated <- validate(raw) } yield validated } - def validate(fold: Fold): Either[Failures, Fold] = { + def validate[F[_] : MonadThrow](fold: Fold): F[Fold] = { asResult(fold, validate(fold.gameId.gid, "gameId", "game's id", isUUID) ++ validate(fold.playerId.pid, "playerId", "player's id", isUUID) ++ @@ -133,14 +137,14 @@ object Validation { ) } - def extractFold(json: Json): Either[Failures, Fold] = { + def extractFold[F[_] : MonadThrow](json: Json): F[Fold] = { for { raw <- parseFoldRequest(json) validated <- validate(raw) } yield validated } - def validate(advancePhase: AdvancePhase): Either[Failures, AdvancePhase] = { + def validate[F[_] : MonadThrow](advancePhase: AdvancePhase): F[AdvancePhase] = { asResult(advancePhase, validate(advancePhase.gameId.gid, "gameId", "game's id", isUUID) ++ validate(advancePhase.playerId.pid, "playerId", "player's id", isUUID) ++ @@ -148,14 +152,14 @@ object Validation { ) } - def extractAdvancePhase(json: Json): Either[Failures, AdvancePhase] = { + def extractAdvancePhase[F[_] : MonadThrow](json: Json): F[AdvancePhase] = { for { raw <- parseAdvancePhaseRequest(json) validated <- validate(raw) } yield validated } - def validate(updateBlind: UpdateBlind): Either[Failures, UpdateBlind] = { + def validate[F[_] : MonadThrow](updateBlind: UpdateBlind): F[UpdateBlind] = { val emptyErrors = if (updateBlind.timerLevels.isEmpty && updateBlind.smallBlind.isEmpty && updateBlind.playing.isEmpty && updateBlind.progress.isEmpty) List( @@ -180,14 +184,14 @@ object Validation { ) } - def extractUpdateBlind(json: Json): Either[Failures, UpdateBlind] = { + def extractUpdateBlind[F[_] : MonadThrow](json: Json): F[UpdateBlind] = { for { raw <- parseUpdateBlindRequest(json) validated <- validate(raw) } yield validated } - def validate(ping: Ping): Either[Failures, Ping] = { + def validate[F[_] : MonadThrow](ping: Ping): F[Ping] = { asResult(ping, validate(ping.gameId.gid, "gameId", "game's id", isUUID) ++ validate(ping.playerId.pid, "playerId", "player's id", isUUID) ++ @@ -195,7 +199,7 @@ object Validation { ) } - def extractPing(json: Json): Either[Failures, Ping] = { + def extractPing[F[_] : MonadThrow](json: Json): F[Ping] = { for { raw <- parsePingRequest(json) validated <- validate(raw) diff --git a/core/src/test/scala/io/adamnfish/pokerdot/TestHelpers.scala b/core/src/test/scala/io/adamnfish/pokerdot/TestHelpers.scala index af341eb..0bb5662 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/TestHelpers.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/TestHelpers.scala @@ -1,36 +1,80 @@ package io.adamnfish.pokerdot -import io.adamnfish.pokerdot.models.{Attempt, Failures} +import cats.effect.IO +import io.adamnfish.pokerdot.models.Failures import io.circe.{Json, parser} import org.scalacheck.Gen import org.scalactic.source.Position +import org.scalatest.compatible.Assertion import org.scalatest.exceptions.TestFailedException import org.scalatest.matchers.HavePropertyMatcher import org.scalatest.matchers.should.Matchers -import org.scalatest.{Assertion, Failed, Succeeded} -import zio.{Exit, Unsafe} - trait TestHelpers extends Matchers { - val testRuntime = zio.Runtime.default + val TODO: IO[Assertion] = IO.pure(assert(true)) + + /** Date millis in a semi-sensible range (we don't need to worry about Long + * overflow, for example) + */ + val dateGen: Gen[Long] = + Gen.chooseNum(0L, 4102444800000L) - def having[A](propertyName: String, propertyValue: A): HavePropertyMatcher[AnyRef, Any] = { - Symbol(propertyName) (propertyValue) + def having[A]( + propertyName: String, + propertyValue: A + ): HavePropertyMatcher[AnyRef, Any] = { + Symbol(propertyName)(propertyValue) } implicit class HavingTestHelperString(propertyName: String) { - def as[A](propertyValue: A)(implicit pos: Position): HavePropertyMatcher[AnyRef, Any] = { - Symbol(propertyName) (propertyValue) + infix def as[A]( + propertyValue: A + )(implicit pos: Position): HavePropertyMatcher[AnyRef, Any] = { + Symbol(propertyName)(propertyValue) } } implicit class RichEither[L, R](e: Either[L, R]) { + def succeeded(implicit pos: Position) = + e.fold( + { l => + throw new TestFailedException( + _ => + Some( + s"The Either on which succeeded was invoked was not a Right, got Left($l)" + ), + None, + pos + ) + }, + _ => () + ) + + def failed(implicit pos: Position) = + e.fold( + _ => (), + { r => + throw new TestFailedException( + _ => + Some( + s"The Either on which failed was invoked was not a Left, got Right($r)" + ), + None, + pos + ) + } + ) + def value(implicit pos: Position): R = { e.fold( { l => throw new TestFailedException( - _ => Some(s"The Either on which value was invoked was not a Right, got Left($l)"), - None, pos + _ => + Some( + s"The Either on which value was invoked was not a Right, got Left($l)" + ), + None, + pos ) }, identity @@ -42,76 +86,41 @@ trait TestHelpers extends Matchers { identity, { r => throw new TestFailedException( - _ => Some(s"The Either on which leftValue was invoked was not a Left, got Right($r)"), - None, position + _ => + Some( + s"The Either on which leftValue was invoked was not a Left, got Right($r)" + ), + None, + position ) } ) } - } - - /** - * For testing 'pure' attempts (i.e. `Either[Failures, ?]`). - */ - implicit class RichEitherFailures[R](er: Either[Failures, R]) { - def is(attemptStatus: AttemptStatus)(implicit pos: Position): Assertion = { - er match { - case Right(a) => - if (attemptStatus == ASuccess) Succeeded - else Failed(s"Expected failed either but got success `$a`").toSucceeded - case Left(left) => - if (attemptStatus == AFailure) Succeeded - else Failed(s"Expected successful either, got failure: ${left.logString}").toSucceeded - } - } - } - - implicit class RichAttempt[A](aa: Attempt[A]) { - def value()(implicit pos: Position): A = { - Unsafe.unsafe { implicit unsafe => - testRuntime.unsafe.run(aa) match { - case Exit.Success(a) => - a - case Exit.Failure(cause) => - throw new TestFailedException( - _ => Some(s"Expected successful attempt, got failures: ${cause.failures.map(_.logString).mkString(" || ")}"), - None, pos - ) - } - } - } def failures()(implicit pos: Position): Failures = { - Unsafe.unsafe { implicit unsafe => - testRuntime.unsafe.run(aa) match { - case Exit.Success(a) => + e.fold( + { + case f: Failures => f + case l => throw new TestFailedException( - _ => Some(s"Expected failed attempt, got successful result: $a"), - None, pos + _ => Some(s"Expected Failures in Left, got $l"), + None, + pos ) - case Exit.Failure(cause) => - Failures(cause.failures.flatMap(_.failures)) - } - } - } - - def is(attemptStatus: AttemptStatus)(implicit pos: Position): Assertion = { - Unsafe.unsafe { implicit unsafe => - testRuntime.unsafe.run(aa) match { - case Exit.Success(a) => - if (attemptStatus == ASuccess) Succeeded - else Failed(s"Expected failed attempt but got success `$a`").toSucceeded - case Exit.Failure(cause) => - if (attemptStatus == AFailure) Succeeded - else Failed(s"Expected successful attempt, got failures: ${cause.failures.map(_.logString).mkString(" || ")}").toSucceeded + }, + { r => + throw new TestFailedException( + _ => + Some( + s"The Either on which leftValue was invoked was not a Left, got Right($r)" + ), + None, + pos + ) } - } + ) } } - - sealed trait AttemptStatus - case object ASuccess extends AttemptStatus - case object AFailure extends AttemptStatus } object TestHelpers { def parseReq(jsonStr: String)(implicit pos: Position): Json = { @@ -126,4 +135,4 @@ object TestHelpers { json } } -} \ No newline at end of file +} diff --git a/core/src/test/scala/io/adamnfish/pokerdot/TestServices.scala b/core/src/test/scala/io/adamnfish/pokerdot/TestServices.scala index 453de88..c07d3db 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/TestServices.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/TestServices.scala @@ -1,17 +1,18 @@ package io.adamnfish.pokerdot -import io.adamnfish.pokerdot.services.{Clock, Rng} +import cats.Applicative +import io.adamnfish.pokerdot.services.{Time, Rng} -object TestClock extends Clock { - override val now: () => Long = () => 0L +class TestTime[F[_] : Applicative] extends Time[F] { + override val now: F[Long] = Applicative[F].pure(0L) } -class ConfigurableTestClock(currentTime: Long) extends Clock { - override val now: () => Long = () => currentTime +class ConfigurableTestTime[F[_] : Applicative](currentTime: Long) extends Time[F] { + override val now: F[Long] = Applicative[F].pure(currentTime) } -object TestRng extends Rng { - override def randomState(): Long = 1L - override def nextState(state: Long): Long = state + 1L +class TestRng[F[_] : Applicative] extends Rng[F] { + override def randomState: F[Long] = Applicative[F].pure(1L) + override def nextState(state: Long): F[Long] = Applicative[F].pure(state + 1L) } diff --git a/core/src/test/scala/io/adamnfish/pokerdot/logic/GamesTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/logic/GamesTest.scala index bbca5bd..1d63b9d 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/logic/GamesTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/logic/GamesTest.scala @@ -1,56 +1,63 @@ package io.adamnfish.pokerdot.logic import io.adamnfish.pokerdot.logic.Cards.RichRank -import io.adamnfish.pokerdot.{PokerGenerators, TestClock, TestHelpers} +import io.adamnfish.pokerdot.{PokerGenerators, TestHelpers, TestTime} import io.adamnfish.pokerdot.logic.Play.generateRound -import io.adamnfish.pokerdot.models._ -import io.adamnfish.pokerdot.logic.Games._ +import io.adamnfish.pokerdot.models.* +import io.adamnfish.pokerdot.logic.Games.* import org.scalacheck.Gen -import org.scalatest.{EitherValues, OptionValues} +import org.scalatest.{EitherValues, OptionValues, TryValues} import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import scala.util.Random +import scala.util.{Random, Try} -class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestHelpers with OptionValues with PokerGenerators { +class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestHelpers with OptionValues with TryValues with PokerGenerators { "newGame" - { "initialises the basic fields correctly" in { - forAll { (gameName: String, trackStacks: Boolean, seed: Long) => - newGame(gameName, trackStacks, TestClock, seed) should have( - "gameName" as gameName, - "players" as Nil, - "spectators" as Nil, - "inTurn" as None, - "button" as 0, - "started" as false, - "trackStacks" as trackStacks, - "timer" as None, - "seed" as seed, - ) + forAll(dateGen) { now => + forAll { (gameName: String, trackStacks: Boolean, seed: Long) => + newGame(gameName, trackStacks, now, seed) should have( + "gameName" as gameName, + "players" as Nil, + "spectators" as Nil, + "inTurn" as None, + "button" as 0, + "started" as false, + "startTime" as now, + "trackStacks" as trackStacks, + "timer" as None, + "seed" as seed, + ) + } } } - "sets expiry based on the time generated by the provided Clock implementation" in { - val game = newGame("game name", false, TestClock, 0) - game.expiry shouldEqual expiryTime(TestClock.now()) + "sets expiry based on the time generated by the provided time" in { + forAll(dateGen) { now => + val game = newGame("game name", false, now, 0) + game.expiry shouldEqual expiryTime(now) + } } "sets startTime to the current time as generated by the provided Clock implementation" in { - val game = newGame("game name", false, TestClock, 0) - game.startTime shouldEqual TestClock.now() + forAll(dateGen) { now => + val game = newGame("game name", false, now, 0) + game.startTime shouldEqual now + } } "sets roundType to pre-flop" in { - val game = newGame("gameName", true, TestClock, 12345L) + val game = newGame("gameName", true, 1000L, 12345L) game.round.phase shouldEqual PreFlop } "cards used for this round are equal if the seed is equal" in { forAll { (seed: Long) => - val game1 = newGame("gameName", false, TestClock, seed) - val game2 = newGame("gameName", false, TestClock, seed) + val game1 = newGame("gameName", false, 1000L, seed) + val game2 = newGame("gameName", false, 1000L, seed) game1.round should have( "burn1" as game2.round.burn1, "flop1" as game2.round.flop1, @@ -69,10 +76,10 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC // this helps prevent intermittent failures when the generator happens to stumble across a clash forAll { (seed1: Long, seed2: Long, seed3: Long) => whenever(Set(seed1, seed2, seed3).size == 3) { - val game1 = newGame("gameName", false, TestClock, seed1) - val game2 = newGame("gameName", false, TestClock, seed2) - val game3 = newGame("gameName", false, TestClock, seed3) - val game4 = newGame("gameName", false, TestClock, seed1 + 1) + val game1 = newGame("gameName", false, 1000L, seed1) + val game2 = newGame("gameName", false, 1000L, seed2) + val game3 = newGame("gameName", false, 1000L, seed3) + val game4 = newGame("gameName", false, 1000L, seed1 + 1) val distinctRounds = Set(game1.round, game2.round, game3.round, game4.round) distinctRounds.size > 1 } @@ -81,7 +88,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "cards can be regenerated from the current game seed" in { forAll { (seed: Long) => - val game = newGame("gameName", false, TestClock, seed) + val game = newGame("gameName", false, 1000L, seed) val regeneratedRound = generateRound(PreFlop, 0, game.seed) game.round shouldEqual regeneratedRound } @@ -90,37 +97,41 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "newPlayer" - { "initialises basic fields correctly" in { - forAll { (gid: String, screenName: String, isHost: Boolean, address: String) => - val player = newPlayer(GameId(gid), screenName, isHost, PlayerAddress(address), TestClock) - player should have( - "gameId" as gid, - "playerAddress" as address, - "screenName" as screenName, - "stack" as 0, - "pot" as 0, - "bet" as 0, - "folded" as false, - "busted" as false, - "hole" as None, - "isHost" as isHost, - ) + forAll(dateGen) { now => + forAll { (gid: String, screenName: String, isHost: Boolean, address: String) => + val player = newPlayer(GameId(gid), screenName, isHost, PlayerAddress(address), now) + player should have( + "gameId" as gid, + "playerAddress" as address, + "screenName" as screenName, + "stack" as 0, + "pot" as 0, + "bet" as 0, + "folded" as false, + "busted" as false, + "hole" as None, + "isHost" as isHost, + ) + } } } "sets expiry based on the time generated by the provided Clock implementation" in { - val player = newPlayer(GameId("gid"), "screenName1", false, PlayerAddress("address1"), TestClock) - player.expiry shouldEqual expiryTime(TestClock.now()) + forAll(dateGen) { now => + val player = newPlayer(GameId("gid"), "screenName1", false, PlayerAddress("address1"), now) + player.expiry shouldEqual expiryTime(now) + } } "produces a different player ID each time it is called" in { - val player1 = newPlayer(GameId("gid"), "screenName1", false, PlayerAddress("address1"), TestClock) - val player2 = newPlayer(GameId("gid"), "screenName2", false, PlayerAddress("address2"), TestClock) + val player1 = newPlayer(GameId("gid"), "screenName1", false, PlayerAddress("address1"), 1000L) + val player2 = newPlayer(GameId("gid"), "screenName2", false, PlayerAddress("address2"), 1000L) player1.playerId should not equal player2.playerId } "produces a different player key each time it is called" in { - val player1 = newPlayer(GameId("gid"), "screenName1", false, PlayerAddress("address1"), TestClock) - val player2 = newPlayer(GameId("gid"), "screenName2", false, PlayerAddress("address2"), TestClock) + val player1 = newPlayer(GameId("gid"), "screenName1", false, PlayerAddress("address1"), 1000L) + val player2 = newPlayer(GameId("gid"), "screenName2", false, PlayerAddress("address2"), 1000L) player1.playerKey should not equal player2.playerKey } } @@ -128,7 +139,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "newSpectator" - { "initialises basic fields correctly" in { forAll { (gid: String, screenName: String, isHost: Boolean, address: String) => - val spectator = newSpectator(GameId(gid), screenName, isHost, PlayerAddress(address), TestClock) + val spectator = newSpectator(GameId(gid), screenName, isHost, PlayerAddress(address), 1000L) spectator should have( "gameId" as gid, "playerAddress" as address, @@ -139,19 +150,21 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC } "sets expiry to the time generated by the provided Clock implementation" in { - val spectator = newSpectator(GameId("gid"), "screenName1", false, PlayerAddress("address1"), TestClock) - spectator.expiry shouldEqual expiryTime(TestClock.now()) + forAll(dateGen) { now => + val spectator = newSpectator(GameId("gid"), "screenName1", false, PlayerAddress("address1"), now) + spectator.expiry shouldEqual expiryTime(now) + } } "produces a different player ID each time it is called" in { - val spectator1 = newSpectator(GameId("gid"), "screenName1", false, PlayerAddress("address1"), TestClock) - val spectator2 = newSpectator(GameId("gid"), "screenName2", false, PlayerAddress("address2"), TestClock) + val spectator1 = newSpectator(GameId("gid"), "screenName1", false, PlayerAddress("address1"), 1000L) + val spectator2 = newSpectator(GameId("gid"), "screenName2", false, PlayerAddress("address2"), 1000L) spectator1.playerId should not equal spectator2.playerId } "produces a different player key each time it is called" in { - val spectator1 = newSpectator(GameId("gid"), "screenName1", false, PlayerAddress("address1"), TestClock) - val spectator2 = newSpectator(GameId("gid"), "screenName2", false, PlayerAddress("address2"), TestClock) + val spectator1 = newSpectator(GameId("gid"), "screenName1", false, PlayerAddress("address1"), 1000L) + val spectator2 = newSpectator(GameId("gid"), "screenName2", false, PlayerAddress("address2"), 1000L) spectator1.playerKey should not equal spectator2.playerKey } } @@ -160,14 +173,14 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "if the address has changed" - { "updates the player address as instructed" in { val newAddress = PlayerAddress("address-2") - val player = newPlayer(GameId("gid"), "screenName", false, PlayerAddress("address"), TestClock) + val player = newPlayer(GameId("gid"), "screenName", false, PlayerAddress("address"), 1000L) updatePlayerAddress(player, newAddress).value.playerAddress shouldEqual newAddress } "doesn't change anything else" in { val oldAddress = PlayerAddress("address") val newAddress = PlayerAddress("address-2") - val player = newPlayer(GameId("gid"), "screenName", false, oldAddress, TestClock) + val player = newPlayer(GameId("gid"), "screenName", false, oldAddress, 1000L) val updatedWithOldAddress = updatePlayerAddress(player, newAddress).value .copy(playerAddress = oldAddress) updatedWithOldAddress shouldEqual player @@ -176,22 +189,22 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "if the address has not changed, returns None" in { val playerAddress = PlayerAddress("address") - val player = newPlayer(GameId("gid"), "screenName", false, playerAddress, TestClock) + val player = newPlayer(GameId("gid"), "screenName", false, playerAddress, 1000L) val result = updatePlayerAddress(player, playerAddress) result shouldEqual None } } "addPlayerIds" - { - val game = newGame("game name", false, TestClock, 123L) + val game = newGame("game name", false, 999L, 123L) val gameDb = Representations.gameToDb(game) - val player1 = newPlayer(game.gameId, "player name", false, PlayerAddress("address 1"), TestClock) + val player1 = newPlayer(game.gameId, "player name", false, PlayerAddress("address 1"), 1000L) val player1Db = Representations.playerToDb(player1) - val player2 = newPlayer(game.gameId, "player 2 name", false, PlayerAddress("address 2"), TestClock) + val player2 = newPlayer(game.gameId, "player 2 name", false, PlayerAddress("address 2"), 1001L) val player2Db = Representations.playerToDb(player2) - val spectator1 = newSpectator(game.gameId, "spectator-1", false, PlayerAddress("spectator-address-1"), TestClock) + val spectator1 = newSpectator(game.gameId, "spectator-1", false, PlayerAddress("spectator-address-1"), 1002L) val spectator1Db = Representations.spectatorToDb(spectator1) - val spectator2 = newSpectator(game.gameId, "spectator-2", false, PlayerAddress("spectator-address-2"), TestClock) + val spectator2 = newSpectator(game.gameId, "spectator-2", false, PlayerAddress("spectator-address-2"), 1003L) val spectator2Db = Representations.spectatorToDb(spectator2) "updates the game's players" - { @@ -244,15 +257,15 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC } "addPlayer" - { - val game = newGame("game name", false, TestClock, 123L) - val player = newPlayer(game.gameId, "player name", false, PlayerAddress("address"), TestClock) + val game = newGame("game name", false, 1000L, 123L) + val player = newPlayer(game.gameId, "player name", false, PlayerAddress("address"), 1001L) "includes passed player in the game" in { addPlayer(game, player).players should contain(player) } "includes passed player as well as any existing players" in { - val player2 = newPlayer(game.gameId, "player 2", false, PlayerAddress("address 2"), TestClock) + val player2 = newPlayer(game.gameId, "player 2", false, PlayerAddress("address 2"), 1002L) val addedPlayers = addPlayer(addPlayer(game, player), player2).players addedPlayers should contain.allOf(player, player2) } @@ -288,13 +301,13 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC } "start" - { - val game = newGame("game name", false, TestClock, 123L) + val game = newGame("game name", false, 999L, 123L) "sets up some key fields" in { - forAll { startTime: Long => - start(game, startTime, None, None, None, game.players.map(_.playerId)) should have( + forAll { (now: Long) => + start(game, now, None, None, None, game.players.map(_.playerId)) should have( "started" as true, - "startTime" as startTime, + "startTime" as now, "button" as 0, ) } @@ -302,7 +315,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "trackStacks" - { "is true if initial stack levels are provided" in { - forAll { startingStacks: Int => + forAll { (startingStacks: Int) => start(game, 0L, None, None, Some(startingStacks), game.players.map(_.playerId)).trackStacks shouldEqual true } } @@ -320,7 +333,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC RoundLevel(10, 2), )) "start time uses the 'now'" in { - forAll { startTime: Long => + forAll { (startTime: Long) => val gameTimer = start(game, startTime, None, timerLevels, None, game.players.map(_.playerId)).timer.value gameTimer.timerStartTime shouldEqual startTime } @@ -344,8 +357,8 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "sets up a round" - { "deals the same cards for the same seed" in { - forAll { initialSeed: Long => - val seededGame = newGame("game name", false, TestClock, initialSeed) + forAll { (initialSeed: Long) => + val seededGame = newGame("game name", false, 0L, initialSeed) val round1 = start(seededGame, 1000L, None, None, None, game.players.map(_.playerId)).round val round2 = start(seededGame, 1000L, None, None, None, game.players.map(_.playerId)).round round1 should have( @@ -368,10 +381,10 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "sets up the players properly" - { val players = List( - newPlayer(game.gameId, "player-1", false, PlayerAddress("address-1"), TestClock), - newPlayer(game.gameId, "player-2", false, PlayerAddress("address-2"), TestClock), - newPlayer(game.gameId, "player-3", false, PlayerAddress("address-3"), TestClock), - newPlayer(game.gameId, "player-4", false, PlayerAddress("address-4"), TestClock), + newPlayer(game.gameId, "player-1", false, PlayerAddress("address-1"), 0L), + newPlayer(game.gameId, "player-2", false, PlayerAddress("address-2"), 0L), + newPlayer(game.gameId, "player-3", false, PlayerAddress("address-3"), 0L), + newPlayer(game.gameId, "player-4", false, PlayerAddress("address-4"), 0L), ) "the players are ordered using the provided player order" in { @@ -406,7 +419,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "player holes are the same for the same seed" in { - forAll { seed: Long => + forAll { (seed: Long) => val players1 = start(game.copy(seed = seed, players = players), 1000L, None, None, None, game.players.map(_.playerId)).players val players2 = start(game.copy(seed = seed, players = players), 1000L, None, None, None, game.players.map(_.playerId)).players players1.map(_.hole) shouldEqual players2.map(_.hole) @@ -512,53 +525,53 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "for a timer update" - { "returns update timer action" in { - updateBlindAction(rawUpdateGame.copy( + updateBlindAction[Try](rawUpdateGame.copy( timerLevels = Some(List(RoundLevel(100, 10), BreakLevel(50))) - )).value shouldEqual EditTimerSummary() + )).success.value shouldEqual EditTimerSummary() } "action is edit timer even if the status also changes" in { - updateBlindAction(rawUpdateGame.copy( + updateBlindAction[Try](rawUpdateGame.copy( timerLevels = Some(List(RoundLevel(100, 10), BreakLevel(50))), playing = Some(true), - )).value shouldEqual EditTimerSummary() + )).success.value shouldEqual EditTimerSummary() } "a progress update also counts as an edit timer action" in { - updateBlindAction(rawUpdateGame.copy( + updateBlindAction[Try](rawUpdateGame.copy( progress = Some(10) - )).value shouldEqual EditTimerSummary() + )).success.value shouldEqual EditTimerSummary() } } "for a playing status update" - { "returns 'playing' timer status action if the timer was started" in { - updateBlindAction(rawUpdateGame.copy( + updateBlindAction[Try](rawUpdateGame.copy( playing = Some(true), - )).value shouldEqual TimerStatusSummary(true) + )).success.value shouldEqual TimerStatusSummary(true) } "returns 'paused' timer status action if the timer was stopped" in { - updateBlindAction(rawUpdateGame.copy( + updateBlindAction[Try](rawUpdateGame.copy( playing = Some(false), - )).value shouldEqual TimerStatusSummary(false) + )).success.value shouldEqual TimerStatusSummary(false) } } "returns update blind status for a manual blind edit" in { - updateBlindAction(rawUpdateGame.copy( + updateBlindAction[Try](rawUpdateGame.copy( smallBlind = Some(10), - )).value shouldEqual EditBlindSummary() + )).success.value shouldEqual EditBlindSummary() } "returns an error if the request doesn't include any actions" in { - updateBlindAction(rawUpdateGame).isLeft shouldEqual true + updateBlindAction[Try](rawUpdateGame).isFailure shouldEqual true } } "expiryTime" - { "expiry time is after the provided date" in { - forAll { now: Long => + forAll { (now: Long) => // let's not think ahead of the year 3000 to avoid Long overflow whenever(now < 32503680000000L) { expiryTime(now) should be > now @@ -569,7 +582,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "resetPlayerForNextPhase" - { val gameId = GameId("game-id") - val player = newPlayer(gameId, "player", false, PlayerAddress("address"), TestClock) + val player = newPlayer(gameId, "player", false, PlayerAddress("address"), 0L) "unchecks a checked player" in { resetPlayerForNextPhase(player.copy(checked = true)).checked shouldEqual false @@ -580,7 +593,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC } "sets the player's bet value to 0" in { - forAll { n: Int => + forAll { (n: Int) => resetPlayerForNextPhase(player.copy(bet = n)).bet shouldEqual 0 } } @@ -596,7 +609,7 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "resetPlayerForShowdown" - { val gameId = GameId("game-id") - val player = newPlayer(gameId, "player", false, PlayerAddress("address"), TestClock) + val player = newPlayer(gameId, "player", false, PlayerAddress("address"), 0L) "updates the player's stack based on their winnings" in { forAll { (winnings: Int, previousStack: Int) => @@ -613,13 +626,13 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC } "if the player does not have a winnings entry, their stack is left as it was" in { - forAll { previousStack: Int => + forAll { (previousStack: Int) => resetPlayerForShowdown(Nil)(player.copy(stack = previousStack)).stack shouldEqual previousStack } } "forces player's checked state to true" in { - forAll { checked: Boolean => + forAll { (checked: Boolean) => resetPlayerForShowdown(Nil)(player.copy(checked = checked)).checked shouldEqual true } } @@ -631,10 +644,10 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "resetPlayerForNextRound" - { val gameId = GameId("game-id") - val player = newPlayer(gameId, "player", false, PlayerAddress("address"), TestClock) + val player = newPlayer(gameId, "player", false, PlayerAddress("address"), 0L) "zeroes the player's pot contribution" in { - forAll { pot: Int => + forAll { (pot: Int) => resetPlayerForNextRound(player.copy(pot = pot)).pot shouldEqual 0 } } @@ -661,70 +674,72 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "requireGame" - { "returns the gameDb if present" in { val gameDb = Representations.gameToDb( - newGame("game name", false, TestClock, 123L) + newGame("game name", false, 0L, 123L) ) - requireGame(Some(gameDb), gameDb.gameId).value shouldEqual gameDb + requireGame[Try](Some(gameDb), gameDb.gameId).success.value shouldEqual gameDb } "fails with a note about the GID, if the game wasn't found" in { - val failures = requireGame(None, "GID").leftValue - failures.logString should include("GID") + requireGame[Try](None, "GID") match { + case util.Failure(failures: Failures) => failures.logString should include("GID") + case unexpected => fail(s"Expected app Failures, got $unexpected") + } } } "ensureNotStarted" - { "is successful if the game has not yet started" in { - val game = newGame("game name", false, TestClock, 123L) - ensureNotStarted( + val game = newGame("game name", false, 0L, 123L) + ensureNotStarted[Try]( game.copy(started = false) - ).isRight shouldEqual true + ).isSuccess shouldEqual true } "fails if the game has already started" in { - val game = newGame("game name", false, TestClock, 123L) - ensureNotStarted( + val game = newGame("game name", false, 0L, 123L) + ensureNotStarted[Try]( game.copy(started = true) - ).isLeft shouldEqual true + ).isFailure shouldEqual true } } "ensureStarted" - { - val game = newGame("Game name", false, TestClock, 123L) + val game = newGame("Game name", false, 0L, 123L) "is successful if the game has begun" in { val started = game.copy(started = true) - ensureStarted(started).value shouldEqual () + ensureStarted[Try](started).success.value shouldEqual () } "fails if the game has not begun" in { - ensureStarted(game).isLeft shouldEqual true + ensureStarted[Try](game).isFailure shouldEqual true } } "ensureNoDuplicateScreenName" - { - val game = newGame("game name", false, TestClock, 123L) + val game = newGame("game name", false, 0L, 123L) "is fine for a game with no players" in { - forAll { screenName: String => - ensureNoDuplicateScreenName(game, screenName).isRight shouldEqual true + forAll { (screenName: String) => + ensureNoDuplicateScreenName[Try](game, screenName).isSuccess shouldEqual true } } "is successful for a screen name that isn't already taken" in { - forAll { screenName: String => + forAll { (screenName: String) => whenever(screenName != "screenname") { - val player = newPlayer(game.gameId, "screenname", false, PlayerAddress("address"), TestClock) + val player = newPlayer(game.gameId, "screenname", false, PlayerAddress("address"), 0L) val gameWithPlayer = addPlayer(game, player) - ensureNoDuplicateScreenName(gameWithPlayer, screenName).isRight shouldEqual true + ensureNoDuplicateScreenName[Try](gameWithPlayer, screenName).isSuccess shouldEqual true } } } "fails for a screen name that is already in use in this game" in { - forAll { screenName: String => - val player = newPlayer(game.gameId, screenName, false, PlayerAddress("address"), TestClock) + forAll { (screenName: String) => + val player = newPlayer(game.gameId, screenName, false, PlayerAddress("address"), 0L) val gameWithPlayer = addPlayer(game, player) - ensureNoDuplicateScreenName(gameWithPlayer, screenName).isLeft shouldEqual true + ensureNoDuplicateScreenName[Try](gameWithPlayer, screenName).isFailure shouldEqual true } } } @@ -732,17 +747,17 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "ensurePlayerCount" - { "succeeds if there are a sensible number of players" in { forAll(Gen.choose(0, 19)) { n => - ensurePlayerCount(n).value shouldEqual () + ensurePlayerCount[Try](n).success.value shouldEqual () } } "fails when there are already 20 players" in { - ensurePlayerCount(20).isLeft shouldEqual true + ensurePlayerCount[Try](20).isFailure shouldEqual true } "fails if there are somehow already more than 20 players" in { forAll(Gen.choose(21, 50)) { n => - ensurePlayerCount(n).isLeft shouldEqual true + ensurePlayerCount[Try](n).isFailure shouldEqual true } } } @@ -750,142 +765,142 @@ class GamesTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "ensureStartingPlayerCount" - { "succeeds if the player count is more than 1" in { forAll(Gen.choose(2, 20)) { n => - ensureStartingPlayerCount(n).value shouldEqual () + ensureStartingPlayerCount[Try](n).success.value shouldEqual () } } "fails if ther is only one player in the game" in { - ensureStartingPlayerCount(1).isLeft shouldEqual true + ensureStartingPlayerCount[Try](1).isFailure shouldEqual true } } "ensureNotAlreadyPlaying" - { - val game = newGame("game name", false, TestClock, 123L) + val game = newGame("game name", false, 0L, 123L) "succeeds for a player address that isn't already in use" in { - forAll { address: String => + forAll { (address: String) => whenever(address != "address1") { - val player = newPlayer(game.gameId, "screenname", false, PlayerAddress("address1"), TestClock) + val player = newPlayer(game.gameId, "screenname", false, PlayerAddress("address1"), 0L) val gameWithPlayer = addPlayer(game, player) - ensureNotAlreadyPlaying(gameWithPlayer.players, PlayerAddress(address)).isRight shouldEqual true + ensureNotAlreadyPlaying[Try](gameWithPlayer.players, PlayerAddress(address)).isSuccess shouldEqual true } } } "fails for a player address that is already being used" in { - forAll { address: String => - val player = newPlayer(game.gameId, "screenname", false, PlayerAddress(address), TestClock) + forAll { (address: String) => + val player = newPlayer(game.gameId, "screenname", false, PlayerAddress(address), 0L) val gameWithPlayer = addPlayer(game, player) - ensureNotAlreadyPlaying(gameWithPlayer.players, PlayerAddress(address)).isLeft shouldEqual true + ensureNotAlreadyPlaying[Try](gameWithPlayer.players, PlayerAddress(address)).isFailure shouldEqual true } } } "ensurePlayerKey" - { - val game = newGame("game name", false, TestClock, 123L) - val player = newPlayer(game.gameId, "player name", false, PlayerAddress("address"), TestClock) + val game = newGame("game name", false, 0L, 123L) + val player = newPlayer(game.gameId, "player name", false, PlayerAddress("address"), 0L) "if the player is part of this game" - { val gameWithPlayer = addPlayer(game, player) "returns the valid player if their key is valid" in { - val playerResult = ensurePlayerKey(gameWithPlayer.players, player.playerId, player.playerKey).value + val playerResult = ensurePlayerKey[Try](gameWithPlayer.players, player.playerId, player.playerKey).success.value playerResult shouldEqual player } "fails if the player key does not match" in { val incorrectPlayerKey = PlayerKey("bad player key") - val result = ensurePlayerKey(gameWithPlayer.players, player.playerId, incorrectPlayerKey) - result.isLeft shouldEqual true + val result = ensurePlayerKey[Try](gameWithPlayer.players, player.playerId, incorrectPlayerKey) + result.isFailure shouldEqual true } } "fails if the player does not exist in the game" in { - val result = ensurePlayerKey(game.players, player.playerId, player.playerKey) - result.isLeft shouldEqual true + val result = ensurePlayerKey[Try](game.players, player.playerId, player.playerKey) + result.isFailure shouldEqual true } } "ensureSpectatorKey" - { - val game = newGame("game name", false, TestClock, 123L) - val spectator = newSpectator(game.gameId, "player name", false, PlayerAddress("address"), TestClock) + val game = newGame("game name", false, 0L, 123L) + val spectator = newSpectator(game.gameId, "player name", false, PlayerAddress("address"), 0L) "if the spectator is part of this game" - { val gameWithPlayer = addSpectator(game, spectator) "returns the valid spectator if their key is valid" in { - val spectatorResult = ensureSpectatorKey(gameWithPlayer.spectators, spectator.playerId, spectator.playerKey).value + val spectatorResult = ensureSpectatorKey[Try](gameWithPlayer.spectators, spectator.playerId, spectator.playerKey).success.value spectatorResult shouldEqual spectator } "fails if the spectator's player key does not match" in { val incorrectPlayerKey = PlayerKey("bad player key") - val result = ensureSpectatorKey(gameWithPlayer.spectators, spectator.playerId, incorrectPlayerKey) - result.isLeft shouldEqual true + val result = ensureSpectatorKey[Try](gameWithPlayer.spectators, spectator.playerId, incorrectPlayerKey) + result.isFailure shouldEqual true } } "fails if the spectator does not exist in the game" in { - val result = ensureSpectatorKey(game.spectators, spectator.playerId, spectator.playerKey) - result.isLeft shouldEqual true + val result = ensureSpectatorKey[Try](game.spectators, spectator.playerId, spectator.playerKey) + result.isFailure shouldEqual true } } "ensureHost" - { val gameId = GameId("game-id") - val host = newPlayer(gameId, "host", true, PlayerAddress("host-address"), TestClock) - val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-1-address"), TestClock) + val host = newPlayer(gameId, "host", true, PlayerAddress("host-address"), 0L) + val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-1-address"), 0L) val players = List(host, player1) "succeeds if the provided player is the host" in { - ensureHost(players, host.playerKey).value shouldEqual host + ensureHost[Try](players, host.playerKey).success.value shouldEqual host } "fails if the provided player is not the host" in { - ensureHost(players, player1.playerKey).isLeft shouldEqual true + ensureHost[Try](players, player1.playerKey).isFailure shouldEqual true } "fails if the provided player does not exist in the game" in { val nonPlayerKey = PlayerKey("not-in-the-game") - ensureHost(players, nonPlayerKey).isLeft shouldEqual true + ensureHost[Try](players, nonPlayerKey).isFailure shouldEqual true } } "ensureAdmin" - { val gameId = GameId("game-id") - val admin = newPlayer(gameId, "host", true, PlayerAddress("host-address"), TestClock) - val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-1-address"), TestClock) + val admin = newPlayer(gameId, "host", true, PlayerAddress("host-address"), 0L) + val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-1-address"), 0L) val players = List(admin, player1) "succeeds if the provided player is the host" in { - ensureAdmin(players, admin.playerKey).value shouldEqual admin + ensureAdmin[Try](players, admin.playerKey).success.value shouldEqual admin } "fails if the provided player is not the host" in { - ensureAdmin(players, player1.playerKey).isLeft shouldEqual true + ensureAdmin[Try](players, player1.playerKey).isFailure shouldEqual true } "fails if the provided player does not exist in the game" in { val nonPlayerKey = PlayerKey("not-in-the-game") - ensureAdmin(players, nonPlayerKey).isLeft shouldEqual true + ensureAdmin[Try](players, nonPlayerKey).isFailure shouldEqual true } } "ensureActive" - { "succeeds if the player is active" in { val pid = PlayerId("player-id") - ensureActive(Some(pid), pid).isRight shouldEqual true + ensureActive[Try](Some(pid), pid).isSuccess shouldEqual true } "fails if the player is not active" in { - ensureActive( + ensureActive[Try]( Some(PlayerId("active-player-id")), PlayerId("different-player-id") - ).isLeft shouldEqual true + ).isFailure shouldEqual true } "fails if no player is active" in { - ensureActive(None, PlayerId("player")).isLeft shouldEqual true + ensureActive[Try](None, PlayerId("player")).isFailure shouldEqual true } } } diff --git a/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayTest.scala index 084cc24..677d278 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayTest.scala @@ -1,18 +1,23 @@ package io.adamnfish.pokerdot.logic -import io.adamnfish.pokerdot.{TestClock, TestHelpers} +import io.adamnfish.pokerdot.TestHelpers import org.scalatest.freespec.AnyFreeSpec -import io.adamnfish.pokerdot.logic.Play._ +import io.adamnfish.pokerdot.logic.Play.* import io.adamnfish.pokerdot.logic.Cards.RichRank import io.adamnfish.pokerdot.logic.Games.newPlayer -import io.adamnfish.pokerdot.models.{Ace, BigBlind, BreakLevel, Clubs, Diamonds, Flop, GameId, Hole, NoBlind, Player, PlayerAddress, PlayerId, PreFlop, River, RoundLevel, Showdown, SmallBlind, Three, TimerStatus, Turn, Two} -import io.adamnfish.pokerdot.services.Clock +import io.adamnfish.pokerdot.models.{Ace, BigBlind, BreakLevel, Clubs, Diamonds, Failures, Flop, GameId, Hole, NoBlind, Player, PlayerAddress, PlayerId, PreFlop, River, RoundLevel, Showdown, SmallBlind, Three, TimerStatus, Turn, Two} +import io.adamnfish.pokerdot.services.Time import org.scalacheck.Gen import org.scalatest.{EitherValues, OptionValues} import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import cats.* +import cats.syntax.all.* +import cats.implicits.* + import scala.util.Random +import scala.util.{Try, Success} class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestHelpers with OptionValues { @@ -26,7 +31,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "generates the same cards from the same seeds" in { - forAll { seed: Long => + forAll { (seed: Long) => val round1 = generateRound(PreFlop, 0, seed) val round2 = generateRound(PreFlop, 0, seed) round1 shouldEqual round2 @@ -34,7 +39,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "there are no duplicate cards in a generated round" in { - forAll { seed: Long => + forAll { (seed: Long) => val round = generateRound(PreFlop, 0, seed) val cards = List(round.burn1, round.flop1, round.flop2, round.flop3, round.burn2, round.turn, round.burn3, round.river) cards shouldEqual cards.distinct @@ -42,7 +47,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "uses the provided small blind amount" in { - forAll { smallBlind: Int => + forAll { (smallBlind: Int) => val round = generateRound(PreFlop, smallBlind, 0L) round.smallBlind shouldEqual smallBlind } @@ -58,7 +63,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "deckOrder" - { "returns the same deck order for the same seed" in { - forAll { seed: Long => + forAll { (seed: Long) => val deck1 = deckOrder(seed) val deck2 = deckOrder(seed) deck1 shouldEqual deck2 @@ -82,16 +87,16 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "dealHoles" - { val gameId = GameId("game-id") val players = List( - newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), TestClock), - newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), TestClock), - newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), TestClock), - newPlayer(gameId, "player-4", false, PlayerAddress("player-address-4"), TestClock), - newPlayer(gameId, "player-5", false, PlayerAddress("player-address-5"), TestClock), - newPlayer(gameId, "player-6", false, PlayerAddress("player-address-6"), TestClock), + newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), 0L), + newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), 0L), + newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), 0L), + newPlayer(gameId, "player-4", false, PlayerAddress("player-address-4"), 0L), + newPlayer(gameId, "player-5", false, PlayerAddress("player-address-5"), 0L), + newPlayer(gameId, "player-6", false, PlayerAddress("player-address-6"), 0L), ) "deals the same cards to each player each time, with the same seed" in { - forAll { seed: Long => + forAll { (seed: Long) => val deck = deckOrder(seed) val players1 = dealHoles(players, deck) val players2 = dealHoles(players, deck) @@ -100,7 +105,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "the round's cards are not dealt to players" in { - forAll { seed: Long => + forAll { (seed: Long) => val round = generateRound(PreFlop, 0, seed) val allPlayerCards = dealHoles(players, deckOrder(seed)) .flatMap { player => @@ -116,7 +121,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "players are never dealt the same cards as each other" in { - forAll { seed: Long => + forAll { (seed: Long) => val allPlayerCards = dealHoles(players, deckOrder(seed)) .flatMap { player => player.hole.toList @@ -143,13 +148,13 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "lookupHoles" - { val player1 = - Games.newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("address-1"), TestClock) + Games.newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("address-1"), 0L) .copy(hole = Some(Hole(Ace of Clubs, Ace of Diamonds))) val player2 = - Games.newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("address-2"), TestClock) + Games.newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("address-2"), 0L) .copy(hole = Some(Hole(Two of Clubs, Two of Diamonds))) val player3 = - Games.newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("address-3"), TestClock) + Games.newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("address-3"), 0L) .copy(hole = Some(Hole(Three of Clubs, Three of Diamonds))) "returns player IDs with their cards" in { @@ -186,14 +191,14 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "playerIsActive" - { "true for an active player" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsActive(player.copy( stack = 1000, )) shouldEqual true } "false for a folded player" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsActive(player.copy( stack = 1000, folded = true, @@ -201,7 +206,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "false for a busted player" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsActive(player.copy( stack = 1000, busted = true, @@ -209,7 +214,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "all-in players can no longer act, and are not active" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsActive(player.copy( stack = 0, )) shouldEqual false @@ -218,7 +223,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "playerIsInvolved" - { "true for an active player" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsInvolved(player.copy( stack = 1000, bet = 10, @@ -227,7 +232,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "an all-in player is still involved" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsInvolved(player.copy( stack = 0, bet = 990, @@ -236,7 +241,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "folded players are not involved" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsInvolved(player.copy( stack = 1000, bet = 10, @@ -246,7 +251,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "busted players are not involved" in { - val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) playerIsInvolved(player.copy( stack = 0, bet = 990, @@ -258,14 +263,14 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "playerIsYetToAct" - { val player = - newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), TestClock) + newPlayer(GameId("game-id"), "player-name", false, PlayerAddress("player-address"), 0L) .copy( hole = Some(Hole(Ace of Clubs, Ace of Diamonds)), bet = 100, stack = 1000, ) val otherPlayer = - newPlayer(GameId("game-id"), "other-player-name", false, PlayerAddress("other-player-address"), TestClock) + newPlayer(GameId("game-id"), "other-player-name", false, PlayerAddress("other-player-address"), 0L) .copy( hole = Some(Hole(Ace of Clubs, Ace of Diamonds)), bet = 100, @@ -300,21 +305,21 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "if all other players are all-in" - { val player2 = - newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("player-2-address"), TestClock) + newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("player-2-address"), 0L) .copy( hole = Some(Hole(Ace of Clubs, Ace of Diamonds)), bet = 100, stack = 0, ) val player3 = - newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("player-3-address"), TestClock) + newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("player-3-address"), 0L) .copy( hole = Some(Hole(Ace of Clubs, Ace of Diamonds)), bet = 90, stack = 0, ) val player4 = - newPlayer(GameId("game-id"), "player-4", false, PlayerAddress("player-4-address"), TestClock) + newPlayer(GameId("game-id"), "player-4", false, PlayerAddress("player-4-address"), 0L) .copy( hole = Some(Hole(Ace of Clubs, Ace of Diamonds)), bet = 90, @@ -368,11 +373,11 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "returns the highest bet amount of all players" in { forAll { (b1: Int, b2: Int, b3: Int) => val players = List( - newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), TestClock) + newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), 0L) .copy(bet = b1), - newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), TestClock) + newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), 0L) .copy(bet = b2), - newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), TestClock) + newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), 0L) .copy(bet = b3), ) val result = currentBetAmount(players) @@ -382,11 +387,11 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "excludes folded players from this calculation" in { val players = List( - newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), TestClock) + newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), 0L) .copy(bet = 10), - newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), TestClock) + newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), 0L) .copy(bet = 20), - newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), TestClock) + newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), 0L) .copy( bet = 30, folded = true, @@ -397,9 +402,9 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "currentRaiseAmount" - { - val player1 = newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), TestClock) - val player2 = newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), TestClock) - val player3 = newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), TestClock) + val player1 = newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), 0L) + val player2 = newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), 0L) + val player3 = newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), 0L) "returns 0 if there are no bets" in { currentRaiseAmount(Nil) shouldEqual 0 @@ -439,13 +444,13 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "nextPlayer" - { - val p1 = newPlayer(GameId("game-id"), "p1", false, PlayerAddress("p1-address"), TestClock) + val p1 = newPlayer(GameId("game-id"), "p1", false, PlayerAddress("p1-address"), 0L) .copy(stack = 1000, playerId = PlayerId("p1-id")) - val p2 = newPlayer(GameId("game-id"), "p2", false, PlayerAddress("p2-address"), TestClock) + val p2 = newPlayer(GameId("game-id"), "p2", false, PlayerAddress("p2-address"), 0L) .copy(stack = 1000, playerId = PlayerId("p2-id")) - val p3 = newPlayer(GameId("game-id"), "p3", false, PlayerAddress("p3-address"), TestClock) + val p3 = newPlayer(GameId("game-id"), "p3", false, PlayerAddress("p3-address"), 0L) .copy(stack = 1000, playerId = PlayerId("p3-id")) - val p4 = newPlayer(GameId("game-id"), "p4", false, PlayerAddress("p4-address"), TestClock) + val p4 = newPlayer(GameId("game-id"), "p4", false, PlayerAddress("p4-address"), 0L) .copy(stack = 1000, playerId = PlayerId("p4-id")) "when a player is already active" - { @@ -717,12 +722,12 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "nextDealerAndBlinds" - { val gameId = GameId("game-id") - val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), TestClock) - val player2 = newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), TestClock) - val player3 = newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), TestClock) - val player4 = newPlayer(gameId, "player-4", false, PlayerAddress("player-address-4"), TestClock) - val player5 = newPlayer(gameId, "player-5", false, PlayerAddress("player-address-5"), TestClock) - val player6 = newPlayer(gameId, "player-6", false, PlayerAddress("player-address-6"), TestClock) + val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), 0L) + val player2 = newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), 0L) + val player3 = newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), 0L) + val player4 = newPlayer(gameId, "player-4", false, PlayerAddress("player-address-4"), 0L) + val player5 = newPlayer(gameId, "player-5", false, PlayerAddress("player-address-5"), 0L) + val player6 = newPlayer(gameId, "player-6", false, PlayerAddress("player-address-6"), 0L) val smallBlind = 5 "for a typical case" - { @@ -753,7 +758,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "heads-up" - { "dealer is always small blind" in { - forAll { b: Boolean => + forAll { (b: Boolean) => val (newButtonIndex, players) = if (b) nextDealerAndBlinds(List( player1.copy(blind = BigBlind), @@ -768,7 +773,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh } "non-dealer is always big blind" in { - forAll { b: Boolean => + forAll { (b: Boolean) => val (newButtonIndex, players) = if (b) nextDealerAndBlinds(List( player1.copy(blind = BigBlind), @@ -1143,11 +1148,11 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "if there is only one active player (game is over)" - { val gameId = GameId("game-id") val players = List( - newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), TestClock) + newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), 0L) .copy(busted = true), - newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), TestClock) + newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), 0L) .copy(busted = true, blind = SmallBlind), - newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), TestClock) + newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), 0L) .copy(blind = BigBlind), ).map(_.copy(busted = true)) val smallBlind = 5 @@ -1166,8 +1171,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh "blindForNextRound" - { "returns the current small blind if no timer status is present" in { - forAll { sb: Int => - blindForNextRound(sb, 0, None) shouldEqual Right(sb) + forAll { (sb: Int) => + blindForNextRound[Try](sb, 0, None) shouldEqual Success(sb) } } @@ -1177,8 +1182,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh val timerStatus = TimerStatus(0, None, List( RoundLevel(300, 10), )) - val newSmallBlind = blindForNextRound(currentSmallBlind, 100 * 1000, Some(timerStatus)).value - newSmallBlind shouldEqual currentSmallBlind + val newSmallBlind = blindForNextRound[Try](currentSmallBlind, 100 * 1000, Some(timerStatus)) + newSmallBlind shouldEqual Success(currentSmallBlind) } "if there has been a timer level advancement, the blinds increase as directed by the timer level" in { @@ -1187,8 +1192,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh RoundLevel(100, 10), RoundLevel(100, 20), )) - val newSmallBlind = blindForNextRound(currentSmallBlind, 120 * 1000, Some(timerStatus)).value - newSmallBlind shouldEqual 20 + val newSmallBlind = blindForNextRound[Try](currentSmallBlind, 120 * 1000, Some(timerStatus)) + newSmallBlind shouldEqual Success(20) } "if multiple timer levels have passed, we update to the most recent (the one that is correct right now)" in { @@ -1199,8 +1204,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh RoundLevel(100, 40), RoundLevel(100, 80), )) - val newSmallBlind = blindForNextRound(currentSmallBlind, 250 * 1000, Some(timerStatus)).value - newSmallBlind shouldEqual 40 + val newSmallBlind = blindForNextRound[Try](currentSmallBlind, 250 * 1000, Some(timerStatus)) + newSmallBlind shouldEqual Success(40) } "if we drop off the end of the timer" - { @@ -1211,8 +1216,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh RoundLevel(100, 20), RoundLevel(100, 50), )) - val newSmallBlind = blindForNextRound(currentSmallBlind, 500 * 1000, Some(timerStatus)).value - newSmallBlind shouldEqual 50 + val newSmallBlind = blindForNextRound[Try](currentSmallBlind, 500 * 1000, Some(timerStatus)) + newSmallBlind shouldEqual Success(50) } "ignore any trailing breaks to find the last valid timer level" in { @@ -1223,8 +1228,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh RoundLevel(100, 50), BreakLevel(100), )) - val newSmallBlind = blindForNextRound(currentSmallBlind, 500 * 1000, Some(timerStatus)).value - newSmallBlind shouldEqual 50 + val newSmallBlind = blindForNextRound[Try](currentSmallBlind, 500 * 1000, Some(timerStatus)) + newSmallBlind shouldEqual Success(50) } } @@ -1236,8 +1241,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - val result = blindForNextRound(currentSmallBlind, 250 * 1000, Some(timerStatus)) - result.isLeft shouldEqual true + val result = blindForNextRound[Try](currentSmallBlind, 250 * 1000, Some(timerStatus)) + result.isFailure shouldEqual true } } @@ -1250,8 +1255,8 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - val result = blindForNextRound(currentSmallBlind, 120 * 1000, Some(timerStatus)) - result.isLeft shouldEqual true + val result = blindForNextRound[Try](currentSmallBlind, 120 * 1000, Some(timerStatus)) + result.isFailure shouldEqual true } } @@ -1265,7 +1270,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - timerSmallBlind(timerStatus, 150 * 1000) shouldEqual Right((20, false)) + timerSmallBlind[Try](timerStatus, 150 * 1000) shouldEqual Success((20, false)) } "calculates the correct blind amount for a running timer that started after 0" in { @@ -1275,7 +1280,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - timerSmallBlind(timerStatus, (100000 + 150) * 1000) shouldEqual Right((20, false)) + timerSmallBlind[Try](timerStatus, (100000 + 150) * 1000) shouldEqual Success((20, false)) } "takes the last blind amount for an expired timer" in { @@ -1285,7 +1290,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - timerSmallBlind(timerStatus, 600 * 1000) shouldEqual Right((50, false)) + timerSmallBlind[Try](timerStatus, 600 * 1000) shouldEqual Success((50, false)) } "calculates the correct blind amount if the timer is paused" in { @@ -1295,7 +1300,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - timerSmallBlind(timerStatus, 800 * 1000) shouldEqual Right((10, false)) + timerSmallBlind[Try](timerStatus, 800 * 1000) shouldEqual Success((10, false)) } "takes the last valid blind amount if we're on a break" in { @@ -1305,7 +1310,7 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - timerSmallBlind(timerStatus, 250 * 1000) shouldEqual Right((20, true)) + timerSmallBlind[Try](timerStatus, 250 * 1000) shouldEqual Success((20, true)) } "takes the last valid blind amount if we're paused during a break" in { @@ -1315,15 +1320,15 @@ class PlayTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyCh BreakLevel(100), RoundLevel(100, 50), )) - timerSmallBlind(timerStatus, 1000 * 1000) shouldEqual Right((20, true)) + timerSmallBlind[Try](timerStatus, 1000 * 1000) shouldEqual Success((20, true)) } } "nextAliveAfterIndex" - { val gameId = GameId("game-id") - val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), TestClock) - val player2 = newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), TestClock) - val player3 = newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), TestClock) + val player1 = newPlayer(gameId, "player-1", false, PlayerAddress("player-address-1"), 0L) + val player2 = newPlayer(gameId, "player-2", false, PlayerAddress("player-address-2"), 0L) + val player3 = newPlayer(gameId, "player-3", false, PlayerAddress("player-address-3"), 0L) "returns the other alive player with 2 players" - { "gets second from first" in { diff --git a/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayerActionsTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayerActionsTest.scala index 4e5575a..7c98466 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayerActionsTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/logic/PlayerActionsTest.scala @@ -2,17 +2,25 @@ package io.adamnfish.pokerdot.logic import io.adamnfish.pokerdot.logic.Cards.RichRank import io.adamnfish.pokerdot.logic.Games.{newGame, newPlayer} -import io.adamnfish.pokerdot.logic.PlayerActions._ -import io.adamnfish.pokerdot.models._ -import io.adamnfish.pokerdot.{ConfigurableTestClock, TestClock, TestHelpers, TestRng} +import io.adamnfish.pokerdot.logic.PlayerActions.* +import io.adamnfish.pokerdot.models.* +import io.adamnfish.pokerdot.{ConfigurableTestTime, TestHelpers, TestRng, TestTime} import org.scalacheck.Gen -import org.scalatest.OptionValues +import org.scalatest.{OptionValues, TryValues} import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import cats.* +import cats.instances.* +import cats.implicits.* +import util.{Success, Try} -class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ScalaCheckDrivenPropertyChecks with OptionValues { + +class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ScalaCheckDrivenPropertyChecks with OptionValues with TryValues { + // random generator that starts at 1 and counts up every time it is used + val testRng = new TestRng[Try] + "bet" - { "reduces player's stack by the bet amount" ignore {} "increases player's bet by the bet amount" ignore {} @@ -53,12 +61,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with } "fold" - { - val rawGame = newGame("Game name", trackStacks = true, TestClock, 1L) - val p1 = newPlayer(rawGame.gameId, "p1", isHost = false, PlayerAddress("p1-address"), TestClock) + val rawGame = newGame("Game name", trackStacks = true, 0L, 1L) + val p1 = newPlayer(rawGame.gameId, "p1", isHost = false, PlayerAddress("p1-address"), 0L) .copy(stack = 1000) - val p2 = newPlayer(rawGame.gameId, "p2", isHost = false, PlayerAddress("p2-address"), TestClock) + val p2 = newPlayer(rawGame.gameId, "p2", isHost = false, PlayerAddress("p2-address"), 0L) .copy(stack = 1000) - val p3 = newPlayer(rawGame.gameId, "p3", isHost = false, PlayerAddress("p3-address"), TestClock) + val p3 = newPlayer(rawGame.gameId, "p3", isHost = false, PlayerAddress("p3-address"), 0L) .copy(stack = 1000) "updates player's folded status" in { @@ -136,20 +144,24 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with "advancePhase" - { "for the simple phases" - { - val game = newGame("Game name", trackStacks = true, TestClock, 1L) - val p1 :: p2 :: p3 :: p4 :: Nil = Play.dealHoles( + val game = newGame("Game name", trackStacks = true, 0L, 1L) + + val (p1, p2, p3, p4) = Play.dealHoles( List( - newPlayer(game.gameId, "p1", isHost = false, PlayerAddress("p1-address"), TestClock) + newPlayer(game.gameId, "p1", isHost = false, PlayerAddress("p1-address"), 0L) .copy(stack = 1000), - newPlayer(game.gameId, "p2", isHost = false, PlayerAddress("p2-address"), TestClock) + newPlayer(game.gameId, "p2", isHost = false, PlayerAddress("p2-address"), 0L) .copy(stack = 1000), - newPlayer(game.gameId, "p3", isHost = false, PlayerAddress("p3-address"), TestClock) + newPlayer(game.gameId, "p3", isHost = false, PlayerAddress("p3-address"), 0L) .copy(stack = 1000), - newPlayer(game.gameId, "p4", isHost = false, PlayerAddress("p4-address"), TestClock) + newPlayer(game.gameId, "p4", isHost = false, PlayerAddress("p4-address"), 0L) .copy(stack = 1000), ), Play.deckOrder(game.seed), - ) + ) match { + case p1 :: p2 :: p3 :: p4 :: Nil => (p1, p2, p3, p4) + case _ => fail("Unexpected player count") + } "game phase is advanced" in { val expected = Map( @@ -158,11 +170,11 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with Turn -> River, ) forAll(Gen.oneOf(PreFlop, Flop, Turn)) { phase => - val (newGame, _, _) = advancePhase( + val (newGame, _, _) = advancePhase[Try]( game.copy( round = game.round.copy(phase = phase), - ), TestClock, TestRng - ).value + ), 0L, testRng + ).success.value val nextPhase = newGame.round.phase nextPhase shouldEqual expected.get(phase).value } @@ -191,7 +203,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (newGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (newGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value newGame.players.map(_.pot) shouldEqual List( 25, 5, 25, 25 ) @@ -221,7 +233,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (newGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (newGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value newGame.players.map(_.bet) shouldEqual List( 0, 0, 0, 0 ) @@ -252,7 +264,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (newGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (newGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value newGame.players.map(_.checked) shouldEqual List( false, false, false, false ) @@ -283,7 +295,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (newGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (newGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value newGame.players.map(_.folded) shouldEqual List( false, true, false, false ) @@ -313,7 +325,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (newGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (newGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value newGame.players.map(_.busted) shouldEqual List( false, true, false, false ) @@ -343,7 +355,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - advancePhase(testGame, TestClock, TestRng).isLeft shouldEqual true + advancePhase[Try](testGame, 0L, testRng).isFailure shouldEqual true } } @@ -373,7 +385,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (newGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (newGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value newGame.inTurn shouldEqual Some(p2.playerId) } } @@ -402,7 +414,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (newGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (newGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value newGame.inTurn shouldEqual Some(p3.playerId) } } @@ -432,7 +444,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - val (updatedGame, _, _) = advancePhase(testGame, TestClock, TestRng).value + val (updatedGame, _, _) = advancePhase[Try](testGame, 0L, testRng).success.value updatedGame.round.phase shouldEqual Showdown } } @@ -516,10 +528,10 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with } "advanceFromRiver" - { - val rawGame = newGame("Game name", trackStacks = true, TestClock, 1L) - val p1 = newPlayer(rawGame.gameId, "p1", false, PlayerAddress("p1-address"), TestClock) - val p2 = newPlayer(rawGame.gameId, "p2", false, PlayerAddress("p2-address"), TestClock) - val p3 = newPlayer(rawGame.gameId, "p3", false, PlayerAddress("p3-address"), TestClock) + val rawGame = newGame("Game name", trackStacks = true, 0L, 1L) + val p1 = newPlayer(rawGame.gameId, "p1", false, PlayerAddress("p1-address"), 0L) + val p2 = newPlayer(rawGame.gameId, "p2", false, PlayerAddress("p2-address"), 0L) + val p3 = newPlayer(rawGame.gameId, "p3", false, PlayerAddress("p3-address"), 0L) val round = Play.generateRound(River, 5, rawGame.seed) "excludes folded players from the player hands" in { @@ -548,15 +560,18 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with } "advanceFromFoldedFinish" - { - val rawGame = newGame("Game name", trackStacks = true, TestClock, 1L) - val p1 :: p2 :: p3 :: Nil = Play.dealHoles( + val rawGame = newGame("Game name", trackStacks = true, 0L, 1L) + val (p1, p2, p3) = Play.dealHoles( List( - newPlayer(rawGame.gameId, "p1", false, PlayerAddress("p1-address"), TestClock), - newPlayer(rawGame.gameId, "p2", false, PlayerAddress("p2-address"), TestClock), - newPlayer(rawGame.gameId, "p3", false, PlayerAddress("p3-address"), TestClock), + newPlayer(rawGame.gameId, "p1", false, PlayerAddress("p1-address"), 0L), + newPlayer(rawGame.gameId, "p2", false, PlayerAddress("p2-address"), 0L), + newPlayer(rawGame.gameId, "p3", false, PlayerAddress("p3-address"), 0L), ), Play.deckOrder(rawGame.seed), - ) + ) match { + case p1 :: p2 :: p3 :: Nil => (p1, p2, p3) + case _ => fail("Unexpected player count") + } "is correct for an example, heads-up" in { forAll(Gen.oneOf(PreFlop, Flop, Turn)) { phase => @@ -595,15 +610,18 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with } "startNewRound" - { - val rawGame = newGame("Game name", trackStacks = true, TestClock, 1L) - val p1 :: p2 :: p3 :: Nil = Play.dealHoles( + val rawGame = newGame("Game name", trackStacks = true, 0L, 1L) + val (p1, p2, p3) = Play.dealHoles( List( - newPlayer(rawGame.gameId, "p1", false, PlayerAddress("p1-address"), TestClock), - newPlayer(rawGame.gameId, "p2", false, PlayerAddress("p2-address"), TestClock), - newPlayer(rawGame.gameId, "p3", false, PlayerAddress("p3-address"), TestClock), + newPlayer(rawGame.gameId, "p1", false, PlayerAddress("p1-address"), 0L), + newPlayer(rawGame.gameId, "p2", false, PlayerAddress("p2-address"), 0L), + newPlayer(rawGame.gameId, "p3", false, PlayerAddress("p3-address"), 0L), ), Play.deckOrder(rawGame.seed), - ) + ) match { + case p1 :: p2 :: p3 :: Nil => (p1, p2, p3) + case _ => fail("Unexpected player count") + } "advances to a new PreFlop round" - { val round = Play.generateRound(Showdown, 5, rawGame.seed) @@ -617,22 +635,22 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ) "round is now PreFlop" in { - val resultingGame = startNewRound(game, TestClock, TestRng).value + val resultingGame = startNewRound[Try](game, 0L, testRng).success.value resultingGame.round.phase shouldEqual PreFlop } "deals new holes to all players non-busted" in { - val resultingGame = startNewRound(game, TestClock, TestRng).value + val resultingGame = startNewRound[Try](game, 0L, testRng).success.value resultingGame.players.map(_.hole.isDefined) shouldEqual List(true, true, false) } "new cards are dealt" in { - val resultingGame = startNewRound(game, TestClock, TestRng).value + val resultingGame = startNewRound[Try](game, 0L, testRng).success.value resultingGame.seed should not equal game.seed } "all players have empty pots after new round starts" in { - val resultingGame = startNewRound(game, TestClock, TestRng).value + val resultingGame = startNewRound[Try](game, 0L, testRng).success.value resultingGame.players.map(_.pot) shouldEqual List(0, 0, 0) } @@ -654,7 +672,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with )) ), ) - val updatedGame = startNewRound(game, new ConfigurableTestClock(150 * 1000), TestRng).value + val updatedGame = startNewRound[Try](game, 150 * 1000, testRng).success.value updatedGame.round.smallBlind shouldEqual 20 } } @@ -669,7 +687,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with p3.copy(stack = 0, busted = true) ) ) - startNewRound(game, TestClock, TestRng).isLeft shouldEqual true + startNewRound[Try](game, 0L, testRng).isFailure shouldEqual true } "fails if there is a paused timer" in { @@ -690,16 +708,16 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with )) ), ) - val result = startNewRound(game, new ConfigurableTestClock(250), TestRng) - result.isLeft shouldEqual true + val result = startNewRound[Try](game, 250L, testRng) + result.isFailure shouldEqual true } } "updateBlind" - { - val rawGame = newGame("Game name", trackStacks = true, TestClock, 1L) - val p1 = newPlayer(rawGame.gameId, "player 1", isHost = false, PlayerAddress("p1-address"), TestClock) - val p3 = newPlayer(rawGame.gameId, "player 2", isHost = false, PlayerAddress("p2-address"), TestClock) - val p2 = newPlayer(rawGame.gameId, "player 3", isHost = false, PlayerAddress("p3-address"), TestClock) + val rawGame = newGame("Game name", trackStacks = true, 0L, 1L) + val p1 = newPlayer(rawGame.gameId, "player 1", isHost = false, PlayerAddress("p1-address"), 0L) + val p3 = newPlayer(rawGame.gameId, "player 2", isHost = false, PlayerAddress("p2-address"), 0L) + val p2 = newPlayer(rawGame.gameId, "player 3", isHost = false, PlayerAddress("p3-address"), 0L) val game = rawGame.copy( players = List(p1, p2, p3), started = true, @@ -712,44 +730,44 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with "for a timer levels update" - { "when creating a timer afresh" - { "updates the timer levels" in { - val updatedGame = updateBlind(game, + val updatedGame = updateBlind[Try](game, rawUpdateBlind.copy( timerLevels = Some(List(RoundLevel(100, 10), BreakLevel(50))) ), now = 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.levels shouldEqual List(RoundLevel(100, 10), BreakLevel(50)) } "updates the timer status" in { - val updatedGame = updateBlind(game, + val updatedGame = updateBlind[Try](game, rawUpdateBlind.copy( timerLevels = Some(List(RoundLevel(100, 10), BreakLevel(50))) ), now = 0L ) - updatedGame.value.timer.value should have( + updatedGame.success.value.timer.value should have( "timerStartTime" as 0L, "pausedTime" as None, ) } "uses the initial progress, if provided" in { - val updatedGame = updateBlind(game, + val updatedGame = updateBlind[Try](game, rawUpdateBlind.copy( timerLevels = Some(List(RoundLevel(100, 10), BreakLevel(50))), progress = Some(50), ), now = 200 * 1000L ) - updatedGame.value.timer.value.timerStartTime shouldEqual ((200 * 1000L) - (50 * 1000L)) + updatedGame.success.value.timer.value.timerStartTime shouldEqual ((200 * 1000L) - (50 * 1000L)) } } "when editing the levels of an existing timer" - { "updates the timer levels" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -768,12 +786,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1800 * 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.levels shouldEqual List(RoundLevel(1200, 10), BreakLevel(50), RoundLevel(1200, 20)) } "uses the progress to set the new timer's start time, when provided" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -793,14 +811,14 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1800 * 1000L ) - updatedGame.value.timer.value should have( + updatedGame.success.value.timer.value should have( "timerStartTime" as (500 * 1000L), "levels" as List(RoundLevel(1200, 10), BreakLevel(50), RoundLevel(1200, 20)), ) } "allows a progress of 0 when creating a new timer" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( round = game.round.copy( smallBlind = 5 @@ -823,15 +841,15 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1800 * 1000L ) - updatedGame.value.round.smallBlind shouldEqual 10 - updatedGame.value.timer.value.timerStartTime shouldEqual 1800 * 1000L + updatedGame.success.value.round.smallBlind shouldEqual 10 + updatedGame.success.value.timer.value.timerStartTime shouldEqual 1800 * 1000L } } } "for a timer progress update" - { "moves the game's start time to match the desired progress" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -850,13 +868,13 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 20000 * 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.timerStartTime shouldEqual ((20000 * 1000L) - (50 * 1000)) } "if the game is paused" - { "adjusts the game start time so that the timer's progress is correct" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -875,14 +893,14 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 20000 * 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.timerStartTime shouldEqual ((200 * 1000L) - (50 * 1000)) } "does not move the game's paused time" in { forAll(Gen.choose(0, 86400)) { progress => val pausedTime = Some(500 * 1000L) - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -901,7 +919,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 20000 * 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.pausedTime shouldEqual pausedTime } } @@ -913,7 +931,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with "for a playing status update" - { "for a pause request" - { "pauses the timer when playing is false" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -931,12 +949,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.pausedTime shouldEqual Some(1000L) } "adjusts the start time if the progress is also being updated" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -955,12 +973,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 950 * 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.timerStartTime shouldEqual 750 * 1000L } "fails to pause game timer if it was already paused" in { - updateBlind( + updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -974,13 +992,13 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with playing = Some(false), ), now = 1000L - ).isLeft shouldEqual true + ).isFailure shouldEqual true } } "for a timer restart" - { "calculates a correct start time from how long has elapsed" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -995,12 +1013,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.timerStartTime shouldEqual 900L } "restarts the timer" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -1015,12 +1033,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.pausedTime shouldEqual None } "if a new timer level would be in effect, does not change the round's small blind amount if the timer has not been otherwise edited)" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -1040,11 +1058,11 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 275L * 1000 ) - updatedGame.value.round.smallBlind shouldEqual 10 + updatedGame.success.value.round.smallBlind shouldEqual 10 } "uses the provided progress (if present) to adjust the timer start time" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -1064,12 +1082,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1500 * 1000L ) - val timerStatus = updatedGame.value.timer.value + val timerStatus = updatedGame.success.value.timer.value timerStatus.timerStartTime shouldEqual ((1500 - 180) * 1000L) } "fails to restart the game timer if the it was already running" in { - updateBlind( + updateBlind[Try]( game.copy( timer = Some( TimerStatus( @@ -1083,7 +1101,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with playing = Some(true), ), now = 1000L - ).isLeft shouldEqual true + ).isFailure shouldEqual true } } } @@ -1098,7 +1116,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with "for a manual blind update" - { "sets the blind to the specified amount" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( round = Round( River, 10, Two of Clubs, Three of Diamonds, Four of Spades, Five of Hearts, Six of Clubs, Seven of Diamonds, Eight of Hearts, Nine of Spades @@ -1109,11 +1127,11 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1000L ) - updatedGame.value.round.smallBlind shouldEqual 20 + updatedGame.success.value.round.smallBlind shouldEqual 20 } "removes any existing timer" in { - val updatedGame = updateBlind( + val updatedGame = updateBlind[Try]( game.copy( round = Round( River, 10, Two of Clubs, Three of Diamonds, Four of Spades, Five of Hearts, Six of Clubs, Seven of Diamonds, Eight of Hearts, Nine of Spades @@ -1124,45 +1142,45 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), now = 1000L ) - updatedGame.value.timer shouldEqual None + updatedGame.success.value.timer shouldEqual None } } } "ensurePlayersHaveFinishedActing" - { - val game = newGame("Game name", trackStacks = true, TestClock, 1L) - val p1 = newPlayer(game.gameId, "player 1", isHost = false, PlayerAddress("p1-address"), TestClock).copy( + val game = newGame("Game name", trackStacks = true, 0L, 1L) + val p1 = newPlayer(game.gameId, "player 1", isHost = false, PlayerAddress("p1-address"), 0L).copy( stack = 1000 ) - val p3 = newPlayer(game.gameId, "player 2", isHost = false, PlayerAddress("p2-address"), TestClock).copy( + val p3 = newPlayer(game.gameId, "player 2", isHost = false, PlayerAddress("p2-address"), 0L).copy( stack = 1000 ) - val p2 = newPlayer(game.gameId, "player 3", isHost = false, PlayerAddress("p3-address"), TestClock).copy( + val p2 = newPlayer(game.gameId, "player 3", isHost = false, PlayerAddress("p3-address"), 0L).copy( stack = 1000 ) "if there are no players yet to act" - { "succeeds when all players have folded" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List(p1, p2, p3).map(_.copy(folded = true)) ) - ).isRight shouldEqual true + ).isSuccess shouldEqual true } "succeeds when all players have checked at the same amount" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List(p1, p2, p3).map(_.copy( bet = 50, checked = true, )) ) - ).isRight shouldEqual true + ).isSuccess shouldEqual true } "succeeds if a player has not acted because they are all-in" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List( p1.copy( @@ -1180,12 +1198,12 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - ).isRight shouldEqual true + ).isSuccess shouldEqual true } } "succeeds if a player doesn't need to act because everyone else has folded" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List( p1.copy( @@ -1203,11 +1221,11 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - ).isRight shouldEqual true + ).isSuccess shouldEqual true } "succeeds if a player doesn't need to act because everyone else is all-in" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List( p1.copy( @@ -1225,11 +1243,11 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - ).isRight shouldEqual true + ).isSuccess shouldEqual true } "fails if a player has not yet bet" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List( p1.copy(bet = 25), @@ -1237,11 +1255,11 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with p3, ) ) - ).isLeft shouldEqual true + ).isFailure shouldEqual true } "fails if a player has not yet checked" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List( p1.copy( @@ -1258,11 +1276,11 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - ).isLeft shouldEqual true + ).isFailure shouldEqual true } "fails if a player checked at a lower bet amount" in { - ensurePlayersHaveFinishedActing( + ensurePlayersHaveFinishedActing[Try]( game.copy( players = List( p1.copy( @@ -1279,7 +1297,7 @@ class PlayerActionsTest extends AnyFreeSpec with Matchers with TestHelpers with ), ) ) - ).isLeft shouldEqual true + ).isFailure shouldEqual true } } } diff --git a/core/src/test/scala/io/adamnfish/pokerdot/logic/PokerHandsTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/logic/PokerHandsTest.scala index 7fe09a6..fb89f10 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/logic/PokerHandsTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/logic/PokerHandsTest.scala @@ -1,6 +1,6 @@ package io.adamnfish.pokerdot.logic -import io.adamnfish.pokerdot.{PokerGenerators, TestClock, TestHelpers} +import io.adamnfish.pokerdot.{PokerGenerators, TestTime, TestHelpers} import io.adamnfish.pokerdot.logic.Cards.RichRank import io.adamnfish.pokerdot.logic.Games.newPlayer import io.adamnfish.pokerdot.logic.PokerHands.{bestHand, bestHands, cardOrd, findDuplicateRanks, findDuplicateSuits, flush, fourOfAKind, fullHouse, handOrd, highCard, pair, playerWinnings, rankOrd, straight, straightFlush, suitOrd, threeOfAKind, twoPair, winnings} @@ -298,7 +298,7 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp Two of Clubs, f1, f2, f3, Two of Spades, t, Two of Diamonds, r ) val players = (1 to 10).toList.map { i => - newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), TestClock) + newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), 0L) .copy(hole = Some(Hole(h1, h2))) } val playerHands = bestHands(round, players) @@ -317,7 +317,7 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp Two of Clubs, f1, f2, f3, Two of Spades, t, Two of Diamonds, r ) val players = (1 to 10).toList.map { i => - newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), TestClock) + newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), 0L) .copy(hole = Some(Hole(h1, h2))) } val playerHands = bestHands(round, players) @@ -336,7 +336,7 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp Two of Clubs, f1, f2, f3, Two of Spades, t, Two of Diamonds, r ) val players = (1 to 10).toList.map { i => - newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), TestClock) + newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), 0L) .copy(hole = Some(Hole(h1, h2))) } val playerHands = bestHands(round, players) @@ -633,42 +633,51 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp "2 players" in { forAll { (rawP1Pot: Int, rawP2Pot: Int, seed: Long) => val (p1Pot, p2Pot) = (abs(rawP1Pot), abs(rawP2Pot)) - val deck = Play.deckOrder(seed) - val c1 :: c2 :: c3 :: c4 :: c5 :: c6 :: c7 :: c8 :: c9 :: c10 :: c11 :: c12 :: _ = deck - val player1 = testPlayerHand(p1Pot, c9, c10, "1") - val player2 = testPlayerHand(p2Pot, c11, c12, "2") + Play.deckOrder(seed) match { + case c1 :: c2 :: c3 :: c4 :: c5 :: c6 :: c7 :: c8 :: c9 :: c10 :: c11 :: c12 :: _ => + val player1 = testPlayerHand(p1Pot, c9, c10, "1") + val player2 = testPlayerHand(p2Pot, c11, c12, "2") - val results = winnings(List(player1, player2)) - results.map(_.potSize).sum shouldEqual (p1Pot + p2Pot) + val results = winnings(List(player1, player2)) + results.map(_.potSize).sum shouldEqual (p1Pot + p2Pot) + case _ => + fail("Couldn't draw enough cards from deck (should be impossible!)") + } } } "3 players" in { forAll { (rawP1Pot: Int, rawP2Pot: Int, rawP3Pot: Int, seed: Long) => val (p1Pot, p2Pot, p3Pot) = (abs(rawP1Pot), abs(rawP2Pot), abs(rawP3Pot)) - val deck = Play.deckOrder(seed) - val c1 :: c2 :: c3 :: c4 :: c5 :: c6 :: c7 :: c8 :: c9 :: c10 :: c11 :: c12 :: c13 :: c14 :: _ = deck - val player1 = testPlayerHand(p1Pot, c9, c10, "1") - val player2 = testPlayerHand(p2Pot, c11, c12, "2") - val player3 = testPlayerHand(p3Pot, c13, c14, "3") - - val results = winnings(List(player1, player2, player3)) - results.map(_.potSize).sum shouldEqual (p1Pot + p2Pot + p3Pot) + Play.deckOrder(seed) match { + case c1 :: c2 :: c3 :: c4 :: c5 :: c6 :: c7 :: c8 :: c9 :: c10 :: c11 :: c12 :: c13 :: c14 :: _ => + val player1 = testPlayerHand(p1Pot, c9, c10, "1") + val player2 = testPlayerHand(p2Pot, c11, c12, "2") + val player3 = testPlayerHand(p3Pot, c13, c14, "3") + + val results = winnings(List(player1, player2, player3)) + results.map(_.potSize).sum shouldEqual (p1Pot + p2Pot + p3Pot) + case _ => + fail("Couldn't draw enough cards from deck (should be impossible!)") + } } } "4 players" in { forAll { (rawP1Pot: Int, rawP2Pot: Int, rawP3Pot: Int, rawP4Pot: Int, seed: Long) => val (p1Pot, p2Pot, p3Pot, p4Pot) = (abs(rawP1Pot), abs(rawP2Pot), abs(rawP3Pot), abs(rawP4Pot)) - val deck = Play.deckOrder(seed) - val c1 :: c2 :: c3 :: c4 :: c5 :: c6 :: c7 :: c8 :: c9 :: c10 :: c11 :: c12 :: c13 :: c14 :: c15 :: c16 :: _ = deck - val player1 = testPlayerHand(p1Pot, c9, c10, "1") - val player2 = testPlayerHand(p2Pot, c11, c12, "2") - val player3 = testPlayerHand(p3Pot, c13, c14, "3") - val player4 = testPlayerHand(p4Pot, c15, c16, "4") - - val results = winnings(List(player1, player2, player3, player4)) - results.map(_.potSize).sum shouldEqual (p1Pot + p2Pot + p3Pot + p4Pot) + Play.deckOrder(seed) match { + case c1 :: c2 :: c3 :: c4 :: c5 :: c6 :: c7 :: c8 :: c9 :: c10 :: c11 :: c12 :: c13 :: c14 :: c15 :: c16 :: _ => + val player1 = testPlayerHand(p1Pot, c9, c10, "1") + val player2 = testPlayerHand(p2Pot, c11, c12, "2") + val player3 = testPlayerHand(p3Pot, c13, c14, "3") + val player4 = testPlayerHand(p4Pot, c15, c16, "4") + + val results = winnings(List(player1, player2, player3, player4)) + results.map(_.potSize).sum shouldEqual (p1Pot + p2Pot + p3Pot + p4Pot) + case _ => + fail("Couldn't draw enough cards from deck (should be impossible!)") + } } } } @@ -1449,7 +1458,7 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp } "Equal rank cards are sorted by suit" in { - forAll { rank: Rank => + forAll { (rank: Rank) => val cardsOfRank = List(rank of Clubs, rank of Diamonds, rank of Hearts, rank of Spades) val shuffled = Random.shuffle(cardsOfRank) shuffled.sortBy(cardOrd(true)) shouldEqual cardsOfRank @@ -1459,7 +1468,7 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp "rankOrd" - { "orders ranks with ace low" in { - forAll { seed: Long => + forAll { (seed: Long) => val shuffledRanks = new Random(seed).shuffle( Cards.deck.map(_.rank).distinct ) @@ -1470,7 +1479,7 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp } "orders ranks with ace high" in { - forAll { seed: Long => + forAll { (seed: Long) => val shuffledRanks = new Random(seed).shuffle( Cards.deck.map(_.rank).distinct ) @@ -1483,7 +1492,7 @@ class PokerHandsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenProp "suitOrd" - { "orders suits sensibly" in { - forAll { seed: Long => + forAll { (seed: Long) => val shuffledSuits = new Random(seed).shuffle( List(Diamonds, Clubs, Hearts, Spades) ) diff --git a/core/src/test/scala/io/adamnfish/pokerdot/logic/RepresentationsTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/logic/RepresentationsTest.scala index cc04532..b05035a 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/logic/RepresentationsTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/logic/RepresentationsTest.scala @@ -2,22 +2,24 @@ package io.adamnfish.pokerdot.logic import io.adamnfish.pokerdot.logic.Cards.RichRank import io.adamnfish.pokerdot.logic.Games.{newGame, newPlayer, newSpectator} -import io.adamnfish.pokerdot.logic.Representations._ +import io.adamnfish.pokerdot.logic.Representations.* import io.adamnfish.pokerdot.models.{Ace, Clubs, GameId, Hole, PlayerAddress, Queen, Spades} -import io.adamnfish.pokerdot.{TestClock, TestHelpers} +import io.adamnfish.pokerdot.TestHelpers import org.scalacheck.Gen -import org.scalatest.EitherValues +import org.scalatest.{EitherValues, TryValues} import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import scala.util.Try -class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyChecks with TestHelpers { + +class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyChecks with TryValues with TestHelpers { "games" - { "round trips a game correctly" in { - val game = newGame("game name", trackStacks = false, TestClock, 1) + val game = newGame("game name", trackStacks = false, 0L, 1) val gameDb = gameToDb(game) - val reconstructedGame = gameFromDb(gameDb, Nil).value + val reconstructedGame = gameFromDb[Try](gameDb, Nil).success.value reconstructedGame shouldEqual game } } @@ -25,7 +27,7 @@ class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrive "players" - { "round trips a player correctly" in { val gameId = GameId("game-id") - val player = newPlayer(gameId, "player", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(gameId, "player", false, PlayerAddress("player-address"), 0L) val playerDb = playerToDb(player) val reconstructedPlayer = playerFromDb(playerDb) reconstructedPlayer shouldEqual player @@ -35,7 +37,7 @@ class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrive "spectators" - { "round trips a spectator correctly" in { val gameId = GameId("game-id") - val spectator = newSpectator(gameId, "spectator", false, PlayerAddress("player-address"), TestClock) + val spectator = newSpectator(gameId, "spectator", false, PlayerAddress("player-address"), 0L) val spectatorDb = spectatorToDb(spectator) val reconstructedSpectator = spectatorFromDb(spectatorDb) reconstructedSpectator shouldEqual spectator @@ -46,23 +48,23 @@ class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrive "returns player db for each provided player" in { forAll(Gen.choose(1, 10)) { n => val players = (0 until n).map { i => - newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), TestClock) + newPlayer(GameId("game-id"), s"player-$i", false, PlayerAddress(s"pa-$i"), 0L) }.toList allPlayerDbs(players).length shouldEqual n } } "returns correct player db for provided player" in { - val player = newPlayer(GameId("game-id"), s"player", false, PlayerAddress(s"pa"), TestClock) + val player = newPlayer(GameId("game-id"), s"player", false, PlayerAddress(s"pa"), 0L) val expected = playerToDb(player) allPlayerDbs(List(player)) shouldEqual List(expected) } } "activePlayerDbs" - { - val p1 = newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), TestClock) - val p2 = newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), TestClock) - val p3 = newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), TestClock) + val p1 = newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), 0L) + val p2 = newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), 0L) + val p3 = newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), 0L) "includes active players" in { activePlayerDbs(List( @@ -90,17 +92,17 @@ class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrive } "filteredPlayerDbs" - { - val p1 = newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), TestClock) - val p2 = newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), TestClock) - val p3 = newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), TestClock) + val p1 = newPlayer(GameId("game-id"), "player-1", false, PlayerAddress("pa-1"), 0L) + val p2 = newPlayer(GameId("game-id"), "player-2", false, PlayerAddress("pa-2"), 0L) + val p3 = newPlayer(GameId("game-id"), "player-3", false, PlayerAddress("pa-3"), 0L) "includes players in the provided set" in { - val result = filteredPlayerDbs(List(p1, p2, p3), Set(p2.playerId, p3.playerId)).value.map(_.playerId) + val result = filteredPlayerDbs[Try](List(p1, p2, p3), Set(p2.playerId, p3.playerId)).success.value.map(_.playerId) result shouldEqual List(p2.playerId.pid, p3.playerId.pid) } "works if allow list contains entries that are not in the provided list" in { - val result = filteredPlayerDbs(List(p1, p2), Set(p2.playerId, p3.playerId)).value.map(_.playerId) + val result = filteredPlayerDbs[Try](List(p1, p2), Set(p2.playerId, p3.playerId)).success.value.map(_.playerId) result shouldEqual List(p2.playerId.pid) } } @@ -136,7 +138,7 @@ class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrive "summariseSelf" - { "hole" - { val hole = Hole(Queen of Clubs, Ace of Spades) - val player = newPlayer(GameId("game-id"), "screen name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "screen name", false, PlayerAddress("player-address"), 0L) "is included if present, even if the hole is not visible" in { summariseSelf( @@ -172,7 +174,7 @@ class RepresentationsTest extends AnyFreeSpec with Matchers with ScalaCheckDrive "summarisePlayer" - { "hole" - { val hole = Hole(Queen of Clubs, Ace of Spades) - val player = newPlayer(GameId("game-id"), "screen name", false, PlayerAddress("player-address"), TestClock) + val player = newPlayer(GameId("game-id"), "screen name", false, PlayerAddress("player-address"), 0L) .copy( hole = Some(hole), ) diff --git a/core/src/test/scala/io/adamnfish/pokerdot/logic/ResponsesTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/logic/ResponsesTest.scala index c0db4c5..f2ef0ea 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/logic/ResponsesTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/logic/ResponsesTest.scala @@ -1,6 +1,6 @@ package io.adamnfish.pokerdot.logic -import io.adamnfish.pokerdot.{TestClock, TestHelpers} +import io.adamnfish.pokerdot.{TestTime, TestHelpers} import io.adamnfish.pokerdot.logic.Games.{addPlayer, newGame, newPlayer} import io.adamnfish.pokerdot.models.{NoActionSummary, PlayerAddress} import org.scalatest.OptionValues @@ -10,14 +10,14 @@ import org.scalatest.matchers.should.Matchers class ResponsesTest extends AnyFreeSpec with Matchers with OptionValues with TestHelpers { "welcome" - { - val rawGame = newGame("game name", false, TestClock, 0) + val rawGame = newGame("game name", false, 0L, 0) val hostAddress = PlayerAddress("host-address") - val host = newPlayer(rawGame.gameId, "host", true, hostAddress, TestClock) + val host = newPlayer(rawGame.gameId, "host", true, hostAddress, 0L) val game = addPlayer(rawGame, host) "generates a welcome message for the new player" - { val playerAddress = PlayerAddress("player-address") - val player = newPlayer(game.gameId, "player", false, playerAddress, TestClock) + val player = newPlayer(game.gameId, "player", false, playerAddress, 0L) "the welcome message is on the response" in { val response = Responses.welcome(game, player, playerAddress) @@ -45,7 +45,7 @@ class ResponsesTest extends AnyFreeSpec with Matchers with OptionValues with Tes "does not generate a status message for the new player" in { val playerAddress = PlayerAddress("player-address") - val player = newPlayer(game.gameId, "player", false, playerAddress, TestClock) + val player = newPlayer(game.gameId, "player", false, playerAddress, 0L) val response = Responses.welcome(game, player, playerAddress) response.statuses.keys should not contain playerAddress @@ -53,7 +53,7 @@ class ResponsesTest extends AnyFreeSpec with Matchers with OptionValues with Tes "generates a status message for the host" in { val playerAddress = PlayerAddress("player-address") - val player = newPlayer(game.gameId, "player", false, playerAddress, TestClock) + val player = newPlayer(game.gameId, "player", false, playerAddress, 0L) val response = Responses.welcome(game, player, playerAddress) response.statuses.keys should contain(hostAddress) @@ -61,13 +61,13 @@ class ResponsesTest extends AnyFreeSpec with Matchers with OptionValues with Tes } "gameStatuses" - { - val rawGame = newGame("game name", false, TestClock, 0) + val rawGame = newGame("game name", false, 0L, 0) val hostAddress = PlayerAddress("host-address") - val host = newPlayer(rawGame.gameId, "host", true, hostAddress, TestClock) + val host = newPlayer(rawGame.gameId, "host", true, hostAddress, 0L) val player1Address = PlayerAddress("player-1-address") - val player1 = newPlayer(rawGame.gameId, "player1", false, player1Address, TestClock) + val player1 = newPlayer(rawGame.gameId, "player1", false, player1Address, 0L) val player2Address = PlayerAddress("player-2-address") - val player2 = newPlayer(rawGame.gameId, "player2", false, player2Address, TestClock) + val player2 = newPlayer(rawGame.gameId, "player2", false, player2Address, 0L) val game = addPlayer(addPlayer(addPlayer(rawGame, host), @@ -96,13 +96,13 @@ class ResponsesTest extends AnyFreeSpec with Matchers with OptionValues with Tes } "roundWinnings" - { - val rawGame = newGame("game name", false, TestClock, 0) + val rawGame = newGame("game name", false, 0L, 0) val hostAddress = PlayerAddress("host-address") - val host = newPlayer(rawGame.gameId, "host", true, hostAddress, TestClock) + val host = newPlayer(rawGame.gameId, "host", true, hostAddress, 0L) val player1Address = PlayerAddress("player-1-address") - val player1 = newPlayer(rawGame.gameId, "player1", false, player1Address, TestClock) + val player1 = newPlayer(rawGame.gameId, "player1", false, player1Address, 0L) val player2Address = PlayerAddress("player-2-address") - val player2 = newPlayer(rawGame.gameId, "player2", false, player2Address, TestClock) + val player2 = newPlayer(rawGame.gameId, "player2", false, player2Address, 0L) val game = addPlayer(addPlayer(addPlayer(rawGame, host), diff --git a/core/src/test/scala/io/adamnfish/pokerdot/logic/UtilsTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/logic/UtilsTest.scala index 05c7980..0084527 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/logic/UtilsTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/logic/UtilsTest.scala @@ -7,7 +7,6 @@ import org.scalatest.exceptions.TestFailedException import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks -import zio.{Exit, IO, Unsafe, ZIO} import scala.util.Random @@ -28,7 +27,7 @@ class UtilsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC "findIndex" - { "returned index is equal to the stdlib's index when present" in { - forAll { seed: Long => + forAll { (seed: Long) => val rng = new Random(seed) val shuffled = rng.shuffle(List(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)) shuffled.findIndex(_ == 1) shouldEqual Some(shuffled.indexWhere(_ == 1)) @@ -39,32 +38,4 @@ class UtilsTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyC List(1, 2, 3).findIndex(_ == 4) shouldEqual None } } - - "ioTraverse" - { - val testRuntime = zio.Runtime.default - - "returns successful accumulated result" in { - val io = List(1, 2, 3).ioTraverse(i => ZIO.succeed(i - 1)) - Unsafe.unsafe { implicit unsafe => - testRuntime.unsafe.run(io) match { - case Exit.Success(value) => - value shouldEqual List(0, 1, 2) - case Exit.Failure(cause) => - fail(s"expected successful attempt, got $cause") - } - } - } - - "returns all errors for failing `f`" in { - val io = List(1, 2, 3).ioTraverse(i => ZIO.fail(Failures(s"fail $i", "failure"))) - Unsafe.unsafe { implicit unsafe => - testRuntime.unsafe.run(io) match { - case Exit.Success(value) => - fail(s"expected failed attempt, got $value") - case Exit.Failure(cause) => - cause.failures.head.failures.map(_.logMessage) shouldEqual List("fail 1", "fail 2", "fail 3") - } - } - } - } } diff --git a/core/src/test/scala/io/adamnfish/pokerdot/models/SerialisationTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/models/SerialisationTest.scala index 34d8dc1..065a5e6 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/models/SerialisationTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/models/SerialisationTest.scala @@ -3,46 +3,56 @@ package io.adamnfish.pokerdot.models import io.adamnfish.pokerdot.TestHelpers import io.adamnfish.pokerdot.TestHelpers.parseReq import io.adamnfish.pokerdot.logic.Cards.RichRank -import io.adamnfish.pokerdot.models.Serialisation.{parseUpdateBlindRequest, _} -import io.circe.Json +import io.adamnfish.pokerdot.models.Serialisation.{parseUpdateBlindRequest, *} +import io.circe.{Decoder, Json} import io.circe.generic.semiauto.deriveDecoder import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers -import io.circe.syntax._ -import org.scalatest.EitherValues +import io.circe.syntax.* +import org.scalatest.{EitherValues, TryValues} +import scala.util.Try -class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { + +class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers with TryValues { "parse" - { "for invalid input" - { "fails" in { - parse("""nope""", "Test message", None).isLeft shouldEqual true + parse[Try]("""nope""", "Test message", None).isFailure shouldEqual true } "uses the provided message in the failure" in { - val failures = parse("""nope""", "Test message", None).leftValue - failures.failures.exists(_.userMessage == "Test message") shouldEqual true + parse[Try]("""nope""", "Test message", None) match { + case util.Failure(failures: Failures) => + failures.failures.exists(_.userMessage == "Test message") shouldEqual true + case unexpected => + fail(s"Expected app Failures, got result: $unexpected") + } } "uses the provided context in the failure" in { - val failures = parse("""nope""", "Test message", Some("context")).leftValue - failures.failures.exists(_.context.contains("context")) shouldEqual true + parse[Try]("""nope""", "Test message", Some("context")) match { + case util.Failure(failures: Failures) => + failures.failures.exists(_.context.contains("context")) shouldEqual true + case unexpected => + fail(s"Expected app Failures, got result: $unexpected") + } } } } "extractJson" - { case class Test(field: String) - implicit val testDecoder = deriveDecoder[Test] + implicit val testDecoder: Decoder[Test] = deriveDecoder "succeeds if the JSON is valid" in { - val result = extractJson(Json.fromFields(List(("field", Json.fromString("value")))), "Test message") - result.value shouldEqual Test(field = "value") + val result = extractJson[Try, Test](Json.fromFields(List(("field", Json.fromString("value")))), "Test message") + result.success.value shouldEqual Test(field = "value") } "fails if the JSON is not in the correct shape" in { - val result = extractJson(Json.fromFields(List(("differentField", Json.fromString("value")))), "Test message") - result.isLeft shouldEqual true + val result = extractJson[Try, Test](Json.fromFields(List(("differentField", Json.fromString("value")))), "Test message") + result.isFailure shouldEqual true } } @@ -56,7 +66,7 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { | "playing": false |}""".stripMargin ) - parseUpdateBlindRequest(json).value should have( + parseUpdateBlindRequest[Try](json).success.value should have( "gameId" as "gid", "playerId" as "pid", "playerKey" as "pkey", @@ -74,7 +84,7 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { | "playing": false |}""".stripMargin ) - parseUpdateBlindRequest(json).value should have( + parseUpdateBlindRequest[Try](json).success.value should have( "gameId" as "gid", "playerId" as "pid", "playerKey" as "pkey", @@ -93,7 +103,7 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { | "playing": true |}""".stripMargin ) - parseUpdateBlindRequest(json).value should have( + parseUpdateBlindRequest[Try](json).success.value should have( "gameId" as "gid", "playerId" as "pid", "playerKey" as "pkey", @@ -111,7 +121,7 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { | "playing": true |}""".stripMargin ) - parseUpdateBlindRequest(json).value should have( + parseUpdateBlindRequest[Try](json).success.value should have( "gameId" as "gid", "playerId" as "pid", "playerKey" as "pkey", @@ -134,7 +144,7 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { | "playing": true |}""".stripMargin ) - parseUpdateBlindRequest(json).value should have( + parseUpdateBlindRequest[Try](json).success.value should have( "gameId" as "gid", "playerId" as "pid", "playerKey" as "pkey", @@ -155,7 +165,7 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { | "smallBlind": 25 |}""".stripMargin ) - parseUpdateBlindRequest(json).value should have( + parseUpdateBlindRequest[Try](json).success.value should have( "gameId" as "gid", "playerId" as "pid", "playerKey" as "pkey", @@ -169,74 +179,74 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { "handEncoder" - { "highCard encoding includes the correct hand name" in { val hand: Hand = HighCard(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "high-card" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("high-card") } "pair encoding includes the correct hand name" in { val hand: Hand = Pair(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "pair" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("pair") } "twoPair encoding includes the correct hand name" in { val hand: Hand = TwoPair(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "two-pair" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("two-pair") } "threeOfAKind encoding includes the correct hand name" in { val hand: Hand = ThreeOfAKind(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "three-of-a-kind" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("three-of-a-kind") } "straight encoding includes the correct hand name" in { val hand: Hand = Straight(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "straight" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("straight") } "flush encoding includes the correct hand name" in { val hand: Hand = Flush(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "flush" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("flush") } "fullHouse encoding includes the correct hand name" in { val hand: Hand = FullHouse(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "full-house" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("full-house") } "fourOfAKind encoding includes the correct hand name" in { val hand: Hand = FourOfAKind(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "four-of-a-kind" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("four-of-a-kind") } "straightFlush encoding includes the correct hand name" in { val hand: Hand = StraightFlush(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - hand.asJson.hcursor.downField("hand").as[String].value shouldEqual "straight-flush" + hand.asJson.hcursor.downField("hand").as[String] shouldEqual Right("straight-flush") } } "roundSummaryEncoder" - { "for pre-flop includes the correct phase name" in { val round: RoundSummary = PreFlopSummary() - round.asJson.hcursor.downField("phase").as[String].value shouldEqual "pre-flop" + round.asJson.hcursor.downField("phase").as[String] shouldEqual Right("pre-flop") } "for flop includes the correct phase name" in { val round: RoundSummary = FlopSummary(Two of Hearts, Three of Clubs, Four of Spades) - round.asJson.hcursor.downField("phase").as[String].value shouldEqual "flop" + round.asJson.hcursor.downField("phase").as[String] shouldEqual Right("flop") } "for turn includes the correct phase name" in { val round: RoundSummary = TurnSummary(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds) - round.asJson.hcursor.downField("phase").as[String].value shouldEqual "turn" + round.asJson.hcursor.downField("phase").as[String] shouldEqual Right("turn") } "for river includes the correct phase name" in { val round: RoundSummary = RiverSummary(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs) - round.asJson.hcursor.downField("phase").as[String].value shouldEqual "river" + round.asJson.hcursor.downField("phase").as[String] shouldEqual Right("river") } "for showdown includes the correct phase name" in { val round: RoundSummary = ShowdownSummary(Two of Hearts, Three of Clubs, Four of Spades, Ten of Diamonds, Queen of Clubs, Nil) - round.asJson.hcursor.downField("phase").as[String].value shouldEqual "showdown" + round.asJson.hcursor.downField("phase").as[String] shouldEqual Right("showdown") } } @@ -245,47 +255,47 @@ class SerialisationTest extends AnyFreeSpec with Matchers with TestHelpers { "gameStartedSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = GameStartedSummary() - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "game-started" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("game-started") } "playerJoinedSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = PlayerJoinedSummary(playerId) - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "player-joined" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("player-joined") } "betSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = BetSummary(playerId, 10) - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "bet" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("bet") } "checkSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = CheckSummary(playerId) - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "check" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("check") } "foldSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = FoldSummary(playerId) - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "fold" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("fold") } "advancePhaseSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = AdvancePhaseSummary() - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "advance-phase" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("advance-phase") } "timerStatusSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = TimerStatusSummary(true) - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "timer-status" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("timer-status") } "editBlindSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = EditBlindSummary() - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "edit-blind" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("edit-blind") } "noActionSummary encoding includes correct the action name" in { val actionSummary: ActionSummary = NoActionSummary() - actionSummary.asJson.hcursor.downField("action").as[String].value shouldEqual "no-action" + actionSummary.asJson.hcursor.downField("action").as[String] shouldEqual Right("no-action") } } } diff --git a/core/src/test/scala/io/adamnfish/pokerdot/services/ClockTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/services/ClockTest.scala deleted file mode 100644 index 5958667..0000000 --- a/core/src/test/scala/io/adamnfish/pokerdot/services/ClockTest.scala +++ /dev/null @@ -1,16 +0,0 @@ -package io.adamnfish.pokerdot.services - -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers - - -class ClockTest extends AnyFreeSpec with Matchers { - "production clock implementation" - { - "now increases as time increases" in { - val oldNow = Clock.now() - Thread.sleep(10) - val newNow = Clock.now() - oldNow should be < newNow - } - } -} diff --git a/core/src/test/scala/io/adamnfish/pokerdot/services/RngTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/services/RngTest.scala index 10f069c..b64334a 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/services/RngTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/services/RngTest.scala @@ -1,22 +1,26 @@ package io.adamnfish.pokerdot.services -import org.scalatest.freespec.AnyFreeSpec -import org.scalatest.matchers.should.Matchers -import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks +import cats.effect.IO +import munit.{CatsEffectSuite, ScalaCheckEffectSuite} +import org.scalacheck.effect.PropF -class RngTest extends AnyFreeSpec with Matchers with ScalaCheckDrivenPropertyChecks { - "production RNG" - { - "returns a random initial seed" in { - val rng = new RandomRng - rng.randomState() should not equal rng.randomState() - } - - "returns a different 'next' seed every time" in { - forAll { seed: Long => - val rng = new RandomRng - rng.nextState(seed) should not equal rng.nextState(seed) - } +class RngTest extends CatsEffectSuite with ScalaCheckEffectSuite { + test("production RNG returns a random initial seed") { + val rng = new RandomRng[IO] + for + nrd1 <- rng.randomState + rnd2 <- rng.randomState + yield assertNotEquals(nrd1, rnd2) + } + + test("production RNG returns a different 'next' seed every time") { + PropF.forAllF { (seed: Long) => + val rng = new RandomRng[IO] + for + nrd1 <- rng.nextState(seed) + rnd2 <- rng.nextState(seed) + yield assertNotEquals(nrd1, rnd2) } } } diff --git a/core/src/test/scala/io/adamnfish/pokerdot/services/TimeTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/services/TimeTest.scala new file mode 100644 index 0000000..b3fb802 --- /dev/null +++ b/core/src/test/scala/io/adamnfish/pokerdot/services/TimeTest.scala @@ -0,0 +1,16 @@ +package io.adamnfish.pokerdot.services + +import cats.effect.IO +import munit.CatsEffectSuite +import scala.concurrent.duration.DurationInt + + +class TimeTest extends CatsEffectSuite { + test("now increases as time increases") { + for + oldNow <- RealTime[IO].now + _ <- IO.sleep(2.seconds) + newNow <- RealTime[IO].now + yield assert(oldNow < newNow, s"$oldNow should be < $newNow") + } +} diff --git a/core/src/test/scala/io/adamnfish/pokerdot/validation/ValidationTest.scala b/core/src/test/scala/io/adamnfish/pokerdot/validation/ValidationTest.scala index 437ffea..547e379 100644 --- a/core/src/test/scala/io/adamnfish/pokerdot/validation/ValidationTest.scala +++ b/core/src/test/scala/io/adamnfish/pokerdot/validation/ValidationTest.scala @@ -1,18 +1,19 @@ package io.adamnfish.pokerdot.validation -import io.adamnfish.pokerdot.TestHelpers -import io.adamnfish.pokerdot.models._ +import io.adamnfish.pokerdot.models.* import io.adamnfish.pokerdot.validation.Validation.{extractAdvancePhase, extractBet, extractCheck, extractCreateGame, extractFold, extractJoinGame, extractPing, extractStartGame, extractUpdateBlind, validate} import io.circe.parser.parse import org.scalacheck.Gen +import org.scalatest.{EitherValues, TryValues} import org.scalatest.freespec.AnyFreeSpec import org.scalatest.matchers.should.Matchers import org.scalatestplus.scalacheck.ScalaCheckDrivenPropertyChecks import java.util.UUID +import scala.util.Try -class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with ScalaCheckDrivenPropertyChecks { +class ValidationTest extends AnyFreeSpec with Matchers with EitherValues with TryValues with ScalaCheckDrivenPropertyChecks { val gameId = UUID.randomUUID().toString val player1Id = UUID.randomUUID().toString val player2Id = UUID.randomUUID().toString @@ -22,7 +23,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca "extractCreateGame" in { val jsonStr = """{"operation":"create-game","screenName":"screen name","gameName":"game name"}""" val json = parse(jsonStr).value - extractCreateGame(json).value shouldEqual CreateGame( + extractCreateGame[Try](json).success.value shouldEqual CreateGame( screenName = "screen name", gameName = "game name", ) @@ -31,7 +32,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca "extractJoinGame" in { val jsonStr = """{"operation":"join-game","gameCode":"abcd","screenName":"screen name"}""" val json = parse(jsonStr).value - extractJoinGame(json).value shouldEqual JoinGame( + extractJoinGame[Try](json).success.value shouldEqual JoinGame( gameCode = "abcd", screenName = "screen name", ) @@ -43,7 +44,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca s"""{"operation":"start-game","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey","playerOrder":["$player1Id","$player2Id","$player3Id"], |"timerConfig":[{"durationSeconds":300,"smallBlind":5},{"durationSeconds":60},{"durationSeconds":500,"smallBlind":10}]}""".stripMargin val json = parse(jsonStr).value - extractStartGame(json).value shouldEqual StartGame( + extractStartGame[Try](json).success.value shouldEqual StartGame( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), startingStack = None, initialSmallBlind = None, @@ -57,7 +58,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca s"""{"operation":"start-game","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey","playerOrder":["$player1Id","$player2Id","$player3Id"], |"startingStack":100,"initialSmallBlind":1}""".stripMargin val json = parse(jsonStr).value - extractStartGame(json).value shouldEqual StartGame( + extractStartGame[Try](json).success.value shouldEqual StartGame( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), startingStack = Some(100), initialSmallBlind = Some(1), @@ -72,7 +73,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca |"timerConfig":[{"durationSeconds":300,"smallBlind":5},{"durationSeconds":60},{"durationSeconds":500,"smallBlind":10}], |"startingStack":100}""".stripMargin val json = parse(jsonStr).value - extractStartGame(json).value shouldEqual StartGame( + extractStartGame[Try](json).success.value shouldEqual StartGame( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), startingStack = Some(100), initialSmallBlind = None, @@ -87,7 +88,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca s"""{"operation":"bet","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey", |"betAmount":100}""".stripMargin val json = parse(jsonStr).value - extractBet(json).value shouldEqual Bet( + extractBet[Try](json).success.value shouldEqual Bet( GameId(gameId), PlayerKey(playerKey), PlayerId(player1Id), 100, ) @@ -97,7 +98,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca val jsonStr = s"""{"operation":"check","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey"}""".stripMargin val json = parse(jsonStr).value - extractCheck(json).value shouldEqual Check( + extractCheck[Try](json).success.value shouldEqual Check( GameId(gameId), PlayerKey(playerKey), PlayerId(player1Id), ) } @@ -106,7 +107,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca val jsonStr = s"""{"operation":"fold","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey"}""".stripMargin val json = parse(jsonStr).value - extractFold(json).value shouldEqual Fold( + extractFold[Try](json).success.value shouldEqual Fold( GameId(gameId), PlayerKey(playerKey), PlayerId(player1Id), ) } @@ -115,7 +116,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca val jsonStr = s"""{"operation":"advance-phase","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey"}""".stripMargin val json = parse(jsonStr).value - extractAdvancePhase(json).value shouldEqual AdvancePhase( + extractAdvancePhase[Try](json).success.value shouldEqual AdvancePhase( GameId(gameId), PlayerKey(playerKey), PlayerId(player1Id), ) } @@ -127,7 +128,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca |"timerLevels":[{"durationSeconds":300,"smallBlind":5},{"durationSeconds":60},{"durationSeconds":500,"smallBlind":10}], |"playing":true}""".stripMargin val json = parse(jsonStr).value - extractUpdateBlind(json).value shouldEqual UpdateBlind( + extractUpdateBlind[Try](json).success.value shouldEqual UpdateBlind( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), timerLevels = Some(List(RoundLevel(300, 5), BreakLevel(60), RoundLevel(500, 10))), smallBlind = None, @@ -141,7 +142,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca s"""{"operation":"update-blind","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey", |"playing":true}""".stripMargin val json = parse(jsonStr).value - extractUpdateBlind(json).value shouldEqual UpdateBlind( + extractUpdateBlind[Try](json).success.value shouldEqual UpdateBlind( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), timerLevels = None, smallBlind = None, @@ -156,7 +157,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca s"""{"operation":"update-blind","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey", |"progress":350}""".stripMargin val json = parse(jsonStr).value - extractUpdateBlind(json).value shouldEqual UpdateBlind( + extractUpdateBlind[Try](json).success.value shouldEqual UpdateBlind( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), timerLevels = None, smallBlind = None, @@ -170,7 +171,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca s"""{"operation":"update-blind","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey", |"progress":-10}""".stripMargin val json = parse(jsonStr).value - extractUpdateBlind(json).isLeft shouldEqual true + extractUpdateBlind[Try](json).isFailure shouldEqual true } } @@ -179,7 +180,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca s"""{"operation":"update-blind","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey", |"smallBlind":50}""".stripMargin val json = parse(jsonStr).value - extractUpdateBlind(json).value shouldEqual UpdateBlind( + extractUpdateBlind[Try](json).success.value shouldEqual UpdateBlind( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), timerLevels = None, smallBlind = Some(50), @@ -193,7 +194,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca val jsonStr = s"""{"operation":"ping","gameId":"$gameId","playerId":"$player1Id","playerKey":"$playerKey"}""".stripMargin val json = parse(jsonStr).value - extractPing(json).value shouldEqual Ping( + extractPing[Try](json).success.value shouldEqual Ping( GameId(gameId), PlayerId(player1Id), PlayerKey(playerKey), ) } @@ -201,46 +202,46 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca "validate CreateGame" - { "returns the request for a valid create game request" in { val request = CreateGame("screen name", "game name") - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "returns a failure if the screen name is empty" in { - validate(CreateGame("", "game name")).isLeft shouldEqual true + validate[Try](CreateGame("", "game name")).isFailure shouldEqual true } "returns a failure if the screen name is very long" in { - validate(CreateGame("a" * 60, "game name")).isLeft shouldEqual true + validate[Try](CreateGame("a" * 60, "game name")).isFailure shouldEqual true } "returns a failure if the game name is empty" in { - validate(CreateGame("screen name", "")).isLeft shouldEqual true + validate[Try](CreateGame("screen name", "")).isFailure shouldEqual true } "returns a failure if the game name is very long" in { - validate(CreateGame("screen name", "a" * 60)).isLeft shouldEqual true + validate[Try](CreateGame("screen name", "a" * 60)).isFailure shouldEqual true } } "validate JoinGame" - { "returns the request for a valid join game request" in { val request = JoinGame("abcde", "screen name") - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "returns a failure if the game code is empty" in { - validate(JoinGame("", "game name")).isLeft shouldEqual true + validate[Try](JoinGame("", "game name")).isFailure shouldEqual true } "returns a failure if the game code doesn't look like a game code" in { - validate(JoinGame("n -ot A! gameCode", "game name")).isLeft shouldEqual true + validate[Try](JoinGame("n -ot A! gameCode", "game name")).isFailure shouldEqual true } "returns a failure if the screen name is empty" in { - validate(JoinGame("abcde", "")).isLeft shouldEqual true + validate[Try](JoinGame("abcde", "")).isFailure shouldEqual true } "returns a failure if the screen name is very long" in { - validate(JoinGame("abcde", "a" * 60)).isLeft shouldEqual true + validate[Try](JoinGame("abcde", "a" * 60)).isFailure shouldEqual true } } @@ -259,7 +260,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca initialSmallBlind = None, timerConfig = None, ) - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "with timer but no stack information" in { @@ -268,7 +269,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca initialSmallBlind = None, timerConfig = Some(timerExample), ) - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "with stack and timer information" in { @@ -277,7 +278,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca timerConfig = Some(timerExample), initialSmallBlind = None, ) - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "with stack and small blind information" in { @@ -286,24 +287,24 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca timerConfig = None, initialSmallBlind = Some(1), ) - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } } "returns a failure if the game id is not valid" in { - validate(rawRequest.copy(gameId = GameId("invalid!"))).isLeft shouldEqual true + validate[Try](rawRequest.copy(gameId = GameId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player id is not valid" in { - validate(rawRequest.copy(playerId = PlayerId("invalid!"))).isLeft shouldEqual true + validate[Try](rawRequest.copy(playerId = PlayerId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player key is not valid" in { - validate(rawRequest.copy(playerKey = PlayerKey("invalid!"))).isLeft shouldEqual true + validate[Try](rawRequest.copy(playerKey = PlayerKey("invalid!"))).isFailure shouldEqual true } "returns a failure if player order is empty" in { - validate(rawRequest.copy(playerOrder = Nil)).isLeft shouldEqual true + validate[Try](rawRequest.copy(playerOrder = Nil)).isFailure shouldEqual true } "if the game is tracking stacks" - { @@ -313,7 +314,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca timerConfig = None, initialSmallBlind = None, ) - validate(request).isLeft shouldEqual true + validate[Try](request).isFailure shouldEqual true } "if the game is tracking stacks, fails if both timer config and initial stack amount are provided" in { @@ -322,7 +323,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca timerConfig = Some(timerExample), initialSmallBlind = Some(1), ) - validate(request).isLeft shouldEqual true + validate[Try](request).isFailure shouldEqual true } "fails if stacks are 0" in { @@ -331,7 +332,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca timerConfig = None, initialSmallBlind = Some(10), ) - validate(request).isLeft shouldEqual true + validate[Try](request).isFailure shouldEqual true } "fails if initial blind is 0" in { @@ -340,7 +341,7 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca timerConfig = None, initialSmallBlind = Some(0), ) - validate(request).isLeft shouldEqual true + validate[Try](request).isFailure shouldEqual true } } @@ -358,43 +359,43 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca "returns the request for a valid update timer requests" - { "with timer levels" in { val timerLevelsRequest = rawRequest.copy(timerLevels = Some(List(RoundLevel(300, 1), BreakLevel(60), RoundLevel(300, 2)))) - validate(timerLevelsRequest).value shouldEqual timerLevelsRequest + validate[Try](timerLevelsRequest).success.value shouldEqual timerLevelsRequest } "without timer levels" in { val requestWithoutTimerLevels = rawRequest.copy(timerLevels = None, smallBlind = Some(10)) - validate(requestWithoutTimerLevels).value shouldEqual requestWithoutTimerLevels + validate[Try](requestWithoutTimerLevels).success.value shouldEqual requestWithoutTimerLevels } "with manual blind update" in { val requestWithSmallBlindAmount = rawRequest.copy(smallBlind = Some(50)) - validate(requestWithSmallBlindAmount).value shouldEqual requestWithSmallBlindAmount + validate[Try](requestWithSmallBlindAmount).success.value shouldEqual requestWithSmallBlindAmount } "with a new timer progress" in { val requestWithTimerProgress = rawRequest.copy(progress = Some(500)) - validate(requestWithTimerProgress).value shouldEqual requestWithTimerProgress + validate[Try](requestWithTimerProgress).success.value shouldEqual requestWithTimerProgress } } "returns a failure if the game id is not valid" in { - validate(rawRequest.copy(gameId = GameId("invalid!"))).isLeft shouldEqual true + validate[Try](rawRequest.copy(gameId = GameId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player id is not valid" in { - validate(rawRequest.copy(playerId = PlayerId("invalid!"))).isLeft shouldEqual true + validate[Try](rawRequest.copy(playerId = PlayerId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player key is not valid" in { - validate(rawRequest.copy(playerKey = PlayerKey("invalid!"))).isLeft shouldEqual true + validate[Try](rawRequest.copy(playerKey = PlayerKey("invalid!"))).isFailure shouldEqual true } "returns a failure if the timer levels are present and empty" in { - validate(rawRequest.copy(timerLevels = Some(Nil))).isLeft shouldEqual true + validate[Try](rawRequest.copy(timerLevels = Some(Nil))).isFailure shouldEqual true } "returns a failure if the update blind request is 'empty'" in { - validate(rawRequest).isLeft shouldEqual true + validate[Try](rawRequest).isFailure shouldEqual true } } @@ -405,28 +406,28 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca ) "returns the request for a valid bet request" in { - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "returns a failure if the game id is not valid" in { - validate(request.copy(gameId = GameId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(gameId = GameId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player id is not valid" in { - validate(request.copy(playerId = PlayerId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerId = PlayerId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player key is not valid" in { - validate(request.copy(playerKey = PlayerKey("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerKey = PlayerKey("invalid!"))).isFailure shouldEqual true } "returns a failure if bet amount is 0" in { - validate(request.copy(betAmount = 0)).isLeft shouldEqual true + validate[Try](request.copy(betAmount = 0)).isFailure shouldEqual true } "returns a failure if bet amount is -ve" in { forAll(Gen.negNum[Int]) { betAmount => - validate(request.copy(betAmount = betAmount)).isLeft shouldEqual true + validate[Try](request.copy(betAmount = betAmount)).isFailure shouldEqual true } } } @@ -437,19 +438,19 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca ) "returns the request for a valid bet request" in { - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "returns a failure if the game id is not valid" in { - validate(request.copy(gameId = GameId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(gameId = GameId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player id is not valid" in { - validate(request.copy(playerId = PlayerId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerId = PlayerId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player key is not valid" in { - validate(request.copy(playerKey = PlayerKey("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerKey = PlayerKey("invalid!"))).isFailure shouldEqual true } } @@ -459,19 +460,19 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca ) "returns the request for a valid bet request" in { - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "returns a failure if the game id is not valid" in { - validate(request.copy(gameId = GameId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(gameId = GameId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player id is not valid" in { - validate(request.copy(playerId = PlayerId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerId = PlayerId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player key is not valid" in { - validate(request.copy(playerKey = PlayerKey("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerKey = PlayerKey("invalid!"))).isFailure shouldEqual true } } @@ -481,19 +482,19 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca ) "returns the request for a valid bet request" in { - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "returns a failure if the game id is not valid" in { - validate(request.copy(gameId = GameId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(gameId = GameId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player id is not valid" in { - validate(request.copy(playerId = PlayerId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerId = PlayerId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player key is not valid" in { - validate(request.copy(playerKey = PlayerKey("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerKey = PlayerKey("invalid!"))).isFailure shouldEqual true } } @@ -503,19 +504,19 @@ class ValidationTest extends AnyFreeSpec with Matchers with TestHelpers with Sca ) "returns the request for a valid bet request" in { - validate(request).value shouldEqual request + validate[Try](request).success.value shouldEqual request } "returns a failure if the game id is not valid" in { - validate(request.copy(gameId = GameId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(gameId = GameId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player id is not valid" in { - validate(request.copy(playerId = PlayerId("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerId = PlayerId("invalid!"))).isFailure shouldEqual true } "returns a failure if the player key is not valid" in { - validate(request.copy(playerKey = PlayerKey("invalid!"))).isLeft shouldEqual true + validate[Try](request.copy(playerKey = PlayerKey("invalid!"))).isFailure shouldEqual true } } } diff --git a/devserver/src/main/scala/io/adamnfish/pokerdot/CatsDevServer.scala b/devserver/src/main/scala/io/adamnfish/pokerdot/CatsDevServer.scala new file mode 100644 index 0000000..b871782 --- /dev/null +++ b/devserver/src/main/scala/io/adamnfish/pokerdot/CatsDevServer.scala @@ -0,0 +1,167 @@ +package io.adamnfish.pokerdot + +import cats.effect.unsafe.implicits.global +import cats.effect.* +import io.adamnfish.pokerdot.Console.* +import io.adamnfish.pokerdot.models.{ + AppContext, + Failures, + PlayerAddress, + TraceId +} +import io.adamnfish.pokerdot.persistence.DynamoDbDatabase +import io.adamnfish.pokerdot.services.* +import io.javalin.Javalin +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.slf4j.Slf4jLogger +import software.amazon.awssdk.auth.credentials.{ + AwsBasicCredentials, + StaticCredentialsProvider +} +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.dynamodb.{ + DynamoDbAsyncClient, + DynamoDbClient +} + +import java.net.URI +import java.security.SecureRandom +import java.util.UUID + +object CatsDevServer extends IOApp: + implicit def logger: Logger[IO] = Slf4jLogger.getLogger[IO] + + private def components( + args: List[String] + ): Resource[IO, DevServerComponents] = + for + messagePrinter <- + (if (args.contains("--debug")) { + for _ <- logger.info( + "debug mode - connection events and messages will be printed" + ) + yield logMessage[IO] + } else { + IO.pure(noOpMessage[IO]) + }).toResource + connectionPrinter = + if (args.contains("--debug")) { + logConnection[IO] + } else { + noOpConnection[IO] + } + messaging <- IO(new DevMessaging(messagePrinter(Outbound))).toResource + + initialSeed <- args + .filterNot(_ == "--debug") + .headOption + .fold(IO.pure(0L)) { seed => + if (seed.toLowerCase == "rng") + IO(new SecureRandom().nextLong()) + else + IO.pure(seed.toLong) + }.toResource + rng = new DevRng[IO](initialSeed) + + client <- Resource.make(IO.blocking { + DynamoDbAsyncClient + .builder() + .endpointOverride(URI.create("http://localhost:8042")) + .region(Region.US_EAST_1) // not used for local dynamodb, but required + .credentialsProvider( + StaticCredentialsProvider.create( + AwsBasicCredentials.create("dummykey", "dummysecret") + ) + ) + .build() + })(client => IO.blocking(client.close())) + _ <- DevServerDB.createGamesTable(client).toResource + _ <- DevServerDB.createPlayersTable(client).toResource + db = new DynamoDbDatabase[IO](client, "games", "players") + time = new RealTime[IO] + + appContextBuilder = + (address: PlayerAddress, traceId: TraceId) => + AppContext(address, traceId, db, messaging, time, rng) + + app <- Resource.make { + IO.blocking { + val app = Javalin.create() + app.start(7000) + app + } + } { app => + IO.blocking(app.stop()) + } + yield DevServerComponents( + app, + appContextBuilder, + messagePrinter, + connectionPrinter + // TODO add separate comection manager here? + ) + + override def run(args: List[String]): IO[ExitCode] = + components(args).use: components => + IO { + components.app.ws( + "/api", + { ws => + ws.onConnect { wctx => + val traceId = TraceId("connect") + val appContext = components.appContextBuilder( + PlayerAddress(wctx.getSessionId), + traceId + ) + val result = for { + _ <- components.connectionPrinter(wctx.getSessionId, true) + _ <- IO.unit // TODO: connect to dev messaging here + } yield () + result.unsafeRunSync() + } + ws.onClose { wctx => + val traceId = TraceId("close") + val appContext = components.appContextBuilder( + PlayerAddress(wctx.getSessionId), + traceId + ) + val result = for { + _ <- components.connectionPrinter(wctx.getSessionId, false) + _ <- IO.unit // TODO: disconnect from dev messaging here + } yield () + result.unsafeRunSync() + } + ws.onMessage { wctx => + val result = + for + _ <- components.messagePrinter(Inbound)( + wctx.getSessionId, + wctx.message + ) + traceId <- IO(TraceId(UUID.randomUUID().toString)) + appContext = components.appContextBuilder( + PlayerAddress(wctx.getSessionId), + traceId + ) + operation <- PokerDot.pokerdot(wctx.message, appContext) + _ <- logger.info(s"completed $operation") + yield () + result + .onError { + case failures: Failures => + logger.error(failures)(s"error: ${failures.logString}") + case err => + logger.error(err)(s"exception: ${err.getMessage}") + } + .unsafeRunSync() + } + } + ) + }.as(ExitCode.Success) >> IO.never + +case class DevServerComponents( + app: Javalin, + appContextBuilder: (PlayerAddress, TraceId) => AppContext[IO], + messagePrinter: Direction => (String, String) => IO[Unit], + connectionPrinter: (String, Boolean) => IO[Unit] +) diff --git a/devserver/src/main/scala/io/adamnfish/pokerdot/Console.scala b/devserver/src/main/scala/io/adamnfish/pokerdot/Console.scala index 579459a..a4fc92d 100644 --- a/devserver/src/main/scala/io/adamnfish/pokerdot/Console.scala +++ b/devserver/src/main/scala/io/adamnfish/pokerdot/Console.scala @@ -1,6 +1,8 @@ package io.adamnfish.pokerdot +import cats.Applicative import io.adamnfish.pokerdot.Console.Direction +import org.typelevel.log4cats.Logger import scala.io.AnsiColor import scala.util.Random @@ -53,31 +55,31 @@ object Console { s"${AnsiColor.CYAN_B}${AnsiColor.BOLD}${AnsiColor.WHITE}", ) - def logMessage(direction: Direction)(uid: String, body: String): Unit = { + def logMessage[F[_] : Logger](direction: Direction)(uid: String, body: String): F[Unit] = { val dir = direction match { case Inbound => "<-" case Outbound => "->" } - println(s"[DEBUG] Message: ${displayId(uid)} $dir $body") + Logger[F].debug(s"[DEBUG] Message: ${displayId(uid)} $dir $body") } - def noOpMessage(direction: Direction)(uid: String, body: String): Unit = { - () + def noOpMessage[F[_] : Applicative](direction: Direction)(uid: String, body: String): F[Unit] = { + Applicative[F].unit } sealed trait Direction case object Inbound extends Direction case object Outbound extends Direction - def logConnection(uid: String, connected: Boolean): Unit = { + def logConnection[F[_] : Logger](uid: String, connected: Boolean): F[Unit] = { if (connected) { - println(s"[DEBUG] Connected: ${displayId(uid, fullId = true)}") + Logger[F].debug(s"[DEBUG] Connected: ${displayId(uid, fullId = true)}") } else { - println(s"[DEBUG] Disconnected: ${displayId(uid)}") + Logger[F].debug(s"[DEBUG] Disconnected: ${displayId(uid)}") } } - def noOpConnection(uid: String, connected: Boolean): Unit = { - () + def noOpConnection[F[_]: Applicative](uid: String, connected: Boolean): F[Unit] = { + Applicative[F].unit } } diff --git a/devserver/src/main/scala/io/adamnfish/pokerdot/DevServer.scala b/devserver/src/main/scala/io/adamnfish/pokerdot/DevServer.scala index 126b442..3e029c6 100644 --- a/devserver/src/main/scala/io/adamnfish/pokerdot/DevServer.scala +++ b/devserver/src/main/scala/io/adamnfish/pokerdot/DevServer.scala @@ -1,95 +1,146 @@ -package io.adamnfish.pokerdot - -import com.typesafe.scalalogging.LazyLogging -import io.adamnfish.pokerdot.Console.{Direction, Inbound, Outbound, displayId, logConnection, logMessage, noOpConnection, noOpMessage} -import io.adamnfish.pokerdot.models.{AppContext, PlayerAddress, TraceId} -import io.adamnfish.pokerdot.persistence.DynamoDbDatabase -import io.adamnfish.pokerdot.services.{Clock, DevMessaging, DevRng, DevServerDB} -import io.javalin.Javalin -import org.scanamo.LocalDynamoDB -import zio.{Exit, Unsafe, ZIO} - -import java.security.SecureRandom -import java.util.UUID - - -object DevServer extends LazyLogging { - val client = LocalDynamoDB.syncClient() - val db = new DynamoDbDatabase(client, "games", "players") - DevServerDB.createGamesTable(client) - DevServerDB.createPlayersTable(client) - - def main(args: Array[String]): Unit = { - val runtime = zio.Runtime.default - - // initials seed defaults to 0, but can be changed at server start time - val initialSeed = args.filterNot(_ == "--debug").headOption - .map { seed => - if (seed.toLowerCase == "rng") - new SecureRandom().nextLong() - else - seed.toLong - } - .getOrElse(0L) - logger.info(s"initial seed: $initialSeed") - val rng = new DevRng(initialSeed) - - val messagePrinter: Direction => (String, String) => Unit = - if (args.contains("--debug")) { - logger.info("debug mode - connection events and messages will be printed") - logMessage - } else { - noOpMessage - } - val connectionPrinter: (String, Boolean) => Unit = - if (args.contains("--debug")) { - logConnection - } else { - noOpConnection - } - - val messaging = new DevMessaging(messagePrinter(Outbound)) - - val app = Javalin.create() - app.start(7000) - app.ws("/api", { ws => - ws.onConnect { wctx => - val id = messaging.connect(wctx) - connectionPrinter(id, true) - } - ws.onClose { wctx => - messaging.disconnect(wctx) - connectionPrinter(wctx.getSessionId, false) - } - ws.onMessage { wctx => - val traceId = TraceId(UUID.randomUUID().toString) - - messagePrinter(Inbound)(wctx.getSessionId, wctx.message) - val appContext = AppContext(PlayerAddress(wctx.getSessionId), traceId, db, messaging, Clock, rng) - Unsafe.unsafe { implicit unsafe => - runtime.unsafe.run { - PokerDot.pokerdot(wctx.message, appContext) - } - } match { - case Exit.Success(operation) => - logger.info(s"completed $operation") - case Exit.Failure(cause) => - cause.failures.foreach { fs => - logger.error(s"error: ${fs.logString}") - fs.exception.foreach { e => - logger.error(s"exception: ${e.printStackTrace()}") - } - } - cause.defects.foreach { err => - logger.error(s"Fatal error: ${err.getMessage}", err) - } - } - } - }) - - Runtime.getRuntime.addShutdownHook(new Thread(() => { - logger.info("Stopping...") - app.stop() - })) - } -} +//package io.adamnfish.pokerdot +// +//import com.typesafe.scalalogging.LazyLogging +//import io.adamnfish.pokerdot.Console.{Direction, Inbound, Outbound, displayId, logConnection, logMessage, noOpConnection, noOpMessage} +//import io.adamnfish.pokerdot.models.{AppContext, PlayerAddress, TraceId} +//import io.adamnfish.pokerdot.persistence.DynamoDbDatabase +//import io.adamnfish.pokerdot.services.{Time, DevMessaging, DevRng, DevServerDB} +//import io.javalin.Javalin +//import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} +//import software.amazon.awssdk.regions.Region +//import software.amazon.awssdk.services.dynamodb.DynamoDbClient +//import zio.{Exit, Unsafe, ZIO} +//import cats.effect.{ExitCode, IO, IOApp, Resource} +// +//import java.net.URI +//import java.security.SecureRandom +//import java.util.UUID +// +// +////object DevServer extends IOApp with LazyLogging { +//// def appContext(args: List[String]): Resource[IO, String => AppContext[IO]] = +//// val initialSeed = args.filterNot(_ == "--debug").headOption +//// .map { seed => +//// if (seed.toLowerCase == "rng") +//// new SecureRandom().nextLong() +//// else +//// seed.toLong +//// } +//// .getOrElse(0L) +//// val messagePrinter: Direction => (String, String) => Unit = +//// if (args.contains("--debug")) { +//// logger.info("debug mode - connection events and messages will be printed") +//// logMessage +//// } else { +//// noOpMessage +//// } +//// val connectionPrinter: (String, Boolean) => Unit = +//// if (args.contains("--debug")) { +//// logConnection +//// } else { +//// noOpConnection +//// } +//// for +//// client <- DynamoDbClient.builder() +//// .endpointOverride(URI.create("http://localhost:8042")) +//// .region(Region.US_EAST_1) // not used for local dynamodb, but required +//// .credentialsProvider(StaticCredentialsProvider.create( +//// AwsBasicCredentials.create("dummykey", "dummysecret"))) +//// .build() +//// db <- Resource.eval: +//// for +//// _ <- DevServerDB.createGamesTable(client) +//// _ <- DevServerDB.createPlayersTable(client) +//// yield new DynamoDbDatabase(client, "games", "players") +//// yield +//// traceId: String => +//// AppContext[IO](PlayerAddress("dev"), TraceId(traceId), db, new DevMessaging(messagePrinter(Outbound)), Clock, DevRng(initialSeed)) +//// +//// override def run(args: List[String]): IO[ExitCode] = +//// ??? +//// +//// val client = DynamoDbClient.builder() +//// .endpointOverride(URI.create("http://localhost:8042")) +//// .region(Region.US_EAST_1) // not used for local dynamodb, but required +//// .credentialsProvider(StaticCredentialsProvider.create( +//// AwsBasicCredentials.create("dummykey", "dummysecret"))) +//// .build() +//// +//// val db = new DynamoDbDatabase(client, "games", "players") +//// DevServerDB.createGamesTable(client) +//// DevServerDB.createPlayersTable(client) +//// +//// def main2(args: Array[String]): Unit = { +//// val runtime = zio.Runtime.default +//// +//// // initials seed defaults to 0, but can be changed at server start time +//// val initialSeed = args.filterNot(_ == "--debug").headOption +//// .map { seed => +//// if (seed.toLowerCase == "rng") +//// new SecureRandom().nextLong() +//// else +//// seed.toLong +//// } +//// .getOrElse(0L) +//// logger.info(s"initial seed: $initialSeed") +//// val rng = new DevRng(initialSeed) +//// +//// val messagePrinter: Direction => (String, String) => Unit = +//// if (args.contains("--debug")) { +//// logger.info("debug mode - connection events and messages will be printed") +//// logMessage +//// } else { +//// noOpMessage +//// } +//// val connectionPrinter: (String, Boolean) => Unit = +//// if (args.contains("--debug")) { +//// logConnection +//// } else { +//// noOpConnection +//// } +//// +//// val app = Javalin.create() +//// app.start(7000) +//// app.ws("/api", { ws => +//// ws.onConnect { wctx => +//// val id = messaging.connect(wctx) +//// connectionPrinter(id, true) +//// } +//// ws.onClose { wctx => +//// messaging.disconnect(wctx) +//// connectionPrinter(wctx.getSessionId, false) +//// } +//// ws.onMessage { wctx => +//// val traceId = TraceId(UUID.randomUUID().toString) +//// +//// messagePrinter(Inbound)(wctx.getSessionId, wctx.message) +//// val appContext = AppContext(PlayerAddress(wctx.getSessionId), traceId, db, messaging, Clock, rng) +//// Unsafe.unsafe { implicit unsafe => +//// runtime.unsafe.run { +//// PokerDot.pokerdot(wctx.message, appContext) +//// } +//// } match { +//// case Exit.Success(operation) => +//// logger.info(s"completed $operation") +//// case Exit.Failure(cause) => +//// cause.failures.foreach { fs => +//// logger.error(s"error: ${fs.logString}") +//// fs.exception.foreach { e => +//// logger.error(s"exception: ${e.printStackTrace()}") +//// } +//// } +//// cause.defects.foreach { err => +//// logger.error(s"Fatal error: ${err.getMessage}", err) +//// } +//// } +//// } +//// }) +//// +//// Runtime.getRuntime.addShutdownHook(new Thread { +//// override def run(): Unit = { +//// logger.info("Stopping...") +//// app.stop() +//// } +//// }) +//// } +////} diff --git a/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevMessaging.scala b/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevMessaging.scala index 57c771f..2732b25 100644 --- a/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevMessaging.scala +++ b/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevMessaging.scala @@ -1,13 +1,15 @@ package io.adamnfish.pokerdot.services -import io.adamnfish.pokerdot.models._ +import cats.MonadThrow +import io.adamnfish.pokerdot.models.* import io.javalin.websocket.WsContext -import zio.ZIO import scala.collection.mutable +import cats.effect.{IO, Sync} +import cats.syntax.all.* -class DevMessaging(logMessage: (String, String) => Unit) extends Messaging { +class DevMessaging[F[_] : Sync : MonadThrow](logMessage: (String, String) => F[Unit]) extends Messaging[F] { private val connections = new mutable.HashMap[String, WsContext] def connect(wctx: WsContext): String = { @@ -22,11 +24,11 @@ class DevMessaging(logMessage: (String, String) => Unit) extends Messaging { } } - override def sendMessage(playerAddress: PlayerAddress, message: Message): Attempt[Unit] = { + override def sendMessage(playerAddress: PlayerAddress, message: Message): F[Unit] = { send(playerAddress.address, Serialisation.encodeMessage(message)) } - override def sendError(playerAddress: PlayerAddress, message: Failures): Attempt[Unit] = { + override def sendError(playerAddress: PlayerAddress, message: Failures): F[Unit] = { send(playerAddress.address, Serialisation.encodeFailure(message)) } @@ -34,27 +36,29 @@ class DevMessaging(logMessage: (String, String) => Unit) extends Messaging { * send failures are internal so clients are not distracted by * constant warnings after someone leaves the game. */ - private def send(recipientId: String, body: String): Attempt[Unit] = { + private def send(recipientId: String, body: String): F[Unit] = { for { - wctx <- ZIO.fromOption(connections.get(recipientId)).mapError(_ => + wctx <- MonadThrow[F].fromOption( + connections.get(recipientId), Failures("User not connected", "connection not found", internal = true) ) _ <- if (wctx.session.isOpen) { - ZIO.unit + MonadThrow[F].unit } else { - ZIO.fail { + MonadThrow[F].raiseError { Failures("Connection has closed", "connection closed", internal = true) } } - result <- - ZIO.attempt { + result <- { + Sync[F].blocking { wctx.send(body) () - }.mapError { err => + }.adaptError { case err => Failures("Error sending websocket message with wctx", "could not send message", exception = Some(err), internal = true) } - _ <- ZIO.attempt(logMessage(recipientId, body)).mapError { err => + } + _ <- Sync[F].blocking(logMessage(recipientId, body)).adaptError { err => Failures("Error logging websocket message", "could not log message", exception = Some(err), internal = true) } } yield result diff --git a/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevRng.scala b/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevRng.scala index 5c69a78..b60111a 100644 --- a/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevRng.scala +++ b/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevRng.scala @@ -1,17 +1,19 @@ package io.adamnfish.pokerdot.services +import cats.Applicative + import scala.util.Random /** * Rng that requires a fixed start value */ -class DevRng(initialSeed: Long) extends Rng { - override def randomState(): Long = { - initialSeed +class DevRng[F[_] : Applicative](initialSeed: Long) extends Rng[F] { + override def randomState: F[Long] = { + Applicative[F].pure(initialSeed) } - override def nextState(state: Long): Long = { - new Random(state).nextLong() + override def nextState(state: Long): F[Long] = { + Applicative[F].pure(new Random(state).nextLong()) } } diff --git a/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevServerDB.scala b/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevServerDB.scala index b6be7ab..1b33628 100644 --- a/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevServerDB.scala +++ b/devserver/src/main/scala/io/adamnfish/pokerdot/services/DevServerDB.scala @@ -1,22 +1,26 @@ package io.adamnfish.pokerdot.services import org.scanamo.LocalDynamoDB -import software.amazon.awssdk.services.dynamodb.DynamoDbClient -import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ - +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.* +import cats.effect.IO object DevServerDB { - def createGamesTable(client: DynamoDbClient): Unit = { - LocalDynamoDB.createTable(client)("games")( - "gameCode" -> S, - "gameId" -> S, - ) + def createGamesTable(client: DynamoDbAsyncClient): IO[Unit] = { + IO.blocking { + LocalDynamoDB.createTable(client)("games")( + "gameCode" -> S, + "gameId" -> S + ) + } } - def createPlayersTable(client: DynamoDbClient): Unit = { - LocalDynamoDB.createTable(client)("players")( - "gameId" -> S, - "playerId" -> S, - ) + def createPlayersTable(client: DynamoDbAsyncClient): IO[Unit] = { + IO.blocking { + LocalDynamoDB.createTable(client)("players")( + "gameId" -> S, + "playerId" -> S + ) + } } } diff --git a/frontend/package-lock.json b/frontend/package-lock.json index bb0fef5..043e2cd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -11,177 +11,117 @@ "reconnecting-websocket": "^4.4.0" }, "devDependencies": { - "@parcel/packager-raw-url": "2.10.3", - "@parcel/packager-xml": "2.10.3", - "@parcel/transformer-elm": "^2.10.3", - "@parcel/transformer-webmanifest": "2.10.3", - "@parcel/transformer-xml": "2.10.3", + "@parcel/optimizer-svgo": "2.13.3", + "@parcel/packager-raw-url": "2.13.3", + "@parcel/packager-xml": "2.13.3", + "@parcel/transformer-elm": "^2.13.3", + "@parcel/transformer-webmanifest": "2.13.3", + "@parcel/transformer-xml": "2.13.3", "elm": "^0.19.1-6", + "elm-format": "^0.8.7", + "elm-json": "^0.2.13", + "elm-review": "^2.12.0", "elm-test": "^0.19.1-revision7", - "parcel": "^2.10.3", - "parcel-plugin-browserconfig": "^1.0.5" + "parcel": "^2.13.3", + "parcel-plugin-browserconfig": "^1.0.5", + "svgo": "^3.3.2" } }, - "node_modules/@babel/code-frame": { - "version": "7.23.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", - "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", + "node_modules/@avh4/elm-format-darwin-arm64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-arm64/-/elm-format-darwin-arm64-0.8.7-2.tgz", + "integrity": "sha512-F5JD44mJ3KX960J5GkXMfh1/dtkXuPcQpX2EToHQKjLTZUfnhZ++ytQQt0gAvrJ0bzoOvhNzjNjUHDA1ruTVbg==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "@babel/highlight": "^7.23.4", - "chalk": "^2.4.2" - }, - "engines": { - "node": ">=6.9.0" - } + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@babel/code-frame/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "node_modules/@avh4/elm-format-darwin-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-darwin-x64/-/elm-format-darwin-x64-0.8.7-2.tgz", + "integrity": "sha512-4pfF1cl0KyTion+7Mg4XKM3yi4Yc7vP76Kt/DotLVGJOSag4ISGic1og2mt8RZZ7XArybBmHNyYkiUbe/cEiCw==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "darwin" + ] }, - "node_modules/@babel/code-frame/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "node_modules/@avh4/elm-format-linux-arm64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-arm64/-/elm-format-linux-arm64-0.8.7-2.tgz", + "integrity": "sha512-WkVmuce2zU6s9dupHhqPc886Vaqpea8dZlxv2fpZ4wSzPUbiiKHoHZzoVndMIMTUL0TZukP3Ps0n/lWO5R5+FA==", + "cpu": [ + "arm64" + ], "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/code-frame/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "node_modules/@avh4/elm-format-linux-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-linux-x64/-/elm-format-linux-x64-0.8.7-2.tgz", + "integrity": "sha512-kmncfJrTBjVT94JtQvMf4M5Pn2Yl0sZt3wo7AzgFiDnB/CiZ+KjJyXuWM64NeGiv4MQqzPq65tsFXUH1CIJeiQ==", + "cpu": [ + "x64" + ], "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/code-frame/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@babel/code-frame/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "node_modules/@avh4/elm-format-win32-x64": { + "version": "0.8.7-2", + "resolved": "https://registry.npmjs.org/@avh4/elm-format-win32-x64/-/elm-format-win32-x64-0.8.7-2.tgz", + "integrity": "sha512-sBdMBGq/8mD8Y5C+fIr5vlb3N50yB7S1MfgeAq2QEbvkr/sKrCZI540i43lZDH9gWsfA1w2W8wCe0penFYzsGw==", + "cpu": [ + "x64" + ], "dev": true, - "engines": { - "node": ">=4" - } + "license": "BSD-3-Clause", + "optional": true, + "os": [ + "win32" + ] }, - "node_modules/@babel/code-frame/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", "dev": true, + "license": "MIT", "dependencies": { - "has-flag": "^3.0.0" + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.22.20", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", - "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", - "dev": true, "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight": { - "version": "7.23.4", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", - "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", "dev": true, - "dependencies": { - "@babel/helper-validator-identifier": "^7.22.20", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0" - }, + "license": "MIT", "engines": { "node": ">=6.9.0" } }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "dev": true, - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "dev": true, - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", - "dev": true - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", - "dev": true, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, "node_modules/@elm_binaries/darwin_arm64": { "version": "0.19.1-0", "resolved": "https://registry.npmjs.org/@elm_binaries/darwin_arm64/-/darwin_arm64-0.19.1-0.tgz", @@ -190,6 +130,7 @@ "arm64" ], "dev": true, + "license": "BSD-3-Clause", "optional": true, "os": [ "darwin" @@ -203,6 +144,7 @@ "x64" ], "dev": true, + "license": "BSD-3-Clause", "optional": true, "os": [ "darwin" @@ -216,6 +158,7 @@ "x64" ], "dev": true, + "license": "BSD-3-Clause", "optional": true, "os": [ "linux" @@ -229,80 +172,167 @@ "x64" ], "dev": true, + "license": "BSD-3-Clause", "optional": true, "os": [ "win32" ] }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", - "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/set-array": "^1.0.1", + "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/trace-mapping": "^0.3.24" }, "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz", - "integrity": "sha512-dSYZh7HhCDtCKm4QakX0xFpsRDqjjtZf/kjI/v3T3Nwt5r8/qz/M19F9ySyOqU94SXBmeG9ttTul+YnR4LOxFA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/set-array": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz", - "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0.0" } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", - "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", "dev": true, + "license": "MIT", "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", - "dev": true + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==", + "dev": true, + "license": "MIT" }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.20", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.20.tgz", - "integrity": "sha512-R8LcPeWZol2zR8mmH3JeKQ6QRCFb7XgUhV9ZlGhHLGyg4wpPiPZNQOOWhFZhxKw8u//yTbNGI42Bx/3paXEQ+Q==", + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "node_modules/@lezer/common": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.1.2.tgz", - "integrity": "sha512-V+GqBsga5+cQJMfM0GdnHmg4DgWvLzgMWjbldBg0+jC3k9Gu6nJNZDLJxXEBT1Xj8KhRN4jmbC5CY7SIL++sVw==", - "dev": true + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==", + "dev": true, + "license": "MIT" }, "node_modules/@lezer/lr": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.3.14.tgz", - "integrity": "sha512-z5mY4LStlA3yL7aHT/rqgG614cfcvklS+8oFRFBYrs4YaWLJyKKM4+nN6KopToX0o9Hj6zmH6M5kinOYuy06ug==", + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0" } @@ -315,6 +345,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -328,6 +359,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -341,6 +373,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -354,6 +387,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -367,6 +401,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -380,6 +415,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -390,6 +426,7 @@ "resolved": "https://registry.npmjs.org/@mischnic/json-sourcemap/-/json-sourcemap-0.1.1.tgz", "integrity": "sha512-iA7+tyVqfrATAIsIRWQG+a7ZLLD0VaOCKV2Wd/v4mqIU3J9c4jx9p7S0nw1XH3gJCKNBOOwACOPYYSUu9pgT+w==", "dev": true, + "license": "MIT", "dependencies": { "@lezer/common": "^1.0.0", "@lezer/lr": "^1.0.0", @@ -400,137 +437,161 @@ } }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.2.tgz", - "integrity": "sha512-9bfjwDxIDWmmOKusUcqdS4Rw+SETlp9Dy39Xui9BEGEk19dDwH0jhipwFzEff/pFg95NKymc6TOTbRKcWeRqyQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.2.tgz", - "integrity": "sha512-lwriRAHm1Yg4iDf23Oxm9n/t5Zpw1lVnxYU3HnJPTi2lJRkKTrps1KVgvL6m7WvmhYVt/FIsssWay+k45QHeuw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.2.tgz", - "integrity": "sha512-MOI9Dlfrpi2Cuc7i5dXdxPbFIgbDBGgKR5F2yWEa6FVEtSWncfVNKW5AKjImAQ6CZlBK9tympdsZJ2xThBiWWA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.2.tgz", - "integrity": "sha512-FU20Bo66/f7He9Fp9sP2zaJ1Q8L9uLPZQDub/WlUip78JlPeMbVL8546HbZfcW9LNciEXc8d+tThSJjSC+tmsg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.2.tgz", - "integrity": "sha512-gsWNDCklNy7Ajk0vBBf9jEx04RUxuDQfBse918Ww+Qb9HCPoGzS+XJTLe96iN3BVK7grnLiYghP/M4L8VsaHeA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.2.tgz", - "integrity": "sha512-O+6Gs8UeDbyFpbSh2CPEz/UOrrdWPTBYNblZK5CxxLisYt4kGX3Sc+czffFonyjiGSq3jWLwJS/CCJc7tBr4sQ==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, - "node_modules/@parcel/bundler-default": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.10.3.tgz", - "integrity": "sha512-a+yq8zH8mrg6FBgUjrC+r3z6cfK7dQVMNzduEU/LF52Z4FVAmTR8gefl/YGmAbquJL3PFAHdhICrljYnQ1WQkg==", + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/graph": "3.0.3", - "@parcel/plugin": "2.10.3", - "@parcel/rust": "2.10.3", - "@parcel/utils": "2.10.3", - "nullthrows": "^1.1.1" + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "engines": { + "node": ">= 8" } }, - "node_modules/@parcel/cache": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.10.3.tgz", - "integrity": "sha512-fNNOFOl4dwOlzP8iAa+evZ+3BakX0sV+3+PiYA0zaps7EmPmkTSGDhCWzaYRSO8fhmNDlrUX9Xh7b/X738LFqA==", + "node_modules/@parcel/bundler-default": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/bundler-default/-/bundler-default-2.13.3.tgz", + "integrity": "sha512-mOuWeth0bZzRv1b9Lrvydis/hAzJyePy0gwa0tix3/zyYBvw0JY+xkXVR4qKyD/blc1Ra2qOlfI2uD3ucnsdXA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/fs": "2.10.3", - "@parcel/logger": "2.10.3", - "@parcel/utils": "2.10.3", - "lmdb": "2.8.5" + "@parcel/diagnostic": "2.13.3", + "@parcel/graph": "3.3.3", + "@parcel/plugin": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/utils": "2.13.3", + "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "peerDependencies": { - "@parcel/core": "^2.10.3" } }, "node_modules/@parcel/codeframe": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.10.3.tgz", - "integrity": "sha512-70ovUzeXBowDMjK+1xaLT4hm3jZUK7EbaCS6tN1cmmr0S1TDhU7g37jnpni+u9de9Lc/lErwTaDVXUf9WSQzQw==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/codeframe/-/codeframe-2.13.3.tgz", + "integrity": "sha512-L/PQf+PT0xM8k9nc0B+PxxOYO2phQYnbuifu9o4pFRiqVmCtHztP+XMIvRJ2gOEXy3pgAImSPFVJ3xGxMFky4g==", "dev": true, + "license": "MIT", "dependencies": { - "chalk": "^4.1.0" + "chalk": "^4.1.2" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", @@ -538,16 +599,17 @@ } }, "node_modules/@parcel/compressor-raw": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.10.3.tgz", - "integrity": "sha512-5SUZ80uwu7o0D+0RjhjBnSUXJRgaayfqVQtBRP3U7/W/Bb1Ixm1yDGXtDlyCbzimWqWVMMJ4/eVCEW7I8Ln4Bw==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/compressor-raw/-/compressor-raw-2.13.3.tgz", + "integrity": "sha512-C6vjDlgTLjYc358i7LA/dqcL0XDQZ1IHXFw6hBaHHOfxPKW2T4bzUI6RURyToEK9Q1X7+ggDKqgdLxwp4veCFg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3" + "@parcel/plugin": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -555,539 +617,443 @@ } }, "node_modules/@parcel/config-default": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.10.3.tgz", - "integrity": "sha512-gHVw5cKZVA9h/J4E33qQLg3QG3cYMyWVruyVzF8dFy/Rar5ebXMof1f38IhR2BIavpoThbnCnxgD4SVK8xOPag==", - "dev": true, - "dependencies": { - "@parcel/bundler-default": "2.10.3", - "@parcel/compressor-raw": "2.10.3", - "@parcel/namer-default": "2.10.3", - "@parcel/optimizer-css": "2.10.3", - "@parcel/optimizer-htmlnano": "2.10.3", - "@parcel/optimizer-image": "2.10.3", - "@parcel/optimizer-svgo": "2.10.3", - "@parcel/optimizer-swc": "2.10.3", - "@parcel/packager-css": "2.10.3", - "@parcel/packager-html": "2.10.3", - "@parcel/packager-js": "2.10.3", - "@parcel/packager-raw": "2.10.3", - "@parcel/packager-svg": "2.10.3", - "@parcel/packager-wasm": "2.10.3", - "@parcel/reporter-dev-server": "2.10.3", - "@parcel/resolver-default": "2.10.3", - "@parcel/runtime-browser-hmr": "2.10.3", - "@parcel/runtime-js": "2.10.3", - "@parcel/runtime-react-refresh": "2.10.3", - "@parcel/runtime-service-worker": "2.10.3", - "@parcel/transformer-babel": "2.10.3", - "@parcel/transformer-css": "2.10.3", - "@parcel/transformer-html": "2.10.3", - "@parcel/transformer-image": "2.10.3", - "@parcel/transformer-js": "2.10.3", - "@parcel/transformer-json": "2.10.3", - "@parcel/transformer-postcss": "2.10.3", - "@parcel/transformer-posthtml": "2.10.3", - "@parcel/transformer-raw": "2.10.3", - "@parcel/transformer-react-refresh-wrap": "2.10.3", - "@parcel/transformer-svg": "2.10.3" + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/config-default/-/config-default-2.13.3.tgz", + "integrity": "sha512-WUsx83ic8DgLwwnL1Bua4lRgQqYjxiTT+DBxESGk1paNm1juWzyfPXEQDLXwiCTcWMQGiXQFQ8OuSISauVQ8dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/bundler-default": "2.13.3", + "@parcel/compressor-raw": "2.13.3", + "@parcel/namer-default": "2.13.3", + "@parcel/optimizer-css": "2.13.3", + "@parcel/optimizer-htmlnano": "2.13.3", + "@parcel/optimizer-image": "2.13.3", + "@parcel/optimizer-svgo": "2.13.3", + "@parcel/optimizer-swc": "2.13.3", + "@parcel/packager-css": "2.13.3", + "@parcel/packager-html": "2.13.3", + "@parcel/packager-js": "2.13.3", + "@parcel/packager-raw": "2.13.3", + "@parcel/packager-svg": "2.13.3", + "@parcel/packager-wasm": "2.13.3", + "@parcel/reporter-dev-server": "2.13.3", + "@parcel/resolver-default": "2.13.3", + "@parcel/runtime-browser-hmr": "2.13.3", + "@parcel/runtime-js": "2.13.3", + "@parcel/runtime-react-refresh": "2.13.3", + "@parcel/runtime-service-worker": "2.13.3", + "@parcel/transformer-babel": "2.13.3", + "@parcel/transformer-css": "2.13.3", + "@parcel/transformer-html": "2.13.3", + "@parcel/transformer-image": "2.13.3", + "@parcel/transformer-js": "2.13.3", + "@parcel/transformer-json": "2.13.3", + "@parcel/transformer-postcss": "2.13.3", + "@parcel/transformer-posthtml": "2.13.3", + "@parcel/transformer-raw": "2.13.3", + "@parcel/transformer-react-refresh-wrap": "2.13.3", + "@parcel/transformer-svg": "2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.10.3" + "@parcel/core": "^2.13.3" } }, "node_modules/@parcel/core": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.10.3.tgz", - "integrity": "sha512-b64FdqJi4CX6iWeLZNfmwdTrC1VLPXHMuFusf1sTZTuRBFw2oRpgJvuiqsrInaZ82o3lbLMo4a9/5LtNaZKa+Q==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/core/-/core-2.13.3.tgz", + "integrity": "sha512-SRZFtqGiaKHlZ2YAvf+NHvBFWS3GnkBvJMfOJM7kxJRK3M1bhbwJa/GgSdzqro5UVf9Bfj6E+pkdrRQIOZ7jMQ==", "dev": true, + "license": "MIT", "dependencies": { "@mischnic/json-sourcemap": "^0.1.0", - "@parcel/cache": "2.10.3", - "@parcel/diagnostic": "2.10.3", - "@parcel/events": "2.10.3", - "@parcel/fs": "2.10.3", - "@parcel/graph": "3.0.3", - "@parcel/logger": "2.10.3", - "@parcel/package-manager": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/profiler": "2.10.3", - "@parcel/rust": "2.10.3", + "@parcel/cache": "2.13.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/events": "2.13.3", + "@parcel/feature-flags": "2.13.3", + "@parcel/fs": "2.13.3", + "@parcel/graph": "3.3.3", + "@parcel/logger": "2.13.3", + "@parcel/package-manager": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/profiler": "2.13.3", + "@parcel/rust": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", - "@parcel/workers": "2.10.3", - "abortcontroller-polyfill": "^1.1.9", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/workers": "2.13.3", "base-x": "^3.0.8", "browserslist": "^4.6.6", "clone": "^2.1.1", - "dotenv": "^7.0.0", - "dotenv-expand": "^5.1.0", + "dotenv": "^16.4.5", + "dotenv-expand": "^11.0.6", "json5": "^2.2.0", "msgpackr": "^1.9.9", "nullthrows": "^1.1.1", "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/diagnostic": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.10.3.tgz", - "integrity": "sha512-Hf3xG9UVkDABDXWi89TjEP5U1CLUUj81kx/QFeupBXnzt5GEQZBhkxdBq6+4w17Mmuvk7H5uumNsSptkWq9PCA==", + "node_modules/@parcel/core/node_modules/@parcel/cache": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/cache/-/cache-2.13.3.tgz", + "integrity": "sha512-Vz5+K5uCt9mcuQAMDo0JdbPYDmVdB8Nvu/A2vTEK2rqZPxvoOTczKeMBA4JqzKqGURHPRLaJCvuR8nDG+jhK9A==", "dev": true, + "license": "MIT", "dependencies": { - "@mischnic/json-sourcemap": "^0.1.0", - "nullthrows": "^1.1.1" + "@parcel/fs": "2.13.3", + "@parcel/logger": "2.13.3", + "@parcel/utils": "2.13.3", + "lmdb": "2.8.5" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.3" } }, - "node_modules/@parcel/events": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.10.3.tgz", - "integrity": "sha512-I3FsZYmKzgvo1f6frUWdF7hWwpeWTshPrFqpn9ICDXs/1Hjlf32jNXLBqon9b9XUDfMw4nSRMFMzMLJpbdheGA==", + "node_modules/@parcel/core/node_modules/@parcel/fs": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.13.3.tgz", + "integrity": "sha512-+MPWAt0zr+TCDSlj1LvkORTjfB/BSffsE99A9AvScKytDSYYpY2s0t4vtV9unSh0FHMS2aBCZNJ4t7KL+DcPIg==", "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/feature-flags": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/types-internal": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.13.3" + }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.3" } }, - "node_modules/@parcel/fs": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.10.3.tgz", - "integrity": "sha512-0w4+Lc7B5VpwqX4GQfjnI5qN7tc9qbGPSPsf/6U2YPWU4dkGsMfPEmLBx7dZvJy3UiGxpsjMMuRHa14+jJ5QrQ==", + "node_modules/@parcel/core/node_modules/@parcel/node-resolver-core": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.4.3.tgz", + "integrity": "sha512-IEnMks49egEic1ITBp59VQyHzkSQUXqpU9hOHwqN3KoSTdZ6rEgrXcS3pa6tdXay4NYGlcZ88kFCE8i/xYoVCg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/rust": "2.10.3", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", - "@parcel/watcher": "^2.0.7", - "@parcel/workers": "2.10.3" + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/diagnostic": "2.13.3", + "@parcel/fs": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/utils": "2.13.3", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "peerDependencies": { - "@parcel/core": "^2.10.3" } }, - "node_modules/@parcel/graph": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.0.3.tgz", - "integrity": "sha512-zUA8KsjR2+v2Q2bFBF7zBk33ejriDiRA/+LK5QE8LrFpkaDa+gjkx76h2x7JqGXIDHNos446KX4nz2OUCVwrNQ==", + "node_modules/@parcel/core/node_modules/@parcel/package-manager": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.13.3.tgz", + "integrity": "sha512-FLNI5OrZxymGf/Yln0E/kjnGn5sdkQAxW7pQVdtuM+5VeN75yibJRjsSGv88PvJ+KvpD2ANgiIJo1RufmoPcww==", "dev": true, + "license": "MIT", "dependencies": { - "nullthrows": "^1.1.1" + "@parcel/diagnostic": "2.13.3", + "@parcel/fs": "2.13.3", + "@parcel/logger": "2.13.3", + "@parcel/node-resolver-core": "3.4.3", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/workers": "2.13.3", + "@swc/core": "^1.7.26", + "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.3" } }, - "node_modules/@parcel/logger": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.10.3.tgz", - "integrity": "sha512-mAVTA0NgbbwEUzkzjBqjqyBBax+8bscRaZIAsEqMiSFWGcUmRgwVlH/jy3QDkFc7OHzwvdPK+XlMLV7s/3DJNw==", + "node_modules/@parcel/diagnostic": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/diagnostic/-/diagnostic-2.13.3.tgz", + "integrity": "sha512-C70KXLBaXLJvr7XCEVu8m6TqNdw1gQLxqg5BQ8roR62R4vWWDnOq8PEksxDi4Y8Z/FF4i3Sapv6tRx9iBNxDEg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/events": "2.10.3" + "@mischnic/json-sourcemap": "^0.1.0", + "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/markdown-ansi": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.10.3.tgz", - "integrity": "sha512-uzN1AJmp1oYh/ZLdD9WA7xP5u/L3Bs/6AFZz5s695zus74RCx9OtQcF0Yyl1hbKVJDfuw9WFuzMfPL/9p/C5DQ==", + "node_modules/@parcel/events": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/events/-/events-2.13.3.tgz", + "integrity": "sha512-ZkSHTTbD/E+53AjUzhAWTnMLnxLEU5yRw0H614CaruGh+GjgOIKyukGeToF5Gf/lvZ159VrJCGE0Z5EpgHVkuQ==", "dev": true, - "dependencies": { - "chalk": "^4.1.0" + "license": "MIT", + "engines": { + "node": ">= 16.0.0" }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/feature-flags": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/feature-flags/-/feature-flags-2.13.3.tgz", + "integrity": "sha512-UZm14QpamDFoUut9YtCZSpG1HxPs07lUwUCpsAYL0PpxASD3oWJQxIJGfDZPa2272DarXDG9adTKrNXvkHZblw==", + "dev": true, + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/namer-default": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.10.3.tgz", - "integrity": "sha512-s7kgB/x7TISIHhen9IK4+CBXgmRJYahVS+oiAbMm18vcUVuXeZDBeTedOco6zUQIKuB71vx/4DBIuiIp6Q9hpg==", + "node_modules/@parcel/graph": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@parcel/graph/-/graph-3.3.3.tgz", + "integrity": "sha512-pxs4GauEdvCN8nRd6wG3st6LvpHske3GfqGwUSR0P0X0pBPI1/NicvXz6xzp3rgb9gPWfbKXeI/2IOTfIxxVfg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", + "@parcel/feature-flags": "2.13.3", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/node-resolver-core": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.1.3.tgz", - "integrity": "sha512-o7XK1KiK3ymO39bhc5qfDQiZpKA1xQmKg0TEPDNiLIXHKLEBheqarhw3Nwwt9MOFibfwsisQtDTIS+2v9A640A==", + "node_modules/@parcel/logger": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/logger/-/logger-2.13.3.tgz", + "integrity": "sha512-8YF/ZhsQgd7ohQ2vEqcMD1Ag9JlJULROWRPGgGYLGD+twuxAiSdiFBpN3f+j4gQN4PYaLaIS/SwUFx11J243fQ==", "dev": true, + "license": "MIT", "dependencies": { - "@mischnic/json-sourcemap": "^0.1.0", - "@parcel/diagnostic": "2.10.3", - "@parcel/fs": "2.10.3", - "@parcel/rust": "2.10.3", - "@parcel/utils": "2.10.3", - "nullthrows": "^1.1.1", - "semver": "^7.5.2" + "@parcel/diagnostic": "2.13.3", + "@parcel/events": "2.13.3" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/optimizer-css": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.10.3.tgz", - "integrity": "sha512-Pc8jwV3U9w5DJDNcRQML5FlKdpPGnuCTtk1P+9FfyEUjdxoVxC+YeMIQcE961clAgl47qh7eNObXtsX/lb04Dg==", + "node_modules/@parcel/markdown-ansi": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/markdown-ansi/-/markdown-ansi-2.13.3.tgz", + "integrity": "sha512-B4rUdlNUulJs2xOQuDbN7Hq5a9roq8IZUcJ1vQ8PAv+zMGb7KCfqIIr/BSCDYGhayfAGBVWW8x55Kvrl1zrDYw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.10.3", - "browserslist": "^4.6.6", - "lightningcss": "^1.16.1", - "nullthrows": "^1.1.1" + "chalk": "^4.1.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/optimizer-htmlnano": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.10.3.tgz", - "integrity": "sha512-KTIZOy19tYeG0j3JRv435A6jnTh3O1LPhsUfo6Xlea7Cz1yUUxAANl9MG8lHZKYbZCFFKbfk2I9QBycmcYxAAw==", + "node_modules/@parcel/namer-default": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/namer-default/-/namer-default-2.13.3.tgz", + "integrity": "sha512-A2a5A5fuyNcjSGOS0hPcdQmOE2kszZnLIXof7UMGNkNkeC62KAG8WcFZH5RNOY3LT5H773hq51zmc2Y2gE5Rnw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "htmlnano": "^2.0.0", - "nullthrows": "^1.1.1", - "posthtml": "^0.16.5", - "svgo": "^2.4.0" + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/optimizer-htmlnano/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@parcel/optimizer-htmlnano/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/@parcel/optimizer-htmlnano/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", + "node_modules/@parcel/optimizer-css": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-css/-/optimizer-css-2.13.3.tgz", + "integrity": "sha512-A8o9IVCv919vhv69SkLmyW2WjJR5WZgcMqV6L1uiGF8i8z18myrMhrp2JuSHx29PRT9uNyzNC4Xrd4StYjIhJg==", "dev": true, + "license": "MIT", "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/source-map": "^2.1.1", + "@parcel/utils": "2.13.3", + "browserslist": "^4.6.6", + "lightningcss": "^1.22.1", + "nullthrows": "^1.1.1" }, "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@parcel/optimizer-htmlnano/node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "dependencies": { - "css-tree": "^1.1.2" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, - "engines": { - "node": ">=8.0.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/optimizer-htmlnano/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "node_modules/@parcel/optimizer-htmlnano/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", + "node_modules/@parcel/optimizer-htmlnano": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.13.3.tgz", + "integrity": "sha512-K4Uvg0Sy2pECP7pdvvbud++F0pfcbNkq+IxTrgqBX5HJnLEmRZwgdvZEKF43oMEolclMnURMQRGjRplRaPdbXg==", "dev": true, + "license": "MIT", "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", + "htmlnano": "^2.0.0", + "nullthrows": "^1.1.1", + "posthtml": "^0.16.5" }, "engines": { - "node": ">=10.13.0" + "node": ">= 16.0.0", + "parcel": "^2.13.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, "node_modules/@parcel/optimizer-image": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.10.3.tgz", - "integrity": "sha512-hbeI6+GoddJxib8MlK5iafbCm1oy3p0UL9bb8s5mjTZiHtj1PORlH8gP7mT1WlYOCgoy45QdHelcrmL9fJ8kBA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-image/-/optimizer-image-2.13.3.tgz", + "integrity": "sha512-wlDUICA29J4UnqkKrWiyt68g1e85qfYhp4zJFcFJL0LX1qqh1QwsLUz3YJ+KlruoqPxJSFEC8ncBEKiVCsqhEQ==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/rust": "2.10.3", - "@parcel/utils": "2.10.3", - "@parcel/workers": "2.10.3" + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/workers": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.10.3" + "@parcel/core": "^2.13.3" } }, "node_modules/@parcel/optimizer-svgo": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-svgo/-/optimizer-svgo-2.10.3.tgz", - "integrity": "sha512-STN7sdjz6wGnQnvy22SkQaLi5C1E+j7J0xy96T0/mCP9KoIsBDE7panCtf53p4sWCNRsXNVrXt5KrpCC+u0LHg==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-svgo/-/optimizer-svgo-2.13.3.tgz", + "integrity": "sha512-piIKxQKzhZK54dJR6yqIcq+urZmpsfgUpLCZT3cnWlX4ux5+S2iN66qqZBs0zVn+a58LcWcoP4Z9ieiJmpiu2w==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", - "svgo": "^2.4.0" + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, - "node_modules/@parcel/optimizer-svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, - "node_modules/@parcel/optimizer-svgo/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "dev": true, - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/@parcel/optimizer-svgo/node_modules/css-tree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", - "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", - "dev": true, - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@parcel/optimizer-svgo/node_modules/csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", - "dev": true, - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@parcel/optimizer-svgo/node_modules/mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true - }, - "node_modules/@parcel/optimizer-svgo/node_modules/svgo": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-2.8.0.tgz", - "integrity": "sha512-+N/Q9kV1+F+UeWYoSiULYo4xYSDQlTgb+ayMobAXPwMnLvop7oxKMo9OzIrX5x3eS4L4f2UHhc9axXwY8DpChg==", - "dev": true, - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^4.1.3", - "css-tree": "^1.1.3", - "csso": "^4.2.0", - "picocolors": "^1.0.0", - "stable": "^0.1.8" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=10.13.0" - } - }, "node_modules/@parcel/optimizer-swc": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.10.3.tgz", - "integrity": "sha512-Cxy05CysiKbv/PtX++ETje4cbhCJySmN6EmFyQBs0jvzsUdWwqnsttavYRoMviUUK9mjm/i5q+cyewBO/8Oc5g==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/optimizer-swc/-/optimizer-swc-2.13.3.tgz", + "integrity": "sha512-zNSq6oWqLlW8ksPIDjM0VgrK6ZAJbPQCDvs1V+p0oX3CzEe85lT5VkRpnfrN1+/vvEJNGL8e60efHKpI+rXGTA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.10.3", - "@swc/core": "^1.3.36", + "@parcel/utils": "2.13.3", + "@swc/core": "^1.7.26", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "node_modules/@parcel/package-manager": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.10.3.tgz", - "integrity": "sha512-KqOW5oUmElrcb7d+hOC68ja1PI2qbPZTwdduduRvB90DAweMt7r1046+W2Df5bd+p9iv72DxGEn9xomX+qz9MA==", - "dev": true, - "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/fs": "2.10.3", - "@parcel/logger": "2.10.3", - "@parcel/node-resolver-core": "3.1.3", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", - "@parcel/workers": "2.10.3", - "semver": "^7.5.2" - }, - "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" - }, - "peerDependencies": { - "@parcel/core": "^2.10.3" } }, "node_modules/@parcel/packager-css": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.10.3.tgz", - "integrity": "sha512-Jk165fFU2XyWjN7agKy+YvvRoOJbWIb57VlVDgBHanB5ptS7aCildambrljGNTivatr+zFrchE5ZDNUFXZhYnw==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-css/-/packager-css-2.13.3.tgz", + "integrity": "sha512-ghDqRMtrUwaDERzFm9le0uz2PTeqqsjsW0ihQSZPSAptElRl9o5BR+XtMPv3r7Ui0evo+w35gD55oQCJ28vCig==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.10.3", + "@parcel/utils": "2.13.3", + "lightningcss": "^1.22.1", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1095,20 +1061,21 @@ } }, "node_modules/@parcel/packager-html": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.10.3.tgz", - "integrity": "sha512-bEI6FhBvERuoqyi/h681qGImTRBUnqNW4sKoFO67q/bxWLevXtEGMFOeqridiVOjYQH9s1kKwM/ln/UwKVazZw==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-html/-/packager-html-2.13.3.tgz", + "integrity": "sha512-jDLnKSA/EzVEZ3/aegXO3QJ/Ij732AgBBkIQfeC8tUoxwVz5b3HiPBAjVjcUSfZs7mdBSHO+ELWC3UD+HbsIrQ==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/plugin": "2.13.3", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", "nullthrows": "^1.1.1", "posthtml": "^0.16.5" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1116,23 +1083,24 @@ } }, "node_modules/@parcel/packager-js": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.10.3.tgz", - "integrity": "sha512-SjLSDw0juC7bEk/0geUtSVXaZqm2SgHL2IZaPnkoBQxVqzh2MdvAxJCrS2LxiR/cuQRfvQ5bnoJA7Kk1w2VNAg==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-js/-/packager-js-2.13.3.tgz", + "integrity": "sha512-0pMHHf2zOn7EOJe88QJw5h/wcV1bFfj6cXVcE55Wa8GX3V+SdCgolnlvNuBcRQ1Tlx0Xkpo+9hMFVIQbNQY6zw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/rust": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/rust": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", "globals": "^13.2.0", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1140,16 +1108,17 @@ } }, "node_modules/@parcel/packager-raw": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.10.3.tgz", - "integrity": "sha512-d236tnP2ViOnUJR0+qG6EHw7MUWSA14fLKnYYzL5SRQ4BVo5XC+CM9HKN5O4YCCVu3+9Su2X1+RESo5sxbFq7w==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw/-/packager-raw-2.13.3.tgz", + "integrity": "sha512-AWu4UB+akBdskzvT3KGVHIdacU9f7cI678DQQ1jKQuc9yZz5D0VFt3ocFBOmvDfEQDF0uH3jjtJR7fnuvX7Biw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3" + "@parcel/plugin": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1157,17 +1126,18 @@ } }, "node_modules/@parcel/packager-raw-url": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-raw-url/-/packager-raw-url-2.10.3.tgz", - "integrity": "sha512-N/8UeOw7ByqdIRqEPA9qtKMA7w2vJ7GOo7kFAUgcYTSLPzCjS7ilwvE7efwobB1jQoFP1pg1VCIZo9n82EGhMQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-raw-url/-/packager-raw-url-2.13.3.tgz", + "integrity": "sha512-Dc8WeVagLGEUzVP4FqJBljXN59XSkvLoZaHeysvN9P33eznocrhIvc9T/OAQjOmsCj18X8jwxm0dIE7LNJbVCA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3" + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1175,19 +1145,20 @@ } }, "node_modules/@parcel/packager-svg": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.10.3.tgz", - "integrity": "sha512-Rk/GokkNs9uLwiy6Ux/xXpD8nMVhA9LN9eIbVqi8+eR42xUmICmEoUoSm+CnekkXxY2a5e3mKpL7JZbT9vOEhA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-svg/-/packager-svg-2.13.3.tgz", + "integrity": "sha512-tKGRiFq/4jh5u2xpTstNQ7gu+RuZWzlWqpw5NaFmcKe6VQe5CMcS499xTFoREAGnRvevSeIgC38X1a+VOo+/AA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/plugin": "2.13.3", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", "posthtml": "^0.16.4" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1195,16 +1166,17 @@ } }, "node_modules/@parcel/packager-wasm": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.10.3.tgz", - "integrity": "sha512-j6VmU84LKy+XRHgZQFoASG98P50a9tkeT3LYRrol3RGGQrvx7PT3/D6rOqbnQjR2iGnaHzYoAlgg9jIMmWXYiA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-wasm/-/packager-wasm-2.13.3.tgz", + "integrity": "sha512-SZB56/b230vFrSehVXaUAWjJmWYc89gzb8OTLkBm7uvtFtov2J1R8Ig9TTJwinyXE3h84MCFP/YpQElSfoLkJw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3" + "@parcel/plugin": "2.13.3" }, "engines": { - "node": ">=12.0.0", - "parcel": "^2.10.3" + "node": ">=16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1212,19 +1184,20 @@ } }, "node_modules/@parcel/packager-xml": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/packager-xml/-/packager-xml-2.10.3.tgz", - "integrity": "sha512-S8Z/odoBPn+/rWfZYDY773FLyVJIqt7i5aZye6YU2tJANUbdvszQWwzYgoRCKFhZSp9WUgT2FujwZM1/PZvrTg==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/packager-xml/-/packager-xml-2.13.3.tgz", + "integrity": "sha512-1ItLPaY3i1O7h0qRxF9IqQQI/dqoXgB0ZDkda9dUGVewuGjt5+xByHl6jN99bDJ+QS9L9YyXQ507x6+9Z5W7IA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", - "@xmldom/xmldom": "^0.7.9" + "@parcel/plugin": "2.13.3", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", + "@xmldom/xmldom": "^0.9.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1232,15 +1205,16 @@ } }, "node_modules/@parcel/plugin": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.10.3.tgz", - "integrity": "sha512-FgsfGKSdtSV1EcO2NWFCZaY14W0PnEEF8vZaRCTML3vKfUbilYs/biaqf5geFOu4DwRuCC8unOTqFy7dLwcK/A==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/plugin/-/plugin-2.13.3.tgz", + "integrity": "sha512-cterKHHcwg6q11Gpif/aqvHo056TR+yDVJ3fSdiG2xr5KD1VZ2B3hmofWERNNwjMcnR1h9Xq40B7jCKUhOyNFA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/types": "2.10.3" + "@parcel/types": "2.13.3" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", @@ -1248,17 +1222,19 @@ } }, "node_modules/@parcel/profiler": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.10.3.tgz", - "integrity": "sha512-yikaM6/vsvjDCcBHAXTKmDsWUF3UvC0lMG8RpnuVSN+R40MGH1vyrR4vNnqhkiCcs0RkVXm7bpuz3cDJLNLYSQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/profiler/-/profiler-2.13.3.tgz", + "integrity": "sha512-ok6BwWSLvyHe5TuSXjSacYnDStFgP5Y30tA9mbtWSm0INDsYf+m5DqzpYPx8U54OaywWMK8w3MXUClosJX3aPA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/events": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/events": "2.13.3", + "@parcel/types-internal": "2.13.3", "chrome-trace-event": "^1.0.2" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", @@ -1266,20 +1242,21 @@ } }, "node_modules/@parcel/reporter-cli": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.10.3.tgz", - "integrity": "sha512-p5xQTPRuB1K3eI3Ro90vcdxpdt0VqIgrUP/VJKtSI8I3fLLGgPBNmSZejqqLup3jFRzUttQPHYkWl/R14LHjAQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/reporter-cli/-/reporter-cli-2.13.3.tgz", + "integrity": "sha512-EA5tKt/6bXYNMEavSs35qHlFdx6cZmRazlZxPBgxPePQYoouNAPMNLUOEQozaPhz9f5fvNDN7EHOFaAWcdO2LA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", - "chalk": "^4.1.0", + "@parcel/plugin": "2.13.3", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", + "chalk": "^4.1.2", "term-size": "^2.2.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1287,17 +1264,18 @@ } }, "node_modules/@parcel/reporter-dev-server": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.10.3.tgz", - "integrity": "sha512-1Kzb2TrlnOYhGwFXZYCeoO18hpVhI3pRXnN22li9ZmdpeugZ0zZJamfPV8Duj4sBvBoSajbZhiPAe/6tQgWDSA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/reporter-dev-server/-/reporter-dev-server-2.13.3.tgz", + "integrity": "sha512-ZNeFp6AOIQFv7mZIv2P5O188dnZHNg0ymeDVcakfZomwhpSva2dFNS3AnvWo4eyWBlUxkmQO8BtaxeWTs7jAuA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3" + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1305,19 +1283,20 @@ } }, "node_modules/@parcel/reporter-tracer": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.10.3.tgz", - "integrity": "sha512-53T9VPJvCi4Co0iTmNN+nqFD+Fkt3QFW8CPXBVlmlQzOtufVjDb01VsE1NPD8/J7O0jd548HJX/s5uqT0380jg==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/reporter-tracer/-/reporter-tracer-2.13.3.tgz", + "integrity": "sha512-aBsVPI8jLZTDkFYrI69GxnsdvZKEYerkPsu935LcX9rfUYssOnmmUP+3oI+8fbg+qNjJuk9BgoQ4hCp9FOphMQ==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", "chrome-trace-event": "^1.0.3", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1325,17 +1304,66 @@ } }, "node_modules/@parcel/resolver-default": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.10.3.tgz", - "integrity": "sha512-TQc1LwpvEKyF3CnU9ifHOKV2usFLVYmMAVAkxyKKGTbnJGEqBDQ0ITqTapA6bJLvZ6d2eUT7guqd4nrBEjeZpw==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/resolver-default/-/resolver-default-2.13.3.tgz", + "integrity": "sha512-urBZuRALWT9pFMeWQ8JirchLmsQEyI9lrJptiwLbJWrwvmlwSUGkcstmPwoNRf/aAQjICB7ser/247Vny0pFxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/node-resolver-core": "3.4.3", + "@parcel/plugin": "2.13.3" + }, + "engines": { + "node": ">= 16.0.0", + "parcel": "^2.13.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/resolver-default/node_modules/@parcel/fs": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.13.3.tgz", + "integrity": "sha512-+MPWAt0zr+TCDSlj1LvkORTjfB/BSffsE99A9AvScKytDSYYpY2s0t4vtV9unSh0FHMS2aBCZNJ4t7KL+DcPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/feature-flags": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/types-internal": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.13.3" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.3" + } + }, + "node_modules/@parcel/resolver-default/node_modules/@parcel/node-resolver-core": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.4.3.tgz", + "integrity": "sha512-IEnMks49egEic1ITBp59VQyHzkSQUXqpU9hOHwqN3KoSTdZ6rEgrXcS3pa6tdXay4NYGlcZ88kFCE8i/xYoVCg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/node-resolver-core": "3.1.3", - "@parcel/plugin": "2.10.3" + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/diagnostic": "2.13.3", + "@parcel/fs": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/utils": "2.13.3", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", @@ -1343,17 +1371,18 @@ } }, "node_modules/@parcel/runtime-browser-hmr": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.10.3.tgz", - "integrity": "sha512-+6+mlJiLL3aNVIEyXMUPbPSgljYgnbl9JNMbEXikDQpGGiXTZ7gNNKsqwYeYzgQBYwgqRfR2ir6Bznc2R7dvxg==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.13.3.tgz", + "integrity": "sha512-EAcPojQFUNUGUrDk66cu3ySPO0NXRVS5CKPd4QrxPCVVbGzde4koKu8krC/TaGsoyUqhie8HMnS70qBP0GFfcQ==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3" + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1361,19 +1390,20 @@ } }, "node_modules/@parcel/runtime-js": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.10.3.tgz", - "integrity": "sha512-EMLgZzBGf5ylOT5U/N2rBK5ZZxnmEM4aJsissEAxcE/2cgE8TyhSng6p3A88vVJlO/unHcwRuFGlxKCueugGsQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/runtime-js/-/runtime-js-2.13.3.tgz", + "integrity": "sha512-62OucNAnxb2Q0uyTFWW/0Hvv2DJ4b5H6neh/YFu2/wmxaZ37xTpEuEcG2do7KW54xE5DeLP+RliHLwi4NvR3ww==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1381,19 +1411,20 @@ } }, "node_modules/@parcel/runtime-react-refresh": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.10.3.tgz", - "integrity": "sha512-l03mni8XJq3fmeAV8UYlKJ/+u0LYRuk6ZVP0VLYLwgK4O0mlRuxwaZWYUeB8r/kTsEjB3gF/9AAtUZdAC7Swow==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.13.3.tgz", + "integrity": "sha512-PYZ1klpJVwqE3WuifILjtF1dugtesHEuJcXYZI85T6UoRSD5ctS1nAIpZzT14Ga1lRt/jd+eAmhWL1l3m/Vk1Q==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", "react-error-overlay": "6.0.9", - "react-refresh": "^0.9.0" + "react-refresh": ">=0.9 <=0.14" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1401,18 +1432,19 @@ } }, "node_modules/@parcel/runtime-service-worker": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.10.3.tgz", - "integrity": "sha512-NjhS80t+O5iBgKXIQ+i07ZEh/VW8XHzanwTHmznJXEoIjLoBpELZ9r6bV/eUD3mYgM1vmW9Aijdu5xtsd0JW6A==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/runtime-service-worker/-/runtime-service-worker-2.13.3.tgz", + "integrity": "sha512-BjMhPuT7Us1+YIo31exPRwomPiL+jrZZS5UUAwlEW2XGHDceEotzRM94LwxeFliCScT4IOokGoxixm19qRuzWg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1420,12 +1452,13 @@ } }, "node_modules/@parcel/rust": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.10.3.tgz", - "integrity": "sha512-s1dD1QI/6JkWLICsFh8/iUvO7W1aj/avx+2mCSzuwEIsMywexpBf56qhVYMa3D9D50hS1h5FMk9RrSnSiPf8WA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/rust/-/rust-2.13.3.tgz", + "integrity": "sha512-dLq85xDAtzr3P5200cvxk+8WXSWauYbxuev9LCPdwfhlaWo/JEj6cu9seVdWlkagjGwkoV1kXC+GGntgUXOLAQ==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", @@ -1437,6 +1470,7 @@ "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", "dev": true, + "license": "MIT", "dependencies": { "detect-libc": "^1.0.3" }, @@ -1445,23 +1479,24 @@ } }, "node_modules/@parcel/transformer-babel": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.10.3.tgz", - "integrity": "sha512-SDTyDZX3WTkX7WS5Dg5cBLjWtIkUeeHezIjeOI4cw40tBjj5bXRR2TBfPsqwOnpTHr5jhNSicD6DN+XfTI2MMw==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-babel/-/transformer-babel-2.13.3.tgz", + "integrity": "sha512-ikzK9f5WTFrdQsPitQgjCPH6HmVU8AQPRemIJ2BndYhtodn5PQut5cnSvTrqax8RjYvheEKCQk/Zb/uR7qgS3g==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.10.3", + "@parcel/utils": "2.13.3", "browserslist": "^4.6.6", "json5": "^2.2.0", "nullthrows": "^1.1.1", "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1469,22 +1504,23 @@ } }, "node_modules/@parcel/transformer-css": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.10.3.tgz", - "integrity": "sha512-qlPYcwVgbqFHrec6CKcTQ4hY7EkjvH40Wyqf0xjAyIoIuOPmrpSUOp+VKjeRdbyFwH/4GBjrDZMBvCUsgeM2GA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-css/-/transformer-css-2.13.3.tgz", + "integrity": "sha512-zbrNURGph6JeVADbGydyZ7lcu/izj41kDxQ9xw4RPRW/3rofQiTU0OTREi+uBWiMENQySXVivEdzHA9cA+aLAA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.10.3", + "@parcel/utils": "2.13.3", "browserslist": "^4.6.6", - "lightningcss": "^1.16.1", + "lightningcss": "^1.22.1", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1492,13 +1528,14 @@ } }, "node_modules/@parcel/transformer-elm": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-elm/-/transformer-elm-2.10.3.tgz", - "integrity": "sha512-tqD3Q155BMTdjRVjD8DOZukNxT6vp+x4mFDM6JZTgT4w1gU7IzGT4MPyd180M2MClFYwD8slNPvMTIzfrqOkew==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-elm/-/transformer-elm-2.13.3.tgz", + "integrity": "sha512-kaTFQByd0p0FgNMZh6w65I5QtcLMxYKMoqvoi0AkUeYE3mHYF7XOHHOucYmPqUx1PaSqidQmet41+L2cNW1VkA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", "command-exists": "^1.2.8", "cross-spawn": "^7.0.3", "elm-hot": "^1.1.5", @@ -1507,8 +1544,8 @@ "terser": "^5.14.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1519,91 +1556,108 @@ } }, "node_modules/@parcel/transformer-html": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.10.3.tgz", - "integrity": "sha512-u0uklWpliEcPADtBlboxhxBvlGrP0yPRZk/A2iL0VhfAi9ONFEuJkEoesispNhAg3KiojEh0Ddzu7bYp9U0yww==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-html/-/transformer-html-2.13.3.tgz", + "integrity": "sha512-Yf74FkL9RCCB4+hxQRVMNQThH9+fZ5w0NLiQPpWUOcgDEEyxTi4FWPQgEBsKl/XK2ehdydbQB9fBgPQLuQxwPg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/rust": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/rust": "2.13.3", "nullthrows": "^1.1.1", "posthtml": "^0.16.5", - "posthtml-parser": "^0.10.1", + "posthtml-parser": "^0.12.1", "posthtml-render": "^3.0.0", "semver": "^7.5.2", "srcset": "4" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/transformer-html/node_modules/srcset": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", + "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/@parcel/transformer-image": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.10.3.tgz", - "integrity": "sha512-At7D7eMauE+/EnlXiDfNSap2te11L0TIW55SC9iTRTI/CqesWfT96ZB/LcH3HXckYy/GJi0xyTjYxC/YjUqDog==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-image/-/transformer-image-2.13.3.tgz", + "integrity": "sha512-wL1CXyeFAqbp2wcEq/JD3a/tbAyVIDMTC6laQxlIwnVV7dsENhK1qRuJZuoBdixESeUpFQSmmQvDIhcfT/cUUg==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", - "@parcel/workers": "2.10.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/workers": "2.13.3", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "peerDependencies": { - "@parcel/core": "^2.10.3" + "@parcel/core": "^2.13.3" } }, "node_modules/@parcel/transformer-js": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.10.3.tgz", - "integrity": "sha512-9pGqrCSLlipXvL7hOrLsaW5Pq4bjFBOTiZ5k5kizk1qeuHKMIHxySGdy0E35eSsJ6JzXP0lTXPywMPysSI6owQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-js/-/transformer-js-2.13.3.tgz", + "integrity": "sha512-KqfNGn1IHzDoN2aPqt4nDksgb50Xzcny777C7A7hjlQ3cmkjyJrixYjzzsPaPSGJ+kJpknh3KE8unkQ9mhFvRQ==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/rust": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/rust": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/utils": "2.10.3", - "@parcel/workers": "2.10.3", + "@parcel/utils": "2.13.3", + "@parcel/workers": "2.13.3", "@swc/helpers": "^0.5.0", "browserslist": "^4.6.6", "nullthrows": "^1.1.1", - "regenerator-runtime": "^0.13.7", + "regenerator-runtime": "^0.14.1", "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.10.3" + "@parcel/core": "^2.13.3" } }, "node_modules/@parcel/transformer-json": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.10.3.tgz", - "integrity": "sha512-cPhiQNgrX92VEATuxf3GCPQnlfnZW1iCsOHMT1CzgmofE7tVlW1hOOokWw21/8spG44Zax0SrRW0udi9TdmpQA==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-json/-/transformer-json-2.13.3.tgz", + "integrity": "sha512-rrq0ab6J0w9ePtsxi0kAvpCmrUYXXAx1Z5PATZakv89rSYbHBKEdXxyCoKFui/UPVCUEGVs5r0iOFepdHpIyeA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", + "@parcel/plugin": "2.13.3", "json5": "^2.2.0" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1611,23 +1665,24 @@ } }, "node_modules/@parcel/transformer-postcss": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.10.3.tgz", - "integrity": "sha512-SpTZQdGQ3aVvl6+3tLlw/txUyzZSsv8t+hcfc9PM0n1rd4mfjWxVKmgNC1Y3nFoSubLMp+03GbMq16ym8t89WQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-postcss/-/transformer-postcss-2.13.3.tgz", + "integrity": "sha512-AIiWpU0QSFBrPcYIqAnhqB8RGE6yHFznnxztfg1t2zMSOnK3xoU6xqYKv8H/MduShGGrC3qVOeDfM8MUwzL3cw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/rust": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/utils": "2.13.3", "clone": "^2.1.1", "nullthrows": "^1.1.1", "postcss-value-parser": "^4.2.0", "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1635,22 +1690,23 @@ } }, "node_modules/@parcel/transformer-posthtml": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.10.3.tgz", - "integrity": "sha512-k6pz0H/W1k+i9uDNXjum7XkaFYKvSSrgEsmhoh7OriXPrLunboIzMBXFQcQSCyxCpw/kLuKFBLP38mQnYC5BbQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-posthtml/-/transformer-posthtml-2.13.3.tgz", + "integrity": "sha512-5GSLyccpHASwFAu3uJ83gDIBSvfsGdVmhJvy0Vxe+K1Fklk2ibhvvtUHMhB7mg6SPHC+R9jsNc3ZqY04ZLeGjw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", "nullthrows": "^1.1.1", "posthtml": "^0.16.5", - "posthtml-parser": "^0.10.1", + "posthtml-parser": "^0.12.1", "posthtml-render": "^3.0.0", "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1658,16 +1714,17 @@ } }, "node_modules/@parcel/transformer-raw": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.10.3.tgz", - "integrity": "sha512-r//P2Hg14m/vJK/XJyq0cmcS4RTRy4bPSL4c0FxbEdDRrSm0Hcd1gdfgl0HeqSQQfcz0Xu4nCM5zAhg6FUpiXQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-raw/-/transformer-raw-2.13.3.tgz", + "integrity": "sha512-BFsAbdQF0l8/Pdb7dSLJeYcd8jgwvAUbHgMink2MNXJuRUvDl19Gns8jVokU+uraFHulJMBj40+K/RTd33in4g==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3" + "@parcel/plugin": "2.13.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1675,18 +1732,19 @@ } }, "node_modules/@parcel/transformer-react-refresh-wrap": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.10.3.tgz", - "integrity": "sha512-Sc6ExGQy/YhNYFxRgEyi4SikYmV3wbATYo/VzqUjvZ4vE9YXM0sC5CyJhcoWVHmMPhm5eowOwFA6UrTsgHd2+g==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.13.3.tgz", + "integrity": "sha512-mOof4cRyxsZRdg8kkWaFtaX98mHpxUhcGPU+nF9RQVa9q737ItxrorsPNR9hpZAyE2TtFNflNW7RoYsgvlLw8w==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3", - "react-refresh": "^0.9.0" + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3", + "react-refresh": ">=0.9 <=0.14" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1694,23 +1752,24 @@ } }, "node_modules/@parcel/transformer-svg": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.10.3.tgz", - "integrity": "sha512-fjkTdPB8y467I/yHPEaNxNxoGtRIgEqNjVkBhtE/ibhF/YfqIEpDlJyI7G5G71pt2peLMLXZnJowzHqeoEUHOQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-svg/-/transformer-svg-2.13.3.tgz", + "integrity": "sha512-9jm7ZF4KHIrGLWlw/SFUz5KKJ20nxHvjFAmzde34R9Wu+F1BOjLZxae7w4ZRwvIc+UVOUcBBQFmhSVwVDZg6Dw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/rust": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/rust": "2.13.3", "nullthrows": "^1.1.1", "posthtml": "^0.16.5", - "posthtml-parser": "^0.10.1", + "posthtml-parser": "^0.12.1", "posthtml-render": "^3.0.0", "semver": "^7.5.2" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1718,18 +1777,19 @@ } }, "node_modules/@parcel/transformer-webmanifest": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-webmanifest/-/transformer-webmanifest-2.10.3.tgz", - "integrity": "sha512-1AGEl0TeiC0vXJR9YYUBprtgLDnf4KMKMwQPAyiJUHdu7MWh97KyLZChNEAIvtzHj7Weatn3OZbqnuRj6FJloQ==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-webmanifest/-/transformer-webmanifest-2.13.3.tgz", + "integrity": "sha512-nUuufZW4VYQTk1qf8HLLckxC5AYtGSWMAHxKTqoJldXl1OgpYkpSPLU+Uh1uiESN+XpQiZB4qnhCExRjPeCRSA==", "dev": true, + "license": "MIT", "dependencies": { "@mischnic/json-sourcemap": "^0.1.0", - "@parcel/diagnostic": "2.10.3", - "@parcel/plugin": "2.10.3", - "@parcel/utils": "2.10.3" + "@parcel/diagnostic": "2.13.3", + "@parcel/plugin": "2.13.3", + "@parcel/utils": "2.13.3" }, "engines": { - "parcel": "^2.10.3" + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1737,17 +1797,18 @@ } }, "node_modules/@parcel/transformer-xml": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/transformer-xml/-/transformer-xml-2.10.3.tgz", - "integrity": "sha512-wVknihWRwt0FZXIFa2GrC61JR60sGAzDxUj4HECSfaUyyKTx8b+cXw+kqrcN711FUt9dRl48gXf/ZVsoHrhdTg==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/transformer-xml/-/transformer-xml-2.13.3.tgz", + "integrity": "sha512-1bN7MdTrhmff5MbuvIMdHjkbKWODI19X5cE82HqCc2qLm76rzLAH1nY1QiVb0wsJkC+bwKfdtzcORWw1bn2pBA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/plugin": "2.10.3", - "@xmldom/xmldom": "^0.7.9" + "@parcel/plugin": "2.13.3", + "@xmldom/xmldom": "^0.9.3" }, "engines": { - "node": ">= 12.0.0", - "parcel": "^2.10.3" + "node": ">= 16.0.0", + "parcel": "^2.13.3" }, "funding": { "type": "opencollective", @@ -1755,37 +1816,47 @@ } }, "node_modules/@parcel/types": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.10.3.tgz", - "integrity": "sha512-4ISgDKcbJsR7NKj2jquPUPQWc/b2x6zHb/jZVdHVzMQxJp98DX+cvQR137iOTXUAFtwkKVjFcHWfejwGdGf9bw==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/types/-/types-2.13.3.tgz", + "integrity": "sha512-+RpFHxx8fy8/dpuehHUw/ja9PRExC3wJoIlIIF42E7SLu2SvlTHtKm6EfICZzxCXNEBzjoDbamCRcN0nmTPlhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/types-internal": "2.13.3", + "@parcel/workers": "2.13.3" + } + }, + "node_modules/@parcel/types-internal": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/types-internal/-/types-internal-2.13.3.tgz", + "integrity": "sha512-Lhx0n+9RCp+Ipktf/I+CLm3zE9Iq9NtDd8b2Vr5lVWyoT8AbzBKIHIpTbhLS4kjZ80L3I6o93OYjqAaIjsqoZw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/cache": "2.10.3", - "@parcel/diagnostic": "2.10.3", - "@parcel/fs": "2.10.3", - "@parcel/package-manager": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/feature-flags": "2.13.3", "@parcel/source-map": "^2.1.1", - "@parcel/workers": "2.10.3", "utility-types": "^3.10.0" } }, "node_modules/@parcel/utils": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.10.3.tgz", - "integrity": "sha512-l9pEQgq+D57t42m2sJkdU08Dpp0HVzDEwVrp/by/l37ZkYPJ2Me3oXtsJhvA+hej2kO8+FuKPm64FaUVaA2g+w==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/utils/-/utils-2.13.3.tgz", + "integrity": "sha512-yxY9xw2wOUlJaScOXYZmMGoZ4Ck4Kqj+p6Koe5kLkkWM1j98Q0Dj2tf/mNvZi4yrdnlm+dclCwNRnuE8Q9D+pw==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/codeframe": "2.10.3", - "@parcel/diagnostic": "2.10.3", - "@parcel/logger": "2.10.3", - "@parcel/markdown-ansi": "2.10.3", - "@parcel/rust": "2.10.3", + "@parcel/codeframe": "2.13.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/logger": "2.13.3", + "@parcel/markdown-ansi": "2.13.3", + "@parcel/rust": "2.13.3", "@parcel/source-map": "^2.1.1", - "chalk": "^4.1.0", + "chalk": "^4.1.2", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", @@ -1793,11 +1864,12 @@ } }, "node_modules/@parcel/watcher": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.3.0.tgz", - "integrity": "sha512-pW7QaFiL11O0BphO+bq3MgqeX/INAk9jgBldVDYjlQPO4VddoZnF22TcF9onMhnLVHuNqBJeRf+Fj7eezi/+rQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "detect-libc": "^1.0.3", "is-glob": "^4.0.3", @@ -1812,28 +1884,30 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "@parcel/watcher-android-arm64": "2.3.0", - "@parcel/watcher-darwin-arm64": "2.3.0", - "@parcel/watcher-darwin-x64": "2.3.0", - "@parcel/watcher-freebsd-x64": "2.3.0", - "@parcel/watcher-linux-arm-glibc": "2.3.0", - "@parcel/watcher-linux-arm64-glibc": "2.3.0", - "@parcel/watcher-linux-arm64-musl": "2.3.0", - "@parcel/watcher-linux-x64-glibc": "2.3.0", - "@parcel/watcher-linux-x64-musl": "2.3.0", - "@parcel/watcher-win32-arm64": "2.3.0", - "@parcel/watcher-win32-ia32": "2.3.0", - "@parcel/watcher-win32-x64": "2.3.0" + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" } }, "node_modules/@parcel/watcher-android-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.3.0.tgz", - "integrity": "sha512-f4o9eA3dgk0XRT3XhB0UWpWpLnKgrh1IwNJKJ7UJek7eTYccQ8LR7XUWFKqw6aEq5KUNlCcGvSzKqSX/vtWVVA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -1847,13 +1921,14 @@ } }, "node_modules/@parcel/watcher-darwin-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.3.0.tgz", - "integrity": "sha512-mKY+oijI4ahBMc/GygVGvEdOq0L4DxhYgwQqYAz/7yPzuGi79oXrZG52WdpGA1wLBPrYb0T8uBaGFo7I6rvSKw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1867,13 +1942,14 @@ } }, "node_modules/@parcel/watcher-darwin-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.3.0.tgz", - "integrity": "sha512-20oBj8LcEOnLE3mgpy6zuOq8AplPu9NcSSSfyVKgfOhNAc4eF4ob3ldj0xWjGGbOF7Dcy1Tvm6ytvgdjlfUeow==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -1887,13 +1963,14 @@ } }, "node_modules/@parcel/watcher-freebsd-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.3.0.tgz", - "integrity": "sha512-7LftKlaHunueAEiojhCn+Ef2CTXWsLgTl4hq0pkhkTBFI3ssj2bJXmH2L67mKpiAD5dz66JYk4zS66qzdnIOgw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -1907,13 +1984,35 @@ } }, "node_modules/@parcel/watcher-linux-arm-glibc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.3.0.tgz", - "integrity": "sha512-1apPw5cD2xBv1XIHPUlq0cO6iAaEUQ3BcY0ysSyD9Kuyw4MoWm1DV+W9mneWI+1g6OeP6dhikiFE6BlU+AToTQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1927,13 +2026,14 @@ } }, "node_modules/@parcel/watcher-linux-arm64-glibc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.3.0.tgz", - "integrity": "sha512-mQ0gBSQEiq1k/MMkgcSB0Ic47UORZBmWoAWlMrTW6nbAGoLZP+h7AtUM7H3oDu34TBFFvjy4JCGP43JlylkTQA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1947,13 +2047,14 @@ } }, "node_modules/@parcel/watcher-linux-arm64-musl": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.3.0.tgz", - "integrity": "sha512-LXZAExpepJew0Gp8ZkJ+xDZaTQjLHv48h0p0Vw2VMFQ8A+RKrAvpFuPVCVwKJCr5SE+zvaG+Etg56qXvTDIedw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1967,13 +2068,14 @@ } }, "node_modules/@parcel/watcher-linux-x64-glibc": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.3.0.tgz", - "integrity": "sha512-P7Wo91lKSeSgMTtG7CnBS6WrA5otr1K7shhSjKHNePVmfBHDoAOHYRXgUmhiNfbcGk0uMCHVcdbfxtuiZCHVow==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -1987,13 +2089,14 @@ } }, "node_modules/@parcel/watcher-linux-x64-musl": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.3.0.tgz", - "integrity": "sha512-+kiRE1JIq8QdxzwoYY+wzBs9YbJ34guBweTK8nlzLKimn5EQ2b2FSC+tAOpq302BuIMjyuUGvBiUhEcLIGMQ5g==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -2007,13 +2110,14 @@ } }, "node_modules/@parcel/watcher-win32-arm64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.3.0.tgz", - "integrity": "sha512-35gXCnaz1AqIXpG42evcoP2+sNL62gZTMZne3IackM+6QlfMcJLy3DrjuL6Iks7Czpd3j4xRBzez3ADCj1l7Aw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2027,13 +2131,14 @@ } }, "node_modules/@parcel/watcher-win32-ia32": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.3.0.tgz", - "integrity": "sha512-FJS/IBQHhRpZ6PiCjFt1UAcPr0YmCLHRbTc00IBTrelEjlmmgIVLeOx4MSXzx2HFEy5Jo5YdhGpxCuqCyDJ5ow==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2047,13 +2152,14 @@ } }, "node_modules/@parcel/watcher-win32-x64": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.3.0.tgz", - "integrity": "sha512-dLx+0XRdMnVI62kU3wbXvbIRhLck4aE28bIGKbRGS7BJNt54IIj9+c/Dkqb+7DJEbHUZAX1bwaoM8PqVlHJmCA==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -2067,38 +2173,64 @@ } }, "node_modules/@parcel/workers": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.10.3.tgz", - "integrity": "sha512-qlN8G3VybPHVIbD6fsZr2gmrXG2UlROUQIPW/kkAvjQ29uRfFn7YEC8CHTICt8M1HhCNkr0cMXkuXQBi0l3kAg==", + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/workers/-/workers-2.13.3.tgz", + "integrity": "sha512-oAHmdniWTRwwwsKbcF4t3VjOtKN+/W17Wj5laiYB+HLkfsjGTfIQPj3sdXmrlBAGpI4omIcvR70PHHXnfdTfwA==", "dev": true, + "license": "MIT", "dependencies": { - "@parcel/diagnostic": "2.10.3", - "@parcel/logger": "2.10.3", - "@parcel/profiler": "2.10.3", - "@parcel/types": "2.10.3", - "@parcel/utils": "2.10.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/logger": "2.13.3", + "@parcel/profiler": "2.13.3", + "@parcel/types-internal": "2.13.3", + "@parcel/utils": "2.13.3", "nullthrows": "^1.1.1" }, "engines": { - "node": ">= 12.0.0" + "node": ">= 16.0.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/parcel" }, "peerDependencies": { - "@parcel/core": "^2.10.3" + "@parcel/core": "^2.13.3" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" } }, "node_modules/@swc/core": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.3.101.tgz", - "integrity": "sha512-w5aQ9qYsd/IYmXADAnkXPGDMTqkQalIi+kfFf/MHRKTpaOL7DHjMXwPp/n8hJ0qNjRvchzmPtOqtPBiER50d8A==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core/-/core-1.10.3.tgz", + "integrity": "sha512-2yjqCcsBx6SNBQZIYNlwxED9aYXW/7QBZyr8LYAxTx5bzmoNhKiClYbsNLe1NJ6ccf5uSbcInw12PjXLduNEdQ==", "dev": true, "hasInstallScript": true, + "license": "Apache-2.0", "dependencies": { - "@swc/counter": "^0.1.1", - "@swc/types": "^0.1.5" + "@swc/counter": "^0.1.3", + "@swc/types": "^0.1.17" }, "engines": { "node": ">=10" @@ -2108,19 +2240,19 @@ "url": "https://opencollective.com/swc" }, "optionalDependencies": { - "@swc/core-darwin-arm64": "1.3.101", - "@swc/core-darwin-x64": "1.3.101", - "@swc/core-linux-arm-gnueabihf": "1.3.101", - "@swc/core-linux-arm64-gnu": "1.3.101", - "@swc/core-linux-arm64-musl": "1.3.101", - "@swc/core-linux-x64-gnu": "1.3.101", - "@swc/core-linux-x64-musl": "1.3.101", - "@swc/core-win32-arm64-msvc": "1.3.101", - "@swc/core-win32-ia32-msvc": "1.3.101", - "@swc/core-win32-x64-msvc": "1.3.101" + "@swc/core-darwin-arm64": "1.10.3", + "@swc/core-darwin-x64": "1.10.3", + "@swc/core-linux-arm-gnueabihf": "1.10.3", + "@swc/core-linux-arm64-gnu": "1.10.3", + "@swc/core-linux-arm64-musl": "1.10.3", + "@swc/core-linux-x64-gnu": "1.10.3", + "@swc/core-linux-x64-musl": "1.10.3", + "@swc/core-win32-arm64-msvc": "1.10.3", + "@swc/core-win32-ia32-msvc": "1.10.3", + "@swc/core-win32-x64-msvc": "1.10.3" }, "peerDependencies": { - "@swc/helpers": "^0.5.0" + "@swc/helpers": "*" }, "peerDependenciesMeta": { "@swc/helpers": { @@ -2129,13 +2261,14 @@ } }, "node_modules/@swc/core-darwin-arm64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.3.101.tgz", - "integrity": "sha512-mNFK+uHNPRXSnfTOG34zJOeMl2waM4hF4a2NY7dkMXrPqw9CoJn4MwTXJcyMiSz1/BnNjjTCHF3Yhj0jPxmkzQ==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-arm64/-/core-darwin-arm64-1.10.3.tgz", + "integrity": "sha512-LFFCxAUKBy69AUE+01rgazQcafIXrYs6tBa9SyKPR51ft6Tp66dAVrWg9MTykaWskuXEe80LPUvUw1ga3bOH3A==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -2145,13 +2278,14 @@ } }, "node_modules/@swc/core-darwin-x64": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.3.101.tgz", - "integrity": "sha512-B085j8XOx73Fg15KsHvzYWG262bRweGr3JooO1aW5ec5pYbz5Ew9VS5JKYS03w2UBSxf2maWdbPz2UFAxg0whw==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-darwin-x64/-/core-darwin-x64-1.10.3.tgz", + "integrity": "sha512-yZNv1+yPg0GvYdThsMI8WpaPRAPuw2gQDMdgijLFfRcRlr2l1sTWsDHqGd7QMTx+acYM3uB537gyd31WjUAwlQ==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "darwin" @@ -2161,13 +2295,14 @@ } }, "node_modules/@swc/core-linux-arm-gnueabihf": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.3.101.tgz", - "integrity": "sha512-9xLKRb6zSzRGPqdz52Hy5GuB1lSjmLqa0lST6MTFads3apmx4Vgs8Y5NuGhx/h2I8QM4jXdLbpqQlifpzTlSSw==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.10.3.tgz", + "integrity": "sha512-Qa6hu5ASoKV4rcYUBGG3y3z+9UT042KAG4A7ivqqYQFcMfkB4NbZb5So2YWOpUc0/5YlSVkgL22h3Mbj5EXy7A==", "cpu": [ "arm" ], "dev": true, + "license": "Apache-2.0", "optional": true, "os": [ "linux" @@ -2177,13 +2312,14 @@ } }, "node_modules/@swc/core-linux-arm64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.3.101.tgz", - "integrity": "sha512-oE+r1lo7g/vs96Weh2R5l971dt+ZLuhaUX+n3BfDdPxNHfObXgKMjO7E+QS5RbGjv/AwiPCxQmbdCp/xN5ICJA==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.10.3.tgz", + "integrity": "sha512-BGnoZrmo0nlkXrOxVHk5U3j9u4BuquFviC+LvMe+HrDc5YLVe1gSXMUSBKhIz9MY9uFgxXW977TnB1XjLSKe5Q==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2193,13 +2329,14 @@ } }, "node_modules/@swc/core-linux-arm64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.3.101.tgz", - "integrity": "sha512-OGjYG3H4BMOTnJWJyBIovCez6KiHF30zMIu4+lGJTCrxRI2fAjGLml3PEXj8tC3FMcud7U2WUn6TdG0/te2k6g==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.10.3.tgz", + "integrity": "sha512-L07/4zKnIY2S/00bE+Yn3oEHkyGjWmGGE8Ta4luVCL+00s04EIwMoE1Hc8E8xFB5zLew5ViKFc5kNb5YZ/tRFQ==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2209,13 +2346,14 @@ } }, "node_modules/@swc/core-linux-x64-gnu": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.3.101.tgz", - "integrity": "sha512-/kBMcoF12PRO/lwa8Z7w4YyiKDcXQEiLvM+S3G9EvkoKYGgkkz4Q6PSNhF5rwg/E3+Hq5/9D2R+6nrkF287ihg==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.10.3.tgz", + "integrity": "sha512-cvTCekY4u0fBIDNfhv/2UxcOXqH4XJE2iNxKuQejS5KIapFJwrZ+fRQ2lha3+yopI/d2p96BlBEWTAcBzeTntw==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2225,13 +2363,14 @@ } }, "node_modules/@swc/core-linux-x64-musl": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.3.101.tgz", - "integrity": "sha512-kDN8lm4Eew0u1p+h1l3JzoeGgZPQ05qDE0czngnjmfpsH2sOZxVj1hdiCwS5lArpy7ktaLu5JdRnx70MkUzhXw==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.10.3.tgz", + "integrity": "sha512-h9kUOTrSSpY9JNc41a+NMAwK62USk/pvNE9Fi/Pfoklmlf9j9j8gRCitqvHpmZcEF4PPIsoMdiGetDipTwvWlw==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "linux" @@ -2241,13 +2380,14 @@ } }, "node_modules/@swc/core-win32-arm64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.3.101.tgz", - "integrity": "sha512-9Wn8TTLWwJKw63K/S+jjrZb9yoJfJwCE2RV5vPCCWmlMf3U1AXj5XuWOLUX+Rp2sGKau7wZKsvywhheWm+qndQ==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.10.3.tgz", + "integrity": "sha512-iHOmLYkZYn3r1Ff4rfyczdrYGt/wVIWyY0t8swsO9o1TE+zmucGFZuYZzgj3ng8Kp4sojJrydAGz8TINQZDBzQ==", "cpu": [ "arm64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2257,13 +2397,14 @@ } }, "node_modules/@swc/core-win32-ia32-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.3.101.tgz", - "integrity": "sha512-onO5KvICRVlu2xmr4//V2je9O2XgS1SGKpbX206KmmjcJhXN5EYLSxW9qgg+kgV5mip+sKTHTAu7IkzkAtElYA==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.10.3.tgz", + "integrity": "sha512-4SqLSE4Ozh8SxuVuHIZhkSyJQru5+WbQMRs5ggLRqeUy3vkUPHOAFAY3oMwDJUN6BwbAr8+664TmdrMwaWh8Ng==", "cpu": [ "ia32" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2273,13 +2414,14 @@ } }, "node_modules/@swc/core-win32-x64-msvc": { - "version": "1.3.101", - "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.3.101.tgz", - "integrity": "sha512-T3GeJtNQV00YmiVw/88/nxJ/H43CJvFnpvBHCVn17xbahiVUOPOduh3rc9LgAkKiNt/aV8vU3OJR+6PhfMR7UQ==", + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.10.3.tgz", + "integrity": "sha512-jTyf/IbNq7NVyqqDIEDzgjALjWu1IMfXKLXXAJArreklIMzkfHU1sV32ZJLOBmRKPyslCoalxIAU+hTx4reUTQ==", "cpu": [ "x64" ], "dev": true, + "license": "Apache-2.0 AND MIT", "optional": true, "os": [ "win32" @@ -2289,55 +2431,121 @@ } }, "node_modules/@swc/counter": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.2.tgz", - "integrity": "sha512-9F4ys4C74eSTEUNndnER3VJ15oru2NumfQxS8geE+f3eB5xvfxpWyqE5XlVnxb/R14uoXi6SLbBwwiDSkv+XEw==", - "dev": true + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "dev": true, + "license": "Apache-2.0" }, "node_modules/@swc/helpers": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.3.tgz", - "integrity": "sha512-FaruWX6KdudYloq1AHD/4nU+UsMTdNE8CKyrseXWEcgjDAbvkwJg2QGPAnfIJLIWsjZOSPLOAykK6fuYp4vp4A==", + "version": "0.5.15", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.15.tgz", + "integrity": "sha512-JQ5TuMi45Owi4/BIMAJBoSQoOJu12oOk/gADqlcUL9JEdHB8vyjUSsxqeNXnmXHjYKMi2WcYtezGEEhqUI/E2g==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "tslib": "^2.4.0" + "tslib": "^2.8.0" } }, "node_modules/@swc/types": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.5.tgz", - "integrity": "sha512-myfUej5naTBWnqOCc/MdVOLVjXUXtIA+NpDrDBKJtLLg2shUjBu3cZmB/85RyitKc55+lUUyl7oRfLOvkr2hsw==", - "dev": true + "version": "0.1.17", + "resolved": "https://registry.npmjs.org/@swc/types/-/types-0.1.17.tgz", + "integrity": "sha512-V5gRru+aD8YVyCOMAjMpWR1Ui577DD5KSJsHP8RAxopAH22jFz6GZd/qxqjO6MJHQhcsjvjOFXyDhyLQUnMveQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3" + } + }, + "node_modules/@szmarczak/http-timer": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz", + "integrity": "sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==", + "dev": true, + "license": "MIT", + "dependencies": { + "defer-to-connect": "^2.0.0" + }, + "engines": { + "node": ">=10" + } }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", "dev": true, + "license": "ISC", "engines": { "node": ">=10.13.0" } }, + "node_modules/@types/cacheable-request": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.3.tgz", + "integrity": "sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-cache-semantics": "*", + "@types/keyv": "^3.1.4", + "@types/node": "*", + "@types/responselike": "^1.0.0" + } + }, + "node_modules/@types/http-cache-semantics": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", + "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/keyv": { + "version": "3.1.4", + "resolved": "https://registry.npmjs.org/@types/keyv/-/keyv-3.1.4.tgz", + "integrity": "sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/responselike": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.3.tgz", + "integrity": "sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@xmldom/xmldom": { - "version": "0.7.13", - "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.13.tgz", - "integrity": "sha512-lm2GW5PkosIzccsaZIz7tp8cPADSIlIHWDFTR1N0SzfinhhYgeIQjFMz4rYzanCScr3DqQLeomUDArp6MWKm+g==", + "version": "0.9.6", + "resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.9.6.tgz", + "integrity": "sha512-Su4xcxR0CPGwlDHNmVP09fqET9YxbyDXHaSob6JlBH7L6reTYaeim6zbk9o08UarO0L5GTRo3uzl0D+9lSxmvw==", "dev": true, + "license": "MIT", "engines": { - "node": ">=10.0.0" + "node": ">=14.6" } }, - "node_modules/abortcontroller-polyfill": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/abortcontroller-polyfill/-/abortcontroller-polyfill-1.7.5.tgz", - "integrity": "sha512-JMJ5soJWP18htbbxJjG7bG6yuI6pRhgJ0scHHTfkUjf6wjP912xZWvM+A4sJK3gqd9E8fcPbDnOefbA9Th/FIQ==", - "dev": true - }, "node_modules/acorn": { - "version": "8.11.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.2.tgz", - "integrity": "sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==", + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", "dev": true, + "license": "MIT", "bin": { "acorn": "bin/acorn" }, @@ -2345,95 +2553,292 @@ "node": ">=0.4.0" } }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", "dev": true, + "license": "MIT", "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" }, "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, + "license": "MIT", "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "type-fest": "^0.21.3" }, "engines": { - "node": ">= 8" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "dev": true + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/asn1": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", + "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": "~2.1.0" + } + }, + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/at-least-node": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", + "integrity": "sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/aws4": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.13.2.tgz", + "integrity": "sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==", + "dev": true, + "license": "MIT" }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/base-x": { - "version": "3.0.9", - "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.9.tgz", - "integrity": "sha512-H7JU6iBHTal1gp56aKoaa//YUxEaAOUiydvrV/pILqIHXTtqxSkATOnDA2u+jZ/61sD+L/412+7kzXRtWukhpQ==", + "version": "3.0.10", + "resolved": "https://registry.npmjs.org/base-x/-/base-x-3.0.10.tgz", + "integrity": "sha512-7d0s06rR9rYaIWHkpfLIFICM/tkSVdoPC9qYAQRpxn9DdKNWNsKC0uk++akckyLq16Tx2WIinnZ6WRriAt6njQ==", "dev": true, + "license": "MIT", "dependencies": { "safe-buffer": "^5.0.1" } }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tweetnacl": "^0.14.3" + } + }, + "node_modules/binary": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/binary/-/binary-0.3.0.tgz", + "integrity": "sha512-D4H1y5KYwpJgK8wk1Cue5LLPgmwHKYSChkbspQg5JtVuR5ulGckxfR62H3AE9UDkdMC8yyXlqYihuz3Aqg2XZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffers": "~0.1.1", + "chainsaw": "~0.1.0" + }, + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", - "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/binwrap": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/binwrap/-/binwrap-0.2.3.tgz", + "integrity": "sha512-N4Pm7iyDEv0BrAMs+dny8WQa+e0nNTdzn2ODkf/MM6XBtKSCxCSUA1ZOQGoc1n7mUqdgOS5pwjsW91rmXVxy2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "request": "^2.88.0", + "tar": "^6.1.0", + "unzip-stream": "^0.3.1" + }, + "bin": { + "binwrap-install": "bin/binwrap-install", + "binwrap-prepare": "bin/binwrap-prepare", + "binwrap-test": "bin/binwrap-test" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" } }, "node_modules/boolbase": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, + "license": "MIT", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" } }, "node_modules/browserslist": { - "version": "4.22.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.2.tgz", - "integrity": "sha512-0UgcrvQmBDvZHFGdYUehrCNIazki7/lUP3kkoi/r3YB2amZbFM9J43ZRkJTXBUZK4gmx56+Sqk9+Vs9mwZx9+A==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -2449,11 +2854,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001565", - "electron-to-chromium": "^1.4.601", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.13" + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" }, "bin": { "browserslist": "cli.js" @@ -2462,25 +2868,90 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/buffers": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/buffers/-/buffers-0.1.1.tgz", + "integrity": "sha512-9q/rDEGSb/Qsvv2qvzIzdluL5k7AaJOTrw23z9reQthrbF7is4CtlT0DXyO1oei2DCp4uojjzQ7igaSHp1kAEQ==", + "dev": true, + "engines": { + "node": ">=0.2.0" + } + }, + "node_modules/cacheable-lookup": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-5.0.4.tgz", + "integrity": "sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.6.0" + } + }, + "node_modules/cacheable-request": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-7.0.4.tgz", + "integrity": "sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-response": "^1.0.2", + "get-stream": "^5.1.0", + "http-cache-semantics": "^4.0.0", + "keyv": "^4.0.0", + "lowercase-keys": "^2.0.0", + "normalize-url": "^6.0.1", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, "node_modules/caniuse-lite": { - "version": "1.0.30001570", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001570.tgz", - "integrity": "sha512-+3e0ASu4sw1SWaoCtvPeyXp+5PsjigkSt8OXZbF9StH5pQWbxEjLAZE3n8Aup5udop1uRiKA7a4utUk/uoSpUw==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -2495,13 +2966,35 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" + }, + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/chainsaw": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz", + "integrity": "sha512-75kWfWt6MEKNC8xYXIdRpDehRYY/tNSgwKaJq+dbbDcxORuVrrQ+SEHoWsniVn9XPYfP4gmdWIeDk/4YNp1rNQ==", + "dev": true, + "license": "MIT/X11", + "dependencies": { + "traverse": ">=0.3.0 <0.4" + }, + "engines": { + "node": "*" + } }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -2514,16 +3007,11 @@ } }, "node_modules/chokidar": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", - "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], + "license": "MIT", "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", @@ -2536,33 +3024,88 @@ "engines": { "node": ">= 8.10.0" }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, "optionalDependencies": { "fsevents": "~2.3.2" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/chrome-trace-event": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", - "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.0" } }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/clone": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.8" } }, + "node_modules/clone-response": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.3.tgz", + "integrity": "sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, + "license": "MIT", "dependencies": { "color-name": "~1.1.4" }, @@ -2574,19 +3117,35 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } }, "node_modules/command-exists": { "version": "1.2.9", "resolved": "https://registry.npmjs.org/command-exists/-/command-exists-1.2.9.tgz", "integrity": "sha512-LTQ/SGc+s0Xc0Fu5WaKnR0YiygZkm9eKFvyS+fRsU7/ZWFF8ykFM6Pc9aCVf1+xasOOZpO3BAVgVrKvsqKHV7w==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/commander": { "version": "9.5.0", "resolved": "https://registry.npmjs.org/commander/-/commander-9.5.0.tgz", "integrity": "sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==", "dev": true, + "license": "MIT", "engines": { "node": "^12.20.0 || >=14" } @@ -2595,18 +3154,27 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==", + "dev": true, + "license": "MIT" }, "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", "dev": true, + "license": "MIT", "dependencies": { + "env-paths": "^2.2.1", "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" + "parse-json": "^5.2.0" }, "engines": { "node": ">=14" @@ -2624,10 +3192,11 @@ } }, "node_modules/cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, + "license": "MIT", "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", @@ -2642,8 +3211,7 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, - "optional": true, - "peer": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -2655,89 +3223,26 @@ "url": "https://github.com/sponsors/fb55" } }, - "node_modules/css-select/node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "node_modules/css-select/node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/css-select/node_modules/domutils": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", - "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/css-select/node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", - "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">= 6" }, @@ -2750,8 +3255,7 @@ "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "css-tree": "~2.2.0" }, @@ -2765,8 +3269,7 @@ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "mdn-data": "2.0.28", "source-map-js": "^1.0.1" @@ -2781,14 +3284,117 @@ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "dev": true, - "optional": true, - "peer": true + "license": "CC0-1.0" + }, + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decompress-response": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", + "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-response": "^3.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/decompress-response/node_modules/mimic-response": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", + "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults/node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/defer-to-connect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", + "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } }, "node_modules/detect-libc": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", "dev": true, + "license": "Apache-2.0", "bin": { "detect-libc": "bin/detect-libc.js" }, @@ -2796,27 +3402,32 @@ "node": ">=0.10" } }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", "dev": true, + "license": "MIT", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" + "path-type": "^4.0.0" }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + "engines": { + "node": ">=8" } }, - "node_modules/dom-serializer/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, "node_modules/domelementtype": { @@ -2829,15 +3440,17 @@ "type": "github", "url": "https://github.com/sponsors/fb55" } - ] + ], + "license": "BSD-2-Clause" }, "node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "domelementtype": "^2.2.0" + "domelementtype": "^2.3.0" }, "engines": { "node": ">= 4" @@ -2847,39 +3460,73 @@ } }, "node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.1.tgz", + "integrity": "sha512-xWXmuRnN9OMP6ptPd2+H0cCbcYBULa5YDTbMm/2lvkWvNA3O4wcW+GvzooqBuNM8yy6pl3VIAeJTUUWUbfI5Fw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" }, "funding": { "url": "https://github.com/fb55/domutils?sponsor=1" } }, "node_modules/dotenv": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-7.0.0.tgz", - "integrity": "sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "dev": true, + "license": "BSD-2-Clause", "engines": { - "node": ">=6" + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" } }, "node_modules/dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-11.0.7.tgz", + "integrity": "sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } }, "node_modules/electron-to-chromium": { - "version": "1.4.615", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.615.tgz", - "integrity": "sha512-/bKPPcgZVUziECqDc+0HkT87+0zhaWSZHNXqF8FLd2lQcptpmUFwoCSWjCdOng9Gdq+afKArPdEg/0ZW461Eng==", - "dev": true + "version": "1.5.76", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.76.tgz", + "integrity": "sha512-CjVQyG7n7Sr+eBXE86HIulnL5N8xZY1sgmOPGuq/F0Rr0FJq63lg0kEtOIDfZBk44FnDLf6FUJ+dsJcuiUDdDQ==", + "dev": true, + "license": "ISC" }, "node_modules/elm": { "version": "0.19.1-6", @@ -2887,6 +3534,7 @@ "integrity": "sha512-mKYyierHICPdMx/vhiIacdPmTPnh889gjHOZ75ZAoCxo3lZmSWbGP8HMw78wyctJH0HwvTmeKhlYSWboQNYPeQ==", "dev": true, "hasInstallScript": true, + "license": "BSD-3-Clause", "bin": { "elm": "bin/elm" }, @@ -2900,23 +3548,97 @@ "@elm_binaries/win32_x64": "0.19.1-0" } }, + "node_modules/elm-format": { + "version": "0.8.7", + "resolved": "https://registry.npmjs.org/elm-format/-/elm-format-0.8.7.tgz", + "integrity": "sha512-sVzFXfWnb+6rzXK+q3e3Ccgr6/uS5mFbFk1VSmigC+x2XZ28QycAa7lS8owl009ALPhRQk+pZ95Eq5ANjpEZsQ==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "bin": { + "elm-format": "bin/elm-format" + }, + "optionalDependencies": { + "@avh4/elm-format-darwin-arm64": "0.8.7-2", + "@avh4/elm-format-darwin-x64": "0.8.7-2", + "@avh4/elm-format-linux-arm64": "0.8.7-2", + "@avh4/elm-format-linux-x64": "0.8.7-2", + "@avh4/elm-format-win32-x64": "0.8.7-2" + } + }, "node_modules/elm-hot": { "version": "1.1.6", "resolved": "https://registry.npmjs.org/elm-hot/-/elm-hot-1.1.6.tgz", "integrity": "sha512-zYZJlfs7Gt4BdjA+D+857K+XAWzwwySJmXCgFpHW1dIEfaHSZCIPYPf7/jinZBLfKRkOAlKzI32AA84DY50g7Q==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/elm-json": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/elm-json/-/elm-json-0.2.13.tgz", + "integrity": "sha512-KpmZIcWJbnGsUn4X1/OqGdPMWnV0kgtrK5thACwfnKHlOg3A2jMyMBWzBOJcycCpQVBC7XTVssClZGetsvaMBQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "binwrap": "^0.2.3" + }, + "bin": { + "elm-json": "bin/elm-json" + } + }, + "node_modules/elm-review": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/elm-review/-/elm-review-2.12.0.tgz", + "integrity": "sha512-so+G1hvCV85A63sQQzEhCNFNYyQVWDexXrz0TNEYg3/IIGHzN1bcRN+W4KJSvvFcmfEImzMSJ9AN20bvemU+4Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "chalk": "^4.0.0", + "chokidar": "^3.5.2", + "cross-spawn": "^7.0.3", + "elm-solve-deps-wasm": "^1.0.2", + "fastest-levenshtein": "^1.0.16", + "find-up": "^4.1.0", + "folder-hash": "^3.3.0", + "fs-extra": "^9.0.0", + "glob": "^10.2.6", + "globby": "^13.2.2", + "got": "^11.8.5", + "graceful-fs": "^4.2.11", + "minimist": "^1.2.6", + "ora": "^5.4.0", + "path-key": "^3.1.1", + "prompts": "^2.2.1", + "rimraf": "^5.0.0", + "strip-ansi": "^6.0.0", + "terminal-link": "^2.1.1", + "which": "^2.0.2", + "wrap-ansi": "^7.0.0" + }, + "bin": { + "elm-review": "bin/elm-review" + }, + "engines": { + "node": ">=10.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/jfmengels" + } }, "node_modules/elm-solve-deps-wasm": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/elm-solve-deps-wasm/-/elm-solve-deps-wasm-1.0.2.tgz", "integrity": "sha512-qnwo7RO9IO7jd9SLHvIy0rSOEIlc/tNMTE9Cras0kl+b161PVidW4FvXo0MtXU8GAKi/2s/HYvhcnpR/NNQ1zw==", - "dev": true + "dev": true, + "license": "MPL-2.0" }, "node_modules/elm-test": { "version": "0.19.1-revision9", "resolved": "https://registry.npmjs.org/elm-test/-/elm-test-0.19.1-revision9.tgz", "integrity": "sha512-lvHJbh16sy7oUBBPIEMN+0v6dGDZFmfvwq+V+TnGPk5ZlI64vcykx8E8+zLt/E+Ja42UKMMXXgN1ZHQ1V6Ic2Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "chalk": "^4.1.2", "chokidar": "^3.5.3", @@ -2936,50 +3658,184 @@ "node": ">=12.20.0" } }, - "node_modules/entities": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", - "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "node_modules/elm-test/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/elm-test/node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, "engines": { - "node": ">=0.12" + "node": ">=12" }, "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "node_modules/elm-test/node_modules/minimatch": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", + "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "license": "MIT", "dependencies": { "is-arrayish": "^0.2.1" } }, "node_modules/escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", "engines": { - "node": ">=0.8.0" + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, + "license": "MIT", "dependencies": { "to-regex-range": "^5.0.1" }, @@ -2992,6 +3848,7 @@ "resolved": "https://registry.npmjs.org/find-elm-dependencies/-/find-elm-dependencies-2.0.4.tgz", "integrity": "sha512-x/4w4fVmlD2X4PD9oQ+yh9EyaQef6OtEULdMGBTuWx0Nkppvo2Z/bAiQioW2n+GdRYKypME2b9OmYTw5tw5qDg==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "firstline": "^1.2.0", "lodash": "^4.17.19" @@ -3003,20 +3860,138 @@ "node": ">=4.0.0" } }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/firstline": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/firstline/-/firstline-1.3.1.tgz", "integrity": "sha512-ycwgqtoxujz1dm0kjkBFOPQMESxB9uKc/PlD951dQDIG+tBXRpYZC2UmJb0gDxopQ1ZX6oyRQN3goRczYu7Deg==", "dev": true, + "license": "MIT", "engines": { "node": ">=6.4.0" } }, + "node_modules/folder-hash": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/folder-hash/-/folder-hash-3.3.3.tgz", + "integrity": "sha512-SDgHBgV+RCjrYs8aUwCb9rTgbTVuSdzvFmLaChsLre1yf+D64khCW++VYciaByZ8Rm0uKF8R/XEpXuTRSGUM1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "graceful-fs": "~4.2.0", + "minimatch": "~3.0.4" + }, + "bin": { + "folder-hash": "bin/folder-hash" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/fs-extra": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz", + "integrity": "sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "at-least-node": "^1.0.0", + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/fsevents": { "version": "2.3.3", @@ -3024,6 +3999,7 @@ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -3037,24 +4013,53 @@ "resolved": "https://registry.npmjs.org/get-port/-/get-port-4.2.0.tgz", "integrity": "sha512-/b3jarXkH8KJoOMQc3uVGHASwGLPq3gSFJ7tgJm2diza+bydJPTGOibin2steecKeOylE8oY2JERlVWkAJO6yw==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } }, - "node_modules/glob": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", - "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", "dev": true, + "license": "MIT", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^5.0.1", - "once": "^1.3.0" + "pump": "^3.0.0" }, "engines": { - "node": ">=12" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -3065,6 +4070,7 @@ "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", "dev": true, + "license": "ISC", "dependencies": { "is-glob": "^4.0.1" }, @@ -3072,11 +4078,38 @@ "node": ">= 6" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, + "license": "MIT", "dependencies": { "type-fest": "^0.20.2" }, @@ -3087,37 +4120,111 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globby": { + "version": "13.2.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", + "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "dir-glob": "^3.0.1", + "fast-glob": "^3.3.0", + "ignore": "^5.2.4", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/got": { + "version": "11.8.6", + "resolved": "https://registry.npmjs.org/got/-/got-11.8.6.tgz", + "integrity": "sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.0.0", + "@szmarczak/http-timer": "^4.0.5", + "@types/cacheable-request": "^6.0.1", + "@types/responselike": "^1.0.0", + "cacheable-lookup": "^5.0.3", + "cacheable-request": "^7.0.2", + "decompress-response": "^6.0.0", + "http2-wrapper": "^1.0.0-beta.5.2", + "lowercase-keys": "^2.0.0", + "p-cancelable": "^2.0.0", + "responselike": "^2.0.0" + }, + "engines": { + "node": ">=10.19.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/got?sponsor=1" + } + }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } }, "node_modules/has-flag": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, "node_modules/htmlnano": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.0.tgz", - "integrity": "sha512-jVGRE0Ep9byMBKEu0Vxgl8dhXYOUk0iNQ2pjsG+BcRB0u0oDF5A9p/iBGMg/PGKYUyMD0OAGu8dVT5Lzj8S58g==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/htmlnano/-/htmlnano-2.1.1.tgz", + "integrity": "sha512-kAERyg/LuNZYmdqgCdYvugyLWNFAm8MWXpQMz1pLpetmCbFwoMxvkSoaAMlFrOC4OKTWI4KlZGT/RsNxg4ghOw==", "dev": true, + "license": "MIT", "dependencies": { - "cosmiconfig": "^8.0.0", + "cosmiconfig": "^9.0.0", "posthtml": "^0.16.5", "timsort": "^0.3.0" }, "peerDependencies": { - "cssnano": "^6.0.0", + "cssnano": "^7.0.0", "postcss": "^8.3.11", - "purgecss": "^5.0.0", + "purgecss": "^6.0.0", "relateurl": "^0.2.7", - "srcset": "4.0.0", + "srcset": "5.0.1", "svgo": "^3.0.2", "terser": "^5.10.0", "uncss": "^0.17.3" @@ -3150,9 +4257,9 @@ } }, "node_modules/htmlparser2": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", - "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -3161,11 +4268,80 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.2", - "domutils": "^2.8.0", - "entities": "^3.0.1" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/http2-wrapper": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz", + "integrity": "sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "quick-lru": "^5.1.1", + "resolve-alpn": "^1.0.0" + }, + "engines": { + "node": ">=10.19.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" } }, "node_modules/import-fresh": { @@ -3173,6 +4349,7 @@ "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", "dev": true, + "license": "MIT", "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" @@ -3188,7 +4365,9 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", "dev": true, + "license": "ISC", "dependencies": { "once": "^1.3.0", "wrappy": "1" @@ -3198,19 +4377,22 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/is-arrayish": { "version": "0.2.1", "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", "dev": true, + "license": "MIT", "dependencies": { "binary-extensions": "^2.0.0" }, @@ -3223,15 +4405,27 @@ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", "dev": true, + "license": "MIT", "dependencies": { "is-extglob": "^2.1.1" }, @@ -3239,38 +4433,96 @@ "node": ">=0.10.0" } }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/is-json": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-json/-/is-json-2.0.1.tgz", "integrity": "sha512-6BEnpVn1rcf3ngfmViLM6vjUjGErbdrL4rwlv+u1NO1XO8kqT4YGL8+19Q+Z/bas8tY90BTWMk2+fW1g6hQjbA==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.12.0" } }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "dev": true, + "license": "ISC" + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/js-yaml": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", "dev": true, + "license": "MIT", "dependencies": { "argparse": "^2.0.1" }, @@ -3278,17 +4530,54 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true, + "license": "(AFL-2.1 OR BSD-3-Clause)" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true, + "license": "ISC" }, "node_modules/json5": { "version": "2.2.3", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", "dev": true, + "license": "MIT", "bin": { "json5": "lib/cli.js" }, @@ -3296,11 +4585,61 @@ "node": ">=6" } }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/lightningcss": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.22.1.tgz", - "integrity": "sha512-Fy45PhibiNXkm0cK5FJCbfO8Y6jUpD/YcHf/BtuI+jvYYqSXKF4muk61jjE8YxCR9y+hDYIWSzHTc+bwhDE6rQ==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.28.2.tgz", + "integrity": "sha512-ePLRrbt3fgjXI5VFZOLbvkLD5ZRuxGKm+wJ3ujCqBtL3NanDHPo/5zicR5uEKAPiIjBYF99BM4K4okvMznjkVA==", "dev": true, + "license": "MPL-2.0", "dependencies": { "detect-libc": "^1.0.3" }, @@ -3312,25 +4651,27 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-darwin-arm64": "1.22.1", - "lightningcss-darwin-x64": "1.22.1", - "lightningcss-freebsd-x64": "1.22.1", - "lightningcss-linux-arm-gnueabihf": "1.22.1", - "lightningcss-linux-arm64-gnu": "1.22.1", - "lightningcss-linux-arm64-musl": "1.22.1", - "lightningcss-linux-x64-gnu": "1.22.1", - "lightningcss-linux-x64-musl": "1.22.1", - "lightningcss-win32-x64-msvc": "1.22.1" + "lightningcss-darwin-arm64": "1.28.2", + "lightningcss-darwin-x64": "1.28.2", + "lightningcss-freebsd-x64": "1.28.2", + "lightningcss-linux-arm-gnueabihf": "1.28.2", + "lightningcss-linux-arm64-gnu": "1.28.2", + "lightningcss-linux-arm64-musl": "1.28.2", + "lightningcss-linux-x64-gnu": "1.28.2", + "lightningcss-linux-x64-musl": "1.28.2", + "lightningcss-win32-arm64-msvc": "1.28.2", + "lightningcss-win32-x64-msvc": "1.28.2" } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.22.1.tgz", - "integrity": "sha512-ldvElu+R0QimNTjsKpaZkUv3zf+uefzLy/R1R19jtgOfSRM+zjUCUgDhfEDRmVqJtMwYsdhMI2aJtJChPC6Osg==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.28.2.tgz", + "integrity": "sha512-/8cPSqZiusHSS+WQz0W4NuaqFjquys1x+NsdN/XOHb+idGHJSoJ7SoQTVl3DZuAgtPZwFZgRfb/vd1oi8uX6+g==", "cpu": [ "arm64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "darwin" @@ -3344,13 +4685,14 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.22.1.tgz", - "integrity": "sha512-5p2rnlVTv6Gpw4PlTLq925nTVh+HFh4MpegX8dPDYJae+NFVjQ67gY7O6iHIzQjLipDiYejFF0yHrhjU3XgLBQ==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.28.2.tgz", + "integrity": "sha512-R7sFrXlgKjvoEG8umpVt/yutjxOL0z8KWf0bfPT3cYMOW4470xu5qSHpFdIOpRWwl3FKNMUdbKtMUjYt0h2j4g==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "darwin" @@ -3364,13 +4706,14 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.22.1.tgz", - "integrity": "sha512-1FaBtcFrZqB2hkFbAxY//Pnp8koThvyB6AhjbdVqKD4/pu13Rl91fKt2N9qyeQPUt3xy7ORUvSO+dPk3J6EjXg==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.28.2.tgz", + "integrity": "sha512-l2qrCT+x7crAY+lMIxtgvV10R8VurzHAoUZJaVFSlHrN8kRLTvEg9ObojIDIexqWJQvJcVVV3vfzsEynpiuvgA==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "freebsd" @@ -3384,13 +4727,14 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.22.1.tgz", - "integrity": "sha512-6rub98tYGfE5I5j0BP8t/2d4BZyu1S7Iz9vUkm0H26snAFHYxLfj3RbQn0xHHIePSetjLnhcg3QlfwUAkD/FYg==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.28.2.tgz", + "integrity": "sha512-DKMzpICBEKnL53X14rF7hFDu8KKALUJtcKdFUCW5YOlGSiwRSgVoRjM97wUm/E0NMPkzrTi/rxfvt7ruNK8meg==", "cpu": [ "arm" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -3404,13 +4748,14 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.22.1.tgz", - "integrity": "sha512-nYO5qGtb/1kkTZu3FeTiM+2B2TAb7m2DkLCTgQIs2bk2o9aEs7I96fwySKcoHWQAiQDGR9sMux9vkV4KQXqPaQ==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.28.2.tgz", + "integrity": "sha512-nhfjYkfymWZSxdtTNMWyhFk2ImUm0X7NAgJWFwnsYPOfmtWQEapzG/DXZTfEfMjSzERNUNJoQjPAbdqgB+sjiw==", "cpu": [ "arm64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -3424,13 +4769,14 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.22.1.tgz", - "integrity": "sha512-MCV6RuRpzXbunvzwY644iz8cw4oQxvW7oer9xPkdadYqlEyiJJ6wl7FyJOH7Q6ZYH4yjGAUCvxDBxPbnDu9ZVg==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.28.2.tgz", + "integrity": "sha512-1SPG1ZTNnphWvAv8RVOymlZ8BDtAg69Hbo7n4QxARvkFVCJAt0cgjAw1Fox0WEhf4PwnyoOBaVH0Z5YNgzt4dA==", "cpu": [ "arm64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -3444,13 +4790,14 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.22.1.tgz", - "integrity": "sha512-RjNgpdM20VUXgV7us/VmlO3Vn2ZRiDnc3/bUxCVvySZWPiVPprpqW/QDWuzkGa+NCUf6saAM5CLsZLSxncXJwg==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.28.2.tgz", + "integrity": "sha512-ZhQy0FcO//INWUdo/iEdbefntTdpPVQ0XJwwtdbBuMQe+uxqZoytm9M+iqR9O5noWFaxK+nbS2iR/I80Q2Ofpg==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -3464,13 +4811,14 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.22.1.tgz", - "integrity": "sha512-ZgO4C7Rd6Hv/5MnyY2KxOYmIlzk4rplVolDt3NbkNR8DndnyX0Q5IR4acJWNTBICQ21j3zySzKbcJaiJpk/4YA==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.28.2.tgz", + "integrity": "sha512-alb/j1NMrgQmSFyzTbN1/pvMPM+gdDw7YBuQ5VSgcFDypN3Ah0BzC2dTZbzwzaMdUVDszX6zH5MzjfVN1oGuww==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "linux" @@ -3483,14 +4831,36 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.28.2.tgz", + "integrity": "sha512-WnwcjcBeAt0jGdjlgbT9ANf30pF0C/QMb1XnLnH272DQU8QXh+kmpi24R55wmWBwaTtNAETZ+m35ohyeMiNt+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.22.1", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.22.1.tgz", - "integrity": "sha512-4pozV4eyD0MDET41ZLHAeBo+H04Nm2UEYIk5w/ts40231dRFV7E0cjwbnZvSoc1DXFgecAhiC0L16ruv/ZDCpg==", + "version": "1.28.2", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.28.2.tgz", + "integrity": "sha512-3piBifyT3avz22o6mDKywQC/OisH2yDK+caHWkiMsF82i3m5wDBadyCjlCQ5VNgzYkxrWZgiaxHDdd5uxsi0/A==", "cpu": [ "x64" ], "dev": true, + "license": "MPL-2.0", "optional": true, "os": [ "win32" @@ -3507,7 +4877,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lmdb": { "version": "2.8.5", @@ -3515,6 +4886,7 @@ "integrity": "sha512-9bMdFfc80S+vSldBmG3HOuLVHnxRdNTlpzR6QDnzqCQtCzGUEAGTzBKYMeIM+I/sU4oZfgbcbS7X7F65/z/oxQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "dependencies": { "msgpackr": "^1.9.5", "node-addon-api": "^6.1.0", @@ -3538,57 +4910,148 @@ "version": "6.1.0", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "dev": true, + "license": "MIT" }, - "node_modules/lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", "dev": true, + "license": "MIT", "dependencies": { - "yallist": "^4.0.0" + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" }, "engines": { "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lowercase-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-2.0.0.tgz", + "integrity": "sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, + "node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/mdn-data": { "version": "2.0.30", "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true, - "optional": true, - "peer": true + "license": "CC0-1.0" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } }, "node_modules/micromatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz", - "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==", + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, + "license": "MIT", "dependencies": { - "braces": "^3.0.2", + "braces": "^3.0.3", "picomatch": "^2.3.1" }, "engines": { "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/minimatch": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.6.tgz", - "integrity": "sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g==", + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", "dev": true, + "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^1.1.7" }, "engines": { - "node": ">=10" + "node": "*" } }, "node_modules/minimist": { @@ -3596,59 +5059,122 @@ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", "dev": true, + "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/mkdirp": { - "version": "0.5.6", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", - "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", "dependencies": { - "minimist": "^1.2.6" + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" } }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/msgpackr": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.10.0.tgz", - "integrity": "sha512-rVQ5YAQDoZKZLX+h8tNq7FiHrPJoeGHViz3U4wIcykhAEpwF/nH2Vbk8dQxmpX5JavkI8C7pt4bnkJ02ZmRoUw==", + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", "dev": true, + "license": "MIT", "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "node_modules/msgpackr-extract": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.2.tgz", - "integrity": "sha512-SdzXp4kD/Qf8agZ9+iTu6eql0m3kWm1A2y1hkpTeVNENutaB0BwHlSvAIaMxwntmRUAUjon2V4L8Z/njd0Ct8A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { - "node-gyp-build-optional-packages": "5.0.7" + "node-gyp-build-optional-packages": "5.2.2" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.2", - "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.2", - "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.2" + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/msgpackr-extract/node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" } }, "node_modules/msgpackr-extract/node_modules/node-gyp-build-optional-packages": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.0.7.tgz", - "integrity": "sha512-YlCCc6Wffkx0kHkmam79GKvDQ6x+QZkMjFGrIMxgFNILFvGSbCp2fCBC55pGTT9gVaz8Na5CLmxt/urtzRv36w==", + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "dev": true, + "license": "MIT", "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", @@ -3659,19 +5185,22 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/node-addon-api": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.0.0.tgz", - "integrity": "sha512-vgbBJTS4m5/KkE16t5Ly0WW9hz46swAstv0hYYwMtbG7AznRhNyfLRe8HZAiWIpcHzoO7HxhLuBQj9rJ/Ho0ZA==", - "dev": true + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT" }, "node_modules/node-elm-compiler": { "version": "5.0.6", "resolved": "https://registry.npmjs.org/node-elm-compiler/-/node-elm-compiler-5.0.6.tgz", "integrity": "sha512-DWTRQR8b54rvschcZRREdsz7K84lnS8A6YJu8du3QLQ8f204SJbyTaA6NzYYbfUG97OTRKRv/0KZl82cTfpLhA==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "cross-spawn": "6.0.5", "find-elm-dependencies": "^2.0.4", @@ -3687,6 +5216,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", "dev": true, + "license": "MIT", "dependencies": { "nice-try": "^1.0.4", "path-key": "^2.0.1", @@ -3703,6 +5233,7 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } @@ -3712,6 +5243,7 @@ "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver" } @@ -3721,6 +5253,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^1.0.0" }, @@ -3733,6 +5266,7 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3742,6 +5276,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -3754,6 +5289,7 @@ "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.1.1.tgz", "integrity": "sha512-+P72GAjVAbTxjjwUmwjVrqrdZROD4nf8KgpBoDxqXXTiYZZt/ud60dE5yvCSr9lRO8e8yv6kgJIC0K0PfZFVQw==", "dev": true, + "license": "MIT", "dependencies": { "detect-libc": "^2.0.1" }, @@ -3764,34 +5300,51 @@ } }, "node_modules/node-gyp-build-optional-packages/node_modules/detect-libc": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.2.tgz", - "integrity": "sha512-UX6sGumvvqSaXgdKGUsgZWqcUyIXZ/vZTrlRT/iobiKhGL0zL4d3osHj3uqllWJK+i+sixDS/3COVEOFbupFyw==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "dev": true, + "license": "Apache-2.0", "engines": { "node": ">=8" } }, "node_modules/node-releases": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", - "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", - "dev": true + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/normalize-url": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-6.1.0.tgz", + "integrity": "sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0" }, @@ -3803,80 +5356,270 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/nullthrows/-/nullthrows-1.1.1.tgz", "integrity": "sha512-2vPPEi+Z7WqML2jZYddDIfy5Dqb0r2fze2zTxNNknZaFpVHU3mFB3R+DWeJWGVx0ecvttSGlJTI+WG+8Z4cDWw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "*" + } }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", "dev": true, + "license": "ISC", "dependencies": { "wrappy": "1" } }, - "node_modules/ordered-binary": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.1.tgz", - "integrity": "sha512-5VyHfHY3cd0iza71JepYG50My+YUbrFtGoUz2ooEydPyPM7Aai/JW098juLr+RG6+rDJuzNNTsEQu2DZa1A41A==", - "dev": true - }, - "node_modules/parcel": { - "version": "2.10.3", - "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.10.3.tgz", - "integrity": "sha512-Ocx33N4ZVnotJTALhMZ0AqPIE9UN5uP6jjA+lYJ4FlEYuYYZsvOQXZQgeMa62pFj6jrOHWh7ho8uJhRdTNwVyg==", - "dev": true, - "dependencies": { - "@parcel/config-default": "2.10.3", - "@parcel/core": "2.10.3", - "@parcel/diagnostic": "2.10.3", - "@parcel/events": "2.10.3", - "@parcel/fs": "2.10.3", - "@parcel/logger": "2.10.3", - "@parcel/package-manager": "2.10.3", - "@parcel/reporter-cli": "2.10.3", - "@parcel/reporter-dev-server": "2.10.3", - "@parcel/reporter-tracer": "2.10.3", - "@parcel/utils": "2.10.3", - "chalk": "^4.1.0", - "commander": "^7.0.0", - "get-port": "^4.2.0" - }, - "bin": { - "parcel": "lib/bin.js" + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" }, "engines": { - "node": ">= 12.0.0" + "node": ">=6" }, "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parcel-plugin-browserconfig": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/parcel-plugin-browserconfig/-/parcel-plugin-browserconfig-1.0.5.tgz", - "integrity": "sha512-D3JNbFZdKcRhuXmH8BC8GayRBAZ1PKMDtntNbTitcixdkexERmg6BgageRKMGrrzBN/ShrMhljE9BfRvhUS/yQ==", + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", "dev": true, + "license": "MIT", "dependencies": { - "xml2js": "^0.4.23" + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/parcel/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-cancelable": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-2.1.1.tgz", + "integrity": "sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==", "dev": true, + "license": "MIT", "engines": { - "node": ">= 10" + "node": ">=8" } }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", "dev": true, + "license": "MIT", "dependencies": { - "callsites": "^3.0.0" + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parcel": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/parcel/-/parcel-2.13.3.tgz", + "integrity": "sha512-8GrC8C7J8mwRpAlk7EJ7lwdFTbCN+dcXH2gy5AsEs9pLfzo9wvxOTx6W0fzSlvCOvZOita+8GdfYlGfEt0tRgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/config-default": "2.13.3", + "@parcel/core": "2.13.3", + "@parcel/diagnostic": "2.13.3", + "@parcel/events": "2.13.3", + "@parcel/feature-flags": "2.13.3", + "@parcel/fs": "2.13.3", + "@parcel/logger": "2.13.3", + "@parcel/package-manager": "2.13.3", + "@parcel/reporter-cli": "2.13.3", + "@parcel/reporter-dev-server": "2.13.3", + "@parcel/reporter-tracer": "2.13.3", + "@parcel/utils": "2.13.3", + "chalk": "^4.1.2", + "commander": "^12.1.0", + "get-port": "^4.2.0" + }, + "bin": { + "parcel": "lib/bin.js" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/parcel-plugin-browserconfig": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/parcel-plugin-browserconfig/-/parcel-plugin-browserconfig-1.0.5.tgz", + "integrity": "sha512-D3JNbFZdKcRhuXmH8BC8GayRBAZ1PKMDtntNbTitcixdkexERmg6BgageRKMGrrzBN/ShrMhljE9BfRvhUS/yQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "xml2js": "^0.4.23" + } + }, + "node_modules/parcel/node_modules/@parcel/fs": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/fs/-/fs-2.13.3.tgz", + "integrity": "sha512-+MPWAt0zr+TCDSlj1LvkORTjfB/BSffsE99A9AvScKytDSYYpY2s0t4vtV9unSh0FHMS2aBCZNJ4t7KL+DcPIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/feature-flags": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/types-internal": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/watcher": "^2.0.7", + "@parcel/workers": "2.13.3" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.3" + } + }, + "node_modules/parcel/node_modules/@parcel/node-resolver-core": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/@parcel/node-resolver-core/-/node-resolver-core-3.4.3.tgz", + "integrity": "sha512-IEnMks49egEic1ITBp59VQyHzkSQUXqpU9hOHwqN3KoSTdZ6rEgrXcS3pa6tdXay4NYGlcZ88kFCE8i/xYoVCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mischnic/json-sourcemap": "^0.1.0", + "@parcel/diagnostic": "2.13.3", + "@parcel/fs": "2.13.3", + "@parcel/rust": "2.13.3", + "@parcel/utils": "2.13.3", + "nullthrows": "^1.1.1", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/parcel/node_modules/@parcel/package-manager": { + "version": "2.13.3", + "resolved": "https://registry.npmjs.org/@parcel/package-manager/-/package-manager-2.13.3.tgz", + "integrity": "sha512-FLNI5OrZxymGf/Yln0E/kjnGn5sdkQAxW7pQVdtuM+5VeN75yibJRjsSGv88PvJ+KvpD2ANgiIJo1RufmoPcww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@parcel/diagnostic": "2.13.3", + "@parcel/fs": "2.13.3", + "@parcel/logger": "2.13.3", + "@parcel/node-resolver-core": "3.4.3", + "@parcel/types": "2.13.3", + "@parcel/utils": "2.13.3", + "@parcel/workers": "2.13.3", + "@swc/core": "^1.7.26", + "semver": "^7.5.2" + }, + "engines": { + "node": ">= 16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "peerDependencies": { + "@parcel/core": "^2.13.3" + } + }, + "node_modules/parcel/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" }, "engines": { "node": ">=6" @@ -3887,6 +5630,7 @@ "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", "dev": true, + "license": "MIT", "dependencies": { "@babel/code-frame": "^7.0.0", "error-ex": "^1.3.1", @@ -3900,11 +5644,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/path-is-absolute": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -3914,30 +5669,58 @@ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", "dev": true, + "license": "MIT", "engines": { "node": ">=8" } }, + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", - "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==", - "dev": true + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, "node_modules/picomatch": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.6" }, @@ -3949,13 +5732,15 @@ "version": "4.2.0", "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/posthtml": { "version": "0.16.6", "resolved": "https://registry.npmjs.org/posthtml/-/posthtml-0.16.6.tgz", "integrity": "sha512-JcEmHlyLK/o0uGAlj65vgg+7LIms0xKXe60lcDOTU7oVX/3LuEuLwrQpW3VJ7de5TaFKiW4kWkaIpJL42FEgxQ==", "dev": true, + "license": "MIT", "dependencies": { "posthtml-parser": "^0.11.0", "posthtml-render": "^3.0.0" @@ -3965,15 +5750,16 @@ } }, "node_modules/posthtml-parser": { - "version": "0.10.2", - "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.10.2.tgz", - "integrity": "sha512-PId6zZ/2lyJi9LiKfe+i2xv57oEjJgWbsHGGANwos5AvdQp98i6AtamAl8gzSVFGfQ43Glb5D614cvZf012VKg==", + "version": "0.12.1", + "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.12.1.tgz", + "integrity": "sha512-rYFmsDLfYm+4Ts2Oh4DCDSZPtdC1BLnRXAobypVzX9alj28KGl65dIFtgDY9zB57D0TC4Qxqrawuq/2et1P0GA==", "dev": true, + "license": "MIT", "dependencies": { - "htmlparser2": "^7.1.1" + "htmlparser2": "^9.0.0" }, "engines": { - "node": ">=12" + "node": ">=16" } }, "node_modules/posthtml-render": { @@ -3981,6 +5767,7 @@ "resolved": "https://registry.npmjs.org/posthtml-render/-/posthtml-render-3.0.0.tgz", "integrity": "sha512-z+16RoxK3fUPgwaIgH9NGnK1HKY9XIDpydky5eQGgAFVXTCSezalv9U2jQuNV+Z9qV1fDWNzldcw4eK0SSbqKA==", "dev": true, + "license": "MIT", "dependencies": { "is-json": "^2.0.1" }, @@ -3988,11 +5775,101 @@ "node": ">=12" } }, + "node_modules/posthtml/node_modules/dom-serializer": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", + "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/posthtml/node_modules/dom-serializer/node_modules/entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", + "dev": true, + "license": "BSD-2-Clause", + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/posthtml/node_modules/domhandler": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", + "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.2.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/posthtml/node_modules/domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/posthtml/node_modules/entities": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/posthtml/node_modules/htmlparser2": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-7.2.0.tgz", + "integrity": "sha512-H7MImA4MS6cw7nbyURtLPO1Tms7C5H602LRETv95z1MxO/7CP7rDVROehUYeYBUYEON94NXXDEPmZuq+hX4sog==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.2", + "domutils": "^2.8.0", + "entities": "^3.0.1" + } + }, "node_modules/posthtml/node_modules/posthtml-parser": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/posthtml-parser/-/posthtml-parser-0.11.0.tgz", "integrity": "sha512-QecJtfLekJbWVo/dMAA+OSwY79wpRmbqS5TeXvXSX+f0c6pW4/SE6inzZ2qkU7oAMCPqIDkZDvd/bQsSFUnKyw==", "dev": true, + "license": "MIT", "dependencies": { "htmlparser2": "^7.1.1" }, @@ -4000,26 +5877,136 @@ "node": ">=12" } }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/psl": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" + } + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", + "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/quick-lru": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", + "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/react-error-overlay": { "version": "6.0.9", "resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz", "integrity": "sha512-nQTTcUu+ATDbrSD1BZHr5kgSD4oF8OFjxun8uAaL8RwPBacGBNPf/yAuVVdx17N8XNzRDMrZ9XcKZHCjPW+9ew==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/react-refresh": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.9.0.tgz", - "integrity": "sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ==", + "version": "0.14.2", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.2.tgz", + "integrity": "sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==", "dev": true, + "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, + "license": "MIT", "dependencies": { "picomatch": "^2.2.1" }, @@ -4030,75 +6017,149 @@ "node_modules/reconnecting-websocket": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/reconnecting-websocket/-/reconnecting-websocket-4.4.0.tgz", - "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==" + "integrity": "sha512-D2E33ceRPga0NvTDhJmphEgJ7FUYF0v4lr1ki0csq06OdlxKfugGzN0dSkxM/NfqCxYELK4KcaTOUOjTV6Dcng==", + "license": "MIT" }, "node_modules/regenerator-runtime": { - "version": "0.13.11", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", - "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==", - "dev": true + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "license": "MIT" + }, + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/resolve-alpn": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", + "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", + "dev": true, + "license": "MIT" }, "node_modules/resolve-from": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, + "license": "MIT", "engines": { "node": ">=4" } }, - "node_modules/rimraf": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", - "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "node_modules/responselike": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-2.0.1.tgz", + "integrity": "sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==", "dev": true, + "license": "MIT", "dependencies": { - "glob": "^7.1.3" + "lowercase-keys": "^2.0.0" }, - "bin": { - "rimraf": "bin.js" + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", "dev": true, + "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" } }, - "node_modules/rimraf/node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true, + "license": "ISC" + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "license": "ISC", "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "glob": "^10.3.7" }, - "engines": { - "node": "*" + "bin": { + "rimraf": "dist/esm/bin.mjs" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" + "queue-microtask": "^1.2.2" } }, "node_modules/safe-buffer": { @@ -4119,22 +6180,29 @@ "type": "consulting", "url": "https://feross.org/support" } - ] + ], + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" }, "node_modules/sax": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.3.0.tgz", - "integrity": "sha512-0s+oAmw9zLl1V1cS9BtZN7JAd0cW5e0QH4W3LWEK6a4LaLEA2OTpGYWDY+6XasBLtz6wkm3u1xRw95mRuJ59WA==", - "dev": true + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "license": "ISC" }, "node_modules/semver": { - "version": "7.5.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.5.4.tgz", - "integrity": "sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==", + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, + "license": "ISC", "bin": { "semver": "bin/semver.js" }, @@ -4147,6 +6215,7 @@ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, + "license": "MIT", "dependencies": { "shebang-regex": "^3.0.0" }, @@ -4154,81 +6223,247 @@ "node": ">=8" } }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/split": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", + "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "through": "2" + }, + "engines": { + "node": "*" + } + }, + "node_modules/srcset": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/srcset/-/srcset-5.0.1.tgz", + "integrity": "sha512-/P1UYbGfJVlxZag7aABNRrulEXAwCSDo7fklafOQrantuPTDmYgijJMks2zusPCVzgW9+4P69mq7w6pYuZpgxw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/sshpk": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", + "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, "engines": { "node": ">=8" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", "dev": true, - "engines": { - "node": ">=0.10.0" - } + "license": "MIT" }, - "node_modules/source-map-js": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz", - "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==", + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "engines": { - "node": ">=0.10.0" + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, - "node_modules/split": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/split/-/split-1.0.1.tgz", - "integrity": "sha512-mTyOoPbrivtXnwnIxZRFYRrPNtEFKlpB2fvjSnCQUiAA6qAZzqwna5envK4uk6OIeP17CsdF3rSBGYVBsU0Tkg==", + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, + "license": "MIT", "dependencies": { - "through": "2" + "ansi-regex": "^5.0.1" }, "engines": { - "node": "*" + "node": ">=8" } }, - "node_modules/srcset": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", - "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "engines": { + "node": ">=8" } }, - "node_modules/stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", - "dev": true - }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, + "license": "MIT", "dependencies": { "has-flag": "^4.0.0" }, @@ -4236,20 +6471,33 @@ "node": ">=8" } }, + "node_modules/supports-hyperlinks": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.3.0.tgz", + "integrity": "sha512-RpsAZlpWcDwOPQA22aCH4J0t7L8JmAvsCxfOSEwm7cQs3LshN36QaTkwd70DnBOXDWGssw2eUoc8CaRWT0XunA==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/svgo": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.1.0.tgz", - "integrity": "sha512-R5SnNA89w1dYgNv570591F66v34b3eQShpIBcQtZtM5trJwm1VvxbIoMpRYY3ybTAutcKTLEmTsdnaknOHbiQA==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "dependencies": { "@trysound/sax": "0.2.0", "commander": "^7.2.0", "css-select": "^5.1.0", - "css-tree": "^2.2.1", + "css-tree": "^2.3.1", "css-what": "^6.1.0", - "csso": "5.0.5", + "csso": "^5.0.5", "picocolors": "^1.0.0" }, "bin": { @@ -4268,17 +6516,45 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, - "optional": true, - "peer": true, + "license": "MIT", "engines": { "node": ">= 10" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, "node_modules/temp": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/temp/-/temp-0.9.4.tgz", "integrity": "sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==", "dev": true, + "license": "MIT", "dependencies": { "mkdirp": "^0.5.1", "rimraf": "~2.6.2" @@ -4287,11 +6563,91 @@ "node": ">=6.0.0" } }, + "node_modules/temp/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/temp/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/temp/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/temp/node_modules/rimraf": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.3.tgz", + "integrity": "sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + } + }, "node_modules/term-size": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/term-size/-/term-size-2.2.1.tgz", "integrity": "sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/terminal-link": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", + "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.2.1", + "supports-hyperlinks": "^2.0.0" + }, "engines": { "node": ">=8" }, @@ -4300,10 +6656,11 @@ } }, "node_modules/terser": { - "version": "5.26.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.26.0.tgz", - "integrity": "sha512-dytTGoE2oHgbNV9nTzgBEPaqAWvcJNl66VZ0BkJqlvp71IjO8CxdBx/ykCNb47cLnCmCvRZ6ZR0tLkqvZCdVBQ==", + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -4321,25 +6678,29 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/timsort": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", "integrity": "sha512-qsdtZH+vMoCARQtyod4imc2nIJwg9Cc7lPRrw9CzF8ZKR0khdr8+2nX80PBhET3tcyTtJDxAffGh2rXH4tyU8A==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, + "license": "MIT", "dependencies": { "is-number": "^7.0.0" }, @@ -4347,17 +6708,63 @@ "node": ">=8.0" } }, + "node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/traverse": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.3.9.tgz", + "integrity": "sha512-iawgk0hLP3SxGKDfnDJf8wTz4p2qImnyihM5Hh/sGvQ3K37dPi/w8sRhdNIxYA1TwFwc5mDhIJq+O0RsvXBKdQ==", + "dev": true, + "license": "MIT/X11", + "engines": { + "node": "*" + } + }, "node_modules/tslib": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", - "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==", - "dev": true + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tunnel-agent": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", + "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/tweetnacl": { + "version": "0.14.5", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", + "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==", + "dev": true, + "license": "Unlicense" }, "node_modules/type-fest": { "version": "0.20.2", "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", "dev": true, + "license": "(MIT OR CC0-1.0)", "engines": { "node": ">=10" }, @@ -4365,10 +6772,51 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true, + "license": "MIT" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unzip-stream": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/unzip-stream/-/unzip-stream-0.3.4.tgz", + "integrity": "sha512-PyofABPVv+d7fL7GOpusx7eRT9YETY2X04PhwbSipdj6bMxVCFJrr+nm0Mxqbf9hUiTin/UsnuFWBXlDZFy0Cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "binary": "^0.3.0", + "mkdirp": "^0.5.1" + } + }, + "node_modules/unzip-stream/node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, "node_modules/update-browserslist-db": { - "version": "1.0.13", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.13.tgz", - "integrity": "sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.1.tgz", + "integrity": "sha512-R8UzCaa9Az+38REPiJ1tXlImTJXlVfgHZsglwBD/k6nj76ctsH1E3q4doGrukiLQd3sGQYu56r5+lo5r94l29A==", "dev": true, "funding": [ { @@ -4384,9 +6832,10 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0" + "escalade": "^3.2.0", + "picocolors": "^1.1.0" }, "bin": { "update-browserslist-db": "cli.js" @@ -4395,26 +6844,82 @@ "browserslist": ">= 4.21.0" } }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, "node_modules/utility-types": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.10.0.tgz", - "integrity": "sha512-O11mqxmi7wMKCo6HKFt5AhO4BwY3VV68YU07tgxfz8zJTIxr4BpsezN49Ffwy9j3ZpwwJp4fkRwjRzq3uWE6Rg==", + "version": "3.11.0", + "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", + "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", "dev": true, + "license": "MIT", "engines": { "node": ">= 4" } }, + "node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "bin/uuid" + } + }, + "node_modules/verror": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", + "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", + "dev": true, + "engines": [ + "node >=0.6.0" + ], + "license": "MIT", + "dependencies": { + "assert-plus": "^1.0.0", + "core-util-is": "1.0.2", + "extsprintf": "^1.2.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "defaults": "^1.0.3" + } + }, "node_modules/weak-lru-cache": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^2.0.0" }, @@ -4425,17 +6930,100 @@ "node": ">= 8" } }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/xml2js": { "version": "0.4.23", "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.23.tgz", "integrity": "sha512-ySPiMjM0+pLDftHgXY4By0uswI3SPKLDw/i3UXbnO8M/p28zqexCUoPmQFrYD+/1BzhGJSs2i1ERWKJAtiLrug==", "dev": true, + "license": "MIT", "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" @@ -4449,6 +7037,7 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=4.0" } @@ -4458,6 +7047,7 @@ "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-15.1.1.tgz", "integrity": "sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==", "dev": true, + "license": "MIT", "engines": { "node": ">=8.0" } @@ -4466,7 +7056,8 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true + "dev": true, + "license": "ISC" } } } diff --git a/frontend/package.json b/frontend/package.json index ac803ea..5cb5394 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -13,15 +13,20 @@ "test": "elm-test" }, "devDependencies": { - "@parcel/packager-raw-url": "2.10.3", - "@parcel/packager-xml": "2.10.3", - "@parcel/transformer-elm": "^2.10.3", - "@parcel/transformer-webmanifest": "2.10.3", - "@parcel/transformer-xml": "2.10.3", + "@parcel/optimizer-svgo": "2.13.3", + "@parcel/packager-raw-url": "2.13.3", + "@parcel/packager-xml": "2.13.3", + "@parcel/transformer-elm": "^2.13.3", + "@parcel/transformer-webmanifest": "2.13.3", + "@parcel/transformer-xml": "2.13.3", "elm": "^0.19.1-6", + "elm-format": "^0.8.7", + "elm-json": "^0.2.13", + "elm-review": "^2.12.0", "elm-test": "^0.19.1-revision7", - "parcel": "^2.10.3", - "parcel-plugin-browserconfig": "^1.0.5" + "parcel": "^2.13.3", + "parcel-plugin-browserconfig": "^1.0.5", + "svgo": "^3.3.2" }, "dependencies": { "reconnecting-websocket": "^4.4.0" diff --git a/integration/src/test/scala/io/adamnfish/pokerdot/integration/CreateGameIntegrationTest.scala b/integration/src/test/scala/io/adamnfish/pokerdot/integration/CreateGameIntegrationTest.scala index d2413da..905a30b 100644 --- a/integration/src/test/scala/io/adamnfish/pokerdot/integration/CreateGameIntegrationTest.scala +++ b/integration/src/test/scala/io/adamnfish/pokerdot/integration/CreateGameIntegrationTest.scala @@ -1,111 +1,166 @@ package io.adamnfish.pokerdot.integration +import cats.effect.* +import cats.effect.testing.scalatest.AsyncIOSpec import io.adamnfish.pokerdot.TestHelpers.parseReq -import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{createGameRequest, performCreateGame} -import io.adamnfish.pokerdot.models._ +import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{ + createGameRequest, + performCreateGame +} +import io.adamnfish.pokerdot.models.* import io.adamnfish.pokerdot.{PokerDot, TestHelpers} import org.scalactic.source.Position import org.scalatest.OptionValues -import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.freespec.AsyncFreeSpec import org.scalatest.matchers.should.Matchers - -class CreateGameIntegrationTest extends AnyFreeSpec with Matchers with IntegrationComponents with TestHelpers with OptionValues { +class CreateGameIntegrationTest + extends AsyncFreeSpec + with AsyncIOSpec + with Matchers + with IntegrationComponents + with TestHelpers + with OptionValues { val hostAddress = PlayerAddress("host-address") val initialSeed = 1L "for a valid request" - { - "is successful" in withAppContext { (context, _) => - performCreateGame(createGameRequest, context(hostAddress), initialSeed) is ASuccess + "is successful" in appContextRes.use { (context, _) => + performCreateGame( + createGameRequest, + context(hostAddress), + initialSeed + ).assertNoException } - "sends a status message out to the host" in withAppContext { (context, _) => - val response = performCreateGame(createGameRequest, context(hostAddress), initialSeed).value() - response.messages.size shouldEqual 1 + "sends a status message out to the host" in appContextRes.use { (context, _) => + for { + response <- performCreateGame( + createGameRequest, + context(hostAddress), + initialSeed + ) + } yield response.messages.size shouldEqual 1 } - "returns a correct welcome message" in withAppContext { (context, _) => - val response = performCreateGame(createGameRequest, context(hostAddress), initialSeed).value() - - response.messages.get(hostAddress).value should have( + "returns a correct welcome message" in appContextRes.use { (context, _) => + for { + response <- performCreateGame( + createGameRequest, + context(hostAddress), + initialSeed + ) + } yield response.messages.get(hostAddress).value should have( "screenName" as "host name", - "gameName" as "game name", + "gameName" as "game name" ) } - "returns a correct game summary" in withAppContext { (context, _) => - val response = performCreateGame(createGameRequest, context(hostAddress), initialSeed).value() - val gameSummary = response.messages.get(hostAddress).value.game - gameSummary should have( + "returns a correct game summary" in appContextRes.use { (context, _) => + for { + response <- performCreateGame( + createGameRequest, + context(hostAddress), + initialSeed + ) + gameSummary = response.messages.get(hostAddress).value.game + } yield gameSummary should have( "gameName" as "game name", "started" as false, "inTurn" as None, - "round" as PreFlopSummary(), + "round" as PreFlopSummary() ) } "persists the saved game to the database" - { - "with key fields" in withAppContext { (context, db) => - val response = performCreateGame(createGameRequest, context(hostAddress), initialSeed).value() - val welcomeMessage = response.messages.get(hostAddress).value - val gameDb = db.getGame(welcomeMessage.gameId).value().value - gameDb should have( + "with key fields" in appContextRes.use { (context, db) => + for { + response <- performCreateGame( + createGameRequest, + context(hostAddress), + initialSeed + ) + welcomeMessage = response.messages.get(hostAddress).value + gameDbOpt <- db.getGame(welcomeMessage.gameId) + } yield gameDbOpt.value should have( "gameId" as welcomeMessage.gameId.gid, "gameName" as "game name", - "phase" as PreFlop, + "phase" as PreFlop ) } - "with an appropriate expiry" in withAppContext { (context, db) => + "with an appropriate expiry" in appContextRes.use { (context, db) => val appContext = context(hostAddress) - val response = performCreateGame(createGameRequest, appContext, initialSeed).value() - val welcomeMessage = response.messages.get(hostAddress).value - val gameDb = db.getGame(welcomeMessage.gameId).value().value - - gameDb.expiry should be > appContext.clock.now() + for { + response <- performCreateGame( + createGameRequest, + appContext, + initialSeed + ) + welcomeMessage = response.messages.get(hostAddress).value + gameDbOpt <- db.getGame(welcomeMessage.gameId) + now <- appContext.time.now + } yield gameDbOpt.value.expiry should be > now } } "persists the saved host to the database" - { - "with some key fields" in withAppContext { (context, db) => - val response = performCreateGame(createGameRequest, context(hostAddress), initialSeed).value() - val welcomeMessage = response.messages.get(hostAddress).value - val hostDb = db.getPlayers(welcomeMessage.gameId).value().head - hostDb should have( + "with some key fields" in appContextRes.use { (context, db) => + for { + response <- performCreateGame( + createGameRequest, + context(hostAddress), + initialSeed + ) + welcomeMessage = response.messages.get(hostAddress).value + dbPlayers <- db.getPlayers(welcomeMessage.gameId) + hostDb = dbPlayers.head + } yield hostDb should have( "playerKey" as welcomeMessage.playerKey.key, "playerId" as welcomeMessage.playerId.pid, - "screenName" as "host name", + "screenName" as "host name" ) } - "with an appropriate expiry" in withAppContext { (context, db) => + "with an appropriate expiry" in appContextRes.use { (context, db) => val appContext = context(hostAddress) - val response = performCreateGame(createGameRequest, appContext, initialSeed).value() - val welcomeMessage = response.messages.get(hostAddress).value - val hostDb = db.getPlayers(welcomeMessage.gameId).value().head - - hostDb.expiry should be > appContext.clock.now() + for { + response <- performCreateGame( + createGameRequest, + appContext, + initialSeed + ) + welcomeMessage = response.messages.get(hostAddress).value + dbPlayers <- db.getPlayers(welcomeMessage.gameId) + hostDb = dbPlayers.head + now <- appContext.time.now + } yield hostDb.expiry should be > now } } } "for invalid submissions" - { - "fails if the game name is empty" in withAppContext { (context, _) => - val appContext = context(hostAddress) - val result = performCreateGame("""{"gameName": "", "screenName": "player"}""", appContext, initialSeed) - result is AFailure + "fails if the game name is empty" in appContextRes.use { (context, _) => + performCreateGame( + """{"gameName": "", "screenName": "player"}""", + context(hostAddress), + initialSeed + ).assertThrows[Failures] } - "fails if the player's screen name is empty" in withAppContext { (context, _) => - val appContext = context(hostAddress) - val result = performCreateGame("""{"gameName": "game name", "screenName": ""}""", appContext, initialSeed) - result is AFailure + "fails if the player's screen name is empty" in appContextRes.use { + (context, _) => + performCreateGame( + """{"gameName": "game name", "screenName": ""}""", + context(hostAddress), + initialSeed + ).assertThrows[Failures] } - "fails if the JSON is not a valid create game request" in withAppContext { (context, _) => - val appContext = context(hostAddress) - val result = performCreateGame("""{}""", appContext, initialSeed) - result is AFailure + "fails if the JSON is not a valid create game request" in appContextRes.use { + (context, _) => + performCreateGame("""{}""", context(hostAddress), initialSeed) + .assertThrows[Failures] } } } @@ -116,7 +171,9 @@ object CreateGameIntegrationTest { | "gameName": "game name" |}""".stripMargin - def performCreateGame(request: String, context: AppContext, seed: Long)(implicit pos: Position): Attempt[Response[Welcome]] = { - PokerDot.createGame(parseReq(request), context, seed) + def performCreateGame(request: String, context: AppContext[IO], seed: Long)( + implicit pos: Position + ): IO[Response[Welcome]] = { + PokerDot.createGame[IO](parseReq(request), context, seed) } -} \ No newline at end of file +} diff --git a/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegration4PTest.scala b/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegration4PTest.scala index 76b4fca..5511fac 100644 --- a/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegration4PTest.scala +++ b/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegration4PTest.scala @@ -7,238 +7,254 @@ import io.adamnfish.pokerdot.integration.JoinGameIntegrationTest.{joinGameReques import io.adamnfish.pokerdot.integration.StartGameIntegrationTest.{performStartGame, startGameRequest} import io.adamnfish.pokerdot.logic.Cards.RichRank import io.adamnfish.pokerdot.models.Serialisation.RequestEncoders.encodeRequest -import io.adamnfish.pokerdot.models._ +import io.adamnfish.pokerdot.models.* import io.adamnfish.pokerdot.{PokerDot, TestHelpers} +import cats.effect.* +import cats.effect.testing.scalatest.AsyncIOSpec import org.scalactic.source.Position import org.scalatest.OptionValues -import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.freespec.AsyncFreeSpec import org.scalatest.matchers.should.Matchers -class GameplayIntegration4PTest extends AnyFreeSpec with Matchers with IntegrationComponents with TestHelpers with OptionValues { +class GameplayIntegration4PTest extends AsyncFreeSpec with AsyncIOSpec with Matchers with IntegrationComponents with TestHelpers with OptionValues { val hostAddress = PlayerAddress("host-address") val player1Address = PlayerAddress("player-1-address") val player2Address = PlayerAddress("player-2-address") val player3Address = PlayerAddress("player-3-address") "poker gameplay works" - { - "for an example game" in withAppContext { (context, db) => - val (_, hostWelcome, p1Welcome, p2Welcome, p3Welcome) = gameFixture(context, - initialSeed = 0L, // determines deck order - startingStack = Some(1000), - initialSmallBlind = Some(5), - timerConfig = None, - ).value() - // community: K♦ A♦ Q♠ 6♥ J♣ - // host: Q♦ 7♣ - // p1: 10♠ 7♦ - // p2: Q♣ J♥ - // p3: 7♠ 6♠ - // host is dealer, p1 small blind, p2 big blind, p3 first to act - // p3 is initial player (left of dealer small blind and big blind) - // p3 gas 7♠ 6♠ and folds - PokerDot.pokerdot(foldRequest(p3Welcome), context(player3Address)).value() - // host has Q♦ 7♣ and calls - PokerDot.pokerdot(betRequest(10, hostWelcome), context(hostAddress)).value() - // p1 has 10♠ 7♦ and calls from small blind - PokerDot.pokerdot(betRequest(5, p1Welcome), context(player1Address)).value() - // p2 has J♥ Q♣ and checks from big blind - PokerDot.pokerdot(checkRequest(p2Welcome), context(player2Address)).value() - // phase is now complete - // TODO: check database for player states here - val playerDbsPreFlop = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsPreFlop.get(hostWelcome.playerId).value should have( - "checked" as true, - "bet" as 10, - "pot" as 0, - ) - playerDbsPreFlop.get(p1Welcome.playerId).value should have( - "checked" as true, - "bet" as 10, - "pot" as 0, - ) - playerDbsPreFlop.get(p2Welcome.playerId).value should have( - "checked" as true, - "bet" as 10, - "pot" as 0, - ) - playerDbsPreFlop.get(p3Welcome.playerId).value should have( - "folded" as true, - "bet" as 0, - "pot" as 0, - ) - PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)).value() + "for an example game" in appContextRes.use { (context, db) => + for { + (_, hostWelcome, p1Welcome, p2Welcome, p3Welcome) <- gameFixture(context, + initialSeed = 0L, // determines deck order + startingStack = Some(1000), + initialSmallBlind = Some(5), + timerConfig = None, + ) + // community: K♦ A♦ Q♠ 6♥ J♣ + // host: Q♦ 7♣ + // p1: 10♠ 7♦ + // p2: Q♣ J♥ + // p3: 7♠ 6♠ + // host is dealer, p1 small blind, p2 big blind, p3 first to act + // p3 is initial player (left of dealer small blind and big blind) + // p3 gas 7♠ 6♠ and folds + _ <- PokerDot.pokerdot(foldRequest(p3Welcome), context(player3Address)) + // host has Q♦ 7♣ and calls + _ <- PokerDot.pokerdot(betRequest(10, hostWelcome), context(hostAddress)) + // p1 has 10♠ 7♦ and calls from small blind + _ <- PokerDot.pokerdot(betRequest(5, p1Welcome), context(player1Address)) + // p2 has J♥ Q♣ and checks from big blind + _ <- PokerDot.pokerdot(checkRequest(p2Welcome), context(player2Address)) + // phase is now complete + // TODO: check database for player states here + playerDbsPreFlop <- db.getPlayers(hostWelcome.gameId).map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsPreFlop.get(hostWelcome.playerId).value should have( + "checked" as true, + "bet" as 10, + "pot" as 0, + ) + _ = playerDbsPreFlop.get(p1Welcome.playerId).value should have( + "checked" as true, + "bet" as 10, + "pot" as 0, + ) + _ = playerDbsPreFlop.get(p2Welcome.playerId).value should have( + "checked" as true, + "bet" as 10, + "pot" as 0, + ) + _ =playerDbsPreFlop.get(p3Welcome.playerId).value should have( + "folded" as true, + "bet" as 0, + "pot" as 0, + ) + _ <- PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)) - // community cards K♦ A♦ Q♠ are now visible - // p1 is first to act, and checks - PokerDot.pokerdot(checkRequest(p1Welcome), context(player1Address)).value() - // p2 bets - PokerDot.pokerdot(betRequest(10, p2Welcome), context(player2Address)).value() - // p3 has folded - // host calls - PokerDot.pokerdot(betRequest(10, hostWelcome), context(hostAddress)).value() - // p1 needs to react to the bet, and calls with a straight draw - PokerDot.pokerdot(betRequest(10, p1Welcome), context(player1Address)).value() - // phase is complete - val playerDbsFlop = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsFlop.get(hostWelcome.playerId).value should have( - "checked" as true, - "bet" as 10, - "pot" as 10, - ) - playerDbsFlop.get(p1Welcome.playerId).value should have( - "checked" as true, - "bet" as 10, - "pot" as 10, - ) - playerDbsFlop.get(p2Welcome.playerId).value should have( - "checked" as true, - "bet" as 10, - "pot" as 10, - ) - playerDbsFlop.get(p3Welcome.playerId).value should have( - "folded" as true, - "bet" as 0, - "bet" as 0, - ) - PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)).value() + // community cards K♦ A♦ Q♠ are now visible + // p1 is first to act, and checks + _ <- PokerDot.pokerdot(checkRequest(p1Welcome), context(player1Address)) + // p2 bets + _ <- PokerDot.pokerdot(betRequest(10, p2Welcome), context(player2Address)) + // p3 has folded + // host calls + _ <- PokerDot.pokerdot(betRequest(10, hostWelcome), context(hostAddress)) + // p1 needs to react to the bet, and calls with a straight draw + _ <- PokerDot.pokerdot(betRequest(10, p1Welcome), context(player1Address)) + // phase is complete + playerDbsFlop <- db.getPlayers(hostWelcome.gameId).map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsFlop.get(hostWelcome.playerId).value should have( + "checked" as true, + "bet" as 10, + "pot" as 10, + ) + _ = playerDbsFlop.get(p1Welcome.playerId).value should have( + "checked" as true, + "bet" as 10, + "pot" as 10, + ) + _ = playerDbsFlop.get(p2Welcome.playerId).value should have( + "checked" as true, + "bet" as 10, + "pot" as 10, + ) + _ = playerDbsFlop.get(p3Welcome.playerId).value should have( + "folded" as true, + "bet" as 0, + "bet" as 0, + ) + _ <- PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)) - // community cards K♦ A♦ Q♠ 6♥ are now visible - // players are cautious about overcards and all check - PokerDot.pokerdot(checkRequest(p1Welcome), context(player1Address)).value() - PokerDot.pokerdot(checkRequest(p2Welcome), context(player2Address)).value() - PokerDot.pokerdot(checkRequest(hostWelcome), context(hostAddress)).value() - // phase is complete - val playerDbsTurn = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsTurn.get(hostWelcome.playerId).value should have( - "checked" as true, - "bet" as 0, - "pot" as 20, - ) - playerDbsTurn.get(p1Welcome.playerId).value should have( - "checked" as true, - "bet" as 0, - "pot" as 20, - ) - playerDbsTurn.get(p2Welcome.playerId).value should have( - "checked" as true, - "bet" as 0, - "pot" as 20, - ) - playerDbsTurn.get(p3Welcome.playerId).value should have( - "folded" as true, - "bet" as 0, - "bet" as 0, - ) - PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)).value() + // community cards K♦ A♦ Q♠ 6♥ are now visible + // players are cautious about overcards and all check + _ <- PokerDot.pokerdot(checkRequest(p1Welcome), context(player1Address)) + _ <- PokerDot.pokerdot(checkRequest(p2Welcome), context(player2Address)) + _ <- PokerDot.pokerdot(checkRequest(hostWelcome), context(hostAddress)) + // phase is complete + playerDbsTurn <- db.getPlayers(hostWelcome.gameId).map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsTurn.get(hostWelcome.playerId).value should have( + "checked" as true, + "bet" as 0, + "pot" as 20, + ) + _ = playerDbsTurn.get(p1Welcome.playerId).value should have( + "checked" as true, + "bet" as 0, + "pot" as 20, + ) + _ = playerDbsTurn.get(p2Welcome.playerId).value should have( + "checked" as true, + "bet" as 0, + "pot" as 20, + ) + _ = playerDbsTurn.get(p3Welcome.playerId).value should have( + "folded" as true, + "bet" as 0, + "bet" as 0, + ) + _ <- PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)) - // all community cards now visible K♦ A♦ Q♠ 6♥ J♣ - // p1 has lucked a straight, and bets - PokerDot.pokerdot(betRequest(50, p1Welcome), context(player1Address)).value() - // p2 has two-pair, decides to call - PokerDot.pokerdot(betRequest(50, p2Welcome), context(player2Address)).value() - // host only has pair of queens and will let these two fight it out - PokerDot.pokerdot(foldRequest(hostWelcome), context(hostAddress)).value() - // phase is complete - val playerDbsRiver = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsRiver.get(hostWelcome.playerId).value should have( - "folded" as true, - "bet" as 0, - "pot" as 20, - ) - playerDbsRiver.get(p1Welcome.playerId).value should have( - "checked" as true, - "bet" as 50, - "pot" as 20, - ) - playerDbsRiver.get(p2Welcome.playerId).value should have( - "checked" as true, - "bet" as 50, - "pot" as 20, - ) - playerDbsRiver.get(p3Welcome.playerId).value should have( - "folded" as true, - "bet" as 0, - "pot" as 0, - ) + // all community cards now visible K♦ A♦ Q♠ 6♥ J♣ + // p1 has lucked a straight, and bets + _ <- PokerDot.pokerdot(betRequest(50, p1Welcome), context(player1Address)) + // p2 has two-pair, decides to call + _ <- PokerDot.pokerdot(betRequest(50, p2Welcome), context(player2Address)) + // host only has a pair of queens and will let these two fight it out + _ <- PokerDot.pokerdot(foldRequest(hostWelcome), context(hostAddress)) + // phase is complete + playerDbsRiver <- db.getPlayers(hostWelcome.gameId).map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsRiver.get(hostWelcome.playerId).value should have( + "folded" as true, + "bet" as 0, + "pot" as 20, + ) + _ = playerDbsRiver.get(p1Welcome.playerId).value should have( + "checked" as true, + "bet" as 50, + "pot" as 20, + ) + _ = playerDbsRiver.get(p2Welcome.playerId).value should have( + "checked" as true, + "bet" as 50, + "pot" as 20, + ) + _ = playerDbsRiver.get(p3Welcome.playerId).value should have( + "folded" as true, + "bet" as 0, + "pot" as 0, + ) - val response = PokerDot.advancePhase(parseReq(advancePhaseRequest(hostWelcome)), context(hostAddress)).value() + response <- PokerDot.advancePhase(parseReq(advancePhaseRequest(hostWelcome)), context(hostAddress)) - // pots are preserved at this stage to help the UI show how the game is changed by the result - val playerDbsShowdown = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsShowdown.get(hostWelcome.playerId).value should have( - "stack" as 980, - "bet" as 0, - "pot" as 20, - ) - playerDbsShowdown.get(p1Welcome.playerId).value should have( - "stack" as 1090, - "bet" as 0, - "pot" as 70, - ) - playerDbsShowdown.get(p2Welcome.playerId).value should have( - "stack" as 930, - "bet" as 0, - "pot" as 70, - ) - playerDbsShowdown.get(p3Welcome.playerId).value should have( - "stack" as 1000, - "bet" as 0, - "pot" as 0, - ) - val roundWinnings = response.messages.get(hostAddress).value.asInstanceOf[RoundWinnings] - // only one player wins, with a straight - roundWinnings.players.toSet shouldEqual Set( - // folded players do not show up (and are commented out here) - // PlayerWinnings(hostWelcome.playerId, Pair(Queen of Spades, Queen of Diamonds, Ace of Diamonds, King of Diamonds, Jack of Clubs), 0), - PlayerWinnings(p1Welcome.playerId, Some(Straight(Ace of Diamonds, King of Diamonds, Queen of Spades, Jack of Clubs, Ten of Spades)), Hole(Ten of Spades, Seven of Diamonds), 160), - PlayerWinnings(p2Welcome.playerId, Some(TwoPair(Queen of Spades, Queen of Clubs, Jack of Hearts, Jack of Clubs, Ace of Diamonds)), Hole(Jack of Hearts, Queen of Clubs), 0), - // PlayerWinnings(p3Welcome.playerId, Pair(Six of Spades, Six of Hearts, Ace of Diamonds, King of Diamonds, Queen of Spades), 0), - ) - // a single pot between players 1 and 2, with player 1 winning - roundWinnings.pots shouldEqual List( - PotWinnings(160, Set(p1Welcome.playerId, p2Welcome.playerId), Set(p1Welcome.playerId)) - ) + // pots are preserved at this stage to help the UI show how the game is changed by the result + playerDbsShowdown <- db.getPlayers(hostWelcome.gameId).map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsShowdown.get(hostWelcome.playerId).value should have( + "stack" as 980, + "bet" as 0, + "pot" as 20, + ) + _ = playerDbsShowdown.get(p1Welcome.playerId).value should have( + "stack" as 1090, + "bet" as 0, + "pot" as 70, + ) + _ = playerDbsShowdown.get(p2Welcome.playerId).value should have( + "stack" as 930, + "bet" as 0, + "pot" as 70, + ) + _ = playerDbsShowdown.get(p3Welcome.playerId).value should have( + "stack" as 1000, + "bet" as 0, + "pot" as 0, + ) + roundWinnings = response.messages.get(hostAddress).value.asInstanceOf[RoundWinnings] + // only one player wins, with a straight + _ = roundWinnings.players.toSet shouldEqual Set( + // folded players do not show up (and are commented out here) + // PlayerWinnings(hostWelcome.playerId, Pair(Queen of Spades, Queen of Diamonds, Ace of Diamonds, King of Diamonds, Jack of Clubs), 0), + PlayerWinnings(p1Welcome.playerId, Some(Straight(Ace of Diamonds, King of Diamonds, Queen of Spades, Jack of Clubs, Ten of Spades)), Hole(Ten of Spades, Seven of Diamonds), 160), + PlayerWinnings(p2Welcome.playerId, Some(TwoPair(Queen of Spades, Queen of Clubs, Jack of Hearts, Jack of Clubs, Ace of Diamonds)), Hole(Jack of Hearts, Queen of Clubs), 0), + // PlayerWinnings(p3Welcome.playerId, Pair(Six of Spades, Six of Hearts, Ace of Diamonds, King of Diamonds, Queen of Spades), 0), + ) + // a single pot between players 1 and 2, with player 1 winning + _ = roundWinnings.pots shouldEqual List( + PotWinnings(160, Set(p1Welcome.playerId, p2Welcome.playerId), Set(p1Welcome.playerId)) + ) - // advance to next round - PokerDot.advancePhase(parseReq(advancePhaseRequest(hostWelcome)), context(hostAddress)).value() + // advance to next round + _ <- PokerDot.advancePhase(parseReq(advancePhaseRequest(hostWelcome)), context(hostAddress)) - // players should be reset for the new round - val playerDbsNewRound = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsNewRound.get(hostWelcome.playerId).value should have( - "stack" as 980, - "checked" as false, - "folded" as false, - "bet" as 0, - "pot" as 0, - "blind" as 0, - ) - playerDbsNewRound.get(p1Welcome.playerId).value should have( - "stack" as 1090, - "checked" as false, - "folded" as false, - "bet" as 0, - "pot" as 0, - "blind" as 0, - ) - playerDbsNewRound.get(p2Welcome.playerId).value should have( - "stack" as 925, // small blind paid out as well as prev round's result - "checked" as false, - "folded" as false, - "bet" as 5, - "pot" as 0, - "blind" as 1, - ) - playerDbsNewRound.get(p3Welcome.playerId).value should have( - "stack" as 990, // big blind paid out as well as prev round's result - "checked" as false, - "folded" as false, - "bet" as 10, - "pot" as 0, - "blind" as 2, - ) - // dealer and active player should have moved correctly - db.getGame(hostWelcome.gameId).value().value should have( + // players should be reset for the new round + playerDbsNewRound <- db.getPlayers(hostWelcome.gameId).map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsNewRound.get(hostWelcome.playerId).value should have( + "stack" as 980, + "checked" as false, + "folded" as false, + "bet" as 0, + "pot" as 0, + "blind" as 0, + ) + _ = playerDbsNewRound.get(p1Welcome.playerId).value should have( + "stack" as 1090, + "checked" as false, + "folded" as false, + "bet" as 0, + "pot" as 0, + "blind" as 0, + ) + _ = playerDbsNewRound.get(p2Welcome.playerId).value should have( + "stack" as 925, // small blind paid out as well as prev round's result + "checked" as false, + "folded" as false, + "bet" as 5, + "pot" as 0, + "blind" as 1, + ) + _ = playerDbsNewRound.get(p3Welcome.playerId).value should have( + "stack" as 990, // big blind paid out as well as prev round's result + "checked" as false, + "folded" as false, + "bet" as 10, + "pot" as 0, + "blind" as 2, + ) + // dealer and active player should have moved correctly + finalGameOpt <- db.getGame(hostWelcome.gameId) + } yield finalGameOpt.value should have( "button" as 1, "inTurn" as Some(hostWelcome.playerId.pid), ) @@ -247,21 +263,29 @@ class GameplayIntegration4PTest extends AnyFreeSpec with Matchers with Integrati "invalid requests" - { "when it isn't the player's turn" - { - "cannot fold" ignore {} - "cannot bet" ignore {} - "cannot check" ignore {} + "cannot fold" ignore { + TODO + } + "cannot bet" ignore { + TODO + } + "cannot check" ignore { + TODO + } } - "bet cannot exceed stack" ignore {} + "bet cannot exceed stack" ignore { + TODO + } } private def gameFixture( - contextBuilder: PlayerAddress => AppContext, + contextBuilder: PlayerAddress => AppContext[IO], initialSeed: Long, startingStack: Option[Int], initialSmallBlind: Option[Int], timerConfig: Option[List[TimerLevel]], - )(implicit pos: Position): Attempt[(GameStatus, Welcome, Welcome, Welcome, Welcome)] = { + )(implicit pos: Position): IO[(GameStatus, Welcome, Welcome, Welcome, Welcome)] = { for { hostResponse <- performCreateGame(createGameRequest, contextBuilder(hostAddress), initialSeed) hostWelcome = hostResponse.messages.find { case (address, _) => diff --git a/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegrationTestHeadsUp.scala b/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegrationTestHeadsUp.scala index 8526971..2523cd6 100644 --- a/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegrationTestHeadsUp.scala +++ b/integration/src/test/scala/io/adamnfish/pokerdot/integration/GameplayIntegrationTestHeadsUp.scala @@ -1,158 +1,238 @@ package io.adamnfish.pokerdot.integration +import cats.effect.* +import cats.effect.testing.scalatest.AsyncIOSpec import io.adamnfish.pokerdot.{PokerDot, TestHelpers} -import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{createGameRequest, performCreateGame} -import io.adamnfish.pokerdot.models.{AppContext, Attempt, GameStatus, PlayerAddress, PlayerId, TimerLevel, Welcome} +import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{ + createGameRequest, + performCreateGame +} +import io.adamnfish.pokerdot.models.{ + AppContext, + GameStatus, + PlayerAddress, + PlayerId, + TimerLevel, + Welcome +} import org.scalatest.OptionValues -import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.freespec.AsyncFreeSpec import org.scalatest.matchers.should.Matchers -import io.adamnfish.pokerdot.integration.IntegrationComponents.{advancePhaseRequest, betRequest, checkRequest, foldRequest} -import io.adamnfish.pokerdot.integration.JoinGameIntegrationTest.{joinGameRequest, performJoinGame} -import io.adamnfish.pokerdot.integration.StartGameIntegrationTest.{performStartGame, startGameRequest} +import io.adamnfish.pokerdot.integration.IntegrationComponents.{ + advancePhaseRequest, + betRequest, + checkRequest, + foldRequest +} +import io.adamnfish.pokerdot.integration.JoinGameIntegrationTest.{ + joinGameRequest, + performJoinGame +} +import io.adamnfish.pokerdot.integration.StartGameIntegrationTest.{ + performStartGame, + startGameRequest +} import org.scalactic.source.Position - -class GameplayIntegrationTestHeadsUp extends AnyFreeSpec with Matchers with IntegrationComponents with TestHelpers with OptionValues { +class GameplayIntegrationTestHeadsUp + extends AsyncFreeSpec + with AsyncIOSpec + with Matchers + with IntegrationComponents + with TestHelpers + with OptionValues { val hostAddress = PlayerAddress("host-address") val player1Address = PlayerAddress("player-1-address") - "example heads-up game" in withAppContext { (context, db) => - val (_, hostWelcome, p1Welcome) = gameFixture(context, - initialSeed = 0L, // determines deck order - startingStack = Some(1000), - initialSmallBlind = Some(5), - timerConfig = None, - ).value() + "example heads-up game" in appContextRes.use { (context, db) => + for { + (_, hostWelcome, p1Welcome) <- gameFixture( + context, + initialSeed = 0L, // determines deck order + startingStack = Some(1000), + initialSmallBlind = Some(5), + timerConfig = None + ) + + // check initial state + gameDbOpt1 <- db.getGame(hostWelcome.gameId) + _ = gameDbOpt1.value should have( + "button" as 0, + "inTurn" as Some(hostWelcome.playerId.pid) + ) - // check initial state - db.getGame(hostWelcome.gameId).value().value should have( - "button" as 0, - "inTurn" as Some(hostWelcome.playerId.pid), - ) - // community: K♦ A♦ Q♠ 6♥ J♣ - // host: Q♦ 7♣ - // p1: 10♠ 7♦ - // host is dealer and small blind, p1 big blind - // host is initial player - // host gas Q♦ 7♣ and folds - PokerDot.pokerdot(foldRequest(hostWelcome), context(hostAddress)).value() - // no more actions required - // advances straight to showdown (since there is only a winner left in the round) - PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)).value() - // advance to next round - PokerDot.pokerdot(advancePhaseRequest(hostWelcome), context(hostAddress)).value() + // community: K♦ A♦ Q♠ 6♥ J♣ + // host: Q♦ 7♣ + // p1: 10♠ 7♦ + // host is dealer and small blind, p1 big blind + // host is initial player + // host gas Q♦ 7♣ and folds + _ <- PokerDot.pokerdot(foldRequest(hostWelcome), context(hostAddress)) + // no more actions required + // advances straight to showdown (since there is only a winner left in the round) + _ <- PokerDot.pokerdot( + advancePhaseRequest(hostWelcome), + context(hostAddress) + ) + // advance to next round + _ <- PokerDot.pokerdot( + advancePhaseRequest(hostWelcome), + context(hostAddress) + ) - // players should be reset for the new round correctly - val playerDbsNewRound = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsNewRound.get(hostWelcome.playerId).value should have( - "checked" as false, - "folded" as false, - "bet" as 10, - "pot" as 0, - "blind" as 2, - ) - playerDbsNewRound.get(p1Welcome.playerId).value should have( - "checked" as false, - "folded" as false, - "bet" as 5, - "pot" as 0, - "blind" as 1, - ) - // dealer should have moved correctly - db.getGame(hostWelcome.gameId).value().value should have( - "button" as 1, - "inTurn" as Some(p1Welcome.playerId.pid), - ) + // players should be reset for the new round correctly + playerDbsNewRound <- db + .getPlayers(hostWelcome.gameId) + .map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsNewRound.get(hostWelcome.playerId).value should have( + "checked" as false, + "folded" as false, + "bet" as 10, + "pot" as 0, + "blind" as 2 + ) + _ = playerDbsNewRound.get(p1Welcome.playerId).value should have( + "checked" as false, + "folded" as false, + "bet" as 5, + "pot" as 0, + "blind" as 1 + ) + // dealer should have moved correctly + gameDbOpt2 <- db.getGame(hostWelcome.gameId) + _ = gameDbOpt2.value should have( + "button" as 1, + "inTurn" as Some(p1Welcome.playerId.pid) + ) - // new round - // player 1 is first to act, and folds - PokerDot.pokerdot(foldRequest(p1Welcome), context(player1Address)).value() + // new round + // player 1 is first to act, and folds + _ <- PokerDot.pokerdot(foldRequest(p1Welcome), context(player1Address)) + } yield assert(true) } - "checking on big blind should end the phase" in withAppContext { (context, db) => - val (_, hostWelcome, p1Welcome) = gameFixture(context, - initialSeed = 0L, // determines deck order - startingStack = Some(1000), - initialSmallBlind = Some(5), - timerConfig = None, - ).value() + "checking on big blind should end the phase" in appContextRes.use { + (context, db) => + for { + (_, hostWelcome, p1Welcome) <- gameFixture( + context, + initialSeed = 0L, // determines deck order + startingStack = Some(1000), + initialSmallBlind = Some(5), + timerConfig = None + ) - // host: Q♦ 7♣ - // p1: 10♠ 7♦ - // host is dealer and small blind, p1 big blind - // host is initial player, and calls - PokerDot.pokerdot(betRequest(5, hostWelcome), context(hostAddress)).value() - // p1 checks as big blind - PokerDot.pokerdot(checkRequest(p1Welcome), context(player1Address)).value() + // host: Q♦ 7♣ + // p1: 10♠ 7♦ + // host is dealer and small blind, p1 big blind + // host is initial player, and calls + _ <- PokerDot + .pokerdot(betRequest(5, hostWelcome), context(hostAddress)) + // p1 checks as big blind + _ <- PokerDot + .pokerdot(checkRequest(p1Welcome), context(player1Address)) - // players have acted and should be marked as checked - val playerDbsNewRound = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsNewRound.get(hostWelcome.playerId).value should have( - "checked" as true, - "bet" as 10, - ) - playerDbsNewRound.get(p1Welcome.playerId).value should have( - "checked" as true, - "bet" as 10, - ) + // players have acted and should be marked as checked + playerDbsNewRound <- db + .getPlayers(hostWelcome.gameId) + .map(playerDbs => + playerDbs + .map(pdb => (PlayerId(pdb.playerId), pdb)) + .toMap + ) + _ = playerDbsNewRound.get(hostWelcome.playerId).value should have( + "checked" as true, + "bet" as 10 + ) + _ = playerDbsNewRound.get(p1Welcome.playerId).value should have( + "checked" as true, + "bet" as 10 + ) - // both players have acted, no-one should be "in turn" - db.getGame(hostWelcome.gameId).value().value should have( - "inTurn" as None, - ) + // both players have acted, no-one should be "in turn" + gameDbOpt <- db.getGame(hostWelcome.gameId) + } yield gameDbOpt.value should have( + "inTurn" as None + ) } - "calling a bet should end the phase" in withAppContext { (context, db) => - val (_, hostWelcome, p1Welcome) = gameFixture(context, - initialSeed = 0L, // determines deck order - startingStack = Some(1000), - initialSmallBlind = Some(5), - timerConfig = None, - ).value() + "calling a bet should end the phase" in appContextRes.use { (context, db) => + for { + (_, hostWelcome, p1Welcome) <- gameFixture( + context, + initialSeed = 0L, // determines deck order + startingStack = Some(1000), + initialSmallBlind = Some(5), + timerConfig = None + ) - // host: Q♦ 7♣ - // p1: 10♠ 7♦ - // host is dealer and small blind, p1 big blind - // host is initial player, and raises - PokerDot.pokerdot(betRequest(15, hostWelcome), context(hostAddress)).value() - // p1 calls - PokerDot.pokerdot(betRequest(10, p1Welcome), context(player1Address)).value() - // players have acted and should be marked as checked - val playerDbsNewRound = db.getPlayers(hostWelcome.gameId).value().map(pdb => (PlayerId(pdb.playerId), pdb)).toMap - playerDbsNewRound.get(hostWelcome.playerId).value should have( - "checked" as true, - "bet" as 20, - ) - playerDbsNewRound.get(p1Welcome.playerId).value should have( - "checked" as true, - "bet" as 20, - ) + // host: Q♦ 7♣ + // p1: 10♠ 7♦ + // host is dealer and small blind, p1 big blind + // host is initial player, and raises + _ <- PokerDot.pokerdot(betRequest(15, hostWelcome), context(hostAddress)) + // p1 calls + _ <- PokerDot.pokerdot(betRequest(10, p1Welcome), context(player1Address)) + // players have acted and should be marked as checked + playerDbsNewRound <- db + .getPlayers(hostWelcome.gameId) + .map(playerDbs => + playerDbs.map(pdb => (PlayerId(pdb.playerId), pdb)).toMap + ) + _ = playerDbsNewRound.get(hostWelcome.playerId).value should have( + "checked" as true, + "bet" as 20 + ) + _ = playerDbsNewRound.get(p1Welcome.playerId).value should have( + "checked" as true, + "bet" as 20 + ) - // both players have acted, no-one should be "in turn" - db.getGame(hostWelcome.gameId).value().value should have( - "inTurn" as None, + // both players have acted, no-one should be "in turn" + gameDbOpt <- db.getGame(hostWelcome.gameId) + } yield gameDbOpt.value should have( + "inTurn" as None ) } private def gameFixture( - contextBuilder: PlayerAddress => AppContext, - initialSeed: Long, - startingStack: Option[Int], - initialSmallBlind: Option[Int], - timerConfig: Option[List[TimerLevel]], - )(implicit pos: Position): Attempt[(GameStatus, Welcome, Welcome)] = { + contextBuilder: PlayerAddress => AppContext[IO], + initialSeed: Long, + startingStack: Option[Int], + initialSmallBlind: Option[Int], + timerConfig: Option[List[TimerLevel]] + )(implicit pos: Position): IO[(GameStatus, Welcome, Welcome)] = { for { - hostResponse <- performCreateGame(createGameRequest, contextBuilder(hostAddress), initialSeed) - hostWelcome = hostResponse.messages.find { case (address, _) => - address == hostAddress - }.map(_._2).value + hostResponse <- performCreateGame( + createGameRequest, + contextBuilder(hostAddress), + initialSeed + ) + hostWelcome = hostResponse.messages + .find { case (address, _) => + address == hostAddress + } + .map(_._2) + .value gameCode = hostWelcome.gameCode - p1JoinResponse <- performJoinGame(joinGameRequest(gameCode, "player-1"), contextBuilder(player1Address)) + p1JoinResponse <- performJoinGame( + joinGameRequest(gameCode, "player-1"), + contextBuilder(player1Address) + ) p1Welcome = p1JoinResponse.messages.get(player1Address).value - startRequest = startGameRequest(hostWelcome, startingStack, initialSmallBlind, timerConfig, + startRequest = startGameRequest( + hostWelcome, + startingStack, + initialSmallBlind, + timerConfig, List(hostWelcome.playerId, p1Welcome.playerId) ) - startResponse <- performStartGame(startRequest, contextBuilder(hostAddress)) + startResponse <- performStartGame( + startRequest, + contextBuilder(hostAddress) + ) gameStatus = startResponse.statuses.get(hostAddress).value } yield (gameStatus, hostWelcome, p1Welcome) } diff --git a/integration/src/test/scala/io/adamnfish/pokerdot/integration/IntegrationComponents.scala b/integration/src/test/scala/io/adamnfish/pokerdot/integration/IntegrationComponents.scala index c54dd83..1c2fc5c 100644 --- a/integration/src/test/scala/io/adamnfish/pokerdot/integration/IntegrationComponents.scala +++ b/integration/src/test/scala/io/adamnfish/pokerdot/integration/IntegrationComponents.scala @@ -1,53 +1,73 @@ package io.adamnfish.pokerdot.integration -import io.adamnfish.pokerdot.TestClock +import cats.effect.IO +import cats.effect.kernel.Resource +import io.adamnfish.pokerdot.{TestRng, TestTime} import io.adamnfish.pokerdot.models.Serialisation.RequestEncoders.encodeRequest -import io.adamnfish.pokerdot.models._ +import io.adamnfish.pokerdot.models.* import io.adamnfish.pokerdot.persistence.DynamoDbDatabase import io.adamnfish.pokerdot.services.{Database, Messaging, Rng} import org.scanamo.LocalDynamoDB -import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType._ -import zio.ZIO +import org.scanamo.LocalDynamoDB.deleteTable +import software.amazon.awssdk.auth.credentials.{AwsBasicCredentials, StaticCredentialsProvider} +import software.amazon.awssdk.regions.Region +import software.amazon.awssdk.services.dynamodb.{DynamoDbAsyncClient, DynamoDbClient} +import software.amazon.awssdk.services.dynamodb.model.{AttributeDefinition, CreateTableRequest, DeleteTableRequest, KeySchemaElement, KeyType, ScalarAttributeType} +import software.amazon.awssdk.services.dynamodb.model.ScalarAttributeType.* +import java.net.URI import java.util.UUID.randomUUID +import java.util.function import scala.util.Random trait IntegrationComponents { - private val client = LocalDynamoDB.syncClient() + private val client = DynamoDbAsyncClient.builder() + .endpointOverride(URI.create("http://localhost:8042")) + .region(Region.US_EAST_1) // not used for local dynamodb, but required + .credentialsProvider(StaticCredentialsProvider.create( + AwsBasicCredentials.create("dummykey", "dummysecret"))) + .build(); - def withAppContext(f: (PlayerAddress => AppContext, Database) => Any /* Assertion */): Any /* Assertion */ = { - val randomSuffix = randomUUID().toString - val gameTableName = s"games-$randomSuffix" - val playerTableName = s"players-$randomSuffix" - val testDb = new DynamoDbDatabase(client, gameTableName, playerTableName) - val testRng = new Rng { - override def randomState(): Long = 0 - override def nextState(state: Long): Long = new Random(state).nextLong() - } - - LocalDynamoDB.withTable(client)(gameTableName)("gameCode" -> S, "gameId" -> S) { - LocalDynamoDB.withTable(client)(playerTableName)("gameId" -> S, "playerId" -> S) { - val addressToContext = AppContext( - _, + def appContextRes: Resource[IO, (PlayerAddress => AppContext[IO], Database[IO])] = + for { + randomSuffix <- IO(randomUUID().toString).toResource + gameTableName = s"games-$randomSuffix" + playerTableName = s"players-$randomSuffix" + testDb = new DynamoDbDatabase[IO](client, gameTableName, playerTableName) + testRng = new TestRng[IO] + _ <- Resource.make( + IO { + val response = LocalDynamoDB.createTable(client)(gameTableName)("gameCode" -> S, "gameId" -> S) + response.tableDescription().tableName() + } + )(tableName => IO(deleteTable(client)(tableName))) + _ <- Resource.make( + IO { + val response = LocalDynamoDB.createTable(client)(playerTableName)("gameId" -> S, "playerId" -> S) + response.tableDescription().tableName() + } + )(tableName => IO(deleteTable(client)(tableName))) + addressToContext = { (playerAddress: PlayerAddress) => + AppContext( + playerAddress, TraceId("trace-id"), testDb, - new Messaging { - override def sendMessage(playerAddress: PlayerAddress, message: Message): Attempt[Unit] = { - ZIO.unit + // TODO: keep track of sent messages so we can perform assertions on that as well + new Messaging[IO] { + override def sendMessage(playerAddress: PlayerAddress, message: Message): IO[Unit] = { + IO.unit } - override def sendError(playerAddress: PlayerAddress, message: Failures): Attempt[Unit] = { - ZIO.unit + override def sendError(playerAddress: PlayerAddress, message: Failures): IO[Unit] = { + IO.unit } }, - TestClock, + new TestTime[IO], testRng, ) - f(addressToContext, testDb) } - } - } + } yield (addressToContext, testDb) } object IntegrationComponents { def betRequest(betAmount: Int, welcome: Welcome): String = { diff --git a/integration/src/test/scala/io/adamnfish/pokerdot/integration/JoinGameIntegrationTest.scala b/integration/src/test/scala/io/adamnfish/pokerdot/integration/JoinGameIntegrationTest.scala index 79fcaaa..c55a2a1 100644 --- a/integration/src/test/scala/io/adamnfish/pokerdot/integration/JoinGameIntegrationTest.scala +++ b/integration/src/test/scala/io/adamnfish/pokerdot/integration/JoinGameIntegrationTest.scala @@ -1,201 +1,362 @@ package io.adamnfish.pokerdot.integration import io.adamnfish.pokerdot.TestHelpers.parseReq -import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{createGameRequest, performCreateGame} -import io.adamnfish.pokerdot.integration.JoinGameIntegrationTest.{joinGameRequest, performJoinGame} -import io.adamnfish.pokerdot.models.{AppContext, Attempt, PlayerAddress, PlayerJoinedSummary, Response, Welcome} +import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{ + createGameRequest, + performCreateGame +} +import io.adamnfish.pokerdot.integration.JoinGameIntegrationTest.{ + joinGameRequest, + performJoinGame +} +import io.adamnfish.pokerdot.models.{ + AppContext, + Failures, + PlayerAddress, + PlayerJoinedSummary, + Response, + Welcome +} import io.adamnfish.pokerdot.{PokerDot, TestHelpers} +import cats.effect.* import org.scalactic.source.Position import org.scalatest.OptionValues -import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.freespec.AsyncFreeSpec +import cats.effect.testing.scalatest.AsyncIOSpec import org.scalatest.matchers.should.Matchers - -class JoinGameIntegrationTest extends AnyFreeSpec with Matchers with IntegrationComponents with TestHelpers with OptionValues { +class JoinGameIntegrationTest + extends AsyncFreeSpec + with AsyncIOSpec + with Matchers + with IntegrationComponents + with TestHelpers + with OptionValues { val initialSeed = 1L val hostAddress = PlayerAddress("host-address") val playerAddress = PlayerAddress("player-address") "for a valid request" - { - "is successful" in withAppContext { (context, _) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - performJoinGame(joinGameRequest(gameCode), context(playerAddress)) is ASuccess - } - - "informs the host that this player has joined" in withAppContext { (context, _) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - val response = performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - val hostStatusMessage = response.statuses.get(hostAddress).value - val playerWelcomeMessage = response.messages.get(playerAddress).value + "is successful" in appContextRes.use { (context, _) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode - hostStatusMessage.action shouldEqual PlayerJoinedSummary(playerWelcomeMessage.playerId) + _ <- performJoinGame(joinGameRequest(gameCode), context(playerAddress)) + } yield assert(true) } - "includes the correct players in a status message sent to the host" in withAppContext { (context, _) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - val response = performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - val welcomeMessage = response.messages.head._2 - val hostStatusMessage = response.statuses.get(hostAddress).value - - hostStatusMessage.game.players.length shouldEqual 2 - hostStatusMessage.game.players.map(_.playerId) shouldEqual List(welcomeMessage.playerId, hostWelcomeMessage.playerId) - } - - "does not send a game status message to the new player" in withAppContext { (context, db) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - val response = performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - response.statuses.keys should not contain playerAddress - } - - "persists the new player to the database" in withAppContext { (context, db) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - val response = performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - val welcomeMessage = response.messages.get(playerAddress).value - val playerDbs = db.getPlayers(welcomeMessage.gameId).value() - val playerDb = playerDbs.find(_.playerId == welcomeMessage.playerId.pid).value - - playerDb should have( - "gameId" as welcomeMessage.gameId.gid, - "playerId" as welcomeMessage.playerId.pid, - "playerAddress" as playerAddress.address, - "playerKey" as welcomeMessage.playerKey.key, - "screenName" as welcomeMessage.screenName, - ) + "informs the host that this player has joined" in appContextRes.use { + (context, _) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + response <- performJoinGame( + joinGameRequest(gameCode), + context(playerAddress) + ) + hostStatusMessage = response.statuses.get(hostAddress).value + playerWelcomeMessage = response.messages.get(playerAddress).value + } yield hostStatusMessage.action shouldEqual PlayerJoinedSummary( + playerWelcomeMessage.playerId + ) } - "does not persist player to the game's database entry" in withAppContext { (context, db) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - val response = performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - val welcomeMessage = response.messages.get(playerAddress).value - val gameDb = db.getGame(welcomeMessage.gameId).value().value - - gameDb.playerIds should not contain welcomeMessage.playerId.pid + "includes the correct players in a status message sent to the host" in appContextRes + .use { (context, _) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + response <- performJoinGame( + joinGameRequest(gameCode), + context(playerAddress) + ) + welcomeMessage = response.messages.head._2 + hostStatusMessage = response.statuses.get(hostAddress).value + + _ = hostStatusMessage.game.players.length shouldEqual 2 + _ = hostStatusMessage.game.players.map(_.playerId) shouldEqual List( + welcomeMessage.playerId, + hostWelcomeMessage.playerId + ) + } yield assert(true) + } + + "does not send a game status message to the new player" in appContextRes + .use { (context, db) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + response <- performJoinGame( + joinGameRequest(gameCode), + context(playerAddress) + ) + } yield response.statuses.keys should not contain playerAddress + } + + "persists the new player to the database" in appContextRes.use { + (context, db) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + response <- performJoinGame( + joinGameRequest(gameCode), + context(playerAddress) + ) + welcomeMessage = response.messages.get(playerAddress).value + playerDbs <- db.getPlayers(welcomeMessage.gameId) + playerDb = playerDbs + .find(_.playerId == welcomeMessage.playerId.pid) + .value + } yield playerDb should have( + "gameId" as welcomeMessage.gameId.gid, + "playerId" as welcomeMessage.playerId.pid, + "playerAddress" as playerAddress.address, + "playerKey" as welcomeMessage.playerKey.key, + "screenName" as welcomeMessage.screenName + ) } - "can join a second player to a game" in withAppContext { (context, _) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - performJoinGame(joinGameRequest(gameCode, "player 2"), context(PlayerAddress("player-2-addr"))).value() + "does not persist player to the game's database entry" in appContextRes + .use { (context, db) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + response <- performJoinGame( + joinGameRequest(gameCode), + context(playerAddress) + ) + welcomeMessage = response.messages.get(playerAddress).value + gameDbOpt <- db.getGame(welcomeMessage.gameId) + } yield gameDbOpt.value.playerIds should not contain welcomeMessage.playerId.pid + } + + "can join a second player to a game" in appContextRes.use { (context, _) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + _ <- performJoinGame(joinGameRequest(gameCode), context(playerAddress)) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 2"), + context(PlayerAddress("player-2-addr")) + ) + } yield assert(true) } - "can join a third player to a game" in withAppContext { (context, _) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - performJoinGame(joinGameRequest(gameCode, "player 2"), context(PlayerAddress("player-2-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 3"), context(PlayerAddress("player-3-addr"))).value() + "can join a third player to a game" in appContextRes.use { (context, _) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + _ <- performJoinGame(joinGameRequest(gameCode), context(playerAddress)) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 2"), + context(PlayerAddress("player-2-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 3"), + context(PlayerAddress("player-3-addr")) + ) + } yield assert(true) } - "can join loads of players to a game" in withAppContext { (context, _) => - val hostWelcomeMessage = createGameFixture(context).value() - val gameCode = hostWelcomeMessage.gameCode - - performJoinGame(joinGameRequest(gameCode), context(playerAddress)).value() - performJoinGame(joinGameRequest(gameCode, "player 2"), context(PlayerAddress("player-2-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 3"), context(PlayerAddress("player-3-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 4"), context(PlayerAddress("player-4-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 5"), context(PlayerAddress("player-5-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 6"), context(PlayerAddress("player-6-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 7"), context(PlayerAddress("player-7-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 8"), context(PlayerAddress("player-8-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 9"), context(PlayerAddress("player-9-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 10"), context(PlayerAddress("player-10-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 11"), context(PlayerAddress("player-11-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 12"), context(PlayerAddress("player-12-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 13"), context(PlayerAddress("player-13-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 14"), context(PlayerAddress("player-14-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 15"), context(PlayerAddress("player-15-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 16"), context(PlayerAddress("player-16-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 17"), context(PlayerAddress("player-17-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 18"), context(PlayerAddress("player-18-addr"))).value() - performJoinGame(joinGameRequest(gameCode, "player 19"), context(PlayerAddress("player-19-addr"))).value() + "can join loads of players to a game" in appContextRes.use { (context, _) => + for { + hostWelcomeMessage <- createGameFixture(context) + gameCode = hostWelcomeMessage.gameCode + + _ <- performJoinGame(joinGameRequest(gameCode), context(playerAddress)) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 2"), + context(PlayerAddress("player-2-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 3"), + context(PlayerAddress("player-3-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 4"), + context(PlayerAddress("player-4-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 5"), + context(PlayerAddress("player-5-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 6"), + context(PlayerAddress("player-6-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 7"), + context(PlayerAddress("player-7-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 8"), + context(PlayerAddress("player-8-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 9"), + context(PlayerAddress("player-9-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 10"), + context(PlayerAddress("player-10-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 11"), + context(PlayerAddress("player-11-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 12"), + context(PlayerAddress("player-12-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 13"), + context(PlayerAddress("player-13-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 14"), + context(PlayerAddress("player-14-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 15"), + context(PlayerAddress("player-15-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 16"), + context(PlayerAddress("player-16-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 17"), + context(PlayerAddress("player-17-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 18"), + context(PlayerAddress("player-18-addr")) + ) + _ <- performJoinGame( + joinGameRequest(gameCode, "player 19"), + context(PlayerAddress("player-19-addr")) + ) + } yield assert(true) } } "for an invalid request" - { - "fails if the screen name is already in use" in withAppContext { (context, _) => - val hostWelcome = createGameFixture(context).value() - val result = performJoinGame(s"""{"gameCode": "${hostWelcome.gameCode}", "screenName": "${hostWelcome.screenName}"}""", context(playerAddress)) - - result is AFailure - } - - "fails if this is a duplicate address" in withAppContext { (context, _) => - val hostWelcome = createGameFixture(context).value() - performJoinGame(s"""{"gameCode": "${hostWelcome.gameCode}", "screenName": "player 1"}""", context(playerAddress)).value() - val result = performJoinGame(s"""{"gameCode": "${hostWelcome.gameCode}", "screenName": "player 2"}""", context(playerAddress)) - - result is AFailure + "fails if the screen name is already in use" in appContextRes.use { + (context, _) => + val result = for { + hostWelcome <- createGameFixture(context) + _ <- performJoinGame( + s"""{"gameCode": "${hostWelcome.gameCode}", "screenName": "${hostWelcome.screenName}"}""", + context(playerAddress) + ) + } yield () + result.assertThrows[Failures] } - "fails (with field context) if the game code is empty" in withAppContext { (context, _) => - createGameFixture(context).value() - val result = performJoinGame("""{"gameCode": "", "screenName": "player name"}""", context(playerAddress)) - - val failureContexts = result.failures().failures.flatMap(_.context) - failureContexts should contain("gameCode") - } - - "fails (with field context) if the player's screen name is empty" in withAppContext { (context, _) => - val hostWelcome = createGameFixture(context).value() - val gameCode = hostWelcome.gameCode - val result = performJoinGame(s"""{"gameCode": "$gameCode", "screenName": ""}""", context(playerAddress)) - - val failureContexts = result.failures().failures.flatMap(_.context) - failureContexts should contain("screenName") + "fails if this is a duplicate address" in appContextRes.use { + (context, _) => + val result = for { + hostWelcome <- createGameFixture(context) + _ <- performJoinGame( + s"""{"gameCode": "${hostWelcome.gameCode}", "screenName": "player 1"}""", + context(playerAddress) + ) + _ <- performJoinGame( + s"""{"gameCode": "${hostWelcome.gameCode}", "screenName": "player 2"}""", + context(playerAddress) + ) + } yield () + result.assertThrows[Failures] } - "fails if the game code is wrong" in withAppContext { (context, _) => - val hostWelcome = createGameFixture(context).value() - val incorrectGameCode = - if (hostWelcome.gameId.gid.toLowerCase.startsWith("aaaaa")) - s"b${hostWelcome.gameCode}" - else - s"a${hostWelcome.gameCode}" - - val result = performJoinGame(s"""{"gameCode": "$incorrectGameCode", "screenName": "player"}""", context(playerAddress)) - - result is AFailure + "fails (with field context) if the game code is empty" in appContextRes + .use { (context, _) => + for { + _ <- createGameFixture(context) + result <- performJoinGame( + """{"gameCode": "", "screenName": "player name"}""", + context(playerAddress) + ).attempt + failureContexts = result.failures().failures.flatMap(_.context) + } yield failureContexts should contain("gameCode") + } + + "fails (with field context) if the player's screen name is empty" in appContextRes + .use { (context, _) => + for { + hostWelcome <- createGameFixture(context) + gameCode = hostWelcome.gameCode + result <- performJoinGame( + s"""{"gameCode": "$gameCode", "screenName": ""}""", + context(playerAddress) + ).attempt + + failureContexts = result.failures().failures.flatMap(_.context) + } yield failureContexts should contain("screenName") + } + + "fails if the game code is wrong" in appContextRes.use { (context, _) => + val result = for { + hostWelcome <- createGameFixture(context) + incorrectGameCode = + if (hostWelcome.gameId.gid.toLowerCase.startsWith("aaaaa")) + s"b${hostWelcome.gameCode}" + else + s"a${hostWelcome.gameCode}" + + result <- performJoinGame( + s"""{"gameCode": "$incorrectGameCode", "screenName": "player"}""", + context(playerAddress) + ) + } yield () + result.assertThrows[Failures] } - "fails if the JSON is not a valid join game request" in withAppContext { (context, _) => - val result = performJoinGame(s"""{"foo": 1}""", context(playerAddress)) - - result is AFailure + "fails if the JSON is not a valid join game request" in appContextRes.use { + (context, _) => + performJoinGame( + s"""{"foo": 1}""", + context(playerAddress) + ).assertThrows[Failures] } } - private def createGameFixture(contextBuilder: PlayerAddress => AppContext)(implicit pos: Position): Attempt[Welcome] = { + private def createGameFixture( + contextBuilder: PlayerAddress => AppContext[IO] + )(implicit pos: Position): IO[Welcome] = { for { - response <- performCreateGame(createGameRequest, contextBuilder(hostAddress), initialSeed) + response <- performCreateGame( + createGameRequest, + contextBuilder(hostAddress), + initialSeed + ) } yield { response.messages.get(hostAddress).value } } } object JoinGameIntegrationTest { - def joinGameRequest(gameCode: String, screenName: String = "player 1"): String = + def joinGameRequest( + gameCode: String, + screenName: String = "player 1" + ): String = s"""{ | "gameCode": "$gameCode", | "screenName": "$screenName" |}""".stripMargin - def performJoinGame(request: String, appContext: AppContext): Attempt[Response[Welcome]] = { + def performJoinGame( + request: String, + appContext: AppContext[IO] + ): IO[Response[Welcome]] = { PokerDot.joinGame(parseReq(request), appContext) } } diff --git a/integration/src/test/scala/io/adamnfish/pokerdot/integration/StartGameIntegrationTest.scala b/integration/src/test/scala/io/adamnfish/pokerdot/integration/StartGameIntegrationTest.scala index 4951c94..d56c5db 100644 --- a/integration/src/test/scala/io/adamnfish/pokerdot/integration/StartGameIntegrationTest.scala +++ b/integration/src/test/scala/io/adamnfish/pokerdot/integration/StartGameIntegrationTest.scala @@ -1,104 +1,192 @@ package io.adamnfish.pokerdot.integration import io.adamnfish.pokerdot.TestHelpers.parseReq -import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{createGameRequest, performCreateGame} -import io.adamnfish.pokerdot.integration.JoinGameIntegrationTest.{joinGameRequest, performJoinGame} -import io.adamnfish.pokerdot.integration.StartGameIntegrationTest.{performStartGame, startGameRequest} +import io.adamnfish.pokerdot.integration.CreateGameIntegrationTest.{ + createGameRequest, + performCreateGame +} +import io.adamnfish.pokerdot.integration.JoinGameIntegrationTest.{ + joinGameRequest, + performJoinGame +} +import io.adamnfish.pokerdot.integration.StartGameIntegrationTest.{ + performStartGame, + startGameRequest +} import io.adamnfish.pokerdot.logic.Games import io.adamnfish.pokerdot.models.Serialisation.RequestEncoders.encodeRequest -import io.adamnfish.pokerdot.models._ -import io.adamnfish.pokerdot.{PokerDot, TestClock, TestHelpers} -import io.circe.syntax._ +import io.adamnfish.pokerdot.models.* +import io.adamnfish.pokerdot.{PokerDot, TestHelpers, TestTime} +import io.circe.syntax.* +import cats.effect.* +import cats.effect.testing.scalatest.AsyncIOSpec import org.scalactic.source.Position import org.scalatest.OptionValues -import org.scalatest.freespec.AnyFreeSpec +import org.scalatest.freespec.AsyncFreeSpec import org.scalatest.matchers.should.Matchers - -class StartGameIntegrationTest extends AnyFreeSpec with Matchers with IntegrationComponents with TestHelpers with OptionValues { +class StartGameIntegrationTest + extends AsyncFreeSpec + with AsyncIOSpec + with Matchers + with IntegrationComponents + with TestHelpers + with OptionValues { val initialSeed = 1L val hostAddress = PlayerAddress("host-address") val player1Address = PlayerAddress("player-1-address") val player2Address = PlayerAddress("player-2-address") "for a basic start game call" - { - "is successful" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)) is ASuccess + "is successful" in appContextRes.use { (context, _) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + } yield assert(true) } "sends status messages" - { - "to every player" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - val response = performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - response.statuses.keys should(contain.allOf(hostAddress, player1Address, player2Address)) + "to every player" in appContextRes.use { (context, _) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + response <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + } yield response.statuses.keys should (contain.allOf( + hostAddress, + player1Address, + player2Address + )) } - "with the game started action" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - val response = performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - response.statuses.values.toList.map(_.action).distinct shouldEqual List(GameStartedSummary()) + "with the game started action" in appContextRes.use { (context, _) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + response <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + } yield response.statuses.values.toList + .map(_.action) + .distinct shouldEqual List(GameStartedSummary()) } } "the player order" - { - "is reflected in the status message's game" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List( - p2Welcome.playerId, p1Welcome.playerId, hostWelcome.playerId - ) - val response = performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - val gameStatusMessage = response.statuses.get(hostAddress).value - gameStatusMessage.game.players.map(_.playerId) shouldEqual playerOrder + "is reflected in the status message's game" in appContextRes.use { + (context, _) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + p2Welcome.playerId, + p1Welcome.playerId, + hostWelcome.playerId + ) + response <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + gameStatusMessage = response.statuses.get(hostAddress).value + } yield { + gameStatusMessage.game.players.map( + _.playerId + ) shouldEqual playerOrder + } } - "is persisted to the database" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List( - p2Welcome.playerId, p1Welcome.playerId, hostWelcome.playerId - ) - performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - val gameDb = db.getGame(hostWelcome.gameId).value().value - gameDb.playerIds shouldEqual playerOrder.map(_.pid) + "is persisted to the database" in appContextRes.use { (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + p2Welcome.playerId, + p1Welcome.playerId, + hostWelcome.playerId + ) + _ <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + gameDbOpt <- db.getGame(hostWelcome.gameId) + } yield gameDbOpt.value.playerIds shouldEqual playerOrder.map(_.pid) } - "determines the initial `inTurn` player - player after dealer and blinds" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List( - p2Welcome.playerId, p1Welcome.playerId, hostWelcome.playerId - ) - val response = performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - val gameStatusMessage = response.statuses.get(hostAddress).value - gameStatusMessage.game.inTurn shouldEqual Some(p2Welcome.playerId) + "determines the initial `inTurn` player - player after dealer and blinds" in appContextRes.use { + (context, _) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + p2Welcome.playerId, + p1Welcome.playerId, + hostWelcome.playerId + ) + response <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + gameStatusMessage = response.statuses.get(hostAddress).value + } yield gameStatusMessage.game.inTurn shouldEqual Some( + p2Welcome.playerId + ) } } - "persists the game to the database" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List( - p2Welcome.playerId, p1Welcome.playerId, hostWelcome.playerId - ) - performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - val gameDb = db.getGame(hostWelcome.gameId).value().value - gameDb should have( + "persists the game to the database" in appContextRes.use { (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + p2Welcome.playerId, + p1Welcome.playerId, + hostWelcome.playerId + ) + _ <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + gameDbOpt <- db.getGame(hostWelcome.gameId) + now <- context(hostAddress).time.now + } yield gameDbOpt.value should have( "started" as true, - "startTime" as TestClock.now(), - "expiry" as Games.expiryTime(TestClock.now()), - "button" as 0, + "startTime" as now, + "expiry" as Games.expiryTime(now), + "button" as 0 ) } - "persists the players to the database" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List( - p2Welcome.playerId, p1Welcome.playerId, hostWelcome.playerId - ) - performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - val playerDbs = db.getPlayers(hostWelcome.gameId).value() - playerDbs.map(_.playerId).toSet shouldEqual playerOrder.map(_.pid).toSet + "persists the players to the database" in appContextRes.use { (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + p2Welcome.playerId, + p1Welcome.playerId, + hostWelcome.playerId + ) + _ <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + playerDbs <- db.getPlayers(hostWelcome.gameId) + } yield playerDbs.map(_.playerId).toSet shouldEqual playerOrder + .map(_.pid) + .toSet } } @@ -108,37 +196,85 @@ class StartGameIntegrationTest extends AnyFreeSpec with Matchers with Integratio "if initial small blind is provided" - { val initialSmallBlind = 5 - "sends game status messages with the correct game state" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - val response = performStartGame(startGameRequest(hostWelcome, Some(initialStack), Some(initialSmallBlind), None, playerOrder), context(hostAddress)).value() - val gameStatus = response.statuses.get(hostAddress).value - gameStatus.game should have( - "round" as PreFlopSummary(), - "smallBlind" as initialSmallBlind, - // ... - ) + "sends game status messages with the correct game state" in appContextRes.use { + (context, _) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + response <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + Some(initialSmallBlind), + None, + playerOrder + ), + context(hostAddress) + ) + gameStatus = response.statuses.get(hostAddress).value + } yield gameStatus.game should have( + "round" as PreFlopSummary(), + "smallBlind" as initialSmallBlind + // ... + ) } - "persists the correct game state" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - performStartGame(startGameRequest(hostWelcome, Some(initialStack), Some(initialSmallBlind), None, playerOrder), context(hostAddress)).value() - val gameDb = db.getGame(hostWelcome.gameId).value().value - gameDb should have( + "persists the correct game state" in appContextRes.use { (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + Some(initialSmallBlind), + None, + playerOrder + ), + context(hostAddress) + ) + gameDbOpt <- db.getGame(hostWelcome.gameId) + } yield gameDbOpt.value should have( "smallBlind" as initialSmallBlind, "timer" as None, "trackStacks" as true, - "phase" as PreFlop, + "phase" as PreFlop ) } - "saves the initial stack config to each player" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - performStartGame(startGameRequest(hostWelcome, Some(initialStack), Some(initialSmallBlind), None, playerOrder), context(hostAddress)).value() - val playerDbs = db.getPlayers(hostWelcome.gameId).value() - playerDbs.map(pdb => pdb.stack + pdb.bet).distinct shouldEqual List(initialStack) + "saves the initial stack config to each player" in appContextRes.use { + (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + Some(initialSmallBlind), + None, + playerOrder + ), + context(hostAddress) + ) + playerDbs <- db.getPlayers(hostWelcome.gameId) + } yield playerDbs + .map(pdb => pdb.stack + pdb.bet) + .distinct shouldEqual List( + initialStack + ) } } @@ -148,112 +284,247 @@ class StartGameIntegrationTest extends AnyFreeSpec with Matchers with Integratio RoundLevel(300, 10), BreakLevel(150), RoundLevel(450, 20), - RoundLevel(450, 50), + RoundLevel(450, 50) ) - "sends game status messages with the correct game state" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - val response = performStartGame(startGameRequest(hostWelcome, Some(initialStack), None, Some(timerConfig), playerOrder), context(hostAddress)).value() - val gameStatus = response.statuses.get(hostAddress).value - gameStatus.game should have( - "round" as PreFlopSummary(), - "smallBlind" as 5, - // ... - ) + "sends game status messages with the correct game state" in appContextRes.use { + (context, _) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + response <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + None, + Some(timerConfig), + playerOrder + ), + context(hostAddress) + ) + gameStatus = response.statuses.get(hostAddress).value + } yield gameStatus.game should have( + "round" as PreFlopSummary(), + "smallBlind" as 5 + // ... + ) } - "persists the game information, including the time config" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - performStartGame(startGameRequest(hostWelcome, Some(initialStack), None, Some(timerConfig), playerOrder), context(hostAddress)).value() - val gameDb = db.getGame(hostWelcome.gameId).value().value - gameDb should have( - "smallBlind" as 5, - "trackStacks" as true, - "phase" as PreFlop, - "timer" as Some( - TimerStatus( - 0L, None, timerConfig + "persists the game information, including the time config" in appContextRes.use { + (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId ) - ), - ) + _ <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + None, + Some(timerConfig), + playerOrder + ), + context(hostAddress) + ) + gameDbOpt <- db.getGame(hostWelcome.gameId) + } yield gameDbOpt.value should have( + "smallBlind" as 5, + "trackStacks" as true, + "phase" as PreFlop, + "timer" as Some( + TimerStatus( + 0L, + None, + timerConfig + ) + ) + ) } - "saves the initial stack config to each player" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - performStartGame(startGameRequest(hostWelcome, Some(initialStack), None, Some(timerConfig), playerOrder), context(hostAddress)).value() - val playerDbs = db.getPlayers(hostWelcome.gameId).value() - playerDbs.map(pdb => pdb.stack + pdb.bet).distinct shouldEqual List(initialStack) + "saves the initial stack config to each player" in appContextRes.use { + (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + None, + Some(timerConfig), + playerOrder + ), + context(hostAddress) + ) + playerDbs <- db.getPlayers(hostWelcome.gameId) + } yield playerDbs + .map(pdb => pdb.stack + pdb.bet) + .distinct shouldEqual List(initialStack) } - "player blind payments should be persisted" in withAppContext { (context, db) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - performStartGame(startGameRequest(hostWelcome, Some(initialStack), None, Some(timerConfig), playerOrder), context(hostAddress)).value() - val playerDbs = db.getPlayers(hostWelcome.gameId).value() - // order playerdb results to match game order - playerDbs.sortBy(pdb => playerOrder.map(_.pid).indexOf(pdb.playerId)) - .map(pdb => pdb.bet) shouldEqual List(0, 5, 10) + "player blind payments should be persisted" in appContextRes.use { + (context, db) => + for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + None, + Some(timerConfig), + playerOrder + ), + context(hostAddress) + ) + playerDbs <- db.getPlayers(hostWelcome.gameId) + } yield { + // order playerdb results to match game order + playerDbs + .sortBy(pdb => playerOrder.map(_.pid).indexOf(pdb.playerId)) + .map(pdb => pdb.bet) shouldEqual List(0, 5, 10) + } } } - "fails if neither timer config nor initial small blind are provided" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - val result = performStartGame(startGameRequest(hostWelcome, Some(initialStack), None, None, playerOrder), context(hostAddress)) - result is AFailure + "fails if neither timer config nor initial small blind are provided" in appContextRes.use { + (context, _) => + val result = for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest( + hostWelcome, + Some(initialStack), + None, + None, + playerOrder + ), + context(hostAddress) + ) + } yield () + result.assertThrows[Failures] } } "fails, when" - { - "the game has already started" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)).value() - // start game a second time - val result = performStartGame(startGameRequest(hostWelcome, None, None, None, playerOrder), context(hostAddress)) - result is AFailure + "the game has already started" in appContextRes.use { (context, _) => + val result = for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ).assertNoException + _ <- performStartGame( + startGameRequest(hostWelcome, None, None, None, playerOrder), + context(hostAddress) + ) + } yield () + result.assertThrows[Failures] } - "the request is not a valid start game request" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val result = performStartGame("""{"foo":"bar"}""", context(hostAddress)) - result is AFailure + "the request is not a valid start game request" in appContextRes.use { + (context, _) => + val result = for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame("""{"foo":"bar"}""", context(hostAddress)) + } yield () + result.assertThrows[Failures] } - "the player making the call is not the host" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - val result = performStartGame(startGameRequest(p1Welcome, None, None, None, playerOrder), context(player1Address)) - result is AFailure + "the player making the call is not the host" in appContextRes.use { + (context, _) => + val result = for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + _ <- performStartGame( + startGameRequest(p1Welcome, None, None, None, playerOrder), + context(player1Address) + ) + } yield () + result.assertThrows[Failures] } - "this player has not joined this game" in withAppContext { (context, _) => - val (hostWelcome, p1Welcome, p2Welcome) = gameFixture(context).value() - val playerOrder = List(hostWelcome.playerId, p1Welcome.playerId, p2Welcome.playerId) - val playerAddress = PlayerAddress("another-address") - val request = StartGame( - hostWelcome.gameId, - PlayerId("different-id"), - PlayerKey("different-key"), - None, None, None, - playerOrder, - ) - val result = performStartGame(encodeRequest(request).noSpaces, context(playerAddress)) - result is AFailure + "this player has not joined this game" in appContextRes.use { (context, _) => + val result = for { + (hostWelcome, p1Welcome, p2Welcome) <- gameFixture(context) + playerOrder = List( + hostWelcome.playerId, + p1Welcome.playerId, + p2Welcome.playerId + ) + playerAddress = PlayerAddress("another-address") + request = StartGame( + hostWelcome.gameId, + PlayerId("different-id"), + PlayerKey("different-key"), + None, + None, + None, + playerOrder + ) + _ <- performStartGame( + encodeRequest(request).noSpaces, + context(playerAddress) + ) + } yield () + result.assertThrows[Failures] } } - private def gameFixture(contextBuilder: PlayerAddress => AppContext)(implicit pos: Position): Attempt[(Welcome, Welcome, Welcome)] = { + private def gameFixture( + contextBuilder: PlayerAddress => AppContext[IO] + )(implicit pos: Position): IO[(Welcome, Welcome, Welcome)] = { for { - hostResponse <- performCreateGame(createGameRequest, contextBuilder(hostAddress), initialSeed) + hostResponse <- performCreateGame( + createGameRequest, + contextBuilder(hostAddress), + initialSeed + ) hostWelcome = hostResponse.messages.get(hostAddress).value gameCode = hostWelcome.gameCode - p1JoinResponse <- performJoinGame(joinGameRequest(gameCode, "player-1"), contextBuilder(player1Address)) + p1JoinResponse <- performJoinGame( + joinGameRequest(gameCode, "player-1"), + contextBuilder(player1Address) + ) p1Welcome = p1JoinResponse.messages.get(player1Address).value - p2JoinResponse <- performJoinGame(joinGameRequest(gameCode, "player-2"), contextBuilder(player2Address)) + p2JoinResponse <- performJoinGame( + joinGameRequest(gameCode, "player-2"), + contextBuilder(player2Address) + ) p2Welcome = p2JoinResponse.messages.get(player2Address).value } yield (hostWelcome, p1Welcome, p2Welcome) } @@ -261,11 +532,11 @@ class StartGameIntegrationTest extends AnyFreeSpec with Matchers with Integratio object StartGameIntegrationTest { def startGameRequest( - welcome: Welcome, - startingStack: Option[Int], - initialSmallBlind: Option[Int], - timerConfig: Option[List[TimerLevel]], - playerOrder: List[PlayerId], + welcome: Welcome, + startingStack: Option[Int], + initialSmallBlind: Option[Int], + timerConfig: Option[List[TimerLevel]], + playerOrder: List[PlayerId] ): String = { val request = StartGame( welcome.gameId, @@ -274,12 +545,15 @@ object StartGameIntegrationTest { startingStack, initialSmallBlind, timerConfig, - playerOrder, + playerOrder ) encodeRequest(request).noSpaces } - def performStartGame(request: String, appContext: AppContext): Attempt[Response[GameStatus]] = { + def performStartGame( + request: String, + appContext: AppContext[IO] + ): IO[Response[GameStatus]] = { PokerDot.startGame(parseReq(request), appContext) } -} \ No newline at end of file +} diff --git a/lambda/src/main/scala/io/adamnfish/pokerdot/AwsMessaging.scala b/lambda/src/main/scala/io/adamnfish/pokerdot/AwsMessaging.scala index b73e369..d5e8c6d 100644 --- a/lambda/src/main/scala/io/adamnfish/pokerdot/AwsMessaging.scala +++ b/lambda/src/main/scala/io/adamnfish/pokerdot/AwsMessaging.scala @@ -1,39 +1,47 @@ package io.adamnfish.pokerdot + +import cats.MonadThrow +import cats.effect.kernel.Sync import com.typesafe.scalalogging.LazyLogging -import io.adamnfish.pokerdot.models._ +import io.adamnfish.pokerdot.models.* import io.adamnfish.pokerdot.services.Messaging +import org.typelevel.log4cats.Logger import software.amazon.awssdk.core.SdkBytes import software.amazon.awssdk.services.apigatewaymanagementapi.ApiGatewayManagementApiClient import software.amazon.awssdk.services.apigatewaymanagementapi.model.PostToConnectionRequest -import zio.ZIO import scala.util.control.NonFatal +import cats.* +import cats.implicits.* + -class AwsMessaging(client: ApiGatewayManagementApiClient, traceId: TraceId) extends Messaging with LazyLogging { - override def sendMessage(playerAddress: PlayerAddress, message: Message): Attempt[Unit] = { +class AwsMessaging[F[_] : MonadThrow : Logger : Sync](client: ApiGatewayManagementApiClient, traceId: TraceId) extends Messaging[F] { + override def sendMessage(playerAddress: PlayerAddress, message: Message): F[Unit] = { send(playerAddress, Serialisation.encodeMessage(message)) } - override def sendError(playerAddress: PlayerAddress, message: Failures): Attempt[Unit] = { + override def sendError(playerAddress: PlayerAddress, message: Failures): F[Unit] = { send(playerAddress, Serialisation.encodeFailure(message)) } - private def send(playerAddress: PlayerAddress, message: String): Attempt[Unit] = { - logger.debug(s"<${traceId.tid}> Message {${playerAddress.address}}: $message") - val request = PostToConnectionRequest.builder - .connectionId(playerAddress.address) - .data(SdkBytes.fromByteArray(message.getBytes("UTF-8"))) - .build() - ZIO.attempt(client.postToConnection(request)).mapError { - case NonFatal(e) => - Failures( - s"AWS messaging failure ${e.getMessage}", - "Unable to send message to player", - None, - Some(e), - internal = true, - ) - }.map(_ => ()) + private def send(playerAddress: PlayerAddress, message: String): F[Unit] = { + for + _ <- Logger[F].debug(s"<${traceId.tid}> Message {${playerAddress.address}}: $message") + request = PostToConnectionRequest.builder + .connectionId(playerAddress.address) + .data(SdkBytes.fromByteArray(message.getBytes("UTF-8"))) + .build() + _ <- Sync[F].blocking(client.postToConnection(request)).adaptError { + case NonFatal(e) => + Failures( + s"AWS messaging failure ${e.getMessage}", + "Unable to send message to player", + None, + Some(e), + internal = true, + ) + } + yield () } } diff --git a/lambda/src/main/scala/io/adamnfish/pokerdot/Lambda.scala b/lambda/src/main/scala/io/adamnfish/pokerdot/Lambda.scala index e43e7e8..5cd6779 100644 --- a/lambda/src/main/scala/io/adamnfish/pokerdot/Lambda.scala +++ b/lambda/src/main/scala/io/adamnfish/pokerdot/Lambda.scala @@ -1,120 +1,118 @@ package io.adamnfish.pokerdot +import cats.effect.std.Env +import cats.effect.unsafe.implicits.global +import cats.effect.{IO, Resource} +import com.amazonaws.services.lambda.runtime.Context as AwsContext import com.amazonaws.services.lambda.runtime.events.{APIGatewayV2WebSocketEvent, APIGatewayV2WebSocketResponse} -import com.amazonaws.services.lambda.runtime.{Context => AwsContext} import com.amazonaws.xray.AWSXRay -import com.amazonaws.xray.entities.Subsegment -import com.typesafe.scalalogging.LazyLogging import io.adamnfish.pokerdot.models.{AppContext, PlayerAddress, TraceId} import io.adamnfish.pokerdot.persistence.DynamoDbDatabase -import io.adamnfish.pokerdot.services.{Clock, RandomRng} +import io.adamnfish.pokerdot.services.{RandomRng, RealTime} +import org.typelevel.log4cats.Logger +import org.typelevel.log4cats.slf4j.Slf4jLogger import software.amazon.awssdk.auth.credentials.EnvironmentVariableCredentialsProvider +import software.amazon.awssdk.http.crt.AwsCrtAsyncHttpClient import software.amazon.awssdk.http.urlconnection.UrlConnectionHttpClient import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.apigatewaymanagementapi.ApiGatewayManagementApiClient -import software.amazon.awssdk.services.dynamodb.DynamoDbClient -import zio.{Exit, Runtime, Unsafe, ZIO} +import software.amazon.awssdk.services.dynamodb.{DynamoDbAsyncClient, DynamoDbClient} +import java.time.Duration import java.net.URI -import scala.jdk.CollectionConverters._ +import scala.jdk.CollectionConverters.* import scala.util.Properties -class Lambda extends LazyLogging { - // initialise everything on every request so that ENV vars can be updated without restarting the lambda - val appContextBuilder: (PlayerAddress, TraceId) => AppContext = { - (for { - // AWS ASK configuration - regionStr <- Properties.envOrNone("REGION") - .toRight("region not configured") - region = Region.of(regionStr) - // API Gateway client configuration - apiGatewayEndpointStr <- Properties.envOrNone("API_ORIGIN_LOCATION") - .toRight("API Gateway endpoint name not configured") - apiGatewayEndpointUri = new URI(s"https://$apiGatewayEndpointStr") - // table names - gamesTableName <- Properties.envOrNone("GAMES_TABLE") - .toRight("games table name not configured") - playersTableName <- Properties.envOrNone("PLAYERS_TABLE") - .toRight("players table name not configured") - // create SDK clients - apiGatewayManagementClient = ApiGatewayManagementApiClient.builder() - .endpointOverride(apiGatewayEndpointUri) - .region(region) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .build() - dynamoDbClient = DynamoDbClient.builder() - .region(region) - .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) - .httpClientBuilder(UrlConnectionHttpClient.builder()) - .build() - db = new DynamoDbDatabase(dynamoDbClient, gamesTableName, playersTableName) - rng = new RandomRng - } yield { (playerAddress: PlayerAddress, traceId: TraceId) => - val messaging = new AwsMessaging(apiGatewayManagementClient, traceId) - AppContext(playerAddress, traceId, db, messaging, Clock, rng) - }).fold( - { errMsg => - throw new RuntimeException(errMsg) - }, - identity - ) - } +class Lambda: + implicit def logger: Logger[IO] = Slf4jLogger.getLogger[IO] - def handleRequest(event: APIGatewayV2WebSocketEvent, awsContext: AwsContext): APIGatewayV2WebSocketResponse = { - val subsegment = AWSXRay.beginSubsegment("io.adamnfish.pokerdot.Lambda::handleRequest") - val traceId = AWSXRay.currentFormattedId() - logger.info(s"<$traceId> route: ${event.getRequestContext.getRouteKey}") - // Debugging - // logger.debug(s"<$handlerTraceId> request body: ${event.getBody}") - // logger.debug(s"<$handlerTraceId> connection ID: ${event.getRequestContext.getConnectionId}") + val app: Resource[IO, (PlayerAddress, TraceId) => AppContext[IO]] = + for + // DB table names + gamesTableName <- Env[IO].get("GAMES_TABLE").flatMap { + case Some(gamesTableName) => IO.pure(gamesTableName) + case None => IO.raiseError(new RuntimeException("GAMES_TABLE not set")) + }.toResource + playersTableName <- Env[IO].get("PLAYERS_TABLE").flatMap { + case Some(playersTableName) => IO.pure(playersTableName) + case None => IO.raiseError(new RuntimeException("PLAYERS_TABLE not set")) + }.toResource + // TODO: maybe use this Async http client for all SDKs and switch to async everywhere? + crtAsyncHttpClient <- Resource.make(IO { + AwsCrtAsyncHttpClient.builder() + .connectionTimeout(Duration.ofSeconds(3)) + .maxConcurrency(100) + .build() + })(client => IO(client.close())) + // AWS clients + dynamoDbClient <- Resource.make(IO { + DynamoDbAsyncClient.builder() + .region(Region.EU_WEST_1) + .httpClient(crtAsyncHttpClient) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .build() + })(client => IO(client.close())) + apiGatewayManagementApiClient <- Resource.make(IO { + ApiGatewayManagementApiClient.builder() + .region(Region.EU_WEST_1) + .httpClient(UrlConnectionHttpClient.create()) + .credentialsProvider(EnvironmentVariableCredentialsProvider.create()) + .endpointOverride(URI.create(Properties.envOrElse("APIGATEWAY_ENDPOINT", "http://localhost:3001"))) + .build() + })(client => IO(client.close())) + // create services + database = new DynamoDbDatabase[IO](dynamoDbClient, gamesTableName, playersTableName) + time = new RealTime[IO] + rng = new RandomRng[IO] + yield (playerAddress, traceId) => + val messaging = new AwsMessaging[IO](apiGatewayManagementApiClient, traceId) + AppContext(playerAddress, traceId, database, messaging, time, rng) + + def handleRequest(event: APIGatewayV2WebSocketEvent, context: AwsContext): APIGatewayV2WebSocketResponse = + program(event, context).unsafeRunSync() - event.getRequestContext.getRouteKey match { - case "$connect" => - // ignore this for now - case "$disconnect" => - // ignore this for now - case "$default" => - val playerAddress = PlayerAddress(event.getRequestContext.getConnectionId) - val appContext = appContextBuilder(playerAddress, TraceId(traceId)) - - Unsafe.unsafe { implicit unsafe => - Runtime.default.unsafe.run( - PokerDot.pokerdot(event.getBody, appContext) - ) - } match { - case Exit.Success(operation) => - logger.info(s"<$traceId> completed $operation") - subsegment.putAnnotation("operation", operation) - case Exit.Failure(cause) => - cause.failures.foreach { fs => - logger.error(s"<$traceId> error: ${fs.logString}") - fs.exception match { - case Some(e) => - logger.error(s"<$traceId> exception: ${e.getMessage}", e) - subsegment.addException(e) - case None => - subsegment.setFault(true) - } - } - cause.defects.foreach { err => - logger.error(s"<$traceId> Fatal error: ${err.getMessage}", err) - subsegment.addException(err) - } + def program(event: APIGatewayV2WebSocketEvent, context: AwsContext): IO[APIGatewayV2WebSocketResponse] = + app.use: appContextBuilder => + for + subsegment <- IO.blocking(AWSXRay.beginSubsegment("io.adamnfish.pokerdot.Lambda::handleRequest")) + traceId <- IO.blocking(AWSXRay.currentFormattedId()) + _ <- logger.info(s"<$traceId> route: ${event.getRequestContext.getRouteKey}") + + _ <- event.getRequestContext.getRouteKey match + case "$connect" => + // ignore this for now + IO.unit + case "$disconnect" => + // ignore this for now + IO.unit + case "$default" => + val playerAddress = PlayerAddress(event.getRequestContext.getConnectionId) + val appContext = appContextBuilder(playerAddress, TraceId(traceId)) + for + operation <- PokerDot.pokerdot[IO](event.getBody, appContext) + .onError { e => + logger.error(e)(s"<$traceId> Error: ${e.getMessage}") &> + IO.blocking(subsegment.addException(e)) + } + _ <- logger.info(s"<$traceId> completed $operation") + _ <- IO.blocking(subsegment.putAnnotation("operation", operation)) + yield + () + case routeKey => + logger.error(s"<$traceId> Unhandled route: $routeKey") + + _ <- logger.info(s"<$traceId> Finished handling request") + + _ <- IO.blocking { + subsegment.end() + AWSXRay.endSubsegment(subsegment) + AWSXRay.sendSubsegment(subsegment) } - logger.info(s"<$traceId> Finished handling request") - } - - subsegment.end() - AWSXRay.endSubsegment(subsegment) - AWSXRay.sendSubsegment(subsegment) - - val response = new APIGatewayV2WebSocketResponse() - response.setStatusCode(200) - response.setHeaders(Map("content-type" -> "application/json").asJava) - response.setBody("") - response - } -} + yield + val response = new APIGatewayV2WebSocketResponse() + response.setStatusCode(200) + response.setHeaders(Map("content-type" -> "application/json").asJava) + response.setBody("") + response diff --git a/lambda/src/main/scala/io/adamnfish/pokerdot/Tracing.scala b/lambda/src/main/scala/io/adamnfish/pokerdot/Tracing.scala new file mode 100644 index 0000000..ac2e606 --- /dev/null +++ b/lambda/src/main/scala/io/adamnfish/pokerdot/Tracing.scala @@ -0,0 +1,14 @@ +package io.adamnfish.pokerdot + +import cats.Applicative +import io.adamnfish.pokerdot.models.TraceId + + +trait Tracing[F[_]]: + val traceId: F[TraceId] + +object Tracing: + def apply[F[_]](implicit T: Tracing[F]): Tracing[F] = T + +class AwsTracing[F[_] : Applicative](_traceId: TraceId) extends Tracing[F]: + override val traceId: F[TraceId] = Applicative[F].pure(_traceId) diff --git a/project/build.properties b/project/build.properties index abbbce5..cc68b53 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=1.9.8 +sbt.version=1.10.11