diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edf6a28b..6d018e86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -96,6 +96,10 @@ jobs: if: matrix.java == 'temurin@8' run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' doc' + - name: Check scalafix lints + if: matrix.java == 'temurin@8' && !startsWith(matrix.scala, '3.') + run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' ''scalafixAll --check''' + - name: Check unused compile dependencies if: matrix.java == 'temurin@8' run: 'sbt ''++${{ matrix.scala }}'' ''set Global / useJSEnv := JSEnv.${{ matrix.jsenv }}'' unusedCompileDependenciesTest' diff --git a/.scalafix.conf b/.scalafix.conf new file mode 100644 index 00000000..ab1b91a6 --- /dev/null +++ b/.scalafix.conf @@ -0,0 +1,15 @@ +rules = [ + Http4sFs2Linters + Http4sGeneralLinters + Http4sUseLiteralsSyntax + LeakingImplicitClassVal + ExplicitResultTypes + OrganizeImports +] + +triggered.rules = [ + Http4sFs2Linters + Http4sGeneralLinters + Http4sUseLiteralsSyntax + LeakingImplicitClassVal +] diff --git a/.scalafmt.conf b/.scalafmt.conf index 7e57088d..e1c3a6dd 100644 --- a/.scalafmt.conf +++ b/.scalafmt.conf @@ -1,4 +1,4 @@ -version = 3.5.2 +version = 3.5.8 runner.dialect = Scala213Source3 diff --git a/README.md b/README.md index a1ba0993..d2f66858 100644 --- a/README.md +++ b/README.md @@ -4,9 +4,9 @@ Use http4s in your browser with Scala.js! Check out the [interactive examples](h Features: -* A [`Client` implementation](fetch.md) backed by [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) -* A [`WSClient` implementation](websocket.md) backed by [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket$.html) -* A [`Service Worker` integration](serviceworker.md) to install your `HttpRoutes` as a [`FetchEvent` handler](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/onfetch) +* A [`Client` implementation](https://http4s.github.io/http4s-dom/fetch.html) backed by [`fetch`](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API) +* A [`WSClient` implementation](https://http4s.github.io/http4s-dom/websocket.html) backed by [`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) +* A [`Service Worker` integration](https://http4s.github.io/http4s-dom/serviceworker.html) to install your `HttpRoutes` as a [`FetchEvent` handler](https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerGlobalScope/fetch_event) * Encoders for [`File`](https://developer.mozilla.org/en-US/docs/Web/API/File), [`Blob`](https://developer.mozilla.org/en-US/docs/Web/API/Blob) and [`ReadableStream`](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) Notably, http4s-dom can also be used to create _serverless_ apps with [Cloudflare Workers](https://workers.cloudflare.com) which have adopted the same APIs used in the browser! @@ -18,5 +18,5 @@ It is also possible to use the `FetchClient` in Node.js v18+, which added [exper [![http4s-dom Scala version support](https://index.scala-lang.org/http4s/http4s-dom/http4s-dom/latest.svg)](https://index.scala-lang.org/http4s/http4s-dom/http4s-dom) ```scala -libraryDependencies += "org.http4s" %%% "http4s-dom" % "0.2.2" +libraryDependencies += "org.http4s" %%% "http4s-dom" % "0.2.3" ``` diff --git a/build.sbt b/build.sbt index 3884514d..3cc16ac1 100644 --- a/build.sbt +++ b/build.sbt @@ -120,9 +120,9 @@ ThisBuild / Test / jsEnv := { } } -val catsEffectVersion = "3.3.12" -val fs2Version = "3.2.7" -val http4sVersion = "1.0.0-M33" +val catsEffectVersion = "3.3.13" +val fs2Version = "3.2.9" +val http4sVersion = "1.0.0-M34" val scalaJSDomVersion = "2.2.0" val circeVersion = "0.14.2" val munitVersion = "0.7.29" diff --git a/docs/fetch.md b/docs/fetch.md index cb7e86c8..d9050a02 100644 --- a/docs/fetch.md +++ b/docs/fetch.md @@ -4,13 +4,17 @@ The @:api(org.http4s.dom.FetchClientBuilder) creates a standard http4s @:api(org ## Example +```scala +libraryDependencies += "org.http4s" %%% "http4s-circe" % "@HTTP4S_VERSION@" +libraryDependencies += "io.circe" %%% "circe-generic" % "@CIRCE_VERSION@" +``` + ```scala mdoc:js -
-

- I'm bored. -

- -

+
+

How many stars?

+ + +
--- import cats.effect._ @@ -22,17 +26,24 @@ import org.scalajs.dom._ val client = FetchClientBuilder[IO].create -val activityElement = document.getElementById("activity") - -case class Activity(activity: String) - -val fetchActivity: IO[Unit] = for { - _ <- IO(activityElement.innerHTML = "fetching...") - activity <- client.expect[Activity]("https://www.boredapi.com/api/activity") - _ <- IO(activityElement.innerHTML = activity.activity) +val repoName = document.getElementById("repo").asInstanceOf[HTMLInputElement] +val repoStars = document.getElementById("stars").asInstanceOf[HTMLElement] + +case class Repo(stargazers_count: Int) + +val fetchRepo: IO[Unit] = for { + _ <- IO(repoStars.innerHTML = "fetching...") + name <- IO(repoName.value) + repo <- client.expect[Repo](s"https://api.github.com/repos/$name").attempt + _ <- IO { + repo match { + case Right(Repo(stars)) => repoStars.innerHTML = s"$stars ★" + case Left(_) => repoStars.innerHTML = s"Not found :(" + } + } } yield () val button = document.getElementById("button").asInstanceOf[HTMLButtonElement] -button.onclick = _ => fetchActivity.unsafeRunAndForget() +button.onclick = _ => fetchRepo.unsafeRunAndForget() ``` diff --git a/docs/index.md b/docs/index.md index 354adfca..f46516ef 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,8 +21,4 @@ libraryDependencies += "org.http4s" %%% "http4s-dom" % "@VERSION@" // recommended, brings in the latest client module libraryDependencies += "org.http4s" %%% "http4s-client" % "@HTTP4S_VERSION@" - -// optional, for JSON support -libraryDependencies += "org.http4s" %%% "http4s-circe" % "@HTTP4S_VERSION@" -libraryDependencies += "io.circe" %%% "circe-generic" % "@CIRCE_VERSION@" ``` diff --git a/dom/src/main/scala/org/http4s/dom/FetchClient.scala b/dom/src/main/scala/org/http4s/dom/FetchClient.scala index 71fcce9f..11848da5 100644 --- a/dom/src/main/scala/org/http4s/dom/FetchClient.scala +++ b/dom/src/main/scala/org/http4s/dom/FetchClient.scala @@ -41,13 +41,13 @@ private[dom] object FetchClient { requestTimeout: Duration, options: FetchOptions )(implicit F: Async[F]): Client[F] = Client[F] { (req: Request[F]) => - Resource.eval { - req.entity match { - case Entity.Empty => None.pure - case Entity.Strict(chunk) => Some(chunk).pure + Resource.eval(req.toStrict(None)).flatMap { req => + val body = req.entity match { + case Entity.Empty => None + case Entity.Strict(chunk) => Some(chunk) case default => default.body.chunkAll.filter(_.nonEmpty).compile.last } - } flatMap { body => + Resource .makeCaseFull { (poll: Poll[F]) => F.delay(new AbortController()).flatMap { abortController => @@ -77,8 +77,9 @@ private[dom] object FetchClient { .foreach(referrer => init.referrer = referrer.renderString) mergedOptions.referrerPolicy.foreach(init.referrerPolicy = _) - val fetch = poll(F.fromPromise(F.delay(Fetch.fetch(req.uri.renderString, init)))) - .onCancel(F.delay(abortController.abort())) + val fetch = + poll(F.fromPromise(F.delay(Fetch.fetch(req.uri.renderString, init)))) + .onCancel(F.delay(abortController.abort())) requestTimeout match { case d: FiniteDuration => @@ -95,7 +96,7 @@ private[dom] object FetchClient { case (r, exitCase) => OptionT.fromOption(Option(r.body)).foreachF(closeReadableStream(_, exitCase)) } - .evalMap(fromDomResponse[F]) + .evalMap(fromDomResponse[F](_)) } } diff --git a/dom/src/main/scala/org/http4s/dom/ServiceWorker.scala b/dom/src/main/scala/org/http4s/dom/ServiceWorker.scala index ccd710a3..17611d46 100644 --- a/dom/src/main/scala/org/http4s/dom/ServiceWorker.scala +++ b/dom/src/main/scala/org/http4s/dom/ServiceWorker.scala @@ -28,8 +28,8 @@ import cats.effect.unsafe.IORuntime import cats.syntax.all._ import fs2.Chunk import org.scalajs.dom.Fetch -import org.scalajs.dom.ResponseInit import org.scalajs.dom.FetchEvent +import org.scalajs.dom.ResponseInit import org.scalajs.dom.ServiceWorkerGlobalScope import org.scalajs.dom.{Response => DomResponse} import org.typelevel.vault.Key diff --git a/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala b/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala index 41ce3dfb..3a96d225 100644 --- a/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala +++ b/dom/src/main/scala/org/http4s/dom/WebSocketClient.scala @@ -26,7 +26,6 @@ import cats.effect.std.Queue import cats.effect.std.Semaphore import cats.effect.syntax.all._ import cats.syntax.all._ -import fs2.INothing import fs2.Stream import org.http4s.Method import org.http4s.client.websocket.WSClientHighLevel @@ -51,7 +50,7 @@ object WebSocketClient { dispatcher <- Dispatcher[F] messages <- Queue.unbounded[F, Option[MessageEvent]].toResource semaphore <- Semaphore[F](1).toResource - error <- F.deferred[Either[Throwable, INothing]].toResource + error <- F.deferred[Throwable].toResource close <- F.deferred[CloseEvent].toResource ws <- Resource.makeCase { F.async_[WebSocket] { cb => @@ -69,8 +68,7 @@ object WebSocketClient { ws.onopen = { _ => ws.onerror = // replace the error handler - e => - dispatcher.unsafeRunAndForget(error.complete(Left(js.JavaScriptException(e)))) + e => dispatcher.unsafeRunAndForget(error.complete(js.JavaScriptException(e))) cb(Right(ws)) } @@ -122,7 +120,7 @@ object WebSocketClient { def receive: F[Option[WSDataFrame]] = semaphore .permit .surround(OptionT(messages.take).map(decodeMessage).value) - .race(error.get.rethrow) + .race(error.get.flatMap(F.raiseError[Option[WSDataFrame]])) .map(_.merge) override def receiveStream: Stream[F, WSDataFrame] = @@ -130,7 +128,7 @@ object WebSocketClient { .resource(semaphore.permit) .flatMap(_ => Stream.fromQueueNoneTerminated(messages)) .map(decodeMessage) - .concurrently(Stream.exec(error.get.rethrow.widen)) + .concurrently(Stream.exec(error.get.flatMap(F.raiseError))) private def decodeMessage(e: MessageEvent): WSDataFrame = e.data match { @@ -156,7 +154,7 @@ object WebSocketClient { } private def errorOr(fu: F[Unit]): F[Unit] = error.tryGet.flatMap { - case Some(error) => F.fromEither[Unit](error) + case Some(error) => F.raiseError(error) case None => fu } diff --git a/project/plugins.sbt b/project/plugins.sbt index 4feb1d15..b838f0b8 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,4 +1,4 @@ -val http4sVersion = "0.23.12" +val http4sVersion = "0.23.13" enablePlugins(BuildInfoPlugin) buildInfoKeys += "http4sVersion" -> http4sVersion @@ -7,7 +7,7 @@ libraryDependencies += "org.scala-js" %% "scalajs-env-selenium" % "1.1.1" libraryDependencies += "org.http4s" %% "http4s-dsl" % http4sVersion libraryDependencies += "org.http4s" %% "http4s-blaze-server" % "0.23.12" -addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.13.2") +addSbtPlugin("org.http4s" % "sbt-http4s-org" % "0.14.3") addSbtPlugin("org.scalameta" % "sbt-mdoc" % "2.3.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.0") +addSbtPlugin("org.scala-js" % "sbt-scalajs" % "1.10.1") addSbtPlugin("com.eed3si9n" % "sbt-buildinfo" % "0.11.0") diff --git a/tests-nodejs/src/test/scala/org/http4s/dom/NodeJSFetchSuite.scala b/tests-nodejs/src/test/scala/org/http4s/dom/NodeJSFetchSuite.scala index 52007a93..bdea5225 100644 --- a/tests-nodejs/src/test/scala/org/http4s/dom/NodeJSFetchSuite.scala +++ b/tests-nodejs/src/test/scala/org/http4s/dom/NodeJSFetchSuite.scala @@ -18,8 +18,10 @@ package org.http4s package dom import cats.effect.IO +import cats.effect.Resource +import org.http4s.client.Client import org.http4s.client.testkit.ClientRouteTestBattery class NodeJSFetchSuite extends ClientRouteTestBattery("FetchClient") { - def clientResource = FetchClientBuilder[IO].resource + def clientResource: Resource[IO, Client[IO]] = FetchClientBuilder[IO].resource } diff --git a/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala b/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala index 55b93736..cd7f4d9a 100644 --- a/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala +++ b/tests/src/test/scala/org/http4s/dom/FetchServiceWorkerSuite.scala @@ -22,6 +22,7 @@ import cats.syntax.all._ import fs2.Stream import munit.CatsEffectSuite import org.http4s.Method._ +import org.http4s.client.Client import org.http4s.client.dsl.io._ import org.http4s.client.testkit.testroutes.GetRoutes import org.http4s.multipart.Multiparts @@ -35,9 +36,9 @@ import scala.scalajs.js class FetchServiceWorkerSuite extends CatsEffectSuite { - val client = FetchClientBuilder[IO].create + val client: Client[IO] = FetchClientBuilder[IO].create - val baseUrl = uri"/" + val baseUrl: Uri = uri"/" test("Install service worker") { IO.fromPromise {