summaryrefslogtreecommitdiff
path: root/std/node
diff options
context:
space:
mode:
authorChris Knight <cknight1234@gmail.com>2020-02-10 23:19:48 +0000
committerGitHub <noreply@github.com>2020-02-10 18:19:48 -0500
commit81905a867ea3f942619229e330840d132c57a5da (patch)
tree4bcdac6a58dfc5fd14e83f04c3f388a6d43968d3 /std/node
parente1105a159406d8b64a833fa3266fd4ac7fc47a00 (diff)
feat: Event emitter node polyfill (#3944)
Diffstat (limited to 'std/node')
-rw-r--r--std/node/README.md2
-rw-r--r--std/node/events.ts383
-rw-r--r--std/node/events_test.ts403
-rw-r--r--std/node/module.ts8
-rw-r--r--std/node/os.ts24
-rw-r--r--std/node/util.ts17
6 files changed, 814 insertions, 23 deletions
diff --git a/std/node/README.md b/std/node/README.md
index 8d9f4ff40..37084ed25 100644
--- a/std/node/README.md
+++ b/std/node/README.md
@@ -16,7 +16,7 @@ deno standard library as it's a compatiblity module.
- [ ] crypto
- [ ] dgram
- [ ] dns
-- [ ] events
+- [x] events
- [x] fs _partly_
- [ ] http
- [ ] http2
diff --git a/std/node/events.ts b/std/node/events.ts
new file mode 100644
index 000000000..f035da2fc
--- /dev/null
+++ b/std/node/events.ts
@@ -0,0 +1,383 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+// Copyright (c) 2019 Denolibs authors. All rights reserved. MIT license.
+// Copyright Joyent, Inc. and other Node contributors.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a
+// copy of this software and associated documentation files (the
+// "Software"), to deal in the Software without restriction, including
+// without limitation the rights to use, copy, modify, merge, publish,
+// distribute, sublicense, and/or sell copies of the Software, and to permit
+// persons to whom the Software is furnished to do so, subject to the
+// following conditions:
+//
+// The above copyright notice and this permission notice shall be included
+// in all copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
+// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
+// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
+// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
+// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
+// USE OR OTHER DEALINGS IN THE SOFTWARE.
+
+import { validateIntegerRange } from "./util.ts";
+
+export interface WrappedFunction extends Function {
+ listener: Function;
+}
+
+/**
+ * See also https://nodejs.org/api/events.html
+ */
+export default class EventEmitter {
+ public static defaultMaxListeners = 10;
+ private maxListeners: number | undefined;
+ private _events: Map<string | symbol, Array<Function | WrappedFunction>>;
+
+ public constructor() {
+ this._events = new Map();
+ }
+
+ private _addListener(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction,
+ prepend: boolean
+ ): this {
+ this.emit("newListener", eventName, listener);
+ if (this._events.has(eventName)) {
+ const listeners = this._events.get(eventName) as Array<
+ Function | WrappedFunction
+ >;
+ if (prepend) {
+ listeners.unshift(listener);
+ } else {
+ listeners.push(listener);
+ }
+ } else {
+ this._events.set(eventName, [listener]);
+ }
+ const max = this.getMaxListeners();
+ if (max > 0 && this.listenerCount(eventName) > max) {
+ const warning = new Error(
+ `Possible EventEmitter memory leak detected.
+ ${this.listenerCount(eventName)} ${eventName.toString()} listeners.
+ Use emitter.setMaxListeners() to increase limit`
+ );
+ warning.name = "MaxListenersExceededWarning";
+ console.warn(warning);
+ }
+
+ return this;
+ }
+
+ /** Alias for emitter.on(eventName, listener). */
+ public addListener(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction
+ ): this {
+ return this._addListener(eventName, listener, false);
+ }
+
+ /**
+ * Synchronously calls each of the listeners registered for the event named
+ * eventName, in the order they were registered, passing the supplied
+ * arguments to each.
+ * @return true if the event had listeners, false otherwise
+ */
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ public emit(eventName: string | symbol, ...args: any[]): boolean {
+ if (this._events.has(eventName)) {
+ const listeners = (this._events.get(eventName) as Function[]).slice(); // We copy with slice() so array is not mutated during emit
+ for (const listener of listeners) {
+ try {
+ listener.apply(this, args);
+ } catch (err) {
+ this.emit("error", err);
+ }
+ }
+ return true;
+ } else if (eventName === "error") {
+ const errMsg = args.length > 0 ? args[0] : Error("Unhandled error.");
+ throw errMsg;
+ }
+ return false;
+ }
+
+ /**
+ * Returns an array listing the events for which the emitter has
+ * registered listeners.
+ */
+ public eventNames(): [string | symbol] {
+ return Array.from(this._events.keys()) as [string | symbol];
+ }
+
+ /**
+ * Returns the current max listener value for the EventEmitter which is
+ * either set by emitter.setMaxListeners(n) or defaults to
+ * EventEmitter.defaultMaxListeners.
+ */
+ public getMaxListeners(): number {
+ return this.maxListeners || EventEmitter.defaultMaxListeners;
+ }
+
+ /**
+ * Returns the number of listeners listening to the event named
+ * eventName.
+ */
+ public listenerCount(eventName: string | symbol): number {
+ if (this._events.has(eventName)) {
+ return (this._events.get(eventName) as Function[]).length;
+ } else {
+ return 0;
+ }
+ }
+
+ private _listeners(
+ target: EventEmitter,
+ eventName: string | symbol,
+ unwrap: boolean
+ ): Function[] {
+ if (!target._events.has(eventName)) {
+ return [];
+ }
+ const eventListeners: Function[] = target._events.get(
+ eventName
+ ) as Function[];
+
+ return unwrap
+ ? this.unwrapListeners(eventListeners)
+ : eventListeners.slice(0);
+ }
+
+ private unwrapListeners(arr: Function[]): Function[] {
+ const unwrappedListeners: Function[] = new Array(arr.length) as Function[];
+ for (let i = 0; i < arr.length; i++) {
+ unwrappedListeners[i] = arr[i]["listener"] || arr[i];
+ }
+ return unwrappedListeners;
+ }
+
+ /** Returns a copy of the array of listeners for the event named eventName.*/
+ public listeners(eventName: string | symbol): Function[] {
+ return this._listeners(this, eventName, true);
+ }
+
+ /**
+ * Returns a copy of the array of listeners for the event named eventName,
+ * including any wrappers (such as those created by .once()).
+ */
+ public rawListeners(
+ eventName: string | symbol
+ ): Array<Function | WrappedFunction> {
+ return this._listeners(this, eventName, false);
+ }
+
+ /** Alias for emitter.removeListener(). */
+ public off(eventName: string | symbol, listener: Function): this {
+ return this.removeListener(eventName, listener);
+ }
+
+ /**
+ * Adds the listener function to the end of the listeners array for the event
+ * named eventName. No checks are made to see if the listener has already
+ * been added. Multiple calls passing the same combination of eventName and
+ * listener will result in the listener being added, and called, multiple
+ * times.
+ */
+ public on(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction
+ ): this {
+ return this.addListener(eventName, listener);
+ }
+
+ /**
+ * Adds a one-time listener function for the event named eventName. The next
+ * time eventName is triggered, this listener is removed and then invoked.
+ */
+ public once(eventName: string | symbol, listener: Function): this {
+ const wrapped: WrappedFunction = this.onceWrap(eventName, listener);
+ this.on(eventName, wrapped);
+ return this;
+ }
+
+ // Wrapped function that calls EventEmitter.removeListener(eventName, self) on execution.
+ private onceWrap(
+ eventName: string | symbol,
+ listener: Function
+ ): WrappedFunction {
+ const wrapper = function(
+ this: {
+ eventName: string | symbol;
+ listener: Function;
+ rawListener: Function;
+ context: EventEmitter;
+ },
+ ...args: any[] // eslint-disable-line @typescript-eslint/no-explicit-any
+ ): void {
+ this.context.removeListener(this.eventName, this.rawListener);
+ this.listener.apply(this.context, args);
+ };
+ const wrapperContext = {
+ eventName: eventName,
+ listener: listener,
+ rawListener: wrapper,
+ context: this
+ };
+ const wrapped = wrapper.bind(wrapperContext);
+ wrapperContext.rawListener = wrapped;
+ wrapped.listener = listener;
+ return wrapped as WrappedFunction;
+ }
+
+ /**
+ * Adds the listener function to the beginning of the listeners array for the
+ * event named eventName. No checks are made to see if the listener has
+ * already been added. Multiple calls passing the same combination of
+ * eventName and listener will result in the listener being added, and
+ * called, multiple times.
+ */
+ public prependListener(
+ eventName: string | symbol,
+ listener: Function | WrappedFunction
+ ): this {
+ return this._addListener(eventName, listener, true);
+ }
+
+ /**
+ * Adds a one-time listener function for the event named eventName to the
+ * beginning of the listeners array. The next time eventName is triggered,
+ * this listener is removed, and then invoked.
+ */
+ public prependOnceListener(
+ eventName: string | symbol,
+ listener: Function
+ ): this {
+ const wrapped: WrappedFunction = this.onceWrap(eventName, listener);
+ this.prependListener(eventName, wrapped);
+ return this;
+ }
+
+ /** Removes all listeners, or those of the specified eventName. */
+ public removeAllListeners(eventName?: string | symbol): this {
+ if (this._events === undefined) {
+ return this;
+ }
+
+ if (this._events.has(eventName)) {
+ const listeners = (this._events.get(eventName) as Array<
+ Function | WrappedFunction
+ >).slice(); // Create a copy; We use it AFTER it's deleted.
+ this._events.delete(eventName);
+ for (const listener of listeners) {
+ this.emit("removeListener", eventName, listener);
+ }
+ } else {
+ const eventList: [string | symbol] = this.eventNames();
+ eventList.map((value: string | symbol) => {
+ this.removeAllListeners(value);
+ });
+ }
+
+ return this;
+ }
+
+ /**
+ * Removes the specified listener from the listener array for the event
+ * named eventName.
+ */
+ public removeListener(eventName: string | symbol, listener: Function): this {
+ if (this._events.has(eventName)) {
+ const arr: Array<Function | WrappedFunction> = this._events.get(
+ eventName
+ );
+
+ let listenerIndex = -1;
+ for (let i = arr.length - 1; i >= 0; i--) {
+ // arr[i]["listener"] is the reference to the listener inside a bound 'once' wrapper
+ if (arr[i] == listener || arr[i]["listener"] == listener) {
+ listenerIndex = i;
+ break;
+ }
+ }
+
+ if (listenerIndex >= 0) {
+ arr.splice(listenerIndex, 1);
+ this.emit("removeListener", eventName, listener);
+ if (arr.length === 0) {
+ this._events.delete(eventName);
+ }
+ }
+ }
+ return this;
+ }
+
+ /**
+ * By default EventEmitters will print a warning if more than 10 listeners
+ * are added for a particular event. This is a useful default that helps
+ * finding memory leaks. Obviously, not all events should be limited to just
+ * 10 listeners. The emitter.setMaxListeners() method allows the limit to be
+ * modified for this specific EventEmitter instance. The value can be set to
+ * Infinity (or 0) to indicate an unlimited number of listeners.
+ */
+ public setMaxListeners(n: number): this {
+ validateIntegerRange(n, "maxListeners", 0);
+ this.maxListeners = n;
+ return this;
+ }
+}
+
+/**
+ * Creates a Promise that is fulfilled when the EventEmitter emits the given
+ * event or that is rejected when the EventEmitter emits 'error'. The Promise
+ * will resolve with an array of all the arguments emitted to the given event.
+ */
+export function once(
+ emitter: EventEmitter | EventTarget,
+ name: string
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+): Promise<any[]> {
+ return new Promise((resolve, reject) => {
+ if (emitter instanceof EventTarget) {
+ // EventTarget does not have `error` event semantics like Node
+ // EventEmitters, we do not listen to `error` events here.
+ emitter.addEventListener(
+ name,
+ (...args) => {
+ resolve(args);
+ },
+ { once: true, passive: false, capture: false }
+ );
+ return;
+ } else if (emitter instanceof EventEmitter) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const eventListener = (...args: any[]): void => {
+ if (errorListener !== undefined) {
+ emitter.removeListener("error", errorListener);
+ }
+ resolve(args);
+ };
+ let errorListener: Function;
+
+ // Adding an error listener is not optional because
+ // if an error is thrown on an event emitter we cannot
+ // guarantee that the actual event we are waiting will
+ // be fired. The result could be a silent way to create
+ // memory or file descriptor leaks, which is something
+ // we should avoid.
+ if (name !== "error") {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ errorListener = (err: any): void => {
+ emitter.removeListener(name, eventListener);
+ reject(err);
+ };
+
+ emitter.once("error", errorListener);
+ }
+
+ emitter.once(name, eventListener);
+ return;
+ }
+ });
+}
diff --git a/std/node/events_test.ts b/std/node/events_test.ts
new file mode 100644
index 000000000..0d8dd52e1
--- /dev/null
+++ b/std/node/events_test.ts
@@ -0,0 +1,403 @@
+import { test } from "../testing/mod.ts";
+import {
+ assert,
+ assertEquals,
+ fail,
+ assertThrows
+} from "../testing/asserts.ts";
+import EventEmitter, { WrappedFunction, once } from "./events.ts";
+
+const shouldNeverBeEmitted: Function = () => {
+ fail("Should never be called");
+};
+
+test({
+ name:
+ 'When adding a new event, "eventListener" event is fired before adding the listener',
+ fn() {
+ let eventsFired = [];
+ const testEmitter = new EventEmitter();
+ testEmitter.on("newListener", event => {
+ if (event !== "newListener") {
+ eventsFired.push("newListener");
+ }
+ });
+ testEmitter.on("event", () => {
+ eventsFired.push("event");
+ });
+ assertEquals(eventsFired, ["newListener"]);
+ eventsFired = [];
+ testEmitter.emit("event");
+ assertEquals(eventsFired, ["event"]);
+ }
+});
+
+test({
+ name:
+ 'When removing a listenert, "removeListener" event is fired after removal',
+ fn() {
+ const eventsFired = [];
+ const testEmitter = new EventEmitter();
+ testEmitter.on("removeListener", () => {
+ eventsFired.push("removeListener");
+ });
+ const eventFunction = function(): void {
+ eventsFired.push("event");
+ };
+ testEmitter.on("event", eventFunction);
+
+ assertEquals(eventsFired, []);
+ testEmitter.removeListener("event", eventFunction);
+ assertEquals(eventsFired, ["removeListener"]);
+ }
+});
+
+test({
+ name:
+ "Default max listeners is 10, but can be changed by direct assignment only",
+ fn() {
+ assertEquals(EventEmitter.defaultMaxListeners, 10);
+ new EventEmitter().setMaxListeners(20);
+ assertEquals(EventEmitter.defaultMaxListeners, 10);
+ EventEmitter.defaultMaxListeners = 20;
+ assertEquals(EventEmitter.defaultMaxListeners, 20);
+ EventEmitter.defaultMaxListeners = 10; //reset back to original value
+ }
+});
+
+test({
+ name: "addListener adds a listener, and listener count is correct",
+ fn() {
+ const testEmitter = new EventEmitter();
+ testEmitter.on("event", shouldNeverBeEmitted);
+ assertEquals(1, testEmitter.listenerCount("event"));
+ testEmitter.on("event", shouldNeverBeEmitted);
+ assertEquals(2, testEmitter.listenerCount("event"));
+ }
+});
+
+test({
+ name: "Emitted events are called synchronously in the order they were added",
+ fn() {
+ const testEmitter = new EventEmitter();
+ const eventsFired = [];
+ testEmitter.on("event", oneArg => {
+ eventsFired.push("event(" + oneArg + ")");
+ });
+ testEmitter.on("event", (oneArg, twoArg) => {
+ eventsFired.push("event(" + oneArg + ", " + twoArg + ")");
+ });
+
+ testEmitter.on("non-event", shouldNeverBeEmitted);
+
+ testEmitter.on("event", (oneArg, twoArg, threeArg) => {
+ eventsFired.push(
+ "event(" + oneArg + ", " + twoArg + ", " + threeArg + ")"
+ );
+ });
+ testEmitter.emit("event", 1, 2, 3);
+ assertEquals(eventsFired, ["event(1)", "event(1, 2)", "event(1, 2, 3)"]);
+ }
+});
+
+test({
+ name: "Registered event names are returned as strings or Sybols",
+ fn() {
+ const testEmitter = new EventEmitter();
+ testEmitter.on("event", shouldNeverBeEmitted);
+ testEmitter.on("event", shouldNeverBeEmitted);
+ const sym = Symbol("symbol");
+ testEmitter.on(sym, shouldNeverBeEmitted);
+ assertEquals(testEmitter.eventNames(), ["event", sym]);
+ }
+});
+
+test({
+ name: "You can set and get max listeners",
+ fn() {
+ const testEmitter = new EventEmitter();
+ assertEquals(testEmitter.getMaxListeners(), 10);
+ testEmitter.setMaxListeners(20);
+ assertEquals(testEmitter.getMaxListeners(), 20);
+ }
+});
+
+test({
+ name: "You can retrieve registered functions for an event",
+ fn() {
+ const testEmitter = new EventEmitter();
+ testEmitter.on("someOtherEvent", shouldNeverBeEmitted);
+ testEmitter.on("event", shouldNeverBeEmitted);
+ const testFunction = (): void => {};
+ testEmitter.on("event", testFunction);
+ assertEquals(testEmitter.listeners("event"), [
+ shouldNeverBeEmitted,
+ testFunction
+ ]);
+ }
+});
+
+test({
+ name: "Off is alias for removeListener",
+ fn() {
+ const testEmitter = new EventEmitter();
+ testEmitter.on("event", shouldNeverBeEmitted);
+ assertEquals(testEmitter.listenerCount("event"), 1);
+ testEmitter.off("event", shouldNeverBeEmitted);
+ assertEquals(testEmitter.listenerCount("event"), 0);
+ }
+});
+
+test({
+ name: "Event registration can be chained",
+ fn() {
+ const testEmitter = new EventEmitter();
+ testEmitter
+ .on("event", shouldNeverBeEmitted)
+ .on("event", shouldNeverBeEmitted);
+ assertEquals(testEmitter.listenerCount("event"), 2);
+ }
+});
+
+test({
+ name: "Events can be registered to only fire once",
+ fn() {
+ let eventsFired = [];
+ const testEmitter = new EventEmitter();
+ //prove multiple emits on same event first (when registered with 'on')
+ testEmitter.on("multiple event", () => {
+ eventsFired.push("multiple event");
+ });
+ testEmitter.emit("multiple event");
+ testEmitter.emit("multiple event");
+ assertEquals(eventsFired, ["multiple event", "multiple event"]);
+
+ //now prove multiple events registered via 'once' only emit once
+ eventsFired = [];
+ testEmitter.once("single event", () => {
+ eventsFired.push("single event");
+ });
+ testEmitter.emit("single event");
+ testEmitter.emit("single event");
+ assertEquals(eventsFired, ["single event"]);
+ }
+});
+
+test({
+ name:
+ "You can inject a listener into the start of the stack, rather than at the end",
+ fn() {
+ const eventsFired = [];
+ const testEmitter = new EventEmitter();
+ testEmitter.on("event", () => {
+ eventsFired.push("first");
+ });
+ testEmitter.on("event", () => {
+ eventsFired.push("second");
+ });
+ testEmitter.prependListener("event", () => {
+ eventsFired.push("third");
+ });
+ testEmitter.emit("event");
+ assertEquals(eventsFired, ["third", "first", "second"]);
+ }
+});
+
+test({
+ name: 'You can prepend a "once" listener',
+ fn() {
+ const eventsFired = [];
+ const testEmitter = new EventEmitter();
+ testEmitter.on("event", () => {
+ eventsFired.push("first");
+ });
+ testEmitter.on("event", () => {
+ eventsFired.push("second");
+ });
+ testEmitter.prependOnceListener("event", () => {
+ eventsFired.push("third");
+ });
+ testEmitter.emit("event");
+ testEmitter.emit("event");
+ assertEquals(eventsFired, ["third", "first", "second", "first", "second"]);
+ }
+});
+
+test({
+ name: "Remove all listeners, which can also be chained",
+ fn() {
+ const testEmitter = new EventEmitter();
+ testEmitter.on("event", shouldNeverBeEmitted);
+ testEmitter.on("event", shouldNeverBeEmitted);
+ testEmitter.on("other event", shouldNeverBeEmitted);
+ testEmitter.on("other event", shouldNeverBeEmitted);
+ testEmitter.once("other event", shouldNeverBeEmitted);
+ assertEquals(testEmitter.listenerCount("event"), 2);
+ assertEquals(testEmitter.listenerCount("other event"), 3);
+
+ testEmitter.removeAllListeners("event").removeAllListeners("other event");
+
+ assertEquals(testEmitter.listenerCount("event"), 0);
+ assertEquals(testEmitter.listenerCount("other event"), 0);
+ }
+});
+
+test({
+ name: "Remove individual listeners, which can also be chained",
+ fn() {
+ const testEmitter = new EventEmitter();
+ testEmitter.on("event", shouldNeverBeEmitted);
+ testEmitter.on("event", shouldNeverBeEmitted);
+ testEmitter.once("other event", shouldNeverBeEmitted);
+ assertEquals(testEmitter.listenerCount("event"), 2);
+ assertEquals(testEmitter.listenerCount("other event"), 1);
+
+ testEmitter.removeListener("other event", shouldNeverBeEmitted);
+ assertEquals(testEmitter.listenerCount("event"), 2);
+ assertEquals(testEmitter.listenerCount("other event"), 0);
+
+ testEmitter
+ .removeListener("event", shouldNeverBeEmitted)
+ .removeListener("event", shouldNeverBeEmitted);
+
+ assertEquals(testEmitter.listenerCount("event"), 0);
+ assertEquals(testEmitter.listenerCount("other event"), 0);
+ }
+});
+
+test({
+ name: "It is OK to try to remove non-existant listener",
+ fn() {
+ const testEmitter = new EventEmitter();
+
+ const madeUpEvent = (): void => {
+ fail("Should never be called");
+ };
+
+ testEmitter.on("event", shouldNeverBeEmitted);
+ assertEquals(testEmitter.listenerCount("event"), 1);
+
+ testEmitter.removeListener("event", madeUpEvent);
+ testEmitter.removeListener("non-existant event", madeUpEvent);
+
+ assertEquals(testEmitter.listenerCount("event"), 1);
+ }
+});
+
+test({
+ name: "all listeners complete execution even if removed before execution",
+ fn() {
+ const testEmitter = new EventEmitter();
+ let eventsProcessed = [];
+ const listenerB = (): number => eventsProcessed.push("B");
+ const listenerA = (): void => {
+ eventsProcessed.push("A");
+ testEmitter.removeListener("event", listenerB);
+ };
+
+ testEmitter.on("event", listenerA);
+ testEmitter.on("event", listenerB);
+
+ testEmitter.emit("event");
+ assertEquals(eventsProcessed, ["A", "B"]);
+
+ eventsProcessed = [];
+ testEmitter.emit("event");
+ assertEquals(eventsProcessed, ["A"]);
+ }
+});
+
+test({
+ name: 'Raw listener will return event listener or wrapped "once" function',
+ fn() {
+ const testEmitter = new EventEmitter();
+ const eventsProcessed = [];
+ const listenerA = (): number => eventsProcessed.push("A");
+ const listenerB = (): number => eventsProcessed.push("B");
+ testEmitter.on("event", listenerA);
+ testEmitter.once("once-event", listenerB);
+
+ const rawListenersForEvent = testEmitter.rawListeners("event");
+ const rawListenersForOnceEvent = testEmitter.rawListeners("once-event");
+
+ assertEquals(rawListenersForEvent.length, 1);
+ assertEquals(rawListenersForOnceEvent.length, 1);
+ assertEquals(rawListenersForEvent[0], listenerA);
+ assertEquals(
+ (rawListenersForOnceEvent[0] as WrappedFunction).listener,
+ listenerB
+ );
+ }
+});
+
+test({
+ name:
+ "Once wrapped raw listeners may be executed multiple times, until the wrapper is executed",
+ fn() {
+ const testEmitter = new EventEmitter();
+ let eventsProcessed = [];
+ const listenerA = (): number => eventsProcessed.push("A");
+ testEmitter.once("once-event", listenerA);
+
+ const rawListenersForOnceEvent = testEmitter.rawListeners("once-event");
+ const wrappedFn: WrappedFunction = rawListenersForOnceEvent[0] as WrappedFunction;
+ wrappedFn.listener();
+ wrappedFn.listener();
+ wrappedFn.listener();
+ assertEquals(eventsProcessed, ["A", "A", "A"]);
+
+ eventsProcessed = [];
+ wrappedFn(); // executing the wrapped listener function will remove it from the event
+ assertEquals(eventsProcessed, ["A"]);
+ assertEquals(testEmitter.listeners("once-event").length, 0);
+ }
+});
+
+test({
+ name: "Can add once event listener to EventEmitter via standalone function",
+ async fn() {
+ const ee: EventEmitter = new EventEmitter();
+ setTimeout(() => {
+ ee.emit("event", 42, "foo");
+ }, 0);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const valueArr: any[] = await once(ee, "event");
+ assertEquals(valueArr, [42, "foo"]);
+ }
+});
+
+test({
+ name: "Can add once event listener to EventTarget via standalone function",
+ async fn() {
+ const et: EventTarget = new EventTarget();
+ setTimeout(() => {
+ const event: Event = new Event("event", { composed: true });
+ et.dispatchEvent(event);
+ }, 0);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const eventObj: any[] = await once(et, "event");
+ assert(!eventObj[0].isTrusted);
+ }
+});
+
+test({
+ name: "Only valid integers are allowed for max listeners",
+ fn() {
+ const ee: EventEmitter = new EventEmitter();
+ ee.setMaxListeners(0);
+ assertThrows(
+ () => {
+ ee.setMaxListeners(-1);
+ },
+ Error,
+ "must be >= 0"
+ );
+ assertThrows(
+ () => {
+ ee.setMaxListeners(3.45);
+ },
+ Error,
+ "must be 'an integer'"
+ );
+ }
+});
diff --git a/std/node/module.ts b/std/node/module.ts
index f27ef25f2..ac436c555 100644
--- a/std/node/module.ts
+++ b/std/node/module.ts
@@ -26,6 +26,7 @@ import * as nodeUtil from "./util.ts";
import * as nodePath from "./path.ts";
import * as nodeTimers from "./timers.ts";
import * as nodeOs from "./os.ts";
+import * as nodeEvents from "./events.ts";
import * as path from "../path/mod.ts";
import { assert } from "../testing/asserts.ts";
@@ -579,11 +580,14 @@ function createNativeModule(id: string, exports: any): Module {
mod.loaded = true;
return mod;
}
+
nativeModulePolyfill.set("fs", createNativeModule("fs", nodeFS));
-nativeModulePolyfill.set("util", createNativeModule("util", nodeUtil));
+nativeModulePolyfill.set("events", createNativeModule("events", nodeEvents));
+nativeModulePolyfill.set("os", createNativeModule("os", nodeOs));
nativeModulePolyfill.set("path", createNativeModule("path", nodePath));
nativeModulePolyfill.set("timers", createNativeModule("timers", nodeTimers));
-nativeModulePolyfill.set("os", createNativeModule("os", nodeOs));
+nativeModulePolyfill.set("util", createNativeModule("util", nodeUtil));
+
function loadNativeModule(
_filename: string,
request: string
diff --git a/std/node/os.ts b/std/node/os.ts
index c5e0f6c76..e4a00c450 100644
--- a/std/node/os.ts
+++ b/std/node/os.ts
@@ -19,6 +19,7 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
import { notImplemented } from "./_utils.ts";
+import { validateIntegerRange } from "./util.ts";
import { EOL as fsEOL } from "../fs/eol.ts";
const SEE_GITHUB_ISSUE = "See https://github.com/denoland/deno/issues/3802";
@@ -117,7 +118,7 @@ export function freemem(): number {
/** Not yet implemented */
export function getPriority(pid = 0): number {
- validateInt32(pid, "pid");
+ validateIntegerRange(pid, "pid");
notImplemented(SEE_GITHUB_ISSUE);
}
@@ -162,8 +163,8 @@ export function setPriority(pid: number, priority?: number): void {
priority = pid;
pid = 0;
}
- validateInt32(pid, "pid");
- validateInt32(priority, "priority", -20, 19);
+ validateIntegerRange(pid, "pid");
+ validateIntegerRange(priority, "priority", -20, 19);
notImplemented(SEE_GITHUB_ISSUE);
}
@@ -211,20 +212,3 @@ export const constants = {
};
export const EOL = Deno.build.os == "win" ? fsEOL.CRLF : fsEOL.LF;
-
-const validateInt32 = (
- value: number,
- name: string,
- min = -2147483648,
- max = 2147483647
-): void => {
- // The defaults for min and max correspond to the limits of 32-bit integers.
- if (!Number.isInteger(value)) {
- throw new Error(`${name} must be 'an integer' but was ${value}`);
- }
- if (value < min || value > max) {
- throw new Error(
- `${name} must be >= ${min} && <= ${max}. Value was ${value}`
- );
- }
-};
diff --git a/std/node/util.ts b/std/node/util.ts
index d0187e616..9879da513 100644
--- a/std/node/util.ts
+++ b/std/node/util.ts
@@ -45,3 +45,20 @@ export function isFunction(value: unknown): boolean {
export function isRegExp(value: unknown): boolean {
return value instanceof RegExp;
}
+
+export function validateIntegerRange(
+ value: number,
+ name: string,
+ min = -2147483648,
+ max = 2147483647
+): void {
+ // The defaults for min and max correspond to the limits of 32-bit integers.
+ if (!Number.isInteger(value)) {
+ throw new Error(`${name} must be 'an integer' but was ${value}`);
+ }
+ if (value < min || value > max) {
+ throw new Error(
+ `${name} must be >= ${min} && <= ${max}. Value was ${value}`
+ );
+ }
+}