Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/js/builtins/EventEmitterOnce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
// The native EventEmitter backing `process` stores `once()` listeners as the original function plus
// a flag, so there is no wrapper to hand back from rawListeners(). Materialize one on demand to
// expose the `.listener` back-pointer Node documents.
export function createOnceWrapper(target, type, listener) {
var fired = false;
function onceWrapper() {
if (fired) return undefined;
fired = true;
// removeListener() resolves a wrapper to the listener it wraps, so this removes whichever of the
// two is actually registered.
target.removeListener(type, onceWrapper);
return listener.$apply(target, arguments);
}
Comment thread
robobun marked this conversation as resolved.
onceWrapper.listener = listener;
return onceWrapper;
}
8 changes: 8 additions & 0 deletions src/js/node/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -865,4 +865,12 @@ Object.assign(EventEmitter, {
listenerCount,
});

// `process` is backed by the native EventEmitter, which gives it a prototype of its own that already
// implements every method above. Splice this prototype in underneath it so `process instanceof
// EventEmitter` holds, as it does in Node.
const processPrototype = Object.getPrototypeOf(process);
if (processPrototype !== EventEmitterPrototype && Object.getPrototypeOf(processPrototype) === Object.prototype) {
Object.setPrototypeOf(processPrototype, EventEmitterPrototype);
}

export default EventEmitter as any as typeof import("node:events");
121 changes: 114 additions & 7 deletions src/jsc/bindings/webcore/EventEmitter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -48,12 +48,30 @@ void EventEmitter::addListenerForBindings(const Identifier& eventType, RefPtr<Ev
addListener(eventType, listener.releaseNonNull(), once, prepend);
}

void EventEmitter::removeListenerForBindings(const Identifier& eventType, RefPtr<EventListener>&& listener)
static inline Identifier removeListenerEventName(JSC::VM& vm)
{
if (!listener)
return Identifier::fromString(vm, "removeListener"_s);
}

// https://nodejs.org/api/events.html#event-removelistener
void EventEmitter::emitRemoveListenerEvent(const Identifier& eventType, JSC::JSObject* listener)
{
auto* context = scriptExecutionContext();
if (!context)
return;

auto& vm = context->vm();
auto removeListenerEventType = removeListenerEventName(vm);
if (!hasEventListeners(removeListenerEventType))
return;

removeListener(eventType, *listener);
MarkedArgumentBuffer args;
args.append(JSC::identifierToSafePublicJSValue(vm, eventType));
args.append(listener ? JSC::JSValue(listener) : JSC::jsUndefined());
if (args.hasOverflowed()) [[unlikely]]
return;

emit(removeListenerEventType, args);
}

bool EventEmitter::removeListener(const Identifier& eventType, EventListener& listener)
Expand All @@ -62,40 +80,112 @@ bool EventEmitter::removeListener(const Identifier& eventType, EventListener& li
if (!data)
return false;

// Read before removing: the registration holds the reference that keeps this listener alive.
auto* listenerFunction = listener.jsFunction();

if (data->eventListenerMap.remove(eventType, listener)) {
eventListenersDidChange();

if (this->onDidChangeListener)
this->onDidChangeListener(*this, eventType, false);

emitRemoveListenerEvent(eventType, listenerFunction);
Comment thread
robobun marked this conversation as resolved.
return true;
}
return false;
}

// `once()` listeners are stored unwrapped, so removeListener() has to accept the wrapper that
// rawListeners() handed out and resolve it back to the registration it was made for.
bool EventEmitter::removeOnceListenerByWrapper(const Identifier& eventType, JSC::JSObject* wrapper)
{
auto* data = eventTargetData();
if (!data)
return false;

auto* listeners = data->eventListenerMap.find(eventType);
if (!listeners)
return false;

for (auto& registration : *listeners) {
if (registration->onceWrapper() != wrapper)
continue;
// Keep the callback alive: removing the registration drops the map's reference to it.
Ref<EventListener> callback = registration->callback();
return removeListener(eventType, callback.get());
}
return false;
}

void EventEmitter::removeAllListenersForBindings(const Identifier& eventType)
{
removeAllListeners(eventType);
}

bool EventEmitter::removeAllListeners()
{
// 'removeListener' handlers run user JS in this frame, and `map` points into this object.
Ref<EventEmitter> protectedThis(*this);

auto* data = eventTargetData();
if (!data)
return false;

auto& map = data->eventListenerMap;
bool any = !map.isEmpty();
if (map.isEmpty())
return false;

if (auto* context = scriptExecutionContext()) {
auto removeListenerEventType = removeListenerEventName(context->vm());
// Drain one event type at a time so 'removeListener' and onDidChangeListener both fire,
// leaving the 'removeListener' event itself for last as Node does.
for (auto& eventType : map.eventTypes()) {
if (eventType == removeListenerEventType)
continue;
removeAllListeners(eventType);
}
removeAllListeners(removeListenerEventType);
}

Comment thread
robobun marked this conversation as resolved.
// A 'removeListener' handler may have registered listeners for event types the drain above had
// already snapshotted past. Node wipes those silently (`_events = {}`), so no event fires for
// them here either, but the native side still has to hear about it or an OS signal handler
// installed by that late registration would outlive the listener that asked for it.
auto orphaned = map.eventTypes();
map.clear();
if (this->onDidChangeListener) {
for (auto& eventType : orphaned)
this->onDidChangeListener(*this, eventType, false);
}

this->m_thisObject.clear();
return any;
return true;
}

bool EventEmitter::removeAllListeners(const Identifier& eventType)
{
// 'removeListener' handlers run user JS between iterations, and `data` points into this object.
Ref<EventEmitter> protectedThis(*this);

auto* data = eventTargetData();
if (!data)
return false;

auto* context = scriptExecutionContext();
if (context && hasEventListeners(removeListenerEventName(context->vm()))) {
auto* listenersVector = data->eventListenerMap.find(eventType);
if (!listenersVector)
return false;

// Node removes listeners in LIFO order, emitting 'removeListener' for each one. Iterate a
// copy: each removal can run JS that mutates the live vector.
SimpleEventListenerVector listeners = *listenersVector;
bool removedAny = false;
for (size_t i = listeners.size(); i > 0; --i)
removedAny |= removeListener(eventType, listeners[i - 1]->callback());
return removedAny;
}

if (data->eventListenerMap.removeAll(eventType)) {
eventListenersDidChange();
if (this->onDidChangeListener)
Expand Down Expand Up @@ -228,15 +318,32 @@ bool EventEmitter::innerInvokeEventListeners(const Identifier& eventType, Simple

auto& callback = registeredListener->callback();

JSObject* jsFunction = callback.jsFunction();
const bool isOnce = registeredListener->isOnce();

if (isOnce) {
// Removing a once() listener emits 'removeListener', which can re-enter and reach this
// registration through another in-flight snapshot. Node's once() wrapper guards against
// running twice with a `fired` flag; this is that guard.
if (registeredListener->hasFired()) [[unlikely]]
continue;
registeredListener->markAsFired();

// Node stores once() listeners wrapped and fires the wrapper. We only materialize one on
// demand, so fire it when it exists, or a wrapper the caller is still holding from
// rawListeners() would invoke the listener a second time.
if (auto* onceWrapper = registeredListener->onceWrapper())
jsFunction = onceWrapper;
}

// Make sure the JS wrapper and function stay alive until the end of this scope. Otherwise,
// event listeners with 'once' flag may get collected as soon as they get unregistered below,
// before we call the js function.
JSObject* jsFunction = callback.jsFunction();
JSC::EnsureStillAliveScope wrapperProtector(callback.wrapper());
JSC::EnsureStillAliveScope jsFunctionProtector(jsFunction);

// Do this before invocation to avoid reentrancy issues.
if (registeredListener->isOnce())
if (isOnce)
removeListener(eventType, callback);

if (!jsFunction) [[unlikely]]
Expand Down
3 changes: 2 additions & 1 deletion src/jsc/bindings/webcore/EventEmitter.h
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ class EventEmitter final : public ScriptWrappable, public CanMakeWeakPtr<EventEm
WEBCORE_EXPORT bool isNode() const { return false; };
bool removeAllListeners();
WEBCORE_EXPORT void addListenerForBindings(const Identifier& eventType, RefPtr<EventListener>&&, bool, bool);
WEBCORE_EXPORT void removeListenerForBindings(const Identifier& eventType, RefPtr<EventListener>&&);
WEBCORE_EXPORT void removeAllListenersForBindings(const Identifier& eventType);
WEBCORE_EXPORT bool emitForBindings(const Identifier&, const MarkedArgumentBuffer&);

WEBCORE_EXPORT bool addListener(const Identifier& eventType, Ref<EventListener>&&, bool, bool);
WEBCORE_EXPORT bool removeListener(const Identifier& eventType, EventListener&);
WEBCORE_EXPORT bool removeOnceListenerByWrapper(const Identifier& eventType, JSC::JSObject* wrapper);
WEBCORE_EXPORT bool removeAllListeners(const Identifier& eventType);

WEBCORE_EXPORT bool emit(const Identifier&, const MarkedArgumentBuffer&);
Expand Down Expand Up @@ -109,6 +109,7 @@ class EventEmitter final : public ScriptWrappable, public CanMakeWeakPtr<EventEm

bool innerInvokeEventListeners(const Identifier&, SimpleEventListenerVector, const MarkedArgumentBuffer& arguments);
void invalidateEventListenerRegions();
void emitRemoveListenerEvent(const Identifier& eventType, JSC::JSObject* listener);

EventEmitterData m_eventTargetData;
unsigned m_maxListeners { 10 };
Expand Down
16 changes: 16 additions & 0 deletions src/jsc/bindings/webcore/IdentifierEventListenerMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
#include <wtf/Lock.h>
#include <wtf/Ref.h>
#include <JavaScriptCore/Identifier.h>
#include <JavaScriptCore/Weak.h>
#include <JavaScriptCore/WeakInlines.h>
#include "EventListener.h"

namespace WebCore {
Expand All @@ -23,17 +25,31 @@ class SimpleRegisteredEventListener : public RefCounted<SimpleRegisteredEventLis

void markAsRemoved() { m_wasRemoved = true; }

// Stands in for the `fired` flag on Node's once() wrapper: an emit that still holds this
// registration in its snapshot must not invoke it twice. Distinct from wasRemoved(), because a
// once() listener removed before it ran does still fire out of an in-flight snapshot.
bool hasFired() const { return m_hasFired; }
void markAsFired() { m_hasFired = true; }

// rawListeners() hands out a wrapper for `once()` listeners, cached here so repeated calls keep
// returning the same function. Weak: once nothing holds the wrapper its identity is unobservable.
JSC::JSObject* onceWrapper() const { return m_onceWrapper.get(); }
void setOnceWrapper(JSC::JSObject* wrapper) { m_onceWrapper = JSC::Weak<JSC::JSObject>(wrapper); }

private:
SimpleRegisteredEventListener(Ref<EventListener>&& listener, bool once)
: m_isOnce(once)
, m_wasRemoved(false)
, m_hasFired(false)
, m_callback(WTF::move(listener))
{
}

bool m_isOnce : 1;
bool m_wasRemoved : 1;
bool m_hasFired : 1;
Ref<EventListener> m_callback;
JSC::Weak<JSC::JSObject> m_onceWrapper;
};

using SimpleEventListenerVector = Vector<RefPtr<SimpleRegisteredEventListener>, 2, CrashOnOverflow, 6>;
Expand Down
Loading
Loading