-
Notifications
You must be signed in to change notification settings - Fork 29.8k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
util: add options
to util.promisify()
#43088
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 <[email protected]>
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.