This chapter will highlight some best practices and tips for adequately leveraging the LivePerson Functions platform.

Prefer Async/Await over Promises/Callbacks

Most of our APIs are based around Promises and therefore can be easily consumed in an Async-Await fashion. Leveraging Async-Await over Promises/Callbacks eases the error handling by allowing try-catch blocks. Furthermore, when using Promise-Chains, you can easily forget the .catch statement and only handle the happy path with .then resulting in unhandled promise rejections, without any logging insights, which usually manifests as a timeout error (as the callback never gets called). Additionally, it keeps the code shallow, avoiding deep nesting. This benefits the overall maintainability of functions.

For this reason, we advise strongly you to prefer Async/Await over Promises. That's also why most of our templates are specially designed to leverage Async-Await. However, you will still need to utilize the callback passed into the function to return a response for an invocation. As demonstrated in the following example code:

const { Toolbelt } = require("lp-faas-toolbelt");
let cert = key = undefined;

async function lambda(input, callback) {
    try {
        const [clientCert, clientKey] = await lazyLoadClientBundle();
        const client = Toolbelt.MTLSClient({cert: clientCert, key: clientKey});
        const {statusCode, body} = await client.get('https://certauth.idrix.fr/json');
        console.info('Status Code', statusCode);
        console.info('Body', body)
        return callback(null, body);
    } catch(err) {
        console.error('Warn', err.message);
        console.error('Stack', err.stack);
        console.error('Name', err.name);
        return callback(null, `MTLS Failed`);
    }
}

async function lazyLoadClientBundle() {
    if (cert && key) {
        return [cert, key];
    }

    const client = Toolbelt.SecretClient();

    if (cert === undefined) {
        const {value} = await client.readSecret('cert');
        cert = value;
    }

    if (key === undefined) {
        const {value} = await client.readSecret('key');
        key = value;
    }

    return [cert, key];
}

}

The code above will change lazy load two secrets, leveraging a cache, to call an MTLS Endpoint. You can see proper error handling of errors returned by client.get and Toolbelt.MTLSClient. Finally, the provided callback is called with a response or a failure, based on the occurrence of any error in the process.

Secret Caching

Why secret caching can be critical? Secret caching will reduce your function execution time since the number of requests to the secrets store will be drastically decreased and will prevent failed invocations in case of an outage.

Our Secret store allows functions to fetch, store and modify credentials securely. We strongly encourage caching the secret, loading it once, and then reusing it throughout the Lifecycle of a Function instance in productive use. Please see the code below for an example setup, which leverages local variables to achieve this. Be aware that this is only possible if the secrets are not modified and therefore are more or less static. You can adjust the caching to reuse the token for secrets like tokens while it is still valid and not yet expired.

Only store credentials in the secret store, other parameters like URLs or other configuration data are best placed as environment variables.

const { Toolbelt } = require("lp-faas-toolbelt");
// It's important that this variable is out of any function scope
let secretCache = {};

async function lambda(input, callback) {
  try {
    const secret = await lazyLoadSecret('name');
    // Use secret from here on
  } catch(error) {
    // Error Handling
  }

}

async function lazyLoadSecret(name) {
    if (secretCache[name] !== undefined) {
        return secretCache[name];
    }

    const client = Toolbelt.SecretClient();
    const { value } = await client.readSecret(name);

    secretCache[name] = value;
    return value;
}
}

The code above will check if the value already exists in the secretCache and will read it from the secret store if that's not the case. secretCache must be outside of any function scope to ensure it does not get cleaned up.

Please look at how to cache inside functions complete example.

LpClient declaration

Declare the LpClient globally before the function execution. The LpClient internally caches the required API key to call the LP APIs. If the LpClient is not declared in the global scope before the function execution, it will create a new LpCLient object for each invocation; the internal cache won't work, making invocations slower, and the secret store will be called for each invocation. You can see an example below:

const { Toolbelt, LpServices } = require('lp-faas-toolbelt');
const lpClient = Toolbelt.LpClient();

async function lambda(input, callback) {
    // lpClient usage example 
    const response = await lpClient(
                LpServices.LE_DATA_REPORTING,
                `/operations/api/account/${process.env.BRAND_ID}/msgqueuehealth/current/?skillIds=${skillId}&v=1`,
                options
        );

    // Your code ...
}

Configuration of Functions using Environment Variables

We strongly recommend the usage of environment variables to make functions easier to configure, even for less tech-savvy people. They do not need to understand the code but only adjust environment variables if necessary. Using environment variables improves the readability of the code by moving the configuration part into them. If you want to learn more about the environment variables setup, head over here.

Environment variables changes need a deployment to take effect. Environment variables are always a string, so you need to parse any non-string type before using it. Be aware of the POSIX1.-2017-compliant naming schema, environment variable names must not start with a digit and may consist solely of uppercase letters, digits, and the ( '_' ).

Design your function to be idempotent

As documented in our Event Source Page, some of the Event Sources will actively react to errors raised by your function and re-invoke your function with the same event/payload following at least one approach. Usually, functions will be triggered only once, but they can also be invoked multiple times. Therefore you should design your function so that a retry will not have harmful side effects or cause unexpected behaviour on your side.

How this is achieved is highly linked to your actual use case and might involve additional API calls to verify the state before acting. You can also leverage the Context Session Store to save information that allows you to determine if a process was already performed or not.

Avoid (CPU) blocking code

As our functions are based on Node.js, they leverage only one thread for execution, as this is how Node.js was designed. There is also official documentation available from Node.js that highlights blocking code/libraries.

If you block the CPU or the Event Loop, this can cause the ongoing request to be halted, resulting in a delay. This can increase the overall response time and, worst-case, exceed the execution timeout yielding an error. Especially cryptographic operations fall into this area.