Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Async/Await answer onMessage event in cross-domain communication with an iframe

I have an iframe, which is communicating with its parent window to set and get some essential cookies via postMessage method.

At first a function in the iframe application requests Cookie A from the parent window.

    function requestCommunication(topic, customerId) {

            function cookieAvailable() {
                return new Promise((resolve) => resolve(getCookieData('cookieName'));
                });
            }

            console.log(cookieAvailable());

            if(!!cookieAvailable()) {
                //doStuff        
            }
     }

cookieAvailable() triggers the message from the iframe to the parent.window. In turn, the window returns the cookie with its data as string. This gets done by using the following:

 async function getCookieData(cookieName) {

        const {data} = await new Promise(resolve => {

            window.onmessage = (event) => {
                resolve(event);
            }
        });

        var cookieContent = JSON.parse(data);
        var cookieContentData = JSON.parse(cookieContent.data);

        return cookieContentData; //this returns the cookie data (in almost all cases)
    }    

I don't get how to use the promise correctly to hand it over to my initial trigger function. I would appreciate any support.

like image 246
Nixen85 Avatar asked Oct 27 '22 16:10

Nixen85


1 Answers

There are obvious problems and anti-patterns in your code. cookieAvailable will return a Promise, so your check if(!!cookieAvailable()) { will always be truthy. You will want to wait for that Promise to resolve before checking if there is indeed a cookie available.

But actually, your cookieAvailable function returns a Promise wrapper for nothing: If thisChatClient.cookie.getCookieData does return a Promise then return it directly, no need to wrap it in a Promise.
And if it returns a synchronous result, then you will only loose by wrapping it in a Promise

async function requestCommunication(topic, customerId) {

  function cookieAvailable() {
    // this is already a Promise
    return thisChatClient.cookie.getCookieData('sess_au');
  }

  const isCookieAvailable = await cookieAvailable();

  if (!!isCookieAvailable) {

  }
}
requestCommunication().catch(console.error);

Now all this can't help to make a proper answer to your question: The link between your two code blocks is not clear at all.

Nothing calls neither functions.

Your getCookieData will be waiting for a MessageEvent, without letting anyone know that it is waiting for it.

I'm not sure how you did plan on letting your iframe know that it should send a message with this information to your window, but that's something you must consider.

But before going there I should note: as tempting as it could be, wrapping events in Promises is generally a bad idea.

Events and Promises are different things, the latter should resolve only once while the former may fire multiple times, and from different sources.

IMM it is only good to do so when you are sure the event will fire only once. With the MessageEvent, you are far from knowing it.
Your user may very well have an extension on their browser that will use postMessage as a means for communication. If this extension is added on all iframes, your code is broken.

Instead, you should check the MessageChannel API, which will offer you mean of communication that you can be sure you'll be the only ones using.
I don't think this answer is the right place to explain how this API works, but have a look at this overview which explains the very basics.

Since you'll be sure to be in control of both ends, from there you may set up a Promise based system.

From your main page, you'll prepare the MessageChannel object, and send it to the iframe while listening for a response. When this response will come, you'll be able to resolve your Promise.

From your iframe, you'll add a listener on the window to trap the MessageChannelPort. When this happens, you'll ask your service for the cookie and send it back through the MessageChannel's port.

Even if a message comes on your main's window during this exchange, you can be sure that it won"t be the one you are waiting for.

// Sets up a new MessageChannel
// so we can return a Promise
function getCookieData() {
  return new Promise((resolve) => {
    const channel = new MessageChannel();
    // this will fire when iframe will answer
    channel.port1.onmessage = e => resolve(e.data);
    // let iframe know we're expecting an answer
    // send it its own port
    frame.contentWindow.postMessage('getCookie', '*', [channel.port2]);  
  });
}

frame.onload = async e => {
  const frameHasCookie = await getCookieData();
  console.log(frameHasCookie);
};

frame.src = generateFrameSRC();

function generateFrameSRC() {
  // The content of your iframe
  const cont = `
<html>
  <head>
    <script>
      const originClean = "null";
      onmessage = async e => {
        // only if it's the origin we expected 
        // and if it does send a  MessagePort 
        // and the message is "getCookie"
        if(e.origin === originClean && e.ports && e.data === "getCookie") {
          const data = await asyncData();
          // respond to main window
          e.ports[0].postMessage(data);
        }
      };
      function asyncData() {
        return new Promise(resolve => 
          setTimeout(() => resolve("the data"), 1000)
        );
      }
    <\/script>
  </head>
  <body>
    hello
  </body>
</html>`;
  return 'data:text/html,' + encodeURIComponent(cont)
}
<iframe id="frame"></iframe>
like image 64
Kaiido Avatar answered Nov 15 '22 11:11

Kaiido