From d4a5c36fdd5847e760ca2600720b107937f14965 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 5 Jun 2026 10:45:19 -0400 Subject: [PATCH 01/20] Project outline --- .../functions/IOAzureFunctionHttpPlatform.scala | 5 +++++ build.sbt | 15 +++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala new file mode 100644 index 00000000..2a065846 --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala @@ -0,0 +1,5 @@ +package feral.functions + +object IOAzureFunctionHttpPlatform { + +} diff --git a/build.sbt b/build.sbt index c22ab4ae..67687590 100644 --- a/build.sbt +++ b/build.sbt @@ -271,3 +271,18 @@ lazy val googleCloudHttp4s = crossProject(JSPlatform, JVMPlatform) "co.fs2" %%% "fs2-io" % fs2Version ) ) + +lazy val azureFunctions = crossProject(JSPlatform, JVMPlatform) + .in(file("azure-functions")) + .settings( + name := "azure-functions", + libraryDependencies ++= Seq() + ) + .settings(commonSettings) + .jsSettings( + libraryDependencies ++= Seq() + ) + .jvmSettings( + Test / fork := true, + libraryDependencies ++= Seq() + ) \ No newline at end of file From de74c6ada2d6acf3b8df2aa8d074b464f004b069 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 19 Jun 2026 12:05:37 -0400 Subject: [PATCH 02/20] Basic vanilla impl --- .../IOAzureFunctionHttpPlatform.scala | 5 --- .../feral/functions/IOAzureHttpFunction.scala | 35 +++++++++++++++++++ 2 files changed, 35 insertions(+), 5 deletions(-) delete mode 100644 azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala create mode 100644 azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala deleted file mode 100644 index 2a065846..00000000 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureFunctionHttpPlatform.scala +++ /dev/null @@ -1,5 +0,0 @@ -package feral.functions - -object IOAzureFunctionHttpPlatform { - -} diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala new file mode 100644 index 00000000..b6eb601a --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -0,0 +1,35 @@ +package feral.functions + +import scala.scalajs.js +import scala.scalajs.js.annotation._ +//import scala.scalajs.js.JSConverters._ + +object IOAzureHttpFunction { + //case class AppObj(methods: js.Array[String], ) +} + +@js.native +@JSImport("@azure/functions", "app") +object App extends js.Object { + def http( + name: String, + appConfig: js.Object + //handler: js.Function2[js.Any, js.Any, js.Promise[js.UndefOr[js.Any]]]): Unit = js.native + ): Unit = js.native +} + +abstract class IOAzureHttpFunction { + final def main(args: Array[String]): Unit = + App.http("functionName", appConfig) + + private lazy val handlerFn: js.Function2[js.Any, js.Any, js.Promise[js.UndefOr[js.Any]]] = { + (request, context) => js.Promise.resolve[js.Any](request) + } + + private val appConfig = js.Dynamic.literal( + methods = js.Array("GET", "PUT"), + authLevel = "anonymous", + route = "{*path}", + handler = handlerFn + ) +} From e351fa880ebac0980265eec1f2e0ef2455b971a1 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 28 Jun 2026 10:01:19 -0400 Subject: [PATCH 03/20] fixed function name --- .../feral/functions/IOAzureHttpFunction.scala | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index b6eb601a..25e72598 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -4,27 +4,20 @@ import scala.scalajs.js import scala.scalajs.js.annotation._ //import scala.scalajs.js.JSConverters._ -object IOAzureHttpFunction { - //case class AppObj(methods: js.Array[String], ) -} - @js.native @JSImport("@azure/functions", "app") object App extends js.Object { def http( name: String, appConfig: js.Object - //handler: js.Function2[js.Any, js.Any, js.Promise[js.UndefOr[js.Any]]]): Unit = js.native ): Unit = js.native } abstract class IOAzureHttpFunction { final def main(args: Array[String]): Unit = - App.http("functionName", appConfig) + App.http(functionName, appConfig) - private lazy val handlerFn: js.Function2[js.Any, js.Any, js.Promise[js.UndefOr[js.Any]]] = { - (request, context) => js.Promise.resolve[js.Any](request) - } + protected val functionName: String = getClass.getSimpleName.init private val appConfig = js.Dynamic.literal( methods = js.Array("GET", "PUT"), @@ -32,4 +25,12 @@ abstract class IOAzureHttpFunction { route = "{*path}", handler = handlerFn ) + + private lazy val handlerFn: js.Function2[js.Any, js.Any, js.Promise[js.UndefOr[js.Any]]] = { + (request, context) => js.Promise.resolve[js.Any](context) + } } + +object IOAzureHttpFunction { + //case class AppObj(methods: js.Array[String], ) +} \ No newline at end of file From 10935445c17aa086993d40666514dd134cd5949d Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 29 Jun 2026 19:50:35 -0400 Subject: [PATCH 04/20] Started work on Invocation Context --- .../feral/functions/IOAzureHttpFunction.scala | 36 +++++++++++++------ .../functions/facade/InvocationContext.scala | 30 ++++++++++++++++ build.sbt | 6 +++- 3 files changed, 60 insertions(+), 12 deletions(-) create mode 100644 azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 25e72598..6024f7ab 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -2,6 +2,7 @@ package feral.functions import scala.scalajs.js import scala.scalajs.js.annotation._ +import feral.functions.facade.InvocationContext //import scala.scalajs.js.JSConverters._ @js.native @@ -19,18 +20,31 @@ abstract class IOAzureHttpFunction { protected val functionName: String = getClass.getSimpleName.init - private val appConfig = js.Dynamic.literal( - methods = js.Array("GET", "PUT"), - authLevel = "anonymous", - route = "{*path}", - handler = handlerFn - ) + private val appConfig = js + .Dynamic + .literal( + methods = js.Array("GET", "PUT"), + authLevel = "anonymous", + route = "{*path}", + handler = handlerFn + ) - private lazy val handlerFn: js.Function2[js.Any, js.Any, js.Promise[js.UndefOr[js.Any]]] = { - (request, context) => js.Promise.resolve[js.Any](context) + private lazy val handlerFn + : js.Function2[js.Any, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { + (request, context) => { + context.log("this is a log msg!!!") + + val response = + js.Dynamic + .literal( + status = 200, + body = "payload", + headers = js.Dynamic.literal("content-type" -> "text/plain") + ) + + js.Promise.resolve[js.UndefOr[js.Any]](response) + } } } -object IOAzureHttpFunction { - //case class AppObj(methods: js.Array[String], ) -} \ No newline at end of file +object IOAzureHttpFunction {} diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala new file mode 100644 index 00000000..fd679abd --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala @@ -0,0 +1,30 @@ +package feral.functions.facade + +import scala.scalajs.js + +@js.native +trait InvocationContext extends js.Object { + def log(args: js.Any*): Unit = js.native + def trace(args: js.Any*): Unit = js.native + def debug(args: js.Any*): Unit = js.native + def info(args: js.Any*): Unit = js.native + def warn(args: js.Any*): Unit = js.native + def error(args: js.Any*): Unit = js.native +} + +/* +invocationId: string; + functionName: string; + extraInputs: InvocationContextExtraInputs; + extraOutputs: InvocationContextExtraOutputs; + log(...args: any[]): void; + trace(...args: any[]): void; + debug(...args: any[]): void; + info(...args: any[]): void; + warn(...args: any[]): void; + error(...args: any[]): void; + retryContext?: RetryContext; + traceContext?: TraceContext; + triggerMetadata?: TriggerMetadata; + options: EffectiveFunctionOptions; + */ \ No newline at end of file diff --git a/build.sbt b/build.sbt index 67687590..e0615a02 100644 --- a/build.sbt +++ b/build.sbt @@ -280,7 +280,11 @@ lazy val azureFunctions = crossProject(JSPlatform, JVMPlatform) ) .settings(commonSettings) .jsSettings( - libraryDependencies ++= Seq() + libraryDependencies ++= Seq( + "org.typelevel" %%% "cats-effect" % catsEffectVersion, + "io.circe" %%% "circe-scodec" % circeVersion, + "org.http4s" %%% "http4s-server" % http4sVersion + ) ) .jvmSettings( Test / fork := true, From f8084eb74cc29595a284e39fd4de7437357c61c4 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 12 Jul 2026 14:29:38 -0400 Subject: [PATCH 05/20] works via local dependancy --- .../feral/functions/IOAzureHttpFunction.scala | 30 ++++++++++++------- build.sbt | 15 ++++++---- 2 files changed, 29 insertions(+), 16 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 6024f7ab..744419de 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -3,20 +3,19 @@ package feral.functions import scala.scalajs.js import scala.scalajs.js.annotation._ import feral.functions.facade.InvocationContext -//import scala.scalajs.js.JSConverters._ -@js.native -@JSImport("@azure/functions", "app") -object App extends js.Object { - def http( - name: String, - appConfig: js.Object - ): Unit = js.native -} +import org.http4s.HttpApp +//import cats.effect.IO + +//import scala.scalajs.js.JSConverters._ +// import org.http4s.nodejs.IncomingMessage +// import org.http4s.nodejs.ServerResponse abstract class IOAzureHttpFunction { + //val appFromIC: InvocationContext => HttpApp[IO] + final def main(args: Array[String]): Unit = - App.http(functionName, appConfig) + IOAzureHttpFunction.App.http(functionName, appConfig) protected val functionName: String = getClass.getSimpleName.init @@ -47,4 +46,13 @@ abstract class IOAzureHttpFunction { } } -object IOAzureHttpFunction {} +object IOAzureHttpFunction { + @js.native + @JSImport("@azure/functions", "app") + object App extends js.Object { + def http( + name: String, + appConfig: js.Object + ): Unit = js.native + } +} diff --git a/build.sbt b/build.sbt index e0615a02..015145ee 100644 --- a/build.sbt +++ b/build.sbt @@ -20,6 +20,7 @@ name := "feral" ThisBuild / tlBaseVersion := "0.3" ThisBuild / startYear := Some(2021) +ThisBuild / isSnapshot := true ThisBuild / developers := List( tlGitHubDev("armanbilge", "Arman Bilge"), @@ -76,6 +77,7 @@ lazy val root = lambdaHttp4s, lambdaCloudFormationCustomResource, googleCloudHttp4s, + azureFunctions, examples, unidocs ) @@ -276,15 +278,18 @@ lazy val azureFunctions = crossProject(JSPlatform, JVMPlatform) .in(file("azure-functions")) .settings( name := "azure-functions", - libraryDependencies ++= Seq() - ) - .settings(commonSettings) - .jsSettings( libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % catsEffectVersion, "io.circe" %%% "circe-scodec" % circeVersion, "org.http4s" %%% "http4s-server" % http4sVersion - ) + ), + tlVersionIntroduced := List("2.13", "3").map(_ -> "0.3.1").toMap + ) + .settings(commonSettings) + .jsSettings( + libraryDependencies ++= Seq( + ), + scalaJSLinkerConfig ~= { _.withModuleKind(ModuleKind.CommonJSModule) } ) .jvmSettings( Test / fork := true, From 865e874710431cafd9b8aa359126625deb4c1d11 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 13 Jul 2026 13:02:02 -0400 Subject: [PATCH 06/20] stable vanilla --- .../main/scala/feral/functions/IOAzureHttpFunction.scala | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 744419de..ba04a4e5 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -5,14 +5,13 @@ import scala.scalajs.js.annotation._ import feral.functions.facade.InvocationContext import org.http4s.HttpApp -//import cats.effect.IO +import cats.effect.IO +import cats.effect.Resource //import scala.scalajs.js.JSConverters._ -// import org.http4s.nodejs.IncomingMessage -// import org.http4s.nodejs.ServerResponse abstract class IOAzureHttpFunction { - //val appFromIC: InvocationContext => HttpApp[IO] + def buildHttpApp(context: InvocationContext): Resource[IO, HttpApp[IO]] final def main(args: Array[String]): Unit = IOAzureHttpFunction.App.http(functionName, appConfig) From 54447e0fff8f9bfc740170a4aa50f7b51a8d0994 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 14 Jul 2026 22:34:41 -0400 Subject: [PATCH 07/20] working on request facade, next is parser --- .../feral/functions/IOAzureHttpFunction.scala | 11 +++-- .../feral/functions/facade/JSRequest.scala | 46 +++++++++++++++++++ .../scala/feral/functions/util/Parser.scala | 5 ++ 3 files changed, 59 insertions(+), 3 deletions(-) create mode 100644 azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala create mode 100644 azure-functions/js/src/main/scala/feral/functions/util/Parser.scala diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index ba04a4e5..f66443dd 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -9,6 +9,9 @@ import cats.effect.IO import cats.effect.Resource //import scala.scalajs.js.JSConverters._ +import org.http4s.Request +import feral.functions.facade.JSRequest +import feral.functions.facade.JSHeaders abstract class IOAzureHttpFunction { def buildHttpApp(context: InvocationContext): Resource[IO, HttpApp[IO]] @@ -16,7 +19,7 @@ abstract class IOAzureHttpFunction { final def main(args: Array[String]): Unit = IOAzureHttpFunction.App.http(functionName, appConfig) - protected val functionName: String = getClass.getSimpleName.init + protected val functionName: String = getClass.getSimpleName.init //may want to add timestamp for testing private val appConfig = js .Dynamic @@ -28,9 +31,11 @@ abstract class IOAzureHttpFunction { ) private lazy val handlerFn - : js.Function2[js.Any, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { + : js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { (request, context) => { - context.log("this is a log msg!!!") + val h = request.headers + val headers = JSHeaders.keyList(h) + context.log(s"headers: $headers") val response = js.Dynamic diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala new file mode 100644 index 00000000..0465a4fd --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala @@ -0,0 +1,46 @@ +package feral.functions.facade + +import scala.scalajs.js + +import scala.collection.mutable.ListBuffer + +@js.native +trait JSRequest extends js.Object { + def method: String = js.native + def url: String = js.native + def headers: JSHeaders = js.native +} + +@js.native +trait JSHeaders extends js.Object { + def get(name: String): js.UndefOr[String] = js.native + def keys(): js.Iterator[String] = js.native +} + +object JSHeaders { + def keyList(h: JSHeaders): List[String] = { + val acc = ListBuffer[String]() + val itr = h.keys() + var entity = itr.next() + + while(!entity.done) { + acc.addOne(entity.value) + entity = itr.next() + } + + acc.result() + } + + object Syntax { + //syntax for method like calls??? + } +} + +/* Parameters + +method: Method.GET, Method.POST, etc. +uri: representation of the request URI +httpVersion: the HTTP version +headers: collection of Headers +body: fs2.Stream[F, Byte] defining the body of the request +attributes: Immutable Map used for carrying additional information in a type safe fashion */ \ No newline at end of file diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala new file mode 100644 index 00000000..da294aaf --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -0,0 +1,5 @@ +package feral.functions.util + +object Parser { + //def decodeRequest[F[_]](request: JSRequest) +} From e050c9a590f80c7cada436f383b2b4e458980c6d Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 21 Jul 2026 15:16:04 -0400 Subject: [PATCH 08/20] next set up dispatcher --- .../feral/functions/IOAzureHttpFunction.scala | 2 +- .../feral/functions/facade/JSRequest.scala | 17 ++++++---- .../scala/feral/functions/util/Parser.scala | 34 ++++++++++++++++++- 3 files changed, 45 insertions(+), 8 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index f66443dd..6d998ca6 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -35,7 +35,7 @@ abstract class IOAzureHttpFunction { (request, context) => { val h = request.headers val headers = JSHeaders.keyList(h) - context.log(s"headers: $headers") + context.log(s"final pipeline! last try for now") val response = js.Dynamic diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala index 0465a4fd..a7f3351e 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala @@ -2,13 +2,14 @@ package feral.functions.facade import scala.scalajs.js -import scala.collection.mutable.ListBuffer +//may want to change to requestFacade.., think it over @js.native trait JSRequest extends js.Object { def method: String = js.native def url: String = js.native def headers: JSHeaders = js.native + def body: JSReadableStream = js.native } @js.native @@ -17,18 +18,21 @@ trait JSHeaders extends js.Object { def keys(): js.Iterator[String] = js.native } +@js.native +trait JSReadableStream extends js.Object + object JSHeaders { def keyList(h: JSHeaders): List[String] = { - val acc = ListBuffer[String]() + val builder = List.newBuilder[String] val itr = h.keys() var entity = itr.next() while(!entity.done) { - acc.addOne(entity.value) + builder.addOne(entity.value) entity = itr.next() } - acc.result() + builder.result() } object Syntax { @@ -40,7 +44,8 @@ object JSHeaders { method: Method.GET, Method.POST, etc. uri: representation of the request URI -httpVersion: the HTTP version +httpVersion: the HTTP version //not used in lambda-http4s headers: collection of Headers body: fs2.Stream[F, Byte] defining the body of the request -attributes: Immutable Map used for carrying additional information in a type safe fashion */ \ No newline at end of file +attributes: Immutable Map used for carrying additional information in a type safe fashion //not used in lambda-http4s +*/ \ No newline at end of file diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index da294aaf..f81dc8a2 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -1,5 +1,37 @@ package feral.functions.util +import org.http4s.Request +import org.http4s.Method +import org.http4s.Uri +import org.http4s.Header +import org.http4s.Headers + +import feral.functions.facade.JSRequest +import feral.functions.facade.JSHeaders + +import cats.effect.kernel.Concurrent +import cats.syntax.all._ + +import org.typelevel.ci.CIString + + + object Parser { - //def decodeRequest[F[_]](request: JSRequest) + def decodeRequest[F[_]: Concurrent](request: JSRequest): F[Request[F]] = + for { + method <- Method.fromString(request.method).liftTo[F] + uri <- Uri.fromString(request.url).liftTo[F] + headers = { + val builder = List.newBuilder[Header.Raw] + val keys = JSHeaders.keyList(request.headers) + + keys.foreach(k => builder.addOne(Header.Raw(CIString(k), request.headers.get(k).getOrElse("")))) + + Headers(builder.result()) + } + } yield Request[F]( + method = method, + uri = uri, + headers = headers + ) } From f37f11daaec8d59a98b86bf799b8fc5a061e5bdb Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 22 Jul 2026 16:37:27 -0400 Subject: [PATCH 09/20] switching to dispatcher --- .../feral/functions/IOAzureHttpFunction.scala | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 6d998ca6..39a27acb 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -7,19 +7,24 @@ import feral.functions.facade.InvocationContext import org.http4s.HttpApp import cats.effect.IO import cats.effect.Resource +import cats.syntax.all._ +import cats.effect.unsafe.IORuntime //import scala.scalajs.js.JSConverters._ import org.http4s.Request import feral.functions.facade.JSRequest import feral.functions.facade.JSHeaders +import cats.effect.std.Dispatcher abstract class IOAzureHttpFunction { - def buildHttpApp(context: InvocationContext): Resource[IO, HttpApp[IO]] + protected def handler: InvocationContext => Resource[IO, HttpApp[IO]] + + private val runtime = IORuntime.global final def main(args: Array[String]): Unit = IOAzureHttpFunction.App.http(functionName, appConfig) - protected val functionName: String = getClass.getSimpleName.init //may want to add timestamp for testing + private val functionName: String = getClass.getSimpleName.init //may want to add timestamp for testing private val appConfig = js .Dynamic @@ -31,8 +36,19 @@ abstract class IOAzureHttpFunction { ) private lazy val handlerFn - : js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { + : js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { + val dispatcherHandle = { + Dispatcher + .parallel[IO](await = true) + .product(Resource.pure(handler)) + .allocated + .map(_._1) //drop unused finalizer, this resource will live for the duration + .unsafeToPromise()(runtime) + } + (request, context) => { + //do dispatcher thing + ///// val h = request.headers val headers = JSHeaders.keyList(h) context.log(s"final pipeline! last try for now") From 89ac8eae99587380dc3e1d1d324a3c397c7bba38 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 24 Jul 2026 07:56:32 -0400 Subject: [PATCH 10/20] Parser workeng except body encoding/decoding. revisit that later --- .../feral/functions/IOAzureHttpFunction.scala | 52 +++++++++++++------ .../scala/feral/functions/util/Parser.scala | 33 +++++++++++- 2 files changed, 67 insertions(+), 18 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 39a27acb..b72f21e3 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -15,6 +15,8 @@ import org.http4s.Request import feral.functions.facade.JSRequest import feral.functions.facade.JSHeaders import cats.effect.std.Dispatcher +import feral.functions.util.Parser +import fs2.text.utf8 abstract class IOAzureHttpFunction { protected def handler: InvocationContext => Resource[IO, HttpApp[IO]] @@ -24,7 +26,8 @@ abstract class IOAzureHttpFunction { final def main(args: Array[String]): Unit = IOAzureHttpFunction.App.http(functionName, appConfig) - private val functionName: String = getClass.getSimpleName.init //may want to add timestamp for testing + private val functionName: String = + getClass.getSimpleName.init // may want to add timestamp for testing private val appConfig = js .Dynamic @@ -36,32 +39,49 @@ abstract class IOAzureHttpFunction { ) private lazy val handlerFn - : js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { + : js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { val dispatcherHandle = { Dispatcher .parallel[IO](await = true) .product(Resource.pure(handler)) .allocated - .map(_._1) //drop unused finalizer, this resource will live for the duration + .map(_._1) // drop unused finalizer, this resource will live for the duration .unsafeToPromise()(runtime) } - (request, context) => { - //do dispatcher thing + (requestJS, context) => { + // do dispatcher thing + context.log("befor FOR COMP") + dispatcherHandle.`then`[js.Any] { + case (dispatcher, handle) => { + val io = for { + _ <- IO(context.log("FOR COMP")) + request <- Parser.decodeRequest[IO](requestJS) + _ <- IO(context.log(request.uri.toString())) + response <- handle(context).use(app => app.run(request)) + _ <- IO(context.log(response.status.toString())) + bodyList <- response.body.through(utf8.decode).compile.toList + _ <- IO(context.log(bodyList.toString())) + respEncoded <- Parser.encodeResponse[IO](response) + _ <- IO(context.log("Decoded")) + _ <- IO(context.log(respEncoded.toString())) + } yield respEncoded + + dispatcher.unsafeToPromise(io) + } + } ///// - val h = request.headers - val headers = JSHeaders.keyList(h) - context.log(s"final pipeline! last try for now") + // impure!!! - val response = - js.Dynamic - .literal( - status = 200, - body = "payload", - headers = js.Dynamic.literal("content-type" -> "text/plain") - ) + // val response = + // js.Dynamic + // .literal( + // status = 200, + // body = "payload", + // headers = js.Dynamic.literal("content-type" -> "text/plain") + // ) - js.Promise.resolve[js.UndefOr[js.Any]](response) + // js.Promise.resolve[js.UndefOr[js.Any]](response) } } } diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index f81dc8a2..ea66d875 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -5,6 +5,7 @@ import org.http4s.Method import org.http4s.Uri import org.http4s.Header import org.http4s.Headers +import org.http4s.Response import feral.functions.facade.JSRequest import feral.functions.facade.JSHeaders @@ -14,10 +15,11 @@ import cats.syntax.all._ import org.typelevel.ci.CIString - +import scala.scalajs.js +//import scala.scalajs.js.annotation._ object Parser { - def decodeRequest[F[_]: Concurrent](request: JSRequest): F[Request[F]] = + def decodeRequest[F[_]: Concurrent](request: JSRequest): F[Request[F]] = { for { method <- Method.fromString(request.method).liftTo[F] uri <- Uri.fromString(request.url).liftTo[F] @@ -34,4 +36,31 @@ object Parser { uri = uri, headers = headers ) + } + + def encodeResponse[F[_]: Concurrent](response: Response[F]): F[js.Any] = { + val headersList = ("TEST", "value") :: response.headers.headers.map(h => (h.name.toString, h.value)) + val headers = js.Dictionary(headersList:_*) + + //val body = response.body //need to figure this out later + + val responseEncoded: js.Any = js + .Dynamic + .literal( + status = response.status.code, + headers = headers, + body = "Body Not Parsed!!!" + ) + + // val resp: js.Any = + // js.Dynamic + // .literal( + // status = 200, + // body = "payload", + // headers = js.Dynamic.literal("content-type" -> "text/plain") + // ) + + responseEncoded.pure[F] + //resp.pure[F] + } } From 28070e37f9a28aba3bb22c6ea31a594c6d08b0e8 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 27 Jul 2026 19:10:04 -0400 Subject: [PATCH 11/20] Decode Request is working --- .../feral/functions/IOAzureHttpFunction.scala | 7 +- .../feral/functions/facade/JSRequest.scala | 75 ++++++++++++++++++- .../scala/feral/functions/util/Parser.scala | 13 +++- build.sbt | 3 +- 4 files changed, 89 insertions(+), 9 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index b72f21e3..18985b68 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -51,17 +51,20 @@ abstract class IOAzureHttpFunction { (requestJS, context) => { // do dispatcher thing - context.log("befor FOR COMP") + //context.log("befor FOR COMP") dispatcherHandle.`then`[js.Any] { case (dispatcher, handle) => { val io = for { _ <- IO(context.log("FOR COMP")) request <- Parser.decodeRequest[IO](requestJS) _ <- IO(context.log(request.uri.toString())) + bodyList <- request.body.through(utf8.decode).compile.toList + _ <- IO(context.log("req body: " + bodyList.toString())) + response <- handle(context).use(app => app.run(request)) _ <- IO(context.log(response.status.toString())) bodyList <- response.body.through(utf8.decode).compile.toList - _ <- IO(context.log(bodyList.toString())) + _ <- IO(context.log("resp body: " + bodyList.toString())) respEncoded <- Parser.encodeResponse[IO](response) _ <- IO(context.log("Decoded")) _ <- IO(context.log(respEncoded.toString())) diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala index a7f3351e..5f229e8a 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala @@ -1,6 +1,14 @@ package feral.functions.facade import scala.scalajs.js +import scala.scalajs.js.typedarray.Uint8Array + +import fs2.Stream +import fs2.Chunk + +//import cats.effect.IO +import cats.effect.kernel.Async +import cats.syntax.all._ //may want to change to requestFacade.., think it over @@ -9,7 +17,7 @@ trait JSRequest extends js.Object { def method: String = js.native def url: String = js.native def headers: JSHeaders = js.native - def body: JSReadableStream = js.native + def body: js.UndefOr[JSReadableStream] = js.native } @js.native @@ -19,7 +27,22 @@ trait JSHeaders extends js.Object { } @js.native -trait JSReadableStream extends js.Object +trait JSReadableStream extends js.Object { + def getReader(): JSReadableStreamDefaultReader = js.native +} + +@js.native +trait JSReadableStreamDefaultReader extends js.Object { + def read(): js.Promise[JSReadObject] = js.native + def releaseLock(): Unit = js.native + def cancel(reason: js.UndefOr[js.Any]): js.Promise[Unit] +} + +@js.native +trait JSReadObject extends js.Object { + def value: js.UndefOr[Uint8Array] = js.native + def done: Boolean = js.native +} object JSHeaders { def keyList(h: JSHeaders): List[String] = { @@ -40,6 +63,54 @@ object JSHeaders { } } +object JSReadableStream { + def toFs2[F[_]: Async](streamOption: js.UndefOr[JSReadableStream]): Stream[F, Byte] = { + streamOption.toOption match { + case None => Stream.empty + case Some(null) => Stream.empty + case Some(stream) => { + Stream.eval(Async[F].delay(stream.getReader())).flatMap{ reader => + def nextChunk = { + Async[F].fromPromise(Async[F].delay(reader.read())).map{ read => + if(read.done) { + None + } else { + val chunk = read.value + .toOption + .map(arr => Chunk.array[Byte](toByteArray(arr))) + .getOrElse(Chunk.empty[Byte]) + + Some(chunk) + } + } + } + + Stream + .repeatEval(nextChunk) + .unNoneTerminate + .flatMap(Stream.chunk) + .onFinalize(Async[F].delay(reader.releaseLock())) + } + } + } + } + + private def toByteArray(array: Uint8Array): Array[Byte] = { + val builder = Array.newBuilder[Byte] + val length = array.length + var index = 0 + + while(index != length) { + builder.addOne(array(index).toByte) + index = index + 1 + } + + builder.result() + } + + object Syntax {} +} + /* Parameters method: Method.GET, Method.POST, etc. diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index ea66d875..1324088c 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -11,18 +11,21 @@ import feral.functions.facade.JSRequest import feral.functions.facade.JSHeaders import cats.effect.kernel.Concurrent +import cats.effect.kernel.Async import cats.syntax.all._ import org.typelevel.ci.CIString import scala.scalajs.js +import feral.functions.facade.JSReadableStream //import scala.scalajs.js.annotation._ object Parser { - def decodeRequest[F[_]: Concurrent](request: JSRequest): F[Request[F]] = { + def decodeRequest[F[_]: Async](request: JSRequest): F[Request[F]] = { for { method <- Method.fromString(request.method).liftTo[F] uri <- Uri.fromString(request.url).liftTo[F] + body = JSReadableStream.toFs2[F](request.body) headers = { val builder = List.newBuilder[Header.Raw] val keys = JSHeaders.keyList(request.headers) @@ -31,15 +34,17 @@ object Parser { Headers(builder.result()) } + //body = JSReadableStream.toFs2[F](request.body) //need to use generic type parameter in tofs2 } yield Request[F]( method = method, uri = uri, - headers = headers + headers = headers, + body = body // need to convert into Entity ) } def encodeResponse[F[_]: Concurrent](response: Response[F]): F[js.Any] = { - val headersList = ("TEST", "value") :: response.headers.headers.map(h => (h.name.toString, h.value)) + val headersList = response.headers.headers.map(h => (h.name.toString, h.value)) val headers = js.Dictionary(headersList:_*) //val body = response.body //need to figure this out later @@ -49,7 +54,7 @@ object Parser { .literal( status = response.status.code, headers = headers, - body = "Body Not Parsed!!!" + body = response.body.through(fs2.text.utf8.decode).compile.toString ) // val resp: js.Any = diff --git a/build.sbt b/build.sbt index 015145ee..d5834b3c 100644 --- a/build.sbt +++ b/build.sbt @@ -281,7 +281,8 @@ lazy val azureFunctions = crossProject(JSPlatform, JVMPlatform) libraryDependencies ++= Seq( "org.typelevel" %%% "cats-effect" % catsEffectVersion, "io.circe" %%% "circe-scodec" % circeVersion, - "org.http4s" %%% "http4s-server" % http4sVersion + "org.http4s" %%% "http4s-server" % http4sVersion, + "co.fs2" %%% "fs2-io" % fs2Version ), tlVersionIntroduced := List("2.13", "3").map(_ -> "0.3.1").toMap ) From 7c16cc30db4d896a36c07a1ec997cad677f85bd7 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 28 Jul 2026 17:47:00 -0400 Subject: [PATCH 12/20] everything seems to be working, time to clean and test --- .../feral/functions/IOAzureHttpFunction.scala | 10 ++--- .../scala/feral/functions/util/Parser.scala | 45 +++++++++++++++---- 2 files changed, 42 insertions(+), 13 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 18985b68..2204b66a 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -58,14 +58,14 @@ abstract class IOAzureHttpFunction { _ <- IO(context.log("FOR COMP")) request <- Parser.decodeRequest[IO](requestJS) _ <- IO(context.log(request.uri.toString())) - bodyList <- request.body.through(utf8.decode).compile.toList - _ <- IO(context.log("req body: " + bodyList.toString())) + //bodyList <- request.body.through(utf8.decode).compile.toList + //_ <- IO(context.log("req body: " + bodyList.toString())) response <- handle(context).use(app => app.run(request)) _ <- IO(context.log(response.status.toString())) - bodyList <- response.body.through(utf8.decode).compile.toList - _ <- IO(context.log("resp body: " + bodyList.toString())) - respEncoded <- Parser.encodeResponse[IO](response) + //bodyList <- response.body.through(utf8.decode).compile.toList + //_ <- IO(context.log("resp body: " + bodyList.toString())) + respEncoded <- Parser.encodeResponse[IO](response, dispatcher) _ <- IO(context.log("Decoded")) _ <- IO(context.log(respEncoded.toString())) } yield respEncoded diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index 1324088c..c54699e4 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -20,6 +20,9 @@ import scala.scalajs.js import feral.functions.facade.JSReadableStream //import scala.scalajs.js.annotation._ +import fs2.Stream +import cats.effect.std.Dispatcher + object Parser { def decodeRequest[F[_]: Async](request: JSRequest): F[Request[F]] = { for { @@ -30,11 +33,12 @@ object Parser { val builder = List.newBuilder[Header.Raw] val keys = JSHeaders.keyList(request.headers) - keys.foreach(k => builder.addOne(Header.Raw(CIString(k), request.headers.get(k).getOrElse("")))) + keys.foreach(k => + builder.addOne(Header.Raw(CIString(k), request.headers.get(k).getOrElse("")))) Headers(builder.result()) } - //body = JSReadableStream.toFs2[F](request.body) //need to use generic type parameter in tofs2 + // body = JSReadableStream.toFs2[F](request.body) //need to use generic type parameter in tofs2 } yield Request[F]( method = method, uri = uri, @@ -43,18 +47,18 @@ object Parser { ) } - def encodeResponse[F[_]: Concurrent](response: Response[F]): F[js.Any] = { - val headersList = response.headers.headers.map(h => (h.name.toString, h.value)) - val headers = js.Dictionary(headersList:_*) + def encodeResponse[F[_]: Async](response: Response[F], dispatcher: Dispatcher[F]): F[js.Any] = { + val headersList = response.headers.headers.map(h => (h.name.toString, h.value)) + val headers = js.Dictionary(headersList: _*) - //val body = response.body //need to figure this out later + // val body = response.body //need to figure this out later val responseEncoded: js.Any = js .Dynamic .literal( status = response.status.code, headers = headers, - body = response.body.through(fs2.text.utf8.decode).compile.toString + body = toReadableStream[F](response.body, dispatcher)//response.body.through(fs2.text.utf8.decode).compile.toString ) // val resp: js.Any = @@ -66,6 +70,31 @@ object Parser { // ) responseEncoded.pure[F] - //resp.pure[F] + // resp.pure[F] + } + + private def toReadableStream[F[_]: Async]( + stream: Stream[F, Byte], + dispatcher: Dispatcher[F]): js.Any = { + js.Dynamic.newInstance(js.Dynamic.global.ReadableStream)( + js.Dynamic.literal( + start = (controller: js.Dynamic) => { + val io = { + stream.chunks.evalMap { chunk => + Async[F].delay { + val array = new js.typedarray.Uint8Array(chunk.size) + chunk.toArray.zipWithIndex.foreach{ case (byte, index) => array(index) = byte } + controller.enqueue(array) + } + .void + } + .onFinalize(Async[F].delay(controller.close()).void) + .compile + .drain + } + dispatcher.unsafeToPromise(io) + } + ) + ) } } From bb6434990b6fae246caaf447c38cbb3245f64cf2 Mon Sep 17 00:00:00 2001 From: Tim Date: Wed, 29 Jul 2026 23:09:18 -0400 Subject: [PATCH 13/20] cleaned up a bit --- .../feral/functions/IOAzureHttpFunction.scala | 36 ++++--------------- .../feral/functions/facade/JSRequest.scala | 12 +------ .../scala/feral/functions/util/Parser.scala | 12 ++----- 3 files changed, 9 insertions(+), 51 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 2204b66a..837d409e 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -11,12 +11,12 @@ import cats.syntax.all._ import cats.effect.unsafe.IORuntime //import scala.scalajs.js.JSConverters._ -import org.http4s.Request +//import org.http4s.Request import feral.functions.facade.JSRequest -import feral.functions.facade.JSHeaders +//import feral.functions.facade.JSHeaders import cats.effect.std.Dispatcher import feral.functions.util.Parser -import fs2.text.utf8 +//import fs2.text.utf8 abstract class IOAzureHttpFunction { protected def handler: InvocationContext => Resource[IO, HttpApp[IO]] @@ -27,7 +27,7 @@ abstract class IOAzureHttpFunction { IOAzureHttpFunction.App.http(functionName, appConfig) private val functionName: String = - getClass.getSimpleName.init // may want to add timestamp for testing + getClass.getSimpleName.init private val appConfig = js .Dynamic @@ -50,41 +50,17 @@ abstract class IOAzureHttpFunction { } (requestJS, context) => { - // do dispatcher thing - //context.log("befor FOR COMP") dispatcherHandle.`then`[js.Any] { case (dispatcher, handle) => { val io = for { - _ <- IO(context.log("FOR COMP")) request <- Parser.decodeRequest[IO](requestJS) - _ <- IO(context.log(request.uri.toString())) - //bodyList <- request.body.through(utf8.decode).compile.toList - //_ <- IO(context.log("req body: " + bodyList.toString())) - response <- handle(context).use(app => app.run(request)) - _ <- IO(context.log(response.status.toString())) - //bodyList <- response.body.through(utf8.decode).compile.toList - //_ <- IO(context.log("resp body: " + bodyList.toString())) respEncoded <- Parser.encodeResponse[IO](response, dispatcher) - _ <- IO(context.log("Decoded")) - _ <- IO(context.log(respEncoded.toString())) - } yield respEncoded - + } yield respEncoded + dispatcher.unsafeToPromise(io) } } - ///// - // impure!!! - - // val response = - // js.Dynamic - // .literal( - // status = 200, - // body = "payload", - // headers = js.Dynamic.literal("content-type" -> "text/plain") - // ) - - // js.Promise.resolve[js.UndefOr[js.Any]](response) } } } diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala index 5f229e8a..b8f382b2 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala @@ -109,14 +109,4 @@ object JSReadableStream { } object Syntax {} -} - -/* Parameters - -method: Method.GET, Method.POST, etc. -uri: representation of the request URI -httpVersion: the HTTP version //not used in lambda-http4s -headers: collection of Headers -body: fs2.Stream[F, Byte] defining the body of the request -attributes: Immutable Map used for carrying additional information in a type safe fashion //not used in lambda-http4s -*/ \ No newline at end of file +} \ No newline at end of file diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index c54699e4..b26736c9 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -10,7 +10,7 @@ import org.http4s.Response import feral.functions.facade.JSRequest import feral.functions.facade.JSHeaders -import cats.effect.kernel.Concurrent +//import cats.effect.kernel.Concurrent import cats.effect.kernel.Async import cats.syntax.all._ @@ -61,18 +61,10 @@ object Parser { body = toReadableStream[F](response.body, dispatcher)//response.body.through(fs2.text.utf8.decode).compile.toString ) - // val resp: js.Any = - // js.Dynamic - // .literal( - // status = 200, - // body = "payload", - // headers = js.Dynamic.literal("content-type" -> "text/plain") - // ) - responseEncoded.pure[F] - // resp.pure[F] } + //probably want to change this to implement pull and cancel, this may work but is not the best impl private def toReadableStream[F[_]: Async]( stream: Stream[F, Byte], dispatcher: Dispatcher[F]): js.Any = { From 9c243f18357244144203095980200c623cf9e2b3 Mon Sep 17 00:00:00 2001 From: Tim Date: Thu, 30 Jul 2026 18:48:21 -0400 Subject: [PATCH 14/20] rewrote encode response --- .../feral/functions/IOAzureHttpFunction.scala | 3 +- .../scala/feral/functions/util/Parser.scala | 141 +++++++++++++++--- .../feral/functions/util/StreamUtil.scala | 10 ++ 3 files changed, 133 insertions(+), 21 deletions(-) create mode 100644 azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 837d409e..75ee5483 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -20,6 +20,7 @@ import feral.functions.util.Parser abstract class IOAzureHttpFunction { protected def handler: InvocationContext => Resource[IO, HttpApp[IO]] + protected def qBound: Int = 100 private val runtime = IORuntime.global @@ -55,7 +56,7 @@ abstract class IOAzureHttpFunction { val io = for { request <- Parser.decodeRequest[IO](requestJS) response <- handle(context).use(app => app.run(request)) - respEncoded <- Parser.encodeResponse[IO](response, dispatcher) + respEncoded <- Parser.encodeResponseV2[IO](response, dispatcher, qBound) } yield respEncoded dispatcher.unsafeToPromise(io) diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index b26736c9..aca15a2d 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -13,6 +13,7 @@ import feral.functions.facade.JSHeaders //import cats.effect.kernel.Concurrent import cats.effect.kernel.Async import cats.syntax.all._ +import cats.effect.syntax.all._ import org.typelevel.ci.CIString @@ -22,6 +23,11 @@ import feral.functions.facade.JSReadableStream import fs2.Stream import cats.effect.std.Dispatcher +//import fs2.Chunk + +import StreamUtil._ +import cats.effect.std.Queue +import cats.effect.kernel.Fiber object Parser { def decodeRequest[F[_]: Async](request: JSRequest): F[Request[F]] = { @@ -47,7 +53,9 @@ object Parser { ) } - def encodeResponse[F[_]: Async](response: Response[F], dispatcher: Dispatcher[F]): F[js.Any] = { + def encodeResponse[F[_]: Async]( + response: Response[F], + dispatcher: Dispatcher[F]): F[js.Any] = { val headersList = response.headers.headers.map(h => (h.name.toString, h.value)) val headers = js.Dictionary(headersList: _*) @@ -58,35 +66,128 @@ object Parser { .literal( status = response.status.code, headers = headers, - body = toReadableStream[F](response.body, dispatcher)//response.body.through(fs2.text.utf8.decode).compile.toString + body = toReadableStream[F]( + response.body, + dispatcher + ) // response.body.through(fs2.text.utf8.decode).compile.toString ) responseEncoded.pure[F] } - //probably want to change this to implement pull and cancel, this may work but is not the best impl + // probably want to change this to implement pull and cancel, this may work but is not the best impl private def toReadableStream[F[_]: Async]( stream: Stream[F, Byte], dispatcher: Dispatcher[F]): js.Any = { - js.Dynamic.newInstance(js.Dynamic.global.ReadableStream)( - js.Dynamic.literal( - start = (controller: js.Dynamic) => { - val io = { - stream.chunks.evalMap { chunk => - Async[F].delay { - val array = new js.typedarray.Uint8Array(chunk.size) - chunk.toArray.zipWithIndex.foreach{ case (byte, index) => array(index) = byte } - controller.enqueue(array) + js.Dynamic + .newInstance(js.Dynamic.global.ReadableStream)( + js.Dynamic + .literal( + start = (controller: js.Dynamic) => { + val io = { + stream + .chunks + .evalMap { chunk => + Async[F].delay { + val array = new js.typedarray.Uint8Array(chunk.size) + chunk.toArray.zipWithIndex.foreach { + case (byte, index) => array(index) = byte + } + controller.enqueue(array) + }.void + } + .onFinalize(Async[F].delay(controller.close()).void) + .compile + .drain } - .void + dispatcher.unsafeToPromise(io) } - .onFinalize(Async[F].delay(controller.close()).void) - .compile - .drain - } - dispatcher.unsafeToPromise(io) - } + ) ) - ) + } + + def encodeResponseV2[F[_]: Async]( + response: Response[F], + dispatcher: Dispatcher[F], + qBound: Int): F[js.Any] = { + for { + q <- Queue.bounded[F, StreamData](qBound) + streamFiber <- createStreamFiber[F](response.body, q) + headers <- createHeaders[F](response.headers) + body <- createBodyStream[F](q, streamFiber, dispatcher) + } yield { + js.Dynamic + .literal( + status = response.status.code, + headers = headers, + body = body + ) + } + } + + def createStreamFiber[F[_]: Async]( + stream: Stream[F, Byte], + q: Queue[F, StreamData]): F[Fiber[F, Throwable, Unit]] = { + stream + .chunks + .evalMap(chunk => q.offer(Data(chunk))) + .compile + .drain + .attempt + .flatMap { + case Right(_) => q.offer(End) + case Left(e) => q.offer(Error(e)) + } + .start + } + + def createHeaders[F[_]: Async](responseHeaders: Headers): F[js.Dictionary[String]] = { + val headersList = responseHeaders.headers.map(h => (h.name.toString, h.value)) + js.Dictionary(headersList: _*).pure[F] + } + + def createBodyStream[F[_]: Async]( + q: Queue[F, StreamData], + streamFiber: Fiber[F, Throwable, Unit], + dispatcher: Dispatcher[F]): F[js.Any] = { + val readableStream: js.Any = js + .Dynamic + .newInstance(js.Dynamic.global.ReadableStream)( + js.Dynamic + .literal( + start = (controller: js.Dynamic) => { /*NoOp*/ }, + pull = (controller: js.Dynamic) => { + val effect = createPullEffect[F](controller, q) + dispatcher.unsafeToPromise(effect) + }, + cancel = (reason: js.UndefOr[js.Any]) => { + dispatcher.unsafeToPromise(streamFiber.cancel) + } + ) + ) + + readableStream.pure[F] + } + + def createPullEffect[F[_]: Async]( + controller: js.Dynamic, + q: Queue[F, StreamData]): F[Unit] = { + q.take.flatMap { + case Error(e) => { + Async[F].delay(controller.error(e.getMessage)).void + } + case End => { + Async[F].delay(controller.close()).void + } + case Data(chunk) => { + Async[F].delay { + val array = new js.typedarray.Uint8Array(chunk.size) + chunk.toArray.zipWithIndex.foreach { + case (byte, index) => array(index) = (byte & 0xff).toShort + } + controller.enqueue(array) + }.void + } + } } } diff --git a/azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala b/azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala new file mode 100644 index 00000000..b914d460 --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala @@ -0,0 +1,10 @@ +package feral.functions.util + +import fs2.Chunk + +object StreamUtil { + sealed trait StreamData + final case class Data(chunk: Chunk[Byte]) extends StreamData + case object End extends StreamData + final case class Error(e: Throwable) extends StreamData +} From 58513a4f5e166d778e2e06de10937c59ff982ae6 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 1 Aug 2026 10:32:33 -0400 Subject: [PATCH 15/20] completed basic invocation context --- .../feral/functions/IOAzureHttpFunction.scala | 2 +- .../functions/facade/InvocationContext.scala | 56 ++++++++++++++----- .../scala/feral/functions/util/Parser.scala | 53 ------------------ 3 files changed, 43 insertions(+), 68 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 75ee5483..df80e183 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -56,7 +56,7 @@ abstract class IOAzureHttpFunction { val io = for { request <- Parser.decodeRequest[IO](requestJS) response <- handle(context).use(app => app.run(request)) - respEncoded <- Parser.encodeResponseV2[IO](response, dispatcher, qBound) + respEncoded <- Parser.encodeResponse[IO](response, dispatcher, qBound) } yield respEncoded dispatcher.unsafeToPromise(io) diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala index fd679abd..3f2a9ba3 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala @@ -10,21 +10,49 @@ trait InvocationContext extends js.Object { def info(args: js.Any*): Unit = js.native def warn(args: js.Any*): Unit = js.native def error(args: js.Any*): Unit = js.native + + def functionId: String = js.native + def functionName: String = js.native + def extraInputs: ExtraInputs = js.native + def extraOutputs: ExtraOutputs = js.native + def retryContext: js.UndefOr[RetryContext] = js.native + def traceContext: js.UndefOr[TraceContext] = js.native + def options: Options = js.native +} + +@js.native +trait ExtraInputs extends js.Object { + def get(binding: js.Any): js.Any = js.native +} + +@js.native +trait ExtraOutputs extends js.Object { + def set(binding: js.Any, value: js.Any): Unit = js.native +} + +@js.native +trait RetryContext extends js.Object { + def retryCount: Int = js.native + def maxRetryCount: Int = js.native + def exception: js.UndefOr[js.Any] = js.native +} + +@js.native +trait TraceContext extends js.Object { + def traceParent: js.UndefOr[String] = js.native + def traceState: js.UndefOr[String] = js.native + def attributes: js.UndefOr[js.Dictionary[js.Any]] = js.native +} + +@js.native +trait Options extends js.Object { + def trigger: js.UndefOr[js.Any] = js.native + def extraInputs: js.UndefOr[js.Array[js.Any]] = js.native + def extraOutputs: js.UndefOr[js.Array[js.Any]] = js.native + def `return`: js.UndefOr[js.Any] = js.native } /* -invocationId: string; - functionName: string; - extraInputs: InvocationContextExtraInputs; - extraOutputs: InvocationContextExtraOutputs; - log(...args: any[]): void; - trace(...args: any[]): void; - debug(...args: any[]): void; - info(...args: any[]): void; - warn(...args: any[]): void; - error(...args: any[]): void; - retryContext?: RetryContext; - traceContext?: TraceContext; - triggerMetadata?: TriggerMetadata; - options: EffectiveFunctionOptions; +https://docs.azure.cn/en-us/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#invocation-context + */ \ No newline at end of file diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index aca15a2d..d77abd3e 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -54,59 +54,6 @@ object Parser { } def encodeResponse[F[_]: Async]( - response: Response[F], - dispatcher: Dispatcher[F]): F[js.Any] = { - val headersList = response.headers.headers.map(h => (h.name.toString, h.value)) - val headers = js.Dictionary(headersList: _*) - - // val body = response.body //need to figure this out later - - val responseEncoded: js.Any = js - .Dynamic - .literal( - status = response.status.code, - headers = headers, - body = toReadableStream[F]( - response.body, - dispatcher - ) // response.body.through(fs2.text.utf8.decode).compile.toString - ) - - responseEncoded.pure[F] - } - - // probably want to change this to implement pull and cancel, this may work but is not the best impl - private def toReadableStream[F[_]: Async]( - stream: Stream[F, Byte], - dispatcher: Dispatcher[F]): js.Any = { - js.Dynamic - .newInstance(js.Dynamic.global.ReadableStream)( - js.Dynamic - .literal( - start = (controller: js.Dynamic) => { - val io = { - stream - .chunks - .evalMap { chunk => - Async[F].delay { - val array = new js.typedarray.Uint8Array(chunk.size) - chunk.toArray.zipWithIndex.foreach { - case (byte, index) => array(index) = byte - } - controller.enqueue(array) - }.void - } - .onFinalize(Async[F].delay(controller.close()).void) - .compile - .drain - } - dispatcher.unsafeToPromise(io) - } - ) - ) - } - - def encodeResponseV2[F[_]: Async]( response: Response[F], dispatcher: Dispatcher[F], qBound: Int): F[js.Any] = { From b5d8db6c87b4464d2f88e9e432eebb195fc625d6 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 1 Aug 2026 11:28:09 -0400 Subject: [PATCH 16/20] done? --- .../feral/functions/IOAzureHttpFunction.scala | 16 +---- .../functions/facade/InvocationContext.scala | 1 - .../feral/functions/facade/JSRequest.scala | 3 - .../feral/functions/util/AppConfig.scala | 59 +++++++++++++++++++ .../scala/feral/functions/util/Parser.scala | 6 +- 5 files changed, 63 insertions(+), 22 deletions(-) create mode 100644 azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index df80e183..1c12a7f2 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -10,16 +10,15 @@ import cats.effect.Resource import cats.syntax.all._ import cats.effect.unsafe.IORuntime -//import scala.scalajs.js.JSConverters._ -//import org.http4s.Request + import feral.functions.facade.JSRequest -//import feral.functions.facade.JSHeaders import cats.effect.std.Dispatcher import feral.functions.util.Parser -//import fs2.text.utf8 +import feral.functions.util.AppConfig.buildDefaultConfig abstract class IOAzureHttpFunction { protected def handler: InvocationContext => Resource[IO, HttpApp[IO]] + protected def appConfig = buildDefaultConfig(handlerFn).toJS protected def qBound: Int = 100 private val runtime = IORuntime.global @@ -30,15 +29,6 @@ abstract class IOAzureHttpFunction { private val functionName: String = getClass.getSimpleName.init - private val appConfig = js - .Dynamic - .literal( - methods = js.Array("GET", "PUT"), - authLevel = "anonymous", - route = "{*path}", - handler = handlerFn - ) - private lazy val handlerFn : js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { val dispatcherHandle = { diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala index 3f2a9ba3..d2ccc4bb 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala @@ -54,5 +54,4 @@ trait Options extends js.Object { /* https://docs.azure.cn/en-us/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#invocation-context - */ \ No newline at end of file diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala index b8f382b2..232c81e3 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala @@ -6,12 +6,9 @@ import scala.scalajs.js.typedarray.Uint8Array import fs2.Stream import fs2.Chunk -//import cats.effect.IO import cats.effect.kernel.Async import cats.syntax.all._ -//may want to change to requestFacade.., think it over - @js.native trait JSRequest extends js.Object { def method: String = js.native diff --git a/azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala b/azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala new file mode 100644 index 00000000..25c99e62 --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala @@ -0,0 +1,59 @@ +package feral.functions.util + +import AppConfig._ +import scala.scalajs.js +import feral.functions.facade.JSRequest +import feral.functions.facade.InvocationContext + +final case class AppConfig( + methods: List[HttpMethod], + authLevel: AuthLevel, + route: AzureRoute, + handlerFn: HandlerFnT) { + def toJS: js.Object = { + js.Dynamic + .literal( + methods = methodsToJS(methods), + authLevel = authLevel.value, + route = route.value, + handler = handlerFn + ) + } +} + +object AppConfig { + sealed trait HttpMethod { + def value: String = { + this match { + case Get => "GET" + case Post => "POST" + case Put => "PUT" + case Patch => "PATCH" + case Delete => "DELETE" + } + } + } + case object Get extends HttpMethod + case object Post extends HttpMethod + case object Put extends HttpMethod + case object Patch extends HttpMethod + case object Delete extends HttpMethod + + def methodsToJS(list: List[HttpMethod]): js.Array[String] = { + val dList = list.distinct.map(_.value) + js.Array(dList: _*) + } + + case class AuthLevel(value: String) + val anonymous: AuthLevel = AuthLevel("anonymous") + // add more? + + case class AzureRoute(value: String) + val catchAll: AzureRoute = AzureRoute("{*path}") + // add more? + + type HandlerFnT = js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] + + def buildDefaultConfig(handlerFn: HandlerFnT) = + AppConfig(List(Get, Post, Put, Patch, Delete), anonymous, catchAll, handlerFn) +} diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index d77abd3e..f0d84f0a 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -10,7 +10,6 @@ import org.http4s.Response import feral.functions.facade.JSRequest import feral.functions.facade.JSHeaders -//import cats.effect.kernel.Concurrent import cats.effect.kernel.Async import cats.syntax.all._ import cats.effect.syntax.all._ @@ -19,11 +18,9 @@ import org.typelevel.ci.CIString import scala.scalajs.js import feral.functions.facade.JSReadableStream -//import scala.scalajs.js.annotation._ import fs2.Stream import cats.effect.std.Dispatcher -//import fs2.Chunk import StreamUtil._ import cats.effect.std.Queue @@ -44,12 +41,11 @@ object Parser { Headers(builder.result()) } - // body = JSReadableStream.toFs2[F](request.body) //need to use generic type parameter in tofs2 } yield Request[F]( method = method, uri = uri, headers = headers, - body = body // need to convert into Entity + body = body ) } From 1dd0dbeaa1df824eacc15ba280316d937b064c12 Mon Sep 17 00:00:00 2001 From: Tim Date: Sat, 1 Aug 2026 11:29:45 -0400 Subject: [PATCH 17/20] handled warnings --- .../js/src/main/scala/feral/functions/util/Parser.scala | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index f0d84f0a..51f8dd16 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -98,12 +98,12 @@ object Parser { .newInstance(js.Dynamic.global.ReadableStream)( js.Dynamic .literal( - start = (controller: js.Dynamic) => { /*NoOp*/ }, + start = (_: js.Dynamic) => { /*NoOp*/ }, pull = (controller: js.Dynamic) => { val effect = createPullEffect[F](controller, q) dispatcher.unsafeToPromise(effect) }, - cancel = (reason: js.UndefOr[js.Any]) => { + cancel = (_: js.UndefOr[js.Any]) => { dispatcher.unsafeToPromise(streamFiber.cancel) } ) From 50035402147dd9aff666a4da9d0f17bd045bf21d Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Aug 2026 13:23:44 -0400 Subject: [PATCH 18/20] ready for first PR --- .../feral/functions/IOAzureHttpFunction.scala | 37 +++++++++++++----- .../functions/facade/InvocationContext.scala | 22 ++++++++--- .../feral/functions/facade/JSRequest.scala | 30 ++++++++++---- .../feral/functions/util/AppConfig.scala | 26 +++++++++++-- .../scala/feral/functions/util/Parser.scala | 39 +++++++++++++------ .../feral/functions/util/StreamUtil.scala | 16 ++++++++ 6 files changed, 133 insertions(+), 37 deletions(-) diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 1c12a7f2..8c3bad8e 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -1,35 +1,52 @@ +/* + * 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.functions import scala.scalajs.js import scala.scalajs.js.annotation._ + import feral.functions.facade.InvocationContext +import feral.functions.facade.JSRequest +import feral.functions.util.AppConfig.buildDefaultConfig +import feral.functions.util.AppConfig +import feral.functions.util.Parser import org.http4s.HttpApp + import cats.effect.IO import cats.effect.Resource import cats.syntax.all._ import cats.effect.unsafe.IORuntime - - -import feral.functions.facade.JSRequest import cats.effect.std.Dispatcher -import feral.functions.util.Parser -import feral.functions.util.AppConfig.buildDefaultConfig abstract class IOAzureHttpFunction { protected def handler: InvocationContext => Resource[IO, HttpApp[IO]] - protected def appConfig = buildDefaultConfig(handlerFn).toJS + protected def appConfig: AppConfig = buildDefaultConfig(handlerFn) protected def qBound: Int = 100 - private val runtime = IORuntime.global + private[functions] val runtime = IORuntime.global final def main(args: Array[String]): Unit = - IOAzureHttpFunction.App.http(functionName, appConfig) + IOAzureHttpFunction.App.http(functionName, appConfig.toJS) - private val functionName: String = + private[functions] val functionName: String = getClass.getSimpleName.init - private lazy val handlerFn + private[functions] lazy val handlerFn : js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] = { val dispatcherHandle = { Dispatcher diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala index d2ccc4bb..35d44f89 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala @@ -1,3 +1,19 @@ +/* + * 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.functions.facade import scala.scalajs.js @@ -50,8 +66,4 @@ trait Options extends js.Object { def extraInputs: js.UndefOr[js.Array[js.Any]] = js.native def extraOutputs: js.UndefOr[js.Array[js.Any]] = js.native def `return`: js.UndefOr[js.Any] = js.native -} - -/* -https://docs.azure.cn/en-us/azure-functions/functions-reference-node?tabs=javascript%2Cwindows%2Cazure-cli&pivots=nodejs-model-v4#invocation-context - */ \ No newline at end of file +} \ No newline at end of file diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala index 232c81e3..bc1a3fba 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala @@ -1,3 +1,19 @@ +/* + * 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.functions.facade import scala.scalajs.js @@ -10,7 +26,7 @@ import cats.effect.kernel.Async import cats.syntax.all._ @js.native -trait JSRequest extends js.Object { +private[functions] trait JSRequest extends js.Object { def method: String = js.native def url: String = js.native def headers: JSHeaders = js.native @@ -18,31 +34,31 @@ trait JSRequest extends js.Object { } @js.native -trait JSHeaders extends js.Object { +private[functions] trait JSHeaders extends js.Object { def get(name: String): js.UndefOr[String] = js.native def keys(): js.Iterator[String] = js.native } @js.native -trait JSReadableStream extends js.Object { +private[functions] trait JSReadableStream extends js.Object { def getReader(): JSReadableStreamDefaultReader = js.native } @js.native -trait JSReadableStreamDefaultReader extends js.Object { +private[functions] trait JSReadableStreamDefaultReader extends js.Object { def read(): js.Promise[JSReadObject] = js.native def releaseLock(): Unit = js.native def cancel(reason: js.UndefOr[js.Any]): js.Promise[Unit] } @js.native -trait JSReadObject extends js.Object { +private[functions] trait JSReadObject extends js.Object { def value: js.UndefOr[Uint8Array] = js.native def done: Boolean = js.native } object JSHeaders { - def keyList(h: JSHeaders): List[String] = { + private[functions] def keyList(h: JSHeaders): List[String] = { val builder = List.newBuilder[String] val itr = h.keys() var entity = itr.next() @@ -61,7 +77,7 @@ object JSHeaders { } object JSReadableStream { - def toFs2[F[_]: Async](streamOption: js.UndefOr[JSReadableStream]): Stream[F, Byte] = { + private[functions] def toFs2[F[_]: Async](streamOption: js.UndefOr[JSReadableStream]): Stream[F, Byte] = { streamOption.toOption match { case None => Stream.empty case Some(null) => Stream.empty diff --git a/azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala b/azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala index 25c99e62..1fcc329b 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/AppConfig.scala @@ -1,7 +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.functions.util import AppConfig._ + import scala.scalajs.js + import feral.functions.facade.JSRequest import feral.functions.facade.InvocationContext @@ -10,7 +28,7 @@ final case class AppConfig( authLevel: AuthLevel, route: AzureRoute, handlerFn: HandlerFnT) { - def toJS: js.Object = { + private[functions] def toJS: js.Object = { js.Dynamic .literal( methods = methodsToJS(methods), @@ -23,7 +41,7 @@ final case class AppConfig( object AppConfig { sealed trait HttpMethod { - def value: String = { + private[functions] def value: String = { this match { case Get => "GET" case Post => "POST" @@ -39,7 +57,7 @@ object AppConfig { case object Patch extends HttpMethod case object Delete extends HttpMethod - def methodsToJS(list: List[HttpMethod]): js.Array[String] = { + private def methodsToJS(list: List[HttpMethod]): js.Array[String] = { val dList = list.distinct.map(_.value) js.Array(dList: _*) } @@ -54,6 +72,6 @@ object AppConfig { type HandlerFnT = js.Function2[JSRequest, InvocationContext, js.Promise[js.UndefOr[js.Any]]] - def buildDefaultConfig(handlerFn: HandlerFnT) = + private[functions] def buildDefaultConfig(handlerFn: HandlerFnT) = AppConfig(List(Get, Post, Put, Patch, Delete), anonymous, catchAll, handlerFn) } diff --git a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala index 51f8dd16..f5ba1f0b 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/Parser.scala @@ -1,3 +1,19 @@ +/* + * 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.functions.util import org.http4s.Request @@ -9,25 +25,26 @@ import org.http4s.Response import feral.functions.facade.JSRequest import feral.functions.facade.JSHeaders +import feral.functions.facade.JSReadableStream -import cats.effect.kernel.Async import cats.syntax.all._ + +import cats.effect.kernel.Async import cats.effect.syntax.all._ +import cats.effect.std.Queue +import cats.effect.kernel.Fiber +import cats.effect.std.Dispatcher import org.typelevel.ci.CIString import scala.scalajs.js -import feral.functions.facade.JSReadableStream import fs2.Stream -import cats.effect.std.Dispatcher import StreamUtil._ -import cats.effect.std.Queue -import cats.effect.kernel.Fiber object Parser { - def decodeRequest[F[_]: Async](request: JSRequest): F[Request[F]] = { + private[functions] def decodeRequest[F[_]: Async](request: JSRequest): F[Request[F]] = { for { method <- Method.fromString(request.method).liftTo[F] uri <- Uri.fromString(request.url).liftTo[F] @@ -49,7 +66,7 @@ object Parser { ) } - def encodeResponse[F[_]: Async]( + private[functions] def encodeResponse[F[_]: Async]( response: Response[F], dispatcher: Dispatcher[F], qBound: Int): F[js.Any] = { @@ -68,7 +85,7 @@ object Parser { } } - def createStreamFiber[F[_]: Async]( + private def createStreamFiber[F[_]: Async]( stream: Stream[F, Byte], q: Queue[F, StreamData]): F[Fiber[F, Throwable, Unit]] = { stream @@ -84,12 +101,12 @@ object Parser { .start } - def createHeaders[F[_]: Async](responseHeaders: Headers): F[js.Dictionary[String]] = { + private def createHeaders[F[_]: Async](responseHeaders: Headers): F[js.Dictionary[String]] = { val headersList = responseHeaders.headers.map(h => (h.name.toString, h.value)) js.Dictionary(headersList: _*).pure[F] } - def createBodyStream[F[_]: Async]( + private def createBodyStream[F[_]: Async]( q: Queue[F, StreamData], streamFiber: Fiber[F, Throwable, Unit], dispatcher: Dispatcher[F]): F[js.Any] = { @@ -112,7 +129,7 @@ object Parser { readableStream.pure[F] } - def createPullEffect[F[_]: Async]( + private def createPullEffect[F[_]: Async]( controller: js.Dynamic, q: Queue[F, StreamData]): F[Unit] = { q.take.flatMap { diff --git a/azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala b/azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala index b914d460..ca56e60e 100644 --- a/azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala +++ b/azure-functions/js/src/main/scala/feral/functions/util/StreamUtil.scala @@ -1,3 +1,19 @@ +/* + * 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.functions.util import fs2.Chunk From 9d68f5ac3718b13454d03ced8c3d41535c04a8a1 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Aug 2026 13:45:47 -0400 Subject: [PATCH 19/20] Hid invocationContext from user --- .../feral/functions/IOAzureHttpFunction.scala | 5 ++-- .../feral/functions/facade/Context.scala | 26 +++++++++++++++++++ .../functions/facade/InvocationContext.scala | 14 +++++----- .../feral/functions/facade/JSRequest.scala | 26 ++++++++++--------- 4 files changed, 50 insertions(+), 21 deletions(-) create mode 100644 azure-functions/js/src/main/scala/feral/functions/facade/Context.scala diff --git a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala index 8c3bad8e..df6081e8 100644 --- a/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala +++ b/azure-functions/js/src/main/scala/feral/functions/IOAzureHttpFunction.scala @@ -32,9 +32,10 @@ import cats.effect.Resource import cats.syntax.all._ import cats.effect.unsafe.IORuntime import cats.effect.std.Dispatcher +import feral.functions.facade.Context abstract class IOAzureHttpFunction { - protected def handler: InvocationContext => Resource[IO, HttpApp[IO]] + protected def handler: Context => Resource[IO, HttpApp[IO]] protected def appConfig: AppConfig = buildDefaultConfig(handlerFn) protected def qBound: Int = 100 @@ -62,7 +63,7 @@ abstract class IOAzureHttpFunction { case (dispatcher, handle) => { val io = for { request <- Parser.decodeRequest[IO](requestJS) - response <- handle(context).use(app => app.run(request)) + response <- handle(Context(context)).use(app => app.run(request)) respEncoded <- Parser.encodeResponse[IO](response, dispatcher, qBound) } yield respEncoded diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/Context.scala b/azure-functions/js/src/main/scala/feral/functions/facade/Context.scala new file mode 100644 index 00000000..9a2e1919 --- /dev/null +++ b/azure-functions/js/src/main/scala/feral/functions/facade/Context.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.functions.facade + +final case class Context(context: InvocationContext) { + def log(m: String): Unit = context.log(m) + def trace(m: String): Unit = context.trace(m) + def debug(m: String): Unit = context.debug(m) + def info(m: String): Unit = context.info(m) + def warn(m: String): Unit = context.warn(m) + def error(m: String): Unit = context.error(m) +} diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala index 35d44f89..e0b33fbb 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/InvocationContext.scala @@ -19,7 +19,7 @@ package feral.functions.facade import scala.scalajs.js @js.native -trait InvocationContext extends js.Object { +private[functions] trait InvocationContext extends js.Object { def log(args: js.Any*): Unit = js.native def trace(args: js.Any*): Unit = js.native def debug(args: js.Any*): Unit = js.native @@ -37,33 +37,33 @@ trait InvocationContext extends js.Object { } @js.native -trait ExtraInputs extends js.Object { +private[functions] trait ExtraInputs extends js.Object { def get(binding: js.Any): js.Any = js.native } @js.native -trait ExtraOutputs extends js.Object { +private[functions] trait ExtraOutputs extends js.Object { def set(binding: js.Any, value: js.Any): Unit = js.native } @js.native -trait RetryContext extends js.Object { +private[functions] trait RetryContext extends js.Object { def retryCount: Int = js.native def maxRetryCount: Int = js.native def exception: js.UndefOr[js.Any] = js.native } @js.native -trait TraceContext extends js.Object { +private[functions] trait TraceContext extends js.Object { def traceParent: js.UndefOr[String] = js.native def traceState: js.UndefOr[String] = js.native def attributes: js.UndefOr[js.Dictionary[js.Any]] = js.native } @js.native -trait Options extends js.Object { +private[functions] trait Options extends js.Object { def trigger: js.UndefOr[js.Any] = js.native def extraInputs: js.UndefOr[js.Array[js.Any]] = js.native def extraOutputs: js.UndefOr[js.Array[js.Any]] = js.native def `return`: js.UndefOr[js.Any] = js.native -} \ No newline at end of file +} diff --git a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala index bc1a3fba..01388e86 100644 --- a/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala +++ b/azure-functions/js/src/main/scala/feral/functions/facade/JSRequest.scala @@ -36,7 +36,7 @@ private[functions] trait JSRequest extends js.Object { @js.native private[functions] trait JSHeaders extends js.Object { def get(name: String): js.UndefOr[String] = js.native - def keys(): js.Iterator[String] = js.native + def keys(): js.Iterator[String] = js.native } @js.native @@ -63,7 +63,7 @@ object JSHeaders { val itr = h.keys() var entity = itr.next() - while(!entity.done) { + while (!entity.done) { builder.addOne(entity.value) entity = itr.next() } @@ -72,27 +72,29 @@ object JSHeaders { } object Syntax { - //syntax for method like calls??? + // syntax for method like calls??? } } object JSReadableStream { - private[functions] def toFs2[F[_]: Async](streamOption: js.UndefOr[JSReadableStream]): Stream[F, Byte] = { + private[functions] def toFs2[F[_]: Async]( + streamOption: js.UndefOr[JSReadableStream]): Stream[F, Byte] = { streamOption.toOption match { case None => Stream.empty case Some(null) => Stream.empty case Some(stream) => { - Stream.eval(Async[F].delay(stream.getReader())).flatMap{ reader => + Stream.eval(Async[F].delay(stream.getReader())).flatMap { reader => def nextChunk = { - Async[F].fromPromise(Async[F].delay(reader.read())).map{ read => - if(read.done) { + Async[F].fromPromise(Async[F].delay(reader.read())).map { read => + if (read.done) { None } else { - val chunk = read.value + val chunk = read + .value .toOption .map(arr => Chunk.array[Byte](toByteArray(arr))) .getOrElse(Chunk.empty[Byte]) - + Some(chunk) } } @@ -108,12 +110,12 @@ object JSReadableStream { } } - private def toByteArray(array: Uint8Array): Array[Byte] = { + private def toByteArray(array: Uint8Array): Array[Byte] = { val builder = Array.newBuilder[Byte] val length = array.length var index = 0 - while(index != length) { + while (index != length) { builder.addOne(array(index).toByte) index = index + 1 } @@ -122,4 +124,4 @@ object JSReadableStream { } object Syntax {} -} \ No newline at end of file +} From 10b57721d7e473736c78252ad4769fe0016a758c Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Aug 2026 14:16:19 -0400 Subject: [PATCH 20/20] Removed snapshot --- build.sbt | 1 - 1 file changed, 1 deletion(-) diff --git a/build.sbt b/build.sbt index d5834b3c..6fb77a50 100644 --- a/build.sbt +++ b/build.sbt @@ -20,7 +20,6 @@ name := "feral" ThisBuild / tlBaseVersion := "0.3" ThisBuild / startYear := Some(2021) -ThisBuild / isSnapshot := true ThisBuild / developers := List( tlGitHubDev("armanbilge", "Arman Bilge"),