Skip to content

craigmichaelmartin/fpromise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

9 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

fPromise

Build Status Greenkeeper badge codecov

Installation

npm install --save fpromise

What is fPromise?

fPromise is a javascript library for working with promises.

It seeks to resolve three problems with promises:

  • Promises have an API which encourages casually dangerous code
  • Promises co-mingle rejected promises with unintended native exceptions
  • Promises lack a suite of convenient API methods to work with results

(For background, and probably a better explanation about this library, read that article about the problems with promises).

fPromise solves these issues by adding a layer of abstraction within promises - re-designing promises's two path design (resolved/rejected) into three paths: a data path, a non-native exception path (ie, for promises rejected by your own intentions), and a native exception path.

With these three paths, we can have an API which is safe, intentional, convenient, and more readable.

Importantly this abstraction:

  • using promises
  • leave the promise prototype untouched
  • provide a safe API for using them which isn't casually dangerous
  • ensures unintentional runtime errors are not handled
  • provides utility methods for working with the data
  • increases readability (vs try blocks)
  • keeps control in main call block (so returns work)

fPromise works by ensuring only native exceptions are ever in the "rejected" lane, and uses a special wrapper object for the "fulfilled" lane: one which has convenient methods available on it, and which unboxes to [data, error].

This mean we never have to try/catch with await (a pattern which is less readable and can encourage casually dangerous code) - and that unintentional errors are then are uncaught (like how syncrounous code works).

Instead of using try/catch, we destructure the special object to receive either the data or the non-native issue which occurred.

Usage

Regular Style

Pass a promise to the fp function.

const { fp } = require('fpromise');
const [data, issue] = await fp(Promise.resolve('foo')); // data === foo
const [data, issue] = await fp(Promise.reject('bar')); // issue === bar
const [data, issue] = await fp(Promise.resolve().then(x => undefined())); // throws! good!

If you want to work with the data, await the promise (so it resolves to the Either, and then use those methods).

  const { fp } = require('fpromise');
  const [data, issue] = (await fp(Promise.resolve('foo'))
    .tap(console.log)
    .map(d => d.toUpperCase())
  // prints foo, data === FOO

Functional Style

  const { map, tap } = require('fpromise');
  const [data, issue] = await Promise.resolve('foo'))
    .then(...tap(console.log))
    .then(...map(d => d.toUpperCase()))
  // prints foo, data === FOO

Example

// data-access/user.js
const save = user => fp(db.execute(user.getInsertSQL()));

// service/user.js
const save = async data =>
  (await save(User(data)))
    .tap(getStandardLog('user_creation'))
    .map(User.parseUserFromDB)
    .itap(logError);

// controllers/user.js
const postHandler = async (userDate, response) => {
  const [user, error] = await save(userData);
  if (error) {
    const errorToCode = { IntegrityError: 422 };
    return response.send(errorToCode[error.constructor.name] || 400);
  }
  response.send(204);
  postEmailToMailChimp(user.email).tapError(logError);
};

If we wanted to use the more functional approach, no need for initially wrapping the promise:

// data-access/user.js
const save = user => db.execute(user.getInsertSQL();

// service/user.js
const save = data => save(data)
  .then(...tap(getStandardLog('user_creation')))
  .then(...map(User.parseUserFromDB))
  .then(...itap(logError))

// controllers/user.js
const postHandler = async (userDate, response) => {
  const [user, error] = await save(userData);
  // ...
}

If we want to move even further in the functional direction, we could:

// data-access/user.js
const save = user => db.execute(user.getInsertSQL();

// service/user.js
const save = data => save(data)
  .then(...tap(getStandardLog('user_creation')))
  .then(...map(User.parseUserFromDB))
  .then(...itap(logError))

// controllers/user.js
const postHandler = (userDate, response) =>
  save(userData).then(...map(
    user => //...
    error => //...
  );

API

function explanation / example
fp Accepts a promise and return a promise which rejects for native errors or resolves to an Either
const [data, issue] = await fp(Promise.resolve('foo')); // data == 'foo'

Data

utility methods explanation / example
map Accepts a transforming fn and returns a Data with the newly transformed value
const [data, issue] = Data('foo').map(x => x.toUpperCase()) // data === FOO
imap (issue map) Accepts a transforming fn, but is a no-op - method targets Issue
const [data, issue] = Data('foo').imap(x => x.toUpperCase()) // data === foo
bmap (both map) Accepts two transforming fns, and returns a Data with the value of the first fn's transform
const [data, issue] = Data('foo').bmap(x => x.toUpperCase(), y => y.length) // data === FOO
raw Accepts a transforming fn and returns its value (without reboxing in Data)
const val = Data('foo').raw(x => x.toUpperCase()) // val === FOO
iraw (issue raw) Accepts a fn, but is a no-op - method targets Issue
const val = Data('foo').iraw(x=> x.toUpperCase()) // val === foo
braw (both raw) Accepts two transforming fns, and returns the value of the first fn's transform (without reboxing)
const val = Data('foo').braw(x => x.toUpperCase(), y => y.length) // val === FOO
tap Accepts a side effect fn and runs it on the data (fn's return is not used)
const [data, issue] = Data('foo').tap(console.log) // foo is printed; data === 'foo'
itap (issue tap) Accepts a side effect fn but is a no-op - method targets Issue
const [data, issue] = Data('foo').itap(console.log) // nothing printed, data === 'foo'
btap (both tap) Accepts two side effect fns, and runs the first fn (fn's return is not used)
const [data, issue] = Data('foo').btap(console.log, console.warn) // foo is printed; data === 'foo'
val Returns data as first element in array
const [data, issue] = Data('foo').val() // data === foo
isData Returns true
Data('foo').isData // true
isIssue Returns false
Data('foo').isIssue // false
[Symbol.iterator] Yields data and nothing else. This is the reason we can "unbox" Data with array destructing
const [data, issue] = Data('foo') // data === 'foo'

Issue

utility methods explanation / example
map Accepts a transforming fn, but is a no-op - method targets Data
const [data, issue] = Issue('bar').imap(x => x.toUpperCase()) // issue === bar
imap (issue map) Accepts a transforming fn and returns an Issue with the newly transformed value
const [data, issue] = Issue('bar').imap(x => x.toUpperCase()) // issue === BAR
bmap (both map) Accepts two transforming fns, and returns a Issue with the value of the second fn's transform
const [data, issue] = Issue('bar').bmap(x => x.length(), y => y.toUpperCase) // issue === BAR
raw Accepts a fn, but is a no-op - method targets Issue
const val = Issue('bar').raw(x=> x.toUpperCase()) // val === bar
iraw (issue raw) Accepts a transforming fn and returns its value (without reboxing in Issue)
const val = Issue('bar').iraw(x => x.toUpperCase()) // val === BAR
braw (both raw) Accepts two transforming fns, and returns the value of the second fn's transform (without reboxing)
const val = Issue('bar').braw(x => x.length(), y => y.toUpperCase()) // val === BAR
tap Accepts a side effect fn but is a no-op - method targets Issue
const [data, issue] = Issue('bar').itap(console.log) // nothing printed, issue === 'bar'
itap (issue tap) Accepts a side effect fn and runs it on the issue (fn's return is not used)
const [data, issue] = Issue('bar').itap(console.log) // bar is printed; issue === 'bar'
btap (both tap) Accepts two side effect fns, and runs the second fn (fn's return is not used)
const [data, issue] = Issue('bar').btap(console.warn, console.log) // bar is printed; issue === 'bar'
val Returns issue as the second element in array
const [data, issue] = Issue('bar').val() // issue === bar
isData Returns false
Data('bar').isData // false
isIssue Returns true
Data('bar').isIssue // true
[Symbol.iterator] Yields undefined and then data. This is the reason we can "unbox" Issue with array destructing
const [data, issue] = Issue('bar') // issue === 'bar'

Funtional

You would use these if you don't use fp to wrap the promise.

functions
map
imap (issue map)
bmap (both map)
raw
iraw (issue raw)
braw (both raw)
tap
itap (issue tap)
btap (both tap)

Woah, this is apparently a thing and so here are Links About This Stuff From Smart People:

Actually Good Projects

Current Status

Known TODOs

  • Actually make the readme good/helpful/clear.

  • Decide about aliases:

    Data Issue Both
    map (dmap) imap bmap (map with two functions)
    raw (draw) iraw braw (raw with two functions)
    tap (dtap) itap btap (tap with two functions)

Is it production ready? This library is a version two of library extracted out of code in production at www.kujo.com. Kujo is still on that version 1, though, for now.

Releases

No releases published

Packages

No packages published