Compile events.once with an AbortSignal option and over stream receivers

events.once(emitter, name, { signal }) lowers to events.onceSignal, served
by a new runtime unit (scr_events_once_signal.c, linked only through the
eventsOnceSignal feature). It follows Node 24's lib/events.js: an
undefined signal is the plain form, a value that is not an AbortSignal
rejects with ERR_INVALID_ARG_TYPE, an aborted signal rejects with an
AbortError whose cause is signal.reason, and otherwise a once,
resist-stop-propagation 'abort' listener removes the event and 'error'
listeners and rejects. The event or the error removes the abort listener.
Options other than an object literal with one signal stay refused.

events.once now accepts stream receivers: a stream's runtime-emitted
events use their fixed tuples, and its 'data' and 'keypress' events stay
refused. A NodeJS stream interface slot that holds a stdio handle requests
the general stream representation.

Node's once is an async function that returns the executor's promise, so
its result adopts that promise through a thenable job. The plain form
settled its result two turns early; both forms now settle on Node's turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Malte Ubl
2026-10-20 08:47:51 -07:00
co-authored by Claude Opus 5.5
parent 8284e61f1e
commit c7b2620e5c
20 changed files with 479 additions and 45 deletions
@@ -26,6 +26,7 @@ export function executableLinkFeatures(mod: IrModule): NativeLinkFeatures {
dynAsync: features.dynAsync,
...(features.dynTier ? { dynTier: true } : {}),
...(features.streamDyn ? { streamDyn: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
events: features.processEvents,
emitter: features.emitter,
symbol: features.symbol,
@@ -5568,8 +5568,11 @@ export class LlEmitter {
return emitterFixedAdapter(this, cbT);
}
emitterOnceAdapter(boxerT: IrType & { kind: "func" }): { fn: string; shim: string } {
return emitterOnceAdapter(this, boxerT);
emitterOnceAdapter(
boxerT: IrType & { kind: "func" },
fire?: string,
): { fn: string; shim: string } {
return emitterOnceAdapter(this, boxerT, fire);
}
wrapEmitterListener(target: string, adapterFn: string): string {
@@ -822,23 +822,27 @@ export function emitterFixedAdapter(
* (the event's tuple to any[]): the runtime-built listener closure holds
* the boxer in caps[0], so the adapter calls it with the event's typed
* arguments, as emitterFixedAdapter calls a listener, and hands the array
* to scr_events_once_fire. */
* to scr_events_once_fire (or `fire`, the signal form's entry). */
export function emitterOnceAdapter(
host: LlvmEmitterContext,
boxerT: IrType & { kind: "func" },
fire = "scr_events_once_fire",
): { fn: string; shim: string } {
const cursor = boxerT.params.length > 4;
const shim = cursor ? "scr_ee_inv_args" : `scr_ee_inv_fixed${boxerT.params.length}`;
if (cursor) host.declare("declare ptr @scr_ee_arg_next(ptr)");
host.declare(`declare void @${shim}(ptr, ptr)`);
const key = `eeOnce:${typeKey(boxerT)}`;
const key =
fire === "scr_events_once_fire"
? `eeOnce:${typeKey(boxerT)}`
: `eeOnce:${fire}:${typeKey(boxerT)}`;
let sym = host.resolveThunks.get(key);
if (sym) return { fn: sym, shim };
sym = `sc_ee_once_${host.resolveThunks.size}`;
host.resolveThunks.set(key, sym);
host.declare(`declare ptr @scr_box_get_ref(ptr)`);
host.declare(`declare void @scr_closure_release(ptr)`);
host.declare(`declare void @scr_events_once_fire(ptr, ptr)`);
host.declare(`declare void @${fire}(ptr, ptr)`);
const params = cursor ? "ptr %args" : boxerT.params.map((_, i) => `ptr %a${i}`).join(", ");
const d: string[] = [
`define internal void @${sym}(ptr %cb${params ? ", " + params : ""}) ${FN_ATTRS} { ; events.once adapter ${typeKey(boxerT)}`,
@@ -869,7 +873,7 @@ export function emitterOnceAdapter(
` %fn = load ptr, ptr %fnp`,
` %arr = call ptr %fn(${passed.join(", ")}) ; the boxed arguments, +1`,
` call void @scr_closure_release(ptr %boxer)`,
` call void @scr_events_once_fire(ptr %cb, ptr %arr) ; moves %arr`,
` call void @${fire}(ptr %cb, ptr %arr) ; moves %arr`,
` ret void`,
`}`,
``,
@@ -755,7 +755,8 @@ export function emitLibCall(host: LlvmEmitterContext, e: LibCallExpr): LlValue {
}
const prefix = e.fn.slice(0, e.fn.indexOf(".")) as LibCallPrefix;
// events.once builds typed listener adapters beside the emitter calls.
if (e.fn === "events.once") return host.emitErrorsEventsLibCall(e);
if (e.fn === "events.once" || e.fn === "events.onceSignal")
return host.emitErrorsEventsLibCall(e);
switch (prefix) {
case "fetch":
case "abort":
@@ -731,27 +731,34 @@ export function emitErrorsEventsLibCall(host: LlvmEmitterContext, e: LibCallExpr
host.emitAlwaysPendingUnwind();
return out;
}
if (e.fn === "events.once") {
// (recv, name, boxer, errorBoxer, listenForError): scr_events_once
// registers one-shot typed listeners behind adapters that box the
// event's tuple (and the 'error' tuple) into the fulfilled array. The
// runtime retains the boxers; the frame's temporaries release as usual.
// May-throw ('newListener' meta listeners run inside).
if (e.fn === "events.once" || e.fn === "events.onceSignal") {
// (recv, name, boxer, errorBoxer, listenForError[, signal]):
// scr_events_once registers one-shot typed listeners behind adapters
// that box the event's tuple (and the 'error' tuple) into the fulfilled
// array. The runtime retains the boxers; the frame's temporaries
// release as usual. May-throw ('newListener' meta listeners run
// inside). The signal form's adapters end in its own fire entry.
const boxerT = e.args[2]!.type;
const errorT = e.args[3]!.type;
if (boxerT.kind !== "func" || errorT.kind !== "func")
throw new InternalCompilerError("llvm emitter bug: events.once boxer not a func");
throw new InternalCompilerError(`llvm emitter bug: ${e.fn} boxer not a func`);
const signal = e.fn === "events.onceSignal";
const args = e.args.map((a) => host.emitExpr(a));
const event = host.emitterOnceAdapter(boxerT);
const error = host.emitterOnceAdapter(errorT);
host.declare(`declare ptr @scr_events_once(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr)`);
const fire = signal ? "scr_events_once_signal_fire" : undefined;
const event = host.emitterOnceAdapter(boxerT, fire);
const error = host.emitterOnceAdapter(errorT, fire);
const entry = signal ? "scr_events_once_signal" : "scr_events_once";
host.declare(
`declare ptr @${entry}(ptr, ptr, ptr, ptr, ptr, ptr, ptr, ptr${signal ? ", ptr" : ""})`,
);
const errorBoxer = B.tmp();
B.line(`${errorBoxer} = select i1 ${args[4]!.name}, ptr ${args[3]!.name}, ptr null`);
const t = B.tmp();
B.line(
`${t} = call ptr @scr_events_once(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
`${t} = call ptr @${entry}(ptr ${args[0]!.name}, ptr ${args[1]!.name}, ` +
`ptr ${args[2]!.name}, ptr @${event.fn}, ptr @${event.shim}, ` +
`ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim})`,
`ptr ${errorBoxer}, ptr @${error.fn}, ptr @${error.shim}` +
`${signal ? `, ptr ${args[5]!.name}` : ""})`,
);
const out = host.own({ name: t, type: e.type });
host.emitPendingCheck();
@@ -43,6 +43,9 @@ export interface NativeLinkFeatures {
/** Checked-dynamic stream slots: the scr_stream_dyn.c unit. Absent (not
* false) for programs without them. */
streamDyn?: boolean;
/** events.once with options: scr_events_once_signal.c. Absent (not
* false) for programs without it. */
eventsOnceSignal?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
@@ -116,6 +116,9 @@ export interface CcOptions {
/** Checked-dynamic stream slots (stream.dynNative / stream.pipeDyn):
* compiles scr_stream_dyn.c. */
streamDyn?: boolean;
/** events.once with options (events.onceSignal): compiles
* scr_events_once_signal.c. */
eventsOnceSignal?: boolean;
/** The program constructs EventTarget/Event/CustomEvent values or
* calls events.addAbortListener (the event.* libCalls), or uses the
* native fetch AbortSignal built on the same engine: compiles
@@ -217,6 +217,9 @@ const RUNTIME_UNITS: readonly RuntimeUnit[] = [
// measures with.
unit((u) => u.opts.events && u.regex, "scr_readline_tty.c"),
unit((u) => u.opts.emitter || u.opts.workers, "scr_events_emitter.c"),
// events.once with options registers through the emitter and
// event-target units.
unit((u) => u.opts.eventsOnceSignal === true, "scr_events_once_signal.c"),
// The checked-dynamic HANDLE support unit (listener gate + runtime
// adapter closures): every referencing unit is one of the emitter or
// net families (http implies net), so handle-free binaries keep
@@ -84,6 +84,7 @@ export interface RuntimeFeatureSet extends NativeLinkFeatures {
eventTarget: boolean;
dynTier: boolean;
streamDyn: boolean;
eventsOnceSignal: boolean;
warningReplace: boolean;
stdioExtended: boolean;
nativeFetch: boolean;
@@ -275,6 +276,7 @@ export function effectiveRuntimeFeatures(
eventTarget: features.eventTarget === true,
dynTier: features.dynTier === true,
streamDyn: features.streamDyn === true,
eventsOnceSignal: features.eventsOnceSignal === true,
warningReplace: features.warningReplace === true,
stdioExtended: features.stdioExtended === true,
nativeFetch,
@@ -66,6 +66,7 @@ export interface EarlyExecutableNativeFeatures {
dynAsync: boolean;
dynTier?: boolean;
streamDyn?: boolean;
eventsOnceSignal?: boolean;
events: boolean;
emitter: boolean;
symbol: boolean;
@@ -72,6 +72,7 @@ import { STREAM_FORCED_EVENT_NAMES, streamForcedTuple, streamSidesOf } from "./l
import { boolLit, strLit, varRef } from "../../ir/build.js";
import { tryLowerExpression } from "./expressions/try-lower-expression.js";
import { lowerAddAbortListener } from "./builtin-calls.js";
import { requestGeneralStreamSurface } from "./general-streams.js";
/** True when the class descends from (or is) the runtime emitter. */
export function emitterRooted(lowerer: Lowerer, info: ClassInfo | undefined | null): boolean {
@@ -2204,24 +2205,54 @@ function eventsOnceBoxer(
return { kind: "closure", fnName: name, captures: [], type: fnType, loc };
}
/** events.once(emitter, name): a promise of the next `name` event's
* arguments as an array, rejected by an earlier 'error' event (unless the
* awaited event is 'error'). The emitter's listeners are typed by the
* program-wide event table, so the runtime registers one-shot typed
* listeners whose compiler-built boxers turn the event's tuple into the
* array. EventTarget and stream receivers, computed names and options are
* refused. */
/** The `signal` of an events.once options object literal (a checked-
* dynamic AbortSignal or undefined), or null when the literal names none.
* Other options forms are refused. */
function eventsOnceSignal(lowerer: Lowerer, node: ts.Expression): IrExpr | null {
if (!ts.isObjectLiteralExpression(node)) {
lowerer.noLowering(
"events.once options that are not an object literal",
node,
"pass the options as an object literal: { signal }",
);
return null;
}
let signal: IrExpr | null = null;
for (const prop of node.properties.nodes) {
const name = prop.name && ts.isIdentifier(prop.name) ? prop.name.text : null;
if (
name === "signal" &&
signal === null &&
(ts.isPropertyAssignment(prop) || ts.isShorthandPropertyAssignment(prop))
) {
signal = lowerer.lowerExprExpecting(
ts.isPropertyAssignment(prop) ? prop.initializer : (prop.name as ts.Identifier),
DYN,
);
} else {
lowerer.noLowering(
"events.once options other than one signal",
prop,
"the lowered options object takes signal: AbortSignal",
);
}
}
return signal;
}
/** events.once(emitter, name[, { signal }]): a promise of the next `name`
* event's arguments as an array, rejected by an earlier 'error' event
* (unless the awaited event is 'error') or by the signal's abort with an
* AbortError. The emitter's listeners are typed by the program-wide event
* table (a stream's runtime-emitted events by their fixed tuples), so the
* runtime registers one-shot typed listeners whose compiler-built boxers
* turn the event's tuple into the array. EventTarget receivers, a stream's
* 'data' event, computed names and options other than a literal { signal }
* are refused. */
export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc: SrcLoc): IrExpr {
const args = call.arguments.nodes;
if (args.length < 2 || args.some(ts.isSpreadElement)) {
lowerer.noLowering("events.once without an emitter and an event name", call);
}
if (args.length > 2) {
lowerer.noLowering(
"events.once with options",
args[2]!,
"the lowered form is events.once(emitter, name); AbortSignal options are not supported yet",
);
if (args.length < 2 || args.length > 3 || args.some(ts.isSpreadElement)) {
lowerer.noLowering("events.once without an emitter, an event name and options", call);
}
// Node's Promise<any[]>: the arguments array of checked-dynamic values.
const arrayT = arrayOf(DYN);
@@ -2229,11 +2260,14 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc:
const receiver = lowerer.lowerExpr(args[0]!);
const info =
receiver.type.kind === "object" ? lowerer.classes.get(receiver.type.className) : undefined;
if (!emitterRooted(lowerer, info) || streamSidesOf(lowerer, info) !== null) {
if (!emitterRooted(lowerer, info)) {
// A NodeJS stream interface slot that holds a stdio handle here: the
// program lowers again with the runtime stream classes.
requestGeneralStreamSurface(lowerer, args[0]!);
lowerer.noLowering(
"events.once over this value",
args[0]!,
"the emitter must be an EventEmitter instance; EventTarget and stream receivers are not supported yet",
"the emitter must be an EventEmitter or stream instance; EventTarget receivers are not supported yet",
);
}
const name = literalEventName(lowerer, args[1]!);
@@ -2245,6 +2279,14 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc:
);
}
const event = name ?? "";
const sides = streamSidesOf(lowerer, info);
if ((sides === "r" || sides === "rw") && (event === "data" || event === "keypress")) {
lowerer.noLowering(
`events.once of a stream's '${event}' event`,
args[1]!,
`the chunk has no checked-dynamic boxing here; register stream.once('${event}', listener) instead`,
);
}
const table = emitterEvents(lowerer);
if (event !== "error" && flexibleJsEvent(event, table.get(event))) {
lowerer.noLowering(
@@ -2252,14 +2294,22 @@ export function lowerEventsOnce(lowerer: Lowerer, call: ts.CallExpression, loc:
args[1]!,
);
}
const tuple = tupleOf(lowerer, table, event, call);
const tuple = streamForcedTuple(lowerer, info, event) ?? tupleOf(lowerer, table, event, call);
const errorTuple: IrType[] = [{ kind: "object", className: "%Error" }];
const boxer = eventsOnceBoxer(lowerer, call, tuple, arrayT, loc);
const errorBoxer = eventsOnceBoxer(lowerer, call, errorTuple, arrayT, loc);
const signal = args.length > 2 ? eventsOnceSignal(lowerer, args[2]!) : null;
const common = [
receiver,
eventNameExpr(event, loc),
boxer,
errorBoxer,
boolLit(event !== "error", loc),
];
return {
kind: "libCall",
fn: "events.once",
args: [receiver, eventNameExpr(event, loc), boxer, errorBoxer, boolLit(event !== "error", loc)],
fn: signal === null ? "events.once" : "events.onceSignal",
args: signal === null ? common : [...common, signal],
type: resultT,
loc,
};
+1
View File
@@ -538,6 +538,7 @@ async function compileExecutableNative(
dynAsync: features.dynAsync,
...(features.dynTier ? { dynTier: true } : {}),
...(features.streamDyn ? { streamDyn: true } : {}),
...(features.eventsOnceSignal ? { eventsOnceSignal: true } : {}),
events: features.events,
emitter: features.emitter,
symbol: features.symbol,
@@ -4174,6 +4174,10 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
* errorBoxer, listenForError] → a pending promise of any[]; the boxers
* turn the event's and the 'error' tuple into that array. */
"events.once": { argTypes: [null, STRING, null, null, BOOL], result: VOID },
/** events.once(emitter, name, { signal }) (scr_events_once_signal.c):
* events.once's arguments plus the checked-dynamic signal (an
* AbortSignal or undefined). */
"events.onceSignal": { argTypes: [null, STRING, null, null, BOOL, DYN], result: VOID },
/** The resolve-value forms: [ms?, value, ref] → a pending promise of the
* value's type that copies the borrowed already-resolved `value` promise
* when the timer (or immediate) fires; ref false unrefs it. The result
@@ -129,6 +129,7 @@ const LEGACY_TEXT_DECODER_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
/** Signal-taking readline, stream and timers/promises entry points, which register abort listeners. */
const ABORT_SIGNAL_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"events.onceSignal",
"http.clientSignal",
"rl.createSignal",
"rl.questionAsyncSignal",
@@ -527,6 +528,8 @@ const RUNTIME_FEATURE_RULES = {
fns: ["events.addAbortListener", ...ABORT_SIGNAL_LIB_FNS],
prefixes: ["event."],
}),
/** events.once(emitter, name, { signal }) (scr_events_once_signal.c). */
eventsOnceSignal: rule({ fns: ["events.onceSignal"] }),
fetch: rule({ prefixes: ["fetch."] }),
fileHandle: rule({ types: ["fileHandle"] }),
fsWatch: rule({ prefixes: ["fs.watch", "watcher."], types: ["fsWatcher"] }),
@@ -652,6 +655,7 @@ function noRuntimeFeatures(): RuntimeFeatures {
dynTier: false,
emitter: false,
eventTarget: false,
eventsOnceSignal: false,
fetch: false,
fileHandle: false,
fsWatch: false,
+2 -2
View File
@@ -4154,7 +4154,7 @@ function validateFunction(
if (!ok) err(`libCall ${e.fn} must be a promise of its ${cryptoPromise} value`, e.loc);
return true;
}
if (e.fn === "events.once") {
if (e.fn === "events.once" || e.fn === "events.onceSignal") {
const [recv, , boxer, errorBoxer] = e.args;
const arrayT = e.type.kind === "promise" ? e.type.inner : null;
const boxes = (b: IrExpr | undefined) =>
@@ -4165,7 +4165,7 @@ function validateFunction(
!boxes(boxer) ||
!boxes(errorBoxer)
) {
err(`libCall events.once must box its event tuples into the promised array`, e.loc);
err(`libCall ${e.fn} must box its event tuples into the promised array`, e.loc);
}
return true;
}
@@ -77,6 +77,7 @@ test.skipIf(process.platform !== "linux" && process.platform !== "darwin")(
eventTarget: false,
dynTier: false,
streamDyn: false,
eventsOnceSignal: false,
warningReplace: false,
stdioExtended: false,
regex: true,
@@ -76,6 +76,8 @@ export const OPTIONAL_RUNTIME_SOURCES = [
["scr_readline.c", "events"],
["scr_readline_tty.c", all("events", "regex")],
["scr_events_emitter.c", "emitter"],
// events.once with options registers through the emitter and event-target units.
["scr_events_once_signal.c", "eventsOnceSignal"],
["scr_dyn_handle.c", any("emitter", "netEffective")],
["scr_symbol.c", "symbol"],
["scr_bigint_assert.c", all("assert", "bigint")],
+30 -3
View File
@@ -518,8 +518,25 @@ static ScrClosure *scr_ee_once_closure(void *adapter, ScrClosure *boxer, ScrEeOn
return c;
}
static ScrBox *scr_ee_once_promise_box(ScrPromise *p) {
ScrBox *box = scr_box_new_obj(&scr_promise_retain_v, &scr_promise_release_v, &scr_promise_trace_v);
scr_box_set_ref(box, scr_promise_retain(p));
return box;
}
static void scr_ee_once_adopt_job(ScrClosure *job) {
ScrPromise *outer = scr_box_get_ref(job->caps[0]);
ScrPromise *inner = scr_box_get_ref(job->caps[1]);
scr_promise_race_join(outer, inner, &scr_promise_adapt_copy);
scr_promise_release(inner);
scr_promise_release(outer);
}
/* boxer and err_boxer are borrowed; the result is +1. err_boxer is NULL
* when the awaited event is 'error' itself. */
* when the awaited event is 'error' itself. Node's once is an async
* function returning the executor's promise, so the result adopts that
* promise through a thenable job queued here, whose reaction settles the
* result one turn after the executor's promise settles. */
ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter,
ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter,
ScrEeInvoke err_inv) {
@@ -558,9 +575,13 @@ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, voi
scr_caught_release(thrown);
scr_promise_reject_pending(st->promise);
}
ScrPromise *p = scr_promise_retain(st->promise);
ScrPromise *outer = scr_promise_new();
ScrClosure *job = scr_closure_new((void *)&scr_ee_once_adopt_job, 2);
job->caps[0] = scr_ee_once_promise_box(outer);
job->caps[1] = scr_ee_once_promise_box(st->promise);
scr_queue_microtask(job);
scr_ee_once_release(st);
return p;
return outer;
}
/* The adapter's tail: `cb` is the firing listener closure, `args` the
@@ -1130,3 +1151,9 @@ double scr_emitter_get_default_max(void) { return scr_emitter_default_max; }
static SCR_TL double scr_emitter_esm_default_max = 10;
double scr_emitter_esm_default_max_get(void) { return scr_emitter_esm_default_max; }
void scr_emitter_esm_sync(void) { scr_emitter_esm_default_max = scr_emitter_default_max; }
/* events.once with a signal (scr_events_once_signal.c) undoes an add whose
* 'newListener' announcement threw. */
void scr_emitter_unadd_quiet(ScrEmitter *em, ScrStr *name, ScrClosure *cb) {
scr_ee_unadd(em, name, cb);
}
+14
View File
@@ -64,6 +64,20 @@ ScrPromise *scr_events_once(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, voi
ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter,
ScrEeInvoke err_inv);
void scr_events_once_fire(ScrClosure *cb, ScrArr *args);
/* events.once(emitter, name, { signal }) (scr_events_once_signal.c): the
* scr_events_once arguments plus the checked-dynamic signal (borrowed;
* undefined for the plain form). Never throws: a non-AbortSignal value,
* an aborted signal or a throwing 'newListener' listener rejects the +1
* result. Its adapters end in scr_events_once_signal_fire. */
typedef struct ScrDyn ScrDyn; /* full definition below (C11 repeat) */
ScrPromise *scr_events_once_signal(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter,
ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter,
ScrEeInvoke err_inv, ScrDyn *signal);
void scr_events_once_signal_fire(ScrClosure *cb, ScrArr *args);
/* Removes the listener entry whose closure is `cb` (borrowed) without a
* 'removeListener' event: the undo of an add whose 'newListener'
* announcement threw. */
void scr_emitter_unadd_quiet(ScrEmitter *em, ScrStr *name, ScrClosure *cb);
ScrEmitter *scr_emitter_on(ScrEmitter *em, ScrStr *name, ScrClosure *cb /*moves*/,
ScrEeInvoke inv, bool once, bool prepend);
/* The dyn-adapted registration family (JS-lane checked-dynamic
@@ -0,0 +1,303 @@
/* events.once(emitter, name, { signal }) over typed listeners — Node 24's
* lib/events.js `once` with an options object. Linked only by programs
* that pass the options (the emitter and event-target units together).
*
* The signal is validated first: undefined is the plain form, a value that
* is not an AbortSignal rejects with ERR_INVALID_ARG_TYPE, and an aborted
* signal rejects with an AbortError whose cause is signal.reason, all
* without registering a listener. Otherwise the event's one-shot listener,
* the 'error' one (unless the event is 'error') and a once,
* resist-stop-propagation 'abort' listener on the signal share the state
* below. The event or the error removes the other emitter listener and the
* abort listener, then settles; the abort removes both emitter listeners
* and rejects with the AbortError.
*
* Ownership: the state owns the promise, the name and the signal. The
* emitter is weak in the state; the abort listener holds it strongly (the
* abort is the one settlement that does not come from the emitter itself,
* like the closure Node's abortListener keeps). The three listener closures
* reach the state through link objects; a link clears the state's pointer
* to its closure when that closure is freed, so removals never compare a
* stale identity. The cycle through the signal's listener ends when the
* promise settles or the signal aborts (or at the exit teardown of the
* signal's listeners). */
#include "scr_runtime.h"
#include <stdlib.h>
typedef struct ScrEosState {
size_t rc;
ScrPromise *promise; /* owned */
ScrEmitter *em; /* weak: alive whenever a listener fires */
ScrStr *name; /* owned */
ScrDyn *signal; /* owned; undefined without an abort listener */
ScrClosure *event_cb; /* weak, NULL once freed */
ScrClosure *error_cb; /* weak, NULL once freed or for 'error' */
ScrDyn *listener; /* weak abort listener, NULL once freed */
bool settled;
} ScrEosState;
enum { SCR_EOS_EVENT, SCR_EOS_ERROR, SCR_EOS_ABORT };
typedef struct ScrEosLink {
size_t rc;
ScrEosState *st; /* owned */
int slot;
} ScrEosLink;
static void scr_eos_oom(void) { scr_trap("scriptc: out of memory in events.once\n"); }
static void scr_eos_state_release(ScrEosState *st) {
if (--st->rc > 0) return;
scr_promise_release(st->promise);
scr_str_release(st->name);
scr_dyn_release(st->signal);
free(st);
}
static void *scr_eos_link_retain(void *p) {
((ScrEosLink *)p)->rc++;
return p;
}
static void scr_eos_link_release(void *p) {
ScrEosLink *link = p;
if (--link->rc > 0) return;
ScrEosState *st = link->st;
if (link->slot == SCR_EOS_EVENT) st->event_cb = NULL;
else if (link->slot == SCR_EOS_ERROR) st->error_cb = NULL;
else st->listener = NULL;
scr_eos_state_release(st);
free(link);
}
static ScrBox *scr_eos_link_box(ScrEosState *st, int slot) {
ScrEosLink *link = calloc(1, sizeof *link);
if (!link) scr_eos_oom();
link->rc = 1;
link->st = st;
link->slot = slot;
st->rc++;
ScrBox *box = scr_box_new_obj(&scr_eos_link_retain, &scr_eos_link_release, NULL);
scr_box_set_ref(box, link);
return box;
}
/* An emitter listener: caps[0] its boxer (the compiled adapter calls it),
* caps[1] its link. */
static ScrClosure *scr_eos_closure(void *adapter, ScrClosure *boxer, ScrEosState *st, int slot) {
ScrClosure *c = scr_closure_new(adapter, 2);
ScrBox *bx = scr_box_new(SCR_BOX_FUNC);
scr_box_set_ref(bx, scr_closure_retain(boxer));
c->caps[0] = bx;
c->caps[1] = scr_eos_link_box(st, slot);
return c;
}
/* new AbortError(undefined, { cause: signal.reason }), thrown. */
static void scr_eos_throw_abort(ScrDyn *signal) {
ScrStr *message = scr_str_new("The operation was aborted", 25);
ScrError *e = scr_error_new(SCR_ERR_ERROR, message);
scr_str_release(message);
scr_str_release(e->name);
e->name = scr_str_new("AbortError", 10);
/* AbortError assigns code, then name: own enumerable properties */
scr_error_set_code(e, "ABORT_ERR");
e->name_present = true;
e->name_enumerable = true;
ScrStr *k = scr_str_new("reason", 6);
ScrDyn *reason = scr_dyn_handle_key_get(signal, k);
scr_str_release(k);
scr_error_define_cause(e, reason ? reason : scr_dyn_undefined());
if (reason) scr_dyn_release(reason);
scr_throw_obj(e, &scr_error_retain_v, &scr_error_release_v, scr_error_trace_arg());
}
/* removeEventListener('abort', listener) on the signal, when the listener
* is still registered. */
static void scr_eos_remove_abort(ScrEosState *st) {
if (st->listener == NULL) return;
bool aborted;
ScrEventTargetCore *core = scr_et_signal_core_fn ? scr_et_signal_core_fn(st->signal, &aborted) : NULL;
if (!core) return;
ScrStr *abort = scr_str_new("abort", 5);
ScrDyn *type = scr_dyn_new_str(abort);
scr_str_release(abort);
ScrDyn *listener = scr_dyn_retain(st->listener);
ScrDyn *remove_args[2] = {type, listener};
ScrDyn *r = scr_et_remove_listener(core, remove_args, 2);
if (r) scr_dyn_release(r);
scr_dyn_release(type);
scr_dyn_release(listener);
}
/* emitter.removeListener(name, cb) for a listener closure still alive. */
static bool scr_eos_off(ScrEmitter *em, ScrStr *name, ScrClosure *cb) {
if (cb == NULL) return true;
scr_emitter_release(scr_emitter_off(em, name, cb));
return !scr_exc_pending();
}
/* The compiled adapter's tail: `cb` is the firing emitter listener, `args`
* the boxed event arguments (moves in). */
void scr_events_once_signal_fire(ScrClosure *cb, ScrArr *args) {
ScrEosLink *link = scr_box_get_ref(cb->caps[1]);
ScrEosState *st = link->st;
if (!st->settled) {
st->settled = true;
bool error = link->slot == SCR_EOS_ERROR;
/* Node's errorListener removes the event's listener and resolver the
* error's, each through removeListener; then the abort listener. */
bool ok;
if (error) {
ok = scr_eos_off(st->em, st->name, st->event_cb);
} else {
ScrStr *error_name = scr_str_new("error", 5);
ok = scr_eos_off(st->em, error_name, st->error_cb);
scr_str_release(error_name);
}
if (ok) {
scr_eos_remove_abort(st);
if (error) {
ScrDyn *reason = (ScrDyn *)scr_arr_get_ref(args, 0);
scr_dyn_throw(reason); /* moves reason */
scr_promise_reject_pending(st->promise);
} else {
scr_promise_fulfill_ref(st->promise, scr_arr_retain(args), scr_arr_retain_v,
scr_arr_release_v, args->cyc ? scr_arr_trace_v : NULL);
}
}
}
scr_arr_release(args);
scr_eos_link_release(link);
}
/* The signal's 'abort' listener: caps[0] its link, caps[1] the emitter. */
static ScrDyn *scr_eos_abort_thunk(ScrClosure *clo, ScrDyn *const *args, size_t argc) {
(void)args;
(void)argc;
ScrEosLink *link = scr_box_get_ref(clo->caps[0]);
ScrEmitter *em = scr_box_get_ref(clo->caps[1]);
ScrEosState *st = link->st;
bool ok = true;
if (!st->settled) {
st->settled = true;
ok = scr_eos_off(em, st->name, st->event_cb);
if (ok) {
ScrStr *error_name = scr_str_new("error", 5);
ok = scr_eos_off(em, error_name, st->error_cb);
scr_str_release(error_name);
}
if (ok) {
scr_eos_throw_abort(st->signal);
scr_promise_reject_pending(st->promise);
}
}
scr_emitter_release(em);
scr_eos_link_release(link);
return ok ? scr_dyn_retain(scr_dyn_undefined()) : NULL;
}
static ScrBox *scr_eos_promise_box(ScrPromise *p) {
ScrBox *box = scr_box_new_obj(&scr_promise_retain_v, &scr_promise_release_v, &scr_promise_trace_v);
scr_box_set_ref(box, scr_promise_retain(p));
return box;
}
static void scr_eos_adopt_job(ScrClosure *job) {
ScrPromise *outer = scr_box_get_ref(job->caps[0]);
ScrPromise *inner = scr_box_get_ref(job->caps[1]);
scr_promise_race_join(outer, inner, &scr_promise_adapt_copy);
scr_promise_release(inner);
scr_promise_release(outer);
}
/* Node's once is an async function returning the executor's promise: its
* result adopts that promise through a thenable job queued now, whose
* reaction settles the result one turn after the inner promise settles.
* Moves `inner` in; the result is +1. */
static ScrPromise *scr_eos_adopt(ScrPromise *inner) {
ScrPromise *outer = scr_promise_new();
ScrClosure *job = scr_closure_new((void *)&scr_eos_adopt_job, 2);
job->caps[0] = scr_eos_promise_box(outer);
job->caps[1] = scr_eos_promise_box(inner);
scr_promise_release(inner);
scr_queue_microtask(job);
return outer;
}
/* boxer and err_boxer are borrowed (err_boxer is NULL when the awaited
* event is 'error'); signal is borrowed. The result is +1 and never throws:
* every failure rejects it, as Node's async function does. */
ScrPromise *scr_events_once_signal(ScrEmitter *em, ScrStr *name, ScrClosure *boxer, void *adapter,
ScrEeInvoke inv, ScrClosure *err_boxer, void *err_adapter,
ScrEeInvoke err_inv, ScrDyn *signal) {
ScrPromise *result = scr_promise_new();
bool with_signal = signal->kind != SCR_DYN_UNDEF;
if (with_signal) {
bool aborted = false;
ScrEventTargetCore *core =
scr_et_signal_core_fn ? scr_et_signal_core_fn(signal, &aborted) : NULL;
if (!core) {
scr_dyn_prop_type_fail("options.signal", "an instance of AbortSignal", signal);
scr_promise_reject_pending(result);
return result;
}
if (aborted) {
scr_eos_throw_abort(signal);
scr_promise_reject_pending(result);
return result;
}
}
ScrEosState *st = calloc(1, sizeof *st);
if (!st) scr_eos_oom();
st->rc = 1;
st->promise = result;
st->em = em;
st->name = scr_str_retain(name);
st->signal = scr_dyn_retain(signal);
ScrClosure *event_cb = scr_eos_closure(adapter, boxer, st, SCR_EOS_EVENT);
ScrClosure *error_cb =
err_boxer != NULL ? scr_eos_closure(err_adapter, err_boxer, st, SCR_EOS_ERROR) : NULL;
st->event_cb = event_cb;
st->error_cb = error_cb;
/* Node registers inside the promise's executor: a throwing 'newListener'
* listener rejects the promise, and the listener it announced is never
* added (the event's listener stays when only the 'error' announcement
* throws). No abort listener follows a throw. */
ScrCaught *thrown = NULL;
scr_emitter_release(scr_emitter_on(em, name, event_cb, inv, true, false));
if (scr_exc_pending()) {
thrown = scr_exc_take();
scr_emitter_unadd_quiet(em, name, event_cb);
if (error_cb != NULL) scr_closure_release(error_cb); /* never registered */
} else if (error_cb != NULL) {
ScrStr *error = scr_str_new("error", 5);
scr_emitter_release(scr_emitter_on(em, error, error_cb, err_inv, true, false));
if (scr_exc_pending()) {
thrown = scr_exc_take();
scr_emitter_unadd_quiet(em, error, error_cb);
}
scr_str_release(error);
}
if (thrown != NULL) {
st->settled = true;
scr_rethrow(thrown);
scr_caught_release(thrown);
scr_promise_reject_pending(st->promise);
} else if (with_signal) {
ScrClosure *clo = scr_closure_new((void *)&scr_eos_abort_thunk, 2);
clo->caps[0] = scr_eos_link_box(st, SCR_EOS_ABORT);
clo->caps[1] = scr_box_new_obj(&scr_emitter_retain_v, &scr_emitter_release_v, NULL);
scr_box_set_ref(clo->caps[1], scr_emitter_retain(em));
ScrDyn *listener = scr_dyn_new_func(clo, &scr_eos_abort_thunk, 0, "()", "abortListener");
st->listener = listener;
ScrDyn *dispose = scr_events_add_abort_listener(signal, listener);
if (dispose) scr_dyn_release(dispose);
/* The signal's listener entry keeps it; the state's pointer is weak. */
scr_dyn_release(listener);
}
ScrPromise *inner = scr_promise_retain(st->promise);
scr_eos_state_release(st);
return scr_eos_adopt(inner);
}