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