diff options
-rw-r--r-- | src/bun.js/bindings/headers-cpp.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/headers.h | 2 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/JSEventEmitter.cpp | 53 | ||||
-rw-r--r-- | src/bun.js/bindings/webcore/JSEventEmitter.h | 2 | ||||
-rw-r--r-- | src/bun.js/builtins/cpp/NodeEventsBuiltins.cpp | 111 | ||||
-rw-r--r-- | src/bun.js/builtins/cpp/NodeEventsBuiltins.h | 9 | ||||
-rw-r--r-- | src/bun.js/builtins/js/NodeEvents.js | 100 | ||||
-rw-r--r-- | src/bun.js/modules/EventsModule.h | 11 | ||||
-rw-r--r-- | test/js/node/events/node-events.node.test.ts | 298 |
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 }); + // } +}); |