npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

composable-flows

v1.0.3

Published

Compose flows with readable interface

Downloads

15

Readme

Composable Flows

Compose flows and use cases using functions.

Check docs.

import { Flow } from 'composable-flows'

function emailValidator(email: string) {
  return true
}

class EmailSender {
  async send(email: string): Promise<string> {
    return Promise.resolve(`E-mail sent to ${email}`)
  }
}

const flow = new Flow([emailValidator, new EmailSender().send])

;(async () => {
  await flow.execute('[email protected]')

  await flow.allOk((resultValues) => {
    console.log(resultValues)
  })
})()

Import/Require

Import

import { Flow, FlowMode } from 'composable-flows'

Require

const { Flow, FlowMode } = require('composable-flows')

Browser

<script src="dist/browser/index.js"></script>

Options

| Option name | Default value | Description | Possible values | | | ------------ | ---------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------- | --- | | isStoppabble | false | If flow should stop when an error occurs | true,false | | | isSafe | true | If should not thrown an exception when an error occurs | true,false | | | mode | FlowMode.DEFAULT | Defines how flow will be executed. DEFAULT: the stages run with same parameter passed on execute method. PIPELINE: the result of a stage is passed as parameter of next stage. | FlowMode.DEFAULT, FlowMode.PIPELINE | |

Passing parameters

A Flow allow you to pass a single parameter to all stages in a single execute call. The same parameter is injected in all stages.

// a parameter passed on `execute`
await flow.execute('[email protected]')

// is injected on each stage
function emailValidator(email: string) {
  // email is '[email protected]'
}

class EmailSender {
  async send(email: string): Promise<string> {
    // email is '[email protected]' also
  }
}

Using promises?

The execute method returns a promise.

// using `then`
flow.execute('[email protected]').then((result) => {})

// or using async/await
const result = await flow.execute('[email protected]')

Stages

Each step of flow (each item of the array) is called Stage. Stages can be functions or methods.

// a simple function
function simpleFunction(email: string) {}

// a method
class Example {
  method(email: string) {}
}

const example = new Example()
const flow = new Flow([simpleFunction, example.method])

Callbacks

Flow comes with callbacks to handle the flow result. This is useful to leverage exeception handling and increase code readability.

Success callbacks

Get the result of all stage when they all run successfully

await flow.allOk((resultValues) => {
  console.log(resultValues)
})

Get the result of stages, when having any stage that run successfully

await flow.anyOk((resultValues) => {
  console.log(resultValues)
})

Get the result of a specific stage by its index

const flow = new Flow([emailValidator, new EmailSender().send])

const index = 0
await flow.ok(index, (resultValue) => {
  //resultValue is the result of emailValidator
  console.log(resultValue)
})

Get the result of a specific stage by its name

const flow = new Flow([
  emailValidator,
  { 'send email': new EmailSender().send },
])

await flow.ok('send email', (resultValue) => {
  //resultValue is the result of EmailSender.send
  console.log(resultValue)
})

Error callbacks

Get the errors of stages when they all fail

await flow.allFail((errors) => {
  console.log(errors)
})

Get the errors of stages, when having any stage fails

await flow.anyFail((errors) => {
  console.log(errors)
})

Get the result of a specific stage by its index

const flow = new Flow([emailValidator, new EmailSender().send])

const index = 0
await flow.fail(index, (error) => {
  // error is the error of emailValidator
  console.log(error)
})

Get the error of a specific stage by its name

const flow = new Flow([
  emailValidator,
  { 'send email': new EmailSender().send },
])

await flow.fail('send email', (error) => {
  //error is the error of EmailSender.send
  console.log(error)
})

Get the result without using callbacks

If you don't want to use the callbacks and wants to handle the result by yourself.

const result = await flow.execute('[email protected]')
console.log('result', result)

The resulting log is:

{
  result: StageResult {
    isError: false,
    error: undefined,
    value: 'E-mail sent to [email protected]'
  },
  resultAll: [
    IndexedStageResult {
      isError: false,
      error: undefined,
      value: true,
      id: 0
    },
    IndexedStageResult {
      isError: false,
      error: undefined,
      value: 'E-mail sent to [email protected]',
      id: 1
    }
  ]
}

Multiples callbacks

You can use multiples callbacks in the same flow.

const flow = new Flow([emailValidator, new EmailSender().send])

await flow.allOk((resultValues) => {
  console.log(resultValues)
})

await flow.anyFail((errors) => {
  console.log(errors)
})

Callbacks as promises

Each callback call is a promise. This is because depending on you flow, you can wait or not for the callback completion. Let's suppose you have an express.js controller like that:

app.get('/', async function (req, res) {
  const flow = new Flow([emailValidator, new EmailSender().send])
  await flow.execute('[email protected]')

  flow.allOk((resultValues) => {
    // This will be called when all stages run
    res.json({ resultValues })
  })

  flow.anyFail((errors) => {
    // This will be called when any stage throws an exeception
    res.status(400).json({ errors })
  })
})

This will work fine with or without the await keyword on flow.allOk and flow.anyFail.

But, let suppose you run an asynchronous code inside of each callback:

function notifyUser(value) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(`[NOTIFY] ${value}`)
      resolve()
    }, 200)
  })
}

const flow = new Flow([emailValidator, new EmailSender().send])

await flow.execute('[email protected]')

flow.allOk(async (resultValues) => {
  console.log('allOk', resultValues)
  await notifyUser('allOk')
})

flow.anyOk(async (resultValues) => {
  console.log('anyOk', resultValues)
  await notifyUser('anyOk')
})

console.log('done')
res.send({ status: 'ok' })

The resulting log will be:

allOk [ true, 'E-mail sent to [email protected]' ]
anyOk [ true, 'E-mail sent to [email protected]' ]
done
[NOTIFY] allOk
[NOTIFY] anyOk

That means, the notifyUser function is being resolved only after the response has been sent.

If you want to wait the callbacks, before sending the response, you must add the await keyword on the callbacks, like that:

  const flow = new Flow([emailValidator, new EmailSender().send])

  await flow.execute('[email protected]')

  await flow.allOk(async (resultValues) => {
    console.log('allOk', resultValues)
    await notifyUser('allOk')
  })

  await flow.anyOk(async (resultValues) => {
    console.log('anyOk', resultValues)
    await notifyUser('anyOk')
  })

  console.log('done')
  res.send({ status: 'ok' })`

And the resulting log will be:

allOk [ true, 'E-mail sent to [email protected]' ]
[NOTIFY] allOk
anyOk [ true, 'E-mail sent to [email protected]' ]
[NOTIFY] anyOk
done

Thus, this will make the callbacks to be resolved, before continuing the execution.

Exception handling

By default, Flow will never thrown an exception. To handle exception and get the errors, you should use the proper callbacks: fail, anyFail and allFail.

But, if you want the handle the exception by yourself, you just simply need to pass the option isSafe: false and use try/catch for that.

If isSafe is false and an exeception occurs, the flow will be interrupted.

function emailValidator(email: string) {
  console.log('>> 1. validating email', email)
  throw new Error('Error on validating user')
}

class EmailSender {
  async send(email: string): Promise<string> {
    console.log('>> 2. email sent')
    return Promise.resolve(`E-mail sent to ${email}`)
  }
}

const options: FlowOptions = {
  isSafe: false,
}
const flow = new Flow([emailValidator, new EmailSender().send], options)

;(async () => {
  try {
    const result = await flow.execute('[email protected]')
    console.log('result', result)
  } catch (error) {
    console.log((error as Error).message)
  }
})()

Note

// This
new Flow([], { isSafe: true })
// is equivalent to
new Flow([])

Stop the flow

By default, if an exception occurs, the flow will never stop, unless you say so with isStoppable: true.

function emailValidator(email: string) {
  console.log('>> 1. validating email', email)
  throw new Error('Error on validating user')
}

class EmailSender {
  async send(email: string): Promise<string> {
    console.log('>> 2. email sent')
    return Promise.resolve(`E-mail sent to ${email}`)
  }
}

const options: FlowOptions = {
  isStoppable: true,
}
const flow = new Flow([emailValidator, new EmailSender().send], options)

;(async () => {
  await flow.execute('[email protected]')

  // EmailSender().send was never executed since emailValidator throws an exeception

  await flow.anyFail((errors) => {
    console.log(errors)
  })
})()

Pipeline mode

By default, the parameter passed on execute function, is used to call each stage.

With PIPELINE mode, the result of a stage is used as parameter of the next stage.

In this mode, the parameter passed on execute is used only in the first stage.

Also, in PIPELINE mode, if an exception occurs, the flow is interrupted. Since the result of a stage is used as parameter of next, in case of exeception, the next stage cannot have the result of a failed stage.

So, in PIPELINE mode, isStoppable is always true.

import { Flow, FlowMode } from 'composable-flows'

interface UserInput {
  email: string
}

interface User {
  id: number
  email: string
}

interface Event {
  name: string
  datetime: Date
}

export class GetUserInfo {
  get(userInput: UserInput): User {
    console.log('1. getting user information:[%s]', userInput.email)

    // this result will be input of EmailSender.send
    return {
      id: 1,
      email: userInput.email,
    }
  }
}

export class EmailSender {
  async send(user: User): Promise<Event> {
    console.log('2. sending email:[%s]', user.email)

    // this result will be input of Database.storeEvent
    return Promise.resolve({
      name: 'email',
      datetime: new Date(),
    })
  }
}

export class Database {
  async storeEvent(event: Event): Promise<boolean> {
    console.log(
      '3. storing log for event:[%s] at [%s]',
      event.name,
      event.datetime,
    )
    return Promise.resolve(true)
  }
}

const getUserInfo = new GetUserInfo()
const emailSender = new EmailSender()
const database = new Database()

const options = {
  mode: FlowMode.PIPELINE,
}

const flow = new Flow<UserInput>(
  [getUserInfo.get, emailSender.send, database.storeEvent],
  options,
)

// This parameter will be passed only to the first stage
flow.execute({ email: '[email protected]' }).then((result) => {
  console.log('done', JSON.stringify(result, null, 2))
})

Note

// This
new Flow([], { mode: FlowMode.DEFAULT })
// is equivalent to
new Flow([])

Advanced usage

Some way you can use Flow.

function emailValidator(email: string) {
  console.log('validating email', email)
  return true
}

class EmailSender {
  async send(email: string): Promise<string> {
    console.log('email sent to %s', email)
    return Promise.resolve(`E-mail sent to ${email}`)
  }
}

const emailSender = new EmailSender()
const flow = new Flow([
  // normal function
  emailValidator,

  // method
  emailSender.send,

  // bind function
  emailSender.send.bind(emailSender, '[email protected]'),

  // an anonymous function
  (email: string) => {
    const newEmail = email.replace('@email.com', '@completelydifferent.com')
    emailSender.send(newEmail)
    return 'DONE'
  },
])

;(async () => {
  await flow.execute('[email protected]')

  await flow.allOk((resultValues) => {
    console.log(resultValues)
  })
})()

Note

When using PIPELINE mode, the anonymous functions are required to return the value since it will be used as input of next stage.

const flow = new Flow([
  // an anonymous function
  (email: string) => {
    const newEmail = email.replace('@email.com', '@different.com')
    const response = emailSender.send(newEmail)

    // Remember to return here, when needed
    return response
  },
])