From 2ace47e78a5a8b3831aac980d542b759b20de4c9 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 20 Aug 2021 09:57:46 +0000 Subject: [PATCH 1/4] WIP create cloudflare modules --- build.sbt | 49 +++- .../cloudflare/worker/ScheduledEvent.scala | 26 ++ .../worker/ScheduledEventListener.scala | 33 +++ .../worker/facade/ScheduledEvent.scala | 25 ++ .../feral/cloudflare/worker/KVNamespace.scala | 276 ++++++++++++++++++ .../cloudflare/worker/facade/Event.scala | 25 ++ .../worker/facade/KVNamespace.scala | 106 +++++++ .../worker/facade/ReadableStream.scala | 40 +++ .../feral/cloudflare/worker/package.scala | 60 ++++ core/src/main/scala/feral/IOSetup.scala | 45 +++ .../main/scala/feral/lambda/IOLambda.scala | 28 +- 11 files changed, 687 insertions(+), 26 deletions(-) create mode 100644 cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala create mode 100644 cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala create mode 100644 cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/KVNamespace.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Event.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/KVNamespace.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/ReadableStream.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala create mode 100644 core/src/main/scala/feral/IOSetup.scala diff --git a/build.sbt b/build.sbt index 486e95f0..7dc4b9ad 100644 --- a/build.sbt +++ b/build.sbt @@ -39,15 +39,28 @@ lazy val root = lambdaEvents.js, lambdaEvents.jvm, lambdaApiGatewayProxyHttp4s.js, - lambdaApiGatewayProxyHttp4s.jvm) + lambdaApiGatewayProxyHttp4s.jvm, + cloudflareWorker, + cloudflareWorkerFetch, + cloudflareWorkerScheduled + ) .enablePlugins(NoPublishPlugin) +lazy val core = crossProject(JSPlatform, JVMPlatform) + .crossType(CrossType.Pure) + .in(file("core")) + .settings( + name := "feral-core", + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-effect" % catsEffectVersion, + ) + ) + lazy val lambda = crossProject(JSPlatform, JVMPlatform) .in(file("lambda")) .settings( name := "feral-lambda", libraryDependencies ++= Seq( - "org.typelevel" %%% "cats-effect" % catsEffectVersion, "io.circe" %%% "circe-core" % circeVersion ) ) @@ -63,6 +76,7 @@ lazy val lambda = crossProject(JSPlatform, JVMPlatform) "io.circe" %%% "circe-fs2" % "0.14.0" ) ) + .dependsOn(core) lazy val lambdaEvents = crossProject(JSPlatform, JVMPlatform) .crossType(CrossType.Pure) @@ -84,3 +98,34 @@ lazy val lambdaApiGatewayProxyHttp4s = crossProject(JSPlatform, JVMPlatform) ) ) .dependsOn(lambda, lambdaEvents) + +lazy val cloudflareWorker = project + .in(file("cloudflare-worker")) + .enablePlugins(ScalaJSPlugin) + .settings( + name := "feral-cloudflare-worker", + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-effect" % catsEffectVersion, + "co.fs2" %%% "fs2-core" % fs2Version, + "io.circe" %%% "circe-scalajs" % circeVersion + ) + ) + +lazy val cloudflareWorkerFetch = project + .in(file("cloudflare-worker-fetch")) + .enablePlugins(ScalaJSPlugin) + .settings( + name := "feral-cloudflare-worker-fetch", + libraryDependencies ++= Seq( + "org.http4s" %%% "http4s-core" % http4sVersion + ) + ) + .dependsOn(core.js, cloudflareWorker) + +lazy val cloudflareWorkerScheduled = project + .in(file("cloudflare-worker-scheduled")) + .enablePlugins(ScalaJSPlugin) + .settings( + name := "feral-cloudflare-worker-scheduled" + ) + .dependsOn(core.js, cloudflareWorker) diff --git a/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala b/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala new file mode 100644 index 00000000..89e01bc9 --- /dev/null +++ b/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare.worker + +import scala.scalajs.js + +final case class ScheduledEvent(cron: String, scheduledTime: js.Date) + +object ScheduledEvent { + private[worker] def apply(event: facade.ScheduledEvent): ScheduledEvent = + ScheduledEvent(event.cron, new js.Date(event.scheduledTime)) +} diff --git a/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala b/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala new file mode 100644 index 00000000..490c804f --- /dev/null +++ b/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala @@ -0,0 +1,33 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral +package cloudflare.worker + +import cats.effect.IO + +trait ScheduledEventListener extends IOSetup { + + def apply(event: ScheduledEvent, setup: Setup): IO[Unit] + + def main(args: Array[String]): Unit = + addEventListener[facade.ScheduledEvent]( + "scheduled", + event => + event.waitUntil( + setupMemo.flatMap(apply(ScheduledEvent(event), _)).unsafeToPromise()(runtime))) + +} diff --git a/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala b/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala new file mode 100644 index 00000000..21fbd4d5 --- /dev/null +++ b/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare.worker.facade + +import scala.scalajs.js + +@js.native +private[worker] sealed trait ScheduledEvent extends Event { + def cron: String = js.native + def scheduledTime: Double = js.native +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/KVNamespace.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/KVNamespace.scala new file mode 100644 index 00000000..38416a0a --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/KVNamespace.scala @@ -0,0 +1,276 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare.worker + +import scala.scalajs.js +import scala.scalajs.js.JSConverters._ +import scala.concurrent.duration.FiniteDuration +import io.circe.Decoder + +import fs2.Stream +import scodec.bits.ByteVector +import io.circe.Encoder +import io.circe.scalajs._ +import cats.effect.kernel.Async +import cats.syntax.all._ +import cats.data.OptionT +import fs2.Chunk +import cats.effect.kernel.Resource + +sealed trait KVNamespace[F[_]] { + + import KVNamespace._ + + def getText(key: String): F[Option[String]] + def getText(key: String, cacheTtl: FiniteDuration): F[Option[String]] + + def get[A: Decoder](key: String): F[Option[A]] + def get[A: Decoder](key: String, cacheTtl: FiniteDuration): F[Option[A]] + + def getByteVector(key: String): F[Option[ByteVector]] + def getByteVector(key: String, cacheTtl: FiniteDuration): F[Option[ByteVector]] + + def getStream(key: String): Resource[F, Option[Stream[F, Byte]]] + def getStream(key: String, cacheTtl: FiniteDuration): Resource[F, Option[Stream[F, Byte]]] + + def getTextWithMetadata[M: Decoder](key: String): F[Option[(String, Option[M])]] + + def getWithMetadata[A: Decoder, M: Decoder](key: String): F[Option[(A, Option[M])]] + + def getByteVectorWithMetadata[M: Decoder](key: String): F[Option[(ByteVector, Option[M])]] + + def getStreamWithMetadata[M: Decoder]( + key: String): Resource[F, Option[(Stream[F, Byte], Option[M])]] + + def put(key: String, value: String): F[Unit] = put[Unit](key, value, PutOptions()) + def put[M: Encoder](key: String, value: String, options: PutOptions[M]): F[Unit] + + def put[A: Encoder](key: String, value: A): F[Unit] = put[A, Unit](key, value, PutOptions()) + def put[A: Encoder, M: Encoder](key: String, value: A, options: PutOptions[M]): F[Unit] = + put(key, Encoder[A].apply(value).noSpaces, options) + + def put(key: String, value: ByteVector): F[Unit] = put[Unit](key, value, PutOptions()) + def put[M: Encoder](key: String, value: ByteVector, options: PutOptions[M]): F[Unit] + + def put(key: String, value: Stream[F, Byte]): F[Unit] = put[Unit](key, value, PutOptions()) + def put[M: Encoder](key: String, value: Stream[F, Byte], options: PutOptions[M]): F[Unit] + + def delete(key: String): F[Unit] + + def list[M: Decoder]: Stream[F, Key[M]] + def list[M: Decoder](prefix: String): Stream[F, Key[M]] + +} + +object KVNamespace { + + final case class Key[+M](name: String, expiration: Option[js.Date], metadata: Option[M]) + + final case class PutOptions[+M]( + expiration: Option[js.Date] = None, + expirationTtl: Option[FiniteDuration] = None, + metadata: Option[M] = None + ) { + private[KVNamespace] def toJS[M0 >: M](implicit e: Encoder[M0]): facade.PutOptions = + facade.PutOptions( + expiration.map(_.getDate() / 1000).orUndefined, + expirationTtl.map(_.toSeconds.toDouble).orUndefined, + metadata.map((_: M0).asJsAny).orUndefined + ) + } + + def apply[F[_]: Async](namespace: String): KVNamespace[F] = + new AsyncKVNamespace[F]( + js.special + .fileLevelThis + .asInstanceOf[js.Dynamic] + .selectDynamic(namespace) + .asInstanceOf[facade.KVNamespace]) + + private final class AsyncKVNamespace[F[_]](kv: facade.KVNamespace)(implicit F: Async[F]) + extends KVNamespace[F] { + + def getText(key: String): F[Option[String]] = + F.fromPromise(F.delay(kv.get(key, facade.GetOptions("text")))) + .map(x => Option(x.asInstanceOf[String])) + + def getText(key: String, cacheTtl: FiniteDuration): F[Option[String]] = + F.fromPromise( + F.delay(kv.get(key, facade.GetOptions("text", cacheTtl.toSeconds.toDouble)))) + .map(x => Option(x.asInstanceOf[String])) + + def get[A: Decoder](key: String): F[Option[A]] = + OptionT( + F.fromPromise(F.delay(kv.get(key, facade.GetOptions("json")))) + .map(x => Option(x.asInstanceOf[js.Any])) + ).semiflatMap(x => F.fromEither(decodeJs[A](x))).value + + def get[A: Decoder](key: String, cacheTtl: FiniteDuration): F[Option[A]] = + OptionT( + F.fromPromise( + F.delay(kv.get(key, facade.GetOptions("json", cacheTtl.toSeconds.toDouble)))) + .map(x => Option(x.asInstanceOf[js.Any])) + ).semiflatMap(x => F.fromEither(decodeJs[A](x))).value + + def getByteVector(key: String): F[Option[ByteVector]] = + OptionT( + F.fromPromise(F.delay(kv.get(key, facade.GetOptions("arrayBuffer")))) + .map(x => Option(x.asInstanceOf[js.typedarray.ArrayBuffer])) + ).map(Chunk.jsArrayBuffer(_).toByteVector).value + + def getByteVector(key: String, cacheTtl: FiniteDuration): F[Option[ByteVector]] = + OptionT( + F.fromPromise( + F.delay(kv.get(key, facade.GetOptions("arrayBuffer", cacheTtl.toSeconds.toDouble)))) + .map(x => Option(x.asInstanceOf[js.typedarray.ArrayBuffer])) + ).map(Chunk.jsArrayBuffer(_).toByteVector).value + + def getStream(key: String): Resource[F, Option[Stream[F, Byte]]] = + OptionT( + Resource.makeCase(F + .fromPromise(F.delay(kv.get(key, facade.GetOptions("stream")))) + .map(x => Option(x.asInstanceOf[facade.ReadableStream[js.typedarray.Uint8Array]]))) { + case (Some(rs), exitCase) => closeReadableStream(rs, exitCase) + case _ => F.unit + } + ).map(fromReadableStream[F]).value + + def getStream(key: String, cacheTtl: FiniteDuration): Resource[F, Option[Stream[F, Byte]]] = + OptionT( + Resource.makeCase( + F.fromPromise( + F.delay(kv.get(key, facade.GetOptions("stream", cacheTtl.toSeconds.toDouble)))) + .map(x => + Option(x.asInstanceOf[facade.ReadableStream[js.typedarray.Uint8Array]]))) { + case (Some(rs), exitCase) => closeReadableStream(rs, exitCase) + case _ => F.unit + } + ).map(fromReadableStream[F]).value + + def getTextWithMetadata[M: Decoder](key: String): F[Option[(String, Option[M])]] = + F.fromPromise( + F.delay(kv.getWithMetadata(key, "text")) + ).flatMap { vm => + OptionT + .fromOption(Option(vm.value.asInstanceOf[String])) + .semiflatMap { value => + OptionT + .fromOption(Option(vm.metadata)) + .semiflatMap(x => F.fromEither(decodeJs[M](x))) + .value + .map((value, _)) + } + .value + } + + def getWithMetadata[A: Decoder, M: Decoder](key: String): F[Option[(A, Option[M])]] = + F.fromPromise( + F.delay(kv.getWithMetadata(key, "json")) + ).flatMap { vm => + OptionT + .fromOption(Option(vm.value.asInstanceOf[js.Any])) + .semiflatMap(x => F.fromEither(decodeJs[A](x))) + .semiflatMap { value => + OptionT + .fromOption(Option(vm.metadata)) + .semiflatMap(x => F.fromEither(decodeJs[M](x))) + .value + .map((value, _)) + } + .value + } + + def getByteVectorWithMetadata[M: Decoder](key: String): F[Option[(ByteVector, Option[M])]] = + F.fromPromise( + F.delay(kv.getWithMetadata(key, "arrayBuffer")) + ).flatMap { vm => + OptionT + .fromOption(Option(vm.value.asInstanceOf[js.typedarray.ArrayBuffer])) + .semiflatMap { value => + OptionT + .fromOption(Option(vm.metadata)) + .semiflatMap(x => F.fromEither(decodeJs[M](x))) + .value + .map((Chunk.jsArrayBuffer(value).toByteVector, _)) + } + .value + } + + def getStreamWithMetadata[M: Decoder]( + key: String): Resource[F, Option[(Stream[F, Byte], Option[M])]] = + OptionT( + Resource.makeCase( + F.fromPromise( + F.delay(kv.getWithMetadata(key, "stream")) + ).flatMap { vm => + OptionT + .fromOption( + Option(vm.value.asInstanceOf[facade.ReadableStream[js.typedarray.Uint8Array]])) + .semiflatMap { value => + OptionT + .fromOption(Option(vm.metadata)) + .semiflatMap(x => F.fromEither(decodeJs[M](x))) + .value + .map((value, _)) + } + .value + }) { + case (Some((rs, _)), exitCase) => closeReadableStream(rs, exitCase) + case _ => F.unit + }).map { + case (rs, m) => + (fromReadableStream(rs), m) + }.value + + def put[M: Encoder](key: String, value: String, options: PutOptions[M]): F[Unit] = + F.fromPromise(F.delay(kv.put(key, value, options.toJS[M]))) + + def put[M: Encoder](key: String, value: ByteVector, options: PutOptions[M]): F[Unit] = + F.fromPromise( + F.delay(kv.put(key, Chunk.byteVector(value).toJSArrayBuffer, options.toJS[M]))) + + def put[M: Encoder](key: String, value: Stream[F, Byte], options: PutOptions[M]): F[Unit] = + ??? + + def delete(key: String): F[Unit] = F.fromPromise(F.delay(kv.delete(key))) + + def list[M: Decoder]: Stream[F, Key[M]] = list[M](js.undefined) + + def list[M: Decoder](prefix: String): Stream[F, Key[M]] = list[M](prefix) + + private def list[M: Decoder](prefix: js.UndefOr[String]): Stream[F, Key[M]] = + Stream + .unfoldLoopEval(js.undefined: js.UndefOr[String]) { cursor => + F.fromPromise(F.delay(kv.list(facade.ListOptions(prefix = prefix, cursor = cursor)))) + .map { response => + (response.keys, if (!response.list_complete) Some(response.cursor) else None) + } + } + .flatMap { keys => + Stream.emits(keys.toSeq).evalMap { key => + OptionT + .fromOption[F](key.metadata.toOption.flatMap(Option(_))) + .semiflatMap(x => F.fromEither(decodeJs[M](x))) + .value + .map { + Key(key.name, key.expiration.toOption.flatMap(Option(_)).map(new js.Date(_)), _) + } + } + } + } + +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Event.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Event.scala new file mode 100644 index 00000000..2a421941 --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Event.scala @@ -0,0 +1,25 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare.worker.facade + +import scala.annotation.nowarn +import scala.scalajs.js + +@js.native +private[worker] trait Event extends js.Object { + @nowarn def waitUntil(promise: js.Promise[Unit]): Unit = js.native +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/KVNamespace.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/KVNamespace.scala new file mode 100644 index 00000000..bd1b6b1e --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/KVNamespace.scala @@ -0,0 +1,106 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare.worker.facade + +import scala.annotation.nowarn +import scala.scalajs.js +import scala.scalajs.js.| + +@js.native +@nowarn +private[worker] sealed trait KVNamespace extends js.Object { + + def get(key: String, options: GetOptions): js.Promise[ + String | js.Any | js.typedarray.ArrayBuffer | ReadableStream[js.typedarray.Uint8Array]] + + def getWithMetadata(key: String, `type`: String): js.Promise[KVValueWithMetadata] = js.native + + def put( + key: String, + value: String | ReadableStream[js.typedarray.Uint8Array] | js.typedarray.ArrayBuffer, + options: js.UndefOr[PutOptions] = js.native): js.Promise[Unit] = js.native + + def delete(key: String): js.Promise[Unit] = js.native + + def list(options: js.UndefOr[ListOptions]): js.Promise[ListResponse] + +} + +@js.native +private[worker] trait GetOptions extends js.Object + +private[worker] object GetOptions { + def apply( + `type`: js.UndefOr[String] = js.undefined, + cacheTtl: js.UndefOr[Double] = js.undefined + ): GetOptions = + js.Dynamic.literal(`type` = `type`, cacheTtl = cacheTtl).asInstanceOf[GetOptions] +} + +@js.native +private[worker] sealed trait KVValueWithMetadata extends js.Object { + def value + : String | js.Any | js.typedarray.ArrayBuffer | ReadableStream[js.typedarray.Uint8Array] = + js.native + def metadata: js.Any = js.native +} + +@js.native +private[worker] trait PutOptions extends js.Object + +private[worker] object PutOptions { + def apply( + expiration: js.UndefOr[Double] = js.undefined, + expirationTtl: js.UndefOr[Double] = js.undefined, + metadata: js.UndefOr[js.Any] = js.undefined + ): PutOptions = js + .Dynamic + .literal(expiration = expiration, expirationTtl = expirationTtl, metadata = metadata) + .asInstanceOf[PutOptions] +} + +@js.native +private[worker] trait ListOptions extends js.Object + +private[worker] object ListOptions { + def apply( + prefix: js.UndefOr[String] = js.undefined, + limit: js.UndefOr[Int] = js.undefined, + cursor: js.UndefOr[String] = js.undefined + ): ListOptions = js + .Dynamic + .literal( + prefix = prefix, + limit = limit, + cursor = cursor + ) + .asInstanceOf[ListOptions] +} + +@js.native +private[worker] sealed trait ListResponse extends js.Object { + def keys: js.Array[Key] = js.native + def list_complete: Boolean = js.native + def cursor: js.UndefOr[String] = js.native +} + +@js.native +private[worker] sealed trait Key extends js.Object { + def name: String = js.native + def expiration: js.UndefOr[Double] = js.native + def metadata: js.UndefOr[js.Any] = js.native +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/ReadableStream.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/ReadableStream.scala new file mode 100644 index 00000000..8b16ad91 --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/ReadableStream.scala @@ -0,0 +1,40 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare.worker.facade + +import scala.scalajs.js +import scala.annotation.nowarn + +@js.native +@nowarn +private[worker] sealed trait ReadableStream[R] extends js.Object { + def locked: Boolean = js.native + def cancel(reason: js.UndefOr[js.Any]): js.Promise[Unit] = js.native + def getReader(): ReadableStreamDefaultReader[R] = js.native +} + +@js.native +private[worker] sealed trait ReadableStreamDefaultReader[R] extends js.Object { + def read(): js.Promise[ReadableStreamDefaultReadResult[R]] = js.native + def releaseLock(): Unit = js.native +} + +@js.native +private[worker] sealed trait ReadableStreamDefaultReadResult[R] extends js.Object { + def done: Boolean = js.native + def value: R = js.native +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala new file mode 100644 index 00000000..323208f0 --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala @@ -0,0 +1,60 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare + +import scala.annotation.nowarn +import scala.scalajs.js +import cats.effect.kernel.Async +import fs2.Stream +import cats.syntax.all._ +import cats.effect.kernel.Resource + +package object worker { + + @js.native + @js.annotation.JSGlobal("addEventListener") + @nowarn + private[worker] def addEventListener[E <: facade.Event]( + `type`: String, + listener: js.Function1[E, Unit]): Unit = js.native + + private[worker] def fromReadableStream[F[_]]( + rs: facade.ReadableStream[js.typedarray.Uint8Array])( + implicit F: Async[F]): Stream[F, Byte] = + Stream.bracket(F.delay(rs.getReader()))(r => F.delay(r.releaseLock())).flatMap { reader => + Stream.unfoldChunkEval(reader) { reader => + F.fromPromise(F.delay(reader.read())).map { chunk => + if (chunk.done) + None + else + Some((fs2.Chunk.uint8Array(chunk.value), reader)) + } + } + } + + private[worker] def closeReadableStream[F[_], A]( + rs: facade.ReadableStream[A], + exitCase: Resource.ExitCase)(implicit F: Async[F]): F[Unit] = exitCase match { + case Resource.ExitCase.Succeeded => + F.fromPromise(F.delay(rs.cancel(js.undefined))).void + case Resource.ExitCase.Errored(ex) => + F.fromPromise(F.delay(rs.cancel(ex.toString().asInstanceOf[js.Any]))).void + case Resource.ExitCase.Canceled => + F.fromPromise(F.delay(rs.cancel(js.undefined))).void + } + +} diff --git a/core/src/main/scala/feral/IOSetup.scala b/core/src/main/scala/feral/IOSetup.scala new file mode 100644 index 00000000..8cf84dca --- /dev/null +++ b/core/src/main/scala/feral/IOSetup.scala @@ -0,0 +1,45 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral + +import cats.effect.unsafe.IORuntime +import cats.effect.kernel.Resource +import cats.effect.IO +import cats.effect.kernel.Deferred +import cats.syntax.all._ + +trait IOSetup { + + protected def runtime: IORuntime = IORuntime.global + + protected type Setup + protected val setup: Resource[IO, Setup] = Resource.pure(null.asInstanceOf[Setup]) + + private[feral] final val setupMemo: IO[Setup] = { + val deferred = Deferred.unsafe[IO, Either[Throwable, Setup]] + setup + .attempt + .allocated + .flatTap { + case (setup, _) => + deferred.complete(setup) + } + .unsafeRunAndForget()(runtime) + deferred.get.rethrow + } + +} diff --git a/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala b/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala index d5e7e0a0..81d22e81 100644 --- a/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala +++ b/lambda/shared/src/main/scala/feral/lambda/IOLambda.scala @@ -14,38 +14,18 @@ * limitations under the License. */ -package feral.lambda +package feral +package lambda -import cats.effect.Deferred import cats.effect.IO -import cats.effect.Resource -import cats.effect.unsafe.IORuntime -import cats.syntax.all._ import io.circe.Decoder import io.circe.Encoder abstract class IOLambda[Event, Result]( implicit private[lambda] val decoder: Decoder[Event], private[lambda] val encoder: Encoder[Result] -) extends IOLambdaPlatform[Event, Result] { - - protected def runtime: IORuntime = IORuntime.global - - protected type Setup - protected val setup: Resource[IO, Setup] = Resource.pure(null.asInstanceOf[Setup]) - - private[lambda] final val setupMemo: IO[Setup] = { - val deferred = Deferred.unsafe[IO, Either[Throwable, Setup]] - setup - .attempt - .allocated - .flatTap { - case (setup, _) => - deferred.complete(setup) - } - .unsafeRunAndForget()(runtime) - deferred.get.rethrow - } +) extends IOLambdaPlatform[Event, Result] + with IOSetup { def apply(event: Event, context: Context, setup: Setup): IO[Option[Result]] } From bc30eac8b9d393bde52cf7099707e00d8f797fef Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 20 Aug 2021 20:28:08 +0000 Subject: [PATCH 2/4] More wip --- build.sbt | 30 ++----- .../worker/FetchEventListener.scala | 83 +++++++++++++++++++ .../cloudflare/worker/ScheduledEvent.scala | 0 .../worker/ScheduledEventListener.scala | 2 +- .../cloudflare/worker/facade/FetchEvent.scala | 26 ++++++ .../cloudflare/worker/facade/Headers.scala | 16 ++++ .../facade/IncomingRequestCfProperties.scala | 6 ++ .../cloudflare/worker/facade/Request.scala | 20 +++++ .../worker/facade/ScheduledEvent.scala | 0 .../feral/cloudflare/worker/package.scala | 46 +++++++++- 10 files changed, 200 insertions(+), 29 deletions(-) create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala rename {cloudflare-worker-scheduled => cloudflare-worker}/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala (100%) rename {cloudflare-worker-scheduled => cloudflare-worker}/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala (95%) create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/FetchEvent.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Headers.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala rename {cloudflare-worker-scheduled => cloudflare-worker}/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala (100%) diff --git a/build.sbt b/build.sbt index 7dc4b9ad..ecdcbbef 100644 --- a/build.sbt +++ b/build.sbt @@ -40,9 +40,7 @@ lazy val root = lambdaEvents.jvm, lambdaApiGatewayProxyHttp4s.js, lambdaApiGatewayProxyHttp4s.jvm, - cloudflareWorker, - cloudflareWorkerFetch, - cloudflareWorkerScheduled + cloudflareWorker ) .enablePlugins(NoPublishPlugin) @@ -107,25 +105,9 @@ lazy val cloudflareWorker = project libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % catsEffectVersion, "co.fs2" %%% "fs2-core" % fs2Version, - "io.circe" %%% "circe-scalajs" % circeVersion - ) - ) - -lazy val cloudflareWorkerFetch = project - .in(file("cloudflare-worker-fetch")) - .enablePlugins(ScalaJSPlugin) - .settings( - name := "feral-cloudflare-worker-fetch", - libraryDependencies ++= Seq( - "org.http4s" %%% "http4s-core" % http4sVersion + "io.circe" %%% "circe-generic" % circeVersion, + "io.circe" %%% "circe-scalajs" % circeVersion, + "org.http4s" %%% "http4s-client" % http4sVersion ) - ) - .dependsOn(core.js, cloudflareWorker) - -lazy val cloudflareWorkerScheduled = project - .in(file("cloudflare-worker-scheduled")) - .enablePlugins(ScalaJSPlugin) - .settings( - name := "feral-cloudflare-worker-scheduled" - ) - .dependsOn(core.js, cloudflareWorker) + ).dependsOn(core.js) + \ No newline at end of file diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala new file mode 100644 index 00000000..5db29ab8 --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala @@ -0,0 +1,83 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral +package cloudflare.worker + +import cats.effect.IO +import cats.effect.kernel.Resource +import cats.effect.std.Supervisor +import org.http4s.ContextRequest +import org.http4s.ContextRoutes +import io.circe.Json +import io.circe.Decoder +import io.circe.generic.semiauto._ +import io.circe.scalajs._ + +trait FetchEventListener extends IOSetup { + + def routes: Resource[IO, ContextRoutes[FetchEventContext[IO], IO]] + + final type Setup = ContextRoutes[FetchEventContext[IO], IO] + protected override final val setup: Resource[IO, ContextRoutes[FetchEventContext[IO], IO]] = + routes + + private def apply(event: facade.FetchEvent): Unit = + event.waitUntil( + Supervisor[IO] + .use { supervisor => + for { + routes <- setupMemo + request <- fromRequest[IO](event.request) + properties <- IO.fromEither(decodeJs[IncomingRequestCfProperties](event.request.cf)) + _ <- routes(ContextRequest(FetchEventContext(supervisor, properties), request)) + .map(toResponse[IO]) + .foldF(IO.unit)(r => IO(event.respondWith(r))) + } yield () + } + .unsafeToPromise()(runtime)) + + final def main(args: Array[String]): Unit = + addEventListener[facade.FetchEvent]("fetch", apply) + +} + +final case class FetchEventContext[F[_]]( + supervisor: Supervisor[F], + properties: IncomingRequestCfProperties) + +final case class IncomingRequestCfProperties( + asn: String, + colo: String, + country: Option[String], + requestPriority: Option[String], + tlsCipher: String, + tlsClientAuth: Option[Json], + tlsVersion: String, + city: Option[String], + continent: Option[String], + latitude: Option[String], + longitude: Option[String], + postalCode: Option[String], + metroCode: Option[String], + region: Option[String], + regionCode: Option[String], + timezone: String +) + +object IncomingRequestCfProperties { + private[worker] implicit def decoder: Decoder[IncomingRequestCfProperties] = deriveDecoder +} diff --git a/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala similarity index 100% rename from cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala rename to cloudflare-worker/src/main/scala/feral/cloudflare/worker/ScheduledEvent.scala diff --git a/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala similarity index 95% rename from cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala rename to cloudflare-worker/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala index 490c804f..8e6de01e 100644 --- a/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/ScheduledEventListener.scala @@ -23,7 +23,7 @@ trait ScheduledEventListener extends IOSetup { def apply(event: ScheduledEvent, setup: Setup): IO[Unit] - def main(args: Array[String]): Unit = + final def main(args: Array[String]): Unit = addEventListener[facade.ScheduledEvent]( "scheduled", event => diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/FetchEvent.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/FetchEvent.scala new file mode 100644 index 00000000..05a568ab --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/FetchEvent.scala @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Typelevel + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package feral.cloudflare.worker.facade + +import scala.scalajs.js +import scala.scalajs.js.| + +@js.native +private[worker] sealed trait FetchEvent extends Event { + def request: Request = js.native + def respondWith(response: Response | js.Promise[Response]): Unit +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Headers.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Headers.scala new file mode 100644 index 00000000..c9473a56 --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Headers.scala @@ -0,0 +1,16 @@ +package feral.cloudflare.worker.facade + +import scala.annotation.nowarn +import scala.scalajs.js +import scala.scalajs.js.annotation.JSGlobal +import scala.scalajs.js.annotation.JSName + +@js.native +@JSGlobal +@nowarn +private[worker] class Headers(map: js.Dictionary[String]) + extends js.Iterable[js.Array[String]] { + + @JSName(js.Symbol.iterator) + def jsIterator(): js.Iterator[js.Array[String]] = js.native +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala new file mode 100644 index 00000000..22e81546 --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala @@ -0,0 +1,6 @@ +package feral.cloudflare.worker.facade + +import scala.scalajs.js + +@js.native +trait IncomingRequestCfProperties extends js.Any \ No newline at end of file diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala new file mode 100644 index 00000000..e1beabed --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala @@ -0,0 +1,20 @@ +package feral.cloudflare.worker.facade + +import scala.scalajs.js +import scala.scalajs.js.annotation.JSGlobal + +@js.native +@JSGlobal +private[worker] class Request extends js.Object { + def body: ReadableStream[js.typedarray.Uint8Array] = js.native + def bodyUsed: Boolean = js.native + def cf: IncomingRequestCfProperties = js.native + def headers: Headers = js.native + def method: String = js.native + def redirect: String = js.native + def url: String = js.native +} + +class Response extends js.Object { + +} diff --git a/cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala similarity index 100% rename from cloudflare-worker-scheduled/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala rename to cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/ScheduledEvent.scala diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala index 323208f0..94c2c962 100644 --- a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala @@ -16,12 +16,20 @@ package feral.cloudflare -import scala.annotation.nowarn -import scala.scalajs.js import cats.effect.kernel.Async -import fs2.Stream -import cats.syntax.all._ import cats.effect.kernel.Resource +import cats.syntax.all._ +import fs2.Stream +import org.http4s.Header +import org.http4s.Headers +import org.http4s.Request +import org.http4s.Response + +import scala.annotation.nowarn +import scala.scalajs.js +import scala.scalajs.js.JSConverters._ +import org.http4s.Method +import org.http4s.Uri package object worker { @@ -32,6 +40,34 @@ package object worker { `type`: String, listener: js.Function1[E, Unit]): Unit = js.native + private[worker] def fromRequest[F[_]](req: facade.Request)(implicit F: Async[F]): F[Request[F]] = for { + method <- F.fromEither(Method.fromString(req.method)) + uri <- F.fromEither(Uri.fromString(req.url)) + } yield Request(method, uri) + + private[worker] def toRequest[F[_]](req: Request[F]): facade.Request = ??? + + private[worker] def fromResponse[F[_]](req: facade.Response): F[Response[F]] = ??? + + private[worker] def toResponse[F[_]](req: Response[F]): facade.Response = ??? + + private[worker] def toDomHeaders(headers: Headers): facade.Headers = + new facade.Headers( + headers + .headers + .view + .map { + case Header.Raw(name, value) => + name.toString -> value + } + .toMap + .toJSDictionary) + + private[worker] def fromDomHeaders(headers: facade.Headers): Headers = + Headers( + headers.toIterable.map { header => header(0) -> header(1) }.toList + ) + private[worker] def fromReadableStream[F[_]]( rs: facade.ReadableStream[js.typedarray.Uint8Array])( implicit F: Async[F]): Stream[F, Byte] = @@ -46,6 +82,8 @@ package object worker { } } + private[worker] def toReadableStream[F[_]](s: Stream[F, Byte]): facade.ReadableStream[js.typedarray.Uint8Array] = ??? + private[worker] def closeReadableStream[F[_], A]( rs: facade.ReadableStream[A], exitCase: Resource.ExitCase)(implicit F: Async[F]): F[Unit] = exitCase match { From 615f5ff166cb78bbcd2a5b498268949967e49544 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Fri, 20 Aug 2021 22:11:30 +0000 Subject: [PATCH 3/4] WIP --- .../worker/FetchEventListener.scala | 32 +++++++++++----- .../facade/IncomingRequestCfProperties.scala | 6 --- .../cloudflare/worker/facade/Request.scala | 28 ++++++++++++-- .../cloudflare/worker/facade/Response.scala | 37 +++++++++++++++++++ .../feral/cloudflare/worker/package.scala | 15 ++++++-- 5 files changed, 95 insertions(+), 23 deletions(-) delete mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala create mode 100644 cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Response.scala diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala index 5db29ab8..f9a11844 100644 --- a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/FetchEventListener.scala @@ -18,22 +18,24 @@ package feral package cloudflare.worker import cats.effect.IO +import cats.effect.SyncIO import cats.effect.kernel.Resource import cats.effect.std.Supervisor -import org.http4s.ContextRequest -import org.http4s.ContextRoutes -import io.circe.Json import io.circe.Decoder +import io.circe.Json import io.circe.generic.semiauto._ import io.circe.scalajs._ +import org.http4s.HttpRoutes +import org.http4s.Request +import org.typelevel.vault.Key +import org.http4s.HttpVersion trait FetchEventListener extends IOSetup { - def routes: Resource[IO, ContextRoutes[FetchEventContext[IO], IO]] + def routes: Resource[IO, HttpRoutes[IO]] - final type Setup = ContextRoutes[FetchEventContext[IO], IO] - protected override final val setup: Resource[IO, ContextRoutes[FetchEventContext[IO], IO]] = - routes + final type Setup = HttpRoutes[IO] + protected override final val setup = routes private def apply(event: facade.FetchEvent): Unit = event.waitUntil( @@ -43,7 +45,10 @@ trait FetchEventListener extends IOSetup { routes <- setupMemo request <- fromRequest[IO](event.request) properties <- IO.fromEither(decodeJs[IncomingRequestCfProperties](event.request.cf)) - _ <- routes(ContextRequest(FetchEventContext(supervisor, properties), request)) + httpVersion <- IO.fromEither(HttpVersion.fromString(properties.httpProtocol)) + _ <- routes(request + .withHttpVersion(httpVersion) + .withAttribute(FetchEventContext.key, FetchEventContext(supervisor, properties))) .map(toResponse[IO]) .foldF(IO.unit)(r => IO(event.respondWith(r))) } yield () @@ -55,14 +60,21 @@ trait FetchEventListener extends IOSetup { } -final case class FetchEventContext[F[_]]( - supervisor: Supervisor[F], +final case class FetchEventContext( + supervisor: Supervisor[IO], properties: IncomingRequestCfProperties) +object FetchEventContext { + private[worker] val key = Key.newKey[SyncIO, FetchEventContext].unsafeRunSync() + def apply(request: Request[IO]): IO[FetchEventContext] = + IO.fromEither(request.attributes.lookup(key).toRight(new NoSuchElementException)) +} + final case class IncomingRequestCfProperties( asn: String, colo: String, country: Option[String], + httpProtocol: String, requestPriority: Option[String], tlsCipher: String, tlsClientAuth: Option[Json], diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala deleted file mode 100644 index 22e81546..00000000 --- a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/IncomingRequestCfProperties.scala +++ /dev/null @@ -1,6 +0,0 @@ -package feral.cloudflare.worker.facade - -import scala.scalajs.js - -@js.native -trait IncomingRequestCfProperties extends js.Any \ No newline at end of file diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala index e1beabed..668de3c2 100644 --- a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Request.scala @@ -1,11 +1,13 @@ package feral.cloudflare.worker.facade +import scala.annotation.nowarn import scala.scalajs.js import scala.scalajs.js.annotation.JSGlobal @js.native @JSGlobal -private[worker] class Request extends js.Object { +@nowarn +private[worker] class Request(input: String, init: RequestInit) extends js.Object { def body: ReadableStream[js.typedarray.Uint8Array] = js.native def bodyUsed: Boolean = js.native def cf: IncomingRequestCfProperties = js.native @@ -15,6 +17,26 @@ private[worker] class Request extends js.Object { def url: String = js.native } -class Response extends js.Object { - +private[worker] sealed trait RequestInit extends js.Any + +private[worker] object RequestInit { + def apply( + cf: js.UndefOr[RequestInitCfProperties] = js.undefined, + method: js.UndefOr[String] = js.undefined, + headers: js.UndefOr[Headers] = js.undefined, + body: js.UndefOr[ReadableStream[js.typedarray.Uint8Array]] = js.undefined, + redirect: js.UndefOr[String] = js.undefined + ): RequestInit = + js.Dynamic + .literal( + cf = cf, + method = method, + headers = headers, + body = body, + redirect = redirect + ).asInstanceOf[RequestInit] } + +@js.native +private[worker] trait IncomingRequestCfProperties extends js.Any +private[worker] sealed trait RequestInitCfProperties extends js.Any diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Response.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Response.scala new file mode 100644 index 00000000..fcc2a7f3 --- /dev/null +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/facade/Response.scala @@ -0,0 +1,37 @@ +package feral.cloudflare.worker.facade + +import scala.annotation.nowarn +import scala.scalajs.js +import scala.scalajs.js.annotation.JSGlobal + +@js.native +@JSGlobal +@nowarn +private[worker] class Response( + body: js.UndefOr[ReadableStream[js.typedarray.Uint8Array]] = js.undefined, + init: js.UndefOr[ResponseInit] = js.undefined) + extends js.Object { + def body: ReadableStream[js.typedarray.Uint8Array] = js.native + def bodyUsed: Boolean = js.native + def encodeBody: String = js.native + def headers: Headers = js.native + def ok: Boolean = js.native + def redirected: Boolean = js.native + def status: Int = js.native + def statusText: String = js.native + def url: String = js.native +} + +@js.native +private[worker] sealed trait ResponseInit extends js.Any + +private[worker] object ResponseInit { + def apply( + status: js.UndefOr[Int] = js.undefined, + statusText: js.UndefOr[String] = js.undefined, + headers: js.UndefOr[Headers] = js.undefined + ): ResponseInit = + js.Dynamic + .literal(status = status, statusText = statusText, headers = headers) + .asInstanceOf[ResponseInit] +} diff --git a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala index 94c2c962..ac2cb323 100644 --- a/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala +++ b/cloudflare-worker/src/main/scala/feral/cloudflare/worker/package.scala @@ -30,6 +30,7 @@ import scala.scalajs.js import scala.scalajs.js.JSConverters._ import org.http4s.Method import org.http4s.Uri +import org.http4s.Status package object worker { @@ -43,15 +44,21 @@ package object worker { private[worker] def fromRequest[F[_]](req: facade.Request)(implicit F: Async[F]): F[Request[F]] = for { method <- F.fromEither(Method.fromString(req.method)) uri <- F.fromEither(Uri.fromString(req.url)) - } yield Request(method, uri) + headers = fromHeaders(req.headers) + body = fromReadableStream(req.body) + } yield Request(method, uri, headers = headers, body = body) private[worker] def toRequest[F[_]](req: Request[F]): facade.Request = ??? - private[worker] def fromResponse[F[_]](req: facade.Response): F[Response[F]] = ??? + private[worker] def fromResponse[F[_]](res: facade.Response)(implicit F: Async[F]): F[Response[F]] = for { + status <- F.fromEither(Status.fromIntAndReason(res.status, res.statusText)) + headers = fromHeaders(res.headers) + body = fromReadableStream(res.body) + } yield Response(status, headers = headers, body = body) private[worker] def toResponse[F[_]](req: Response[F]): facade.Response = ??? - private[worker] def toDomHeaders(headers: Headers): facade.Headers = + private[worker] def toHeaders(headers: Headers): facade.Headers = new facade.Headers( headers .headers @@ -63,7 +70,7 @@ package object worker { .toMap .toJSDictionary) - private[worker] def fromDomHeaders(headers: facade.Headers): Headers = + private[worker] def fromHeaders(headers: facade.Headers): Headers = Headers( headers.toIterable.map { header => header(0) -> header(1) }.toList ) From dc58235518b50fd28f0a9afc62398bb542609620 Mon Sep 17 00:00:00 2001 From: Arman Bilge Date: Sun, 22 Aug 2021 21:00:12 -0700 Subject: [PATCH 4/4] Add core projects to root aggregate --- build.sbt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/build.sbt b/build.sbt index ecdcbbef..5b2f23a8 100644 --- a/build.sbt +++ b/build.sbt @@ -34,6 +34,8 @@ lazy val root = project .in(file(".")) .aggregate( + core.js, + core.jvm, lambda.js, lambda.jvm, lambdaEvents.js, @@ -110,4 +112,4 @@ lazy val cloudflareWorker = project "org.http4s" %%% "http4s-client" % http4sVersion ) ).dependsOn(core.js) - \ No newline at end of file +