mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-28 06:53:42 +08:00
Promisify functions whose callback is optional
`util.promisify(client.close.bind(client))` over `close(callback?: (error?: Error) => void)` was refused because the callback parameter is `callback | undefined`. The promisified function now takes the union's function arm and passes the callback into the union. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
fa64a7cf5a
commit
297ec38f07
@@ -25,6 +25,9 @@ export interface CallbackShape {
|
||||
resultT: IrType & { kind: "func" };
|
||||
args: readonly IrType[];
|
||||
callbackT: IrType & { kind: "func" };
|
||||
/** The parameter's declared type: callbackT, or `callbackT | undefined`
|
||||
* for an optional callback. */
|
||||
callbackParamT: IrType;
|
||||
inner: IrType;
|
||||
/** The error parameter's union and the tag of its Error arm, or null when
|
||||
* the callback declares no error parameter. */
|
||||
@@ -39,7 +42,20 @@ function callbackShape(
|
||||
): CallbackShape | null {
|
||||
if (fnT?.kind !== "func" || resultT?.kind !== "func") return null;
|
||||
if (fnT.rest || fnT.restAbi || fnT.argumentsAll || resultT.rest || resultT.restAbi) return null;
|
||||
const callbackT = fnT.params[fnT.params.length - 1];
|
||||
const callbackParamT = fnT.params[fnT.params.length - 1];
|
||||
// An optional callback (`close(callback?: (err?: Error) => void)`) takes
|
||||
// its function arm; the call passes the callback into the union.
|
||||
const callbackT =
|
||||
callbackParamT?.kind === "union"
|
||||
? (() => {
|
||||
const arms = lowerer.unions.get(callbackParamT.unionId)?.arms ?? [];
|
||||
const fns = arms.filter((arm) => arm.kind === "func");
|
||||
return fns.length === 1 &&
|
||||
arms.every((arm) => arm.kind === "func" || arm.kind === "undefinedT")
|
||||
? fns[0]
|
||||
: undefined;
|
||||
})()
|
||||
: callbackParamT;
|
||||
if (callbackT?.kind !== "func" || callbackT.ret.kind !== "void") return null;
|
||||
if (callbackT.rest || callbackT.restAbi || callbackT.params.length > 2) return null;
|
||||
const args = fnT.params.slice(0, -1);
|
||||
@@ -71,7 +87,7 @@ function callbackShape(
|
||||
const probe: IrExpr = { kind: "varRef", localId: "%probe", type: valueT, loc };
|
||||
if (!typeEquals(lowerer.coerceToExpected(probe, inner).type, inner)) return null;
|
||||
}
|
||||
return { fnT, resultT, args, callbackT, inner, error };
|
||||
return { fnT, resultT, args, callbackT, callbackParamT: callbackParamT!, inner, error };
|
||||
}
|
||||
|
||||
/** The callback shape of a `promisify(fn)` call, or null. */
|
||||
@@ -137,7 +153,7 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st
|
||||
if (existing) return existing;
|
||||
const base = `%promisify.${lowerer.arrHofHelpers.size}`;
|
||||
lowerer.arrHofHelpers.set(key, base);
|
||||
const { fnT, resultT, args, callbackT, inner, error } = shape;
|
||||
const { fnT, resultT, args, callbackT, callbackParamT, inner, error } = shape;
|
||||
const promiseT = resultT.ret;
|
||||
const resolveT = funcOf(inner.kind === "void" ? [] : [inner], VOID);
|
||||
const rejectT = funcOf([ERROR_T], VOID);
|
||||
@@ -270,13 +286,16 @@ function promisifyMaker(lowerer: Lowerer, shape: CallbackShape, loc: SrcLoc): st
|
||||
callee: varRef("f.0", fnT, loc),
|
||||
args: [
|
||||
...args.map((type, i) => varRef(argIds[i]!, type, loc)),
|
||||
{
|
||||
kind: "call",
|
||||
callee: `${base}.mkcb`,
|
||||
args: [varRef("res.0", resolveT, loc), varRef("rej.0", rejectT, loc)],
|
||||
type: callbackT,
|
||||
loc,
|
||||
},
|
||||
lowerer.coerceToExpected(
|
||||
{
|
||||
kind: "call",
|
||||
callee: `${base}.mkcb`,
|
||||
args: [varRef("res.0", resolveT, loc), varRef("rej.0", rejectT, loc)],
|
||||
type: callbackT,
|
||||
loc,
|
||||
},
|
||||
callbackParamT,
|
||||
),
|
||||
],
|
||||
type: VOID,
|
||||
loc,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// util.promisify over typed callback-only functions (a bound method, a
|
||||
// function declaration, an inline arrow): the error argument rejects, the
|
||||
// value fulfills, a synchronous throw rejects, and settlement is async.
|
||||
// util.promisify over typed callback-only functions (bound methods, one with
|
||||
// an optional callback, a function declaration, an inline arrow): the error
|
||||
// argument rejects, the value fulfills, a synchronous throw rejects, and
|
||||
// settlement is async.
|
||||
import { promisify } from "node:util";
|
||||
|
||||
class Connection {
|
||||
@@ -19,6 +20,9 @@ class Connection {
|
||||
callback();
|
||||
}, 1);
|
||||
}
|
||||
flush(callback?: (error?: Error) => void): void {
|
||||
setImmediate(() => callback?.(this.closed ? new Error("flushed after close") : undefined));
|
||||
}
|
||||
stats(callback: (error: Error | null, open: number) => void): void {
|
||||
setImmediate(() => (this.closed ? callback(new RangeError("closed"), 0) : callback(null, 3)));
|
||||
}
|
||||
@@ -32,6 +36,8 @@ async function main(): Promise<void> {
|
||||
const c = new Connection("db");
|
||||
const stats = promisify(c.stats.bind(c));
|
||||
console.log("open", await stats());
|
||||
await promisify(c.flush.bind(c))();
|
||||
console.log("flushed");
|
||||
const closeAsync = promisify(c.close.bind(c));
|
||||
await closeAsync();
|
||||
console.log("closed once");
|
||||
@@ -45,6 +51,7 @@ async function main(): Promise<void> {
|
||||
} catch (e) {
|
||||
console.log("stats:", (e as Error).name, (e as Error).message);
|
||||
}
|
||||
await promisify(c.flush.bind(c))().catch((e) => console.log("flush:", (e as Error).message));
|
||||
try {
|
||||
await promisify(boom)();
|
||||
} catch (e) {
|
||||
|
||||
Reference in New Issue
Block a user