aveazul
Version:
Bluebird drop-in replacement built on native Promise
612 lines • 21.3 kB
JavaScript
/* eslint-disable @typescript-eslint/no-explicit-any */
import * as xaa from "xaa";
import { promisify } from "./promisify.js";
import { promisifyAll } from "./promisify-all.js";
import { Disposer } from "./disposer.js";
import { using } from "./using.js";
import { isPromise, triggerUncaughtException, toArray } from "./util.js";
import { AggregateError } from "@jchip/error";
import { OperationalError, isOperationalError, isProgrammerError } from "./operational-error.js";
/**
* @fileoverview
* AveAzul ("Blue Bird" in Spanish) - Extended Promise class that provides Bluebird like utility methods
* This implementation is inspired by and provides similar APIs to the Bluebird Promise library,
* but built on top of native Promises. The name is a Spanish play on words referencing Bluebird.
* @extends Promise
*/
class AveAzul extends Promise {
constructor(executor) {
super(executor);
}
/**
* Note: Per ECMAScript specification, when extending Promise, both .then() and static methods
* (resolve, reject, all, etc) must return instances of the derived class (AveAzul), so there's
* no need to explicitly wrap returns in new AveAzul(). This behavior is standard across all
* spec-compliant JS engines (V8, SpiderMonkey, JavaScriptCore, etc).
*/
/**
* Bluebird-style tap() method that lets you perform side effects in a chain
* Similar to Bluebird's Promise.prototype.tap()
* @param fn - Function to execute with the resolved value
* @returns Promise that resolves with the original value
*/
tap(fn) {
return this.then(async (value) => {
await fn(value);
return value;
});
}
/**
* Bluebird-style filter() method for array operations
* Similar to Bluebird's Promise.prototype.filter()
* @param fn - Filter function to apply to each element
* @returns Promise that resolves with the filtered array
*/
filter(fn) {
return this.then((value) => xaa.filter(value, fn));
}
/**
* Bluebird-style map() method for array operations
* Similar to Bluebird's Promise.prototype.map()
* @param fn - Map function to apply to each element
* @returns Promise that resolves with the mapped array
*/
map(fn, options = { concurrency: 50 }) {
return this.then((value) => xaa.map(value, fn, options));
}
/**
* Bluebird-style mapSeries() method for array operations
* Similar to Bluebird's Promise.prototype.mapSeries()
* @param fn - Map function to apply to each element
* @returns Promise that resolves with the mapped array
*/
mapSeries(fn) {
return this.map(fn, { concurrency: 1 });
}
/**
* Bluebird-style return() method to inject a value into the chain
* Similar to Bluebird's Promise.prototype.return()
* @param value - Value to return
* @returns Promise that resolves with the new value
*/
return(value) {
return this.then(() => value);
}
/**
* Bluebird-style any() method for waiting for any promises to resolve
* @returns Promise that resolves with the first resolved promise
*/
any() {
return this.then((args) => {
return AveAzul.any(toArray(args));
});
}
/**
* Bluebird-style each() method for array iteration
* Similar to Bluebird's Promise.prototype.each()
* @param fn - Function to execute for each element
* @returns Promise that resolves when iteration is complete
*/
each(fn) {
return this.then(async (value) => {
const arr = value;
const result = [];
for (let i = 0; i < arr.length; i++) {
let x = arr[i];
if (isPromise(x)) {
x = await x;
}
await fn(x, i, arr.length);
result.push(x);
}
return result;
});
}
/**
* Bluebird-style delay() method
* @param ms - Milliseconds to delay
* @returns Promise that resolves after the delay
*/
delay(ms) {
return xaa.delay(ms);
}
/**
* Bluebird-style timeout() method
* @param ms - Milliseconds before timeout
* @param message - Optional error message
* @returns Promise that rejects if timeout occurs
*/
timeout(ms, message = "operation timed out") {
return xaa
.timeout(ms, message, {
Promise: AveAzul,
TimeoutError: OperationalError,
})
.run(this);
}
/**
* Bluebird-style props() for object properties
* @returns Promise that resolves with an object of resolved values
*/
props() {
return this.then((value) => {
const obj = value;
const keys = Object.keys(obj);
const values = keys.map((k) => obj[k]);
return AveAzul.all(values).then((results) => {
const resolved = {};
keys.forEach((k, i) => {
resolved[k] = results[i];
});
return resolved;
});
});
}
/**
* Bluebird-style tapCatch() for side effects on rejection
* @param fn - Function to execute on rejection
* @returns Promise that maintains the rejection
*/
tapCatch(fn) {
return this.catch((err) => {
fn(err);
throw err;
});
}
/**
* Bluebird-style reduce() method for array reduction
* Similar to Bluebird's Promise.prototype.reduce()
* @param fn - Reducer function to apply to each element
* @param initialValue - Optional initial value
* @returns Promise that resolves with the final reduced value
*/
reduce(fn, initialValue) {
const hasInitial = arguments.length > 1;
return this.then(async (array) => {
const arr = array;
const len = arr.length;
let value;
let idx;
if (hasInitial) {
idx = 0;
value = initialValue;
}
else {
idx = 1;
value = arr[0];
}
value = isPromise(value) ? await value : value;
for (; idx < len; idx++) {
let x = arr[idx];
if (isPromise(x)) {
x = await x;
}
value = await fn(value, x, idx, len);
}
return value;
});
}
/**
* Bluebird-style throw() that returns a rejected promise with the given reason
* @param reason - Value to reject the promise with
* @returns Promise that rejects with the given reason
*/
throw(reason) {
return AveAzul.reject(reason);
}
/**
* Bluebird-style catchThrow() that catches an error and throws a new one
* @param reason - Value to reject the promise with
* @returns Promise that rejects with the new reason
*/
catchThrow(reason) {
return this.catch(() => {
throw reason;
});
}
/**
* Bluebird-style catchReturn() that catches an error and returns a value instead
* @param value - Value to return
* @returns Promise that resolves with the given value
*/
catchReturn(value) {
return this.catch(() => value);
}
/**
* Bluebird-style get() for retrieving a property value
* @param key - Key to retrieve
* @returns Promise that resolves with the property value
*/
get(key) {
return this.then((value) => value[key]);
}
/**
* Bluebird-style disposer() for resource cleanup
* @param fn - Cleanup function
* @returns Disposer object
*/
disposer(fn) {
if (typeof fn !== "function") {
throw new TypeError("Expected a function");
}
return new Disposer(fn, this);
}
/**
* Bluebird-style spread() method for handling array arguments
* Similar to Bluebird's Promise.prototype.spread()
* @param fn - Function to apply to the array arguments
* @returns Promise that resolves with the function's return value
*/
spread(fn) {
if (typeof fn !== "function") {
return AveAzul.reject(new TypeError("expecting a function but got " + fn));
}
return this.then(async (args) => {
if (Array.isArray(args)) {
for (let i = 0; i < args.length; i++) {
if (isPromise(args[i])) {
args[i] = await args[i];
}
}
return fn(...args);
}
else {
return fn(args);
}
});
}
some(count) {
return this.then((args) => {
const arr = toArray(args);
return new AveAzul((resolve, reject) => {
// If too many promises are rejected so that the promise can never become fulfilled,
// it will be immediately rejected with an AggregateError of the rejection reasons
// in the order they were thrown in.
const errors = [];
// The fulfillment value is an array with count values
// in the order they were fulfilled.
const results = [];
const len = arr.length;
let settled = false;
const addDone = (result) => {
if (settled)
return;
results.push(result);
if (results.length >= count) {
settled = true;
// Resolve with exactly count results to match Bluebird's behavior
resolve(results.slice(0, count));
}
};
const addError = (err) => {
if (settled)
return;
errors.push(err);
if (len - errors.length < count) {
settled = true;
reject(new AggregateError(errors, `aggregate error`));
}
};
for (let i = 0; i < len; i++) {
const x = arr[i];
if (isPromise(x)) {
x.then(addDone, addError);
}
else {
addDone(x);
}
}
});
});
}
/**
* Bluebird-style all() method for array operations
* Similar to Promise.all() but operates on the resolved value of this promise
* @returns Promise that resolves when all items in the array resolve
*/
all() {
return this.then((value) => {
return AveAzul.all(toArray(value));
});
}
/**
* Bluebird-style asCallback() method
* Attaches a callback to the promise and returns the promise.
* The callback is invoked when the promise is resolved or rejected.
*
* @param cb - Node.js-style callback function (err, value)
* @param options - Additional options
* @returns The same promise instance
*/
asCallback(cb, options = {}) {
if (typeof cb !== "function") {
return this;
}
const spread = options && options.spread === true;
this.then((value) => {
try {
if (spread && Array.isArray(value)) {
cb(null, ...value);
}
else {
cb(null, value);
}
}
catch (err) {
AveAzul.___throwUncaughtError(err);
}
}, (reason) => {
try {
cb(reason);
}
catch (err) {
AveAzul.___throwUncaughtError(err);
}
});
return this;
}
nodeify(cb, options) {
return this.asCallback(cb, options);
}
/**
* Bluebird-style call() method for calling a method on the resolved value
* @param methodName - Name of the method to call
* @param args - Arguments to pass to the method
* @returns Promise that resolves with the method's return value
*/
call(methodName, ...args) {
return this.then(function (obj) {
const method = obj[methodName];
if (typeof method === "function") {
return method.call(obj, ...args);
}
throw new TypeError(`${String(methodName)} is not a function`);
});
}
/**
* Catches only operational errors and passes them to the handler.
* Programmer errors (non-operational) are rethrown.
* @param handler - Function to handle operational errors
* @returns Promise with the error handled or rethrown
*/
error(handler) {
return this.catch((err) => {
if (isOperationalError(err)) {
return handler(err);
}
throw err;
});
}
/**
* Static helper methods
*/
/**
* Bluebird-style delay() that resolves after specified milliseconds
* @param ms - Milliseconds to delay
* @param value - Optional value to resolve with
* @returns Promise that resolves after the delay
*/
static delay(ms, value) {
if (value === undefined) {
return AveAzul.resolve(xaa.delay(ms));
}
return AveAzul.resolve(xaa.delay(ms, value));
}
/**
* Bluebird-style map() for array operations
* @param value - Array to map over
* @param fn - Map function to apply to each element
* @returns Promise that resolves with the mapped array
*/
static map(value, fn, options = { concurrency: 50 }) {
return AveAzul.resolve(value).map(fn, options);
}
/**
* Bluebird-style mapSeries() for array operations
* @param value - Array to map over
* @param fn - Map function to apply to each element
* @returns Promise that resolves with the mapped array
*/
static mapSeries(value, fn) {
return AveAzul.map(value, fn, { concurrency: 1 });
}
/**
* Bluebird-style try() for wrapping sync/async functions
* @param fn - Function to execute
* @returns Promise that resolves with the function's return value
*/
static try(fn) {
return AveAzul.resolve(xaa.wrap(fn));
}
/**
* Bluebird-style props() for object properties
* @param obj - Object with promise values
* @returns Promise that resolves with an object of resolved values
*/
static props(obj) {
const keys = Object.keys(obj);
const values = keys.map((k) => obj[k]);
return AveAzul.all(values).then((results) => {
const resolved = {};
keys.forEach((k, i) => {
resolved[k] = results[i];
});
return resolved;
});
}
/**
* Bluebird-style defer() for creating a deferred promise
* @returns Deferred object with promise, resolve, and reject methods
*/
static defer() {
return xaa.makeDefer(AveAzul);
}
/**
* Bluebird-style each() for array iteration
* @param items - Array to iterate over
* @param fn - Iterator function to call for each item
* @returns Promise that resolves when iteration is complete
*/
static each(items, fn) {
return AveAzul.resolve(items).each(fn);
}
/**
* Bluebird-style reduce() for array reduction
* @param array - Array to reduce
* @param fn - Reducer function (value, item, index, length)
* @param initialValue - Optional initial value
* @returns Promise that resolves with the final reduced value
*/
static reduce(array, fn, initialValue) {
if (arguments.length > 2) {
return AveAzul.resolve(array).reduce(fn, initialValue);
}
return AveAzul.resolve(array).reduce(fn);
}
/**
* Bluebird-style promisify() for converting callback-based functions to promises
* @param fn - Function to promisify
* @param options - Options object
* @returns Promisified function
*/
static promisify(fn, options) {
return promisify(fn, {
...options,
Promise: AveAzul,
});
}
/**
* Bluebird-style promisifyAll() for converting callback-based functions to promises
* @param target - Object to promisify
* @param options - Options object
* @returns Object with promisified methods
*/
static promisifyAll(target, options) {
return promisifyAll(target, {
...options,
Promise: AveAzul,
});
}
/**
* Bluebird-style method() for creating a method that returns a promise
* @param fn - Function to create a method for
* @returns Method function that returns a promise
*/
static method(fn) {
return function (...args) {
return new AveAzul((resolve, reject) => {
try {
const result = fn.call(this, ...args);
resolve(result);
}
catch (error) {
reject(error);
}
});
};
}
/**
* Bluebird-style using() for resource management. There is only a static version of this method.
* After the handler finish and returns, regardless of whether it resolves or rejects, the resources will be disposed.
*
* @param resources - Resource disposers, either an array of disposers or a variadic argument list
* @param args - Handler function that will receive the resources as arguments
* @returns Promise that resolves with handler result
*/
static using(resources, ...args) {
if (args.length === 0) {
throw new TypeError("resrouces and handler function required");
}
if (Array.isArray(resources)) {
if (args.length > 1) {
throw new TypeError("only two arguments are allowed when passing an array of resources");
}
return using(resources, args[0], AveAzul, true);
}
const handler = args.pop();
return using([resources, ...args], handler, AveAzul, false);
}
/**
* Bluebird-style join() for joining promises
*
* @param args - Promises to join
* @returns Promise that resolves with the handler's return value
*/
static join(...args) {
if (args.length > 1 && typeof args[args.length - 1] === "function") {
const handler = args.pop();
return AveAzul.all(args).then((results) => handler(...results));
}
else {
return AveAzul.all(args);
}
}
/**
* Bluebird-style fromCallback() for converting callback-based functions to promises
* @param fn - Function to convert
* @param options - Options object
* @returns Promise that resolves with the function's return value
*/
static fromCallback(fn, options) {
return new AveAzul((resolve, reject) => {
try {
fn((err, ...args) => {
if (err) {
reject(err);
}
else {
if (options && options.multiArgs) {
resolve(args);
}
else {
resolve(args[0]);
}
}
});
}
catch (err) {
reject(err);
}
});
}
/**
* Bluebird-style some() for waiting for some promises to resolve
* @param promises - Array or iterable of promises
* @param count - Number of promises that need to resolve
* @returns Promise that resolves when count promises have resolved
*/
static some(promises, count) {
return AveAzul.resolve(promises).some(count);
}
/**
* Bluebird-style any() for waiting for any promise to resolve
* @param args - Array or iterable of promises
* @returns Promise that resolves with the first resolved value
*/
/* v8 ignore next 4 */
static any(args) {
// Will be overwritten by addStaticAny
throw new Error("any not initialized");
}
}
AveAzul.fromNode = AveAzul.fromCallback;
/**
* When fatal error and AveAzul needs to crash the process,
* this method is used to throw the error.
*
* @param error - The error to throw.
*/
AveAzul.___throwUncaughtError = triggerUncaughtException;
AveAzul.OperationalError = OperationalError;
AveAzul.isOperationalError = isOperationalError;
AveAzul.isProgrammerError = isProgrammerError;
AveAzul.Disposer = Disposer;
// Setup the any method
import { addStaticAny } from "./any.js";
addStaticAny(AveAzul);
// Setup the not implemented methods
import { setupNotImplemented } from "./not-implemented.js";
setupNotImplemented(AveAzul);
export { AveAzul };
export default AveAzul;
//# sourceMappingURL=aveazul.js.map