Functions

Lolo Applications and Functions

Functions

This part defines how you create and work with inline Functions in your Application.

A new Function is created in an Application by clicking the "New Function" button in the bottom right. The Function has two tabs in the IDE.

  • Handler where you edit your inline JavaScript.
  • Settings where you set the name and define how data should flow from your function to other nodes via ports (the default ports are 'in' and 'out')
12801280

The Settings specifically enables you to to:

  • Set the name and description of the Function.
  • Delete the Function.
  • Set number of and name of input and output ports, i.e. define how data should flow into and out of your function (explained in more detail here).

Function Lifecycle Methods

We have two methods that you can use within your inline Functions, a setup method that will run when the Application starts and a handler method that will run every time the Function is triggered by an event. This means that if you use the setup method within a Function it will always run on start, regardless of which Function it is located in. If you use the handler method it will only run if the Function is triggered by an event, but will do so for every event hit.

exports.handler

The handler runs every time an event is received by the Function. This is where you have access to the event object along with the context (ctx) and were you'll write most of your code. Because of this, the handler is already set for you in every new inline Function you add.

exports.handler = async(ev, ctx) => {
};

When you add a new Function you will receive a boiler template like the code below. All this does is route the ev data to the next port so the next node can receive this information. In this Function it assumes you only have one port and thus the name of the port is not defined. More on ports you can find here.

exports.handler = async(ev, ctx) => {
  const { route } = ctx;
  
  // route to default port
  route(ev);
};

export.setup

The setup runs when the Application starts. Handy when you only need to have something run at the start of the Application's lifecycle. Such as setting up a helper function that will be accessible throughout the rest of your Application or producing your own event triggers.

exports.setup = async ctx => {
}

If you want a simple demo on what lifecycle methods look like from a Function, see the video below where we use a predefined Timer Trigger to set off events every 2 seconds and log from both the handler and the setup method. Take note of the first log that is coming from the setup method and the logs coming from the handler for every event the timer is triggering.

Context (ctx)

What you have access to in the context (ctx) in the lifecycle methods differs. See the full extent of the attributes in the context you can extract from the different lifecycle methods below.

exports.handler = async(ev, ctx) => {
  // Basecontext exists in both lifecycle methods 
  { functionId, functionName, appId, appName, inputs, outputs, params, log, events, env } = ctx;
  // Along with the basecontext attributes you also have access to 
  { input, route, state, once, emit } = ctx;
};

exports.setup = async(ctx) => {
  // Basecontext exists in both lifecycle methods 
  { functionId, functionName, appId, appName, inputs, outputs, params, log, events, env } = ctx;
  // Along with the basecontext attributes you also have access to 
  { addHelper, httpServer, produceEvent } = ctx;
};

Specifically, the context contains the following attributes and how you use them are specified below.

Key

Context

Description

functionId

setup / handler

The Function ID as a string.

functionName

setup / handler

The Function Name as a string.

appId

setup / handler

The Application ID as a string.

appName

setup / handler

The Name of the Application as a string.

inputs

setup / handler

Access all Function Inputs that are available in the Function. Access as a JSON object.

outputs

setup / handler

Access all Function Outputs that are available in the Function. Access as a JSON object.

params

setup / handler

Relevant for Library Functions. Extract the variables from the set Schema by the Library Functions. Read more about this here.

Note that variable expressions referencing event attributes will not be resolved in setup context and will still contain the template expression, such as {event.foo}

log

setup / handler

The most common use is with the log.info(). Read more about logs here.

events

setup / handler

Standard NodeJS event emitter. used to trigger lifecycle events on 'pause' and 'resume.'

env

setup / handler

The environment context. Here you can access your Application variables. See example here.

Input

handler

The input that has data flowing into it. See example with use of ports here.

state

handler

Shared key / value state across the Lolo Application throughout the Application's lifecycle. Access them with get and set. More information here.

route(ev, output)

handler

  • Route data to the next port. More information about ports here.
    • The output argument is optional if the function has a single output.
    • Calling route in a function without outputs is a noop.

once(signal, callback, timeoutMs = 30 * 1000)

handler

  • Once is used to register listeners that are triggered by emit()
    • Once is deferred until a specific signal from emit() is sent back.
    • By default, a subscriptions are automatically cancelled after 30 seconds.

emit(signal, ...args)

handler

Emit() is used to trigger the event set with once(). Optional data arguments.

addHelper(name, callback)

setup

Extend the handler context by registering a helper function that will be available to all functions in the current application.

httpServer

setup

Implements a server which handles HTTPS requests. Primarily used as httpServer.addRoute or httpServer.ws.addRoute

produceEvent

setup

Use the produceEvent attribute to fire off events within the Application.

If you are interested in checking out the contents of a few JSON objects in the Logs console don't forget to stringify them before you log them otherwise you will get an empty message output.

To see examples for every attribute in the context (ctx) go to this page.

Ports

Ports are used to control how data and information flows from one node (Function) to another when an event happens. For any Trigger you add you will usually see an 'out' port that has been set. You can drag this port to another input port in another node as you've seen previously which will route the data to that node when the event occurs.

The naming of the ports isn't important but the default ports of Functions usually have one input port 'in' and an output port 'out,' while Trigger Functions usually use an output port called 'out.'

12801280

Although you do receive the default 'in' and 'out' ports when creating a new Function, you may add and remove ports as you wish within your own Functions to specify how you want data to flow from one node to another. See a quick example below.

12801280

Working with Output Ports

To access a specific output port within the Function, pass its name to the route method.

exports.handler = async(ev, ctx) => {
  const { route, functionName } = ctx;
  
  ev.newStuffToAdd = "You've been in node " + functionName;

  // this whill change the event data for the port 'myNewPort'
  route(ev, 'myNewPort');
}

Working with Input Ports

To listen to different input ports coming in to the Function extract the input attribute from the context (ctx) and use a Switch statement.

exports.handler = async(ev, ctx) => {
  const { input, inputs, route } = ctx;

  switch (input) {
  case inputs.in:
  // do this if coming from port 'in'
  break;
  case inputs.newIn:
  // do this if coming from port 'newIn'
  break;
  }
  
}

If you need an example of using the above in an Application look at the video below, where we use two HTTP triggers to either multiply or sum two numbers based on the post request being made. We then use another inline Function to respond to the request and log the total number in the Logs.

Note that there is a slight error in the code, it should be inputs.sum rather than input.sum in the Calculate Function.

If you want to try to replicate this, make sure you send two numbers in your post request body otherwise the response will simply be null. See the example structure of your post request below.

curl https://dev.lolo.company/:appId/multiply \
  -X POST -d '{"number1":2, "number2":4}'

State Key / Value Store

Lolo's philosophy is aligned with the FaaS concept that functions should be small and do one thing. Most FaaS architectures is stateless, which means that the state must be stored externally and our experience is that this turns most FaaS development into an incomprehensible spaghetti of code, data storage, YAML configuration and performance challenges.

Lolo has a baked in key / value store in the Context called 'state' with a get() and set() methods which are accessible across the entire Application for better application performance, ease of understanding and making maintenance and evolution faster.

See the example below where a Function is using State. When the the Function is triggered it will increase the count variable by 1 and it will do so for every event it receives.

exports.handler = async(ev, ctx) => {
  const { route, state, emit, log } = ctx;

  let count = state.get('count') || 0;
  count++;
  state.set('count', count);

  const body = { response: 'hello from your counter', count };

  emit('response', { statusCode: '200', body });
};

Copy the code above and paste it into a new inline Function and have it triggered by an HTTP request. Try to access the 'count' key within the state context in another Function.

Remember to send the HTTP request too if you are using the example above. The video is showing someone triggering it three times and then logging it from both the 'Access State' Function and the 'Emit' Function. Hence the count number that is logged to the console is 3 for both Function sources in the Logs. This number will increase by 1 for every HTTP request the 'Emit' Function receives.

Use of state must be kept in mind in development and the following factors considered:
@Anders

Composite Functions

Lolo supports (and encourages) the notion of function composition whereby a number of Functions are composed together into a single function referred to as a Composite Function.

See the video below when we shift click on several nodes and the "Create Composite" button appears that will allow us to create a Composite Function. Just remember to add ports in the composite to route the data back to the parent Function and to be accessed by your other linked nodes.

The Composite Function can then be exported to be a Library Function (to be reused). To ensure a Library Function can be reused in the IDE or exposed to other frontends, a Function Schema should be added though. More on reusability and exporting inline Functions and Composite Functions here.

Exporting a Function

Read more about how you export an inline Function and about reusability here.


What’s Next

Try to create a simple Websocket Application or continue reading about how to reuse your Functions as Library Functions