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')
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.'
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.
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.
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.
Updated over 1 year ago
Try to create a simple Websocket Application or continue reading about how to reuse your Functions as Library Functions