diff --git a/src/js/internal/promisify.ts b/src/js/internal/promisify.ts new file mode 100644 index 0000000000..76a6ecefc6 --- /dev/null +++ b/src/js/internal/promisify.ts @@ -0,0 +1,79 @@ +const kCustomPromisifiedSymbol = Symbol.for("nodejs.util.promisify.custom"); +const kCustomPromisifyArgsSymbol = Symbol("customPromisifyArgs"); + +function defineCustomPromisify(target, callback) { + Object.defineProperty(target, kCustomPromisifiedSymbol, { + value: callback, + __proto__: null, + configurable: true, + }); + + return callback; +} + +function defineCustomPromisifyArgs(target, args) { + Object.defineProperty(target, kCustomPromisifyArgsSymbol, { + __proto__: null, + value: args, + enumerable: false, + }); + return args; +} + +var promisify = function promisify(original) { + if (typeof original !== "function") throw new TypeError('The "original" argument must be of type Function'); + const custom = original[kCustomPromisifiedSymbol]; + if (custom) { + if (typeof custom !== "function") { + throw new TypeError('The "util.promisify.custom" argument must be of type Function'); + } + // ensure that we don't create another promisified function wrapper + return defineCustomPromisify(custom, custom); + } + + const callbackArgs = original[kCustomPromisifyArgsSymbol]; + + function fn(...originalArgs) { + const { promise, resolve, reject } = Promise.withResolvers(); + try { + original.$apply(this, [ + ...originalArgs, + function (err, ...values) { + if (err) { + return reject(err); + } + + if (callbackArgs !== undefined && values.length > 0) { + if (!Array.isArray(callbackArgs)) { + throw new TypeError('The "customPromisifyArgs" argument must be of type Array'); + } + if (callbackArgs.length !== values.length) { + throw new Error("Mismatched length in promisify callback args"); + } + const result = {}; + for (let i = 0; i < callbackArgs.length; i++) { + result[callbackArgs[i]] = values[i]; + } + resolve(result); + } else { + resolve(values[0]); + } + }, + ]); + } catch (err) { + reject(err); + } + + return promise; + } + Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); + defineCustomPromisify(fn, fn); + return Object.defineProperties(fn, Object.getOwnPropertyDescriptors(original)); +}; +promisify.custom = kCustomPromisifiedSymbol; + +export default { + defineCustomPromisify, + defineCustomPromisifyArgs, + promisify, +}; diff --git a/src/js/node/crypto.ts b/src/js/node/crypto.ts index 8a38faef89..8727f9a901 100644 --- a/src/js/node/crypto.ts +++ b/src/js/node/crypto.ts @@ -11935,14 +11935,17 @@ function _generateKeyPairSync(algorithm, options) { } crypto_exports.generateKeyPairSync = _generateKeyPairSync; -crypto_exports.generateKeyPair = function (algorithm, options, callback) { +function _generateKeyPair(algorithm, options, callback) { try { const result = _generateKeyPairSync(algorithm, options); typeof callback === "function" && callback(null, result.publicKey, result.privateKey); } catch (err) { typeof callback === "function" && callback(err); } -}; +} +const { defineCustomPromisifyArgs } = require("internal/promisify"); +defineCustomPromisifyArgs(_generateKeyPair, ["publicKey", "privateKey"]); +crypto_exports.generateKeyPair = _generateKeyPair; crypto_exports.createSecretKey = function (key, encoding) { if (key instanceof KeyObject || key instanceof CryptoKey) { diff --git a/src/js/node/timers.ts b/src/js/node/timers.ts index 6150ce4579..7bbe39b921 100644 --- a/src/js/node/timers.ts +++ b/src/js/node/timers.ts @@ -1,4 +1,32 @@ // Hardcoded module "node:timers" +const { defineCustomPromisify } = require("internal/promisify"); + +// Lazily load node:timers/promises promisified functions onto the global timers. +{ + const { setTimeout: timeout, setImmediate: immediate, setInterval: interval } = globalThis; + + if (timeout && $isCallable(timeout)) { + defineCustomPromisify(timeout, function setTimeout(arg1) { + const fn = defineCustomPromisify(timeout, require("node:timers/promises").setTimeout); + return fn.$apply(this, arguments); + }); + } + + if (immediate && $isCallable(immediate)) { + defineCustomPromisify(immediate, function setImmediate(arg1) { + const fn = defineCustomPromisify(immediate, require("node:timers/promises").setImmediate); + return fn.$apply(this, arguments); + }); + } + + if (interval && $isCallable(interval)) { + defineCustomPromisify(interval, function setInterval(arg1) { + const fn = defineCustomPromisify(interval, require("node:timers/promises").setInterval); + return fn.$apply(this, arguments); + }); + } +} + export default { setTimeout, clearTimeout, diff --git a/src/js/node/util.ts b/src/js/node/util.ts index d6280be6fd..58fb2f9277 100644 --- a/src/js/node/util.ts +++ b/src/js/node/util.ts @@ -3,6 +3,7 @@ const types = require("node:util/types"); /** @type {import('node-inspect-extracted')} */ const utl = require("internal/util/inspect"); const { ERR_INVALID_ARG_TYPE, ERR_OUT_OF_RANGE } = require("internal/errors"); +const { promisify } = require("internal/promisify"); const internalErrorName = $newZigFunction("node_util_binding.zig", "internalErrorName", 1); @@ -158,80 +159,7 @@ var _extend = function (origin, add) { } return origin; }; -var kCustomPromisifiedSymbol = Symbol.for("nodejs.util.promisify.custom"); -function defineCustomPromisify(target, callback) { - Object.defineProperty(target, kCustomPromisifiedSymbol, { - value: callback, - __proto__: null, - configurable: true, - }); - return callback; -} - -// Lazily load node:timers/promises promisifed functions onto the global timers. -// This is not a complete solution, as one could load these without loading the "util" module -// But it is better than nothing. -{ - const { setTimeout: timeout, setImmediate: immediate, setInterval: interval } = globalThis; - - if (timeout && $isCallable(timeout)) { - defineCustomPromisify(timeout, function setTimeout(arg1) { - const fn = defineCustomPromisify(timeout, require("node:timers/promises").setTimeout); - return fn.$apply(this, arguments); - }); - } - - if (immediate && $isCallable(immediate)) { - defineCustomPromisify(immediate, function setImmediate(arg1) { - const fn = defineCustomPromisify(immediate, require("node:timers/promises").setImmediate); - return fn.$apply(this, arguments); - }); - } - - if (interval && $isCallable(interval)) { - defineCustomPromisify(interval, function setInterval(arg1) { - const fn = defineCustomPromisify(interval, require("node:timers/promises").setInterval); - return fn.$apply(this, arguments); - }); - } -} - -var promisify = function promisify(original) { - if (typeof original !== "function") throw new TypeError('The "original" argument must be of type Function'); - const custom = original[kCustomPromisifiedSymbol]; - if (custom) { - if (typeof custom !== "function") { - throw new TypeError('The "util.promisify.custom" argument must be of type Function'); - } - // ensure that we don't create another promisified function wrapper - return defineCustomPromisify(custom, custom); - } - - function fn(...originalArgs) { - const { promise, resolve, reject } = Promise.withResolvers(); - try { - original.$apply(this, [ - ...originalArgs, - function (err, ...values) { - if (err) { - return reject(err); - } - - resolve(values[0]); - }, - ]); - } catch (err) { - reject(err); - } - - return promise; - } - Object.setPrototypeOf(fn, Object.getPrototypeOf(original)); - defineCustomPromisify(fn, fn); - return Object.defineProperties(fn, getOwnPropertyDescriptors(original)); -}; -promisify.custom = kCustomPromisifiedSymbol; function callbackifyOnRejected(reason, cb) { if (!reason) { var newReason = new Error("Promise was rejected with a falsy value"); diff --git a/test/regression/issue/09469.test.ts b/test/regression/issue/09469.test.ts new file mode 100644 index 0000000000..9d8db4304d --- /dev/null +++ b/test/regression/issue/09469.test.ts @@ -0,0 +1,27 @@ +const crypto = require("crypto"); +const util = require("util"); + +if (!crypto.generateKeyPair) { + test.skip("missing crypto.generateKeyPair"); +} + +test("09469", async () => { + const generateKeyPairAsync = util.promisify(crypto.generateKeyPair); + const ret = await generateKeyPairAsync("rsa", { + publicExponent: 3, + modulusLength: 512, + publicKeyEncoding: { + type: "pkcs1", + format: "pem", + }, + privateKeyEncoding: { + type: "pkcs8", + format: "pem", + }, + }); + + expect(Object.keys(ret)).toHaveLength(2); + const { publicKey, privateKey } = ret; + expect(typeof publicKey).toBe("string"); + expect(typeof privateKey).toBe("string"); +});