Tutorial

JS Task Executor in browser Quickstart explained

Introduction

In this tutorial, you will create a simple web page that will trigger your requestor script and display the results and output logs in the browser window.

In the Quickstart, the js script is in an external file. In this tutorial we will keep both HTML and js script in the same file.

Prerequisites

Before proceeding, you'll need to install and launch the Yagna service, version 0.15.2 or later. Installation instructions can be found through the manual Yagna installation guide available here.

In addition, you need to start Yagna with a parameter that allows you to handle REST API requests with a CORS policy. You can do this by running the following command:

yagna service run --api-allow-origin='http://localhost:8080'
warning

The --api-allow-origin value should be set to the URL where your web application will be served. In this example, we will use http-server.

Setting up the project

mkdir web_golem
cd web_golem

next

npm install --global http-server

This will install the http-server utility to host our web page, where we will run our Golem app.

HTML page

Next, we'll create the main index.html file with a minimal layout:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>WebRequestor Task API</title>
  </head>
  <body>
    <h1>WebRequestor - Hello World</h1>
    <div class="container">
      <div class="col-6">
        <h3>Options</h3>
        <div class="column">
          <div>
            <label for="YAGNA_API_BASEPATH">Yagna Api BaseUrl: </label>
            <input
              id="YAGNA_API_BASEPATH"
              type="text"
              value="http://127.0.0.1:7465"
            />
          </div>
          <div>
            <label for="SUBNET_TAG">Subnet Tag: </label>
            <input id="SUBNET_TAG" type="text" value="public" />
          </div>
        </div>
        <h3>Actions</h3>
        <div class="row vertical">
          <div>
            <button id="echo">Echo Hello World</button>
          </div>
        </div>
        <div class="results console">
          <h3>Results</h3>
          <ul id="results"></ul>
        </div>
      </div>
      <div class="col-6 border-left">
        <div class="logs console">
          <h3>Logs</h3>
          <ul id="logs"></ul>
        </div>
      </div>
    </div>

    <script type="module">
      // replace with script code
    </script>
  </body>
</html>

In this layout, there are three elements:

  • A "Echo Hello World" button, which executes the script on Golem
  • A "Results" container, which displays the results
  • A "Logs" container, which displays the API logs

Take note of the <script> tag in the <head> section; this is where we'll place our JavaScript code.

Using the @golem-sdk/task-executor bundle library

First, we will import the @golem-sdk/task-executor library:

<script type="module">
    import { TaskExecutor } from "https://unpkg.com/@golem-sdk/task-executor";
</script>

Task Executor

When the user presses the Echo Hello World button, the run() function will be invoked. The body of this function should contain the typical sequence necessary to run TaskExecutor. We will first create it, then execute the task function, and finally, we will shutdown it.

Let's look at the create method parameters.

const executor = await TaskExecutor.create({
  logger,
  api: { key: "try_golem" },
  ...
  • logger - we use our own logger implementation, which displays the logs in an html logs container.
  • api: {key: } - a key that will give us access to yagna REST API. yagna is a service that connects us to the network. In this example, we will use api-key that was generated in the process of Yagna installation.
...
  demand: {
    workload: {
      imageTag: "golem/node:20-alpine",
    },
  },
...
  • In the demand section we define the environment needed to run our task. We do it by indicating what image is to be run on a remote node. We will use an image publicly available on the registry portal, therefore it is enough to provide a tag golem/node:20-alpine - it indicates an image based on alpine distribution and has node.js installed.

When defining the demand users can also specify other parameters like the minimal number of threads, memory, or disk size as needed.

...
    market: {
      rentHours: 0.5,
      pricing: {
        model: "linear",
        maxStartPrice: 0.5,
        maxCpuPerHourPrice: 1.0,
        maxEnvPerHourPrice: 0.5,
      },
    }

Finally, we define market parameters.

  • The rentHour defines the maximum duration of the engagements with providers before automatic termination.
  • In pricing we also precise the maximum acceptable prices using the linear price model. It will be used to select offers from providers and let you manage the costs of running your tasks.

The body of the executor.run() method is identical as in the case of the Node.js executor script: It is a task function that receives the ExeUnit object. It is designed to execute the command echo 'Hello World'. The exe.run() method returns a Promise which resolves to a result object. This object has a stdout property that holds the output of our command.

The result is passed as an input parameter of the appendResults() function that will be responsible for displaying the result on the screen.

Getting results

Now let's create the appendResults() function which will put the output of our application into the designated results container.

<script type="module">
  //
  // .. previously added import statement
  //
  export function appendResults(result) {
    const results_el = document.getElementById('results')
    const li = document.createElement('li')
    li.appendChild(document.createTextNode(result))
    results_el.appendChild(li)
  }

  //
  // .. async function run ....
  //
</script>

Getting logs

The TaskExecutor offers an optional logger parameter. It will accept an object that implements the Logger interface. The logger will utilize an appendLog function to add applicable records to the log storage area.

<script type="module">
  //
  // .. previously added code
  //
  function appendLog(msg, level = "info") {
      const logs = document.getElementById("logs");
      const div = document.createElement("div");
      div.appendChild(document.createTextNode(`[${new Date().toISOString()}] [${level}] ${msg}`));
      logs.appendChild(div);
  }

  const logger = {
      error: (msg) => appendLog(msg, "error"),
      info: (msg) => appendLog(msg, "info"),
      warn: (msg) => appendLog(msg, "warn"),
      debug: (msg) => appendLog(msg, "debug"),
      child: () => logger,
  };

  //
  // .. async function run ....
  //
</script>

Run the script

Now that we have all the necessary components defined, the code between <script> tags should look like this:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>WebRequestor Task API</title>
  </head>
  <body>
    <h1>WebRequestor - Hello World</h1>
    <div class="container">
      <div class="col-6">
        <h3>Options</h3>
        <div class="column">
          <div>
            <label for="YAGNA_API_BASEPATH">Yagna Api BaseUrl: </label>
            <input id="YAGNA_API_BASEPATH" type="text" value="http://127.0.0.1:7465" />
          </div>
          <div>
            <label for="SUBNET_TAG">Subnet Tag: </label>
            <input id="SUBNET_TAG" type="text" value="public" />
          </div>
          <div>
            <label for="PAYMENT_NETWORK">Payment network: </label>
            <input id="PAYMENT_NETWORK" type="text" value="holesky" />
          </div>
        </div>
        <h3>Actions</h3>
        <div class="row vertical">
          <div>
            <button id="echo">Echo Hello World</button>
          </div>
        </div>
        <div class="results console">
          <h3>Results</h3>
          <ul id="results"></ul>
        </div>
      </div>
      <div class="col-6 border-left">
        <div class="logs console">
          <h3>Logs</h3>
          <ul id="logs"></ul>
        </div>
      </div>
    </div>

    <script type="module">
      import { TaskExecutor } from "https://unpkg.com/@golem-sdk/task-executor";

      export function appendLog(msg) {
        const logs_el = document.getElementById("logs");
        const li = document.createElement("li");
        li.appendChild(document.createTextNode(msg));
        logs_el.appendChild(li);
      }
      export function appendResults(result) {
        const results_el = document.getElementById("results");
        const li = document.createElement("li");
        li.appendChild(document.createTextNode(result));
        results_el.appendChild(li);
      }

      const logger = {
        warn: (msg) => appendLog(`[${new Date().toISOString()}] [warn] ${msg}`),
        debug: (msg) => console.log(msg),
        error: (msg) => appendLog(`[${new Date().toISOString()}] [error] ${msg}`),
        info: (msg) => appendLog(`[${new Date().toISOString()}] [info] ${msg}`),
        child: () => logger,
      };
      async function run() {
        const executor = await TaskExecutor.create({
          logger,
          api: { key: "try_golem", url: document.getElementById("YAGNA_API_BASEPATH").value },
          demand: {
            workload: {
              imageTag: "golem/node:20-alpine",
            },
            subnetTag: document.getElementById("SUBNET_TAG").value,
          },
          market: {
            rentHours: 0.5,
            pricing: {
              model: "linear",
              maxStartPrice: 0.5,
              maxCpuPerHourPrice: 1.0,
              maxEnvPerHourPrice: 0.5,
            },
          },
          payment: { network: document.getElementById("PAYMENT_NETWORK").value },
        });
        await executor
          .run(async (exe) => appendResults((await exe.run("echo 'Hello World'")).stdout))
          .catch((e) => logger.error(e));
        await executor.shutdown();
      }
      document.getElementById("echo").onclick = run;
    </script>
  </body>
</html>

Now if we have:

  • The Yagna service is running, and it's started with the --api-allow-origin parameter correctly set to http://localhost:8080.
  • Your Yagna app-key is either set to try_golem, or the apiKey has been assigned a value of another valid 32-character key (More details here)."

Run http-server to start the webserver.

You should see the app available in the browser.

Open localhost

If you click the Echo Hello World button, after a while in the result container, we should get the result of the script, and in the log container we should see the logs of executed commands.

Output logs

Next steps

Other tutorials.

See also

Introduction to JS Task API