Using Highland streams for event handling

To use HighlandJS in your project using Bower:

$ bower install --save highland

Example:

var $ = require('jquery');
var _s = require('highland');

_s($.when($.ready))
.flatMap(function ($) {
return _s('click', $('.select-product'));
})
.debounce(1000)
.map(function (e) {
return $(e.target).data('product');
})
.flatMap(function (product) {
return _s($.getJSON('/products/' + product))
});
.errors(function (err) {
console.error('Error: %s', err);
})
.each(function (response) {
$('.product-selected-list').append(response.html);
});

This is equivalent to:

var $ = require('jquery');
var _ = require('lodash');

$(function () {
$('.select-product').on('click', _.debounce(function (e) {
var product = $(e.target).data('product');
$.getJSON('/products/' + product)
.fail(function (err) {
console.log('Error: %s', err);
})
.done(function (response) {
$('.product-selected-list').append(response.html);
});
}, 1000));
});

Although the code in the non stream based version is shorter it is harder to reason about because of the way it is nested. Initially, stream operations like flatMap may seem alien and difficult to decipher in the stream based version.

Basically we are saying

  1. After the document is ready
  2. On click for the element .select-product
  3. Wait a second (1000 ms) to check that there are no further additional clicks
  4. Retrieve the product identifier from the last clicked element’s data attributes
  5. Use this to request the product data from the server
  6. If there are any errors log them to the console
  7. Append the html in the included response to the .product-selected-list element.

The flatMap operation, enables you to map values to streams. The return value for the passed function should be a stream, flatMap will then merge all of the results of those returned streams as a single stream.

The first flatMap (above) is a bit more theoretical than the second as we know there should only be one document ready event as a value of that stream, therefore, we would only expect the flatMap operation to be called once. You could see it as we are replacing the original document ready stream with the click stream.

The second flatMap operation, receives the stream of product identifiers and we are returning a stream that will return a response, effectively resulting in us having a stream of streams.

For example if we were to use the basic map operation we would have:

Stream[ProductIdentifier] -> map(ProductIdentifier => Stream[Response]) -> Stream[Stream[Response]]

The flatMap operation flattens this stream of streams, effectively merging all of the streams into one single stream:

Stream[ProductIdentifier] -> flatMap(ProductIdentifier => Stream[Response]) -> Stream[Response]

In fact flatMap is equivalent to:

Stream[ProductIdentifier] -> map(ProductIdentifier => Stream[Response]) -> Stream[Stream[Response]] -> sequence() -> Stream[Response]

Where sequence is an operation that flattens a stream of streams.

In a follow-up I’ll go into details as to how using stream can result in more consise and usable patterns and how back pressure can be used to manage demand responsively and without requiring large amounts of managment code and intervention.

Stuart Wakefield

Software engineer and musician. I like graphic novels, illustration and games. I dabble with digital art and game development.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s