Surfaced by the per-module parity matrix (#812 baseline; `events` is at diff=26).
EventEmitter is foundational — `net`, `http`, `stream`, `child_process`, and most npm packages use it as a base. A correctness gap here cascades. The good news is many of the broken methods look like they share a single root cause (the internal listener-table representation), so a single fix likely repairs much of the diff.
Specific gaps (Perry vs Node)
| Probe |
Node |
Perry |
| `em.listenerCount('inc')` (after 2 .on()) |
`2` |
`0` |
| `em.eventNames()` |
`[ 'inc' ]` |
`0` (wrong type entirely) |
| `em.once → handler arg captured` |
`[ 7 ]` |
`[]` |
| `em.addListener / removeListener` |
`[ 1 ]` |
`[]` |
| `em.prependListener` order |
`[ 'a', 'b' ]` |
`[ 'b' ]` (only second listener kept) |
| `em.prependOnceListener` order |
`[ 'a', 'b', 'b' ]` |
`[ 'b', 'b' ]` |
| `em.listeners('inc')` count |
`2` |
`undefined` |
| `em.rawListeners('inc')` count |
`2` |
`undefined` |
| `em.getMaxListeners()` after setMaxListeners(42) |
`42` |
`0` (setMax doesn't persist) |
| `em.removeAllListeners()` then names |
`[]` |
`0` |
| `events.once(em,'x')` (module-level helper) |
resolves to array |
throws "Cannot read properties of undefined (reading 'length')" |
| `events.on(em,'evt')` async-iterator |
`[ 1, 2, 3 ]` |
`[]` |
| `events.getEventListeners(em,'e')` |
array |
throws same |
| `events.getMaxListeners(em)` |
number |
undefined |
| `events.setMaxListeners(em,20) → get` |
`20` |
`0` |
| `events.listenerCount(em,'e')` static helper |
`3` |
`undefined` |
| `EventEmitter.listenerCount(em,'e')` static helper |
`3` |
`undefined` |
| `events.addAbortListener(signal, fn)` |
fires + returns Disposable |
not invoked |
| `events.defaultMaxListeners` |
`10` |
(missing) |
| `events.errorMonitor` / `events.captureRejectionSymbol` |
symbols |
(missing) |
| `'newListener'` / `'removeListener'` synthetic events |
array of names |
not emitted |
Root-cause hypothesis
The pattern "listenerCount → 0", "eventNames → 0", "listeners → undefined" is consistent with the internal listener map either not being populated by `.on()` / `.addListener()`, or being populated under a different per-instance key than the readers read from. The `prependListener` case is the most diagnostic: when called twice, only the second listener is retained — pointing at the on-store side overwriting rather than appending.
`events.once` / `events.on` / `events.getEventListeners` all bottom out in "`Cannot read properties of undefined`" — those are module-level helpers reaching into the EE's internal state via a getter that's missing entirely.
`events.defaultMaxListeners`, `events.errorMonitor`, `events.captureRejectionSymbol` are simple exported constants the module never exposed — quick wins.
Why this matters
A working EventEmitter is a prerequisite for: `net`, `http` (`server.on('request')`), `stream` (`.on('data')`, `.on('end')`, `.on('error')`), `child_process`, any `Readable`/`Writable` flow, and ~most npm packages that emit progress / lifecycle events. Several other parity-matrix modules with large diffs (`net` at 63, `stream` at 78, `http` at 259) probably collapse significantly once `events` is correct, because the test programs there call `.on()` / `listenerCount()` / `listeners()` to introspect state.
Surface impact
Probably the single highest-leverage parity fix on the matrix today, ahead of the #841 submodule cluster (which is wider but lower-impact per module). Closing this likely pulls `events` to PASS and meaningfully cuts the diffs on net/stream/http.
Part of #793. Surfaced by the #812 per-module matrix baseline.
Surfaced by the per-module parity matrix (#812 baseline; `events` is at diff=26).
EventEmitter is foundational — `net`, `http`, `stream`, `child_process`, and most npm packages use it as a base. A correctness gap here cascades. The good news is many of the broken methods look like they share a single root cause (the internal listener-table representation), so a single fix likely repairs much of the diff.
Specific gaps (Perry vs Node)
Root-cause hypothesis
The pattern "listenerCount → 0", "eventNames → 0", "listeners → undefined" is consistent with the internal listener map either not being populated by `.on()` / `.addListener()`, or being populated under a different per-instance key than the readers read from. The `prependListener` case is the most diagnostic: when called twice, only the second listener is retained — pointing at the on-store side overwriting rather than appending.
`events.once` / `events.on` / `events.getEventListeners` all bottom out in "`Cannot read properties of undefined`" — those are module-level helpers reaching into the EE's internal state via a getter that's missing entirely.
`events.defaultMaxListeners`, `events.errorMonitor`, `events.captureRejectionSymbol` are simple exported constants the module never exposed — quick wins.
Why this matters
A working EventEmitter is a prerequisite for: `net`, `http` (`server.on('request')`), `stream` (`.on('data')`, `.on('end')`, `.on('error')`), `child_process`, any `Readable`/`Writable` flow, and ~most npm packages that emit progress / lifecycle events. Several other parity-matrix modules with large diffs (`net` at 63, `stream` at 78, `http` at 259) probably collapse significantly once `events` is correct, because the test programs there call `.on()` / `listenerCount()` / `listeners()` to introspect state.
Surface impact
Probably the single highest-leverage parity fix on the matrix today, ahead of the #841 submodule cluster (which is wider but lower-impact per module). Closing this likely pulls `events` to PASS and meaningfully cuts the diffs on net/stream/http.
Part of #793. Surfaced by the #812 per-module matrix baseline.