Skip to content

baskakov/bask-promise

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

BASK Promise

Set of common promise utilities, not available in basic node.js features

  • Delay promise
  • Delay before or after promise
  • Wait until a minimum time has elapsed
  • Delay a rejection
  • Run promises in sequence, e.g. requests to database
  • Repeated failed promises
  • Repeated failed promises with exponential backoff
  • Timeout a promise
  • Race a promise against a guard promise
  • Deferred promise with state inspection

Install

npm i bask-promise --save

Then import module in code:

import * as promises from 'bask-promise';
promises.delay(100);

Or

import { delay } from 'bask-promise';
delay(100);

Delay

Simple delay

import { delay } from 'bask-promise';
delay(1000).then(() => console.log('Run after 1 second'));

Delay after promise

import { delayAfter } from 'bask-promise';
delayAfter(Promise.resolve('1 second delay after'), 1000);

Delay before promise

import { delayBefore } from 'bask-promise';
delayBefore(() => Promise.resolve('1 second delay before'), 1000);

Random delay

Random time period delay

import { random } from 'bask-promise';
random(1000).then(() => console.log('Run after 0...1 second'));

Specify lower bound

import { random } from 'bask-promise';
random(2000, 1000).then(() => console.log('Run after 1...2 seconds'));

Random delay after promise

import { randomAfter } from 'bask-promise';
randomAfter(Promise.resolve('0...1 second delay after'), 1000);
randomAfter(Promise.resolve('0...2 second delay after'), 2000, 1000);

Random delay before promise

import { randomBefore } from 'bask-promise';
randomBefore(() => Promise.resolve('0...1 second delay before'), 1000);
randomBefore(() => Promise.resolve('1...2 second delay before'), 2000, 1000);

Delay callback

If you want to use delay as a callback inside then, you can use delayFun(ms)

import { delayFun } from 'bask-promise';
Promise.resolve("Delay for 1s in then after promise").then(delayFun(1000));

Delay till

Waits until both the promise resolves and the minimum time has elapsed. Useful for avoiding UI flicker on fast operations — e.g. keeping a spinner visible for at least 500ms.

import { delayTill } from 'bask-promise';

// Resolves after at least 5000ms, even if fetchData() finishes in 1000ms
delayTill(fetchData(), 5000);

If the promise takes longer than milliseconds, it resolves as soon as the promise does:

// fetchData() takes 6000ms, milliseconds is 5000ms → resolves after 6000ms
delayTill(fetchData(), 5000);

By default, errors are propagated immediately. Set delayError: true to also delay rejection:

// If fetchData() fails early, the rejection is still held until 5000ms have passed
delayTill(fetchData(), 5000, true);

Delay throw

Returns a .catch-compatible callback that delays a rejection by the given number of milliseconds before re-throwing. Useful as a building block or as an inline callback.

import { delayThrow } from 'bask-promise';

// Delays the rejection by 1 second before propagating it
fetchData().catch(delayThrow(1000));

Combined with delayTill:

import { delayTill, delayThrow } from 'bask-promise';

// Equivalent to delayTill(fetchData(), 5000, true)
delayTill(fetchData().catch(delayThrow(5000)), 5000);

Sequence

Node.js have basic Promise.all function to run promises in parallel, but to run sequence you have to implement it yourself. This is a typical task for example when quering database or external services.

import { sequence } from 'bask-promise';
sequence([
        () => Promise.resolve('Query database 1'),
        () => Promise.resolve('Query database 2'),
    ])

If you have the same query function, but different arguments - use keySequence:

import { keySequence } from 'bask-promise';
keySequence([1,2,3,4,5], key => queryDatabasePromise(key));

If you want to mix sequence and parallel mode and define concurrency number - use concurrent. For concurrent <= 1 - this is sequence() For concurrent >= array length - this is Promise.all()

import { concurrent } from 'bask-promise';
concurrent(2, [
        () => Promise.resolve('Query database 1 (first thread)'),
        () => Promise.resolve('Query database 2 (second thread)'),
        () => Promise.resolve('Query database 3 (first thread)'),
        () => Promise.resolve('Query database 4 (second thread)')
    ])

Repeat

Sometimes you need to ensure request that is properly executed. But due to network connections, requests can fail. In that case you may want to repeat failed requests automatically.

Repeat up to 3 times (4 calls total):

import { repeat } from 'bask-promise';
repeat(() => repeatThisPromiseIfFails(), 3);

Repeat until success:

repeat(() => repeatThisPromiseIfFails(), -1);

Log every error

repeat(() => repeatThisPromiseIfFails(), 3, error => console.error(error));

Delay after error

import { repeat, delay } from 'bask-promise';
repeat(() => repeatThisPromiseIfFails(), 3, error => delay(1000));

Options object variant

All parameters can be passed as a single RepeatOptions object instead. This is the recommended style when specifying multiple options together, as it is more readable and self-documenting.

interface RepeatOptions<T> {
    promiseFun: () => Promise<T>;
    times?: number;        // default: 1
    onError?: (error: Error) => void;
    shouldRetry?: (error: Error, attempt: number) => boolean;
    backoff?: (attempt: number) => number;
    jitter?: 'full' | 'equal' | false;  // default: false
    signal?: AbortSignal;
}

Repeat up to 3 times:

import { repeat } from 'bask-promise';
repeat({ promiseFun: () => repeatThisPromiseIfFails(), times: 3 });

Repeat until success:

repeat({ promiseFun: () => repeatThisPromiseIfFails(), times: -1 });

Log every error and delay after each failure:

import { repeat, delay } from 'bask-promise';
repeat({
    promiseFun: () => repeatThisPromiseIfFails(),
    times: 3,
    onError: error => { console.error(error); return delay(1000); },
});

Retry only on specific errors:

import { repeat } from 'bask-promise';

// Only retry on network errors, fail immediately on anything else
repeat({
    promiseFun: () => fetchData(),
    times: 3,
    shouldRetry: error => error instanceof NetworkError,
});

Retry based on error code:

repeat({
    promiseFun: () => fetchData(),
    times: 5,
    shouldRetry: err => err.code === 'ECONNRESET',
});

Use the attempt argument to limit retries by error type and count together:

repeat({
    promiseFun: () => fetchData(),
    times: 10,
    shouldRetry: (error, attempt) => error instanceof NetworkError && attempt < 3,
});

Use backoff to add a delay before each retry. The function receives the current attempt number (starting at 0) and returns a delay in milliseconds:

Fixed delay between retries:

repeat({
    promiseFun: () => fetchData(),
    times: 5,
    backoff: () => 1000,
});

Exponential backoff (doubles each attempt, capped at 30 seconds):

import { repeat, exponential } from 'bask-promise';

repeat({
    promiseFun: () => fetchData(),
    times: 10,
    backoff: exponential(),                  // 1000ms, 2000ms, 4000ms … capped at 30000ms
});

exponential(baseDelay?, maxDelay?) is a convenience factory for the common pattern attempt => Math.min(baseDelay * 2 ** attempt, maxDelay). Both parameters are optional:

Parameter Default Description
baseDelay 1000 Delay for the first retry in milliseconds
maxDelay 30000 Upper cap in milliseconds
exponential()           // 1000ms → 2000ms → 4000ms … → 30000ms
exponential(500)        // 500ms  → 1000ms → 2000ms … → 30000ms
exponential(200, 5000)  // 200ms  → 400ms  → 800ms  … → 5000ms

Combine backoff with shouldRetry and onError:

import { repeat, exponential } from 'bask-promise';

repeat({
    promiseFun: () => fetchData(),
    times: 5,
    shouldRetry: error => error instanceof NetworkError,
    backoff: exponential(1000, 30_000),
    onError: error => console.error(`Attempt failed: ${error.message}`),
});

Use jitter to add randomness to backoff delays, which prevents many clients from retrying in sync and hammering the server at the same time (AWS Architecture Blog).

Value Behaviour
false No jitter — exact backoff value is used (default)
'full' Uniformly random between 0 and the full backoff: random(0, backoff)
'equal' Keeps half the backoff, randomises the other half: backoff/2 + random(0, backoff/2)

'full' jitter — most effective at spreading load:

import { repeat, exponential } from 'bask-promise';

repeat({
    promiseFun: () => fetchData(),
    times: 10,
    backoff: exponential(),
    jitter: 'full',
});

'equal' jitter — steadier slowdown with some spread:

repeat({
    promiseFun: () => fetchData(),
    times: 10,
    backoff: exponential(),
    jitter: 'equal',
});

jitter without backoff has no effect, since there is no delay to randomise.

Pass an AbortSignal via signal to cancel the retry loop at any point. The returned promise rejects with the abort reason as soon as the signal fires — either immediately if the signal is already aborted, or mid-backoff-delay if it fires while waiting between retries.

Cancel after a timeout using AbortSignal.timeout:

import { repeat, exponential } from 'bask-promise';

repeat({
    promiseFun: () => fetchData(),
    times: -1,
    backoff: exponential(),
    signal: AbortSignal.timeout(10_000), // give up entirely after 10 seconds
});

Cancel manually with AbortController:

import { repeat } from 'bask-promise';

const ac = new AbortController();

repeat({
    promiseFun: () => fetchData(),
    times: -1,
    backoff: () => 1000,
    signal: ac.signal,
});

// cancel from outside whenever needed
ac.abort(new Error('user cancelled'));

Repeat with exponential backoff

Like repeat, but automatically adds an increasing delay between retries. The delay doubles after each failed attempt: baseDelay * 2^attempt.

Repeat up to 3 times with default 100ms base delay (delays: 100ms, 200ms, 400ms):

import { repeatExponential } from 'bask-promise';
repeatExponential({ promiseFun: () => repeatThisIfFails(), times: 3 });

Custom base delay:

repeatExponential({ promiseFun: () => repeatThisIfFails(), times: 5, baseDelay: 200 });

Repeat until success:

repeatExponential({ promiseFun: () => repeatThisIfFails(), times: -1 });

Log every error:

repeatExponential({
    promiseFun: () => repeatThisIfFails(),
    times: 3,
    onError: error => console.error(error),
});

Promise resolve from outside

If you need to create a promise instance, pass it somewhere, and resolve it later from outside, you can use deferred function

import { deferred } from 'bask-promise';
const d = deferred();

// use or pass promise somewhere
d.promise.then(() => null).catch(() => null);

// then resolve or reject from outside
d.resolve(42);
// or
d.reject(new Error('foo'));

Inspecting deferred state

The deferred object exposes read-only properties to inspect its current state at any time:

Property Type Description
isPending boolean true until resolved or rejected
isResolved boolean true after resolve() is called
isFailed boolean true after reject() is called
value T | undefined The resolved value (available after the next microtask), undefined otherwise
error unknown | undefined The rejection reason, undefined otherwise
import { deferred } from 'bask-promise';
const d = deferred();

d.isPending;   // true
d.isResolved;  // false
d.isFailed;    // false
d.value;       // undefined
d.error;       // undefined

d.resolve(42);

d.isPending;   // false
d.isResolved;  // true

await Promise.resolve(); // wait one microtask tick
d.value;       // 42
const d = deferred();
d.reject(new Error('boom'));

d.isPending;   // false
d.isFailed;    // true
d.error;       // Error: boom

Timer

Creates a promise that rejects after a given delay. Useful as a standalone building block or combined with left.

import { timer } from 'bask-promise';

// Rejects with default message after 1 second
timer(1000);

// Rejects with a custom message
timer(1000, 'Operation took too long');

Timeout

Races a promise against a timer. Rejects with a timeout error if the promise does not settle within the given milliseconds.

import { timeout } from 'bask-promise';

// Rejects with "Timeout exceeded: 5000ms" if fetchData() takes longer than 5 seconds
timeout(fetchData(), 5000);

Left

Races a promise against a guard promise. Resolves with the value of the primary promise, but rejects if the guard promise rejects — regardless of the primary promise state. The resolved value of the guard is ignored.

import { left } from 'bask-promise';

// Resolves with the result of primary(), but aborts if guard rejects
left(primary(), guard());

A practical use case is combining left with timer to build a custom timeout with a specific error message:

import { left, timer } from 'bask-promise';

left(fetchData(), timer(5000, 'fetchData timed out'));

About

Typical node.js promise snippets

Resources

Stars

0 stars

Watchers

1 watching

Forks

Packages

 
 
 

Contributors