1

I'm trying to create a simple loading animation for an arbitrary function in JQuery Terminal. I know that JavaScript is single threaded and synchronous, but are there any workarounds?

I've seen the JQuery progress bar animation example, but I'm not sure how I can run an animation similar to that while another function runs on the background. Could I potentially use async and await functions?

Below is a simple reproducible example:

var terminal = $('#terminal').terminal( function(command, term) {
                    arbitrary_function();
                    let i = 0;
                    let loading_animation = ['[LOADING     ]', '[LOADING .   ]', '[LOADING ..  ]', '[LOADING ... ]', '[LOADING ....]']
                    (function loop() {
                        terminal.set_prompt(loading_animation[i]);
                        timer = setTimeout(loop, 550);
                        i = ((i + 1) % 5);
                    })();   
                }

Ideally, I'd want everything below arbitrary_function() to run until arbitrary_function() is done. How can I possibly do that in JavaScript?

Alex Hira
  • 41
  • 4
  • [Web Workers](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) might be a way to get around the single-threadedness. – Wyck Mar 27 '22 at 16:12

2 Answers2

0

The problem here is how do you know the progress of your arbitary_function? It would have to update the "main process" with it's status. You can run arbitrary_function async by using setTimeout

In the example below the arbitary_function executes itself in a loop via setTimeout with random delays and updates progress variable with each execution. Meanwhile showProgress function runs in a loop independently from arbitrary_function and displays data from progress variable.

const elProgress = document.querySelector(".progress-bar > .progress");

let iProgress = 0; //this will hold the progress of our function
function arbitary_function()
{
  iProgress += Math.random() * 10;
  if (iProgress < 100)
    return setTimeout(arbitary_function, Math.random() * 500);

  iProgress = 100;
}

arbitary_function();


function showProgress(timestamp)
{
  elProgress.textContent = ~~iProgress + "%";
  elProgress.style.width = iProgress + "%";
  if (iProgress < 100)
    requestAnimationFrame(showProgress);
}

showProgress();
.progress-bar
{
  border: 1px solid black;
  height: 3em;
  border-radius: 3em;
  overflow: hidden;
}
.progress-bar > .progress
{
  background-color: red;
  height: 100%;
  border-right: 1px solid gray;
  transition: width 0.5s;
  line-height: 3em;
  text-align: center;
  vertical-align: middle;
  overflow: hidden;
}
<div class="progress-bar">
  <div class="progress"></div>
</div>
vanowm
  • 9,466
  • 2
  • 21
  • 37
0

@Wyck is right you can use Web Workers for this if your arbitrary_function do long computation, here is example how to do this using Comlink library

const terminal = $('body').terminal(function() {
    const promise = arbitrary_function();
    // we don't want interactivity, but want prompt to be visible
    terminal.pause(true);
    let i = 0;
    const loading_animation = [
        '[LOADING     ]',
        '[LOADING .   ]',
        '[LOADING ..  ]',
        '[LOADING ... ]',
        '[LOADING ....]'
    ];
    let stop = false;
    const prompt = terminal.get_prompt();
    const done = new Promise(resolve => {
        (function loop() {
            terminal.set_prompt(loading_animation[i]);
            if (!stop) {
                timer = setTimeout(loop, 550);
            } else {
                // clear the animation
                terminal
                    .echo(loading_animation[i])
                    .set_prompt(prompt)
                    .resume();
                resolve();
            }
            i = ((i + 1) % 5);
        })();
    });
    promise.then(function() {
        // long calculation is done
        stop = true;
    });
    done.then(() => promise).then(x => {
        terminal.echo(x);
    });
});

function arbitrary_function() {
  return worker.longTask();
}

const worker = Comlink.wrap(fworker(function() {
  importScripts("https://cdn.jsdelivr.net/npm/comlink/dist/umd/comlink.min.js");

  Comlink.expose({
    longTask() {
      let i = 3000000000;
      while (i--) {
        Math.pow(2, 100000000);
      }
      return "any value";
    }
  });
}));

// function to worker
function fworker(fn) {
    var str = '(' + fn.toString() + ')()';
    // ref: https://stackoverflow.com/a/10372280/387194
    var URL = self.URL || self.webkitURL;
    var blob;
    try {
        blob = new Blob([str], { type: 'application/javascript' });
    } catch (e) { // Backwards-compatibility
        const BlobBuilder = self.BlobBuilder ||
              self.WebKitBlobBuilder ||
              self.MozBlobBuilder;
        blob = new BlobBuilder();
        blob.append(str);
        blob = blob.getBlob();
    }
    return new Worker(URL.createObjectURL(blob));
}
<script src="https://cdn.jsdelivr.net/npm/comlink/dist/umd/comlink.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery"></script>
<script src="https://cdn.jsdelivr.net/npm/jquery.terminal/js/jquery.terminal.min.js"></script>
<link href="https://cdn.jsdelivr.net/npm/jquery.terminal/css/jquery.terminal.css" rel="stylesheet"/>

NOTE: If your function is just async and doesn't block the main thread you can just use Promises and don't need Web Worker for that.

function arbitrary_function() {
    return new Promise(resolve => {
        setTimeout(resolve, 1000);
    });
}

Instead of setTimeout, you can use any function. The rest of the code will be the same (but without a Web Worker).

jcubic
  • 61,973
  • 54
  • 229
  • 402