Invoking a JavaScript function from oncomplete handler of p:remoteCommand - simulating the same using some JavaScript code

前端 未结 1 1913
孤独总比滥情好
孤独总比滥情好 2021-01-05 04:05

Caution : Although this question covers long textual information with a mess of Java code snippets, it is merely targeted to JavaScript/jQuery and a bit of

相关标签:
1条回答
  • 2021-01-05 04:56

    I don't think I understood every aspect of your problem, but anyway I try to help a bit. Note that I do not know PrimeFaces, so all I did was reading the docs.

    What I understand is, that you try to get rid of the global variable. But I am afraid, I do not think this is possible.

    The problem here is, that PrimeFaces does not allow you to pass something transparently from your invocation of the remote call further to the oncomplete call (except you pass it to a Java code of the Bean and then back to the UI, and this usually is not what you want).

    However, I hope, you can come very close to it.

    Part 1, JS returns early

    Please also note that there probably is some misconception about Java and JavaScript.

    Java is multithreaded and runs several commands in parallel, while JavaScript is singlethreaded and usually never waits for something to complete. Doing things asychronously is mandatory to get a responsive Web-UI.

    Hence your remoteCommand invocation (seen from the JS side) will (usually, async case) return long before the oncomplete handler will be invoked. That means, if window[msg]() returns, you are not finished with the remoteCommand yet.

    So what you want to manage with following code

    if (window[msg]) {
        window[msg]();
    
        //Do something to call notifyAll() on oncomplete of remote command.
        dosomethinghere();
    }
    

    will fail. dosomethinghere() will not be invoked when the remoteCommand returned (as JS does not want to wait for some event, which might never happen). This means, dosomethinghere() will be invoked when the Ajax-request was just opened to the remote (to the Java application).

    To run something after the Ajax call finished, this must be done in the oncomplete routine (or onsuccess). This is why it's there.

    Part 2, validate msg

    Please note something different about window[msg](). This can be considered a bit dangerous if you cannot trust the pushed message completely. window[msg]() essentially runs any function named with the contents of the variable msg. For example if msg happen to be close then window.close() will be run, which probably is not what you want.

    You should make sure, msg is one expected word, and decline all other words. Example code for this:

    var validmsg = { updateModel:1, rc:1 }
    
    [..]
    
    if (validmsg[msg] && window[msg])
      window[msg]();
    

    Part 3: How to handle multiple JSON messages in parallel

    The global variable has some drawback. There is only one. If you happen to receive another JSON message on the WebSocket while the previous message still is processing in the remoteCommand, this will overwrite the previous message. So the notifyAll() will see the newer message twice, the old one is lost.

    A classical race condition. What you must do is, to create something like a registry to register all the messages, and then pass some value to notifyAll() to tell, which of the registered messages shall be processed.

    With only a little change, you can either parallely (here) or serially (Part 4) process the messages.

    First, create a counter to be able to distinguish the messages. Also an object to store all the messages. And we declare all valid messages we expect (see Part 2):

    var jsonMsgNr = 0;
    var jsonMessages = {};
    var validmsg = { updateModel:1 }
    

    Now add a message each time we receive one:

    if (window.WebSocket) {
        var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");
    
        ws.onmessage = function (event) {
            var jsonMsg = event.data;
            var json = JSON.parse(jsonMsg);        
            var msg=json["jsonMessage"];
    
            if (validmsg[msg] && window[msg]) {
                var nr = ++jsonMsgNr;
                jsonMessages[nr] = { jsonMsg:jsonMsg, json:json };
    

    To be able to pass the nr to NotifyAll() an additional parameter needs to be passed to the Bean. Let's call it msgNr:

                // Following might look a bit different on older PrimeFaces
                window[msg]([{name:'msgNr', value:nr}]);
            }
        }
    }
    

    Perhaps have a look into https://stackoverflow.com/a/7221579/490291 for more on passing values this way.

    The remoteAction bean now gets an additional parameter msgNr passed, which must be passed back via Ajax.

    Unfortunately I have no idea (sorry) how this looks in Java. So make sure, your answer to the AjaxCall copies the msgNr out again.

    Also, as the documentation is quiet about this subject, I am not sure how the parameters are passed back to the oncomplete handler. According to the JavaScript debugger, notifyAll() gets 3 parameters: xhdr, payload, and pfArgs. Unfortunately I was not able to setup a test case to find out how things look like.

    Hence the function looks a bit like (bear with me, please):

    function notifyAll(x, data, pfArgs) {
       var nr = ???; // find out how to extract msgNr from data
    
       var jsonMsg = jsonMessages[nr].jsonMsg;
       var json = jsonMessages[nr].json;
       jsonMessages[nr] = null;  // free memory
    
       sendMessage(jsonMsg);
    
       dosomething(json);
    }
    

    If you split this into two functions, then you can invoke the notifyAll() from other parts in your application:

    function notifyAll(x, data, unk) {
       var nr = ???; // find out how to extract msgNr from data
    
       realNotifyAll(nr);
    }
    
    function realNotifyAll(nr) {
      if (!(nr in jsonMessages)) return;
    
      var jsonMsg = jsonMessages[nr].jsonMsg;
      var json = jsonMessages[nr].json;
      delete jsonMessages[nr];  // free memory
    
      sendMessage(jsonMsg);
    
      dosomething(json);
    }
    

    Some things here are a bit redundant. For example you perhaps do not need the json element in jsonMessages or want to parse the json again to spare some memory in case the json is very big. However the code is meant not to be optimal but to be easy to adjust to your needs.

    Part 4: serialize requests

    Now to the changes to serialize things. That's quite easy by adding some semaphore. Semaphores in JavaScript are just variables. This is because there is only one global thread.

    var jsonMsgNr = 0;
    var jsonMessages = {};
    var validmsg = { updateModel:1 }
    var jsonMsgNrLast = 0;           // ADDED
    
    if (window.WebSocket) {
        var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");
    
        ws.onmessage = function (event) {
            var jsonMsg = event.data;
            var json = JSON.parse(jsonMsg);        
            var msg=json["jsonMessage"];
    
            if (validmsg[msg] && window[msg]) {
                var nr = ++jsonMsgNr;
                jsonMessages[nr] = { jsonMsg:jsonMsg, json:json };
    
                if (!jsonMsgNrLast) {    // ADDED
                    jsonMsgNrLast = nr;  // ADDED
                    window[msg]([{name:'msgNr', value:nr}]);
                }
            }
        }
    }
    
    function realNotifyAll(nr) {
      if (!(nr in jsonMessages)) return;
    
      var jsonMsg = jsonMessages[nr].jsonMsg;
      var json = jsonMessages[nr].json;
      delete jsonMessages[nr];  // free memory
    
      sendMessage(jsonMsg);
    
      dosomething(json);
    
      // Following ADDED
      nr++;
      jsonMsgNrLast = 0;
      if (nr in jsonMessages)
        {
          jsonMsgNrLast = nr;
          window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]);
        }
    }
    

    Note: jsonMsgNrLast could be just a flag (true/false). However having the current processed number in a variable perhaps can help somewhere else.

    Having said that, there is a starvation problem in case something fails in sendMessage or dosomething. So perhaps you can interleave it a bit:

    function realNotifyAll(nr) {
      if (!(nr in jsonMessages)) return;
    
      var jsonMsg = jsonMessages[nr].jsonMsg;
      var json = jsonMessages[nr].json;
      delete jsonMessages[nr];  // free memory
    
      nr++;
      jsonMsgNrLast = 0;
      if (nr in jsonMessages)
        {
          jsonMsgNrLast = nr;
          // Be sure you are async here!
          window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]);
        }
    
      // Moved, but now must not rely on jsonMsgNrLast:
      sendMessage(jsonMsg);
      dosomething(json);
    }
    

    This way the AJAX request is already send out while sendMessage is running. If now dosomething has a JavaScript error or similar, the messages are still processed correctly.

    Please note: All this was typed in without any tests. There might be syntax errors or worse. Sorry, I tried my best. If you find a bug, edit is your friend.

    Part 5: Direct Invocation from JS

    Now, with all this in place and a serialized Run, you can always invoke the previous notifyAll() using realNotifyAll(jsonMsgNrLast). Or you can display the jsonMessages in a list and choose any arbitrary number.

    By skipping the call to window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]); (and above window[msg]([{name:'msgNr', value:nr}]);) you also can halt the Bean processing and run it on-demand using the usual JQuery callbacks. For this create a function and change the code a bit again:

    var jsonMsgNr = 0;
    var jsonMessages = {};
    var validmsg = { updateModel:1 }
    var jsonMsgNrLast = 0;
    var autoRun = true;        // ADDED, set false control through GUI
    
    if (window.WebSocket) {
        var ws = new WebSocket("wss://localhost:8181/ContextPath/AdminPush");
    
        ws.onmessage = function (event) {
            var jsonMsg = event.data;
            var json = JSON.parse(jsonMsg);        
    
            if (validmsg[msg] && window[msg]) {
                var nr = ++jsonMsgNr;
                jsonMessages[nr] = { jsonMsg:jsonMsg, json:json };
    
                updateGuiPushList(nr, 1);
    
                if (autoRun && !jsonMsgNrLast) {
                    runRemote(nr);
                }
            }
        }
    }
    
    function realNotifyAll(nr) {
      if (!(nr in jsonMessages)) return;
    
      var jsonMsg = jsonMessages[nr].jsonMsg;
      var json = jsonMessages[nr].json;
      delete jsonMessages[nr];  // free memory
    
      updateGuiPushList(nr, 0);
    
      jsonMsgNrLast = 0;
      if (autoRun)
        runRemote(nr+1);
    
      // Moved, but now must not rely on jsonMsgNrLast:
      sendMessage(jsonMsg);
      dosomething(json);
    }
    
    function runRemote(nr) {
      if (nr==jsonMsgNrLast) return;
      if (nr in jsonMessages)
        {
          if (jsonMsgNrLast) { alert("Whoopsie! Please wait until processing finished"); return; }
          jsonMsgNrLast = nr;
    
          updateGuiPushList(nr, 2);
    
          // Be sure you are async here!
          window[jsonMessages[nr].json.msg]([{name:'msgNr', value:nr}]);
        }
    }
    

    Now you can start the processing with runRemote(nr) and invoke the completion function with realNotifyAll(nr).

    The function updateGuiPushList(nr, state) with state=0:finished 1=added 2=running is the callback to your GUI code which updates the on-screen list of waiting pushes to process. Set autoRun=false to stop automatic processing and autoRun=true for automatic processing.

    Note: After setting autoRun from false to true you need to trigger runRemote once with the lowest nr, of course.

    0 讨论(0)
提交回复
热议问题