promises.js 4.5 KB

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