How to Use Js-awe to Avoid Await Contamination

Written by josuamanuel | Published 2023/06/01
Tech Story Tags: javascript | javascript-libraries | functional-javascript | javascript-async-await | javascript-utility | promises-in-javascript | learn-to-code-javascript | learn-to-code

TLDRJS-awe library has a “plan” function that can help you with that. “Plan” is an execution planner that pipe functions to run in sequence. It handles for you the promise chaining and data passing ** so you can write pure functions free of await**.via the TL;DR App

This new functional async style helps you avoid await contamination

Async await has done a lot to improve the readability of code when compared with callbacks style. But sometimes it is not a good construct, especially if you want to use it in a functional style!!!

One problem I see, is the spread of async await around the source code wherever it is handy. This casual handling of await usually makes code non performant.

Every-time we use await, an async branch will be created. The result will be an execution flow with the shape of a tree, with some chain functions running in sequence and others running concurrently.

Understanding the tree of execution flow turns difficult. This is mainly due to the fact that the construct of the tree is not explicitly coded in one place.

The js-awe library has a “plan” function that can help you with that.

“Plan” tries to solve this problem by declaring this tree explicitly in one place and in a simple elegant way.

plan().build([
  fun1,                      
  [fun2A, fun3],
  [fun2B, fun4],
  fun5                  
])

The corresponding running execution flow will be as follow:

       |-> fun2A -> fun3-|
fun1 --|                 |-> fun5
       |-> fun2B -> fun4-|

It uses array nesting to define this tree. It does not use weird DSL; “plan” is an execution planner that pipe functions to run in sequence and functions to run concurrently. It handles for you the promise chaining and data passing so you can write pure functions free of async await.

The construct to run in sequence:

[ fun1, fun2, fun3 ]

execution and data flow coincides:

fun1 -> fun2 -> fun3

* fun1 receives all the parameters when running the plan:
   plan().build(...)(param1, param2)
* fun2 receives output from fun1
* fun3 receives output from fun2

The construct ro run concurrently:

[ fun1, [fun2], [fun3], fun4 ]

execution and data flow coincides:

       |-> fun2 --|
fun1 --|          |-> fun4
       |-> fun3 --|

* fun1 receives all the parameters when running the plan:
   plan().build(...)(param1, param2)
* fun2 receives output from fun1
* fun3 receives output from fun1
* fun4 receives an array that contains two values:
  [outputFromfun2, outputFromFun3]

The best thing is to view an example. First, we need to install it:

npm install js-awe

The below is a simple example of its use. This could be part of an API to get the bank balances of all the holdings (savings and loans) for a specific customer:

import { plan } from 'js-awe'


const getCustomerBalances = plan().build([
  fetchAccounts,
  [filterSavings, getSavingBalances],
  [filterLoans, getLoanBalances],
  format,
])

console.log('result: ', await getCustomerBalances('0396d9b0'))

Execution:

            |->filterSavings -> getSavingBalances -|
getAccounts-|                                      |-> format
            |->filterLoans   -> getLoanBalances   -|

Flow of data:

  • Return values from functions are passed to the next function to run, in a pipe style way.

  • When the return value is a promise, the planner will wait for its resolution before calling to the next function.

You can see the whole example here:

import { plan } from 'js-awe'

const getCustomerBalances = plan().build([
  fetchAccounts,
  [filterSavings, getSavingBalances],
  [filterLoans, getLoanBalances],
  format,
])

console.log('result: ', await getCustomerBalances('0396d9b0'))

function filterSavings(accounts) {
  return accounts.filter((account) => account.type === 'saving')
}

function getSavingBalances(savingAccounts) {
  const listOfAcccountsToFetch = savingAccounts.map((account) => account.id)
  return fetchSavingBalances(listOfAcccountsToFetch)
}

function filterLoans(accounts) {
  return accounts.filter((account) => account.type === 'loan')
}
function getLoanBalances(loanAccounts) {
  const listOfAcccountsToFetch = loanAccounts.map((account) => account.id)
  return fetchLoanBalances(listOfAcccountsToFetch)
}

function format([savingBalances, loanBalances]) {
  return [...savingBalances, ...loanBalances]
}

// Data fetch services are mocked for local running.
// In production they should be fetch APIs to real implementations.
function fetchAccounts(customerId) {
  return Promise.resolve([
    { id: 1, type: 'saving' },
    { id: 2, type: 'loan' },
  ])
}

function fetchSavingBalances(listOfAcccountsToFetch) {
  return Promise.resolve([
    {
      id: 1,
      type: 'saving',
      balance: 13,
    },
  ])
}

function fetchLoanBalances(listOfAcccountsToFetch) {
  return Promise.resolve([
    {
      id: 2,
      type: 'loan',
      balance: 24,
    },
  ])
}

The Plan utility is recommended when we have a complex tree, and you want to manifest explicitly this async flow. For example, This utility would be a good tool for an API that generate its response based in different calls to other APIS. Specially if some of the calls needs to be call in sequence and others can be run concurrently.

When it is not recommended:

  • Simple async flows. Introducing another tool to learn may not be worth it.
  • You have the skills to do it better yourself: more flexible, readible and performance.
  • You are tired of new libraries, frameworks and abstractions. I get it!
  • You are happy with your current approach.


Written by josuamanuel | Always learning and improving...
Published by HackerNoon on 2023/06/01