Expand Node 24 callback and promise adapter compatibility

This commit is contained in:
Chris Tate
2026-10-03 09:38:03 -05:00
parent c76ac2b95f
commit 8f04039d26
13 changed files with 2170 additions and 2078 deletions
@@ -2,6 +2,6 @@
"schemaVersion": 3,
"nodeVersion": "24.15.0",
"nodeCommit": "848430679556aed0bd073f2bc263331ad84fa119",
"artifactVersion": "3c31fba8150926a20ac8",
"artifactVersion": "18ed68d47d451a20f50c",
"rowCount": 3662
}
@@ -84848,7 +84848,7 @@
},
"dynamic": {
"status": "partial",
"detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.",
"detail": "Adapts callbacks and promises with receiver preservation, custom promisify hooks, function metadata, and Node-compatible argument and rejection handling; scheduling follows the island microtask queue.",
"verification": "test-backed"
}
},
@@ -85520,7 +85520,7 @@
},
"dynamic": {
"status": "partial",
"detail": "Available through the embedded Node compatibility shim; exact member and overload coverage may be narrower.",
"detail": "Adapts callbacks and promises with receiver preservation, custom promisify hooks, function metadata, and Node-compatible argument and rejection handling; scheduling follows the island microtask queue.",
"verification": "test-backed"
}
},
@@ -19,6 +19,7 @@
"wasi": "by-design"
},
"features": [
{ "chapter": "util", "symbols": ["util.promisify", "util.callbackify"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts", "tests/fixtures/npm/cases/node24-util-adapters/main.ts"] },
{ "chapter": "util", "symbols": ["util.parseArgs", "parseArgs"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/util-shims/main.ts", "tests/fixtures/npm/cases/node24-parseargs/main.ts"] },
{ "chapter": "buffer", "symbols": ["buffer.isAscii", "buffer.isUtf8", "buffer.transcode"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] },
{ "chapter": "util", "parent": "Class: `util.TextEncoder`", "signatures": ["`textEncoder.encodeInto(src, dest)`", "`encoding` Type: {string}"], "status": "partial", "evidence": ["tests/fixtures/npm/cases/node24-text-encoding/main.ts"] },
@@ -94721,9 +94721,10 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-export:node:util.callbackify",
"source": "island-feature:util.util.callbackify",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-util-adapters/main.ts"
]
}
}
@@ -95561,9 +95562,10 @@
"action": "audit-partial",
"verification": "test-backed",
"confidence": "high",
"source": "island-export:node:util.promisify",
"source": "island-feature:util.util.promisify",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-util-adapters/main.ts"
]
}
}
@@ -111045,6 +111045,7 @@
"tests/fixtures/npm/cases/node24-env/main.ts",
"tests/fixtures/npm/cases/node24-parseargs/main.ts",
"tests/fixtures/npm/cases/node24-text-encoding/main.ts",
"tests/fixtures/npm/cases/node24-util-adapters/main.ts",
"tests/fixtures/npm/cases/util-shims/main.ts"
]
},
@@ -112010,9 +112011,10 @@
},
"dynamic": {
"status": "partial",
"evidence": "island-export:node:util.callbackify",
"evidence": "island-feature:util.util.callbackify",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-util-adapters/main.ts"
]
},
"anchorSource": "exact"
@@ -112833,9 +112835,10 @@
},
"dynamic": {
"status": "partial",
"evidence": "island-export:node:util.promisify",
"evidence": "island-feature:util.util.promisify",
"tests": [
"tests/fixtures/npm/cases/util-shims/main.ts"
"tests/fixtures/npm/cases/util-shims/main.ts",
"tests/fixtures/npm/cases/node24-util-adapters/main.ts"
]
},
"anchorSource": "exact"
+3
View File
@@ -678,6 +678,9 @@ function publicDetail(tier) {
return "No dynamic-island implementation exists for this Node API family yet.";
}
if (source.startsWith("island-feature:")) {
if (/^island-feature:util\.util\.(?:promisify|callbackify)$/.test(source)) {
return "Adapts callbacks and promises with receiver preservation, custom promisify hooks, function metadata, and Node-compatible argument and rejection handling; scheduling follows the island microtask queue.";
}
if (/^island-feature:util\.(?:util\.)?parseArgs$/.test(source)) {
return "Parses command-line options, positionals, defaults, and tokens with Node-compatible configuration validation.";
}
@@ -13408,6 +13408,12 @@
"<repo>/tests/fixtures/npm/cases/node24-parseargs/main.ts"
],
"diags": []
},
"<repo>/tests/fixtures/npm/cases/node24-util-adapters/main.ts": {
"order": [
"<repo>/tests/fixtures/npm/cases/node24-util-adapters/main.ts"
],
"diags": []
}
}
}
File diff suppressed because it is too large Load Diff
+4
View File
@@ -0,0 +1,4 @@
// @dynamic
// @stderr
import { report } from "node24-util-adapters-fixture";
report();
@@ -0,0 +1 @@
{"type":"module"}
@@ -0,0 +1 @@
export function report(): Promise<void>;
+78
View File
@@ -0,0 +1,78 @@
import { promisify, callbackify } from 'node:util';
export async function report() {
const line = (name, value) => console.log(name, JSON.stringify(value));
const error = (name, fn) => {
try { fn(); line(name, 'no error'); }
catch (e) { line(name, [e.name, e.code, e.message]); }
};
const descriptor = (fn, key) => {
const d = Object.getOwnPropertyDescriptor(fn, key);
return [d.value, d.writable, d.enumerable, d.configurable];
};
for (const value of [undefined, null, false, 42, -0, 2n, Symbol('x'), 'no', "can't", 'a'.repeat(29), {}, [], Object.create(null)]) {
error('promisify invalid', () => promisify(value));
error('callbackify invalid', () => callbackify(value));
}
const metadata = Symbol('metadata');
function sum(a, b, cb) { cb(null, this.base + a + b, 'ignored'); }
Object.defineProperty(sum, 'detail', { get() { return 17; }, enumerable: true });
sum[metadata] = 'symbol';
const customPrototype = Object.create(Function.prototype);
Object.setPrototypeOf(sum, customPrototype);
const wrapped = promisify(sum);
line('promisify metadata', [descriptor(wrapped, 'name'), descriptor(wrapped, 'length'), wrapped.detail, wrapped[metadata], Object.getPrototypeOf(wrapped) === customPrototype]);
line('promisify identity', [promisify(wrapped) === wrapped, wrapped[promisify.custom] === wrapped, Symbol.for('nodejs.util.promisify.custom') === promisify.custom]);
line('promisify receiver', await wrapped.call({ base: 10 }, 2, 3));
line('promisify no result', await promisify(cb => cb(null))() === undefined);
line('promisify falsy error', await promisify(cb => cb(0, 'ok'))());
line('promisify first settlement', await promisify(cb => { cb(null, 1); cb(new Error('late')); throw new Error('later'); })());
const boom = new Error('sync');
line('promisify sync throw', await promisify(() => { throw boom; })().catch(e => e === boom));
function custom() {}
custom[promisify.custom] = function chosen() { return this.value; };
const chosen = promisify(custom);
line('custom', [chosen === custom[promisify.custom], promisify(chosen) === chosen, chosen.call({ value: 23 })]);
for (const value of [true, 'wrong', {}, 5, 3n, Symbol('bad')]) {
custom[promisify.custom] = value;
error('invalid custom', () => promisify(custom));
}
custom[promisify.custom] = false;
line('falsy custom ignored', typeof promisify(custom));
async function add(a, b) { return this.base + a + b; }
Object.setPrototypeOf(add, customPrototype);
add[metadata] = 'async symbol';
const callback = callbackify(add);
line('callbackify metadata', [descriptor(callback, 'name'), descriptor(callback, 'length'), callback[metadata], Object.getPrototypeOf(callback) === Function.prototype]);
await new Promise(resolve => {
let synchronous = true;
const receiver = { base: 20 };
const returned = callback.call(receiver, 2, 3, function(err, result) {
line('callbackify success', [err, result, this === receiver, synchronous, arguments.length]); resolve();
});
line('callbackify return', returned === undefined);
synchronous = false;
});
for (const value of [undefined, null, 1, 'callback', {}, Symbol('cb')]) error('invalid callback', () => callback(value));
for (const reason of [undefined, null, false, 0, -0, NaN, '', 0n, 1, true, 'failure', 2n, Symbol('failure'), {}, new Error('failure')]) {
await new Promise(resolve => callbackify(() => Promise.reject(reason))(function(err, result) {
line('callbackify rejection', [typeof reason, !reason, err === reason, err?.code ?? null, Object.is(err?.reason, reason), arguments.length, result === undefined, err instanceof Error, err?.message ?? null, Object.hasOwn(Object(err), 'reason')]); resolve();
}));
}
await new Promise(resolve => {
const order = [];
callbackify(() => ({ then(fulfilled) { order.push('then'); fulfilled(8); } }))(function(err, value) {
order.push('callback'); line('thenable asynchronous', [order, err, value]); resolve();
});
order.push('returned');
});
error('callbackify sync throw', () => callbackify(() => { throw new Error('sync failure'); })(() => {}));
for (const values of [[7, 'renamed'], ['strange', 12], [NaN, '']]) {
function altered() { return Promise.resolve(1); }
Object.defineProperty(altered, 'length', { value: values[0] });
Object.defineProperty(altered, 'name', { value: values[1] });
const p = promisify(altered), c = callbackify(altered);
line('altered metadata', [descriptor(p, 'name'), descriptor(p, 'length'), descriptor(c, 'name'), descriptor(c, 'length')]);
}
}
@@ -0,0 +1 @@
{"name":"node24-util-adapters-fixture","version":"1.0.0","type":"module","main":"index.js","types":"index.d.ts"}