| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283 |
- const { promisify, callbackify } = require('node:util')
- const pLimit = require('p-limit')
- module.exports = {
- promisify,
- promisifyAll,
- promisifyClass,
- promisifyMultiResult,
- callbackify,
- callbackifyAll,
- callbackifyClass,
- callbackifyMultiResult,
- expressify,
- expressifyErrorHandler,
- promiseMapWithLimit,
- promiseMapSettledWithLimit,
- }
- /**
- * Promisify all functions in a module.
- *
- * This is meant to be used only when all functions in the module are async
- * callback-style functions.
- *
- * It's very much tailored to our current module structure. In particular, it
- * binds `this` to the module when calling the function in order not to break
- * modules that call sibling functions using `this`.
- *
- * This will not magically fix all modules. Special cases should be promisified
- * manually.
- *
- * The second argument is a bag of options:
- *
- * - without: an array of function names that shouldn't be promisified
- *
- * - multiResult: an object whose keys are function names and values are lists
- * of parameter names. This is meant for functions that invoke their callbacks
- * with more than one result in separate parameters. The promisifed function
- * will return these results as a single object, with each result keyed under
- * the corresponding parameter name.
- */
- function promisifyAll(module, opts = {}) {
- const { without = [], multiResult = {} } = opts
- const promises = {}
- for (const propName of Object.getOwnPropertyNames(module)) {
- if (without.includes(propName)) {
- continue
- }
- const propValue = module[propName]
- if (typeof propValue !== 'function') {
- continue
- }
- if (multiResult[propName] != null) {
- promises[propName] = promisifyMultiResult(
- propValue,
- multiResult[propName]
- ).bind(module)
- } else {
- promises[propName] = promisify(propValue).bind(module)
- }
- }
- return promises
- }
- /**
- * Promisify all methods in a class.
- *
- * Options are the same as for promisifyAll
- */
- function promisifyClass(cls, opts = {}) {
- const promisified = class extends cls {}
- const { without = [], multiResult = {} } = opts
- for (const propName of Object.getOwnPropertyNames(cls.prototype)) {
- if (propName === 'constructor' || without.includes(propName)) {
- continue
- }
- const propValue = cls.prototype[propName]
- if (typeof propValue !== 'function') {
- continue
- }
- if (multiResult[propName] != null) {
- promisified.prototype[propName] = promisifyMultiResult(
- propValue,
- multiResult[propName]
- )
- } else {
- promisified.prototype[propName] = promisify(propValue)
- }
- }
- return promisified
- }
- /**
- * Promisify a function that returns multiple results via additional callback
- * parameters.
- *
- * The promisified function returns the results in a single object whose keys
- * are the names given in the array `resultNames`.
- *
- * Example:
- *
- * function f(callback) {
- * return callback(null, 1, 2, 3)
- * }
- *
- * const g = promisifyMultiResult(f, ['a', 'b', 'c'])
- *
- * const result = await g() // returns {a: 1, b: 2, c: 3}
- */
- function promisifyMultiResult(fn, resultNames) {
- function promisified(...args) {
- return new Promise((resolve, reject) => {
- try {
- fn.bind(this)(...args, (err, ...results) => {
- if (err != null) {
- return reject(err)
- }
- const promiseResult = {}
- for (let i = 0; i < resultNames.length; i++) {
- promiseResult[resultNames[i]] = results[i]
- }
- resolve(promiseResult)
- })
- } catch (err) {
- reject(err)
- }
- })
- }
- return promisified
- }
- /**
- * Reverse of `promisifyAll`.
- *
- * Callbackify all async functions in a module and return them in an object. In
- * contrast with `promisifyAll`, all other exports from the module are added to
- * the result.
- *
- * This is meant to be used like this:
- *
- * const MyPromisifiedModule = {...}
- * module.exports = {
- * ...callbackifyAll(MyPromisifiedModule),
- * promises: MyPromisifiedModule
- * }
- *
- * @param {Object} module - The module to callbackify
- * @param {Object} opts - Options
- * @param {Array<string>} [opts.without] - Array of method names to exclude from
- * being callbackified
- * @param {Object} [opts.multiResult] - Spec of methods to be callbackified with
- * callbackifyMultiResult()
- */
- function callbackifyAll(module, opts = {}) {
- const { without = [], multiResult = {} } = opts
- const callbacks = {}
- for (const propName of Object.getOwnPropertyNames(module)) {
- if (without.includes(propName)) {
- continue
- }
- const propValue = module[propName]
- if (typeof propValue === 'function') {
- if (propValue.constructor.name === 'AsyncFunction') {
- if (multiResult[propName] != null) {
- callbacks[propName] = callbackifyMultiResult(
- propValue,
- multiResult[propName]
- ).bind(module)
- } else {
- callbacks[propName] = callbackify(propValue).bind(module)
- }
- } else {
- callbacks[propName] = propValue.bind(module)
- }
- } else {
- callbacks[propName] = propValue
- }
- }
- return callbacks
- }
- /**
- * Callbackify all methods in a class.
- *
- * Options are the same as for callbackifyAll
- */
- function callbackifyClass(cls, opts = {}) {
- const callbackified = class extends cls {}
- const { without = [], multiResult = {} } = opts
- for (const propName of Object.getOwnPropertyNames(cls.prototype)) {
- if (propName === 'constructor' || without.includes(propName)) {
- continue
- }
- const propValue = cls.prototype[propName]
- if (typeof propValue !== 'function') {
- continue
- }
- if (multiResult[propName] != null) {
- callbackified.prototype[propName] = callbackifyMultiResult(
- propValue,
- multiResult[propName]
- )
- } else {
- callbackified.prototype[propName] = callbackify(propValue)
- }
- }
- return callbackified
- }
- /**
- * Reverse the effect of `promisifyMultiResult`.
- *
- * This is meant for providing a temporary backward compatible callback
- * interface while we migrate to promises.
- */
- function callbackifyMultiResult(fn, resultNames) {
- function callbackified(...args) {
- const [callback] = args.splice(-1)
- fn.apply(this, args)
- .then(result => {
- const cbResults = resultNames.map(resultName => result[resultName])
- callback(null, ...cbResults)
- })
- .catch(err => {
- callback(err)
- })
- }
- return callbackified
- }
- /**
- * Transform an async function into an Express middleware
- *
- * Any error will be passed to the error middlewares via `next()`
- */
- function expressify(fn) {
- return (req, res, next) => {
- return fn(req, res, next).catch(next)
- }
- }
- /**
- * Transform an async function into an Error Handling Express middleware
- *
- * Any error will be passed to the error middlewares via `next()`
- */
- function expressifyErrorHandler(fn) {
- return (err, req, res, next) => {
- fn(err, req, res, next).catch(next)
- }
- }
- /**
- * Map values in `array` with the async function `fn`
- *
- * Limit the number of unresolved promises to `concurrency`.
- * @template T
- * @template V
- * @param {number} concurrency
- * @param {Array<T>} array
- * @param {(arg: T) => Promise<V>} fn
- * @return {Promise<Array<Awaited<V>>>}
- */
- async function promiseMapWithLimit(concurrency, array, fn) {
- const limit = pLimit(concurrency)
- return await Promise.all(array.map(x => limit(() => fn(x))))
- }
- /**
- * Map values in `array` with the async function `fn`
- *
- * Limit the number of unresolved promises to `concurrency`.
- *
- * @template T, U
- * @param {number} concurrency
- * @param {Array<T>} array
- * @param {(T) => Promise<U>} fn
- * @return {Promise<Array<PromiseSettledResult<U>>>}
- */
- function promiseMapSettledWithLimit(concurrency, array, fn) {
- const limit = pLimit(concurrency)
- return Promise.allSettled(array.map(x => limit(() => fn(x))))
- }
|