Repository navigation
Share one listener list between typed and untyped uses of an EventEmitter - #1563
Merged
Merged
Conversation
…tter
An emitter used both through its static type and through an untyped
reference aborted: a typed `on` followed by an untyped `emit` (or a typed
`emit` from a class body reaching a listener added through `any`) hit
"computed event collided with a fixed event", and an untyped
`emit('error', ...)` trapped outright. Listener entries held either a
typed adapter or a checked-dynamic thunk, and the emit core trapped when
an entry lacked the form the emit carried.
Entries may now carry both forms. Listeners registered through
scr_emitter_on_dyn keep their thunk beside the typed adapter; typed
emits call the adapter and untyped emits call the thunk with the
arguments as given. The class dispatch of compiled emitters registers
untyped listeners under an event typed code fixes with a typed adapter
for its tuple, and converts the arguments of an untyped emit of such an
event to the tuple for listeners with typed parameters
(scr_emitter_emit_mixed; a mismatch throws the checked conversion's
TypeError). Registrations and emits through untyped values otherwise go
through scr_emitter_on_any/emit_any: runtime adapters under 'error' and
the meta events, and Node's unhandled-'error' contract. The emit core
throws instead of trapping when an entry has no usable form. The
dynamic half of the registry sits behind a hook only on_any installs,
and on_flex/emit_flex keep their plain behavior, so programs without
untyped emitter calls link none of it.
listenerCount, eventNames, removeAllListeners, listeners, rawListeners,
setMaxListeners and getMaxListeners through an untyped value on a
compiled emitter answered "is not a function"; they now dispatch to the
native registry.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Reading an EventEmitter.prototype method off a compiled emitter (a program class extending EventEmitter, or new EventEmitter()) through an untyped value answered undefined (`typeof o.on`, `const f = o.on; f.call(o, ...)`, `o.emit.bind(o)`), and EventEmitter.prototype.on in JavaScript was a fence. Such reads, typed value reads, computed keys, `in`, and EventEmitter.prototype.<method> now answer the runtime's EventEmitter.prototype functions, the ones constructor-function emitters inherit, so `o.on === o.addListener === EventEmitter.prototype.on` and names and lengths match Node. The first read installs a program dispatch that those functions call for a compiled emitter receiver; it invokes the method through the existing untyped dispatch with the call's argument count (emit forwards up to 16 arguments after the name, then fences). An untyped emit on, or read of emit from, a class that overrides emit now fences instead of bypassing the override for the native emit. A subclass visited under the boxed EventEmitter capsule keeps its own dispatch plan for values boxed as itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The dispatch instantiates one bridged emit per argument count and emitter class; capping it at four keeps programs that only read emitter members through computed keys from growing by the full table. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Revert 3a27a92 ("Forward at most four emit arguments through EventEmitter method values") and 0cf62eb ("Answer EventEmitter methods read as values off compiled emitters"). Under the scope-B plan, EventEmitter methods read as values, `EventEmitter.prototype.on` in JavaScript and constructor-function emitters belong to Node islands, not the native compiler. The typed/untyped listener sharing and the catchable throw in place of the trap stay. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
An event that typed code only listened to with a listener taking no parameters had no bridge entry: the scan marked a listener as typed only when one of its parameters was not a checked-dynamic value. Emitting the event through an `any` reference then threw instead of calling the listener. A TypeScript listener with no parameters now marks the event as typed. A JavaScript function without parameters still registers as before, since it can read `arguments`.
This branch was successfully deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An EventEmitter used both through its static type and through an untyped reference aborted the process. A typed
onfollowed by an untypedemitof the same event, or a typedemitfrom a class body reaching a listener added throughany(the shape of function-local class expressions whose instances escape untyped), hitscr_trap("scriptc: computed event collided with a fixed event"), and an untypedemit('error', ...)trapped unconditionally.Root cause: a listener entry held either a typed adapter (called with the emit site's fixed tuple as C variadics) or a checked-dynamic thunk (called with the untyped argument vector), and the emit core trapped when an entry lacked the form the emit carried.
Runtime (
scr_events_emitter.c):scr_emitter_on_dynkeeps the listener's thunk beside its typed adapter, so typed emits call the adapter and untyped emits call the thunk with the arguments as given (extra arguments included).scr_emitter_emit_mixedcarries both the untyped arguments and a compiler-converted typed tuple, for untyped emits of an event that has listeners with typed parameters.scr_emitter_has_typedsays whether the conversion is needed.scr_emitter_on_any/scr_emitter_emit_anyserve registrations and emits through untyped values: runtime-built typed adapters undererrorand the meta events;emit('error')emits to errorMonitor first, then to listeners, and with none throws an Error payload itself and primitive payloads asERR_UNHANDLED_ERROR. UntypednewListener/removeListenerlisteners receive the listener function when it was registered through an untyped value.scr_emitter_on_anyinstalls, andscr_emitter_on_flex/emit_flexkeep their plain behavior for computed names and the worker registries, so programs without untyped emitter calls link none of the new dynamic code.Compiler:
on/once/prepend*under an event typed code emits or listens to with typed parameters throughemitter.onDynwith a typed adapter for the event's tuple, and untypedemitof such an event converts its arguments to the tuple (a mismatch throws the checked conversion'sTypeError) whenhasTypedsays a typed listener is registered. These helpers exist only in programs that have such events and untyped emitter calls.listenerCount,eventNames,removeAllListeners,listeners,rawListeners,setMaxListenersandgetMaxListenerscalled through an untyped value on a compiled emitter (a program class extending EventEmitter, ornew EventEmitter()) answered "is not a function"; they now dispatch to the native registry with Node's results. In TypeScript the last four names join the dyn-dispatch method set so such calls lower to a dispatch.Explicit refusals remain for what is not modeled, documented on the EventEmitter limitations page:
listeners()through an untyped value of an event with a typed listener, untypedemit('error')of a non-Error object with no listener, untyped emits whose arguments do not convert to a typed listener's tuple (TypeError before any listener runs), and typed listener functions as the second meta-event argument (undefined).Scope: under the scope-B plan, this PR keeps only the typed/untyped listener sharing and the catchable throw in place of the trap. The follow-up that answered EventEmitter methods read as values (
o.on,EventEmitter.prototype.onin JavaScript, constructor-function emitters,dyn.emitterMethod, corpus 6600–6602) was reverted in 3f3e90b, because those forms belong to Node islands. Reading EventEmitter methods as properties through an untyped value still answersundefined, as the limitations page says. main (0237d70) is merged in.Evidence (Linux sandbox, final branch against main 0237d70, both trees built with their own runtime pack):
-t "658[0-4]|emitter"18 passed, sanitized 6 passed,-t "event|5137|6469|648[0-2]|6494|cjs-class-hierarchy"56 passed.packages/runtime/testandpackages/compiler/src/ir: 492 passed, 1 failed (bigint runtime builds for wasm32, the known wasm32-wasi environment failure in the sandbox).pnpm lint,pnpm format:check,pnpm test:ts7 --baselines-onlypass; the self-hosted--emit=irbuild ofpackages/compiler/src/native/cli.tsreports no diagnostics.scripts/zero-cost-gate.mjs, bench, self and TypeScript corpus, 2718 programs): 2502 identical, 216 differ. The self-hosted compiler is identical. 209 differ only in the executable, because the emitter runtime unit they link changed (scr_ee_addin 207,scr_ee_emit_corein 6; growth +0.01% to +3.3%). Six bench workloads are among them (channel-messages, atomics-contended, intern-table, thread-slots, typed-array-scan-workers, worker-messages: +120 to +128 B each); their IR and LLVM are identical. 7 differ in IR/LLVM: 6469, 6480, 6481, 6482, 6494 and cjs-class-hierarchy-npm make untyped calls on compiled emitters, and their class dispatch now calls the new entry points (the intended change); fs-extra-package differs only by the shim's absolute checkout path. Compile time: bench geomean -0.27% [95% CI -1.71%, +0.96%], self-hosted -1.6%, both within tolerance.Overlap: #1544 (
opt/ee-class-value-crash) also touches EventEmitter handling through untyped values (builtin-class-dyn.ts, relational.ts, lowerer.ts, lower-classes.ts); this change touches none of those files.🤖 Generated with Claude Code