diff --git a/lib/util.js b/lib/util.js index 91e17cb651c0f9..6ca3124d5290aa 100644 --- a/lib/util.js +++ b/lib/util.js @@ -1045,3 +1045,58 @@ process.versions[exports.inspect.custom] = (depth) => exports.format(JSON.parse(JSON.stringify(process.versions))); exports.promisify = internalUtil.promisify; + +// Callbackify section +const CBFY_ERR = 'The last argument to the callbackified function must be a ' + + 'callback style function'; + + +function callbackifyOnRejected(reason, cb) { + // !reason guard inspired by BlueBird https://goo.gl/t5IS6M + // Because `null` is a special error value in callbacks which means "no error + // occurred". We error-wrap so the callback consumer can distinguish between + // "the promise rejected with null" or "the promise fulfilled with undefined". + if (!reason) { + const newRej = new Error(reason + ''); + newRej.cause = reason; + reason = newRej; + } + return cb(reason); +} + + +function assertLastArgIsFunction(args) { + const cb = args.pop(); + if (typeof cb !== 'function') throw new TypeError(CBFY_ERR); + return cb; +} + + +/** + * @function callbackify + * @param {function(): Promise} asyncEndpoint + * @public + */ +function callbackify(asyncEndpoint) { + if (typeof asyncEndpoint !== 'function') + throw new TypeError('The argument to callbackify() must be a function'); + + // We DO NOT return the promise as it gives the user a false sense that + // the promise is actually somehow related to the callback's execution + // and that the callback throwing will reject the promise. + function callbackified(...args) { + const cb = assertLastArgIsFunction(args); + asyncEndpoint.call(this, ...args) + .then((ret) => process.nextTick(cb, null, ret), + (rej) => process.nextTick(callbackifyOnRejected, rej, cb) + ); + } + + Object.setPrototypeOf(callbackified, Object.getPrototypeOf(asyncEndpoint)); + Object.defineProperties(callbackified, + Object.getOwnPropertyDescriptors(asyncEndpoint)); + return callbackified; +} + +exports.callbackify = callbackify; +// End Callbackify