-
-
Notifications
You must be signed in to change notification settings - Fork 31.6k
util: add options
to util.promisify()
#43088
New issue
Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? # to your account
Conversation
`options.resolveArray` allows to resolve all callback arguments `options.callbackPosition` allows to adjust place of callback
Worth noting that the TypeError is not on const fn = (foo, bar, baz, cb) => void cb(baz !== undefined, foo, bar, baz);
fn(1, 2, undefined, () => {}); // works
fn(1, 2, () => {}); // TypeError: cb is not a function
I don't think that pattern is very common, probably not to the point it deserves a change of the
Worth noting that |
Yes, this version doesn't change that. Thoughts behind thisI'm considering an additional option that would allow to force the number of parameters desired by original function. Roughly:
This looks convenient when
It is, while |
My understanding is that When I come across unusual signatures, it's often enough to have a simple wrapper (with some downsides such as losing the function name, etc.): const PfnArr = promisify((foo, bar, baz, cb) => fn(foo, bar, baz, (err, ...r) => cb(err, r)); That being said, as far as I can tell, these new options still wouldn't support the conversion of multiple results to an object (instead of to an array), which can be implemented using |
Yep, technically, anything can be achieved with the The inconsistent partconst util = require('node:util');
const child_process = require('node:child_process');
const fs = require('node:fs');
const readline = require('node:readline');
const http2 = require('node:http2');
function fancyNamedFunction(a, b, cb) { cb(a + b); }
function dummyFunction() {}
console.log('fNF:', fancyNamedFunction.name);
console.log('P fNF:', util.promisify(fancyNamedFunction).name);
Object.defineProperty(fancyNamedFunction, util.promisify.custom, {
value: (a, b) => {
return new Promise((resolve) => fancyNamedFunction(a, b, resolve));
}, configurable: true
});
console.log('C fNFe:', util.promisify(fancyNamedFunction).name);
Object.defineProperty(fancyNamedFunction, util.promisify.custom, {
value: dummyFunction
});
console.log('D fNFe:', util.promisify(fancyNamedFunction).name);
console.log('exec:', child_process.exec.name);
console.log('C exec:', util.promisify(child_process.exec).name);
console.log('exists:', fs.exists.name);
console.log('C exists:', util.promisify(fs.exists).name);
console.log('P read:', util.promisify(fs.read).name);
console.log('connect:', http2.connect.name);
console.log('C connect:', util.promisify(http2.connect).name); fNF: fancyNamedFunction
P fNF: fancyNamedFunction
C fNFe: value
D fNFe: dummyFunction
exec: exec
C exec:
exists: exists
C exists: value
P read: read
connect: connect
C connect: value Of course "customly" promisified function doesn't inherit any other property as well, while "normally" promisified does. - ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
- value: fn, enumerable: false, writable: false, configurable: true
- })
+ ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
+ ObjectDefineProperties(fn, ObjectGetOwnPropertyDescriptors(original)); Also, while we're at it, was there any intentional reason to keep
I originally considered array to be superior, but it makes sense. Added |
Co-authored-by: Antoine du Hamel <duhamelantoine1995@gmail.com>
e3596b7
to
eba911f
Compare
I don't think I agree with this. The point of In fact, this feels wrong to me: // Third-party library:
const fn = (a, b, cb) => cb(null, a, b);
// Somewhere:
fn[util.promisify.custom] = util.promisify(fn);
// Application A:
util.promisify(fn)('a', 'b').then((x) => console.log({ x }));
// Application B:
// fn.length is 3, so we will surely get an array of 2 values back... nope.
util.promisify(fn, {
resolveArray: true
})('a', 'b').then((x) => console.log({ y }); I'd consider most node-style callback APIs legacy at this point, with a few exceptions. Many modern runtimes don't have a built-in |
The main usecase for
It is wrong indeed! Updated it to neither get or set This also solves potential issues in case of this scenario: // third-party
function stRead(buffer, ..., cb) {
...
cb(err, byteLength, buffer)
}
// end-user code is happy with byteLength and doesn't need returned buffer, they already have it
PstRead = promisifiy(stRead);
const length = await PstRead(buffer, ...);
// someone (author of thirdpartie?) somewhere suddenly improved it
stRead[promisify.custom] = (...) => new Promise((res) => stRead(..., (err, byteLength, buffer) => res({ byteLength, buffer }));
// now end-user code is broken and there was no way to preemptively avoid that
typeof await PstRead(buffer, ...) === 'number'; // false
// this would work as long as stRead itself is unchanged
typeof (await promisify(stRead, { resolveObject: ['byteLength'] })(buffer, ...)).byteLength === 'number';
typeof await promisify(stRead, { resolveObject: false })(buffer, ...) === 'number'; Also, a factor of lower importance: it feels weird for the original function decoration to give names to its "returned" values. Idiomatically, these names should be unexposed ( Of course if opinions on unusualness of different signatures across ecosystem, legacy reasons and complexity are still strong enough, I'm not insisting on adding that. :) |
Closing as unneeded; the alternative is userland implementation. Thanks for reviews! |
This PR adds an optional second argument to
util.promisify()
.options.resolveArray
solves a problem with callbacks taking more than one non-error argument: Promise can't be resolved with more than one value, so further results are lost. With this option, Promise is resolved with an array of all non-error arguments.For internal methods, this problem is solved with appropriate
kCustomPromisifiedSymbol
andkCustomPromisifyArgsSymbol
properties. Bututil.promisify()
is a part of public API, and this approach is not applicable to userland.options.callbackPosition
solves a problem with functions not having callback-last signature.Most notably, a function can't have callback as last argument when we want to be able to omit last arguments or use
...rest
:util.promisify((foo, optionalBar, cb) => cb(!foo, optionalBar))('fooValue', callbackFn)
and expectoptionalBar
to be magically undefined orarguments.length
to be magically adjusted.(foo, ...optionalArgs, cb) => {}
at all.With that new option, we'll be able to promisify
(foo, cb, optionalBar = 'defaultBar', ...restArgs) => {}
.Example:
Documentation might require some rewording.