Skip to content
Open
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
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
278 changes: 85 additions & 193 deletions src/js/node/worker_threads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,189 +101,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 @@ -810,19 +628,93 @@ function fakeParentPort() {
value: self.addEventListener.bind(self),
});

// MessagePort.prototype.on/etc. require a real MessagePort receiver; shadow
// them so calls on this stand-in forward to the global scope. A local map
// tracks which listeners were added via parentPort so listenerCount/
// eventNames/removeAllListeners see only those (self may carry internal
// listeners the user must not touch).
Comment thread
robobun marked this conversation as resolved.
const byType = new SafeMap();
Object.defineProperty(fake, "removeEventListener", {
value: self.removeEventListener.bind(self),
});

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

Object.defineProperty(fake, "addListener", {
value: self.addEventListener.bind(self),
enumerable: false,
value(type: string, listener: any, options?: any) {
const set = byType.get(type);
self.removeEventListener(type, set?.get(listener) ?? listener, options);
set?.delete(listener);
},
});
Comment thread
robobun marked this conversation as resolved.
function track(type: string, listener: any, registered: any) {
let set = byType.get(type);
if (!set) byType.set(type, (set = new SafeMap()));
// First registration of a given listener wins (including its once-ness),
// matching node's NodeEventTarget dedup.
Comment thread
robobun marked this conversation as resolved.
if (set.has(listener)) return false;
set.set(listener, registered);
return true;
}
function on(this: any, type: string, listener: any) {
if (track(type, listener, listener))
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) {
const wrapper = (arg: any) => {
byType.get(type)?.delete(listener);
return listener(arg);
};
if (track(type, listener, wrapper))
self.addEventListener(type, wrapper, { 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) {
const set = byType.get(type);
self.removeEventListener(type, set?.get(listener) ?? listener);
set?.delete(listener);
return this;
}
function listenerCount(type: string) {
return byType.get(type)?.size ?? 0;
}
function eventNames() {
const out: string[] = [];
for (const [k, v] of byType) if (v.size > 0) out.push(k);
return out;
}
function removeAllListeners(this: any, type?: string) {
const clear = (t: string) => {
const set = byType.get(t);
if (set) {
for (const registered of set.values()) self.removeEventListener(t, registered);
byType.delete(t);
}
};
if (arguments.length === 0) for (const t of byType.keys()) clear(t);
else clear(type!);
Comment thread
robobun marked this conversation as resolved.
return this;
}
function emit(type: string, arg?: any) {
const had = (byType.get(type)?.size ?? 0) > 0;
const ev =
type === "message" || type === "messageerror"
? new MessageEvent(type, { data: arg })
: new CustomEvent(type, { detail: arg });
self.dispatchEvent(ev);
return had;
}
let maxListeners = 10;
for (const [name, fn] of [
["on", on],
["addListener", on],
["once", once],
["off", off],
["removeListener", off],
["listenerCount", listenerCount],
["eventNames", eventNames],
["removeAllListeners", removeAllListeners],
["emit", emit],
["getMaxListeners", () => maxListeners],
["setMaxListeners", n => ((maxListeners = n), fake)],
] 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
Loading
Loading