NoSleepJavascript Blog

Š 2022, nosleepjavascript.com
#ibridge#typescript#javascript#iframe#postMessage#event emitter#Emittery#jsdom

Breaking the iframe frontier: ibridge

December 11, 2020 • 9 min read

Written by franleplant: Tech Lead and Software developer with a degree in Engineering. Woodworker, all things Javascript, Rust and Software Architecture. Github

TLDR#

  • I forked postmate and called it ibridge.
  • It uses Typescript and has cool features regarding type safety.
  • I improved the semantics of iparent.get.
  • I made the code simpler.
  • I made both ibridge.Parent and ibridge.Child be Event Emitters for better composition.
  • It has worked super fine!
ibridge logo

ibridge

logo by twemoji

Introduction#

I have recently published ibridge, a tiny, typesafe, promise-based library for bidirectional and secure iframe communication.

What is ibridge#

ibridge let’s you

  • Retrieve data from the child to the parent.
  • Implement complex communication flows between parent and child iframes.
  • Pre establish a communication flow via a simple handshake protocol.

How it works#

ibridge high level flow diagram

  • ibridge is an abstraction on top of postMessage.
  • Exposes ibridge.Parent and ibridge.Child, used by the parent and child document respectively.
  • In here we call their instances iparent and ichild respectively.
  • Both Parent and Child are event emitters implemented via Emittery.
  • Security is handled entirely by native CSP headers.
  • At the initial phase it performs a handshake in which the Parent sends a special message to the child and waits for a special response.

Remote function calls from Parent to Child (the most common flow)#

The Child is able to define a Model which is made off all the functions that live in the Child that can be called remotely by the Parent.

Model can be a trivially deeply nested object, and each Model function can by sync or async, returning a resolved or rejected Promise that will be automatically sent back to the Parent without any surprises. Return values and arguments can be anything that can serializable, plus, return values can be any Promise to a serializable value. Let’s see an example:

try {
  const resolvedValue = await iparent.get(
    "blog.analytics.getPageViews",
    "page1",
    "page2"
  );
} catch (rejectedValue) {
  // handle errors
}

Internally this means an event flow like the following:

1) parent to child

sendToChild({
  type: "GET",
  modelPath: "blog.analytics.getPageViews",
  arguments: ["page1", "page2"],
});

2) child to parent

try {
  const value = await model.blog.analytics.getPageViews(
    "page1",
    "page2"
  );
  sendToParent({
    type: "GET-RESOLVE",
    value,
  });
} catch (error) {
  sendToParent({
    type: "GET-REJECT",
    error,
  });
}

This makes for a pretty standard and easy to understand model, this thing that seems so intuitive was one of the main reasons I departed from the original implementation in postmate, more on this later.

If it feels natural or even dumb then I have succeeded.

Free form communication#

ibrigde also lets you build more complex bidirectional flows via high level events

// Send events to the child
iparent.emitToChild("ping", { value: "i am father" });

// listen to events from the child
iparent.on("pong", (msg) => console.log(msg));
// listen to events from the parent
ichild.on("ping", (msg) => {
  // send message to the parent
  ichild.emitToParent("pong", { value: "i am child" });
});

Internally ibridge has a single event listener for postMessage (the Dispatcher) in both the Parent and the Child that will listen to valid ibridge messages and dispatch them as Emittery events. This allows you to use higher level event emitters abstractions such as once, off, onAny, etc.

Fun fact, ibridge uses also Emittery for the handshake mechanism.

Why ibridge was created#

I have been designing and implementing an sdk to provide consumers (other companies and services) to use our platform (let’s call it Platform A) from outside our own controlled domains.

For different circumstances and limitations I can’t really state publicly we ended up choosing an iframe based solution so that consumers (parent document) could interact through high level abstract APIs with our platform (child document).

This is similar to what other providers already do like Patreon (see link at the bottom), Twitter embedded tweets (see tweet below), etc. This bypasses certain cross domain limitations by using an embedded document.

DANGER ALERT iframes are a delicate topic since there are a lot of security concerns present. Make sure you allow your page to be rendered inside an iframe by controlling the Content-Security-Policy HTTP header. Read more in MDN, and be sure to understand how the browser security model works related to iframes fully before launching to production.

Since most of the work in building such sdk lies on building a good higher level communication protocol between parent and child we started by using an already existing solution called postmate, and it worked well until it didn’t anymore. We tried hacking around it but in the end the implementation was far too noisy because of some core problems in postmate, that’s why I decided to fork it.

Why Postmate was not enough#

I created an issue in Postmate’s repo to see if we can unify efforts eventually.

These are the problems I found with Postmate that made me fork it:

Wrong semantics for iparent.get#

Postmate doesn’t have good semantics for .get, in fact, if you call a child model through Postmate’s .get and that model throws then you won’t receive a failing promise in the parent, in fact that’s one of the main things we had to hack around internally and other’s have opened issues and hacked around too.

One of our main use cases, and when you think about it, it’s probably the main use case for a lot of users; was and is to remotely call functions that live in the child and get the resolved or rejected values with that same semantics in the Parent so we can report back to the parent’s consumer.

ibridge gives better semantics to .get by making it a deconstructed remote function call that handles return values and errors thrown in ways that feel natural, hiding the underlying mechanisms completely.

Wrong semantics and name for iparent.call#

On first read one might think that iparent.call is the main way of remote calling model functions in the child from the parent but is actually not, it just a way of calling model functions just for the side effects.

This makes no sense, we already can cover that with .get, if the consumer doesn’t care about the model’s return function then it simply can be omitted. This is exactly how we handle function calls in regular programs. If we do not care about their return value we just call them for their side effects with the same mechanism we call them when we do care about their return values or errors.

Another use case for .call can be simply emitting events to the child, but it doesn’t have a good name to reflect that and being strictly related to child model functions not always fits the flow users have in mind.

ibridge doesn’t have .call and instead you simply can iparent.emitToParent and we also provide the opposite: ichild.emitToChild and everything is abstracted by relying on Emittery as much as possible.

The Model implementation is just too simplistic#

We very early found ourselves hoping to be able to call deeply nested Model functions but found that postmate only accepted model keys, this means that you need to collapse all your model functions into a single shallow object.

This is too simplistic and prevents users from building more complex model structures.

iparent.get("blogPostsGetPageViews", ...args);

// child model
const model = {
  blogPostsGetPageViews: () => {
    /*...*/
  },
};

ibridge allows .get to accept a lodash’s path to the model, by relying on lodash.get.

iparent.get("blog.posts.getPageViews", ...args);

// child model
const model = {
  blog: {
    posts: {
      getPageViews: () => {
        /*...*/
      },
    },
  },
};

Model context#

Another thing we find ourselves wanting was the capability of providing a simple Model context that could be accessible to all model functions should they need to.

With Postmate you need to roll out your own implementation, but there’s a really easy way of providing a simple context implementation without letting the user roll out their own implementation.

ibridge provides an easy way of passing context to all Model functions.

function myModel(this: IContext) {
  // accessible via this
  return context.doSomething();
}

Debugging#

Postmate used a simplistic approach based on an env variable and a bunch of if statements. This wasn’t ideal since it didn’t work always as expected, required bundler configuration, and the handling was way to manual.

ibridge uses debug for a more structured way of configuring and outputting debug / logs. Check the docs for more information.

Example of enabling verbose output.

// you might need to do this in the child document too
// check the `storage` dev tools tab.

localStorage.debug = "ibridge:*";

This will output a lot of information that should make it really easy to debug the workings of any consumer of the lib and the lib it self.

example debug output

Typescript#

Finally ibridge uses Typescript, this let us express certain things such as that the Child is generic for TModel and TContext and so you can have better validations in terms of type safety.

Additionally I am a big fan of Typescript and the guarantees it provides so I basically do nothing without Typescript.

Closing#

Postmate is a great library that was in need for a little bit of love. I am open to unifying efforts with the Postmate team so that we can have a single version of this library with all the benefits that ibridge brings to the table.

I will also try to give ibridge more support, if you are interested in helping me maintain it let me know!


Like the content? Consider subscribing, buying me a coffee or even becoming a Patreon below.

buy me coffee

Subscribe to our mailing list!