Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

(JS) Expose handler function type to allow manual exporting of Lambda handler #248

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
29 changes: 20 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,22 @@ Next, implement your Lambda. Please refer to the [examples](examples/src/main/sc
There are several options to deploy your Lambda. For example you can use the [Lambda console](https://docs.aws.amazon.com/lambda/latest/dg/foundation-console.html), the [SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html), or the [serverless framework](https://www.serverless.com/framework/docs/providers/aws/guide/deploying).

To deploy a Scala.js Lambda, you will need to know the following:

1. The runtime for your Lambda is Node.js 16.
2. The handler for your Lambda is `index.yourLambdaName`.
- `index` refers to the `index.js` file containing the JavaScript sources for your Lambda.
- `yourLambdaName` is the name of the Scala `object` you created that extends from `IOLambda`.
3. Run `sbt npmPackage` to package your Lambda for deployment. Note that you can currently only have one Lambda per sbt (sub-)project. If you have multiple, you will need to select the one to deploy using `Compile / mainClass := Some("my.lambda.handler")`.
2. Add a `@JSExportTopLevel` inside your lambda object:

```scala
@JSExportTopLevel("yourLambdaName")
val impl: HandlerFn = handlerFn
```

- The type `HandlerFn` is important so Scala.js will emit your lambda as a JavaScript function.
- `val` is important so your lambda function is emitted as a value containing a function, instead of a function returning another function.
- The handler for your Lambda is `index.yourLambdaName`
- `index` refers to the `index.js` file containing the JavaScript sources for your Lambda.
- `yourLambdaName` is the name of the export and can be changed if desired.

3. Run `sbt npmPackage` to package your Lambda for deployment. You can have multiple lambda's per sbt project by exporting multiple handler functions.
4. For the tooling of your choice, follow their instructions for deploying a Node.js Lambda using the contents of the `target/scala-2.13/npm-package/` directory.

As the feral project develops, one of the goals is to provide an sbt plugin that simplifies and automates the deployment process. If this appeals to you, please contribute feature requests, ideas, and/or code!
Expand All @@ -37,13 +48,13 @@ As the feral project develops, one of the goals is to provide an sbt plugin that

The premise that you can (and should!) write production-ready serverless functions in Scala targeting JavaScript may be a surprising one. This project—and the rapid maturity of the Typelevel.js ecosystem—is motivated by three ideas.

1. **JavaScript is the ideal compile target for serverless functions.**
There are a lot of reasons for this, cold-start being one of them, but more generally it's important to remember what the JVM is and is not good at. In particular, the JVM excels at long-lived multithreaded applications which are relatively memory-heavy and rely on medium-lifespan heap allocations. So in other words, persistent microservices.
1. **JavaScript is the ideal compile target for serverless functions.**

There are a lot of reasons for this, cold-start being one of them, but more generally it's important to remember what the JVM is and is not good at. In particular, the JVM excels at long-lived multithreaded applications which are relatively memory-heavy and rely on medium-lifespan heap allocations. So in other words, persistent microservices.

Serverless functions are, by definition, not this. They are not persistent, they are (generally) single-threaded, and they need to start very quickly with minimal warming. They do often apply moderate-to-significant heap pressure, but this factor is more than outweighed by the others.
Serverless functions are, by definition, not this. They are not persistent, they are (generally) single-threaded, and they need to start very quickly with minimal warming. They do often apply moderate-to-significant heap pressure, but this factor is more than outweighed by the others.

V8 (the JavaScript engine in Node.js) is a very good runtime for these kinds of use-cases. Realistically, it may be the best-optimized runtime in existence for these requirements, similar to how the JVM is likely the best-optimized runtime in existence for the persistent microservices case.
V8 (the JavaScript engine in Node.js) is a very good runtime for these kinds of use-cases. Realistically, it may be the best-optimized runtime in existence for these requirements, similar to how the JVM is likely the best-optimized runtime in existence for the persistent microservices case.

2. **Scala.js and Cats Effect work together to provide powerful, well-defined semantics for writing JavaScript applications.**

Expand Down
2 changes: 1 addition & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@

name := "feral"

ThisBuild / tlBaseVersion := "0.1"
ThisBuild / tlBaseVersion := "0.2"
ThisBuild / startYear := Some(2021)

ThisBuild / developers := List(
Expand Down
17 changes: 12 additions & 5 deletions lambda/js/src/main/scala/feral/lambda/IOLambdaPlatform.scala
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,19 @@ import scala.scalajs.js.|
private[lambda] trait IOLambdaPlatform[Event, Result] {
this: IOLambda[Event, Result] =>

final def main(args: Array[String]): Unit =
js.Dynamic.global.exports.updateDynamic(handlerName)(handlerFn)
/**
* Lambda handler. Implement this type with a val and a call to `handlerFn` to export your
* handler.
*
* @example
* {{{
* @JSExportTopLevel("handler")
* val handler: HandlerFn = handlerFn
* }}}
*/
final type HandlerFn = js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]]
hugo-vrijswijk marked this conversation as resolved.
Show resolved Hide resolved

protected def handlerName: String = getClass.getSimpleName.init

private lazy val handlerFn
final protected lazy val handlerFn
: js.Function2[js.Any, facade.Context, js.Promise[js.Any | Unit]] = {
(event: js.Any, context: facade.Context) =>
(for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ object LambdaJSPlugin extends AutoPlugin {
override def projectSettings: Seq[Setting[_]] = Seq(
libraryDependencies +=
BuildInfo.organization %%% BuildInfo.name.drop(4) % BuildInfo.version,
scalaJSUseMainModuleInitializer := true,
scalaJSUseMainModuleInitializer := false,
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.CommonJSModule)),
npmPackageOutputFilename := "index.js",
npmPackageStage := Stage.FullOpt
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import _root_.io.circe.syntax._

scalaVersion := "2.13.8"
enablePlugins(LambdaJSPlugin)
scalaJSLinkerConfig ~= (_.withModuleKind(ModuleKind.ESModule))
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
sbt.version=1.7.1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("org.typelevel" % "sbt-feral-lambda" % sys.props("plugin.version"))
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import cats.effect._
import feral.lambda._
import scala.scalajs.js.annotation._

object mySimpleHandler extends IOLambda.Simple[Unit, INothing] {
def apply(event: Unit, context: Context[IO], init: Init): IO[Option[INothing]] = IO.none

@JSExportTopLevel("mySimpleHandler")
val impl: HandlerFn = handlerFn
}
4 changes: 4 additions & 0 deletions sbt-lambda/src/sbt-test/lambda-js-plugin/iolambda-es/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
> npmPackage
$ exists target/scala-2.13/npm-package/index.js
$ exists target/scala-2.13/npm-package/package.json
$ exec node test-export.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
if (typeof (await import('./target/scala-2.13/npm-package/index.js')).mySimpleHandler === 'function') {
process.exit(0);
} else {
process.exit(1);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import cats.effect._
import feral.lambda._
import scala.scalajs.js.annotation._

object mySimpleHandler extends IOLambda.Simple[Unit, INothing] {
def apply(event: Unit, context: Context[IO], init: Init): IO[Option[INothing]] = IO.none

@JSExportTopLevel("mySimpleHandler")
val impl: HandlerFn = handlerFn
}