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?
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
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.
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.
The loop in step 1 continues iterating and fulfills the task promise for gameTick
4.
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).
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
sleep
and/or initial scheduling of a task.resolve
or task functions it needs to call. It then increments gameTick
after processing such an array if found. This implies:
gameTick
values obtained outside the clock will be one more than the last tick number processed by the clock.schedule
function below, called with zero ticksLater
, will call the task function at the next clock tick.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>