Miss any of our Open RFC calls?Watch the recordings here! »

stateful-promises

1.0.2 • Public • Published

StatefulPromises

Actions Status BCH compliance npm version License: MIT

StatefulPromises is an NPM package implemented in Typescript using Knockout 3.5 for working with promises statefully.

This project is an extension of rafaelpernil2/ParallelPromises.

Table of Contents

Installation

Install it on your project

npm install --save stateful-promises

Why?

As described by MDN, a Promise has 3 possible states: Pending, Fulfilled and Rejected.

.

But... We can't take a peek at a Promise status at any time without doing some hacking.

This design choice makes sense for many applications but many times we need to know which is the state of our Promise after .then was executed, which one of our promise batch has been rejected or systematically wait until a set of promises has been completed while using the responses as each of them is fulfilled...

So you might be thinking... If I know I will need the state of my promise afterwards, I could store that status in a variable.

Yeah, okay, fair enough. But, what if you have a few dozen promises? You'd have to remember to save the status of each promise at their fulfilled and rejected callbacks... Hello boilerplate code. This does not scale and it's prone to mistakes.

StatefulPromises solves that problem with some more thought put into it.

Features

  • Execution of single-use promise batches:

    • One by one with exec.
    • Concurrently limited with promiseAll and promiseAny.
    • Optional result caching for independently defined callbacks (when using exec, check cached).
    • Independent promise validation with validate.
    • Independent done and catch callbacks.
    • Access to promise status at any time with observeStatus.
  • Automated Test Suite: 100 automated tests ensure each commit works as intended through Github Actions.

  • Full type safety: Generic methods and interfaces like ICustomPromise<T> to type your Promises accordingly.

API

ICustomPromise<T>

This interface defines the basic type for interacting with promises statefully. The type variable T defines the type of your function returning a PromiseLike. PromiseLike is the less strict version of the Promise type, thus allowing the usage of JQuery Promises or other implementations.

name

Specifies the name of the custom promise.

function(...args: any[]): PromiseLike<T>

Specifies the function to be called, that has to return a PromiseLike<T>, being T the parameter of the interface.

These two properties are mandatory. Here is an example:

const customPromise: ICustomPromise<string> = {
  name: 'HelloPromise',
  function: () => Promise.resolve('Hello World!')
};

thisArg

Specifies the context of the function. See this documentation by MDN.

Example:
const customPromise: ICustomPromise<number> = {
  name: 'CountStars',
  thisArg: this.starProvider,
  function: this.starProvider.getCount
};

args

Specifies the arguments to pass to the function in an array format.

Example:

Let's imagine this.starProvider.getStarBySystem(currentSystem,namingConvention) has two arguments, currentSystem and namingConvention.

const customPromise: ICustomPromise<number> = {
  name: 'GetStarByPlanetarySystem',
  thisArg: this.starProvider,
  args: [this.currentSystem, this.namingConvention],
  function: this.starProvider.getStarBySystem
};

cached

Determines if future executions of this stateful promise return a value cached in the first execution or undefined. When this value is not specified, it always returns undefined in future executions.

Default behaviour:

const customPromise: ICustomPromise<string> = {
  name: 'HelloPromiseUncached',
  cached: false, // It's the same as not specifying it
  function: () => Promise.resolve('Hello World!')
};
 
const firstExec = await promiseBatch.exec(customPromise); // firstExec = 'Hello World!'
const secondExec = await promiseBatch.exec(customPromise); // secondExec = undefined

Example with cached = true:

const customPromise: ICustomPromise<string> = {
  name: 'HelloPromiseCached',
  cached: true,
  function: () => Promise.resolve('Hello World!')
};
 
const firstExec = await promiseBatch.exec(customPromise); // firstExec = 'Hello World!'
const secondExec = await promiseBatch.exec(customPromise); // secondExec = 'Hello World!'

validate?(response: T): boolean

This function validates the response of the function to determine if it should be rejected. The response parameter cannot be modified.

Example:
const customPromise: ICustomPromise<string> = {
  name: 'CheckLights',
  thisArg: this.lightProvider,
  function: this.lightProvider.checkLights,
  validate: (response: string) => response === 'BLINK' || response === 'STROBE'
};
 
promiseBatch.exec(customPromise).then((response)=>{
  // This block is executed if the response IS 'BLINK' or 'STROBE'
},
(error)=>{
  // This block is executed if the response IS NOT 'BLINK' or 'STROBE'
});
 

doneCallback?(response: T): T

This function is executed when the promise is fulfilled and valid (validate returns true). The syntax is inspired by JQuery Promises.

Example:
const customPromise: ICustomPromise<string> = {
  name: 'CheckLights',
  thisArg: this.lightProvider,
  function: this.lightProvider.checkLights,
  validate: (response: string) => response === 'BLINK' || response === 'STROBE',
  doneCallback: (response: string) => 'Light status: ' + response
};
// Let's imagine this function returns BLINK...
promiseBatch.exec(customPromise).then((response)=>{
  // response = 'Light status: BLINK'
},
(error)=>{
  // This block is not executed
});

catchCallback?(error: any): any

This function is executed when the promise is rejected or invalid (validate returns false).

Example:
const customPromise: ICustomPromise<string> = {
  name: 'CheckLights',
  thisArg: this.lightProvider,
  function: this.lightProvider.checkLights,
  validate: (response: string) => response === 'BLINK' || response === 'STROBE',
  doneCallback: (response: string) => 'Light status: ' + response,
  catchCallback: (error: string) => 'Failure: ' + error
};
// Let's imagine this function returns OFF...
promiseBatch.exec(customPromise).then((response)=>{
  // This block is not executed
},
(error)=>{
  // error = 'Failure: OFF'
});

finallyCallback?(response: any): any

This function is always executed after fulfillment or rejection. The syntax is inspired by JQuery Promises.

Example:
const customPromise: ICustomPromise<string> = {
  name: 'CheckLights',
  thisArg: this.lightProvider,
  function: this.lightProvider.checkLights,
  validate: (response: string) => response === 'BLINK' || response === 'STROBE',
  doneCallback: (response: string) => 'Light status: ' + response,
  catchCallback: (error: string) => 'Failure: ' + error,
  finallyCallback: (response: string) => 'Overall status: { ' + response + ' }'
};
// Let's imagine this function returns BLINK...
promiseBatch.exec(customPromise).then((response)=>{
  // response = 'Overall status: { Light status: BLINK }'
},
(error)=>{
  // This block is not executed
});

PromiseBatch

This class provides a set of methods for working statefully with a single use set of Promises. Each PromiseBatch has a set of customPromises to execute either using exec for individual execution, promiseAll or promiseAny for batch execution, while providing methods to retry failed promises, check statuses or notify promises as finished for making sure all .then post-processing is done without race conditions.

By desing, it is a single use batch to avoid expensive calls to functions when the current result is already loaded. Also, it allows to keep track of different sets of executions thus creating a more organized code base.

Initialization:
const promiseBatch = new PromiseBatch(yourCustomPromiseArrayArray<ICustomPromise<unknown>>); // The parameter is optional

add(customPromise: ICustomPromise)

Adds a single ICustomPromise<T> to the batch, with T being the type of the promise.

Example:
const customPromise: ICustomPromise<string> = {
  name: 'HelloPromise',
  function: () => Promise.resolve('Hello World!')
};
promiseBatch.add(customPromise);

addList(customPromiseList: Array<ICustomPromise>)

Adds an array of ICustomPromise<unknown> to the batch. This means that all promises in the array can have different response types. The type will be resolved in execution time.

Example:
const customPromiseList: ICustomPromise<unknown>= [
  {
    name: 'HelloPromise',
    function: () => Promise.resolve('Hello World!')
  },
  {
    name: 'GoodbyePromise',
    function: () => Promise.resolve('Goodbye World!')
  }
];
promiseBatch.addList(customPromiseList);

promiseAll(concurrentLimit?: number):

Your classic Promise.all() but:

  • Saves all results no matter what.
  • Saves all results in an object using the name of each promise as a key instead of an array.
  • Provides an optional concurrency limit for specifying how many promises you want to execute in parallel.
Important note:

This operation finishes all promises automatically, so if you need to handle each promise response individually, you MUST use doneCallback or catchCallback.

Example:
const concurrentLimit = 2; // Executes at maximum 2 promises at a time
promiseBatch.promiseAll(concurrentLimit).then((response)=>{
  // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!" }
}, (error)=>{
  // error = Some promise was rejected: RejectPromise
});

promiseAny(concurrentLimit?: number):

Same as promiseAll() but never throws an error. For providing seamless user experiences.

Important note:

This operation finishes all promises automatically, so if you need to handle each promise response individually, you MUST use doneCallback or catchCallback.

Example:
const concurrentLimit = 2; // Executes at maximum 2 promises at a time
promiseBatch.promiseAny(concurrentLimit).then((response)=>{
  // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!" }
}, (error)=>{
  // This is never executed. If you see an error here, it means this library is not working as intended
});

retryRejected(concurrentLimit?: number):

If after calling promiseAll() or promiseAny(), some promise failed, you may retry those with this method. Ideal for automatic error recovery.

Important note:

This operation finishes all promises automatically, so if you need to handle each promise response individually, you MUST use doneCallback or catchCallback.

Example:
const concurrentLimit = 2; // Executes at maximum 2 promises at a time
promiseBatch.promiseAll().catch((error)=>{
  // Some promise was rejected: RejectPromise
 
  // Maybe it was due to a data issue or a network issue, so, we can try to fix the issue and try again
  customPromiseList[0].args = ["Now it's ok"];
 
  promiseBatch.retryRejected(concurrentLimit).then(...) // Same as promiseAll
});

exec<T>(nameOrCustomPromise: string | ICustomPromise<T>):

It executes a single promise. Behaves exactly as promiseAll and promiseAny, saving the promise and its result to the associated batch.

Important note:

For a single execution to be considered finished, you MUST define a callback for the case you are contemplating: Fullfillment or Rejection.

There are two ways of doing that:

Remember that if you only cover fullfillment case (.doneCallback or .then), on rejection, the promise won't be considered finished and viceversa.

TL;DR

If you plan to execute a batch of Promises one by one, read the above note.

Example:
const helloPromise: ICustomPromise<string> = {
    name: 'HelloPromise',
    function: () => Promise.resolve('Hello World!')
  };
const goodbyePromise: ICustomPromise<string> = {
    name: 'GoodbyePromise',
    function: () => Promise.reject('Goodbye World!'),
    catchCallback: (response: string) => 'ERROR: ' + response,
  };
promiseBatch.add(goodbyePromise);
promiseBatch.exec(helloPromise).then((result)=>{
  // result = 'Hello World!'
 
  // To finish the promise, call finishPromise here.
  promiseBatch.finishPromise('HelloPromise');
}, (error)=>{
  // Nothing
});
 
promiseBatch.exec('GoodbyePromise').then((result)=>{
  // Nothing
}, (error)=>{
  // This promise is considered finished since it has a catchCallback defined
  // which has already been executed
 
  // error = 'ERROR: Goodbye World!'
});

isBatchCompleted():

Returns true once all the promises in the batch have been resolved or rejected.

Example:
promiseBatch.promiseAll(); // Executing...
promiseBatch.isBatchCompleted().then((response)=>{
  // Once the set of promises has been completed, i.e, it is resolved or rejected...
  // response = true
});

isBatchFulfilled():

Once the batch is completed, it returns true if all promises were resolved and false if some had been rejected.

Example:
promiseBatch.promiseAll(); // Executing...
promiseBatch.isBatchCompleted().then((response)=>{
  // Once the set of promises has been completed, i.e, it is resolved or rejected...
  // response = true
});
promiseBatch.isBatchFulfilled().then((response)=>{
  // If all are fulfilled, true, else, false. isBatchCompleted has to be true to return anything.
  // response = true
});

finishPromise<T>(nameOrCustomPromise: string | ICustomPromise<T>):

Sets a promise as finished. This affects exec calls whose customPromise does not define a doneCallback or catchCallback properties. This is designed for making sure you can do all post-processing after the promise is resolved without running into race conditions.

Example:
const helloPromise = {
    name: 'HelloPromise',
    function: () => Promise.resolve('Hello World!')
  };
const goodbyePromise = {
    name: 'GoodbyePromise',
    function: () => Promise.reject('Goodbye World!')
  };
promiseBatch.add(goodbyePromise);
promiseBatch.exec(helloPromise).then((result)=>{
  // result = 'Hello World!'
  // Do some data processing...
  promiseBatch.isBatchCompleted().then((result)=>{
    // result=true once promiseBatch.finishPromise is executed
  });
  promiseBatch.finishPromise('HelloPromise');
}, (error)=>{
  // Nothing
});
 
promiseBatch.exec('GoodbyePromise').then((result)=>{
  // Nothing
}, (error)=>{
  // error = 'Goodbye World!'
  // Do some data processing...
  promiseBatch.isBatchCompleted().then((result)=>{
    // result=true once promiseBatch.finishPromise is executed
  });
  promiseBatch.finishPromise(goodbyePromise);
});

finishAllPromises():

Same as before but does it for all promises.

Example:
const helloPromise = {
    name: 'HelloPromise',
    function: () => Promise.resolve('Hello World!')
  };
const goodbyePromise = {
    name: 'GoodbyePromise',
    function: () => Promise.resolve('Goodbye World!')
  };
promiseBatch.add(goodbyePromise);
await promiseBatch.exec(helloPromise); // result = 'Hello World!'
await promiseBatch.exec('GoodbyePromise'); // result = 'Goodbye World!'
 
promiseBatch.isBatchCompleted().then((result)=>{
  // result=true once promiseBatch.finishAllPromises is executed
});
promiseBatch.finishAllPromises();

observeStatus(promiseName: string):

Allows you to access the current status of any of the promises of your batch.

Example:
promiseBatch.promiseAll().then((response)=>{
  promiseBatch.observeStatus('HelloPromise'); // 'f' (Fulfilled)
});
promiseBatch.observeStatus('HelloPromise'); // 'p' (Pending)

getStatusList():

Returns an object with the statuses of all promises in the batch. Each status property is a Knockout Observable variable.

Example:
const statusList = promiseBatch.getStatusList()// statusList = { HelloPromise: ko.observable(...), ... }

resetPromise<T>(nameOrCustomPromise: string | ICustomPromise<T>):

Resets the status of a promise inside the batch. This means it will behave like it was never called and all caching would be reset in the next execution.

Example:
const helloPromise = {
    name: 'HelloPromise',
    function: () => Promise.resolve('Hello World!')
};
 
promiseBatch.add(goodbyePromise);
await promiseBatch.exec(helloPromise); // result = 'Hello World!'
 
promiseBatch.observeStatus('HelloPromise') // 'f' (Fulfilled)
promiseBatch.resetPromise('HelloPromise');
promiseBatch.observeStatus('HelloPromise') // 'p' (Pending)

reset():

Resets the whole batch including all statuses and responseData. It is like initializing again the promiseBatch.

Example:
// Imagine we are making an HTTP request to a REST API and the response changes each time...
 
const concurrentLimit = 2; // Executes at maximum 2 promises at a time
await promiseBatch.promiseAll(concurrentLimit); // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!" }
// The same
await promiseBatch.promiseAll(concurrentLimit); // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!" }
 
promiseBatch.reset();
 
// Now it is different. The promise function has been called
await promiseBatch.promiseAll(concurrentLimit); // response = { HelloPromise: "Hola Mundo!', GoodbyePromise: "Au revoir le monde!" }
 

Usage

Usage with Typescript

import { PromiseBatch, ICustomPromise } from 'stateful-promises';
 
type Comic = {
    nombre: string;
}
 
let allComics = [];
 
const getAllComics: ICustomPromise<Comic[]> = {
      name: 'GetAllComics',
      function: () => Promise.resolve([{ nombre: "SuperComic" }, {nombre: "OtroComic"}]),
      validate: (data) => Math.floor(Math.random() * 1000) % 2 === 0,
      doneCallback: (data) => {
        data[0].nombre = 'Modified by doneCallback';
        return data;
      },
      catchCallback: (data) => {
        data[0].nombre = 'Modified by catchCallback';
        return data;
      }
    };
const promiseBatch = new PromiseBatch([getAllComics]);
promiseBatch.exec(getAllComics).then((res) => {
  allComics = res;
  console.log("OK",allComics);
  promiseBatch.finishPromise(getAllComics);
}, error => {
  allComics = error;
  console.log("ERROR",allComics);
  promiseBatch.finishPromise(getAllComics);
});
promiseBatch.isBatchCompleted().then((ready) => {
  console.log('COMPLETED', ready);
});
promiseBatch.isBatchFulfilled().then((ready) => {
  console.log('FULFILLED', ready);
});
 
// CONSOLE LOG
 
/**
 * COMPLETED true
 * FULFILLED true
 * OK [ { nombre: 'Modified by doneCallback' }, { nombre: 'OtroComic' } ]
 */
 
/**
 * COMPLETED true
 * FULFILLED false
 * ERROR [ { nombre: 'Modified by catchCallback' }, { nombre: 'OtroComic' } ]
 */

Usage with Javascript

const { PromiseBatch: PromiseBatch } = require("stateful-promises");
// or const StatefulPromises = require("stateful-promises");
// and then... new StatefulPromises.PromiseBatch()
 
let allComics = [];
 
const getAllComics = {
  name: "GetAllComics",
  function: () => Promise.resolve([{ nombre: "SuperComic" }, {nombre: "OtroComic"}]),
  validate: data => Math.floor(Math.random() * 1000) % 2 === 0,
  doneCallback: data => {
    data[0].nombre = "Modified by doneCallback";
    return data;
  },
  catchCallback: data => {
    data[0].nombre = "Modified by catchCallback";
    return data;
  }
};
const promiseBatch = new PromiseBatch([getAllComics]);
promiseBatch.exec(getAllComics).then(
  res => {
    allComics = res;
    console.log("OK",allComics);
    promiseBatch.finishPromise(getAllComics);
  },
  error => {
    allComics = error;
    console.log("ERROR", allComics);
    promiseBatch.finishPromise(getAllComics);
  }
);
promiseBatch.isBatchCompleted().then(ready => {
  console.log("COMPLETED", ready);
});
promiseBatch.isBatchFulfilled().then(ready => {
  console.log("FULFILLED", ready);
});
 
// CONSOLE LOG
 
/**
 * COMPLETED true
 * FULFILLED true
 * OK [ { nombre: 'Modified by doneCallback' }, { nombre: 'OtroComic' } ]
 */
 
/**
 * COMPLETED true
 * FULFILLED false
 * ERROR [ { nombre: 'Modified by catchCallback' }, { nombre: 'OtroComic' } ]
 */

Contributing

There is no plan regarding contributions in this project.

Credits

This NPM package has been developed by:

Rafael Pernil Bronchalo - Developer

License

This project is licensed under the MIT License - see the LICENSE.md file for details.

Install

npm i stateful-promises

DownloadsWeekly Downloads

1

Version

1.0.2

License

MIT

Unpacked Size

238 kB

Total Files

55

Last publish

Collaborators

  • avatar