UNPKG

aveazul

Version:

Bluebird drop-in replacement built on native Promise

646 lines (579 loc) 19.1 kB
"use strict"; const xaa = require("xaa"); const { promisify } = require("./promisify"); const { promisifyAll } = require("./promisify-all"); const { Disposer } = require("./disposer"); const { using } = require("./using"); const { isPromise, triggerUncaughtException, toArray } = require("./util"); const { AggregateError } = require("@jchip/error"); const { OperationalError, isOperationalError } = require("./operational-error"); /** * @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 {Function} fn - Function to execute with the resolved value * @returns {Promise} 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 {Function} fn - Filter function to apply to each element * @returns {Promise} 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 {Function} fn - Map function to apply to each element * @returns {Promise} 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 {Function} fn - Map function to apply to each element * @returns {Promise} 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} Promise that resolves with the new value */ return(value) { return this.then(() => value); } /** * Bluebird-style any() method for waiting for any promises to resolve * @param {Array|Iterable} promises - Array or iterable of promises * @returns {Promise} 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 {Function} fn - Function to execute for each element * @returns {Promise} Promise that resolves when iteration is complete */ each(fn) { return this.then(async (value) => { const result = []; for (let i = 0; i < value.length; i++) { let x = value[i]; if (isPromise(x)) { x = await x; } await fn(x, i, value.length); result.push(x); } return result; }); } /** * Bluebird-style delay() method * @param {number} ms - Milliseconds to delay * @returns {Promise} Promise that resolves after the delay */ delay(ms) { return xaa.delay(ms); } /** * Bluebird-style timeout() method * @param {number} ms - Milliseconds before timeout * @param {string} [message] - Optional error message * @returns {Promise} 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 * @param {Object} obj - Object with promise values * @returns {Promise} Promise that resolves with an object of resolved values */ props() { return this.then((value) => { const keys = Object.keys(value); const values = keys.map((k) => value[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 {Function} fn - Function to execute on rejection * @returns {Promise} 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 {Function} fn - Reducer function to apply to each element * @param {*} [initialValue] - Optional initial value * @returns {Promise} Promise that resolves with the final reduced value */ reduce(fn, initialValue) { const hasInitial = arguments.length > 1; return this.then(async (array) => { const len = array.length; let value; let idx; if (hasInitial) { idx = 0; value = initialValue; } else { idx = 1; value = array[0]; } value = isPromise(value) ? await value : value; for (; idx < len; idx++) { let x = array[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} 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} 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} Promise that resolves with the given value */ catchReturn(value) { return this.catch(() => value); } /** * Bluebird-style get() for retrieving a property value * @param {string|number} key - Key to retrieve * @returns {Promise} Promise that resolves with the property value */ get(key) { return this.then((value) => value[key]); } /** * Bluebird-style disposer() for resource cleanup * @param {Function} fn - Cleanup function * @returns {Disposer} 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 {Function} fn - Function to apply to the array arguments * @returns {Promise} 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) => { args = 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 = args.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 = args[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} 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 {Function} cb - Node.js-style callback function (err, value) * @param {Object} [options] - Additional options * @param {boolean} [options.spread=false] - Pass array values as arguments to callback * @returns {Promise} 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 {string} methodName - Name of the method to call * @param {...any} args - Arguments to pass to the method * @returns {Promise} Promise that resolves with the method's return value */ call(methodName, ...args) { return this.then(function (obj) { return obj[methodName].call(obj, ...args); }); } /** * Catches only operational errors and passes them to the handler. * Programmer errors (non-operational) are rethrown. * @param {Function} handler - Function to handle operational errors * @returns {Promise} - 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 {number} ms - Milliseconds to delay * @param {*} [value] - Optional value to resolve with * @returns {Promise} Promise that resolves after the delay */ AveAzul.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 {Array} value - Array to map over * @param {Function} fn - Map function to apply to each element * @returns {Promise} Promise that resolves with the mapped array */ AveAzul.map = (value, fn, options = { concurrency: 50 }) => AveAzul.resolve(value).map(fn, options); /** * Bluebird-style mapSeries() for array operations * @param {Array} value - Array to map over * @param {Function} fn - Map function to apply to each element * @returns {Promise} Promise that resolves with the mapped array */ AveAzul.mapSeries = (value, fn) => AveAzul.map(value, fn, { concurrency: 1 }); /** * Bluebird-style try() for wrapping sync/async functions * @param {Function} fn - Function to execute * @returns {Promise} Promise that resolves with the function's return value */ AveAzul.try = (fn) => AveAzul.resolve(xaa.wrap(fn)); /** * Bluebird-style props() for object properties * @param {Object} obj - Object with promise values * @returns {Promise} Promise that resolves with an object of resolved values */ AveAzul.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 {Object} Deferred object with promise, resolve, and reject methods */ AveAzul.defer = () => { return xaa.makeDefer(AveAzul); }; /** * Bluebird-style each() for array iteration * @param {Array} items - Array to iterate over * @param {Function} fn - Iterator function to call for each item * @returns {Promise} Promise that resolves when iteration is complete */ AveAzul.each = function (items, fn) { return AveAzul.resolve(items).each(fn); }; /** * Bluebird-style reduce() for array reduction * @param {Array} array - Array to reduce * @param {Function} fn - Reducer function (value, item, index, length) * @param {*} [initialValue] - Optional initial value * @returns {Promise} Promise that resolves with the final reduced value */ AveAzul.reduce = function (array, ...args) { return AveAzul.resolve(array).reduce(...args); }; /** * Bluebird-style promisify() for converting callback-based functions to promises * @param {Function} fn - Function to promisify * @param {Object} [options] - Options object * @returns {Function} Promisified function */ AveAzul.promisify = (fn, options) => { return promisify(fn, { ...options, Promise: AveAzul, }); }; /** * Bluebird-style promisifyAll() for converting callback-based functions to promises * @param {Object} target - Object to promisify * @param {Object} [options] - Options object * @returns {Object} Object with promisified methods */ AveAzul.promisifyAll = (target, options) => { return promisifyAll(target, { ...options, Promise: AveAzul }); }; /** * Bluebird-style method() for creating a method that returns a promise * @param {Function} fn - Function to create a method for * @returns {Function} Method function that returns a promise */ AveAzul.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 {Disposer|Array<Disposer>} resources - Resource disposers, either an array of disposers or a variadic argument list * @param {Function} handler - Handler function that will receive the resources as arguments * @returns {Promise} Promise that resolves with handler result */ AveAzul.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 {...Promise} args - Promises to join * @param {Function} handler - Handler function to apply to the joined results * @returns {Promise} Promise that resolves with the handler's return value */ AveAzul.join = function (...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); } }; function 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); } }); } AveAzul.fromNode = fromCallback; /** * Bluebird-style fromCallback() for converting callback-based functions to promises * @param {Function} fn - Function to convert * @param {Object} [options] - Options object * @returns {Promise} Promise that resolves with the function's return value */ AveAzul.fromCallback = fromCallback; /** * @description * When fatal error and AveAzul needs to crash the process, * this method is used to throw the error. * * @param {Error} error - The error to throw. */ AveAzul.___throwUncaughtError = triggerUncaughtException; /** * Bluebird-style some() for waiting for some promises to resolve * @param {Array|Iterable} promises - Array or iterable of promises * @param {number} count - Number of promises that need to resolve * @returns {Promise} Promise that resolves when count promises have resolved */ AveAzul.some = function (promises, count) { return AveAzul.resolve(promises).some(count); }; const { addStaticAny } = require("./any"); addStaticAny(AveAzul); // Setup the not implemented methods const { setupNotImplemented } = require("./not-implemented"); setupNotImplemented(AveAzul); // Add these static properties after the class definition AveAzul.OperationalError = OperationalError; module.exports = AveAzul;