Skip to content

A set of tools for working with promises statefully using Typescript

License

Notifications You must be signed in to change notification settings

rafaelpernil2/StatefulPromises

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

StatefulPromises

Actions Status Coverage Status npm version License: MIT

StatefulPromises is an NPM package implemented in Typescript with zero dependencies 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

  • ICustomPromise<T> a.k.a. "custom promise", an interface to extend JS Promise functionality and syntax.

  • Execution of single-use custom promise batches:

    • One by one with exec.
    • Concurrently limited with all and allSettled.
    • Optional result caching for independently defined callbacks (when using exec, check cached).
    • Independent custom promise validation with validate.
    • Independent done and catch callbacks.
    • Access to custom promise status at any time with observeStatus.
  • Automated Test Suite: 85 automated tests ensure each commit works as intended through Github Actions. Feel free to run the tests locally by executing npm run test.

  • 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.

Important note:

name and function properties are mandatory.

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 custom promise return a value cached in the first execution or undefined. When this value is not specified, it always returns undefined in future executions.

Important note:

If a cached custom promise is still being executed (pending state), posterior calls will wait until this execution resolves, without calling function.

When this execution resolves, it can be fulfilled or rejected:

  • On fulfillment, the response is cached and returned in all posterior executions
  • On rejection, no value is cached and function will be called again in the next execution.

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 custom 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 custom 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, all or allSettled 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 and remains valid. Also, it allows to keep track of different sets of executions, thus, creating a more organized code base.

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

add<T>(customPromise: ICustomPromise<T>)

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);

remove(nameOrCustomPromise: string | ICustomPromise<unknown>)

Removes a custom promise of the batch given a custom promise or the name of a custom promise inside the batch.

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

addList(customPromiseList: Array<ICustomPromise<unknown>>)

Adds an array of ICustomPromise<unknown> to the batch. This means that all promises in the array can have different response types, which can be individually narrowed at further points in the code.

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

getCustomPromiseList()

Returns the list of custom promises previously added using add or addList or at initialization.

Example:
const customPromiseList: ICustomPromise<unknown>= [
  {
    name: 'HelloPromise',
    function: () => Promise.resolve('Hello World!')
  },
  {
    name: 'GoodbyePromise',
    function: () => Promise.resolve('Goodbye World!')
  }
];
const anotherPromise: ICustomPromise<string>= {
  name: 'AnotherPromise',
  function: () => Promise.resolve('Another')
}
promiseBatch.add(anotherPromise)
promiseBatch.addList(customPromiseList);
const result = promiseBatch.getCustomPromiseList();
// [
//   {
//     name: 'HelloPromise',
//     function: () => Promise.resolve('Hello World!')
//   },
//   {
//     name: 'GoodbyePromise',
//     function: () => Promise.resolve('Goodbye World!')
//   }
//   {
//     name: 'AnotherPromise',
//     function: () => Promise.resolve('Another')
//   }
// ];

all(concurrencyLimit?: number)

Your classic Promise.all() but:

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

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

Example:
const concurrencyLimit = 2; // Executes at maximum 2 promises at a time
promiseBatch.all(concurrencyLimit).then((response)=>{
  // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!" }
}, (error)=>{
  // error = Some custom promise was rejected: RejectPromise
  promiseBatch.getBatchResponse() // Even if some custom promise was rejected, getBatchResponse contains all fulfilled promises
});

allSettled(concurrencyLimit?: number)

Same as JS Promise.allSettled(), it returns the batch with all rejected and fulfilled promises. This is ideal for providing seamless user experiences.

Important note:

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

Example:
const concurrencyLimit = 2; // Executes at maximum 2 promises at a time
promiseBatch.allSettled(concurrencyLimit).then((response)=>{
  // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!", RejectPromise: "This is an error" }
  // You can access the status of each custom promise at any time, and, as you can see, the rejected custom promise keeps its status
  promiseBatch.observeStatus('RejectPromise');
  // {
  //    promiseStatus: PromiseStatus.Rejected,
  //    afterProcessingStatus: PromiseStatus.Fulfilled
  // }
}, (error)=>{
  // This is never executed. If you see an error here, it means this library is not working as intended
});

retryRejected(concurrencyLimit?: number)

Calls all previously rejected promises, that may have appeared after calling exec, all or allSettled. This is ideal for automatic error recovery.

Important note:

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

Example:
const concurrencyLimit = 2; // Executes at maximum 2 promises at a time
promiseBatch.all().catch((error)=>{
  // Some custom 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(concurrencyLimit).then(...) // Same as all
});

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

Executes a single custom promise given a custom promise or the name of a custom promise inside the batch. Behaves exactly as all, allSettled and retryRejected, saving the custom 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 custom 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 custom promise, call finishPromise here.
  promiseBatch.finishPromise('HelloPromise');
}, (error)=>{
  // Nothing
});

promiseBatch.exec('GoodbyePromise').then((result)=>{
  // Nothing
}, (error)=>{
  // This custom promise is considered finished since it has a catchCallback defined
  // which has already been executed

  // error = 'ERROR: Goodbye World!'
});

getCacheList()

Returns an object containing the execution result of each custom promise with cached = true. It is indexed by the property name of each custom promise.

Example:
const uncachedPromise: ICustomPromise<string> = {
  name: 'HelloPromiseUncached',
  cached: false, // It's the same as not specifying it
  function: () => Promise.resolve('Hello World from the preset!')
};
const cachedPromise: ICustomPromise<string> = {
  name: 'HelloPromiseCached',
  cached: true,
  function: () => Promise.resolve('Hello World from the past!')
};
promiseBatch.add(uncachedPromise);
promiseBatch.add(cachedPromise);
await promiseBatch.all(); // { HelloPromiseUncached: 'Hello World from the preset!', HelloPromiseCached: 'Hello World from the past!' }
promiseBatch.getCacheList(); // { HelloPromiseCached: 'Hello World from the past!' }

getBatchResponse()

Returns an object containing the response of previous executions of custom promises inside the batch. This is the same object returned as fulfillment result of all, allSettled and retryRejected.

Example:
const anotherPromise: ICustomPromise<string>= {
  name: 'AnotherPromise',
  function: () => Promise.resolve('Another')
}
await promiseBatch.all(); // { HelloPromise: 'Hello World!', GoodbyePromise: 'Goodbye World!' }
promiseBatch.exec(anotherPromise); // { AnotherPromise: 'Another' }
promiseBatch.getBatchResponse() // { HelloPromise: 'Hello World!', GoodbyePromise: 'Goodbye World!',  AnotherPromise: 'Another' }

isBatchCompleted()

Returns true once all the custom promises in the batch have been fulfilled or rejected and marked as finished (see finishPromise).

Important note:

If some custom promise was not resolved (fulfilled or rejected) or some custom promise was not marked as finished, this function will wait indefinitely

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

isBatchFulfilled()

Awaits for the batch to be completed (see isBatchCompleted) and then returns true if all custom promises in the batch were fulfilled and false if some were rejected.

Example:
promiseBatch.all(); // Executing...
promiseBatch.isBatchCompleted().then((response)=>{
  // Once the set of promises has been completed, i.e, it is fulfilled 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>)

Marks the "after processing" status of a given a custom promise or the name of a custom promise inside the batch as fulfilled. 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 custom promise is resolved (fulfilled or rejected) 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.finishPromise('HelloPromise');
}, (error)=>{
  // Nothing
});

promiseBatch.exec('GoodbyePromise').then((result)=>{
  // Nothing
}, (error)=>{
  // error = 'Goodbye World!'
  // Do some data processing...
  promiseBatch.finishPromise(goodbyePromise);
});

promiseBatch.isBatchCompleted().then((result)=>{
    // result=true once promiseBatch.finishPromise is executed for both custom promises
});

finishAllPromises()

Marks all promises in the batch as finished (see finishPromise)

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(nameOrCustomPromise: string | ICustomPromise<unknown>)

Returns the current execution status and "after processing" status (see finishPromise) of a custom promise given a custom promise or the name of a custom promise inside the batch.

Example:
promiseBatch.all().then((response)=>{
  promiseBatch.observeStatus('HelloPromise');
  // {
  //    promiseStatus: PromiseStatus.Fulfilled,
  //    afterProcessingStatus: PromiseStatus.Pending
  // }
  promiseBatch.finishPromise();
  promiseBatch.observeStatus('HelloPromise');
  // {
  //    promiseStatus: PromiseStatus.Fulfilled,
  //    afterProcessingStatus: PromiseStatus.Fulfilled
  // }
});
promiseBatch.observeStatus('HelloPromise');
// {
//    promiseStatus: PromiseStatus.Pending,
//    afterProcessingStatus: PromiseStatus.Pending
// }

getStatusList()

Returns an object with the current promise and "after processing" status of all custom promises in the batch.

Example:
const statusList = promiseBatch.getStatusList(); // statusList = { HelloPromise: { promiseStatus: PromiseStatus.Fulfilled, afterProcessingStatus: PromiseStatus.Pending }, ... }

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

Resets the status of a custom promise given a custom promise or the name of a custom 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.finishPromise(helloPromise);
promiseBatch.observeStatus('HelloPromise') // { promiseStatus: PromiseStatus.Fulfilled, afterProcessingStatus: PromiseStatus.Fulfilled }
promiseBatch.resetPromise('HelloPromise');
promiseBatch.observeStatus('HelloPromise') // { promiseStatus: PromiseStatus.Pending, afterProcessingStatus: PromiseStatus.Pending }

reset()

Resets the whole batch including all statuses and all execution results. It is like creating a new PromiseBatch with the same list of custom promises.

Example:
// Imagine we are making an HTTP request to a REST API and the response changes each time...

const concurrencyLimit = 2; // Executes at maximum 2 promises at a time
await promiseBatch.all(concurrencyLimit); // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!" }
// The same
await promiseBatch.all(concurrencyLimit); // response = { HelloPromise: "Hello World!', GoodbyePromise: "Goodbye World!" }

promiseBatch.reset();

// Now it is different. The custom promise function has been called
await promiseBatch.all(concurrencyLimit); // 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

Changelog

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning - see the CHANGELOG.md file for details.

License

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

About

A set of tools for working with promises statefully using Typescript

Resources

License

Stars

Watchers

Forks

Packages