Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(node/events): make on and emit methods callable by non-EventEmitter objects #1454

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions node/_tools/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"test-dns.js",
"test-event-emitter-add-listeners.js",
"test-event-emitter-check-listener-leaks.js",
"test-event-emitter-emit-context.js",
"test-event-emitter-get-max-listeners.js",
"test-event-emitter-invalid-listener.js",
"test-event-emitter-listener-count.js",
"test-event-emitter-listeners-side-effects.js",
Expand Down
25 changes: 25 additions & 0 deletions node/_tools/suites/parallel/test-event-emitter-emit-context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file

// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 16.12.0
// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually

'use strict';
const common = require('../common');
const assert = require('assert');
const EventEmitter = require('events');

// Test emit called by other context
const EE = new EventEmitter();

// Works as expected if the context has no `constructor.name`
{
const ctx = Object.create(null);
assert.throws(
() => EE.emit.call(ctx, 'error', new Error('foo')),
common.expectsError({ name: 'Error', message: 'foo' })
);
}

assert.strictEqual(EE.emit.call({}, 'foo'), false);
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// deno-fmt-ignore-file
// deno-lint-ignore-file

// Copyright Joyent and Node contributors. All rights reserved. MIT license.
// Taken from Node 16.12.0
// This file is automatically generated by "node/_tools/setup.ts". Do not modify this file manually

'use strict';
require('../common');
const assert = require('assert');
const EventEmitter = require('events');

const emitter = new EventEmitter();

assert.strictEqual(emitter.getMaxListeners(), EventEmitter.defaultMaxListeners);

emitter.setMaxListeners(0);
assert.strictEqual(emitter.getMaxListeners(), 0);

emitter.setMaxListeners(3);
assert.strictEqual(emitter.getMaxListeners(), 3);

// https://github.com/nodejs/node/issues/523 - second call should not throw.
const recv = {};
EventEmitter.prototype.on.call(recv, 'event', () => {});
EventEmitter.prototype.on.call(recv, 'event', () => {});
185 changes: 106 additions & 79 deletions node/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,46 +132,12 @@ export class EventEmitter {
EventEmitter.#init(this);
}

private _addListener(
eventName: string | symbol,
listener: GenericFunction | WrappedFunction,
prepend: boolean,
): this {
this.checkListenerArgument(listener);
this.emit("newListener", eventName, this.unwrapListener(listener));
if (this.hasListeners(eventName)) {
let listeners = this._events[eventName];
if (!Array.isArray(listeners)) {
listeners = [listeners];
this._events[eventName] = listeners;
}

if (prepend) {
listeners.unshift(listener);
} else {
listeners.push(listener);
}
} else if (this._events) {
this._events[eventName] = listener;
} else {
EventEmitter.#init(this);
(this._events as EventMap)[eventName] = listener;
}
const max = this.getMaxListeners();
if (max > 0 && this.listenerCount(eventName) > max) {
const warning = new MaxListenersExceededWarning(this, eventName);
this.warnIfNeeded(eventName, warning);
}

return this;
}

/** Alias for emitter.on(eventName, listener). */
addListener(
eventName: string | symbol,
listener: GenericFunction | WrappedFunction,
): this {
return this._addListener(eventName, listener, false);
return EventEmitter.#addListener(this, eventName, listener, false);
}

/**
Expand All @@ -182,10 +148,10 @@ export class EventEmitter {
*/
// deno-lint-ignore no-explicit-any
public emit(eventName: string | symbol, ...args: any[]): boolean {
if (this.hasListeners(eventName)) {
if (hasListeners(this._events, eventName)) {
if (
eventName === "error" &&
this.hasListeners(EventEmitter.errorMonitor)
hasListeners(this._events, EventEmitter.errorMonitor)
) {
this.emit(EventEmitter.errorMonitor, ...args);
}
Expand All @@ -201,7 +167,7 @@ export class EventEmitter {
}
return true;
} else if (eventName === "error") {
if (this.hasListeners(EventEmitter.errorMonitor)) {
if (hasListeners(this._events, EventEmitter.errorMonitor)) {
this.emit(EventEmitter.errorMonitor, ...args);
}
const errMsg = args.length > 0 ? args[0] : Error("Unhandled error.");
Expand All @@ -226,22 +192,15 @@ export class EventEmitter {
* EventEmitter.defaultMaxListeners.
*/
public getMaxListeners(): number {
return this.maxListeners == null
? EventEmitter.defaultMaxListeners
: this.maxListeners;
return EventEmitter.#getMaxListeners(this);
}

/**
* Returns the number of listeners listening to the event named
* eventName.
*/
public listenerCount(eventName: string | symbol): number {
if (this.hasListeners(eventName)) {
const maybeListeners = this._events[eventName];
return Array.isArray(maybeListeners) ? maybeListeners.length : 1;
} else {
return 0;
}
return EventEmitter.#listenerCount(this, eventName);
}

static listenerCount(
Expand All @@ -256,38 +215,22 @@ export class EventEmitter {
eventName: string | symbol,
unwrap: boolean,
): GenericFunction[] {
if (!target.hasListeners(eventName)) {
if (!hasListeners(target._events, eventName)) {
return [];
}

const eventListeners = target._events[eventName];
if (Array.isArray(eventListeners)) {
return unwrap
? this.unwrapListeners(eventListeners)
? unwrapListeners(eventListeners)
: eventListeners.slice(0) as GenericFunction[];
} else {
return [
unwrap ? this.unwrapListener(eventListeners) : eventListeners,
unwrap ? unwrapListener(eventListeners) : eventListeners,
] as GenericFunction[];
}
}

private unwrapListeners(
arr: (GenericFunction | WrappedFunction)[],
): GenericFunction[] {
const unwrappedListeners = new Array(arr.length) as GenericFunction[];
for (let i = 0; i < arr.length; i++) {
unwrappedListeners[i] = this.unwrapListener(arr[i]);
}
return unwrappedListeners;
}

private unwrapListener(
listener: GenericFunction | WrappedFunction,
): GenericFunction {
return (listener as WrappedFunction)["listener"] ?? listener;
}

/** Returns a copy of the array of listeners for the event named eventName.*/
public listeners(eventName: string | symbol): GenericFunction[] {
return this._listeners(this, eventName, true);
Expand Down Expand Up @@ -350,7 +293,7 @@ export class EventEmitter {
eventName: string | symbol,
listener: GenericFunction,
): WrappedFunction {
this.checkListenerArgument(listener);
checkListenerArgument(listener);
const wrapper = function (
this: {
eventName: string | symbol;
Expand Down Expand Up @@ -399,7 +342,7 @@ export class EventEmitter {
eventName: string | symbol,
listener: GenericFunction | WrappedFunction,
): this {
return this._addListener(eventName, listener, true);
return EventEmitter.#addListener(this, eventName, listener, true);
}

/**
Expand All @@ -423,13 +366,13 @@ export class EventEmitter {
}

if (eventName) {
if (this.hasListeners(eventName)) {
if (hasListeners(this._events, eventName)) {
const listeners = ensureArray(this._events[eventName]).slice()
.reverse();
for (const listener of listeners) {
this.removeListener(
eventName,
this.unwrapListener(listener),
unwrapListener(listener),
);
}
}
Expand All @@ -453,8 +396,8 @@ export class EventEmitter {
eventName: string | symbol,
listener: GenericFunction,
): this {
this.checkListenerArgument(listener);
if (this.hasListeners(eventName)) {
checkListenerArgument(listener);
if (hasListeners(this._events, eventName)) {
const maybeArr = this._events[eventName];

assert(maybeArr);
Expand Down Expand Up @@ -664,14 +607,73 @@ export class EventEmitter {
}
}

private checkListenerArgument(listener: unknown): void {
if (typeof listener !== "function") {
throw new ERR_INVALID_ARG_TYPE("listener", "function", listener);
// The generic type here is a workaround for `TS2322 [ERROR]: Type 'EventEmitter' is not assignable to type 'this'.` error.
static #addListener<T extends EventEmitter>(
target: T,
eventName: string | symbol,
listener: GenericFunction | WrappedFunction,
prepend: boolean,
): T {
checkListenerArgument(listener);
let events = target._events;
if (events == null) {
EventEmitter.#init(target);
events = target._events;
}

if (events.newListener) {
target.emit("newListener", eventName, unwrapListener(listener));
}

if (hasListeners(events, eventName)) {
let listeners = events[eventName];
if (!Array.isArray(listeners)) {
listeners = [listeners];
events[eventName] = listeners;
}

if (prepend) {
listeners.unshift(listener);
} else {
listeners.push(listener);
}
} else if (events) {
events[eventName] = listener;
}

const max = EventEmitter.#getMaxListeners(target);
if (max > 0 && EventEmitter.#listenerCount(target, eventName) > max) {
const warning = new MaxListenersExceededWarning(target, eventName);
EventEmitter.#warnIfNeeded(target, eventName, warning);
}

return target;
}

static #getMaxListeners(target: EventEmitter): number {
return target.maxListeners == null
? EventEmitter.defaultMaxListeners
: target.maxListeners;
}

static #listenerCount(
target: EventEmitter,
eventName: string | symbol,
): number {
if (hasListeners(target._events, eventName)) {
const maybeListeners = target._events[eventName];
return Array.isArray(maybeListeners) ? maybeListeners.length : 1;
} else {
return 0;
}
}

private warnIfNeeded(eventName: string | symbol, warning: Error): void {
const listeners = this._events[eventName];
static #warnIfNeeded(
target: EventEmitter,
eventName: string | symbol,
warning: Error,
) {
const listeners = target._events[eventName];
if (listeners.warned) {
return;
}
Expand All @@ -688,12 +690,37 @@ export class EventEmitter {
maybeProcess.emit("warning", warning);
}
}
}

private hasListeners(eventName: string | symbol): boolean {
return this._events && Boolean(this._events[eventName]);
function checkListenerArgument(listener: unknown): void {
if (typeof listener !== "function") {
throw new ERR_INVALID_ARG_TYPE("listener", "function", listener);
}
}

function hasListeners(
maybeEvents: EventMap | null | undefined,
eventName: string | symbol,
): boolean {
return maybeEvents != null && Boolean(maybeEvents[eventName]);
}

function unwrapListeners(
arr: (GenericFunction | WrappedFunction)[],
): GenericFunction[] {
const unwrappedListeners = new Array(arr.length) as GenericFunction[];
for (let i = 0; i < arr.length; i++) {
unwrappedListeners[i] = unwrapListener(arr[i]);
}
return unwrappedListeners;
}

function unwrapListener(
listener: GenericFunction | WrappedFunction,
): GenericFunction {
return (listener as WrappedFunction)["listener"] ?? listener;
}

// EventEmitter#on should point to the same function as EventEmitter#addListener.
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
// EventEmitter#off should point to the same function as EventEmitter#removeListener.
Expand Down