vm-browserify icon indicating copy to clipboard operation
vm-browserify copied to clipboard

setTimeout is not working

Open ahmadalibaloch opened this issue 6 years ago • 10 comments

setTimeout, setInterval and other interval built in methods do not work, even if I expose them in context.

console.log('start');
setTimeout(()=> {
     console.log('hello timeout');
 }, 2000);
 
 console.log('end');

output:

start
end

It is to be noted that when I add .bind(this) , then timeout works and breaks at that line saying .bind is not a function.

console.log('start');
setTimeout(()=> {
     console.log('hello timeout');
 }, 2000).bind(this);
 
 console.log('end');

output:

start
hello timeout
// and an error in console saying setTimeout(...).bind is not a function

ahmadalibaloch avatar Apr 19 '19 17:04 ahmadalibaloch

yeah, this is because vm-browserify creates an iframe to run the code in, and immediately destroys it after running the code. so if you schedule async callbacks, they will not get called.

it would be nice to find a solution for this, i'm not sure how to do it because we don't want to keep the iframes around forever.

goto-bus-stop avatar Apr 19 '19 19:04 goto-bus-stop

But why does it work when I write .bind(this), and script stops working at that line too, because of the errro .bind is not a function.

ahmadalibaloch avatar Apr 20 '19 15:04 ahmadalibaloch

setTimeout doesn't return anything, so you can't call methods on it. setTimeout itself is synchronous, so the code following it executes immediately. This is true in both cases. The .bind call throws an error synchronously after the callback is scheduled. So in both cases a callback is scheduled but never run because the iframe is destroyed

On April 20, 2019 5:53:21 PM GMT+02:00, Ahmad Ali [email protected] wrote:

But why does it work when I write .bind(this), and script stops working at that line too, because of the errro .bind is not a function.

-- You are receiving this because you commented. Reply to this email directly or view it on GitHub: https://github.com/browserify/vm-browserify/issues/24#issuecomment-485137832

-- Sent from my Android device with K-9 Mail. Please excuse my brevity.

goto-bus-stop avatar Apr 20 '19 15:04 goto-bus-stop

The issue is same for builtin vm module but I tested this in node.js and it works fine. I think there is some problem of this of the setTimeout, but I dont know how to solve it.

This issue is also discussed here, any workarounds are appreciated. setTimeout and/or context issue (runInContext) setTimeout not run in webview's Node vm

ahmadalibaloch avatar Apr 20 '19 16:04 ahmadalibaloch

The .bind error is fixed by putting it on the function instead of on setTimeout:

setTimeout(
  function () {
    // ...
  }.bind(this)
)

But it's just not possible to use setTimeout in vm-browserify. async code doesn't work, because we destroy the iframe immediately.

goto-bus-stop avatar Apr 20 '19 16:04 goto-bus-stop

If you say that the iFrame is destroyed in which the code was running then how my custom implementation of setTimeout and setInterval is working.

const setTimeouts = [];
function customSetTimeout(cb, interval) {
  const now = window.performance.now();
  const index = setTimeouts.length;
  setTimeouts[index] = () => {
    cb();
  };
  setTimeouts[index].active = true;
  const handleMessage = (evt) => {
    if (evt.data === index) {
      if (window.performance.now() - now >= interval) {
        window.removeEventListener('message', handleMessage);
        if (setTimeouts[index].active) {
          setTimeouts[index]();
        }
      } else {
        window.postMessage(index, '*');
      }
    }
  };
  window.addEventListener('message', handleMessage);
  window.postMessage(index, '*');
  return index;
}

function customClearTimeout(setTimeoutId) {
  if (setTimeouts[setTimeoutId]) {
    setTimeouts[setTimeoutId].active = false;
  }
}

Even if I add customSetTimeout for 1 minute, it works

 const codeToEval = `console.log('start)
 var a = setTimeout(function() {
    console.log('setTimeout  async done');
 }, 10000);
 console.log('sync');`
 const sandbox = {
      setTimeout: customSetTimeout,
      clearTimeout: customClearTimeout,
    };
vm.runInNewContext(codeToEval, sandbox);

ahmadalibaloch avatar Apr 23 '19 12:04 ahmadalibaloch

It's because there are multiple windows at play. If you call window.setTimeout inside the iframe, a new callback is scheduled inside the iframe. If the iframe is destroyed, this callabck will not run.

If you call your custom setTimeout, or pass the setTimeout function like so:

vm.runInNewContext('code', { setTimeout: window.setTimeout })

this calls out to the parent window; so in fact your callback is scheduled in the parent window, not inside the iframe. Then if the iframe is destroyed the callback still lingers and will be called by the parent window.

goto-bus-stop avatar Apr 23 '19 13:04 goto-bus-stop

I tried checking your suggestion vm.runInNewContext('code', { setTimeout: window.setTimeout }) but it doesn't work. Custom setTimeout do work.

ahmadalibaloch avatar Apr 23 '19 13:04 ahmadalibaloch

I ended up creating a custom setTimeout, setInterval, clearTimeout, clearInterval, explained in the link below. And it plays very well! https://gist.github.com/ahmadalibaloch/6c7d70244c83b90aa77bb83fa28cd0df

ahmadalibaloch avatar Sep 11 '19 17:09 ahmadalibaloch

I've found a faster solution:

vm.runInNewContext('code', {
    setTimeout: (callback, wait) => setTimeout(() => callback(), wait),
    clearTimeout: handle => clearTimeout(handle)
})

Yarflam avatar Jul 18 '23 15:07 Yarflam