1

I'm trying to do something that involves a global context that knows about what function is running at the moment.

This is easy with single-threaded synchronous functions, they start off running and finish running when they return.

But async functions can pop to the bottom of the program and climb back up multiple times before completing.

let currentlyRunningStack: string[] = [];
function run(id: string, cb: () => any) {
  currentlyRunningStack.push(id);
  cb()
  currentlyRunningStack.shift();
}

// works with synchronous stuff
run("foo", () => {
  run("bar", () => console.log("bar should be running"));
  console.log("now foo is running");
});

// can it work with asynchronous
run("qux", async () => {
  // async functions never run immediately...
  await somePromise();
  // and they start and stop a lot
});

Is it possible to keep track of whether or not an asynchronous function is currently running or currently waiting on something?


EDIT: there appears to be something similar called Zone.js. Used by Angular I guess.


EDIT: per @Bergi's suggestion, the word "stack" has been updated to "program" for clarification

Seph Reed
  • 8,797
  • 11
  • 60
  • 125
  • 3
    This seems like a really, really bad idea.. – Qix - MONICA WAS MISTREATED May 27 '22 at 00:41
  • Would you mind elaborating? – Seph Reed May 27 '22 at 00:44
  • 1
    "*pop to the bottom of the stack and climb back up*" - no, an async function that has been suspending is not in the stack at all. – Bergi May 27 '22 at 00:59
  • You seem to be looking for [async hooks](https://nodejs.org/api/async_hooks.html) – Bergi May 27 '22 at 00:59
  • 3
    What exactly is your goal here? – Bergi May 27 '22 at 01:00
  • @Bergi when it's done being suspended, does it automatically jump to the top of the stack? I've seen async hooks in node a long time ago. Thanks for giving me the keyword again. There's nothing like this front-end though, I presume. – Seph Reed May 27 '22 at 01:13
  • @SephReed When the promise is settled, a job to resume the async function is scheduled on the promise job queue. This resumption will happen only when the stack is empty (so the async function will become bottom and top of the stack at the same time). – Bergi May 27 '22 at 01:18
  • Ah thanks for the clarification. I suppose I need a new keyword here. For a single threaded program, what would you call the order of execution contexts as it exists (stacks and jobs included)? I've always called it the stack, but I do recognize that's a misnomer. – Seph Reed May 27 '22 at 01:26
  • The job queues and the stack would be part of the event loop. If you want to include "stacks" of suspended (generator) functions, I dunno, maybe just call it "the program"? – Bergi May 27 '22 at 01:37
  • Re one of the code comments, `async` functions run synchronously when called up until the first `await` operator. Calling code is only resumed after `await` has saved its execution content and restored the previous running execution context, or the `async` function returns without ever using `await`. – traktor May 27 '22 at 01:51
  • 3
    maybe you can show more ways you intend to interact with these async tasks? i am smelling an anti-pattern. – Mulan May 27 '22 at 05:14
  • I'm testing something out, for the sake of exploration and learning. It won't be put into anything production level without first testing and proving sufficient value as compared to complexity it may introduce. Don't worry, my sense of code smell is fine. – Seph Reed May 31 '22 at 14:54

2 Answers2

0

It is possible, and someone has done it -- the angular developers, no less -- but it costs a whopping 5.41Mb!!

https://www.npmjs.com/package/zone.js

This was ripped from this very similar question: Something like async_hooks for the browser?

In order to distinguish from that question a bit, I'll answer the core query here:

Yes, you can tell when an async function stops and starts. You can see a lot of what's required in the project code

In particular, this file appears to handle poly-filling promises, though I'd need more time to verify this is where the magic happens. Perhaps with some effort I can distill this into something simpler to understand, that doesn't require 5.41 Mb to acheive.

Seph Reed
  • 8,797
  • 11
  • 60
  • 125
-3

Yes, it possible.

Using a running context, like a mutex, provided by Edgar W. Djiskistra, stack queues, Promise states and Promise.all executor. This way you can keep track if there's a running function in the program. You will have to implement a garbage collector, keeping the mutex list clean and will need a timer(setTimeout) to verify if the context is clean. When the context is clean, you will call a callback-like function to end you program, like process.exit(0). By context we refeer to the entire program order of execution.

Transforming the function into a promise with an .then callback to pop/clean the stack of the mutex after the execution of content of the function with a try/catch block to throw, handle, or log errors add more control to the hole program.

The introduction of setTimeout propicies a state machine, combined with the mutex/lock and introduces a memory leak that you will need to keep track of the timer to clear the memory allocated by each function.

This is done by neste try/catch. The use of setInterval for it introduces a memory leak that will cause a buffer overflow.

The timer will do the end of the program and there's it. You can keep track if a function is running or not and have every function registered running in a syncrhonous manner using await with and mutex.

Running the program/interpreter in a syncrhonous way avoid the memory leaks and race conditions, and work well. Some code example below.


const async run (fn) => {
  const functionContextExecutionStackLength = functionExecutionStackLength + 1
  const checkMutexStackQueue = () => {
    if (mutexStack[0] instanceof Promise) {
      if (mutex[0].state == "fullfiled") {
        mutexStack = mutexStack.split(1, mutexStack.length)
        runner.clear()
        runner()
      }
    }

    if (mutexStack.length == 0) process.exit(0)
  }

  // clear function Exection Context
  const stackCleaner = setTimeout(1000, (err, callback) => {
    if (functionContextExecutionStackLength == 10) {
      runner.clear()
    }
  })

  stackCleaner = stackCleaner()

  // avoid memory leak on function execution context
  if (functionContextExecutionStackLength == 10) {
    stackCleaner.clear()
    stackCleaner()
  }

  // the runner
  const runner = setTimeout(1, async (err, callback) => {
    // run syncronous 
    const append = (fn) => mutex.append(util.promisfy(fn)
      .then(appendFunctionExectionContextTimes)
      .then(checkMutexStackQueue))

    // tranaform into promise with callback
    const fn1 = () => util.promify(fn)
    const fn2 = () => util.promisfy(append)

    const orderOfExecution = [fn1, fn2, fn]

    // a for await version can be considered
    for (let i = 0; i < orderOfExecution.length; i++) {
      if (orderOfExecution.length == index) {
        orderOfExecution[orderOfExecution.length]()
      } else {
        try {
          orderOfExecution[i]()
        } catch (err) {
          throw err
          console.error(err)
        }
      }
    }
  }
}

(() => run())(fn)

On the code above we take the assynchronous caracterisc of javascript very seriously. Avoiding it when necessary and using it when is needed.


Obs:

  • Some variables was ommited but this is a demo code.
  • Sometimes you will see a variables context switching and call before execution, this is due to the es modules characteriscts of reading it all and interpreting it later.
lukaswilkeer
  • 305
  • 4
  • 13