Context (ctx)
Lolo Applications and Functions
Context (ctx)
This part defines how you work with the context (ctx) within the lifecycle methods.
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. | Go to strings |
functionName | setup / handler | The Function Name as a string. | Go to strings |
appId | setup / handler | The Application ID as a string. | Go to strings |
appName | setup / handler | The Name of the Application as a string. | Go to strings |
inputs | setup / handler | Access all Function Inputs that are available in the Function. Access as a JSON object. | Go to inputs/input |
outputs | setup / handler | Access all Function Outputs that are available in the Function. Access as a JSON object. | Go to outputs |
params | setup / handler | Extract the variables from the set Schema by the Library Functions. | Go to params |
log | setup / handler | Log to the Logs console using this method | Go to logs |
events | setup / handler | Standard NodeJS event emitter used to register engine lifecycle events. You can register listeners events.on('pause') and events.on('resume') that will be triggered accordingly by the engine. | Go to events |
env | setup / handler | The environment context. Here you can access your Application variables. | Go to env |
Input | handler | The input that has data flowing into it. | Go to inputs/input |
state | handler | Shared key / value state across the Lolo Application throughout the Application's lifecycle. Access them with get('key') and set('key', value). | Go to state key/value store |
route(ev, output) | handler | Reroute data to the next node via route(ev, output). | Go to route |
once(signal, callback, timeoutMs = 30 * 1000) | handler | Once is used defer the execution of a piece of code until a specific signal is observed. | Go to once |
emit(signal, ...args) | handler | Emit is the signal that once is waiting for. Use with optional data arguments. | Go to emit |
addHelper(name, callback) | setup | Extend the handler context by registering a helper function that will be available to all functions in the current application. | Go to addHelper |
httpServer | setup | Implements a server which handles HTTPS requests. Primarily used as httpServer.addRoute or httpServer.ws.addRoute | Go to httpServer |
produceEvent | setup | Use the produceEvent attribute to fire off events within the Application. | Go to produceEvent |
Strings
Use these directly in your application. AppId may be interesting if you are setting up your own http server.
// accessible in setup
exports.setup = async(ctx) => {
const { functionId, functionName, appId, appName } = ctx;
// use directly as strings i.e.
log.info("show when app starts", appId);
}
// accessible in handler
exports.handler = async(ev, ctx) => {
const { functionId, functionName, appId, appName } = ctx;
// use directly as strings i.e.
log.info("show when triggered", appId);
}
Input / Inputs
Inputs gives you an object with all inputs ports of the Function, whereas Input gives you the port that has data flowing into it when triggered by another node.
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;
}
}
Input ports are a relevant when working with ports. If you haven't already, read up on ports first.
Outputs
To route different data to different ports you can use the outputs attribute by extracting it from the context (ctx) and use a switch statement.
Here we are extracting the HTTP request method on the event data to see which port we want the data to route through.
exports.handler = async(ev, ctx) => {
const { route, outputs } = ctx;
let output;
// depending on the method used we want to route data to different ports
switch(ev.method) {
case 'GET':
output = outputs.R;
break;
case 'POST':
output = outputs.C;
break;
case 'PUT':
output = outputs.U;
break;
case 'DELETE':
output = outputs.D;
break;
case 'PATCH':
output = outputs.P;
break;
default:
throw new Error('unsupported request type', ev);
}
route(ev, output);
};
Output ports are a relevant when working with ports. If you haven't already, read up on ports first.
Params
Relevant for Library Functions. Extract the variables from the set Schema by the Library Functions. Read more about Library Functions, setting Schema and using params here.
To use params in your Functions simply extract it from the context (ctx).
// accessible in setup
exports.setup = async(ctx) => {
const { params, log } = ctx;
// if the schema is set with a fieldName property extract it like so
log.info("print when app starts", params.fieldName);
};
// accessible in handler
exports.handler = async(ev, ctx) => {
const { params, log } = ctx;
// if the schema is set with a fieldName property extract it like so
log.info("print when triggered", params.fieldName);
};
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
Use this method to log to the console. The most common use is the log.info().
// accessible in setup
exports.setup = async(ctx) => {
const { log } = ctx;
// Log to the Logs console when application starts
log.info("Print this on App start");
};
// accessible in handler
exports.handler = async(ev, ctx) => {
const { log } = ctx;
// Log to the Logs console when function is triggered
log.info("Print me when triggered");
};
Read more about logs here.
Events
Standard NodeJS event emitter used to register engine lifecycle events in Lolo. You may use this to register and trigger your own events within your Lolo Application although using emit and once would be more efficient.
You can register listeners events.on('pause') and events.on('resume') that will be triggered accordingly by the lolo-engine.
// accessible in setup
exports.setup = async ctx => {
const { events } = ctx;
events.on('resume', () => {
// continue process
});
events.on('pause', () => {
// kill process
});
};
// accessible in handler
exports.handler = async (ev, ctx) => {
const { events } = ctx;
events.on('resume', () => {
// continue process
});
events.on('pause', () => {
// kill process
});
};
See example use with produceEvent.
Env
The environment context. Here you can access your static Application variables that you've set in your Variables tab in the Application. To access these variables in your setup or handler use ctx.env like the example below.
// accessible in setup
exports.setup = async ctx => {
const { MONGODB_COLLECTION, MONGODB_TABLE } = ctx.env;
};
// accessible in handler
exports.handler = async (ev, ctx) => {
const { MONGODB_COLLECTION, MONGODB_TABLE } = ctx.env;
};
To use in the Parameter's tab within the settings of a Function, simply use {env.MONGODB_COLLECTION}.
See example use here.
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.
Route
The route method is important when working with output ports. If you haven't already, read up on ports here first.
To route data via the route(ev, port) method just pass in the data as the first argument and then the output port that it should be routed to as the second argument.
- The output argument is optional if the function has a single output.
- Calling route in a function without outputs is a noop.
- If output argument points to non-existing port, there is a warning logged, but no exception is thrown.
- If output is connected to one input, the event is passed by reference.
- if output is connected to multiple inputs, each branch will get a deep-copy of the event.
To access a specific output port within the handler in 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 'myNewOut'
route(ev, 'myNewOut');
}
Once
Once and emit is an event-local pub / sub mechanism that allows communication between functions on the same event-path using signals. That is, you can set up a once method to register a listener that will be triggered when the emit method is used within the Application. This allows you to defer the execution of a piece of code until a specific signal is observed from emit. Third argument starts a setTimeout to automatically remove the listener.
See example use below, where we register a listener called 'myEvent.' This piece of code won't get executed until an emit with the same key has been called.
exports.handler = async(ev, ctx) => {
const { route, once, log } = ctx;
// register event listener using once
once('myEvent', (a) => {
if(a) log.info("I've been triggered");
}, 2 * 60 * 1000);
route(ev);
};
You can set up your own event listeners with once but Lolo is using once('response') in the HTTP trigger to defer sending back a response until emit has been called. See more information below under emit.
Emit
Emit is used to trigger an event registered with once so a piece of code can be executed only after it has been asked to do so.
See the code below that will execute a once method with the key 'myEvent' demonstrated in the previous code block. I.e. it will log "I've been triggered" to the console.
exports.handler = async(ev, ctx) => {
const { emit } = ctx;
emit('myEvent', true);
};
Look at the quick clip below that is using once within the first Function and emit in the second Function and is triggered by an HTTP call using Postman.
In Lolo we have a once('response') method setup for the HTTP trigger, that is why you call emit('response', body) when you want to send back a response to the HTTP call within a node. See the Hello World Application that will demonstrate this.
AddHelper
To limit the need to rewrite code you may extend the context (ctx) with helper functions that will be accessible throughout your Application.
To create a function that you can use add the addHelper attribute in the context (ctx) within the setup method.
exports.setup = async ctx => {
const { addHelper } = ctx;
addHelper('myFunction', () => (foo) => {
return "hey " + foo;
// do something even more clever with foo than just return a string...
});
};
Nested functions!
Note that the function passed to addHelper returns a function, which will be accessible on the context by the supplied name.
The function 'myFunction' can now be accessed by all event handlers anywhere in the Application.
exports.handler = async(ev, ctx) => {
const { myFunction, log } = ctx;
// Log the contents on the returned string
log.info(myFunction("some name"));
};
When you try to extract the 'myFunction' from the ctx in your handlers, you may encounter a syntax error which you can ignore. This happens as the editor doesn't recognise the property as it hasn't been set by us. The Application should run fine.
See us work something simple with the addHelper below.
ProduceEvent
An event may be a timer or an HTTP call as you've seen us use in the previous sections. We have already shared a few with you that you can find in your Function's Gallery. To create a new event trigger you can extract the produceEvent attribute from the context (ctx) within an inline Function.
In its simplest use, all you need is to call produceEvent from within the setup method like the example below.
// Example code to produce one event when the Application starts
// and route the event to the default out port
exports.setup = async ctx => {
const { produceEvent } = ctx;
produceEvent({});
};
exports.handler = async (ev, ctx) => {
const { route } = ctx;
route(ev);
};
When produceEvent is called, an event will be produced that will trigger the functions own handler, which in turn can route it as appropriate.
As the produceEvent attribute is used within the setup method, this code would only produce one event when the Application starts. If you need the event to fire on a schedule you would need to wrap it in an setInterval or use a cron schedule.
To also make sure the app will exit when requested by the engine, we use engine lifecycle events to make sure we clear the interval on 'pause.' The result could otherwise be slower redeploy times because K8s needs to force-kill the pod after a a timeout.
// Best practice to produce an event every 5 seconds with setInterval
exports.setup = async ctx => {
const { produceEvent, events } = ctx;
let interval;
events.on('resume', () => {
interval = setInterval(() => produceEvent({}), 5000);
});
events.on('pause', () => {
clearInterval(interval);
});
};
exports.handler = async (ev, ctx) => {
const { route } = ctx;
route(ev);
};
HttpServer
The HTTP and Websocket Triggers are the most common Library Functions used in Lolo. In order to implement a web server of your own use the httpServer extracted from the context (ctx) within the setup. Register the request handler on a particular HTTP method and path like we are demonstrating below.
httpServer.addRoute(actionMethods[action], path,
async (req, res) => // do something
);
// websocket server
httpServer.ws.addRoute(path,
async(ws, req) => // do something
);
If you want to see what this would look in practice, the code below will create a POST request for https://dev.lolo.company/:appId, trigger an event and respond with a 200 status code and a 'hello world!'
exports.setup = async ctx => {
const { httpServer, produceEvent, appId} = ctx;
const url = "/" + appId;
// listen for a post request for the url set with appId
httpServer.addRoute('post', url, async(req, res) => {
await produceEvent({ req, res });
});
}
exports.handler = async(ev, ctx) => {
const { req, res } = ev;
// send a response
res.status(200);
res.json({ response: 'hello world!'});
};
You probably want to defer sending a response back in the handler, to do this look into once/emit.
Updated about 1 year ago