index.js 5.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214
  1. const { promisify, callbackify } = require('util')
  2. const pLimit = require('p-limit')
  3. module.exports = {
  4. promisify,
  5. promisifyAll,
  6. promisifyClass,
  7. promisifyMultiResult,
  8. callbackify,
  9. callbackifyAll,
  10. callbackifyMultiResult,
  11. expressify,
  12. promiseMapWithLimit,
  13. }
  14. /**
  15. * Promisify all functions in a module.
  16. *
  17. * This is meant to be used only when all functions in the module are async
  18. * callback-style functions.
  19. *
  20. * It's very much tailored to our current module structure. In particular, it
  21. * binds `this` to the module when calling the function in order not to break
  22. * modules that call sibling functions using `this`.
  23. *
  24. * This will not magically fix all modules. Special cases should be promisified
  25. * manually.
  26. *
  27. * The second argument is a bag of options:
  28. *
  29. * - without: an array of function names that shouldn't be promisified
  30. *
  31. * - multiResult: an object whose keys are function names and values are lists
  32. * of parameter names. This is meant for functions that invoke their callbacks
  33. * with more than one result in separate parameters. The promisifed function
  34. * will return these results as a single object, with each result keyed under
  35. * the corresponding parameter name.
  36. */
  37. function promisifyAll(module, opts = {}) {
  38. const { without = [], multiResult = {} } = opts
  39. const promises = {}
  40. for (const propName of Object.getOwnPropertyNames(module)) {
  41. if (without.includes(propName)) {
  42. continue
  43. }
  44. const propValue = module[propName]
  45. if (typeof propValue !== 'function') {
  46. continue
  47. }
  48. if (multiResult[propName] != null) {
  49. promises[propName] = promisifyMultiResult(
  50. propValue,
  51. multiResult[propName]
  52. ).bind(module)
  53. } else {
  54. promises[propName] = promisify(propValue).bind(module)
  55. }
  56. }
  57. return promises
  58. }
  59. /**
  60. * Promisify all methods in a class.
  61. *
  62. * Options are the same as for promisifyAll
  63. */
  64. function promisifyClass(cls, opts = {}) {
  65. const promisified = class extends cls {}
  66. const { without = [], multiResult = {} } = opts
  67. for (const propName of Object.getOwnPropertyNames(cls.prototype)) {
  68. if (propName === 'constructor' || without.includes(propName)) {
  69. continue
  70. }
  71. const propValue = cls.prototype[propName]
  72. if (typeof propValue !== 'function') {
  73. continue
  74. }
  75. if (multiResult[propName] != null) {
  76. promisified.prototype[propName] = promisifyMultiResult(
  77. propValue,
  78. multiResult[propName]
  79. )
  80. } else {
  81. promisified.prototype[propName] = promisify(propValue)
  82. }
  83. }
  84. return promisified
  85. }
  86. /**
  87. * Promisify a function that returns multiple results via additional callback
  88. * parameters.
  89. *
  90. * The promisified function returns the results in a single object whose keys
  91. * are the names given in the array `resultNames`.
  92. *
  93. * Example:
  94. *
  95. * function f(callback) {
  96. * return callback(null, 1, 2, 3)
  97. * }
  98. *
  99. * const g = promisifyMultiResult(f, ['a', 'b', 'c'])
  100. *
  101. * const result = await g() // returns {a: 1, b: 2, c: 3}
  102. */
  103. function promisifyMultiResult(fn, resultNames) {
  104. function promisified(...args) {
  105. return new Promise((resolve, reject) => {
  106. try {
  107. fn.bind(this)(...args, (err, ...results) => {
  108. if (err != null) {
  109. return reject(err)
  110. }
  111. const promiseResult = {}
  112. for (let i = 0; i < resultNames.length; i++) {
  113. promiseResult[resultNames[i]] = results[i]
  114. }
  115. resolve(promiseResult)
  116. })
  117. } catch (err) {
  118. reject(err)
  119. }
  120. })
  121. }
  122. return promisified
  123. }
  124. /**
  125. * Reverse of `promisifyAll`.
  126. *
  127. * Callbackify all async functions in a module and return them in an object. In
  128. * contrast with `promisifyAll`, all other exports from the module are added to
  129. * the result.
  130. *
  131. * This is meant to be used like this:
  132. *
  133. * const MyPromisifiedModule = {...}
  134. * module.exports = {
  135. * ...callbackifyAll(MyPromisifiedModule),
  136. * promises: MyPromisifiedModule
  137. * }
  138. *
  139. * @param {Object} module - The module to callbackify
  140. * @param {Object} opts - Options
  141. * @param {Object} opts.multiResult - Spec of methods to be callbackified with
  142. * callbackifyMultiResult()
  143. */
  144. function callbackifyAll(module, opts = {}) {
  145. const { multiResult = {} } = opts
  146. const callbacks = {}
  147. for (const propName of Object.getOwnPropertyNames(module)) {
  148. const propValue = module[propName]
  149. if (typeof propValue === 'function') {
  150. if (propValue.constructor.name === 'AsyncFunction') {
  151. if (multiResult[propName] != null) {
  152. callbacks[propName] = callbackifyMultiResult(
  153. propValue,
  154. multiResult[propName]
  155. ).bind(module)
  156. } else {
  157. callbacks[propName] = callbackify(propValue).bind(module)
  158. }
  159. } else {
  160. callbacks[propName] = propValue.bind(module)
  161. }
  162. } else {
  163. callbacks[propName] = propValue
  164. }
  165. }
  166. return callbacks
  167. }
  168. /**
  169. * Reverse the effect of `promisifyMultiResult`.
  170. *
  171. * This is meant for providing a temporary backward compatible callback
  172. * interface while we migrate to promises.
  173. */
  174. function callbackifyMultiResult(fn, resultNames) {
  175. function callbackified(...args) {
  176. const [callback] = args.splice(-1)
  177. fn(...args)
  178. .then(result => {
  179. const cbResults = resultNames.map(resultName => result[resultName])
  180. callback(null, ...cbResults)
  181. })
  182. .catch(err => {
  183. callback(err)
  184. })
  185. }
  186. return callbackified
  187. }
  188. /**
  189. * Transform an async function into an Express middleware
  190. *
  191. * Any error will be passed to the error middlewares via `next()`
  192. */
  193. function expressify(fn) {
  194. return (req, res, next) => {
  195. fn(req, res, next).catch(next)
  196. }
  197. }
  198. /**
  199. * Map values in `array` with the async function `fn`
  200. *
  201. * Limit the number of unresolved promises to `concurrency`.
  202. */
  203. function promiseMapWithLimit(concurrency, array, fn) {
  204. const limit = pLimit(concurrency)
  205. return Promise.all(array.map(x => limit(() => fn(x))))
  206. }