0

I need to implement a cross-site comet http server push mechanism using script tag long polling. (phew...) For this, I dynamically insert script tags into the DOM and the server sends back short js scripts that simply call a local callback function that processes the incoming messages. I am trying to figure out a way to associate each one of these callback calls with the script tag that sent it, to match incoming replies with their corresponding requests.

Clearly, I could simply include a request ID in the GET url, which is then returned back in the js script that the server generates, but this creates a bunch of unnecessary traffic and doesn't strike me as particularly elegant or clever.

What I would like to do is to somehow associate the request ID with the script tag that I generate and then read out this request ID from within the callback function that is called from inside this script tag. That way, all the request management would remain on the client.

This leads me to the following question: Is there a way to ask the browser for the DOM element of the currently executing script tag, so I can use the tag element to pass arguments to the contained javascript?

I found this thread:

Getting the currently executing, dynamically appended, script tag

Which is asking exactly this question, but the accepted answer isn't useful to me since it still requires bloat in the server-returned js script (setting marker-variables inside the script) and it relies on unique filenames for the scripts, which I don't have.

Also, this thread is related:

How may I reference the script tag that loaded the currently-executing script?

And, among other things, suggests to simply grab the last script in the DOM, as they are executed in order. But this seems to only work while the page is loading and not in a scenario where scripts are added dynamically and may complete loading in an order that is independent of their insertion.

Any thoughts?

PS: I am looking for a client-only solution, i.e. no request IDs or unique callback function names or other non-payload data that needs to get sent to and handled by the server. I would like for the server to (theoretically) be able to return two 100% identical scripts and the client still being able to associate them correctly.

Community
  • 1
  • 1
Markus A.
  • 12,349
  • 8
  • 52
  • 116
  • I realize that there are more modern approaches to replace long-polling, but I've investigated the other options and have come to the conclusion that for my scenario, this approach is the most suitable. Therefore, I'd like to avoid a discussion about abandoning this approach entirely. Thanks. – Markus A. Oct 13 '12 at 22:18
  • hmm. It might not help, but the script will fire an onload event when it loads. It might be possible to use that to call the callback function, but I'd need to have a better understanding of how you're generating the scripts and what's being returned to be of more help. Plus it doesn't answer your actual question. – thelastshadow Oct 13 '12 at 23:06
  • @thelastshadow: Actually, that may help, because I can assign a closure to the onload event that tracks my request id. Then, the callback function simply stores the last received message and the onload handler picks it up and processes it. Now the two questions are: Does onload always get called after the script is executed? Could two script tags that are received by the browser interleave? I.e. both call their callback and after that both fire their onload event? – Markus A. Oct 13 '12 at 23:18
  • If you view my other comment on my answer after your last question, you should know that its pointless to try and get the actual script tag. Even if you define code inside of that script, it never executes and is inaccessible to the javascript that is loaded from the response. – Geuis Oct 13 '12 at 23:51
  • "I would like for the server to (theoretically) be able to return two 100% identical scripts and the client still being able to associate them correctly." Sorry Markus, but this just isn't possible. I tried the very same thing a few years ago. Since the http requests are async (no order guaranteed), its impossible to tie them to specific code order. – Geuis Oct 14 '12 at 00:21
  • @Markus The onload "should" get called after the script loads but before it executes. If the scripts are loaded asynchronously then it is possible that one could load and before it's script is executed another script could load and execute, but I would say this is unlikely. – thelastshadow Oct 14 '12 at 16:17

4 Answers4

0

It's most definitely possible but you need a little trick. It's a common technique known as JSONP.

In JavaScript:

var get_a_unique_name = (function () {
    var counter = 0;
    return function () {
        counter += 1;
        return "function_" + counter;
    }
}()); // no magic, just a closure

var script = document.createElement("script");
var callback_name = get_a_unique_name();
script.src = "/request.php?id=12345&callback_name=" + callback_name;

// register the callback function globally
window[callback_name] = function (the_data) {
    console.log(the_data);
    handle_data(the_data); // implement this function
};

// add the script
document.head.appendChild(script);

The serverside you can have:

$callback_name = $_GET["callback_name"];
$the_data = handle_request($_GET["id"]); // implement handle_request
echo $callback_name . "(" . json_encode($the_data) . ");";
exit; // done

The script that is returened by /request.php?id=12345&callback_name=XXX will look something like this:

function_0({ "hello": "world", "foo" : "bar" });
Halcyon
  • 57,230
  • 10
  • 89
  • 128
  • That will definitely work, but unfortunately it involves sending the required data to the server and back, which is something I was trying to avoid. I am hoping to find a client-side-only solution. (See second paragraph of my question) – Markus A. Oct 13 '12 at 23:32
  • How does the server know what to return if you don't send it any input? If the server _does_ know what state the client is in, or it dictates what data the client gets, then you don't need to send any input other than the `callback_name`. – Halcyon Oct 13 '12 at 23:33
  • I do send it input, but the server doesn't need to know anything about my local request management. So any info I need to send it that relates to that is dead weight... :) – Markus A. Oct 13 '12 at 23:34
  • True, but this is unavoidable sadly. You're perhaps looking for a trick that would look at the source of a JavaScript file and read it as data or something, can't be done to my knowledge. This works, it even works cross domain if you set the right headers :) – Halcyon Oct 13 '12 at 23:35
0

I know you would like to avoid discussions about changing the approach, but that's really what you need to do.

First, each of the script tags being added to the DOM to fire off the poll request is disposable, i.e. each needs to be removed from the DOM as soon as its purpose has been served. Else you end up flooding your client DOM with hundreds or more dead script tags.

A good comparable example of how this works is jsonp implementations. You create a client-side named function, create your script tag to make the remote request, and pass the function name in the request. The response script wraps the json object in a function call with the name, which then executes the function on return and passes the json payload into your function. After execution, the client-side function is then deleted. jQuery does this by creating randomly generated names (they exist in the global context, which is really the only way this process works), and then deletes the callback function when its done.

In regards to long polling, its a very similar process. Inherently, there is no need for the response function call to know, nor care, about what script tag initiated it.

Lets look at an example script:

window.callback = function(obj){
    console.log(obj);   
}

setInterval(function(){

    var remote = document.createElement('script');
    remote.src = 'http://jsonip.com/callback';
    remote.addEventListener('load', function(){
        remote.parentNode.removeChild(remote);
    },false);

    document.querySelector('head').appendChild(remote);

}, 2000);​

This script keeps no references to the script elements because again, they are disposable. As soon as their jobs are done, they are summarily shot.

The example can be slightly modified to not use a setInterval, in which case you would replace setInterval with a named function and add logic into the remote load event to trigger the function when the load event completes. That way, the timing between script tag events depends on the response time of your server and is much closer to the actual long polling process.

You can extend this even further by using a queueing system to manage your callbacks. This could be useful if you have different functions to respond to different kinds of data coming back.

Alternatively, and probably better, is to have login in your callback function that handles the data returned from each poll and executes whatever other specific client-side logic at that point. This also means you only need 1 callback function and can get away from creating randomly generated callback names.

If you need more assistance with this, leave a comment with any specific questions and I can go into more detail.

Geuis
  • 41,122
  • 56
  • 157
  • 219
  • The callbacks must be unique because requests can be handled asynchronously. If you can guarantee that this will never happen (I don't see how though, how will you handle timeouts?), then sure, you can always have the global function `callback` handle the data. It doesn't really change the solution though, it's just a trivial optimization .. and you know what they say about optimization :D – Halcyon Oct 13 '12 at 23:36
  • Absolutely agree. In fact, I do delete the script tags. But your approach, again, involves the server in the association of requests with replies (via the callback function name, which does a round-trip). This is what I was trying to see if it can be avoided. It sounds like there might be an approach using onload events, so I'll post this as an answer to allow discussion. – Markus A. Oct 13 '12 at 23:40
  • It can't be avoided, as far as I know. Realize that the javascript being loaded from script tags operates in the global scope. Even if you generate the tags inside a closure, the script that gets loaded from the server still loads into the global scope with no knowledge of the closure where it originated. The other issue is that script tags operate in 2 ways. One, where you give a src attribute. If you define any code in that script tag, that code is not executed. Two, where you have code with setting the src attribute. – Geuis Oct 13 '12 at 23:44
0

There may be a solution using onload/onreadystate events on the script. I can pass these events a closure function that carries my request ID. Then, the callback function doesn't handle the server reply immediately but instead stores it in a global variable. The onload/onreadystate handler then picks up the last stored reply and tags it with the request ID it knows and then processes the reply.

For this to work, I need to be able to rely on the order of events. If onload is always executed right after the corresponding script tag finishes execution, this will work beautifully. But, if I have two tags loading simultaneously and they return at the same time and there is a chance that the browser will execute both and afterwards execute botth onload/onreadystate events, then I will loose one reply this way.

Does anyone have any insight on this?

.

Here's some code to demonstrate this:

function loadScript(url, requestID) {
  var script = document.createElement('script');
  script.setAttribute("src", url);
  script.setAttribute("type", "text/javascript");
  script.setAttribute("language", "javascript");
  script.onerror = script.onload = function() {
    script.onerror = script.onload = script.onreadystatechange = function () {}
    document.body.removeChild(script);
    completeRequest(requestID);
  }
  script.onreadystatechange = function () {
    if (script.readyState == 'loaded' || script.readyState == 'complete') {
      script.onerror = script.onload = script.onreadystatechange = function () {}
      document.body.removeChild(script);
      completeRequest(requestID);
    }
  }
  document.body.appendChild(script);
}

var lastReply;

function myCallback(reply) {
  lastReply = reply;
}

function completeRequest(requestID) {
  processReply(requestID, lastReply);
}

function processReply(requestID, reply) {
  // Do something
}

Now, the server simply returns scripts of the form

myCallback(message);

and doesn't need to worry at all about request IDs and such and can always use the same callback function.

The question is: If I have two scripts returning "simultaneously" is it possible that this leads to the following calling order:

myCallback(message1);
myCallback(message2);
completeRequest(requestID1);
completeRequest(requestID2);

If so, I would loose the actual reply to request 1 and wrongly associate the reply to request 2 with request 1.

Markus A.
  • 12,349
  • 8
  • 52
  • 116
  • You cannot rely on the order of http responses. In a fashion, if I create 10 script tags in my header when the page initially loads, those 10 tags will execute in order but it delays the loading and execution of anything else in the page. This is a big issue when optimizing pages for speed. In regards to long-polling and jsonp techniques, it doesn't block but you cannot guarantee the order in which the script tag load events fire. – Geuis Oct 13 '12 at 23:49
  • @Geuis: With this I wouldn't need to rely on the order of responses, in fact I know that they will be out of order. The only thing I need to be able to rely on is that the onload event gets called right after the respective script is loaded. I don't really care which script loads first. The only thing I need is that [script execution and event calling] is one atomic event as denoted by [] and doesn't get interrupted/interleaved with other script executions/event calls. – Markus A. Oct 14 '12 at 00:04
  • You can't guarantee that the load event will fire immediately, nor in order. That's what I was trying to get at. Even if your remote server returns a single-character empty string, you still have to take latency into account and the user's connection speed. 3 identical requests will take slightly different response times, and each load event cannot be guaranteed to load in a particular order. – Geuis Oct 14 '12 at 00:17
  • @Geuis: Absolutely correct... But I don't care about callback(1), onload(1), callback(2), onload(2) vs. callback(2), onload(2), callback(1), onload(1). The only thing I can't have is something like callback(2), onload(1), callback(1), onload(2), which may never happen anyways, if the callback(x), onload(x) always come together, independent of the order of the x's (which I don't need to be consecutive). – Markus A. Oct 14 '12 at 00:25
  • I get what you're intent on Markus. But if you aren't using XHR requests, there is absolutely no way beyond having the server respond with some identifying id or function call to get this to work how you want. The *only* other alternative would be to have a proxy server in the middle, so the client js routes to that and the proxy loads the remote data and returns it. Any if you go the route of using a proxy server, you can just use XHR requests and save yourself a lot of headache. – Geuis Oct 14 '12 at 00:39
  • One other thing, if the remote server for some odd reason supports CORS requests but not jsonp, you can use XHR requests via CORS support. This is unlikely, but possible. – Geuis Oct 14 '12 at 00:40
0

It should be quite simple. There is only one script element for each server "connection", and it can easily be stored in a scoped, static variable.

function connect(nameOfCallback, eventCallback) {
    var script;
    window[nameOfCallback] = function() { // this is what the response invokes
        reload();
        eventCallback.call(null, arguments);
    };
    reload();

    function reload() {
        if (script && script.parentNode)
            script.parentNode.removeChild(script);
        script = document.createElement(script);
        script.src = "…";
        script.type = "text/javascript";
        document.head.appendChild(script);
        // you might use additional error handling, e.g. something like
        // script.onerror = reload;
        // but I guess you get the concept
    }
}
Bergi
  • 630,263
  • 148
  • 957
  • 1,375
  • Unfortunately, this is the same suggestion as has been made several times already by Frits van Campen and Geuis and involves me sending otherwise useless data (the callback function name) to the server. – Markus A. Oct 14 '12 at 00:12
  • The name of the callback function is not useless data, but essential for the technology to work. How else would your push server know the *local callback* you spoke of? Of course you could hardcode that name, but this would be very restrictive for the client and also limit it to only one open poll request at a time (which would be enough) – Bergi Oct 14 '12 at 00:16
  • Exactly: I want to hard-code it. In my case it's not very restrictive for the client as it's easy for me to pick a unique name. I really would like to keep the server load that my app generates to an absolute minimum to reduce cost and improve responsiveness as much as possible. This includes reducing sent data as much as possible and not burdening the server with avoidable processing. And the "limit to only one open poll request at a time"-part is exactly what I am trying to solve. :) – Markus A. Oct 14 '12 at 00:20
  • Uh, c'mon, reading one request parameter and writing it back into the response is *nothing* in comparison to what your server does elsewhile. However: Hardcoding the callback's name makes no difference, you still can identify the `script` element by storing it in a static variable. – Bergi Oct 14 '12 at 00:25
  • @limit: Why would your application need more than one connection open at a time? And if so, how would they differ? You then could select the right script tag just by that difference. – Bergi Oct 14 '12 at 00:27
  • The problem is that you can't stop a script tag from loading. So, let's say you write a chatroom app with multiple rooms and you would like to stop listening to one room and subscribe to another one, you need to open a second connection with the first one potentially still open, unless, of course, you would like to go the FTP route and have one control connection and one data connection. But that opens a whole other can of worms, after which it really doesn't matter any more whether I had just sent the request ID or not. :) – Markus A. Oct 14 '12 at 00:40
  • OK, even though you can't `abort()` JSONP, the request to a different chatroom would either invoke a different callback function or invoke the same function with information stating that it's a different room. – Bergi Oct 14 '12 at 00:55