JavaScript Functions

This topic provides an overview of how to write JavaScript functions for Pivotal Function Service (PFS). For more in-depth coverage see the riff node-function-invoker on GitHub.

Requirements

Build a JavaScript Function from GitHub

The node runtime is detected when there is a package.json file in the root, or the artifact is specified as a .js file. The package.json may be used to specify additional dependent packages to install.

This example uses the sample hello.js function from GitHub. It consists of a single JavaScript file named hello.js with the following code:

module.exports = x => `hello ${x}`

Create a hello function by running the CLI command below:

pfs function create hello \
--git-repo https://github.com/projectriff-samples/hello.js \
--artifact hello.js \
--tail
Created function "hello"
...
...[build-step-build]: -----> Node Engine Buildpack 0.0.16
...
Exporting layer 'io.projectriff.node:riff-invoker-node' with SHA sha256:52e564b0d41296f736bbfef58c8ef3098266ea1e5971d22b7c17a2f17781a60b
...[build-step-export]: *** Images:
...[build-step-export]:       registry.pfs.svc.cluster.local:5000/testuser/hello - succeeded
...[build-step-export]:
...[build-step-export]: *** Digest: sha256:1c51fd142d71e9c5734824f97e40d822e4025be384d9d3e6131ed0ff6644f3ed

Build a JavaScript function from local source

Alternatively, to build a function from a local directory, use --local-path instead of --git-repo.

E.g. if you are in a directory with a file called hello.js just like the one above:

pfs function create hello \
--local-path . \
--artifact hello.js \
--tail

Deploying a function

Please see the runtime documentation for how to deploy and invoke the function.

Cleanup

When done with the function, delete the function resource to stop creating new builds.

Images built by the function continue to exist in the container registry and may continue to be consumed by a runtime.

riff function delete square
Deleted function "square"

How the Node Invoker Works

The Node Function Invoker provides a host for functions consisting of a single Node.js module. It accepts HTTP requests, invokes the function for each request, and sends the function’s output to the HTTP response.

At runtime, the node function invoker will require() the target function module. This module must export the function to invoke.

// square
module.exports = x => x ** 2;

The first argument is the triggering message’s payload and the returned value is the resulting message’s payload.

Async

Asynchronous work can be completed by defining either an async function or by returning a Promise.

// async
module.exports = async x => x ** 2;

// promise
module.exports = x => Promise.resolve(x ** 2);

Streams (Experimental)

Streaming functions can be created by setting the $interactionModel property on the function to node-streams. The function is invoked with two arguments: an input Readable Stream and an output Writeable Stream. Both streams are object streams. Any value returned by the function is ignored, and new messages must be written to the output stream.

// echo.js
module.exports = (input, output) => {
  input.pipe(output);
};
module.exports.$interactionModel = "node-streams";

Any npm package that works with Node Streams can be used.

// upperCase.js
const miss = require("mississippi");

const upperCaser = miss.through.obj((chunk, enc, cb) => {
  cb(null, chunk.toUpperCase());
});

module.exports = (input, output) => {
  input.pipe(upperCaser).pipe(output);
};
module.exports.$interactionModel = "node-streams";

The Content-Type for output messages can be set with the $defaultContentType property. By default, text/plain is used. For request-reply function, the Accept header is used, but there is no Accept header in a stream.

// greeter.js
const miss = require("mississippi");

const greeter = miss.through.obj((chunk, enc, cb) => {
  cb(null, {
    greeting: `Hello ${chunk}!`
  });
});

module.exports = (input, output) => {
  input.pipe(greeter).pipe(output);
};
module.exports.$interactionModel = "node-streams";
module.exports.$defaultContentType = "application/json";

Messages Versus Payloads

By default, functions accept and produce payloads. Functions that need to interact with headers can instead opt to receive and/or produce messages. A message is an object that contains both headers and a payload. Message headers are a map with case-insensitive keys and multiple string values.

Since JavaScript and Node have no built-in type for messages or headers, PFS uses the @projectriff/message npm module. To use messages, functions install the @projectriff/message package:

npm install --save @projectriff/message

Receiving Messages

const { Message } = require('@projectriff/message');

// a function that accepts a message, which is an instance of Message
module.exports = message => {
    const authorization = message.headers.getValue('Authorization');
    ...
};

// tell the invoker the function wants to receive messages
module.exports.$argumentType = 'message';

// tell the invoker to produce this particular type of message
Message.install();

Producing Messages

const { Message } = require("@projectriff/message");

const instanceId = Math.round(Math.random() * 10000);
let invocationCount = 0;

// a function that produces a Message
module.exports = name => {
  return Message.builder()
    .addHeader("X-PFS-Instance", instanceId)
    .addHeader("X-PFS-Count", invocationCount++)
    .payload(`Hello ${name}!`)
    .build();
};

// even if the function receives payloads, it can still produce a message
module.exports.$argumentType = "payload";

Lifecycle

Functions that communicate with external services, like a database, can use the $init and $destroy lifecycle hooks on the function. These methods are invoked once per function invoker instance, whereas the target function may be invoked multiple times within a single function invoker instance.

The $init method is guaranteed to finish before the main function is invoked. The $destroy method is guaranteed to be invoked after all of the main functions are finished.

let client;

// function
module.exports = async ({ key, amount }) => {
  return await client.incrby(key, amount);
};

// setup
module.exports.$init = async () => {
  const Redis = require("redis-promise");
  client = new Redis();
  await client.connect();
};

// cleanup
module.exports.$destroy = async () => {
  await client.quit();
};

The lifecycle methods are optional and should only be implemented when needed. The hooks may be either traditional or async functions. Lifecycle functions have up to 10 seconds to complete their work or the function invoker aborts.