aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--src/bun.js/bindings/headers-cpp.h2
-rw-r--r--src/bun.js/bindings/headers.h2
-rw-r--r--src/bun.js/bindings/webcore/JSEventEmitter.cpp53
-rw-r--r--src/bun.js/bindings/webcore/JSEventEmitter.h2
-rw-r--r--src/bun.js/builtins/cpp/NodeEventsBuiltins.cpp111
-rw-r--r--src/bun.js/builtins/cpp/NodeEventsBuiltins.h9
-rw-r--r--src/bun.js/builtins/js/NodeEvents.js100
-rw-r--r--src/bun.js/modules/EventsModule.h11
-rw-r--r--test/js/node/events/node-events.node.test.ts298
9 files changed, 501 insertions, 87 deletions
diff --git a/src/bun.js/bindings/headers-cpp.h b/src/bun.js/bindings/headers-cpp.h
index 5eac6785d..fcf694b8e 100644
--- a/src/bun.js/bindings/headers-cpp.h
+++ b/src/bun.js/bindings/headers-cpp.h
@@ -1,4 +1,4 @@
-//-- AUTOGENERATED FILE -- 1680635796
+//-- AUTOGENERATED FILE -- 1680843050
// clang-format off
#pragma once
diff --git a/src/bun.js/bindings/headers.h b/src/bun.js/bindings/headers.h
index 5b0e9527c..b842848da 100644
--- a/src/bun.js/bindings/headers.h
+++ b/src/bun.js/bindings/headers.h
@@ -1,5 +1,5 @@
// clang-format off
-//-- AUTOGENERATED FILE -- 1680635796
+//-- AUTOGENERATED FILE -- 1680843050
#pragma once
#include <stddef.h>
diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.cpp b/src/bun.js/bindings/webcore/JSEventEmitter.cpp
index aa9fb4832..2a573b3b5 100644
--- a/src/bun.js/bindings/webcore/JSEventEmitter.cpp
+++ b/src/bun.js/bindings/webcore/JSEventEmitter.cpp
@@ -573,57 +573,4 @@ JSC_DEFINE_HOST_FUNCTION(Events_functionListenerCount,
RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(JSC::jsNumber(impl.listenerCount(eventType))));
}
-JSC_DEFINE_HOST_FUNCTION(Events_functionOnce,
- (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
-{
- auto& vm = JSC::getVM(lexicalGlobalObject);
- auto throwScope = DECLARE_THROW_SCOPE(vm);
- UNUSED_PARAM(throwScope);
- UNUSED_PARAM(callFrame);
-
- if (UNLIKELY(callFrame->argumentCount() < 3))
- return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject));
- auto argument0 = jsEventEmitterCastFast(vm, lexicalGlobalObject, callFrame->uncheckedArgument(0));
- if (UNLIKELY(!argument0)) {
- throwException(lexicalGlobalObject, throwScope, createError(lexicalGlobalObject, "Expected EventEmitter"_s));
- return JSValue::encode(JSC::jsUndefined());
- }
- auto& impl = argument0->wrapped();
- auto eventType = callFrame->uncheckedArgument(1).toPropertyKey(lexicalGlobalObject);
- RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
- EnsureStillAliveScope argument2 = callFrame->uncheckedArgument(2);
- auto listener = convert<IDLNullable<IDLEventListener<JSEventListener>>>(*lexicalGlobalObject, argument2.value(), *argument0, [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentMustBeObjectError(lexicalGlobalObject, scope, 2, "listener", "EventEmitter", "removeListener"); });
- RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
- RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
- vm.writeBarrier(argument0, argument2.value());
- RELEASE_AND_RETURN(throwScope, JSC::JSValue::encode(argument0));
-}
-
-// JSC_DEFINE_HOST_FUNCTION(Events_functionOn,
-// (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
-// {
-// auto& vm = JSC::getVM(lexicalGlobalObject);
-// auto throwScope = DECLARE_THROW_SCOPE(vm);
-// UNUSED_PARAM(throwScope);
-// UNUSED_PARAM(callFrame);
-
-// if (UNLIKELY(callFrame->argumentCount() < 3))
-// return throwVMError(lexicalGlobalObject, throwScope, createNotEnoughArgumentsError(lexicalGlobalObject));
-// auto argument0 = jsEventEmitterCastFast(vm, lexicalGlobalObject, callFrame->uncheckedArgument(0));
-// if (UNLIKELY(!argument0)) {
-// throwException(lexicalGlobalObject, throwScope, createError(lexicalGlobalObject, "Expected EventEmitter"_s));
-// return JSValue::encode(JSC::jsUndefined());
-// }
-// auto& impl = argument0->wrapped();
-// auto eventType = callFrame->uncheckedArgument(1).toPropertyKey(lexicalGlobalObject);
-// RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
-// EnsureStillAliveScope argument2 = callFrame->uncheckedArgument(2);
-// auto listener = convert<IDLNullable<IDLEventListener<JSEventListener>>>(*lexicalGlobalObject, argument2.value(), *argument0, [](JSC::JSGlobalObject& lexicalGlobalObject, JSC::ThrowScope& scope) { throwArgumentMustBeObjectError(lexicalGlobalObject, scope, 2, "listener", "EventEmitter", "removeListener"); });
-// RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
-// auto result = JSValue::encode(toJS<IDLUndefined>(*lexicalGlobalObject, throwScope, [&]() -> decltype(auto) { return impl.addListenerForBindings(WTFMove(eventType), WTFMove(listener), false, false); }));
-// RETURN_IF_EXCEPTION(throwScope, encodedJSValue());
-// vm.writeBarrier(argument0, argument2.value());
-// return result;
-// }
-
}
diff --git a/src/bun.js/bindings/webcore/JSEventEmitter.h b/src/bun.js/bindings/webcore/JSEventEmitter.h
index 855241011..d09854afd 100644
--- a/src/bun.js/bindings/webcore/JSEventEmitter.h
+++ b/src/bun.js/bindings/webcore/JSEventEmitter.h
@@ -9,8 +9,6 @@ namespace WebCore {
JSC_DECLARE_HOST_FUNCTION(Events_functionGetEventListeners);
JSC_DECLARE_HOST_FUNCTION(Events_functionListenerCount);
-JSC_DECLARE_HOST_FUNCTION(Events_functionOnce);
-JSC_DECLARE_HOST_FUNCTION(Events_functionOn);
class JSEventEmitter : public JSDOMWrapper<EventEmitter> {
public:
diff --git a/src/bun.js/builtins/cpp/NodeEventsBuiltins.cpp b/src/bun.js/builtins/cpp/NodeEventsBuiltins.cpp
index dca0ee431..852dfd6ee 100644
--- a/src/bun.js/builtins/cpp/NodeEventsBuiltins.cpp
+++ b/src/bun.js/builtins/cpp/NodeEventsBuiltins.cpp
@@ -51,7 +51,7 @@ namespace WebCore {
const JSC::ConstructAbility s_nodeEventsOnAsyncIteratorCodeConstructAbility = JSC::ConstructAbility::CannotConstruct;
const JSC::ConstructorKind s_nodeEventsOnAsyncIteratorCodeConstructorKind = JSC::ConstructorKind::None;
const JSC::ImplementationVisibility s_nodeEventsOnAsyncIteratorCodeImplementationVisibility = JSC::ImplementationVisibility::Public;
-const int s_nodeEventsOnAsyncIteratorCodeLength = 4455;
+const int s_nodeEventsOnAsyncIteratorCodeLength = 4565;
static const JSC::Intrinsic s_nodeEventsOnAsyncIteratorCodeIntrinsic = JSC::NoIntrinsic;
const char* const s_nodeEventsOnAsyncIteratorCode =
"(function (emitter, event, options) {\n" \
@@ -59,18 +59,21 @@ const char* const s_nodeEventsOnAsyncIteratorCode =
"\n" \
" var { AbortSignal, Symbol, Number, Error } = globalThis;\n" \
"\n" \
- " var AbortError = class AbortError extends Error {\n" \
- " constructor(message = \"The operation was aborted\", options = void 0) {\n" \
- " if (options !== void 0 && typeof options !== \"object\") {\n" \
- " throw new Error(`Invalid AbortError options:\\n" \
+ " function makeAbortError(msg, opts = void 0) {\n" \
+ " var AbortError = class AbortError extends Error {\n" \
+ " constructor(message = \"The operation was aborted\", options = void 0) {\n" \
+ " if (options !== void 0 && typeof options !== \"object\") {\n" \
+ " throw new Error(`Invalid AbortError options:\\n" \
"\\n" \
"${JSON.stringify(options, null, 2)}`);\n" \
+ " }\n" \
+ " super(message, options);\n" \
+ " this.code = \"ABORT_ERR\";\n" \
+ " this.name = \"AbortError\";\n" \
" }\n" \
- " super(message, options);\n" \
- " this.code = \"ABORT_ERR\";\n" \
- " this.name = \"AbortError\";\n" \
- " }\n" \
- " };\n" \
+ " };\n" \
+ " return new AbortError(msg, opts);\n" \
+ " }\n" \
"\n" \
" if (@isUndefinedOrNull(emitter)) @throwTypeError(\"emitter is required\");\n" \
" //\n" \
@@ -86,7 +89,7 @@ const char* const s_nodeEventsOnAsyncIteratorCode =
"\n" \
" if (signal?.aborted) {\n" \
" //\n" \
- " throw new AbortError(@undefined, { cause: signal?.reason });\n" \
+ " throw makeAbortError(@undefined, { cause: signal?.reason });\n" \
" }\n" \
"\n" \
" var highWatermark = options.highWatermark ?? Number.MAX_SAFE_INTEGER;\n" \
@@ -107,7 +110,7 @@ const char* const s_nodeEventsOnAsyncIteratorCode =
" var listeners = [];\n" \
"\n" \
" function abortListener() {\n" \
- " errorHandler(new AbortError(@undefined, { cause: signal?.reason }));\n" \
+ " errorHandler(makeAbortError(@undefined, { cause: signal?.reason }));\n" \
" }\n" \
"\n" \
" function eventHandler(value) {\n" \
@@ -222,6 +225,90 @@ const char* const s_nodeEventsOnAsyncIteratorCode =
"})\n" \
;
+const JSC::ConstructAbility s_nodeEventsOncePromiseCodeConstructAbility = JSC::ConstructAbility::CannotConstruct;
+const JSC::ConstructorKind s_nodeEventsOncePromiseCodeConstructorKind = JSC::ConstructorKind::None;
+const JSC::ImplementationVisibility s_nodeEventsOncePromiseCodeImplementationVisibility = JSC::ImplementationVisibility::Public;
+const int s_nodeEventsOncePromiseCodeLength = 2418;
+static const JSC::Intrinsic s_nodeEventsOncePromiseCodeIntrinsic = JSC::NoIntrinsic;
+const char* const s_nodeEventsOncePromiseCode =
+ "(function (emitter, name, options) {\n" \
+ " \"use strict\";\n" \
+ "\n" \
+ " var { AbortSignal, Error } = globalThis;\n" \
+ "\n" \
+ " function makeAbortError(msg, opts = void 0) {\n" \
+ " var AbortError = class AbortError extends Error {\n" \
+ " constructor(message = \"The operation was aborted\", options = void 0) {\n" \
+ " if (options !== void 0 && typeof options !== \"object\") {\n" \
+ " throw new Error(`Invalid AbortError options:\\n" \
+ "\\n" \
+ "${JSON.stringify(options, null, 2)}`);\n" \
+ " }\n" \
+ " super(message, options);\n" \
+ " this.code = \"ABORT_ERR\";\n" \
+ " this.name = \"AbortError\";\n" \
+ " }\n" \
+ " };\n" \
+ " return new AbortError(msg, opts);\n" \
+ " }\n" \
+ "\n" \
+ " if (@isUndefinedOrNull(emitter)) return @Promise.@reject(@makeTypeError(\"emitter is required\"));\n" \
+ " //\n" \
+ " if (!(@isObject(emitter) && @isCallable(emitter.emit) && @isCallable(emitter.on)))\n" \
+ " return @Promise.@reject(@makeTypeError(\"emitter must be an EventEmitter\"));\n" \
+ "\n" \
+ " if (@isUndefinedOrNull(options)) options = {};\n" \
+ "\n" \
+ " //\n" \
+ " var signal = options.signal;\n" \
+ " if (signal !== @undefined && (!@isObject(signal) || !(signal instanceof AbortSignal)))\n" \
+ " return @Promise.@reject(@makeTypeError(\"options.signal must be an AbortSignal\"));\n" \
+ "\n" \
+ " if (signal?.aborted) {\n" \
+ " //\n" \
+ " return @Promise.@reject(makeAbortError(@undefined, { cause: signal?.reason }));\n" \
+ " }\n" \
+ "\n" \
+ " var eventPromiseCapability = @newPromiseCapability(@Promise);\n" \
+ "\n" \
+ " var errorListener = (err) => {\n" \
+ " emitter.removeListener(name, resolver);\n" \
+ " if (!@isUndefinedOrNull(signal)) {\n" \
+ " signal.removeEventListener(\"abort\", abortListener);\n" \
+ " }\n" \
+ " eventPromiseCapability.@reject.@call(@undefined, err);\n" \
+ " };\n" \
+ "\n" \
+ " var resolver = (...args) => {\n" \
+ " if (@isCallable(emitter.removeListener)) {\n" \
+ " emitter.removeListener(\"error\", errorListener);\n" \
+ " }\n" \
+ " if (!@isUndefinedOrNull(signal)) {\n" \
+ " signal.removeEventListener(\"abort\", abortListener);\n" \
+ " }\n" \
+ " eventPromiseCapability.@resolve.@call(@undefined, args);\n" \
+ " };\n" \
+ " \n" \
+ " emitter.once(name, resolver);\n" \
+ " if (name !== \"error\" && @isCallable(emitter.once)) {\n" \
+ " //\n" \
+ " //\n" \
+ " emitter.once(\"error\", errorListener);\n" \
+ " }\n" \
+ "\n" \
+ " function abortListener() {\n" \
+ " emitter.removeListener(name, resolver);\n" \
+ " emitter.removeListener(\"error\", errorListener);\n" \
+ " eventPromiseCapability.@reject.@call(@undefined, makeAbortError(@undefined, { cause: signal?.reason }));\n" \
+ " }\n" \
+ "\n" \
+ " if (!@isUndefinedOrNull(signal))\n" \
+ " signal.addEventListener(\"abort\", abortListener, { once: true });\n" \
+ "\n" \
+ " return eventPromiseCapability.@promise;\n" \
+ "})\n" \
+;
+
#define DEFINE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \
JSC::FunctionExecutable* codeName##Generator(JSC::VM& vm) \
diff --git a/src/bun.js/builtins/cpp/NodeEventsBuiltins.h b/src/bun.js/builtins/cpp/NodeEventsBuiltins.h
index 11b70ea92..6a3f15403 100644
--- a/src/bun.js/builtins/cpp/NodeEventsBuiltins.h
+++ b/src/bun.js/builtins/cpp/NodeEventsBuiltins.h
@@ -52,17 +52,26 @@ extern const int s_nodeEventsOnAsyncIteratorCodeLength;
extern const JSC::ConstructAbility s_nodeEventsOnAsyncIteratorCodeConstructAbility;
extern const JSC::ConstructorKind s_nodeEventsOnAsyncIteratorCodeConstructorKind;
extern const JSC::ImplementationVisibility s_nodeEventsOnAsyncIteratorCodeImplementationVisibility;
+extern const char* const s_nodeEventsOncePromiseCode;
+extern const int s_nodeEventsOncePromiseCodeLength;
+extern const JSC::ConstructAbility s_nodeEventsOncePromiseCodeConstructAbility;
+extern const JSC::ConstructorKind s_nodeEventsOncePromiseCodeConstructorKind;
+extern const JSC::ImplementationVisibility s_nodeEventsOncePromiseCodeImplementationVisibility;
#define WEBCORE_FOREACH_NODEEVENTS_BUILTIN_DATA(macro) \
macro(onAsyncIterator, nodeEventsOnAsyncIterator, 3) \
+ macro(oncePromise, nodeEventsOncePromise, 3) \
#define WEBCORE_BUILTIN_NODEEVENTS_ONASYNCITERATOR 1
+#define WEBCORE_BUILTIN_NODEEVENTS_ONCEPROMISE 1
#define WEBCORE_FOREACH_NODEEVENTS_BUILTIN_CODE(macro) \
macro(nodeEventsOnAsyncIteratorCode, onAsyncIterator, ASCIILiteral(), s_nodeEventsOnAsyncIteratorCodeLength) \
+ macro(nodeEventsOncePromiseCode, oncePromise, ASCIILiteral(), s_nodeEventsOncePromiseCodeLength) \
#define WEBCORE_FOREACH_NODEEVENTS_BUILTIN_FUNCTION_NAME(macro) \
macro(onAsyncIterator) \
+ macro(oncePromise) \
#define DECLARE_BUILTIN_GENERATOR(codeName, functionName, overriddenName, argumentCount) \
JSC::FunctionExecutable* codeName##Generator(JSC::VM&);
diff --git a/src/bun.js/builtins/js/NodeEvents.js b/src/bun.js/builtins/js/NodeEvents.js
index ded8d1af7..14af767bf 100644
--- a/src/bun.js/builtins/js/NodeEvents.js
+++ b/src/bun.js/builtins/js/NodeEvents.js
@@ -28,16 +28,19 @@ function onAsyncIterator(emitter, event, options) {
var { AbortSignal, Symbol, Number, Error } = globalThis;
- var AbortError = class AbortError extends Error {
- constructor(message = "The operation was aborted", options = void 0) {
- if (options !== void 0 && typeof options !== "object") {
- throw new Error(`Invalid AbortError options:\n\n${JSON.stringify(options, null, 2)}`);
+ function makeAbortError(msg, opts = void 0) {
+ var AbortError = class AbortError extends Error {
+ constructor(message = "The operation was aborted", options = void 0) {
+ if (options !== void 0 && typeof options !== "object") {
+ throw new Error(`Invalid AbortError options:\n\n${JSON.stringify(options, null, 2)}`);
+ }
+ super(message, options);
+ this.code = "ABORT_ERR";
+ this.name = "AbortError";
}
- super(message, options);
- this.code = "ABORT_ERR";
- this.name = "AbortError";
- }
- };
+ };
+ return new AbortError(msg, opts);
+ }
if (@isUndefinedOrNull(emitter)) @throwTypeError("emitter is required");
// TODO: Do a more accurate check
@@ -53,7 +56,7 @@ function onAsyncIterator(emitter, event, options) {
if (signal?.aborted) {
// TODO: Make this a builtin
- throw new AbortError(@undefined, { cause: signal?.reason });
+ throw makeAbortError(@undefined, { cause: signal?.reason });
}
var highWatermark = options.highWatermark ?? Number.MAX_SAFE_INTEGER;
@@ -74,7 +77,7 @@ function onAsyncIterator(emitter, event, options) {
var listeners = [];
function abortListener() {
- errorHandler(new AbortError(@undefined, { cause: signal?.reason }));
+ errorHandler(makeAbortError(@undefined, { cause: signal?.reason }));
}
function eventHandler(value) {
@@ -187,3 +190,78 @@ function onAsyncIterator(emitter, event, options) {
});
return iterator;
}
+
+function oncePromise(emitter, name, options) {
+ "use strict";
+
+ var { AbortSignal, Error } = globalThis;
+
+ function makeAbortError(msg, opts = void 0) {
+ var AbortError = class AbortError extends Error {
+ constructor(message = "The operation was aborted", options = void 0) {
+ if (options !== void 0 && typeof options !== "object") {
+ throw new Error(`Invalid AbortError options:\n\n${JSON.stringify(options, null, 2)}`);
+ }
+ super(message, options);
+ this.code = "ABORT_ERR";
+ this.name = "AbortError";
+ }
+ };
+ return new AbortError(msg, opts);
+ }
+
+ if (@isUndefinedOrNull(emitter)) return @Promise.@reject(@makeTypeError("emitter is required"));
+ // TODO: Do a more accurate check
+ if (!(@isObject(emitter) && @isCallable(emitter.emit) && @isCallable(emitter.on)))
+ return @Promise.@reject(@makeTypeError("emitter must be an EventEmitter"));
+
+ if (@isUndefinedOrNull(options)) options = {};
+
+ // Parameters validation
+ var signal = options.signal;
+ if (signal !== @undefined && (!@isObject(signal) || !(signal instanceof AbortSignal)))
+ return @Promise.@reject(@makeTypeError("options.signal must be an AbortSignal"));
+
+ if (signal?.aborted) {
+ // TODO: Make this a builtin
+ return @Promise.@reject(makeAbortError(@undefined, { cause: signal?.reason }));
+ }
+
+ var eventPromiseCapability = @newPromiseCapability(@Promise);
+
+ var errorListener = (err) => {
+ emitter.removeListener(name, resolver);
+ if (!@isUndefinedOrNull(signal)) {
+ signal.removeEventListener("abort", abortListener);
+ }
+ eventPromiseCapability.@reject.@call(@undefined, err);
+ };
+
+ var resolver = (...args) => {
+ if (@isCallable(emitter.removeListener)) {
+ emitter.removeListener("error", errorListener);
+ }
+ if (!@isUndefinedOrNull(signal)) {
+ signal.removeEventListener("abort", abortListener);
+ }
+ eventPromiseCapability.@resolve.@call(@undefined, args);
+ };
+
+ emitter.once(name, resolver);
+ if (name !== "error" && @isCallable(emitter.once)) {
+ // EventTarget does not have `error` event semantics like Node
+ // EventEmitters, we listen to `error` events only on EventEmitters.
+ emitter.once("error", errorListener);
+ }
+
+ function abortListener() {
+ emitter.removeListener(name, resolver);
+ emitter.removeListener("error", errorListener);
+ eventPromiseCapability.@reject.@call(@undefined, makeAbortError(@undefined, { cause: signal?.reason }));
+ }
+
+ if (!@isUndefinedOrNull(signal))
+ signal.addEventListener("abort", abortListener, { once: true });
+
+ return eventPromiseCapability.@promise;
+}
diff --git a/src/bun.js/modules/EventsModule.h b/src/bun.js/modules/EventsModule.h
index e60624ac9..15981fcca 100644
--- a/src/bun.js/modules/EventsModule.h
+++ b/src/bun.js/modules/EventsModule.h
@@ -24,10 +24,6 @@ inline void generateEventsSourceCode(JSC::JSGlobalObject *lexicalGlobalObject,
exportValues.append(JSC::JSFunction::create(
vm, lexicalGlobalObject, 0, MAKE_STATIC_STRING_IMPL("listenerCount"),
Events_functionListenerCount, ImplementationVisibility::Public));
- exportNames.append(JSC::Identifier::fromString(vm, "once"_s));
- exportValues.append(JSC::JSFunction::create(
- vm, lexicalGlobalObject, 0, MAKE_STATIC_STRING_IMPL("once"),
- Events_functionOnce, ImplementationVisibility::Public));
exportNames.append(
JSC::Identifier::fromString(vm, "captureRejectionSymbol"_s));
exportValues.append(Symbol::create(
@@ -48,6 +44,13 @@ inline void generateEventsSourceCode(JSC::JSGlobalObject *lexicalGlobalObject,
PropertyAttribute::Builtin | PropertyAttribute::DontDelete);
exportValues.append(onAsyncIterFnPtr);
+ exportNames.append(JSC::Identifier::fromString(vm, "once"_s));
+ auto *oncePromiseFnPtr = eventEmitterModuleCJS->putDirectBuiltinFunction(
+ vm, globalObject, JSC::Identifier::fromString(vm, "once"_s),
+ nodeEventsOncePromiseCodeGenerator(vm),
+ PropertyAttribute::Builtin | PropertyAttribute::DontDelete);
+ exportValues.append(oncePromiseFnPtr);
+
eventEmitterModuleCJS->putDirect(
vm,
PropertyName(
diff --git a/test/js/node/events/node-events.node.test.ts b/test/js/node/events/node-events.node.test.ts
index c086aa0b7..22b7d23f9 100644
--- a/test/js/node/events/node-events.node.test.ts
+++ b/test/js/node/events/node-events.node.test.ts
@@ -1,10 +1,12 @@
-import { EventEmitter, on } from "node:events";
+import { EventEmitter, getEventListeners, on, once } from "node:events";
import { createTest } from "node-harness";
-const { beforeAll, expect, assert, describe, it, createCallCheckCtx, createDoneDotAll } = createTest(import.meta.path);
+const { beforeAll, expect, assert, strictEqual, describe, it, createCallCheckCtx, createDoneDotAll } = createTest(
+ import.meta.path,
+);
// const NodeEventTarget = globalThis.EventTarget;
-describe("node:events.on (EventEmitter AsyncIterator)", () => {
+describe("node:events.on() (EventEmitter AsyncIterator)", () => {
it("should return an async iterator", async () => {
const ee = new EventEmitter();
const iterable = on(ee, "foo");
@@ -425,3 +427,293 @@ describe("node:events.on (EventEmitter AsyncIterator)", () => {
// });
// }
});
+
+describe("node:events.once()", () => {
+ it("should resolve with the first event", async () => {
+ const ee = new EventEmitter();
+
+ setImmediate(() => {
+ ee.emit("myevent", 42);
+ });
+
+ const [value] = await once(ee, "myevent");
+ assert.strictEqual(value, 42);
+ assert.strictEqual(ee.listenerCount("error"), 0);
+ assert.strictEqual(ee.listenerCount("myevent"), 0);
+ });
+
+ it("should allow passing `null` for `options` arg", async () => {
+ const ee = new EventEmitter();
+
+ setImmediate(() => {
+ ee.emit("myevent", 42);
+ });
+
+ // @ts-ignore
+ const [value] = await once(ee, "myevent", null);
+ assert.strictEqual(value, 42);
+ });
+
+ it("should return two args when two args are emitted", async () => {
+ const ee = new EventEmitter();
+
+ setImmediate(() => {
+ ee.emit("myevent", 42, 24);
+ });
+
+ const value = await once(ee, "myevent");
+ assert.deepStrictEqual(value, [42, 24]);
+ });
+
+ it("should throw an error when an error is emitted", async () => {
+ const ee = new EventEmitter();
+
+ const expected = new Error("kaboom");
+ setImmediate(() => {
+ ee.emit("error", expected);
+ });
+
+ let err;
+ try {
+ await once(ee, "myevent");
+ } catch (_e) {
+ err = _e;
+ }
+
+ assert.strictEqual(err, expected);
+ assert.strictEqual(ee.listenerCount("error"), 0);
+ assert.strictEqual(ee.listenerCount("myevent"), 0);
+ });
+
+ it("should throw an error when an error is emitted when `AbortSignal` is attached", async () => {
+ const ee = new EventEmitter();
+ const ac = new AbortController();
+ const signal = ac.signal;
+
+ const expected = new Error("boom");
+ let err;
+ setImmediate(() => {
+ ee.emit("error", expected);
+ });
+
+ const promise = once(ee, "myevent", { signal });
+ strictEqual(ee.listenerCount("error"), 1);
+
+ // TODO: Uncomment when getEventListeners is working properly
+ // strictEqual(getEventListeners(signal, "abort").length, 1);
+
+ try {
+ await promise;
+ } catch (e) {
+ err = e;
+ }
+
+ strictEqual(err, expected);
+ strictEqual(ee.listenerCount("error"), 0);
+ strictEqual(ee.listenerCount("myevent"), 0);
+ // strictEqual(getEventListeners(signal, "abort").length, 0);
+ });
+
+ it("should stop listening if we throw an error", async () => {
+ const ee = new EventEmitter();
+
+ const expected = new Error("kaboom");
+ let err;
+
+ setImmediate(() => {
+ ee.emit("error", expected);
+ ee.emit("myevent", 42, 24);
+ });
+
+ try {
+ await once(ee, "myevent");
+ } catch (_e) {
+ err = _e;
+ }
+
+ strictEqual(err, expected);
+ strictEqual(ee.listenerCount("error"), 0);
+ strictEqual(ee.listenerCount("myevent"), 0);
+ });
+
+ it("should return error instead of throwing if event is error", async () => {
+ const ee = new EventEmitter();
+
+ const expected = new Error("kaboom");
+ setImmediate(() => {
+ ee.emit("error", expected);
+ });
+
+ const promise = once(ee, "error");
+ strictEqual(ee.listenerCount("error"), 1);
+ const [err] = await promise;
+ strictEqual(err, expected);
+ strictEqual(ee.listenerCount("error"), 0);
+ strictEqual(ee.listenerCount("myevent"), 0);
+ });
+
+ it("should throw on invalid signal option", async done => {
+ const ee = new EventEmitter();
+ ee.on("error", err => {
+ done(new Error("should not be called", { cause: err }));
+ });
+ let iters = 0;
+ for (const signal of [1, {}, "hi", null, false]) {
+ let threw = false;
+ try {
+ await once(ee, "foo", { signal });
+ } catch (e) {
+ threw = true;
+ expect(e).toBeInstanceOf(TypeError);
+ }
+ expect(threw).toBe(true);
+ iters++;
+ }
+ expect(iters).toBe(5);
+ done();
+ });
+
+ it("should throw `AbortError` when signal is already aborted", async done => {
+ const ee = new EventEmitter();
+ ee.on("error", err => done(new Error("should not be called", { cause: err })));
+ const abortedSignal = AbortSignal.abort();
+
+ expect(() => on(ee, "foo", { signal: abortedSignal })).toThrow(/aborted/);
+
+ // let threw = false;
+ // try {
+ // await once(ee, "foo", { signal: abortedSignal });
+ // } catch (e) {
+ // threw = true;
+ // expect(e).toBeInstanceOf(Error);
+ // expect((e as Error).name).toBe("AbortError");
+ // }
+
+ // expect(threw).toBe(true);
+ done();
+ });
+
+ it("should throw `AbortError` when signal is aborted before event is emitted", async done => {
+ const ee = new EventEmitter();
+ ee.on("error", err => done(new Error("should not be called", { cause: err })));
+ const ac = new AbortController();
+ const signal = ac.signal;
+
+ const promise = once(ee, "foo", { signal });
+ ac.abort();
+
+ let threw = false;
+ try {
+ await promise;
+ } catch (e) {
+ threw = true;
+ expect(e).toBeInstanceOf(Error);
+ expect((e as Error).name).toBe("AbortError");
+ }
+
+ expect(threw).toBe(true);
+ done();
+ });
+
+ it("should not throw `AbortError` when signal is aborted after event is emitted", async () => {
+ const ee = new EventEmitter();
+ const ac = new AbortController();
+ const signal = ac.signal;
+
+ setImmediate(() => {
+ ee.emit("foo");
+ ac.abort();
+ });
+
+ const promise = once(ee, "foo", { signal });
+ // TODO: Uncomment when getEventListeners is working properly
+ // strictEqual(getEventListeners(signal, "abort").length, 1);
+
+ await promise;
+ expect(true).toBeTruthy();
+ // strictEqual(getEventListeners(signal, "abort").length, 0);
+ });
+
+ it("should remove listeners when signal is aborted", async () => {
+ const ee = new EventEmitter();
+ const ac = new AbortController();
+
+ const promise = once(ee, "foo", { signal: ac.signal });
+ strictEqual(ee.listenerCount("foo"), 1);
+ strictEqual(ee.listenerCount("error"), 1);
+
+ setImmediate(() => {
+ ac.abort();
+ });
+
+ try {
+ await promise;
+ } catch (e) {
+ expect(e).toBeInstanceOf(Error);
+ expect((e as Error).name).toBe("AbortError");
+
+ strictEqual(ee.listenerCount("foo"), 0);
+ strictEqual(ee.listenerCount("error"), 0);
+ }
+ });
+
+ // TODO: Uncomment event target tests once we have EventTarget support for once()
+
+ // async function onceWithEventTarget() {
+ // const et = new EventTarget();
+ // const event = new Event("myevent");
+ // process.nextTick(() => {
+ // et.dispatchEvent(event);
+ // });
+ // const [value] = await once(et, "myevent");
+ // strictEqual(value, event);
+ // }
+
+ // async function onceWithEventTargetError() {
+ // const et = new EventTarget();
+ // const error = new Event("error");
+ // process.nextTick(() => {
+ // et.dispatchEvent(error);
+ // });
+
+ // const [err] = await once(et, "error");
+ // strictEqual(err, error);
+ // }
+
+ // async function eventTargetAbortSignalBefore() {
+ // const et = new EventTarget();
+ // const abortedSignal = AbortSignal.abort();
+
+ // await Promise.all(
+ // [1, {}, "hi", null, false].map(signal => {
+ // return rejects(once(et, "foo", { signal }), {
+ // code: "ERR_INVALID_ARG_TYPE",
+ // });
+ // }),
+ // );
+
+ // return rejects(once(et, "foo", { signal: abortedSignal }), {
+ // name: "AbortError",
+ // });
+ // }
+
+ // async function eventTargetAbortSignalAfter() {
+ // const et = new EventTarget();
+ // const ac = new AbortController();
+ // const r = rejects(once(et, "foo", { signal: ac.signal }), {
+ // name: "AbortError",
+ // });
+ // process.nextTick(() => ac.abort());
+ // return r;
+ // }
+
+ // async function eventTargetAbortSignalAfterEvent() {
+ // const et = new EventTarget();
+ // const ac = new AbortController();
+ // process.nextTick(() => {
+ // et.dispatchEvent(new Event("foo"));
+ // ac.abort();
+ // });
+ // await once(et, "foo", { signal: ac.signal });
+ // }
+});