Try   HackMD

Back to LOC CLI Guidebook
Setup LOC CLI Environment
LOC CLI Installation
LOC CLI Commands
Local Simple Runtime Execution

Getting Started - Hello World (TypeScript)

In this tutorial, we are going to create a simple LOC data process using TypeScript.

This data process would receive a POST request with JSON data, which contains a name field. It would return a response including this message:

{
    message: Hello, <name>!
}

Create a Data Process

See the setup guide for how to prepare your local LOC CLI environment, including getting a .yaml profile file and install Node.js. We also assume you are have renamed your LOC CLI to loc.exe.

You can also open the LOC CLI workspace directory (for example, /FST) in VS Code (File -> Open Folder).

Generate a Data Process Project

In your LOC CLI workspace root directory (FST in our example), create a new data process with

loc new hello-world

You should see a new project

FST
    hello-world-ts
        generic-logic
            1.ts
            2.ts
        aggregator-logic.ts
        api-route-config
        config.yaml
        package.json
        tsconfig.json

Then open the LOC CLI root directory (not the project directory) as workspace in your VS Code.

LOC data processes will be executed in a Deno runtime so you don't need to install TypeScript on your machine.

Install Dependencies

A LOC CLI TypeScript project requires dependency packages for type checks and deployment rolling-up.

  1. Open a new terminal and switch to the project root dir:
cd hello-world-ts
  1. Install project dependencies and type definitions (using NPM from Node.js and Git):
npm i lodash
npm i -D @types/lodash
npm i github:fstnetwork/saffron-sdk#logic-0.5.1

Since fstnetwork/saffron-sdk is currently a private Github repository, please contact FST Network for granting you access.

  1. Switch back to the LOC workspace dir:

(Windows)

cd..

(macOS/Linux)

cd -

Project Config

First look at the config.yaml in the project directory:

version: 0.1.0
name: hello-world-ts
description: description
timeoutSeconds: 180
aggregatorLogic:
  name: aggregator
  file: aggregator-logic.ts
genericLogics:
  - name: generic-1
    file: 1.ts
  - name: generic-2
    file: 2.ts

This file contains key information of the data process, including time out and the file name/order of generic logics. For now we won't change anything here, but keep in mind you can configure them as you like.

Implement Logics

Generic Logics #1

The 1st generic reads the name field from JSON data, convert it into Unicode string and store it in the data process' session storage:

import { GenericContext, RailwayError } from "@saffron/logic";
import isEmpty from "lodash/isEmpty";

/**
*
*  The codes in 'run' are executed when no error occurrs in Generic Logic.
*
*/
export async function run(ctx: GenericContext) {

  // a function that transforms byte array to string
  const UTF8ArrToStr = (aBytes: number[]): string => {
    if (isEmpty(aBytes)) {
      return "{}";
    }
    const utf8decoder = new TextDecoder();
    return utf8decoder.decode(new Uint8Array(aBytes));
  }

  // type checking data source (may be http or message queue)
  let body: number[] = [];
  if ("http" in ctx.payload) {
    body = ctx.payload.http.body;
  } else if ("messageQueue" in ctx.payload) {
    body = ctx.payload.messageQueue.data;
  } else {
    throw new Error("this logic accept http/messageQueue payload");
  }

  // read and parse JSON data from the request body
  const payload = JSON.parse(UTF8ArrToStr(body));
    
  // log payload content
  ctx.agents.logging.info({ payload: payload });

  // extract name from payload (default value is "World")
  const name: string = (payload?.name) || "World";

  // write name into session store
  await ctx.agents.sessionStorage.putString("name", name);
}

/**
*
*  The codes in 'handleError' is executed when an error occurrs in Generic Logic,
*  or the CURRENT running Logic just gets an error.
*
*/
export async function handleError(ctx: GenericContext, error: RailwayError) {
  ctx.agents.logging.error(error.message);  // log the error
}

See Agent List for reference of LOC agents.

While using TypeScript, it is necessary to check what's in the ctx.payload (using so-called type guards) to avoid type errors. It would be ctx.payload.http.body in this example, but the TypeScript compiler won't know that.

Generic Logic #2

The 2nd generic logic reads the name from session storage and send an event.

import { GenericContext, RailwayError } from "@saffron/logic";

/**
*
*  The codes in 'run' is executed when no error occurred in Generic Logic.
*
*/
export async function run(ctx: GenericContext) {

  // read name from session store
  const name = await ctx.agents.sessionStorage.get("name") as string;

  const events = [
    {
      sourceDID: "Hello_World",  // source DID
      targetDID: name,  // target DID will be user's name
      labelName: `Hello, ${name}!`,  // event label (greeting message)
      meta: "",  // meta (empty)
      type: "default",  // default group
    },
  ];

  // emit an event to event store
  await ctx.agents.eventStore.emit(events);

  // log event
  ctx.agents.logging.info(`emit event: ${JSON.stringify(events)}`);
}

/**
*
*  The codes in 'handleError' is executed when there is any error occurred
*  in Generic Logic or CURRENT running Logic just gets an error.
*
*/
export async function handleError(ctx: GenericContext, error: RailwayError) {
  ctx.agents.logging.error(error.message);  // log the error
}

Aggregator Logic

The aggregator logic also reads the name from sessoin storage and send a response to the caller.

import { AggregatorContext, RailwayError } from "@saffron/logic";

/**
*
*  The codes in 'run' are executed when no error occurrs in Aggregator Logic.
*
*/
export async function run(ctx: AggregatorContext) {

  // read name from session store
  const name = await ctx.agents.sessionStorage.get("name") as string;

  const response = {
    message: `Hello, ${name}!`
  };

  // send response to API route
  ctx.agents.result.finalize({
    status: "ok",
    taskId: ctx.task.taskId,
    response: response
  });

  // log response
  ctx.agents.logging.info({ response: response });
}

/**
*
*  The codes in 'handleError' is executed when an error occurrs in Generic Logic,
*  or the CURRENT running Logic just gets an error.
*
*/
export async function handleError(ctx: AggregatorContext, error: RailwayError) {
  ctx.agents.logging.error(error.message);  // log the error
}

Deploy and Manage the Data Process/API Route

See Getting Started - Hello World (JavaScript) for more details.

  • Login:
loc login
  • Deploy data process hello-world-ts using profile example:
loc deploy hello-world-ts -ts -p example
  • Deploy API route in api-route-config.yaml (be mindful of the project path):
loc ar deploy -f hello-world-ts/api-route-config.yaml
  • Deploy hello-world-ts with its API route at the same time:
loc deploy hello-world-ts -ts -ar

./loc deploy hello-world-ts -ts -ar replaces the API route but will still upload a new data process. You need to manually delete the older one(s) in order to release unused resources.

  • Display the list of data processes or API routes:
loc dp list
loc ar list
  • Remove a data process of API route:
loc dp delete <data process pid>
loc ar delete <api route pid>

LOC CLI Guidebook Catalogue

LOC CLI Introduction
LOC Concept
LOC CLI Commands
Getting Started - JavaScript
Getting Started - TypeScript
Reference - Agent List
Reference - Event Fields Comparison Table

tags: LOC CLI