javascriptnode.jstypescriptasync-awaitlua

Why doesn't the statement after await continue to execute after promise resolves?


I want to implement an asynchronous task scheduling. This is my code:

let gameTick = 0;

const tasks: Record<string, Function[]> = {};

function sleep(t: number) {
    const targetTick = gameTick + t;
    if (!tasks[targetTick]) {
        tasks[targetTick] = [];
    }
    return new Promise<void>(resolve => {
        tasks[targetTick].push(resolve);
    });
}

async function task1() {
  print('start task1');
  for (let i = 0; i < 3; i++) {
    print(`execute: ${i}`);
    await sleep(2);
  }
}

// task1();

for (gameTick = 0; gameTick < 10; gameTick++) {
    print(`tick: ${gameTick}`);
    tasks[gameTick]?.forEach(f => f())
    if (gameTick == 2) task1();
}

This code behaves differently in the TypeScriptToLua environment and in the node.js environment, which confuses me.

TypeScriptToLua nodejs
tick: 0 tick: 0
tick: 1 tick: 1
tick: 2 tick: 2
start task1 start task1
execute: 0 execute: 0
tick: 3 tick: 3
tick: 4 tick: 4
execute: 1 tick: 5
tick: 5 tick: 6
tick: 6 tick: 7
execute: 2 tick: 8
tick: 7 tick: 9
tick: 8 execute: 1
tick: 9

I can understand the process of TypeScriptToLua, which is equivalent to resuming the coroutine after resolve and immediately executing the code after await. But I don't understand NodeJs. Why does it continue to execute after the loop ends, and only execute once?


Solution

  • Timing errors

    The nodejs code is working as programmed. This answer does not look into lua code output except to say it's not following the same timing rules as JavaScipt.

    The timing error arises because

    1.    for (gameTick = 0; gameTick < 10; gameTick++) {
            print(`tick: ${gameTick}`);
            tasks[gameTick]?.forEach(f => f())
            if (gameTick == 2) task1();
         }
      

    executes a synchronous loop, calling task1 with gameTick set to 2, and exiting the loop with gameTick set to 10.

    1. The first iteration of the loop in task1 calls sleep(2) which sets up a task for gameTick = 4, before await causes task1 to return a promise to the caller.

    2. The loop in step 1 continues iterating and fulfills the task promise for gameTick 4.

    3. The loop in 1 has finished, and the await operator in task1 returns to its surrounding loop code after "execute 0" has printed. The next iteration of the loop in task1 calls sleep(2) with i set to 1 and gameTick set to 10 (by completion of the loop in 1).

    4. sleep(2) sets up a promise for a task at gameTick 12. This promise is never fulfilled in posted code, leaving task1 waiting for it with execute 1 left as the last line printed.

    Short Answer

    The await doesn't return because the promise hasn't been resolved.

    Debugging

    This first snippet added some output to show the value of i and gameTick in more places and log when await returns. It was used to uncover and confirm why the await promise operand was not fulfilled.

    function print(txt) {document.querySelector('#log').textContent += txt + '\n';}
    
    let gameTick = 0;
    
    const tasks = {};
    
    function sleep(t) {
        const targetTick = gameTick + t;
        if (!tasks[targetTick]) {
            tasks[targetTick] = [];
        }
        return new Promise(resolve => {
            tasks[targetTick].push(resolve);
        });
    }
    
    async function task1() {
      print('start task1');
      for (let i = 0; i < 3; i++) {
        print(`execute: ${i}`);
        await sleep(2);
        print(` sleep await done with i = ${i}, gameTick now ${gameTick} `);
      }
    }
    
    // task1();
    
    for (gameTick = 0; gameTick < 10; gameTick++) {
        print(`tick: ${gameTick} tasks length = ${tasks[gameTick]?.length || 0}`);
        tasks[gameTick]?.forEach(f => f())
        if (gameTick == 2) task1();
    }
    
    // fix
    
    function hammer() {
        print ("\n **** hammer fix **** ");
        for (let targetTick =10; targetTick < 20; targetTick++) {
            print(`tick: ${targetTick}`);
            print(`tick ${targetTick} tasks length = ${tasks[targetTick]?.length || 0}`)
            tasks[targetTick]?.forEach(f => f())
        }
    }
    setTimeout(hammer, 2000);
    setTimeout(hammer, 4000);
    <pre><code id=log></code></pre>

    Solution

    function print(txt) {document.querySelector('#log').textContent += txt + '\n';}
    
    const tasks = {};
    
    function sleep(t) {
        const targetTick = gameTick + t;
        if (!tasks[targetTick]) {
            tasks[targetTick] = [];
        }
        return new Promise(resolve => {
            tasks[targetTick].push(resolve);
        });
    }
    
    async function task1() {
      print('start task1');
      for (let i = 0; i < 3; i++) {
        print(`execute: ${i}`);
        await sleep(2);
        print(` sleep await done with i = ${i}, gameTick now ${gameTick} `);
      }
    }
    
    
    //*****  Asynchronous clock *****
    
    let gameTick = 0;
    function clock() {
      print("tick: " + gameTick);
      tasks[gameTick]?.forEach(f => f())
      ++gameTick;
    }
    let clockTimer = setInterval(clock, 100) // start clock running at 10hz for example
    
    //**** scheduler
    
    function schedule( task, ticksLater) {
       const t = gameTick + ticksLater;
       if( !tasks[t]) tasks[t] = [];
       tasks[t].push(task);
    }
    
    //**** Test ****
    
    schedule(task1, 2);
    schedule(function() {
       print("Simulate game end, clock stopped ");
       clearInterval( clockTimer);
    }, 30);
    <pre><code id=log></code></pre>