Wasm Builders 🧱

Cover image for A simple calc serverless function with Capsule, NATS and Node.js
Philippe Charrière
Philippe Charrière

Posted on

A simple calc serverless function with Capsule, NATS and Node.js

Quick reminders

Capsule?

Capsule is a runner (or launcher) of wasm functions. Capsule can serve the functions through HTTP, NATS and MQTT (it’s possible to use Capsule as a simple CLI).

The wasm functions are wasm modules importing the host functions provided by Capsule. All these modules are built with TinyGo thanks to its WASI support.

capsule_features

Capsule is developed thanks to the Wazero project: "the zero dependency WebAssembly runtime for Go developers".

Btw, Capsule is small (~17 mb), wasm modules too (~100 ko to 900 ko), then it runs like a charm on a Raspberry Pi 3 A+ 🤩

The NATS support in Capsule is very new (subject to change).

Install the last version of Capsule

I usually run these commands:

CAPSULE_VERSION="v0.2.6"
OS="linux"
ARCH="amd64"

wget https://github.com/bots-garden/capsule/releases/\
download/${CAPSULE_VERSION}/\
capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz

sudo tar -zxf \
capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz \
--directory /usr/local/bin

rm capsule-${CAPSULE_VERSION}-${OS}-${ARCH}.tar.gz
Enter fullscreen mode Exit fullscreen mode

NATS? (very quickly)

The NATS protocol allows client applications to exchange data on subjects through a network. Subjects are like channels on Slack (his explanation engages only me 🙂). A clients could be a publisher of message (on a subject) or a subscriber (listening on a subject). A subscriber will be triggered if a message happens on the subject. So, you can see it as a "pub/sub" system.

Here we go 🚀

This post does not present all the NATS capabilities of Capsule but only a simple example of how to write a calc function (the wasm module) and how to call it from JavaScript (with Node.js) thanks to the NATS protocol.

We needs a NATS server

The NATS server installation is pretty straightforward:

NATS_VERSION="2.9.0"
NATS_OS="linux-amd64"
curl -L https://github.com/nats-io/nats-server/releases/download/v${NATS_VERSION}/nats-server-v${NATS_VERSION}-${NATS_OS}.zip -o nats-server.zip
unzip nats-server.zip -d nats-server
sudo cp nats-server/nats-server-v${NATS_VERSION}-${NATS_OS}/nats-server /usr/bin
Enter fullscreen mode Exit fullscreen mode

Of course adapt this for your own platform

  • To start the NATS server: nats-server
  • To stop the NATS server: nats-server --signal stop (or Ctrl+C)

The calc function (in Go, built with TinyGo)

The wasm calc will operate a NATS subscriber.

A "NATS subscriber" function with Capsule will always have the following form:

package main

import (
  hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"
)

func main() {
  hf.OnNatsMessage(Handle)
}

func Handle(params []string) {
  natsMessage := params[0]
  // foo
}
Enter fullscreen mode Exit fullscreen mode

The Handle method is called when a message happens on a specific subject.

The calc module will receive json string messages like:

{"operation":"*","operand1":21,"operand2":2}
Enter fullscreen mode Exit fullscreen mode

Let's write the calc function:

package main

import (
  /* string to json */
  "github.com/tidwall/gjson"
  /* create json string */
  "github.com/tidwall/sjson"

  hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"
)

func main() {
  hf.OnNatsMessage(Handle)
}

func Handle(params []string) {
  natsMessage := params[0] // 1️⃣

  operation := gjson.Get(natsMessage, "operation").String()
  operand1 := gjson.Get(natsMessage, "operand1").Float() 
  operand2 := gjson.Get(natsMessage, "operand2").Float() 

  var res float64

  switch operation {
  case "+":
    res = operand1 + operand2
  case "-":
    res = operand1 - operand2
  case "*":
    res = operand1 * operand2
  case "/":
    res = operand1 / operand2
  default:
    res = 0.0
  }

  result := `{"result": ""}`
  result, _ = sjson.Set(result, "result", res)

  hf.NatsReply(result, 10) // 2️⃣ 
}

//export OnLoad
func OnLoad() {
  hf.Log("🙂 I'm the calc function")
  hf.Log("👂Listening on: " + hf.NatsGetSubject())
  hf.Log("👋 NATS server: " + hf.NatsGetServer())
}

//export OnExit
func OnExit() {
  hf.Log("👋🤗 Bye! Have a nice day")
}
Enter fullscreen mode Exit fullscreen mode
  • 1️⃣ the message is the first item of the array params.
  • 2️⃣ the subscriber reply with result to the publisher (on the same subject), 10 is a time out in seconds.

Build the module

Run the following command:

tinygo build -o calc.wasm -scheduler=none -target wasi ./calc.go
Enter fullscreen mode Exit fullscreen mode

Serve the module

Run the following command:

capsule \
   -wasm=./calc.wasm \
   -mode=nats \
   -natssrv=localhost:4222 \
   -subject=faas
Enter fullscreen mode Exit fullscreen mode

Capsule will load the wasm module and become a NATS subscriber, listening on faas subject. At every message on faas, Capsule will create an instance of the module and call the function.

Let's write the Node.js NATS publisher

Install the NATS dependency

In a directory create a package.json file with the below content:

{
  "dependencies": {
      "nats": "^2.8.0"
  },
  "type": "module"
}
Enter fullscreen mode Exit fullscreen mode

Then install the dependency by typing:

npm install
Enter fullscreen mode Exit fullscreen mode

The source code of the publisher

Create a calc.js file with this content:

import { connect, StringCodec } from "nats";

const nc = await connect({ servers: ["localhost:4222"] });

// create a codec
const sc = StringCodec();

// subject/topic
const subject = "faas";

const addition = {
  operation: "+",
  operand1: 30,
  operand2: 12
};

const subtraction = {
  operation: "-",
  operand1: 50,
  operand2: 8
};

const multiplication = {
  operation: "*",
  operand1: 21,
  operand2: 2
};

const division = {
  operation: "/",
  operand1: 84,
  operand2: 2
};

async function wasmCalc(operation) {
  let jsonString = JSON.stringify(operation)

  await nc.request(subject, sc.encode(jsonString), { timeout: 1000 }) // 1️⃣
    .then((message) => {
      let result = sc.decode(message.data)
      console.log("🟢 Result:", result);
    })
    .catch((err) => {
      console.log("🔴 Error:", err.message);
    });

}

await wasmCalc(addition)
await wasmCalc(subtraction)
await wasmCalc(multiplication)
await wasmCalc(division)

await nc.close();
Enter fullscreen mode Exit fullscreen mode
  • 1️⃣ The client makes a request and receives a promise for a message. The request time out is of 1000 millis.

Run it

Run node calc.js, you should get:

🟢 Result: {"result": 42}
🟢 Result: {"result": 42}
🟢 Result: {"result": 42}
🟢 Result: {"result": 42}
Enter fullscreen mode Exit fullscreen mode

As you see, it's pretty simple, and you can easily run other Capsule subscriber processes to add new functions to your network.

Last but not least: create a wasm NATS publisher

You can use a Capsule module as a publisher:

nats-publisher.go

package main

import (
  hf "github.com/bots-garden/capsule/capsulemodule/hostfunctions"
)

func main() {
  hf.SetHandle(Handle)
}

func Handle(params []string) (string, error) {

  natsSrv := "localhost:4222"
  subject := "faas"
  operation := `{"operation":"+", "operand1":40, "operand2":2}`

  result, err := hf.NatsConnectRequest(natsSrv, subject, operation, 1)

  return result, err
}
Enter fullscreen mode Exit fullscreen mode

Build it:

tinygo build -o nats-publisher.wasm -scheduler=none \
-target wasi ./nats-publisher.go
Enter fullscreen mode Exit fullscreen mode

Run it:

capsule \
   -wasm=./nats-publisher.wasm \
   -mode=cli
Enter fullscreen mode Exit fullscreen mode

This time, we run Capsule with the CLI mode to execute the wasm module (like with Wasmer, Wasmtime, Wasmedge, ...)

You should get something like this:

{"result": 42}
Enter fullscreen mode Exit fullscreen mode

That's all for today! Happy Wasi 🎉

You can find all the source code examples here: https://github.com/bots-garden/capsule-hello-universe/tree/main/nats-faas

If you use Gitpod, the project is "Gitpod compliant", so you will get of the necessary tools (Golang, TinyGo, NATS server, ...); otherwise, it should work with Dev Container.

Top comments (0)