2023-10-25 05:24:35 -04:00
|
|
|
const { promisify, callbackify } = require('util')
|
|
|
|
const pLimit = require('p-limit')
|
|
|
|
|
|
|
|
module.exports = {
|
|
|
|
promisify,
|
|
|
|
promisifyAll,
|
|
|
|
promisifyClass,
|
|
|
|
promisifyMultiResult,
|
|
|
|
callbackify,
|
|
|
|
callbackifyAll,
|
2024-03-08 04:27:18 -05:00
|
|
|
callbackifyClass,
|
2023-10-25 05:24:35 -04:00
|
|
|
callbackifyMultiResult,
|
|
|
|
expressify,
|
2024-02-22 09:59:17 -05:00
|
|
|
expressifyErrorHandler,
|
2023-10-25 05:24:35 -04:00
|
|
|
promiseMapWithLimit,
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* 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
|
2024-02-19 04:43:28 -05:00
|
|
|
* @param {Array<string>} opts.without - Array of method names to exclude from
|
|
|
|
* being callbackified
|
2023-10-25 05:24:35 -04:00
|
|
|
* @param {Object} opts.multiResult - Spec of methods to be callbackified with
|
|
|
|
* callbackifyMultiResult()
|
|
|
|
*/
|
|
|
|
function callbackifyAll(module, opts = {}) {
|
2024-02-19 04:43:28 -05:00
|
|
|
const { without = [], multiResult = {} } = opts
|
2023-10-25 05:24:35 -04:00
|
|
|
const callbacks = {}
|
|
|
|
for (const propName of Object.getOwnPropertyNames(module)) {
|
2024-02-19 04:43:28 -05:00
|
|
|
if (without.includes(propName)) {
|
|
|
|
continue
|
|
|
|
}
|
2023-10-25 05:24:35 -04:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2024-03-08 04:27:18 -05:00
|
|
|
/**
|
|
|
|
* 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
|
|
|
|
}
|
|
|
|
|
2023-10-25 05:24:35 -04:00
|
|
|
/**
|
|
|
|
* 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)
|
2024-03-08 04:27:18 -05:00
|
|
|
fn.apply(this, args)
|
2023-10-25 05:24:35 -04:00
|
|
|
.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) => {
|
|
|
|
fn(req, res, next).catch(next)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-02-22 09:59:17 -05:00
|
|
|
/**
|
|
|
|
* 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)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-25 05:24:35 -04:00
|
|
|
/**
|
|
|
|
* Map values in `array` with the async function `fn`
|
|
|
|
*
|
|
|
|
* Limit the number of unresolved promises to `concurrency`.
|
|
|
|
*/
|
|
|
|
function promiseMapWithLimit(concurrency, array, fn) {
|
|
|
|
const limit = pLimit(concurrency)
|
|
|
|
return Promise.all(array.map(x => limit(() => fn(x))))
|
|
|
|
}
|