Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
6 changes: 6 additions & 0 deletions src/js/builtins.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -475,6 +475,12 @@ declare interface AddEventListenerOptions {
* internal `kResistStopPropagation`.
*/
$kResistStopPropagation?: boolean;
/**
* Private symbol read by the native EventTarget. A listener registered with it is
* invoked with the event's underlying value (data / detail / error) instead of the
* Event wrapper. Mirrors Node.js's internal `kIsNodeStyleListener`.
*/
Comment thread
robobun marked this conversation as resolved.
$kIsNodeStyleListener?: boolean;
}

declare class OutOfMemoryError {
Expand Down
1 change: 1 addition & 0 deletions src/js/builtins/BunBuiltinNames.h
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ using namespace JSC;
macro(isUntransferable) \
macro(join) \
macro(json) \
macro(kIsNodeStyleListener) \
macro(kResistStopPropagation) \
macro(key) \
macro(lazy) \
Expand Down
214 changes: 22 additions & 192 deletions src/js/node/worker_threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ declare const self: typeof globalThis;
type WebWorker = InstanceType<typeof globalThis.Worker>;

const EventEmitter = require("node:events");
const { SafeMap } = require("internal/primordials");
const Readable = require("internal/streams/readable");
const Writable = require("internal/streams/writable");
const { throwNotImplemented, warnNotImplementedOnce } = require("internal/shared");
Expand Down Expand Up @@ -101,189 +100,7 @@ type NodeWorkerOptions = import("node:worker_threads").WorkerOptions;
// after their Worker exits
let urlRevokeRegistry: FinalizationRegistry<string> | undefined = undefined;

function injectFakeEmitter(Class) {
// Per-instance registry mapping each event to (user listener -> wrapper), so
// listenerCount/eventNames/removeAllListeners work over EventTarget's opaque
// internal map and off() can find the wrapper a given listener registered.
// SafeMap: its prototype is a frozen, null-proto snapshot of Map.prototype, so
// .get/.set/.size/.values()/iteration all bypass a user-replaced Map.prototype.
// (It has no @get/@set private names, so the $-intrinsics don't apply to it.)
// Keyed by a module-local symbol, not a WeakMap — WeakMap has neither defence.
const kListenerRegistry = Symbol("listenerRegistry");
function registryFor(target, create) {
let map = target[kListenerRegistry];
if (!map && create) target[kListenerRegistry] = map = new SafeMap();
return map;
}

function messageEventHandler(event: MessageEvent) {
return event.data;
}

function errorEventHandler(event: ErrorEvent) {
return event.error;
}

function customEventHandler(event) {
return event.detail;
}

function wrapped(run, listener) {
return function (event) {
return listener(run(event));
};
}

function functionForEventType(event, listener) {
switch (event) {
case "error":
case "messageerror": {
return wrapped(errorEventHandler, listener);
}

case "message": {
return wrapped(messageEventHandler, listener);
}

default: {
return wrapped(customEventHandler, listener);
}
}
}

function EventClass(eventName) {
if (eventName === "error" || eventName === "messageerror") {
return ErrorEvent;
}

return MessageEvent;
}

// EventTarget dedupes on (type, callback), so in node the FIRST registration of
// a listener wins outright -- including its once-ness -- and later adds of the
// same function are no-ops. Keying wrappers per listener reproduces that.
function register(target, event, listener, wrapper, options) {
const map = registryFor(target, true)!;
let byListener = map.get(event);
if (!byListener) map.set(event, (byListener = new SafeMap()));
if (byListener.has(listener)) return false;
target.addEventListener(event, wrapper, options);
byListener.set(listener, wrapper);
return true;
}

function on(event, listener) {
register(this, event, listener, functionForEventType(event, listener), undefined);
return this;
}

function off(event, listener) {
if (listener) {
const byListener = registryFor(this, false)?.get(event);
const wrapper = byListener?.get(listener) ?? listener;
this.removeEventListener(event, wrapper);
byListener?.delete(listener);
} else {
this.removeEventListener(event);
}
return this;
}

function once(event, listener) {
const wrapper = functionForEventType(event, listener);
const target = this;
// EventTarget drops a {once:true} listener natively, without telling the
// registry — so purge it here or listenerCount()/eventNames() keep counting
// a listener that already fired.
function onceWrapper(ev) {
registryFor(target, false)?.get(event)?.delete(listener);
return wrapper(ev);
}
register(this, event, listener, onceWrapper, { once: true });
return this;
}

function emit(event, ...args) {
switch (event) {
case "error":
case "messageerror":
case "message":
this.dispatchEvent(new (EventClass(event))(event, ...args));
break;
default:
// Non-standard events surface as CustomEvent (detail = first arg) to
// addEventListener and as the raw argument to .on(), matching node.
this.dispatchEvent(new CustomEvent(event, { detail: args[0] }));
break;
}
return this;
}

const kMaxListeners = Symbol("kMaxListeners");
function setMaxListeners(n) {
this[kMaxListeners] = n;
return this;
}
function getMaxListeners() {
return this[kMaxListeners] ?? 10;
}
function listenerCount(type) {
return registryFor(this, false)?.get(type)?.size ?? 0;
}
function eventNames() {
const map = registryFor(this, false);
if (!map) return [];
const out: string[] = [];
for (const [k, v] of map) if (v.size > 0) out.push(k);
return out;
}
function removeAllListeners(type) {
const map = registryFor(this, false);
if (!map) return this;
const removeType = t => {
const byListener = map.get(t);
if (byListener) {
for (const w of byListener.values()) this.removeEventListener(t, w);
map.delete(t);
}
};
if (arguments.length === 0) {
// removeType only deletes `t`, and a Map iterator tolerates deleting the
// entry it just yielded — so no snapshot copy is needed here.
for (const t of map.keys()) removeType(t);
} else {
removeType(type);
}
return this;
}

// node inherits these from NodeEventTarget.prototype (a curated subset of
// EventEmitter, not EventEmitter itself); use an intermediate prototype so
// Object.getOwnPropertyNames(MessagePort.prototype) matches node.
const proto = Class.prototype;
const inherited = Object.create(Object.getPrototypeOf(proto));
const emitterMethods: [string, Function][] = [
["on", on],
["off", off],
["once", once],
["emit", emit],
["addListener", on],
["removeListener", off],
["listenerCount", listenerCount],
["eventNames", eventNames],
["removeAllListeners", removeAllListeners],
["setMaxListeners", setMaxListeners],
["getMaxListeners", getMaxListeners],
];
for (const [methodName, value] of emitterMethods) {
Object.defineProperty(inherited, methodName, { value, writable: true, enumerable: false, configurable: true });
}
Object.setPrototypeOf(proto, inherited);
}

const _MessagePort = globalThis.MessagePort;
injectFakeEmitter(_MessagePort);

const MessagePort = _MessagePort;

// node's close(cb) registers cb as a one-time "close" listener before the native close.
Expand Down Expand Up @@ -814,15 +631,28 @@ function fakeParentPort() {
value: self.removeEventListener.bind(self),
});
Comment thread
robobun marked this conversation as resolved.

Object.defineProperty(fake, "removeListener", {
value: self.removeEventListener.bind(self),
enumerable: false,
});

Object.defineProperty(fake, "addListener", {
value: self.addEventListener.bind(self),
enumerable: false,
});
// MessagePort.prototype.on/etc. require a real MessagePort receiver; forward to self.
function on(this: any, type: string, listener: any) {
self.addEventListener(type, listener, { $kIsNodeStyleListener: true } as AddEventListenerOptions);
return this;
}
Comment thread
robobun marked this conversation as resolved.
function once(this: any, type: string, listener: any) {
self.addEventListener(type, listener, { once: true, $kIsNodeStyleListener: true } as AddEventListenerOptions);
return this;
Comment thread
robobun marked this conversation as resolved.
}
Comment thread
robobun marked this conversation as resolved.
Comment thread
robobun marked this conversation as resolved.
function off(this: any, type: string, listener: any) {
self.removeEventListener(type, listener);
return this;
}
for (const [name, fn] of [
["on", on],
["addListener", on],
["once", once],
["off", off],
["removeListener", off],
] as const) {
Object.defineProperty(fake, name, { value: fn, enumerable: false, configurable: true, writable: true });
}

return fake;
}
Expand Down
5 changes: 5 additions & 0 deletions src/jsc/bindings/webcore/AddEventListenerOptions.h
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,11 @@ struct AddEventListenerOptions : EventListenerOptions {
// modules, mirroring Node.js's kResistStopPropagation: a listener registered
// with it still runs after another listener called stopImmediatePropagation().
bool resistStopPropagation { false };

// Not part of the DOM standard. Mirrors Node.js's kIsNodeStyleListener: when
// set, the listener is invoked with the event's underlying value (data/detail/
// error) instead of the Event wrapper. MessagePort.prototype.on/.once set it.
Comment thread
robobun marked this conversation as resolved.
bool isNodeStyleListener { false };
};

} // namespace WebCore
4 changes: 2 additions & 2 deletions src/jsc/bindings/webcore/EventFactory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -88,8 +88,8 @@ JSC::JSValue toJSNewlyCreated(JSC::JSGlobalObject*, JSDOMGlobalObject* globalObj
}
// case CompositionEventInterfaceType:
// return createWrapper<CompositionEvent>(globalObject, WTF::move(impl));
// case CustomEventInterfaceType:
// return createWrapper<CustomEvent>(globalObject, WTF::move(impl));
case CustomEventInterfaceType:
return createWrapper<CustomEvent>(globalObject, WTF::move(impl));
// #if ENABLE(DEVICE_ORIENTATION)
// case DeviceMotionEventInterfaceType:
// return createWrapper<DeviceMotionEvent>(globalObject, WTF::move(impl));
Expand Down
4 changes: 2 additions & 2 deletions src/jsc/bindings/webcore/EventHeaders.h
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,8 @@
#include "JSCloseEvent.h"
// #include "CompositionEvent.h"
// #include "JSCompositionEvent.h"
// #include "CustomEvent.h"
// #include "JSCustomEvent.h"
#include "CustomEvent.h"
#include "JSCustomEvent.h"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
// #if ENABLE(DEVICE_ORIENTATION)
// #include "DeviceMotionEvent.h"
// #include "JSDeviceMotionEvent.h"
Expand Down
3 changes: 3 additions & 0 deletions src/jsc/bindings/webcore/EventListener.h
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class EventListener : public RefCounted<EventListener>, public CanMakeWeakPtr<Ev
virtual ~EventListener() = default;
virtual bool operator==(const EventListener&) const = 0;
virtual void handleEvent(ScriptExecutionContext&, Event&) = 0;
// For listeners registered with kIsNodeStyleListener; JSEventListener
// overrides this to pass the event's carried value (.data/.detail/.error).
Comment thread
robobun marked this conversation as resolved.
virtual void handleEventNodeStyle(ScriptExecutionContext& ctx, Event& event) { handleEvent(ctx, event); }

virtual void visitJSFunction(JSC::AbstractSlotVisitor&) {}
virtual void visitJSFunction(JSC::SlotVisitor&) {}
Expand Down
17 changes: 17 additions & 0 deletions src/jsc/bindings/webcore/EventListenerMap.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,23 @@ bool EventListenerMap::remove(const AtomString& eventType, EventListener& listen
return false;
}

bool EventListenerMap::removeAll(const AtomString& eventType)
{
releaseAssertOrSetThreadUID();
Locker locker { m_lock };

for (unsigned i = 0; i < m_entries.size(); ++i) {
if (m_entries[i].first == eventType) {
for (auto& listener : m_entries[i].second)
listener->markAsRemoved();
m_entries.removeAt(i);
return true;
}
}

return false;
}

EventListenerVector* EventListenerMap::find(const AtomString& eventType)
{
for (auto& entry : m_entries) {
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/webcore/EventListenerMap.h
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ class EventListenerMap {
void replace(const AtomString& eventType, EventListener& oldListener, Ref<EventListener>&& newListener, const RegisteredEventListener::Options&);
RegisteredEventListener* add(const AtomString& eventType, Ref<EventListener>&&, const RegisteredEventListener::Options&);
bool remove(const AtomString& eventType, EventListener&, bool useCapture);
bool removeAll(const AtomString& eventType);
WEBCORE_EXPORT EventListenerVector* find(const AtomString& eventType);
const EventListenerVector* find(const AtomString& eventType) const { return const_cast<EventListenerMap*>(this)->find(eventType); }
Vector<AtomString> eventTypes() const;
Expand Down
19 changes: 17 additions & 2 deletions src/jsc/bindings/webcore/EventTarget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -103,7 +103,7 @@ bool EventTarget::addEventListener(const AtomString& eventType, Ref<EventListene
// if (!passive.has_value() && Quirks::shouldMakeEventListenerPassive(*this, eventType, listener.get()))
// passive = true;

auto* registeredListener = ensureEventTargetData().eventListenerMap.add(eventType, listener.copyRef(), { options.capture, passive.value_or(false), options.once, options.resistStopPropagation });
auto* registeredListener = ensureEventTargetData().eventListenerMap.add(eventType, listener.copyRef(), { options.capture, passive.value_or(false), options.once, options.resistStopPropagation, options.isNodeStyleListener });
if (!registeredListener)
return false;

Expand Down Expand Up @@ -340,7 +340,10 @@ void EventTarget::innerInvokeEventListeners(Event& event, EventListenerVector li
#endif

// InspectorInstrumentation::willHandleEvent(context, event, *registeredListener);
registeredListener->callback().handleEvent(context, event);
if (registeredListener->isNodeStyleListener()) [[unlikely]]
registeredListener->callback().handleEventNodeStyle(context, event);
else
registeredListener->callback().handleEvent(context, event);
// InspectorInstrumentation::didHandleEvent(context, event, *registeredListener);

if (registeredListener->isPassive())
Expand All @@ -366,6 +369,18 @@ const EventListenerVector& EventTarget::eventListeners(const AtomString& eventTy
return listenerVector ? *listenerVector : emptyVector.get();
}

void EventTarget::removeAllEventListenersForType(const AtomString& eventType)
{
auto* data = eventTargetData();
if (!data)
return;
if (data->eventListenerMap.removeAll(eventType)) {
if (this->onDidChangeListener) [[unlikely]]
this->onDidChangeListener(*this, eventType, OnDidChangeListenerKind::Clear);
eventListenersDidChange();
}
}

void EventTarget::removeAllEventListeners()
{
// auto& threadData = threadGlobalData();
Expand Down
1 change: 1 addition & 0 deletions src/jsc/bindings/webcore/EventTarget.h
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@ class EventTarget : public ScriptWrappable, public CanMakeWeakPtrWithBitField<Ev
WEBCORE_EXPORT virtual bool removeEventListener(const AtomString& eventType, EventListener&, const EventListenerOptions& = {});

WEBCORE_EXPORT virtual void removeAllEventListeners();
void removeAllEventListenersForType(const AtomString&);
WEBCORE_EXPORT virtual void dispatchEvent(Event&);
WEBCORE_EXPORT virtual void uncaughtExceptionInEventHandler();

Expand Down
6 changes: 6 additions & 0 deletions src/jsc/bindings/webcore/JSAddEventListenerOptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,12 @@ template<> AddEventListenerOptions convertDictionary<AddEventListenerOptions>(JS
result.resistStopPropagation = convert<IDLBoolean>(lexicalGlobalObject, resistStopPropagationValue);
RETURN_IF_EXCEPTION(throwScope, {});
}
JSValue isNodeStyleListenerValue = object->get(&lexicalGlobalObject, builtinNames(vm).kIsNodeStyleListenerPrivateName());
RETURN_IF_EXCEPTION(throwScope, {});
if (!isNodeStyleListenerValue.isUndefined()) {
result.isNodeStyleListener = convert<IDLBoolean>(lexicalGlobalObject, isNodeStyleListenerValue);
RETURN_IF_EXCEPTION(throwScope, {});
}
}
return result;
}
Expand Down
Loading
Loading