skip to Main Content

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?

2

Answers


  1. The problem is that promises are asynchronous, but your game loop is synchronous. When you call resolve(), that does not directly continue executing the coroutine. It only resolves the promise, which schedules any registered promise reactions (such as the continuation of the asynchronous code that awaited that promise, or callback functions added via .then()) to run soon, in the next microtask. So you’ll need to give it some time to do that before running the next game tick:

    (tasks[2] ??= []).push(task1);
    
    let gameTick = 0;    
    while (gameTick < 10) {
        print(`tick: ${gameTick}`);
        tasks[gameTick]?.forEach(f => f())
        await Promise.resolve(); // if not even setTimeout()
        gameTick++;
    }
    
    Login or Signup to reply.
  2. 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.

    Solution

    Redesign of algorithm, code implementation and/or testing code as appropriate to where the problem with the posted code originates.

    Debugging undertaken

    The following snippet adds some output to show the value of i and gameTick in more places and log when await returns. It "fixes" the problem by resolving promises in a timer callback after the hang.

    However it’s not a solution – the third iteration if the loop in task2 reintroduces an unresolved pending promise.

    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[gameTick]?.length || 0}`)
            tasks[targetTick]?.forEach(f => f())
        }
    }
    setTimeout(hammer, 2000);
    setTimeout(hammer, 4000);
    <pre><code id=log></code></pre>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search