The impossible inline Javascript delay/sleep
Asked Answered
H

8

8

There is a JavaScript function, of which I have zero control of the code, which calls a function that I wrote. My function uses DOM to generate an iFrame, defines it's src and then appends it to another DOM element. However, before my function returns, and thus allows continued execution of the containing function, it is imperative that the iFrame be fully loaded.

Here are the things that I have tried and why they do not work :

1. The SetTimeout option :
99.999% of the time, this is THE answer. As a matter of fact, in the past decade that I have been mentoring in JavaScript, I have always insisted that code could always be refactored to use this option, and never believed a scenario existed where that was not the case. Well, I finally found one! The problem is that because my function is being called inline, if the very next line is executed before my iFrame finishes loading, it totally neuters my script, and since the moment my script completes, the external script continues. A callback of sorts will not work

2. The "Do nothing" loop :
This option you use while(//iFrame is not loaded){//do nothing}. In theory this would not return until the frame is loaded. The problem is that since this hogs all the resources, the iFrame never loads. This trick, although horribly unprofessional, dirty etc. will work when you just need an inline delay, but since I require an external thread to complete, it will not.
In FF, after a few seconds, it pauses the script and an alert pops up stating that there is an unresponsive script. While that alert is up, the iFrame is able to load, and then my function is able to return, but having the browser frozen for 10 seconds, and then requiring the user to correctly dismiss an error is a no go.

3. The model dialogue :
I was inspired by the fact that the FF popup allowed the iFrame to load while halting the execution of the function, and thinking about it, I realized that it is because the modal dialogue, is a way of halting execution yet allowing other threads to continue! Brilliant, so I decided to try other modal options. Things like alert() work beautifully! When it pops up, even if only up for 1/10th of a second, the iFrame is able to complete, and all works great. And just in case the 1/10 of a second is not sufficient, I can put the model dialogue in the while loop from solution 2, and it would ensure that the iFrame is loaded in time. Sweet right? Except for the fact that I now have to pop up a very unprofessional dialogue for the user to dismiss in order to run my script. I fought with myself about this cost/benefit of this action, but then I encountered a scenario where my code was called 10 times on a single page! Having to dismiss 10 alerts before acessing a page?! That reminds me of the late 90s script kiddie pages, and is NOT an option.

4. A gazillion other delay script out there:
There are about 10 jQuery delay or sleep functions, some of them actually quite cleverly developed, but none worked. A few prototype options, and again, none I found could do it! A dozen or so other libraries and frameworks claimed they had what I needed, but alas they all conspired to give me false hope.

I am convinced that since a built in model dialogue can halt execution, while allowing other threads to continue, there must be some code accessible way to do the same thing with out user input.

The Code is literally thousands upon thousands of lines and is proprietary, so I wrote this little example of the problem for you to work with. It is important to note the ONLY code you are able to change is in the onlyThingYouCanChange function

Test File :

<html>
<head>
</head>
</html>
<body>
<div id='iFrameHolder'></div>
<script type='text/javascript'>
function unChangeableFunction()
{
    new_iFrame = onlyThingYouCanChange(document.getElementById('iFrameHolder'));
    new_iFrame_doc = (new_iFrame.contentWindow || new_iFrame.contentDocument);
    if(new_iFrame_doc.document)new_iFrame_doc=new_iFrame_doc.document;
    new_iFrame_body = new_iFrame_doc.body;
    if(new_iFrame_body.innerHTML != 'Loaded?')
    {
        //The world explodes!!!
        alert('you just blew up the world!  Way to go!');
    }
    else
    {
        alert('wow, you did it!  Way to go!');
    }
}
var iFrameLoaded = false;
function onlyThingYouCanChange(objectToAppendIFrameTo)
{
    iFrameLoaded = false;
    iframe=document.createElement('iframe');
    iframe.onload = new Function('iFrameLoaded = true');
    iframe.src = 'blank_frame.html'; //Must use an HTML doc on the server because there is a very specific DOM structure that must be maintained.
    objectToAppendIFrameTo.appendChild(iframe);
    var it = 0;
    while(!iFrameLoaded) //I put the limit on here so you don't 
    {
        //If I was able to put some sort of delay here that paused the exicution of the script, but did not halt all other browser threads, and did not require user interaction we'd be golden!
        //alert('test'); //This would work if it did not require user interaction!
    }
    return iframe;
}
unChangeableFunction();
</script>
</body>

blank_frame.html :

<html>
<head>
</head>
<body style='margin:0px'>Loaded?</body>
</html>


HERE IS THE ANSWER I MADE FROM COMBINING IDEAS FROM RESPONDERS! YOU GUYS ROCK!
new source of the function I was allowed to change :
function onlyThingYouCanChange(objectToAppendIFrameTo)
{
    iFrameLoaded = false;
    iframe=document.createElement('iframe');
    iframe.onload = new Function('iFrameLoaded = true');
    iframe.src = 'blank_frame.html'; //Must use an HTML doc on the server because there is a very specific DOM structure that must be maintained.
    objectToAppendIFrameTo.appendChild(iframe);
    var it = 0;
    while(!iFrameLoaded) //I put the limit on here so you don't
    {
        if (window.XMLHttpRequest)
        {
            AJAX=new XMLHttpRequest();
        }
        else
        {
            AJAX=new ActiveXObject("Microsoft.XMLHTTP");
        }
        if (AJAX)
        {
            AJAX.open("GET", 'slow_page.php', false);
            AJAX.send(null);
        }
        else
        {
            alert('something is wrong with AJAX!');
        }

        //If I was able to put some sort of delay here that paused the exicution of the script, but did not halt all other browser threads, and did not require user interaction we'd be golden!
        //alert('test'); //This would work if it did not require user interaction!
    }
    return iframe;
}

slow_page.php :

<?
usleep(100000);//sleep for 1/10th of a second, to allow iFrame time to load without DOSing our own server!
?>

I do want to note that I stated that there was nothing outside of that function that I could change, and adding the php page did violate that "rule" but in may case I was able to do that. If I were not able to do that, I could have called blank_frame.html instead of slow_page.php, and it should have only ever needed to call it once (so 2 times per frame load) assuming that it responded in an identical amount of time as the iFrame load. If for some reason the iFrame load was slower, it might call it 2ce (a total of 3 calls to the server)

Headmistress answered 1/1, 2011 at 23:57 Comment(5)
Can't you combine new Function(){ return code; } with the callback of setTimeout?Staminody
Personally I think any design that requires this to be the case is highly flawed, but from an academic perspective I'd be interested in seeing whether there is an answer.Paperboy
meder, setTimeout will allow continued Javascript execution, unless you have a way of combining those that I am not aware of. I mentioned some jQuery plugins that did do some very clever things, with that, but I analyzed all that I could find and none would work for me. If you can find a way to get it to work in the above example I would be thrilled! Brian. I agree with you 99.999%, and a few days ago I would have said 100%, and would have argued with anyone who said an inline delay was necessary. Unfortunately I don't have the control to fix it here!Headmistress
Javascript is inherently asynchronous. This is the wrong architecture to be using with the language. You should change your API and provide a callback function as a parameter to be called when your operation is completed. Doing it any other way is like using HTML divs 1 pixel wide with background colors to draw a JPG image. Surely you can do it, but that's not what you're supposed to be doing. Know your language, and design your code thinking the way the language was meant to be used. You don't write your PHP like you write your Javascript, so why write your Javascript as if it were C?Grunt
I would really love to give 66% credit to lonesomeday and 34% credit to Zeki because it was two ideas combined that they gave me that I made work. unfortunately I don't know how to do that, so I will have to give it all to lonesomeday. I will then post the combined code that worked perfectly.Headmistress
E
2

NB This is extremely hacky, and I wouldn't use it in any real-world situation. Among other potential issues, given sufficient traffic you could end up DDOSing yourself.

You could create sleep functionality by making non-asynchronous (A)JAX calls. In some older browsers this may freeze everything, but at least it won't require any kind of user response.

while (!iFrameLoaded)
{
    if (XMLHTTPRequest) {
        var request = new XMLHttpRequest();
    } else {
        var request = new ActiveXObject("Microsoft.XMLHTTP");
    }

    request.open('GET', 'anyoldfile.htm', false);
    request.send();

    // check if the iframe is loaded and set iFrameLoaded
}
Eastwood answered 2/1, 2011 at 0:17 Comment(2)
I could put in 100 urls on different servers if I needed to (google, amazon, yahoo, whtismyip etc) for the ajax calls that it could loop through... I would just be discarding the results anyway... I'm going to toy with this idea for a bit, my guess is that 1-2 calls would be plenty of a delay anyway. I'll let you know how it goes!Headmistress
@Headmistress Given the same-origin policy, you'll need URLs on your own domain, otherwise the requests will fail instantly.Eastwood
L
3

Yeah, the fact that javascript is single threaded really bites you here. You can use a synchronous ajax call to a purposefully slow page to emulate a sleep, but you aren't going to get the results you want. Why don't you just make sure that your IFrame is loaded before unchangeable function is called?

Lineman answered 2/1, 2011 at 0:21 Comment(3)
unchangeable function calls the only function which I have control of, so I would not have any way of introducing a wait before that. I am exploring the synchronous ajax though.Headmistress
Yeah, if you do that, you don't need to hit the page hundreds of times, all you need to do is put a sleep in your php/jsp, or whatever else you use. Add a 100ms sleep, and loading it 10 times will give you a 1 second sleep.Lineman
You gave me a very important key in solving this problem (the usleep PHP script), but I could only accept one answer. I wish I could have accepted both. Thank you!Headmistress
E
2

What you really need is an event to be fired when the iFrame content has loaded. This is actually really easy because the page inside the iFrame has its own events and it can access scripts on the parent page. You will need to be able to change the contents of the iFrame though.

In your iFrame, you'll need this piece of code

// Use whichever DOMReady function you like, or window.onload would work
window.addEventListener('DOMContentLoaded', function() {
    if (parent.window.myFunction) {
        parent.window.myFunction();
    }
}, false);

Then in your parent page, make a function called "myFunction" and put all the scripts you need to fire in there. This should work every time.

Edit: To get this to work you really need two functions. I'm assuming that's really not an option so we'll hack the one function to contain two functions and call the right part when we need it to.

function onlyThingYouCanChange(stringOrObject) {
    function createIFrame(objectToAppendIFrameTo) {
        // This comment represents all the code that appends your iFrame
    }
    function onIFrameReady() {
        // This comment represents all the stuff you want to happen when the iFrame is ready
    }

    // The bones of it
    if (stringOrObject === "iFrameLoaded") {
        onIFrameReady();
    } else {
        createIFrame(stringOrObject);
    }
}

The script in the iFrame should now be changed to something like this:

// Use whichever DOMReady function you like, or window.onload would work
window.addEventListener('DOMContentLoaded', function() {
    if (parent.window.onlyThingYouCanChange) {
        parent.window.onlyThingYouCanChange('iFrameLoaded');
    }
}, false);

I haven't tested it, but in theory that should do it

Escarpment answered 2/1, 2011 at 0:15 Comment(2)
There are a few problems with this in which I covered in my question. 1) I can not change anything outside of that function I specified. The reasons behind this are complex, so I am just going to have to say trust me on this one 2) If you look at my code, you will actually see I already do call a function when then iFrame finishes loading. That is the simple part. The problem is, I need to delay the rest of the subsequent code execution until that event is fired, which I don't see how your solution does that at all.Headmistress
If there is any way you can get your solution to work with my sample, even if you have to change blank_frame.html, I might be able to accomplish that in a creative way too.Headmistress
E
2

NB This is extremely hacky, and I wouldn't use it in any real-world situation. Among other potential issues, given sufficient traffic you could end up DDOSing yourself.

You could create sleep functionality by making non-asynchronous (A)JAX calls. In some older browsers this may freeze everything, but at least it won't require any kind of user response.

while (!iFrameLoaded)
{
    if (XMLHTTPRequest) {
        var request = new XMLHttpRequest();
    } else {
        var request = new ActiveXObject("Microsoft.XMLHTTP");
    }

    request.open('GET', 'anyoldfile.htm', false);
    request.send();

    // check if the iframe is loaded and set iFrameLoaded
}
Eastwood answered 2/1, 2011 at 0:17 Comment(2)
I could put in 100 urls on different servers if I needed to (google, amazon, yahoo, whtismyip etc) for the ajax calls that it could loop through... I would just be discarding the results anyway... I'm going to toy with this idea for a bit, my guess is that 1-2 calls would be plenty of a delay anyway. I'll let you know how it goes!Headmistress
@Headmistress Given the same-origin policy, you'll need URLs on your own domain, otherwise the requests will fail instantly.Eastwood
L
1

A stupefyingly simple ;-} answer using XPCOM:

// Get instance of the XPCOM thread manager.
var threadManager=Components.classes['@mozilla.org/thread-manager;1'].getService(
                      Components.interfaces.nsIThreadManager);
// Release current thread.
function doThread() {threadManager.currentThread.processNextEvent(false);};

// Event enabled delay, time in ms.
function delay(time) {
  var end;
  var start=Date.now();
  do {
    end=Date.now();
    doThread();
  } while ((end-start) <= time);
}

Works in recent version of Firefox. Sorry no hope for Explorer!

Lorient answered 27/9, 2011 at 22:23 Comment(0)
D
1

A recursive function might help out in this case. just call the function until a global variable indicates that the frame is loaded

var iFrameStarted = false; //you need two global vars
var iFrameLoaded  = false;


function onlyThingYouCanChange(objectToAppendIFrameTo)
{
  if (iFrameLoaded=false)  // if the frame has loaded then you are done. skip everything and return iframe
  { if (iFrameStarted = false) //otherwise start the frame if it has not been
  {
   iFrameStarted = true;
   iframe=document.createElement('iframe');
   iframe.onload = new Function('iFrameLoaded = true');
   iframe.src = 'blank_frame.html'; //Must use an HTML doc on the server because there is a very specific DOM structure
   objectToAppendIFrameTo.appendChild(iframe);
   var it = 0;
   for (i=0;i<10000;i++) {}  //slow down execution so you are not recursing yourself to death
   onlyThingYouCanChange(objectToAppendIFrameTo);   //start the recursion process

  }
  else  //the frame has been started so continue recursion until the frame loaded
  {
   for (i=0;i<10000;i++) {}  //slow down execution so you are not recursing yourself to death
   onlyThingYouCanChange(objectToAppendIFrameTo);   recursively call your function until the frame is loaded

  }

}

return iframe;  //you only get here when all the recursions are finished   
}
Diena answered 1/7, 2012 at 8:3 Comment(1)
Glancing over this, I don't see how it is different from '2. The "Do nothing" loop' Can you elaborate?Headmistress
T
0

Why can you not modify the base code? For example, it could be fairly simple to change the core function from

function unChangeableFunction()
{
    new_iFrame = onlyThingYouCanChange(document.getElementById('iFrameHolder'));
    new_iFrame_doc = (new_iFrame.contentWindow || new_iFrame.contentDocument);
    if(new_iFrame_doc.document)new_iFrame_doc=new_iFrame_doc.document;
    new_iFrame_body = new_iFrame_doc.body;
    if(new_iFrame_body.innerHTML != 'Loaded?')
    {
        //The world explodes!!!
        alert('you just blew up the world!  Way to go!');
    }
    else
    {
        alert('wow, you did it!  Way to go!');
    }
}

To something like this:

function unChangeableFunction()
{
    var new_iFrame = onlyThingYouCanChange(document.getElementById('iFrameHolder'));
    new_iFrame.onload = function()
    {
        new_iFrame_doc = (new_iFrame.contentWindow || new_iFrame.contentDocument);
        if(new_iFrame_doc.document)new_iFrame_doc=new_iFrame_doc.document;
        new_iFrame_body = new_iFrame_doc.body;
        if(new_iFrame_body.innerHTML != 'Loaded?')
        {
            //The world explodes!!!
            alert('you just blew up the world!  Way to go!');
        }
        else
        {
            alert('wow, you did it!  Way to go!');
        }
    };
}

If that doesn't work for you, how about a transparent modification of the original code? Compile it with Javascript Strands and use the built-in futures support to handle this. Note that Javascript 1.7 also supports continuations, but would require changing the code manually to use them.

Thankful answered 2/1, 2011 at 0:16 Comment(0)
E
0

Another solution that may not be applicable, depending on how much you have simplified the original code. You could set an onload handler, then throw an error, then call unChangeableFunction in your onload handler:

function onlyThingYouCanChange(objectToAppendIFrameTo)
{
    // using global variable func_called
    if (!func_called) {
        func_called = true;
        var iframe=document.createElement('iframe');
        iframe.src = 'blank_frame.html';
        iframe.id = 'myIframe';
        iframe.onload = function() {
            unChangeableFunction();
        };
        objectToAppendIFrameTo.appendChild(iframe);

        throw new Error('not an error');
    } else {
        return document.getElementById('myIframe');
    }
}

This function (like unChangeableFunction) will be called twice: once in the first instance, then again when the onload handler is triggered. The two different pathways reflect this.

Again, this is hacky, and a definite abuse of JS's error functionality.

Eastwood answered 2/1, 2011 at 0:39 Comment(0)
M
0

you can use cookie and setTimeout like that:

in blank_frame.html add a script:

<script type="text/javascript">
function deleteCookie(cookie_name)
{
  var cookie_date=new Date();
  cookie_date.setTime(cookie_date.getTime()-1);
  document.cookie=cookie_name+="=;expires="+cookie_date.toGMTString();
}
function setCookie(name,value,expires,path,domain,secure){
    document.cookie=name+"="+escape(value)+((expires)?"; expires="+expires.toGMTString():"")+((path)?"; path="+path:"")+((domain)?"; domain="+domain:"")+((secure)?"; secure":"");
}

window.onload=function(){
    setCookie('iframe_loaded','yes',false,'/',false,false);
}
</script>

Basically you're adding a cookie iframe_loaded with value yes. IMO it's better to remove the cookie as you need to do the same if you'll reload the page. You can as well set the domain in setCookie function call.

Now in main file we'll use setTimeout with function that will check if the cookie exists, if it does then the function will return iframe like in your code:

function onlyThingYouCanChange(objectToAppendIFrameTo)
{
    function get_cookie(cookie_name){
        var results = document.cookie.match('(^|;) ?'+cookie_name+'=([^;]*)(;|$)');
        return results?unescape(results[2]):null;
    }
    function deleteCookie(cookie_name){
        var cookie_date=new Date();
        cookie_date.setTime(cookie_date.getTime()-1);
        document.cookie=cookie_name+="=;expires="+cookie_date.toGMTString();
    }

    iFrameLoaded = false;
    iframe=document.createElement('iframe');
    iframe.onload = new Function('iFrameLoaded = true');
    iframe.src = 'blank_frame.html'; //Must use an HTML doc on the server because there is a very specific DOM structure that must be maintained.
    objectToAppendIFrameTo.appendChild(iframe);
    var it = 0;

    function checkiframe(){
        if(get_cookie('iframe_loaded')=="yes"){
            alert('iframe loaded');
            deleteCookie('iframe_loaded');
            return iframe;
        }else{
            setTimeout(checkiframe,1000);
        }
    }

    checkiframe();
}

As a failsafe cookie is being deleted in this file as well. Hopefully that will give you something to work with :)

Cheers

G.

Mycah answered 2/1, 2011 at 0:40 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.