The Magic of Serializing Functions

  • Between different threads or processes
  • Between different iframes or windows
  • Between a web page and a web-worker or service-worker
  • Between a client application and a server, via a web-socket

Basic Fire-and-Forget Serialized Messages

  • Serializing data to a string is thread-safe. We aren’t risking any window modifying the state of another.
  • Event-driven, asynchronous messaging allows for one window to send and receive messages without blocking the main thread.
  • We also have some basic security mechanisms that allow us to verify the sender and receiver of each message.

It ain’t great though

  • Messages are fire-and-forget. How do I know if my message arrived and was handled, or failed to get through or errored out?
  • Messages don’t have responses. I can just send another message back as a response, but it’s tricky to correlate that response to the original request. There is no “one request, one response” guarantee, like with an HTTP request.

The alternative: Request and Response

  • It acknowledges that the original request was actually received and acknowledged
  • It gives an opportunity for the receiver to send back some response data that is relevant to the original request
  • For every message the sender sends, generate a unique “request id”
  • Store that request id in the sender’s memory or some other state store
  • Include the request id in the request message from the sender.
  • Include the same request id in the response message from the receiver
  • When the response arrives, the sender can then look up the request id, correlate it with the original request, and then call a success handler function for that request
  • We can easily send and receive requests and responses without worrying about the sequence or timing of messages coming to-and-from each window.
  • We now have a 1:1 relationship between a request and response. For example, in the response handler above, we can be sure the user data we received correlates with the userID we passed in the original request.
  • We can handle errors (or time out the request after a delay) if the other window doesn’t — or can’t — respond.

A quick note on security

  • Am I sending messages to the party I expect?
  • Am I receiving messages from the party I expect?

Problem solved?

  • The interface is still a little cumbersome; for example determining the window and/or domain I want to send messages to or receive messages from, and having to deal with global listeners and global event names.
  • Everything is still scoped in a very “global” way. Listeners are global to a window, and as such the event name for each listener must be totally unique. This makes it tricky to set up ad-hoc throw-away listeners for a specific use case.
  • I may also need to listen for messages from many different sources at once, for example I may need to communicate with many different iframes, or many other processes, or many web-socket clients. To do this, I have a lot of work to make sure listeners are correctly set up for each potential sender, then to juggle all incoming requests from all of those senders and make sure they are authorized to get the data or perform the action they are requesting.

A solution: Serialized Functions

  • I call a function and pass some arguments. This is like “making a request”
  • The function returns a value. This is like “sending a response”

What’s stopping us?

  • First, functions (at least in JavaScript) can be both synchronous or asynchronous. We’re going to have to mandate “asynchronous only” here, since any messaging behind the scenes can typically only be asynchronous. That’s easy enough in JavaScript: we can just mandate that a function will only ever return promises.
  • Second, it’s not enough just to stringify a function and send it over the wire to another window, since that will not help us call the original function back in the original window. We need a way for functions to actually send messages and receive responses. That would normally be tricky, but we’re in luck! We already did most of the work above to invent a messaging protocol to send requests and receive responses, so we can just re-use that!
  • Third, we don’t want to create any functions that can be arbitrarily called by anyone — especially layers we don’t trust. We want to share functions only with trusted layers, allowing only those specific layers to call those functions, and no others.
  • The calling window calls a fake “serialized function” wrapper and passes in arguments. (This is a function that was previously passed over from the other window)
  • Calling that function sends a message to the receiver window. This message includes an identifier for the function, and the serialized arguments which should be passed into the function.
  • The receiver window looks up the original function based on the identifier, and deserializes the arguments.
  • The receiver window then calls the original function with those arguments, and that function then returns a value.
  • That return value is serialized, and sent in a response message back to the calling window.
  • The calling window deserializes the response, and resolves the promise with the deserialized return value.

Another quick note on security:

  • When you pass a function to a window, that function can only be called by the exact window and domain that the function was originally passed to. All you need to ask is “Am I passing this function to the window and domain I expect to call it in future?”. If not: don’t send it.
  • Sending small self-contained functions can actually limit potential misuse, since it is easier to make functions as specific and tightly-scoped as you like. For example, in the above example, the logout function can only be used to log out that one specific user. We didn’t need to accept an id parameter, since we already had the user’s id in closure scope when we created the logout function. So the function is much more specific than a generic, global event listener.

In the real world

In Conclusion

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store