Compare commits

...

6 Commits

Author SHA1 Message Date
Derrick Farris
d115f278de fix(node:events): correct node:events.once, add tests 2023-04-07 02:20:12 -05:00
Derrick Farris
f3d593c9bd fix(node:events): fix abort behavior 2023-04-06 17:34:32 -05:00
Derrick Farris
671074ac8a test(node:events): remove console.log 2023-04-06 17:34:32 -05:00
Derrick Farris
bfd92d059c feat(node:events): finish EE.AsyncIterator and node:events.on(), add some tests 2023-04-06 17:34:31 -05:00
Derrick Farris
b2625b4ab9 wip(node:events): get async iter working, work through test flakiness 2023-04-06 17:34:31 -05:00
Derrick Farris
91f02989dc wip(node:events): fix node:events.on async iter to spec 2023-04-06 17:34:31 -05:00
17 changed files with 1571 additions and 172 deletions

View File

@@ -1,4 +1,4 @@
//-- AUTOGENERATED FILE -- 1679200292
//-- AUTOGENERATED FILE -- 1680843050
// clang-format off
#pragma once

View File

@@ -1,5 +1,5 @@
// clang-format off
//-- AUTOGENERATED FILE -- 1679530947
//-- AUTOGENERATED FILE -- 1680843050
#pragma once
#include <stddef.h>

View File

@@ -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;
}
}

View File

@@ -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:

View File

@@ -147,6 +147,7 @@ using namespace JSC;
macro(nextTick) \
macro(normalize) \
macro(on) \
macro(onAsyncIterator) \
macro(once) \
macro(options) \
macro(origin) \

View File

@@ -4,7 +4,7 @@ TLDR:
```bash
# Delete the built files
make clean-bindings generate-bindings && \
make clean-bindings generate-builtins && \
# Compile all the C++ files which live in ../bindings
make bindings -j10 && \
# Re-link the binary without compiling zig (so it's faster)
@@ -31,7 +31,7 @@ The `js` directory is necessary for the bindings generator to work.
To regenerate the builtins, run this from Bun's project root (where the `Makefile` is)
```bash
make builtins
make generate-builtins
```
You'll want to also rebuild all the C++ bindings or you will get strange crashes on start

View File

@@ -39,6 +39,7 @@
#include "ImportMetaObjectBuiltins.cpp"
#include "JSBufferConstructorBuiltins.cpp"
#include "JSBufferPrototypeBuiltins.cpp"
#include "NodeEventsBuiltins.cpp"
#include "ProcessObjectInternalsBuiltins.cpp"
#include "ReadableByteStreamControllerBuiltins.cpp"
#include "ReadableByteStreamInternalsBuiltins.cpp"

View File

@@ -0,0 +1,323 @@
/*
* Copyright (c) 2015 Igalia
* Copyright (c) 2015 Igalia S.L.
* Copyright (c) 2015 Igalia.
* Copyright (c) 2015, 2016 Canon Inc. All rights reserved.
* Copyright (c) 2015, 2016, 2017 Canon Inc.
* Copyright (c) 2016, 2020 Apple Inc. All rights reserved.
* Copyright (c) 2022 Codeblog Corp. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*
*/
// DO NOT EDIT THIS FILE. It is automatically generated from JavaScript files for
// builtins by the script: Source/JavaScriptCore/Scripts/generate-js-builtins.py
#include "config.h"
#include "NodeEventsBuiltins.h"
#include "WebCoreJSClientData.h"
#include <JavaScriptCore/HeapInlines.h>
#include <JavaScriptCore/IdentifierInlines.h>
#include <JavaScriptCore/ImplementationVisibility.h>
#include <JavaScriptCore/Intrinsic.h>
#include <JavaScriptCore/JSCJSValueInlines.h>
#include <JavaScriptCore/JSCellInlines.h>
#include <JavaScriptCore/StructureInlines.h>
#include <JavaScriptCore/VM.h>
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 = 4565;
static const JSC::Intrinsic s_nodeEventsOnAsyncIteratorCodeIntrinsic = JSC::NoIntrinsic;
const char* const s_nodeEventsOnAsyncIteratorCode =
"(function (emitter, event, options) {\n" \
" \"use strict\";\n" \
"\n" \
" var { AbortSignal, Symbol, Number, 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)) @throwTypeError(\"emitter is required\");\n" \
" //\n" \
" if (!(@isObject(emitter) && @isCallable(emitter.emit) && @isCallable(emitter.on)))\n" \
" @throwTypeError(\"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" \
" @throwTypeError(\"options.signal must be an AbortSignal\");\n" \
"\n" \
" if (signal?.aborted) {\n" \
" //\n" \
" throw makeAbortError(@undefined, { cause: signal?.reason });\n" \
" }\n" \
"\n" \
" var highWatermark = options.highWatermark ?? Number.MAX_SAFE_INTEGER;\n" \
" if (highWatermark < 1) \n" \
" @throwRangeError(\"options.highWatermark must be >= 1\");\n" \
"\n" \
" var lowWatermark = options.lowWatermark ?? 1;\n" \
" if (lowWatermark < 1) \n" \
" @throwRangeError(\"options.lowWatermark must be >= 1\");\n" \
"\n" \
" var unconsumedEvents = @createFIFO();\n" \
" var unconsumedPromises = @createFIFO();\n" \
"\n" \
" var paused = false;\n" \
" var error = null;\n" \
" var finished = false;\n" \
" var size = 0;\n" \
" var listeners = [];\n" \
"\n" \
" function abortListener() {\n" \
" errorHandler(makeAbortError(@undefined, { cause: signal?.reason }));\n" \
" }\n" \
"\n" \
" function eventHandler(value) {\n" \
" if (unconsumedPromises.isEmpty()) {\n" \
" size++;\n" \
" if (!paused && size > highWatermark) {\n" \
" paused = true;\n" \
" emitter.pause();\n" \
" }\n" \
" unconsumedEvents.push(value);\n" \
" } else unconsumedPromises.shift().@resolve.@call(@undefined, [value]);\n" \
" }\n" \
"\n" \
" function closeHandler() {\n" \
" removeAllListeners();\n" \
" finished = true;\n" \
" while (!unconsumedPromises.isEmpty()) {\n" \
" const promise = unconsumedPromises.shift();\n" \
" promise.@resolve.@call(@undefined, [@undefined]);\n" \
" }\n" \
" return @createFulfilledPromise([@undefined]);\n" \
" }\n" \
"\n" \
" function errorHandler(err) {\n" \
" if (unconsumedPromises.isEmpty()) error = err;\n" \
" else unconsumedPromises.shift().@reject.@call(@undefined, err);\n" \
" \n" \
" closeHandler();\n" \
" }\n" \
" \n" \
" function addEventListener(emitter, event, handler) {\n" \
" emitter.on(event, handler);\n" \
" listeners.push([emitter, event, handler]);\n" \
" }\n" \
" \n" \
" function removeAllListeners() {\n" \
" while (listeners.length > 0) {\n" \
" var entry = listeners.pop();\n" \
" var [emitter, event, handler] = entry;\n" \
" emitter.off(event, handler);\n" \
" }\n" \
" }\n" \
"\n" \
" var createIterator = async function* NodeEventsOnAsyncIterator() {\n" \
" //\n" \
" try {\n" \
" while (true) {\n" \
" //\n" \
" while (size) {\n" \
" const value = unconsumedEvents.shift();\n" \
" size--;\n" \
" if (paused && size < lowWatermark) {\n" \
" emitter.resume();\n" \
" paused = false;\n" \
" break;\n" \
" }\n" \
" yield @createFulfilledPromise([value]);\n" \
" }\n" \
"\n" \
" //\n" \
" //\n" \
" //\n" \
" if (error) {\n" \
" throw error;\n" \
" }\n" \
"\n" \
" //\n" \
" if (finished) break;\n" \
"\n" \
" //\n" \
" var nextEventPromiseCapability = @newPromiseCapability(@Promise);\n" \
" unconsumedPromises.push(nextEventPromiseCapability);\n" \
" yield nextEventPromiseCapability.@promise;\n" \
" }\n" \
" } finally {\n" \
" closeHandler();\n" \
" }\n" \
" };\n" \
"\n" \
" //\n" \
" addEventListener(emitter, event, eventHandler);\n" \
" if (event !== \"error\" && typeof emitter.on === \"function\") {\n" \
" addEventListener(emitter, \"error\", errorHandler);\n" \
" }\n" \
" var closeEvents = options?.close;\n" \
" if (closeEvents?.length) {\n" \
" for (var i = 0; i < closeEvents.length; i++) {\n" \
" addEventListener(emitter, closeEvents[i], closeHandler);\n" \
" }\n" \
" }\n" \
"\n" \
" if (signal) signal.addEventListener(\"abort\", abortListener, { once: true });\n" \
"\n" \
" var iterator = createIterator();\n" \
" @Object.defineProperties(iterator, {\n" \
" return: {\n" \
" value: () => closeHandler(),\n" \
" },\n" \
" throw: {\n" \
" value: (err) => {\n" \
" if (!err || !(err instanceof Error)) {\n" \
" throw new TypeError(\"EventEmitter.AsyncIterator must be called with an error\");\n" \
" }\n" \
" errorHandler(err);\n" \
" },\n" \
" },\n" \
" [Symbol.asyncIterator]: {\n" \
" value: () => iterator,\n" \
" },\n" \
" });\n" \
" return iterator;\n" \
"})\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) \
{\
JSVMClientData* clientData = static_cast<JSVMClientData*>(vm.clientData); \
return clientData->builtinFunctions().nodeEventsBuiltins().codeName##Executable()->link(vm, nullptr, clientData->builtinFunctions().nodeEventsBuiltins().codeName##Source(), std::nullopt, s_##codeName##Intrinsic); \
}
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_CODE(DEFINE_BUILTIN_GENERATOR)
#undef DEFINE_BUILTIN_GENERATOR
} // namespace WebCore

View File

@@ -0,0 +1,137 @@
/*
* Copyright (c) 2015 Igalia
* Copyright (c) 2015 Igalia S.L.
* Copyright (c) 2015 Igalia.
* Copyright (c) 2015, 2016 Canon Inc. All rights reserved.
* Copyright (c) 2015, 2016, 2017 Canon Inc.
* Copyright (c) 2016, 2020 Apple Inc. All rights reserved.
* Copyright (c) 2022 Codeblog Corp. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*
*/
// DO NOT EDIT THIS FILE. It is automatically generated from JavaScript files for
// builtins by the script: Source/JavaScriptCore/Scripts/generate-js-builtins.py
#pragma once
#include <JavaScriptCore/BuiltinUtils.h>
#include <JavaScriptCore/Identifier.h>
#include <JavaScriptCore/JSFunction.h>
#include <JavaScriptCore/UnlinkedFunctionExecutable.h>
namespace JSC {
class FunctionExecutable;
}
namespace WebCore {
/* NodeEvents */
extern const char* const s_nodeEventsOnAsyncIteratorCode;
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&);
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_CODE(DECLARE_BUILTIN_GENERATOR)
#undef DECLARE_BUILTIN_GENERATOR
class NodeEventsBuiltinsWrapper : private JSC::WeakHandleOwner {
public:
explicit NodeEventsBuiltinsWrapper(JSC::VM& vm)
: m_vm(vm)
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_FUNCTION_NAME(INITIALIZE_BUILTIN_NAMES)
#define INITIALIZE_BUILTIN_SOURCE_MEMBERS(name, functionName, overriddenName, length) , m_##name##Source(JSC::makeSource(StringImpl::createWithoutCopying(s_##name, length), { }))
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_CODE(INITIALIZE_BUILTIN_SOURCE_MEMBERS)
#undef INITIALIZE_BUILTIN_SOURCE_MEMBERS
{
}
#define EXPOSE_BUILTIN_EXECUTABLES(name, functionName, overriddenName, length) \
JSC::UnlinkedFunctionExecutable* name##Executable(); \
const JSC::SourceCode& name##Source() const { return m_##name##Source; }
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_CODE(EXPOSE_BUILTIN_EXECUTABLES)
#undef EXPOSE_BUILTIN_EXECUTABLES
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_FUNCTION_NAME(DECLARE_BUILTIN_IDENTIFIER_ACCESSOR)
void exportNames();
private:
JSC::VM& m_vm;
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_FUNCTION_NAME(DECLARE_BUILTIN_NAMES)
#define DECLARE_BUILTIN_SOURCE_MEMBERS(name, functionName, overriddenName, length) \
JSC::SourceCode m_##name##Source;\
JSC::Weak<JSC::UnlinkedFunctionExecutable> m_##name##Executable;
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_CODE(DECLARE_BUILTIN_SOURCE_MEMBERS)
#undef DECLARE_BUILTIN_SOURCE_MEMBERS
};
#define DEFINE_BUILTIN_EXECUTABLES(name, functionName, overriddenName, length) \
inline JSC::UnlinkedFunctionExecutable* NodeEventsBuiltinsWrapper::name##Executable() \
{\
if (!m_##name##Executable) {\
JSC::Identifier executableName = functionName##PublicName();\
if (overriddenName)\
executableName = JSC::Identifier::fromString(m_vm, overriddenName);\
m_##name##Executable = JSC::Weak<JSC::UnlinkedFunctionExecutable>(JSC::createBuiltinExecutable(m_vm, m_##name##Source, executableName, s_##name##ImplementationVisibility, s_##name##ConstructorKind, s_##name##ConstructAbility), this, &m_##name##Executable);\
}\
return m_##name##Executable.get();\
}
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_CODE(DEFINE_BUILTIN_EXECUTABLES)
#undef DEFINE_BUILTIN_EXECUTABLES
inline void NodeEventsBuiltinsWrapper::exportNames()
{
#define EXPORT_FUNCTION_NAME(name) m_vm.propertyNames->appendExternalName(name##PublicName(), name##PrivateName());
WEBCORE_FOREACH_NODEEVENTS_BUILTIN_FUNCTION_NAME(EXPORT_FUNCTION_NAME)
#undef EXPORT_FUNCTION_NAME
}
} // namespace WebCore

View File

@@ -41,6 +41,7 @@
#include "ImportMetaObjectBuiltins.h"
#include "JSBufferConstructorBuiltins.h"
#include "JSBufferPrototypeBuiltins.h"
#include "NodeEventsBuiltins.h"
#include "ProcessObjectInternalsBuiltins.h"
#include "ReadableByteStreamControllerBuiltins.h"
#include "ReadableByteStreamInternalsBuiltins.h"
@@ -71,6 +72,7 @@ public:
, m_importMetaObjectBuiltins(m_vm)
, m_jsBufferConstructorBuiltins(m_vm)
, m_jsBufferPrototypeBuiltins(m_vm)
, m_nodeEventsBuiltins(m_vm)
, m_processObjectInternalsBuiltins(m_vm)
, m_readableByteStreamControllerBuiltins(m_vm)
, m_readableByteStreamInternalsBuiltins(m_vm)
@@ -101,6 +103,7 @@ public:
ImportMetaObjectBuiltinsWrapper& importMetaObjectBuiltins() { return m_importMetaObjectBuiltins; }
JSBufferConstructorBuiltinsWrapper& jsBufferConstructorBuiltins() { return m_jsBufferConstructorBuiltins; }
JSBufferPrototypeBuiltinsWrapper& jsBufferPrototypeBuiltins() { return m_jsBufferPrototypeBuiltins; }
NodeEventsBuiltinsWrapper& nodeEventsBuiltins() { return m_nodeEventsBuiltins; }
ProcessObjectInternalsBuiltinsWrapper& processObjectInternalsBuiltins() { return m_processObjectInternalsBuiltins; }
ReadableByteStreamControllerBuiltinsWrapper& readableByteStreamControllerBuiltins() { return m_readableByteStreamControllerBuiltins; }
ReadableByteStreamInternalsBuiltinsWrapper& readableByteStreamInternalsBuiltins() { return m_readableByteStreamInternalsBuiltins; }
@@ -126,6 +129,7 @@ private:
ImportMetaObjectBuiltinsWrapper m_importMetaObjectBuiltins;
JSBufferConstructorBuiltinsWrapper m_jsBufferConstructorBuiltins;
JSBufferPrototypeBuiltinsWrapper m_jsBufferPrototypeBuiltins;
NodeEventsBuiltinsWrapper m_nodeEventsBuiltins;
ProcessObjectInternalsBuiltinsWrapper m_processObjectInternalsBuiltins;
ReadableByteStreamControllerBuiltinsWrapper m_readableByteStreamControllerBuiltins;
ReadableByteStreamInternalsBuiltinsWrapper m_readableByteStreamInternalsBuiltins;

View File

@@ -112,4 +112,4 @@ function write(input) {
writer.flush(true);
return wrote;
}
}

View File

@@ -0,0 +1,267 @@
/*
* Copyright 2022 Codeblog Corp. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
* EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
* PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
* OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
function onAsyncIterator(emitter, event, options) {
"use strict";
var { AbortSignal, Symbol, Number, 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)) @throwTypeError("emitter is required");
// TODO: Do a more accurate check
if (!(@isObject(emitter) && @isCallable(emitter.emit) && @isCallable(emitter.on)))
@throwTypeError("emitter must be an EventEmitter");
if (@isUndefinedOrNull(options)) options = {};
// Parameters validation
var signal = options.signal;
if (signal !== undefined && (!@isObject(signal) || !(signal instanceof AbortSignal)))
@throwTypeError("options.signal must be an AbortSignal");
if (signal?.aborted) {
// TODO: Make this a builtin
throw makeAbortError(@undefined, { cause: signal?.reason });
}
var highWatermark = options.highWatermark ?? Number.MAX_SAFE_INTEGER;
if (highWatermark < 1)
@throwRangeError("options.highWatermark must be >= 1");
var lowWatermark = options.lowWatermark ?? 1;
if (lowWatermark < 1)
@throwRangeError("options.lowWatermark must be >= 1");
var unconsumedEvents = @createFIFO();
var unconsumedPromises = @createFIFO();
var paused = false;
var error = null;
var finished = false;
var size = 0;
var listeners = [];
function abortListener() {
errorHandler(makeAbortError(@undefined, { cause: signal?.reason }));
}
function eventHandler(value) {
if (unconsumedPromises.isEmpty()) {
size++;
if (!paused && size > highWatermark) {
paused = true;
emitter.pause();
}
unconsumedEvents.push(value);
} else unconsumedPromises.shift().@resolve.@call(@undefined, [value]);
}
function closeHandler() {
removeAllListeners();
finished = true;
while (!unconsumedPromises.isEmpty()) {
const promise = unconsumedPromises.shift();
promise.@resolve.@call(@undefined, [@undefined]);
}
return @createFulfilledPromise([@undefined]);
}
function errorHandler(err) {
if (unconsumedPromises.isEmpty()) error = err;
else unconsumedPromises.shift().@reject.@call(@undefined, err);
closeHandler();
}
function addEventListener(emitter, event, handler) {
emitter.on(event, handler);
listeners.push([emitter, event, handler]);
}
function removeAllListeners() {
while (listeners.length > 0) {
var entry = listeners.pop();
var [emitter, event, handler] = entry;
emitter.off(event, handler);
}
}
var createIterator = async function* NodeEventsOnAsyncIterator() {
// First, we consume all unread events
try {
while (true) {
// Go through queued events
while (size) {
const value = unconsumedEvents.shift();
size--;
if (paused && size < lowWatermark) {
emitter.resume();
paused = false;
break;
}
yield @createFulfilledPromise([value]);
}
// Check if error happened before yielding anything
// This happens one time if at all, because after 'error'
// we stop listening
if (error) {
throw error;
}
// If the iterator is finished, break
if (finished) break;
// Wait until an event happens
var nextEventPromiseCapability = @newPromiseCapability(@Promise);
unconsumedPromises.push(nextEventPromiseCapability);
yield nextEventPromiseCapability.@promise;
}
} finally {
closeHandler();
}
};
// Adding event handlers
addEventListener(emitter, event, eventHandler);
if (event !== "error" && typeof emitter.on === "function") {
addEventListener(emitter, "error", errorHandler);
}
var closeEvents = options?.close;
if (closeEvents?.length) {
for (var i = 0; i < closeEvents.length; i++) {
addEventListener(emitter, closeEvents[i], closeHandler);
}
}
if (signal) signal.addEventListener("abort", abortListener, { once: true });
var iterator = createIterator();
@Object.defineProperties(iterator, {
return: {
value: () => closeHandler(),
},
throw: {
value: (err) => {
if (!err || !(err instanceof Error)) {
throw new TypeError("EventEmitter.AsyncIterator must be called with an error");
}
errorHandler(err);
},
},
[Symbol.asyncIterator]: {
value: () => iterator,
},
});
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;
}

View File

@@ -2206,4 +2206,4 @@ function readableStreamDefineLazyIterators(prototype) {
@Object.@defineProperty(prototype, asyncIterator, { value: createAsyncIterator });
@Object.@defineProperty(prototype, "values", { value: createValues });
return prototype;
}
}

View File

@@ -24,14 +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, "on"_s));
exportValues.append(JSC::JSFunction::create(
vm, lexicalGlobalObject, 0, MAKE_STATIC_STRING_IMPL("on"),
Events_functionOn, ImplementationVisibility::Public));
exportNames.append(
JSC::Identifier::fromString(vm, "captureRejectionSymbol"_s));
exportValues.append(Symbol::create(
@@ -41,16 +33,30 @@ inline void generateEventsSourceCode(JSC::JSGlobalObject *lexicalGlobalObject,
jsCast<JSFunction *>(WebCore::JSEventEmitter::getConstructor(
vm, reinterpret_cast<Zig::GlobalObject *>(globalObject)));
for (size_t i = 0; i < exportNames.size(); i++) {
eventEmitterModuleCJS->putDirect(vm, exportNames[i], exportValues.at(i), 0);
}
exportNames.append(JSC::Identifier::fromString(vm, "on"_s));
auto *onAsyncIterFnPtr = eventEmitterModuleCJS->putDirectBuiltinFunction(
vm, globalObject, JSC::Identifier::fromString(vm, "on"_s),
nodeEventsOnAsyncIteratorCodeGenerator(vm),
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(
Identifier::fromUid(vm.symbolRegistry().symbolForKey("CommonJS"_s))),
jsNumber(0), 0);
for (size_t i = 0; i < exportNames.size(); i++) {
eventEmitterModuleCJS->putDirect(vm, exportNames[i], exportValues.at(i), 0);
}
exportNames.append(JSC::Identifier::fromString(vm, "default"_s));
exportValues.append(eventEmitterModuleCJS);
}

View File

@@ -1,11 +1,61 @@
import { test, describe, expect, it } from "bun:test";
import { heapStats } from "bun:jsc";
import { expectMaxObjectTypeCount, gc } from "harness";
// this is also testing that imports with default and named imports in the same statement work
// our transpiler transform changes this to a var with import.meta.require
import EventEmitter, { getEventListeners, captureRejectionSymbol } from "node:events";
const waysOfCreating = [
() => Object.create(EventEmitter.prototype),
() => new EventEmitter(),
() => new (class extends EventEmitter {})(),
() => {
class MyEmitter extends EventEmitter {}
return new MyEmitter();
},
() => {
var foo = {};
Object.setPrototypeOf(foo, EventEmitter.prototype);
return foo;
},
() => {
function FakeEmitter(this: any) {
return EventEmitter.call(this);
}
Object.setPrototypeOf(FakeEmitter.prototype, EventEmitter.prototype);
Object.setPrototypeOf(FakeEmitter, EventEmitter);
return new (FakeEmitter as any)();
},
() => {
const FakeEmitter: any = function FakeEmitter(this: any) {
EventEmitter.call(this);
} as any;
Object.assign(FakeEmitter.prototype, EventEmitter.prototype);
Object.assign(FakeEmitter, EventEmitter);
return new FakeEmitter();
},
() => {
var foo = {};
Object.assign(foo, EventEmitter.prototype);
return foo;
},
];
describe("EventEmitter", () => {
const emitters = [EventEmitter, require("events")];
it("should emit events", () => {
for (let Emitter of emitters) {
const emitter = new Emitter();
var called = false;
const listener = () => {
called = true;
};
emitter.on("test", listener);
emitter.emit("test");
expect(called).toBe(true);
}
});
it("captureRejectionSymbol", () => {
expect(EventEmitter.captureRejectionSymbol).toBeDefined();
expect(captureRejectionSymbol).toBeDefined();
@@ -74,91 +124,55 @@ describe("EventEmitter", () => {
emitter.on("wow", () => done());
setTimeout(() => emitter.emit("wow"), 1);
});
});
const waysOfCreating = [
() => Object.create(EventEmitter.prototype),
() => new EventEmitter(),
() => new (class extends EventEmitter {})(),
() => {
class MyEmitter extends EventEmitter {}
return new MyEmitter();
},
() => {
var foo = {};
Object.setPrototypeOf(foo, EventEmitter.prototype);
return foo;
},
() => {
function FakeEmitter(this: any) {
return EventEmitter.call(this);
}
Object.setPrototypeOf(FakeEmitter.prototype, EventEmitter.prototype);
Object.setPrototypeOf(FakeEmitter, EventEmitter);
return new (FakeEmitter as any)();
},
() => {
const FakeEmitter: any = function FakeEmitter(this: any) {
EventEmitter.call(this);
} as any;
Object.assign(FakeEmitter.prototype, EventEmitter.prototype);
Object.assign(FakeEmitter, EventEmitter);
return new FakeEmitter();
},
() => {
var foo = {};
Object.assign(foo, EventEmitter.prototype);
return foo;
},
];
for (let create of waysOfCreating) {
it(`${create.toString().slice(10, 40).replaceAll("\n", "\\n").trim()} should work`, () => {
var myEmitter = create();
var called = false;
(myEmitter as EventEmitter).once("event", function () {
called = true;
// @ts-ignore
expect(this).toBe(myEmitter);
});
var firstEvents = myEmitter._events;
expect(myEmitter.listenerCount("event")).toBe(1);
for (let create of waysOfCreating) {
it(`${create.toString().slice(10, 40).replaceAll("\n", "\\n").trim()} should work`, () => {
var myEmitter = create();
var called = false;
(myEmitter as EventEmitter).once("event", function () {
called = true;
// @ts-ignore
expect(this).toBe(myEmitter);
expect(myEmitter.emit("event")).toBe(true);
expect(myEmitter.listenerCount("event")).toBe(0);
expect(firstEvents).toBe(myEmitter._events);
expect(called).toBe(true);
});
var firstEvents = myEmitter._events;
expect(myEmitter.listenerCount("event")).toBe(1);
}
expect(myEmitter.emit("event")).toBe(true);
expect(myEmitter.listenerCount("event")).toBe(0);
expect(firstEvents).toBe(myEmitter._events);
expect(called).toBe(true);
test("EventEmitter.on", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.on("foo", () => {})).toBe(myEmitter);
});
}
test("EventEmitter.on", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.on("foo", () => {})).toBe(myEmitter);
});
test("EventEmitter.off", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.off("foo", () => {})).toBe(myEmitter);
});
// Internally, EventEmitter has a JSC::Weak with the thisValue of the listener
test("EventEmitter GCs", async () => {
gc();
const startCount = heapStats().objectTypeCounts["EventEmitter"] ?? 0;
(function () {
function EventEmitterSubclass(this: any) {
EventEmitter.call(this);
}
Object.setPrototypeOf(EventEmitterSubclass.prototype, EventEmitter.prototype);
Object.setPrototypeOf(EventEmitterSubclass, EventEmitter);
// @ts-ignore
var myEmitter = new EventEmitterSubclass();
myEmitter.on("foo", () => {});
myEmitter.emit("foo");
})();
await expectMaxObjectTypeCount(expect, "EventEmitter", startCount);
test("EventEmitter.off", () => {
var myEmitter = new EventEmitter();
expect(myEmitter.off("foo", () => {})).toBe(myEmitter);
});
// Internally, EventEmitter has a JSC::Weak with the thisValue of the listener
test("EventEmitter GCs", async () => {
gc();
const startCount = heapStats().objectTypeCounts["EventEmitter"] ?? 0;
(function () {
function EventEmitterSubclass(this: any) {
EventEmitter.call(this);
}
Object.setPrototypeOf(EventEmitterSubclass.prototype, EventEmitter.prototype);
Object.setPrototypeOf(EventEmitterSubclass, EventEmitter);
// @ts-ignore
var myEmitter = new EventEmitterSubclass();
myEmitter.on("foo", () => {});
myEmitter.emit("foo");
})();
await expectMaxObjectTypeCount(expect, "EventEmitter", startCount);
});
});

View File

@@ -1,18 +0,0 @@
import { describe, it, expect } from "bun:test";
import { EventEmitter } from "events";
var emitters = [EventEmitter, require("events")];
describe("EventEmitter", () => {
it("should emit events", () => {
for (let Emitter of emitters) {
const emitter = new Emitter();
var called = false;
const listener = () => {
called = true;
};
emitter.on("test", listener);
emitter.emit("test");
expect(called).toBe(true);
}
});
});

View File

@@ -0,0 +1,719 @@
import { EventEmitter, getEventListeners, on, once } from "node:events";
import { createTest } from "node-harness";
const { beforeAll, expect, assert, strictEqual, describe, it, createCallCheckCtx, createDoneDotAll } = createTest(
import.meta.path,
);
// const NodeEventTarget = globalThis.EventTarget;
describe("node:events.on() (EventEmitter AsyncIterator)", () => {
it("should return an async iterator", async () => {
const ee = new EventEmitter();
const iterable = on(ee, "foo");
ee.emit("foo", "bar");
// 'bar' is a spurious event, we are testing
// that it does not show up in the iterable
ee.emit("bar", 24);
ee.emit("foo", 42);
const expected = [["bar"], [42]];
for await (const event of iterable) {
const current = expected.shift();
assert.deepStrictEqual(current, event);
if (expected.length === 0) {
break;
}
}
assert.strictEqual(ee.listenerCount("foo"), 0);
assert.strictEqual(ee.listenerCount("error"), 0);
});
it("should throw an error when the first argument is not an EventEmitter", () => {
expect(() => on({} as any, "foo")).toThrow();
});
it("should throw an error when an error event is emitted", async () => {
const ee = new EventEmitter();
const _err = new Error("kaboom");
const iterable = on(ee, "foo");
ee.emit("error", _err);
let looped = false;
let thrown = false;
try {
// eslint-disable-next-line no-unused-vars
for await (const event of iterable) {
looped = true;
}
} catch (err) {
thrown = true;
assert.strictEqual(err, _err);
}
assert.strictEqual(thrown, true);
assert.strictEqual(looped, false);
});
it("should throw when error emitted after successful events", async () => {
const ee = new EventEmitter();
const _err = new Error("kaboom");
const iterable = on(ee, "foo");
ee.emit("foo", 42);
ee.emit("error", _err);
const expected = [[42]] as (number[] | undefined[])[];
const current = [] as (number[] | undefined[])[];
const received = [] as (number[] | undefined[])[];
let thrownErr: any;
try {
for await (const event of iterable) {
const _expected = expected.shift();
if (_expected !== undefined) current.push(_expected);
received.push(event);
}
} catch (err) {
thrownErr = err;
}
assert.deepStrictEqual(current, received);
assert.strictEqual(ee.listenerCount("foo"), 0);
assert.strictEqual(ee.listenerCount("error"), 0);
expect(thrownErr).toBeInstanceOf(Error);
assert.strictEqual(thrownErr, _err);
});
it("should throw when error thrown from inside loop", async () => {
const ee = new EventEmitter();
const _err = new Error("kaboom");
const iterable = on(ee, "foo");
ee.emit("foo", 42);
let looped = false;
let thrown = false;
try {
// eslint-disable-next-line no-unused-vars
for await (const event of iterable) {
assert.deepStrictEqual(event, [42]);
looped = true;
throw _err;
}
} catch (err) {
thrown = true;
assert.strictEqual(err, _err);
}
assert.strictEqual(thrown, true);
assert.strictEqual(looped, true);
assert.strictEqual(ee.listenerCount("foo"), 0);
assert.strictEqual(ee.listenerCount("error"), 0);
});
it("should allow for async iteration via .next()", async done => {
const ee = new EventEmitter();
const iterable = on(ee, "foo");
process.nextTick(() => {
ee.emit("foo", "bar");
ee.emit("foo", 42);
// @ts-ignore
iterable.return();
});
const results = await Promise.all([iterable.next(), iterable.next(), iterable.next()]);
assert.deepStrictEqual(results, [
{
value: ["bar"],
done: false,
},
{
value: [42],
done: false,
},
{
value: undefined,
done: true,
},
]);
assert.deepStrictEqual(await iterable.next(), {
value: undefined,
done: true,
});
done();
});
it("it should fulfill subsequent deferred promises with `undefined` when the emitter emits an error", async done => {
const ee = new EventEmitter();
const iterable = on(ee, "foo");
const _err = new Error("kaboom");
process.nextTick(function () {
ee.emit("error", _err);
});
const results = await Promise.allSettled([iterable.next(), iterable.next(), iterable.next()]);
assert.deepStrictEqual(results, [
{
status: "rejected",
reason: _err,
},
{
status: "fulfilled",
value: {
value: undefined,
done: true,
},
},
{
status: "fulfilled",
value: {
value: undefined,
done: true,
},
},
]);
assert.strictEqual(ee.listeners("error").length, 0);
done();
});
describe(".throw()", () => {
let ee: EventEmitter;
let iterable: AsyncIterableIterator<any>;
beforeAll(() => {
ee = new EventEmitter();
iterable = on(ee, "foo");
});
it("should throw a `TypeError` when calling without args", async () => {
expect(() => {
iterable.throw!();
}).toThrow(TypeError);
assert.strictEqual(ee.listenerCount("foo"), 1);
assert.strictEqual(ee.listenerCount("error"), 1);
});
it("should throw when called with an error", async () => {
const _err = new Error("kaboom");
ee.emit("foo", "bar");
ee.emit("foo", 42);
iterable.throw!(_err);
const expected = [["bar"], [42]];
let thrown = false;
let looped = false;
try {
for await (const event of iterable) {
assert.deepStrictEqual(event, expected.shift());
looped = true;
}
} catch (err) {
thrown = true;
assert.strictEqual(err, _err);
}
assert.strictEqual(looped, true);
assert.strictEqual(thrown, true);
assert.strictEqual(ee.listenerCount("foo"), 0);
assert.strictEqual(ee.listenerCount("error"), 0);
});
});
it("should add an error listener when the iterable is created", () => {
const ee = new EventEmitter();
on(ee, "foo");
assert.strictEqual(ee.listenerCount("error"), 1);
});
it("should throw when called with an aborted signal", () => {
const ee = new EventEmitter();
const abortedSignal = AbortSignal.abort();
[1, {}, null, false, "hi"].forEach((signal: any) => {
assert.throws(() => on(ee, "foo", { signal }), Error);
});
assert.throws(() => on(ee, "foo", { signal: abortedSignal }), {
name: "AbortError",
});
});
it("should NOT THROW an `AbortError` AFTER done iterating over events", async _done => {
let _doneCalled = false;
const done = (err?: Error) => {
if (_doneCalled) return;
_doneCalled = true;
_done(err);
};
const ee = new EventEmitter();
const ac = new AbortController();
const i = setInterval(() => ee.emit("foo", "foo"), 1);
let count = 0;
async function foo() {
for await (const f of on(ee, "foo", { signal: ac.signal })) {
assert.strictEqual(f[0], "foo");
if (++count === 5) break;
}
ac.abort(); // No error will occur
}
foo()
.catch(err => done(err))
.finally(() => {
clearInterval(i);
if (!_doneCalled) expect(true).toBe(true);
done();
});
});
it("should THROW an `AbortError` BEFORE done iterating over events", async _done => {
let _doneCalled = false;
const done = (err?: Error) => {
if (_doneCalled) return;
_doneCalled = true;
_done(err);
};
let count = 0;
const createDone = createDoneDotAll(done);
const { mustCall, closeTimers } = createCallCheckCtx(createDone());
const finalDone = createDone();
const ee = new EventEmitter();
const ac = new AbortController();
const i = setInterval(() => ee.emit("foo", "foo"), 10);
setTimeout(() => ac.abort(), 50);
async function foo() {
for await (const f of on(ee, "foo", { signal: ac.signal })) {
assert.deepStrictEqual(f, ["foo"]);
}
}
foo()
.then(() => done(new Error("Should not be called")))
.catch(
mustCall(error => {
assert.strictEqual(error.name, "AbortError");
}),
)
.finally(() => {
clearInterval(i);
closeTimers();
if (!_doneCalled) finalDone();
});
});
// TODO: Uncomment tests for NodeEventTarget and Web EventTarget
// async function eventTarget() {
// const et = new EventTarget();
// const tick = () => et.dispatchEvent(new Event("tick"));
// const interval = setInterval(tick, 0);
// let count = 0;
// for await (const [event] of on(et, "tick")) {
// count++;
// assert.strictEqual(event.type, "tick");
// if (count >= 5) {
// break;
// }
// }
// assert.strictEqual(count, 5);
// clearInterval(interval);
// }
// async function nodeEventTarget() {
// const et = new NodeEventTarget();
// const tick = () => et.dispatchEvent(new Event("tick"));
// const interval = setInterval(tick, 0);
// let count = 0;
// for await (const [event] of on(et, "tick")) {
// count++;
// assert.strictEqual(event.type, "tick");
// if (count >= 5) {
// break;
// }
// }
// assert.strictEqual(count, 5);
// clearInterval(interval);
// }
// async function eventTargetAbortableOnBefore() {
// const et = new EventTarget();
// const abortedSignal = AbortSignal.abort();
// [1, {}, null, false, "hi"].forEach(signal => {
// assert.throws(() => on(et, "foo", { signal }), {
// code: "ERR_INVALID_ARG_TYPE",
// });
// });
// assert.throws(() => on(et, "foo", { signal: abortedSignal }), {
// name: "AbortError",
// });
// }
// async function eventTargetAbortableOnAfter() {
// const et = new EventTarget();
// const ac = new AbortController();
// const i = setInterval(() => et.dispatchEvent(new Event("foo")), 10);
// async function foo() {
// for await (const f of on(et, "foo", { signal: ac.signal })) {
// assert(f);
// }
// }
// foo()
// .catch(
// common.mustCall(error => {
// assert.strictEqual(error.name, "AbortError");
// }),
// )
// .finally(() => {
// clearInterval(i);
// });
// process.nextTick(() => ac.abort());
// }
// async function eventTargetAbortableOnAfter2() {
// const et = new EventTarget();
// const ac = new AbortController();
// const i = setInterval(() => et.dispatchEvent(new Event("foo")), 10);
// async function foo() {
// for await (const f of on(et, "foo", { signal: ac.signal })) {
// assert(f);
// // Cancel after a single event has been triggered.
// ac.abort();
// }
// }
// foo()
// .catch(
// common.mustCall(error => {
// assert.strictEqual(error.name, "AbortError");
// }),
// )
// .finally(() => {
// clearInterval(i);
// });
// }
});
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 });
// }
});