justjake / quickjs-emscripten

Safely execute untrusted Javascript in your Javascript, and execute synchronous code that uses async functions
https://www.npmjs.com/package/quickjs-emscripten
Other
1.23k stars 95 forks source link

Await is not working as expected inside evalCodeAsync #187

Closed rakeshar3796 closed 2 weeks ago

rakeshar3796 commented 2 weeks ago

I've a custom function which returns a promise but the code isn't waiting till the response


const { newQuickJSAsyncWASMModule } = require("quickjs-emscripten");

async function main() {
  const _module = await newQuickJSAsyncWASMModule();
  const runtime = _module.newRuntime();
  const context = runtime.newContext();

  let delayCounter = 0;

  try {
    const logHandle = context.newFunction("log", (...args) => {
      const nativeArgs = args.map(context.dump);
      console.log(...nativeArgs);
    });
    const consoleHandle = context.newObject();
    context.setProp(consoleHandle, "log", logHandle);
    context.setProp(context.global, "console", consoleHandle);
    consoleHandle.dispose();
    logHandle.dispose();

    function delay(timer = 1000) {
      delayCounter++;
      return new Promise((resolve) => {
        console.log(`****** DELAY ${delayCounter} - SCHEDULED ******`)
        setTimeout(() => {
          console.log(`****** DELAY ${delayCounter} - RESOLVED ******`)
          resolve();
        }, timer)
      })
    }

    const mockFetchHandle = context.newAsyncifiedFunction("mockFetch", async (_value) => {
      let value = context.dump(_value);
      const _object = context.newObject();
      await delay(2000);
      context.setProp(_object, "json", context.newString(value))
      console.log("****** MOCK FETCH COMPLETED ******")
      return _object;
    });
    mockFetchHandle.consume((fn) => context.setProp(context.global, "mockFetch", fn))

    const result = await context.evalCodeAsync(
      `
    async function request(value) {
        const data = await mockFetch(value);
        console.log("RESULT::", data.json);
    }

    try {
        await request("HELLO");
        await request("WORLD");
        globalThis.result = "COMPLETED";
    } catch(err) {
        console.log("ERR", err)
    }
  `,
      "",
      { type: "module" }
    );
    runtime.executePendingJobs();
    await context.unwrapResult(result).dispose();
    const response = context.getProp(context.global, "result").consume(context.getString)
    console.log("HOST::", JSON.stringify(response));
  } catch (err) {
    console.error("err", err);
  }
}

main();

The output am getting as

****** DELAY 1 - SCHEDULED ******
****** DELAY 1 - RESOLVED ******
****** MOCK FETCH COMPLETED ******
RESULT:: HELLO
****** DELAY 2 - SCHEDULED ******
HOST:: "undefined"
****** DELAY 2 - RESOLVED ******
****** MOCK FETCH COMPLETED ******
RESULT:: WORLD

The expected output is,

****** DELAY 1 - SCHEDULED ******
****** DELAY 1 - RESOLVED ******
****** MOCK FETCH COMPLETED ******
RESULT:: HELLO
****** DELAY 2 - SCHEDULED ******
****** DELAY 2 - RESOLVED ******
****** MOCK FETCH COMPLETED ******
RESULT:: WORLD
HOST:: "COMPLETED"

The host console execution should wait till the code execution gets complete. When i complete the request once its working fine as expected but if i call more than once am seeing some uncertanities.

@justjake can you check this out!

rakeshar3796 commented 2 weeks ago

@justjake am edited the script for better understanding, could you take a look and point it out if see any issues above?

justjake commented 2 weeks ago

If you are going to use await inside the VM to call async functions, you don't need newQuickJSAsyncWASMModule. newQuickJSAsyncWASMModule is only necessary when the VM wants to call a synchronous function, but the synchronous function needs to be an async function in the host.

Here what is happening is that at the await in await request("WORLD"), quickjs suspends execution as a pending job. Execution continues once you call runtime.executePendingJobs,