Skip to main content
Version: LOC v0.10 (legacy)

Logic and Session

Learning Objective
  • To understand how logic are executed in a data process.
  • To pass data from one logic to another using the Session Storage agent.
  • To control the logic flow with session variables.

Logic are the core elements of a data process. They contains the implementation of a data pipeline (which may be part of a bigger one). And the session storage is the thing to link them up together during a task.

Understand Logic Functions

Each logic is, in fact, consisted with two JavaScript functions - run and handleError:

  • run() is where the normal logic code runs.
  • handleError() will only be executed if run() (or run() in any logic before this one) throws an unhandled error.
info

We will discuss more about logic error handling in Tips on Error Handling.

The source of a logic is like this:

import { ... } from '@fstnetwork/loc-logic-sdk';

export async function run(ctx) {
// main logic code
}

export async function handleError(ctx, error) {
// error handling code
}

Both run and handleError has to be exported with export keyword. SDK is imported for accessing agents and related types.

Data Process and Session

When a data process is invoked in a task, its linked logic (run() of them to be exact) will be executed by LOC runtime one by one. The logic in the same task would share one session storage, which is how logic share data between them.

The session storage would be purged once the task is done. Logic also has no access to session storages created for other tasks.

Using Session to Share Data

Logic can read and write session data using the Session Storage Agent. We will use a simple example as demostration.

Use Case: Simple Interest Calculator

Assuming you are a junior developer in a commercial bank and is tasked to write a simple interest calculator service. A user would submit a principal amount, a yearly interest rate and a time period (months), and the service would return the final amount with interest.

Assuming the input JSON payload is

payload.json
{
"principle": 10000,
"interest": 0.06,
"months": 36
}

10,000 at 6% yearly rate for 36 months

The data process should return

{
"amount": 11800
}

10,000 x (1 + (0.06 / 12) x 36) = 11,800

Code Walkthrough

Since every stage in the data process all use JSON objects, this is pretty straightforward (we assume that the input format is always correct):

LogicNamePurpose
Generic logic #1Payload JSON Parser (source)Read and parse JSON payload
Generic logic #2Simple Interest CalculatorCalculate simple interest
Aggregator logicResult Aggregator (source)Finalise task result

Two of the three logic are reused from our Quick Start tutorial. The new generic logic #2 (Simple Interest Calculator) is as follows:

Generic #2: Simple Interest Calculator
import { LoggingAgent, SessionStorageAgent } from "@fstnetwork/loc-logic-sdk";

export async function run(ctx) {
// read parsed payload from session
const parsed = await SessionStorageAgent.get("parsed");

// exit logic if required fields do not exist
if (
!("principle" in parsed) ||
!("interest" in parsed) ||
!("months" in parsed)
)
return;

// calculate simple interest amount
const amount =
parsed.principle * (1 + (parsed.interest / 12) * parsed.months);

// write the result (in a JSON object) to session
await SessionStorageAgent.putJson("result", { amount: amount });
}

export async function handleError(ctx, error) {
// error logging
LoggingAgent.error({
error: true,
errorMessage: error.message,
stack: error.stack,
taskId: ctx.task.taskKey,
});
}

In real life, you might need to do more checking, for example, if all the fields are legit numbers. Here we let handleError() handles all other possible errors.

Build a new data process with these three logic. When we invoke a data process consisted with these three logic, it should return a result like this:

{
"_status": 200,
"_metadata": {
"..."
},
"data": {
"status": "ok",
"taskKey": {
"..."
},
"data": {
"amount": 18800
}
}
}

Logic Flow Control Using Session

Session data can also be used as flags for selectly running logic or to choose between results. This will inevitable introduce more coupling between logic, but still worth it if you want a data process to be more versatile depending on situations.

Use Case: Simple/Compound Interest Dual Calculator

Continue the last example, your manager now asks you to expand the service so that it can calculate compound interest as well. The user will provide an additional field type in the payload to indicate which type of calculation they need:

{
"type": "compound",
"principle": 10000,
"interest": 0.06,
"months": 36
}
type valueSelected logic
"simple"simple interest
"compound"compound interest

So if the payload specify a "compound" type, the JSON input above should get

payload.json
{
"amount": 11966.805248234146
}

10,000 x (1 + (0.06 / 12))36 ~= 11,966.81

Code Walkthrough

In this example we'll add a new logic componend-interest-calculator into the data process, as well as modifying the original simple-interest-calculator:

LogicNamePurpose
Generic logic #1Payload JSON Parser (source)Read and parse JSON payload
Generic logic #2Simple Interest CalculatorCalculate simple interest (only run when type = "simple")
Generic logic #3Componend Interest CalculatorCalculate componend interest (only when if type = "compound")
Aggregator logicResult Aggregator (source)Finalise result

Since logic will still run one by one, both calculator logic will be executed. However we can have them to skip their job if type is not the correct one.

If type is neither "simple" or "compound", the aggregator will read a null result value since both logic would be skipped and no one will write the value into session storage.

info

The first logic (payload parser) and the aggregator logic in this section are the same as the previous version.

Generic #3: Compound Interest Calculator
import { LoggingAgent, SessionStorageAgent } from "@fstnetwork/loc-logic-sdk";

export async function run(ctx) {
// read parsed payload from session
const parsed = await SessionStorageAgent.get("parsed");

// exit logic if type != "compound"
if (parsed?.type != "compound") return;

// exit logic if required fields do not exist
if (
!("principle" in parsed) ||
!("interest" in parsed) ||
!("months" in parsed)
)
return;

// calculate simple interest amount
const amount =
parsed.principle * Math.pow(1 + parsed.interest / 12, parsed.months);

// write the result (in a JSON object) to session
await SessionStorageAgent.putJson("result", { amount: amount });
}

export async function handleError(ctx, error) {
// error logging
LoggingAgent.error({
error: true,
errorMessage: error.message,
stack: error.stack,
taskId: ctx.task.taskKey,
});
}

When requesting the data process to calculate compound interest, the result would be

{
"_status": 200,
"_metadata": {
"..."
},
"data":{
"status": "ok",
"taskKey": {
"..."
},
"data": {
"amount": 11966.805248234146
}
}
}

In order to create reusable logic to work in complex situations, you should create a set of agreements (like deciding common session variables names, expected logic behaviors and documentations) between your team. It is also a good practice to implement sufficient error handling in logic to avoid operation issues.