Skip to content
Merged
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
9 changes: 9 additions & 0 deletions src/js/builtins.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -468,6 +468,15 @@ declare interface UnderlyingSource {
$stream?: ReadableStream;
}

declare interface AddEventListenerOptions {
/**
* Private symbol read by the native EventTarget. A listener registered with it still
* runs after another listener called `stopImmediatePropagation()`. Mirrors Node.js's
* internal `kResistStopPropagation`.
*/
$kResistStopPropagation?: boolean;
}

declare class OutOfMemoryError {
constructor();
}
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(kResistStopPropagation) \
macro(key) \
macro(lazy) \
macro(lineText) \
Expand Down
7 changes: 4 additions & 3 deletions src/js/internal/abort_listener.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const { validateAbortSignal, validateFunction } = require("internal/validators");
const { kResistStopPropagation } = require("internal/shared");
const { resistStopPropagation } = require("internal/shared");

function addAbortListener(signal: AbortSignal, listener: EventListener): Disposable {
if (signal === undefined) {
Expand All @@ -13,16 +13,17 @@ function addAbortListener(signal: AbortSignal, listener: EventListener): Disposa
queueMicrotask(() => listener());
} else {
// TODO(atlowChemi) add { subscription: true } and return directly
signal.addEventListener("abort", listener, { once: true, [kResistStopPropagation]: true });
signal.addEventListener("abort", listener, resistStopPropagation({ __proto__: null, once: true }));
removeEventListener = () => {
signal.removeEventListener("abort", listener);
};
}
return {
__proto__: null,
[Symbol.dispose]() {
removeEventListener?.();
},
};
} as Disposable;
}

export default {
Expand Down
11 changes: 10 additions & 1 deletion src/js/internal/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,15 @@ function once(callback, { preserveReturnValue = false } = kEmptyObject) {

const kEmptyObject = ObjectFreeze(Object.create(null));

// Marks an addEventListener() options object so that dispatch still invokes the
// listener after an unrelated listener called event.stopImmediatePropagation().
// `$kResistStopPropagation` is a private symbol the native EventTarget reads, so
// only these internal modules can reach it.
function resistStopPropagation<T extends object>(options: T): T {
(options as AddEventListenerOptions).$kResistStopPropagation = true;
return options;
}

function getLazy<T>(initializer: () => T) {
let value: T;
let initialized = false;
Expand Down Expand Up @@ -321,6 +330,7 @@ export default {
ErrnoException,
once,
getLazy,
resistStopPropagation,

hasObserver,
startPerf,
Expand All @@ -332,7 +342,6 @@ export default {

kHandle: Symbol("kHandle"),
kAutoDestroyed: Symbol("kAutoDestroyed"),
kResistStopPropagation: Symbol("kResistStopPropagation"),
kWeakHandler: Symbol("kWeak"),
kGetNativeReadableProto: Symbol("kGetNativeReadableProto"),
kEmptyObject,
Expand Down
4 changes: 2 additions & 2 deletions src/js/internal/streams/operators.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use strict";

const { validateAbortSignal, validateFunction, validateInteger, validateObject } = require("internal/validators");
const { kWeakHandler, kResistStopPropagation } = require("internal/shared");
const { kWeakHandler, resistStopPropagation } = require("internal/shared");
const { finished } = require("internal/streams/end-of-stream");

const MathFloor = Math.floor;
Expand Down Expand Up @@ -233,7 +233,7 @@ async function reduce(reducer, initialValue, options) {
const ac = new AbortController();
const signal = ac.signal;
if (options?.signal) {
const opts = { once: true, [kWeakHandler]: this, [kResistStopPropagation]: true };
const opts = resistStopPropagation({ once: true, [kWeakHandler]: this });
options.signal.addEventListener("abort", () => ac.abort(), opts);
}
let gotAnyItemFromStream = false;
Expand Down
35 changes: 5 additions & 30 deletions src/js/node/events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const {
validateFunction,
validateString,
} = require("internal/validators");
const { addAbortListener } = require("internal/abort_listener");
const { resistStopPropagation } = require("internal/shared");

const types = require("node:util/types");
let inspect: typeof import("node:util").inspect | undefined;
Expand Down Expand Up @@ -574,7 +576,8 @@ async function once(emitter, type, options = kEmptyObject) {
}
resolve(args);
};
eventTargetAgnosticAddListener(emitter, type, resolver, { once: true });
const opts = resistStopPropagation({ __proto__: null, once: true });
eventTargetAgnosticAddListener(emitter, type, resolver, opts);
if (type !== "error" && typeof emitter.once === "function") {
// EventTarget does not have `error` event semantics like Node
// EventEmitters, we listen to `error` events only on EventEmitters.
Expand All @@ -586,7 +589,7 @@ async function once(emitter, type, options = kEmptyObject) {
reject($makeAbortError(undefined, { cause: signal?.reason }));
}
if (signal != null) {
eventTargetAgnosticAddListener(signal, "abort", abortListener, { once: true });
eventTargetAgnosticAddListener(signal, "abort", abortListener, opts);
}

return promise;
Expand Down Expand Up @@ -874,34 +877,6 @@ function getMaxListeners(emitterOrTarget) {
}
Object.defineProperty(getMaxListeners, "name", { value: "getMaxListeners" });

// Copy-pasta from Node.js source code
function addAbortListener(signal, listener) {
if (signal === undefined) {
throw $ERR_INVALID_ARG_TYPE("signal", "AbortSignal", signal);
}

validateAbortSignal(signal, "signal");
if (typeof listener !== "function") {
throw $ERR_INVALID_ARG_TYPE("listener", "function", listener);
}

let removeEventListener;
if (signal.aborted) {
queueMicrotask(() => listener());
} else {
signal.addEventListener("abort", listener, { __proto__: null, once: true });
removeEventListener = () => {
signal.removeEventListener("abort", listener);
};
}
return {
__proto__: null,
[Symbol.dispose]() {
removeEventListener?.();
},
};
}

let EventEmitterReferencingAsyncResource;
function lazyLoadAsyncResource() {
if (!AsyncResource) {
Expand Down
7 changes: 4 additions & 3 deletions src/js/node/timers.promises.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// https://github.com/niksy/isomorphic-timers-promises/blob/master/index.js

const { validateBoolean, validateAbortSignal, validateObject, validateNumber } = require("internal/validators");
const { resistStopPropagation } = require("internal/shared");

const symbolAsyncIterator = Symbol.asyncIterator;
const setImmediateGlobal = globalThis.setImmediate;
Expand Down Expand Up @@ -65,7 +66,7 @@ function setTimeout(after = 1, value, options = {}) {
clearTimeout(timeout);
reject($makeAbortError(undefined, { cause: signal.reason }));
};
signal.addEventListener("abort", onCancel);
signal.addEventListener("abort", onCancel, resistStopPropagation({ __proto__: null }));
}
});
return typeof onCancel !== "undefined"
Expand Down Expand Up @@ -104,7 +105,7 @@ function setImmediate(value, options = {}) {
clearImmediate(immediate);
reject($makeAbortError(undefined, { cause: signal.reason }));
};
signal.addEventListener("abort", onCancel);
signal.addEventListener("abort", onCancel, resistStopPropagation({ __proto__: null }));
}
});
return typeof onCancel !== "undefined"
Expand Down Expand Up @@ -187,7 +188,7 @@ function setInterval(after = 1, value, options = {}) {
callback = undefined;
}
};
signal.addEventListener("abort", onCancel);
signal.addEventListener("abort", onCancel, resistStopPropagation({ __proto__: null, once: true }));
}

return asyncIterator({
Expand Down
3 changes: 2 additions & 1 deletion src/js/node/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const types = require("node:util/types");
const utl = require("internal/util/inspect");
const { promisify } = require("internal/promisify");
const { validateString, validateOneOf, validateBoolean } = require("internal/validators");
const { resistStopPropagation } = require("internal/shared");
const { MIMEType, MIMEParams } = require("internal/util/mime");
const { deprecate } = require("internal/util/deprecate");

Expand Down Expand Up @@ -275,7 +276,7 @@ function aborted(signal: AbortSignal, resource: object) {
// Do not leak the current scope into the listener.
// Instead, create a new function.
unregisterToken,
{ once: true },
resistStopPropagation({ __proto__: null, once: true }),
);

if (!lazyAbortedRegistry) {
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 @@ -43,6 +43,11 @@ struct AddEventListenerOptions : EventListenerOptions {
std::optional<bool> passive;
bool once { false };
RefPtr<AbortSignal> signal;

// Not part of the DOM standard. Set through a private symbol by Bun's internal
// modules, mirroring Node.js's kResistStopPropagation: a listener registered
// with it still runs after another listener called stopImmediatePropagation().
bool resistStopPropagation { false };
};

} // namespace WebCore
11 changes: 6 additions & 5 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 });
auto* registeredListener = ensureEventTargetData().eventListenerMap.add(eventType, listener.copyRef(), { options.capture, passive.value_or(false), options.once, options.resistStopPropagation });
if (!registeredListener)
return false;

Expand Down Expand Up @@ -316,10 +316,11 @@ void EventTarget::innerInvokeEventListeners(Event& event, EventListenerVector li
// if (InspectorInstrumentation::isEventListenerDisabled(*this, event.type(), registeredListener->callback(), registeredListener->useCapture()))
// continue;

// If stopImmediatePropagation has been called, we just break out immediately, without
// handling any more events on this target.
if (event.immediatePropagationStopped())
break;
// If stopImmediatePropagation has been called, skip the remaining listeners. Listeners
// registered with resistStopPropagation still run: they are how internal modules attach
// teardown that unrelated code sharing the event target must not be able to suppress.
if (event.immediatePropagationStopped() && !registeredListener->resistsStopPropagation())
continue;

// 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,
Expand Down
11 changes: 11 additions & 0 deletions src/jsc/bindings/webcore/JSAddEventListenerOptions.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
#include "config.h"
#include "JSAddEventListenerOptions.h"

#include "BunClientData.h"
#include "JSAbortSignal.h"
#include "JSDOMConvertBoolean.h"
#include "JSDOMConvertInterface.h"
Expand Down Expand Up @@ -86,6 +87,16 @@ template<> AddEventListenerOptions convertDictionary<AddEventListenerOptions>(JS
result.signal = convert<IDLInterface<AbortSignal>>(lexicalGlobalObject, signalValue);
RETURN_IF_EXCEPTION(throwScope, {});
}
// Bun extension, keyed off a private symbol that only internal modules can
// reach. See AddEventListenerOptions::resistStopPropagation.
if (!isNullOrUndefined) {
JSValue resistStopPropagationValue = object->get(&lexicalGlobalObject, builtinNames(vm).kResistStopPropagationPrivateName());
RETURN_IF_EXCEPTION(throwScope, {});
if (!resistStopPropagationValue.isUndefined()) {
result.resistStopPropagation = convert<IDLBoolean>(lexicalGlobalObject, resistStopPropagationValue);
RETURN_IF_EXCEPTION(throwScope, {});
}
}
return result;
}

Expand Down
7 changes: 6 additions & 1 deletion src/jsc/bindings/webcore/RegisteredEventListener.h
Original file line number Diff line number Diff line change
Expand Up @@ -36,16 +36,18 @@ class WeakPtrImplWithEventTargetData;
class RegisteredEventListener : public RefCounted<RegisteredEventListener> {
public:
struct Options {
Options(bool capture = false, bool passive = false, bool once = false)
Options(bool capture = false, bool passive = false, bool once = false, bool resistStopPropagation = false)
: capture(capture)
, passive(passive)
, once(once)
, resistStopPropagation(resistStopPropagation)
{
}

bool capture;
bool passive;
bool once;
bool resistStopPropagation;
};

static Ref<RegisteredEventListener> create(Ref<EventListener>&& listener, const Options& options)
Expand All @@ -60,6 +62,7 @@ class RegisteredEventListener : public RefCounted<RegisteredEventListener> {
bool isPassive() const { return m_isPassive; }
bool isOnce() const { return m_isOnce; }
bool wasRemoved() const { return m_wasRemoved; }
bool resistsStopPropagation() const { return m_resistStopPropagation; }

void markAsRemoved();

Expand All @@ -77,6 +80,7 @@ class RegisteredEventListener : public RefCounted<RegisteredEventListener> {
, m_isPassive(options.passive)
, m_isOnce(options.once)
, m_wasRemoved(false)
, m_resistStopPropagation(options.resistStopPropagation)
, m_callback(WTF::move(listener))
{
}
Expand All @@ -85,6 +89,7 @@ class RegisteredEventListener : public RefCounted<RegisteredEventListener> {
bool m_isPassive : 1;
bool m_isOnce : 1;
bool m_wasRemoved : 1;
bool m_resistStopPropagation : 1;
uint32_t m_abortAlgorithmIdentifier { 0 };
Ref<EventListener> m_callback;
WeakPtr<AbortSignal, WeakPtrImplWithEventTargetData> m_abortSignal;
Expand Down
Loading
Loading