Poppy.io / An Introduction

Poppy I/O Icon
Poppy I/O: An Introduction

Poppy I/O is a JavaScript framework for sending data between web apps client-side through the user's web browser. It works by having one page (a client) launch another (a service) in a popup window (a "poppy"—hence "Poppy I/O"), connecting the two through a common cross-document messaging protocol.

This introduction is an interactive demo that lets you try it out and see what it looks like in code. It was written in May 2022 as part of the initial 0.1.0 release of Poppy I/O.

It's got three parts:

Note: This page uses some relatively modern JavaScript that won't work in older browsers, but Poppy I/O itself is tested to work in IE 11 (with a Promise polyfill). Also, this project isn't affiated with or endorsed by Flickr or Imgur or any other person, project, or organization that might be referenced in this page (aside from the author).

Part I: Pick an Image

For this first part, we are going to select a photo from the Flickr Commons to bring into this page. The Flickr Commons is a collection of archival photographs with no known copyright restrictions, so we should be free to do whatever we want with the photo you pick when we get to Parts II and III.

This page you're currently on will function as the client, on the domain . We'll be using Poppy I/O to launch and transfer data to and from various service pages in popup windows—all of which, importantly, will be on different domains, the first being poppyio-commons-picker.glitch.me.

To get started, click the button below to launch the Commons photo picker:

(After you pick an image, it'll go here)

(After you pick an image, the origin of the image will be inserted here)

Hopefully that worked for you and you see the image you selected in the space above. Now let's take a look at the code. First a little setup:

Note 1: Packaging and ModalRequest

Purely for convenience we're using a script bundle that sets up the poppyio global variable for this intro, specifically

The more modern thing to do would have been to import it as an ES module, where the equivalent would look like this:

And the more "production grade" thing would have been to use the poppyio package in npm and run it though a bundler.

The ModalRequest class is the main thing we're interested in as a client; we extract that into its own global out of poppyio to make it look a little nicer.

The en in the bundle and inject-en import have the effect of configuring a default version of two UI elements with English-language strings (You can supply your own versions of these and not use the default):

  • A Modal Overlay, which covers up the client page while the poppy is open making only option available to the user canceling the request and closing the poppy.
  • A Launcher used to allow the user to select a Poppy I/O service where one isn't explicitly specified by the code (this gets used in Part III).

ModalRequest.prepareProxy() we discuss in Note 2

Note 2: Proxy iframes and ModalRequst.prepareProxy()

This helps work around some popup blockers, in particular Chrome on Android (at least as of May 2022).

The issue is that we don't open up the poppy popup window directly from the client page's window. Instead we use an intermediate proxy iframe that's sandboxed to prevent the poppy service from changing the client page URL behind the user's back.

It turns out creating the proxy iframe at the same time we open the popup can somehow taint the iframe causing the popup to be blocked. To work around this, we just have to create the iframe ahead of time, which is what ModalRequest.prepareProxy() does. Note each time we open a poppy we create a new proxy (and destroy it on close), this just sets up the first one.

The method takes an optional parameter, the parent element under which to create the proxy (and modal overlay); by default it's added to the end of the body.

With that out of the way, here's the button onclick handler:

Note 1: ModalRequest constructor

The ModalRequest class manages the "poppy" (the popup window hosting the service page). While the request is open we try to make it act like a modal dialog - making sure only one is open at a time, automatically closing the popup when the user leaves the client page, and covering up the client page with a modal overlay that restricts the user to only cancelling the open request and closing the poppy.

(The rationale for all that is to make it harder for the user to lose track of the poppy and have extra popup windows they're not sure what to do with floating about...)

The ModalRequest constructor takes an optional parameter indicating the URL of the service page we want to load into the poppy. If no service URL is specified, a "launcher" is presented to allow the user to specify a service to use. As with the modal overlay, a default implementation is included that you can replace.

Creating a ModalRequest doesn't open the popup (or have any side effects at all) - to open it use the open() method, described in Note 3.

Note 2: Accepting Intent

An Intent object specifies what a page on one side of a Poppy I/O exchange wants from the page on the other side, and how it will handle it. In this case, what the client wants from the service.

Every Intent is either accepting or offering something, indicated by the presence of an accepting or offering property on the Intent object (but not both), with the value being a string or array of strings indicating the form of the exchange we are able to accept or offer. The string can be any value, so long as both sides agree on what they mean. Here in Part I we use content-download as the form (see Note 7 for more), in Part II we use edit-blob, and in Part III we use content-blob.

The page opposite (the service) will present its own matching intent objects - accepting a form we are offering or offering a form we are accepting.

The having property is optional and is an object indicating further details of the exchange, such as the content-type of the data. Its meaning is specific to the form and is not interpreted by the Poppy I/O library, but is simply passed through to the peer. This may be counter-intuitive: if you are a client accepting an image/* content-blob for instance and the service is offering an audio/mp3 content-blob, then the Poppy I/O library won't prevent the exchange even though it doesn't make sense. But consider:

  • A service is able to inspect the having value sent by a client and see whether or not it's able to reasonably handle the request, and can inform the user if it isn't able to.
  • A client (or a service) should always be prepared to handle unexpected data coming from a peer—Poppy I/O doesn't perform any data validation for you.

It's not used here, but an accepting intent may send a reply back to the offering page using the replying property. This may be a fixed value, but more usefully it can be a function that returns the reply value (or a promise that resolves to the reply value). The function will take a single PeerOffer parameter, an object with 4 properties:

  • matched - the matched PeerIntent (See Note 5)
  • data - the "arrayified" offer data (See Note 4)
  • ports - an array of MessagePorts included with the offer, for when a single reply/response is not sufficient. The ports will be closed automatically when the replying() completes (when the returned promise is resolved/rejected if asynchronous)
  • closing - promise that resolves when the request is closing (and there's no point waiting on any asynchronous operations anymore)

Instead of a replying() function an accepting intent may alternatively specify a using() function, which receives a second parameter - a postReply() function used to send the reply which additionally accepts a second transferList array parameter allowing Transferable objects to be sent with the reply.

Note 3: ModalRequest.open()

To actually open the poppy and request the exchange call ModalRequest.open() and pass it the intent object - or objects; you can also pass an array of intents to provide more than one option, in which case one matching object will be selected.

The open() method returns a Promise that resolves to the result of the exchange after it's complete. Note that the popup may still be open at that point; if you want to wait for when the popup is closed (to perform an animation for instance), await ModalRequest.closing instead.

Because we're opening a popup window, ModalRequest.open() needs to be called synchronously with respect to a user gesture (like pushing a button) in order to avoid the popup being blocked.

See Note 4 for more about the result object.

Note 4: The Match Result Object

The promise ModalRequest.open() returns resolves to a MatchResult object with 1 or 2 properties. There's always a matched property, which may be:

  • null, if there wasn't a successful match (for instance the client was accepting a content-download but the service could only offer a content-blob)
  • A PeerIntent object, if there was a successful match. See Note 7 for more about the PeerIntent object.

In the second case, where a match was successful and matched is not null, the object additionally has a value property with the result of the exchange. For a basic intent with no using() callback, the result will be the data recieved from the peer (the offered data on the accepting side, or reply data on the offering side), arrayified.

"Arrayification" is just making it so that the data received is always accessed through an array, the process being:

  • If the offering side sends an array, then the value is a new array with the same elements as the received array.
  • If the offering side sends null or undefined, then the value is an empty array.
  • Otherwise, the value is a single-element array with single element being the sent object.

The array has an additional raw property allowing you to get the original value recieved, pre-arrayification.

For "Posting" intents (those which have a using() function that calls postOffer() or postReply() to send data to the peer) the result value will be whatever the using() function returns.

Note 5: PeerIntent objects

As mentioned in Note 4, if the exchange was successful, the matched property of the result will be a PeerIntent. A PeerIntent represents the matched intent on the peer web page.

A PeerIntent object has 4 properties:

  • origin - a string, the web origin of the page
  • side - either the string accepting or offering, which will be the opposite of our side.
  • form - a string, the form of the exchange offered or accepted. In contrast to a submitted intent which may have an array of forms, only one matching form will be in the peer intent.
  • having - an object, equivalent to the having object in the submitted intent.
Note 6: Validating Data

As mentioned in Note 5, with the simple accepting intent we have, the result.value will be an array (with an additional raw property).

Although the exchange was successful, so we should expect at least one value in the array, that isn't guaranteed. Poppy I/O doesn't perform data validation for you. It's just a generic way of connecting pages together.

The if condition validates the result is usable:

  • The first part, !result.value[0] checks that:
    • There's at least 1 element in the array (accessing a non-existent array element isn't an error in JavaScript - it evaluates to undefined)
    • The first element in the array isn't null or undefined - both are "falsy" values
  • One the first part passes, we check result.value[0].download !== 'string' to make sure the URL is a string. (The URL may not be valid, but rather than validate that here we just try to load it and let it fail). See Note 7 for more about the download property in a content-download offer.
Note 7: content-download form

In content-download form, data is passed by a download URL (as opposed to a share URL or hotlink URL). It's expected to be usable from client-side JavaScript directly, so it must be served using HTTPS with appropriate CORS headers. But it's not expected to be long lived and can expire shortly (although that is not the case here.)

To allow for additional metadata to be included with the URL, it's not sent directly but is wrapped in an object with the URL value being in the download property, for instance:

{
	download: "https://example.com/hello-world.txt",
	title: "A greeting"
}

Part II: Transform It

In Part I we had a one-way transfer from the service to the client, now we'll do a two-way transfer - from the client to the service and then from the service back to the client - to take the image you selected and transform it into something else.

For this I took two existing projects I thought were pretty cool and then hacked them up quickly to make them function as Poppy I/O services. If anything is weird or broken, definitely blame me and not the original authors!

primitive.js

First we have primitive.js, by Ondřej Žára and based on the original Primitive in Go by Michael Fogleman. What this does is take an input image:

Image of Space Shuttle Launch

And then try to re-produce it by building an image up from primitive geometric shapes, giving it an abstract look. This for instance is with 145 triangles:

Reconstructed Image of Space Shuttle Launch

jpg-glitch

And then we have jpg-glitch by Georg Fischer. This is kind of the opposite of Primitive in that instead of building an image up, it takes an input image:

Man on the Moon

And then tears it down, rendering it as a JPEG and then corrupting it slightly (or not-so-slightly) to make it glitch:

Glitched Man on the Moon

The actual effect seems to depend on the JPEG rendering of the browser used to produce the glitch, subjectively I think it's a lot less interesting looking when run on Safari than on other browsers (I suppose on iOS it all looks the same), but maybe you'll think different.

Try It

If you're not sure which one to try I suggest jpg-glitch first since it's faster.

For reference, here's the original image again:

(After you pick an image, it'll go here)

(After you pick an image, the origin of the image will be inserted here)

And here it is after you run one of the 2 options above:

(After you edit the image, it'll go here)

(After you edit the image, the origin of the editor will be inserted here)

And now for the code:

Note 1: Offering an edit-blob

The simple way to offer an object is to use sending, the value of which can be:

  • An object to send
  • A promise that resolves to an object to send
  • A function that returns an object or promise that resolves to an object to send.

The function gets a single parameter, a PeerAcceptor with two properties: matched, PeerIntent with the details of the matching intent (described in part one), and, and closing, a Promise that resolves when the request is closing.

Because opening a ModalRequest involves opening a popup window, you can't wait for an asynchronous operation in between the user gesture requesting a poppy be opened and the opening of a poppy, instead you should use a promise or asynchronous function returning a promise as the sending value.

In this case the form we're using is edit-blob, where we send a blob we want the service to transform, sending us the transformed blob in the reply. As with content-download the blobs (both ways) are wrapped in an envelope object with the Blob value in the blob property, for instance:

{
	blob: new Blob(["Hello, World!"], {type:"text/plain"},
	title: "A Greeting"
}

We expect a reply with the same shape (but of course need to be prepared for the case where the reply is not)

Note 2: Common Interfaces

Both services support the same interface (edit-blob form) with the only difference being the service URL.

This makes it easier to integrate new services and also potentially allows users to specify entirely new services to use themself, demonsrtated in Part III.

Part III: Share It

In Part II we sent an image out to a service to get another back in return. So naturally you can also use Poppy I/O to send an image out and then just like... not get something back.

Let's do that and publish your image to Imgur for the world to see. But instead of specifying the service URL to use in the code, let's let the user (i.e. you) pick the service they want to use.

Service Discovery

There are a number ways that could work—a very simple option would be to just have the user enter the service page URL. More user-friendly might be a big directory or app store kind of thing with a whole bunch of services to choose from (all zero of them).

What's actually implemented is a simpler (for the user) variant of the former, where instead of entering a full service page URL, you just enter a domain name and the code will do a little extra work to figure out exactly what the service page URL is.

For instance, if a user enters i3r.poppy.io, it will:

  1. Make a request to https://i3r.poppy.io/.well-known/host-meta.json
    • The request is always made over HTTPS
    • To allow client-side code to make the request directly, /.well-known/host-meta.json must be served with appropriate CORS headers for access from any origin
  2. Expect host-meta.json to be a JRD document. Within it, we find the first link with a rel of https://purl.org/pio/a/ModalService
  3. If one exists, navigate to the service URL in the href. Otherwise, display an error message.

A nice property of this is that if you enter a domain for a service that doesn't exist then we can detect that and stop you rather then sending you off and letting you fend for yourself. A not so nice property is that you might make a typo and enter a domain for a service that does exist... but only to trade your data for Satoshis.

So be careful.

Running The Service Yourself

So right now i3r.poppy.io is the only service currently operational. But you can run it yourself, on your own domain, making it kinda like another service. All you need is an Imgur Client ID, and it's pretty simple to deploy it to Heroku for free, or run it locally (using something like ngrok to take care of SSL).

If you want to give it a go, check out poppyio-i3r on Github.

Try It

When prompted for the domain, enter i3r.poppy.io or that of your own running instance. After you finish, the link to your image on Imgur and a link to delete it if you're so inclined will be inserted here:

And here's the code:

Note 1: Opening a ModalRequest without specifying a service

We don't specify a service to use, which causes the ModalRequest to open a launcher instead. In this case, we're using a default implementation included with the library which simply shows an input to allow the user to specify a service domain to use. (The process used to find the service page URL based on the domain is outlined above).

The launcher is configurable by specifying the launcher property of ModalRequest, and can be configured globally by setting ModalRequest.prototype.launcher, as the inject-en.js module does when we import it from setup.js (in Part I).

Note 2: Reply Links and Validation

I don't have a firm idea of what to send back from a one-way object offer, but one thing that seems useful is a list of links to where the data now lives.

Remember Poppy I/O doesn't do any data validation and there's nothing stopping the peer page from sending a javascript: link for instance to perform a cross-site scripting attack. So we make sure it's at least an https:// link before inserting it into the page.

Basically treat anything you receive from Poppy I/O as you would any untrusted data from an external service.

Final Remarks

Well! Hopefully this introduction gave you a good sense of what Poppy I/O is all about. There's a lot more we could go over, like more "advanced" usage beyond sending simple objects back and forth, the idea of connecting clients together peer-to-peer, the details of the protocol itself, integration with browser extensions, some discussion of safety and security...

Until I get around to that you can try filling in some of that on your own by looking through the code of Poppy I/O itself which you can find here on Github. Maybe also check out:

Being at 0.1.0 neither the API nor underlying protocol are set in stone and I am very happy to receive your suggestions for breaking improvements.

Finally I thought I should mention what you might call "prior art", although I don't think that's entirely accurate to say since I've been playing around with this idea for an embarassingly long time—the oldest related artifact I can find online is this page which has an HTTP Last-Modified date of Wed, 06 Oct 2010 02:40:08 GMT (I think it was me experimenting with UI code). So, "parallel art" maybe?

So yeah, I guess we'll see where that goes. If you want to keep up to date in case there's anything new with Poppy I/O consider following my currently very empty Twitter, @paulgaspardo, and if you have any issues or suggestions or questions feels free to open up an issue or discussion on the main Poppy I/O repository on Github.

Thanks :)


Paul Gaspardo
May 2022