hyper is an API on top of services (data, cache, search, etc) and one of the most powerful capabilities of hyper is the ability to compose services.

Composing is the combining multiple effects together in one pipeline of processing.

Movie Review Example

We are building a movie review application and one of the functional requirements is to allow users to react to movie reviews with either a like or dislike vote. Which leads to another functional requirement, the ability for every viewer of the review to see how many likes and dislikes the review received. To satisfy the second feature, we could create a query to retrieve all of the reactions for a review then count all the likes and dislikes, but overtime for every view of a review, this seems expensive.

Caching reactions

Let's cache the reactions into a counts object, this way every time a reaction is created we increment the cache. And when we view a review, we attach the cached counts document to the review document. This can save time and performance at scale.

For every recorded reaction lets make a SET call to our cache updating the count and the like or dislike properties.

{
  "count": 3,
  "like": 2,
  "dislike": 1
}

Then when we request a review, we will append a counts property on the review object.

{
  "id": "...",
  "movieId: "ghostbusters",
  "rating": 5,
  "summary": "....",
  "author": "bob",
  "counts": {
    "count": 3,
    "like": 2,
    "dislike": 1
  }
}

This will provide the client developer the total number of reactions as well as number of likes and dislikes for the review, with out having to burden the data service to perform an expensive lookup every time a review is viewed.

Lets view the code

During the creation of a reaction (i.e. the viewing user submits a like or dislike)

Async.of(reaction) 
      .map(createId)
      .chain(validate)
      .map(assoc('type', 'reaction'))
      .chain(reaction => Async.all([
        services.data.create(reaction),
        services.cache.inc(`review-${reaction.reviewId}`, reaction.reaction)
      ]))

As we save the reaction record, we call the cache inc function, giving it the review id and the reaction.

In our service layer we create the inc function:

function increment(id, prop) {
  return getFromCache(id)
    .coalesce(
      always({count: 1, [prop]: 1}),
      compose(
        over(lensProp('count'), inc),
        over(lensProp(prop), inc)
      )
    )
    .chain(v => set(id, v))
}

First we get the object from cache if it already exists, otherwise we create a new one. If one exists, we increment the count and the like or dislike depending on the reaction. Finally, we save it back to the cache.

During the retrieval, we simply get the latest cached object:

function get(id) {
    return services.data.get(id)
      .chain(review => services.cache.get(`review-${id}`)
        .coalesce(() => review, counts => assoc('counts', counts, review))
      )
      .chain(validate).bimap(e => ({status: 404, message: 'Review Not Found'}) , identity)
  }

As we get the review document, we also get the cache document, and if it exists we attach it to our review document.

Summary

with hyper we can easily create aggregate data objects and put them in our cache to retrieve without having to make expensive queries on the data store. This can save processing cycles at scale, which results in a better performing application and a happier client.

Having trouble reading the code? Don't know what a Async ADT is or lensProp?
* https://crocks.dev - check out crocks for info on Async, coalesce, chain, bichan
* https://ramdajs.com - check out ramdajs for info on compose, over, lensProp