UNPKG

@devgrid/common

Version:
264 lines 8.55 kB
import { entries } from "./entries"; import { noop, truly } from "./primitives"; import { isNumber, isPromise, isFunction } from "./predicates"; export const defer = () => { const deferred = {}; deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve; deferred.reject = reject; }); return deferred; }; export const delay = (ms, value, options) => new Promise((resolve) => { const timer = setTimeout(resolve, ms, value); if (options?.unref && typeof timer.unref === "function") { timer.unref(); } }); export const timeout = (promise, ms, options = {}) => { if (!isPromise(promise)) { throw new TypeError("The first argument must be a promise"); } if (!isNumber(ms) || ms <= 0) { throw new TypeError("Timeout must be a positive number"); } return new Promise((resolve, reject) => { let timeoutId; const cleanup = () => { if (timeoutId) { clearTimeout(timeoutId); timeoutId = null; } if (options.signal) { options.signal.removeEventListener('abort', onAbort); } }; const onAbort = () => { cleanup(); reject(new Error('Timeout aborted')); }; if (options.signal) { if (options.signal.aborted) { onAbort(); return; } options.signal.addEventListener('abort', onAbort, { once: true }); } timeoutId = setTimeout(() => { cleanup(); reject(new Error(`Timeout of ${ms}ms exceeded`)); }, ms); if (options.unref && typeof timeoutId.unref === 'function') { timeoutId.unref(); } promise.then((value) => { cleanup(); resolve(value); }, (error) => { cleanup(); reject(error); }); }); }; export const nodeify = (promise, cb) => { if (!isPromise(promise)) { throw new TypeError("The first argument must be a promise"); } if (!isFunction(cb)) { return promise; } promise.then((x) => { cb(null, x); }, (y) => { cb(y); }); return promise; }; export const callbackify = (fn) => { if (!isFunction(fn)) { throw new TypeError("The first argument must be a function"); } return function _(...args) { if (args.length && isFunction(args[args.length - 1])) { const cb = args.pop(); return nodeify(fn.apply(this, args), cb); } return fn.apply(this, args); }; }; const processFn = (fn, context, args, multiArgs, resolve, reject) => { if (multiArgs) { args.push((...result) => { if (result[0]) { reject(result); } else { result.shift(); resolve(result); } }); } else { args.push((err, result) => { if (err) { reject(err); } else { resolve(result); } }); } fn.apply(context, args); }; export const promisify = (fn, options) => { if (!isFunction(fn)) { throw new TypeError("The first argument must be a function"); } return options && options.context ? (...args) => new Promise((resolve, reject) => { processFn(fn, options.context, args, options && Boolean(options.multiArgs), resolve, reject); }) : function _(...args) { return new Promise((resolve, reject) => { processFn(fn, this, args, Boolean(options?.multiArgs), resolve, reject); }); }; }; export const promisifyAll = (source, options) => { const suffix = options && options.suffix ? options.suffix : "Async"; const filter = options && typeof options.filter === "function" ? options.filter : truly; if (isFunction(source)) { return promisify(source, options); } const target = Object.create(source); for (const [key, value] of entries(source, { all: true })) { if (isFunction(value) && filter(key)) { target[`${key}${suffix}`] = promisify(value, options); } } return target; }; const _finally = (promise, onFinally) => { onFinally = onFinally || noop; return promise.then((val) => new Promise((resolve) => { resolve(onFinally()); }).then(() => val), (err) => new Promise((resolve) => { resolve(onFinally()); }).then(() => { throw err; })); }; export { _finally as finally }; export const retry = async (callback, options) => { if (!callback || !options) { throw new Error("requires a callback and an options set or a number"); } const opts = isNumber(options) ? { max: options } : options; const config = { $current: opts.$current || 1, max: opts.max, timeout: opts.timeout, match: Array.isArray(opts.match) ? opts.match : opts.match ? [opts.match] : [], backoffBase: opts.backoffBase ?? 100, backoffExponent: opts.backoffExponent || 1.1, report: opts.report || null, name: opts.name || callback.name || "unknown" }; const shouldRetry = (error, attempt) => { if (attempt >= config.max) return false; if (config.match.length === 0) return true; return config.match.some(match => match === error.toString() || match === error.message || (isFunction(match) && error instanceof match) || (match instanceof RegExp && (match.test(error.message) || match.test(error.toString())))); }; let lastError = null; while (true) { try { if (config.report) { config.report(`Attempt ${config.name} #${config.$current}`, config); } let result = callback({ current: config.$current }); if (isPromise(result)) { if (config.timeout) { result = await timeout(result, config.timeout); } else { result = await result; } } if (config.report) { config.report(`Success ${config.name} #${config.$current}`, config); } return result; } catch (error) { lastError = error; if (config.report) { config.report(`Failed ${config.name} #${config.$current}: ${error.toString()}`, config, error); } if (!shouldRetry(error, config.$current)) { throw lastError; } const retryDelay = Math.floor(config.backoffBase * Math.pow(config.backoffExponent ?? 1.1, config.$current - 1)); config.$current++; if (retryDelay > 0) { if (config.report) { config.report(`Delaying retry of ${config.name} by ${retryDelay}ms`, config); } await delay(retryDelay); } } } }; export const props = async (obj) => { const result = {}; await Promise.all(Object.keys(obj).map(async (key) => { Object.defineProperty(result, key, { enumerable: true, value: await obj[key], }); })); return result; }; const try_ = (fn, ...args) => new Promise((resolve) => { resolve(fn(...args)); }); export { try_ as try }; export const universalify = (fn) => Object.defineProperties(function _(...args) { if (isFunction(args[args.length - 1])) { return fn.apply(this, args); } return new Promise((resolve, reject) => { args.push((err, res) => { if (err) { reject(err); } else { resolve(res); } }); fn.apply(this, args); }); }, { name: { value: fn.name, }, ...Object.keys(fn).reduce((props_, k) => { props_[k] = { enumerable: true, value: fn[k], }; return props_; }, {}), }); export const universalifyFromPromise = (fn) => Object.defineProperty(function _(...args) { const cb = args[args.length - 1]; if (!isFunction(cb)) { return fn.apply(this, args); } return fn.apply(this, args).then((r) => cb(null, r), cb); }, "name", { value: fn.name }); //# sourceMappingURL=promise.js.map