From 636e188c839a4a5b76786895c91230e9bdd4fda5 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Wed, 16 Jul 2025 12:59:07 +0900 Subject: [PATCH 01/18] wrap Event and EventTarget --- ext/node/polyfills/_events.mjs | 11 +- ext/web/02_event.js | 1405 ++++++-------------------------- ext/web/03_abort_signal.js | 10 +- ext/web/15_performance.js | 12 +- ext/web/event.rs | 1188 +++++++++++++++++++++++++++ ext/web/lib.rs | 13 + runtime/js/99_main.js | 8 +- 7 files changed, 1450 insertions(+), 1197 deletions(-) create mode 100644 ext/web/event.rs diff --git a/ext/node/polyfills/_events.mjs b/ext/node/polyfills/_events.mjs index e854fc1402..750657448d 100644 --- a/ext/node/polyfills/_events.mjs +++ b/ext/node/polyfills/_events.mjs @@ -72,10 +72,7 @@ import { } from "ext:deno_node/internal/validators.mjs"; import { spliceOne } from "ext:deno_node/_utils.ts"; import { nextTick } from "ext:deno_node/_process/process.ts"; -import { - eventTargetData, - kResistStopImmediatePropagation, -} from "ext:deno_web/02_event.js"; +import { getListeners } from "ext:deno_web/02_event.js"; export { addAbortListener } from "./internal/events/abort_listener.mjs"; @@ -867,9 +864,7 @@ export function getEventListeners(emitterOrTarget, type) { return emitterOrTarget.listeners(type); } if (emitterOrTarget instanceof EventTarget) { - return emitterOrTarget[eventTargetData]?.listeners?.[type]?.map(( - listener, - ) => listener.callback) || []; + return getListeners(emitterOrTarget, type); } throw new ERR_INVALID_ARG_TYPE( "emitter", @@ -934,7 +929,7 @@ export async function once(emitter, name, options = kEmptyObject) { signal, "abort", abortListener, - { once: true, [kResistStopImmediatePropagation]: true }, + { once: true, [SymbolFor("Deno.stopImmediatePropagation")]: true }, ); } }); diff --git a/ext/web/02_event.js b/ext/web/02_event.js index 508ba0f868..49f3a530fa 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -7,21 +7,13 @@ import { core, primordials } from "ext:core/mod.js"; const { - ArrayPrototypeIncludes, - ArrayPrototypeIndexOf, - ArrayPrototypeMap, - ArrayPrototypePush, - ArrayPrototypeSlice, - ArrayPrototypeSplice, - ArrayPrototypeUnshift, - Boolean, + ArrayPrototypeFlat, Error, FunctionPrototypeCall, MapPrototypeGet, MapPrototypeSet, - ObjectCreate, ObjectDefineProperty, - ObjectGetOwnPropertyDescriptor, + ObjectDefineProperties, ObjectPrototypeIsPrototypeOf, ReflectDefineProperty, SafeArrayIterator, @@ -29,12 +21,21 @@ const { StringPrototypeStartsWith, Symbol, SymbolFor, - SymbolToStringTag, TypeError, } = primordials; - +import { + CloseEvent, + ErrorEvent, + Event, + EventTarget, + op_event_dispatch, + op_event_get_target_listener_count, + op_event_get_target_listeners, + op_event_set_is_trusted, + op_event_set_target, + op_event_wrap_event_target, +} from "ext:core/ops"; import * as webidl from "ext:deno_webidl/00_webidl.js"; -import { DOMException } from "./01_dom_exception.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; // This should be set via setGlobalThis this is required so that if even @@ -45,1180 +46,254 @@ function saveGlobalThisReference(val) { globalThis_ = val; } -// accessors for non runtime visible data - -function getDispatched(event) { - return Boolean(event[_dispatched]); -} - -function getPath(event) { - return event[_path] ?? []; -} - -function getStopImmediatePropagation(event) { - return Boolean(event[_stopImmediatePropagationFlag]); -} - -function setCurrentTarget( - event, - value, -) { - event[_attributes].currentTarget = value; -} - -function setIsTrusted(event, value) { - event[_isTrusted] = value; -} - -function setDispatched(event, value) { - event[_dispatched] = value; -} - -function setEventPhase(event, value) { - event[_attributes].eventPhase = value; -} - -function setInPassiveListener(event, value) { - event[_inPassiveListener] = value; -} - -function setPath(event, value) { - event[_path] = value; -} - -function setRelatedTarget( - event, - value, -) { - event[_attributes].relatedTarget = value; -} - -function setTarget(event, value) { - event[_attributes].target = value; -} - -function setStopImmediatePropagation( - event, - value, -) { - event[_stopImmediatePropagationFlag] = value; -} - -const isTrusted = ObjectGetOwnPropertyDescriptor({ - get isTrusted() { - return this[_isTrusted]; - }, -}, "isTrusted").get; - -const _attributes = Symbol("[[attributes]]"); -const _canceledFlag = Symbol("[[canceledFlag]]"); -const _stopPropagationFlag = Symbol("[[stopPropagationFlag]]"); -const _stopImmediatePropagationFlag = Symbol( - "[[stopImmediatePropagationFlag]]", -); -const _inPassiveListener = Symbol("[[inPassiveListener]]"); -const _dispatched = Symbol("[[dispatched]]"); -const _isTrusted = Symbol("[[isTrusted]]"); -const _path = Symbol("[[path]]"); - -class Event { - constructor(type, eventInitDict = { __proto__: null }) { - // TODO(lucacasonato): remove when this interface is spec aligned - this[SymbolToStringTag] = "Event"; - this[_canceledFlag] = false; - this[_stopPropagationFlag] = false; - this[_stopImmediatePropagationFlag] = false; - this[_inPassiveListener] = false; - this[_dispatched] = false; - this[_isTrusted] = false; - this[_path] = []; - - webidl.requiredArguments( - arguments.length, - 1, - "Failed to construct 'Event'", - ); - type = webidl.converters.DOMString( - type, - "Failed to construct 'Event'", - "Argument 1", - ); - - this[_attributes] = { - type, - bubbles: !!eventInitDict.bubbles, - cancelable: !!eventInitDict.cancelable, - composed: !!eventInitDict.composed, - currentTarget: null, - eventPhase: Event.NONE, - target: null, - timeStamp: 0, - }; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(EventPrototype, this), - keys: EVENT_PROPS, - }), - inspectOptions, - ); - } - - get type() { - return this[_attributes].type; - } - - get target() { - return this[_attributes].target; - } - - get srcElement() { - return null; - } - - set srcElement(_) { - // this member is deprecated - } - - get currentTarget() { - return this[_attributes].currentTarget; - } - - composedPath() { - const path = this[_path]; - if (path.length === 0) { - return []; - } - - if (!this.currentTarget) { - throw new Error("assertion error"); - } - const composedPath = [ - { - item: this.currentTarget, - itemInShadowTree: false, - relatedTarget: null, - rootOfClosedTree: false, - slotInClosedTree: false, - target: null, - touchTargetList: [], - }, - ]; - - let currentTargetIndex = 0; - let currentTargetHiddenSubtreeLevel = 0; - - for (let index = path.length - 1; index >= 0; index--) { - const { item, rootOfClosedTree, slotInClosedTree } = path[index]; - - if (rootOfClosedTree) { - currentTargetHiddenSubtreeLevel++; - } - - if (item === this.currentTarget) { - currentTargetIndex = index; - break; - } - - if (slotInClosedTree) { - currentTargetHiddenSubtreeLevel--; - } - } - - let currentHiddenLevel = currentTargetHiddenSubtreeLevel; - let maxHiddenLevel = currentTargetHiddenSubtreeLevel; - - for (let i = currentTargetIndex - 1; i >= 0; i--) { - const { item, rootOfClosedTree, slotInClosedTree } = path[i]; - - if (rootOfClosedTree) { - currentHiddenLevel++; - } - - if (currentHiddenLevel <= maxHiddenLevel) { - ArrayPrototypeUnshift(composedPath, { - item, - itemInShadowTree: false, - relatedTarget: null, - rootOfClosedTree: false, - slotInClosedTree: false, - target: null, - touchTargetList: [], - }); - } - - if (slotInClosedTree) { - currentHiddenLevel--; - - if (currentHiddenLevel < maxHiddenLevel) { - maxHiddenLevel = currentHiddenLevel; - } - } - } - - currentHiddenLevel = currentTargetHiddenSubtreeLevel; - maxHiddenLevel = currentTargetHiddenSubtreeLevel; - - for (let index = currentTargetIndex + 1; index < path.length; index++) { - const { item, rootOfClosedTree, slotInClosedTree } = path[index]; - - if (slotInClosedTree) { - currentHiddenLevel++; - } - - if (currentHiddenLevel <= maxHiddenLevel) { - ArrayPrototypePush(composedPath, { - item, - itemInShadowTree: false, - relatedTarget: null, - rootOfClosedTree: false, - slotInClosedTree: false, - target: null, - touchTargetList: [], - }); - } - - if (rootOfClosedTree) { - currentHiddenLevel--; - - if (currentHiddenLevel < maxHiddenLevel) { - maxHiddenLevel = currentHiddenLevel; - } - } - } - return ArrayPrototypeMap(composedPath, (p) => p.item); - } - - get NONE() { - return Event.NONE; - } - - get CAPTURING_PHASE() { - return Event.CAPTURING_PHASE; - } - - get AT_TARGET() { - return Event.AT_TARGET; - } - - get BUBBLING_PHASE() { - return Event.BUBBLING_PHASE; - } - - get eventPhase() { - return this[_attributes].eventPhase; - } - - stopPropagation() { - this[_stopPropagationFlag] = true; - } - - get cancelBubble() { - return this[_stopPropagationFlag]; - } - - set cancelBubble(value) { - this[_stopPropagationFlag] = webidl.converters.boolean(value); - } - - stopImmediatePropagation() { - this[_stopPropagationFlag] = true; - this[_stopImmediatePropagationFlag] = true; - } - - get bubbles() { - return this[_attributes].bubbles; - } - - get cancelable() { - return this[_attributes].cancelable; - } - - get returnValue() { - return !this[_canceledFlag]; - } - - set returnValue(value) { - if (!webidl.converters.boolean(value)) { - this[_canceledFlag] = true; - } - } - - preventDefault() { - if (this[_attributes].cancelable && !this[_inPassiveListener]) { - this[_canceledFlag] = true; - } - } - - get defaultPrevented() { - return this[_canceledFlag]; - } - - get composed() { - return this[_attributes].composed; - } - - get initialized() { - return true; - } - - get timeStamp() { - return this[_attributes].timeStamp; - } -} - -ObjectDefineProperty(Event, "NONE", { - __proto__: null, - value: 0, - writable: false, - enumerable: true, - configurable: false, -}); - -ObjectDefineProperty(Event, "CAPTURING_PHASE", { - __proto__: null, - value: 1, - writable: false, - enumerable: true, - configurable: false, -}); - -ObjectDefineProperty(Event, "AT_TARGET", { - __proto__: null, - value: 2, - writable: false, - enumerable: true, - configurable: false, -}); - -ObjectDefineProperty(Event, "BUBBLING_PHASE", { - __proto__: null, - value: 3, - writable: false, - enumerable: true, - configurable: false, -}); - -const EventPrototype = Event.prototype; - -// Not spec compliant. The spec defines it as [LegacyUnforgeable] -// but doing so has a big performance hit -ReflectDefineProperty(Event.prototype, "isTrusted", { - __proto__: null, - enumerable: true, - get: isTrusted, -}); - -function defineEnumerableProps( - Ctor, - props, -) { +function defineEnumerableProps(prototype, props) { for (let i = 0; i < props.length; ++i) { const prop = props[i]; - ReflectDefineProperty(Ctor.prototype, prop, { + ReflectDefineProperty(prototype, prop, { __proto__: null, enumerable: true, }); } } +// accessors for non runtime visible data + +/** + * @param {Event} event + * @param {boolean} value + */ +function setIsTrusted(event, value) { + op_event_set_is_trusted(event, value); +} + +/** + * @param {Event} event + * @param {object} value + */ +function setTarget(event, value) { + op_event_set_target(event, value); +} + +ObjectDefineProperties(Event, { + NONE: { + __proto__: null, + value: 0, + writable: false, + enumerable: true, + configurable: false, + }, + CAPTURING_PHASE: { + __proto__: null, + value: 1, + writable: false, + enumerable: true, + configurable: false, + }, + AT_TARGET: { + __proto__: null, + value: 2, + writable: false, + enumerable: true, + configurable: false, + }, + BUBBLING_PHASE: { + __proto__: null, + value: 3, + writable: false, + enumerable: true, + configurable: false, + }, +}); + +webidl.configureInterface(Event); +const EventPrototype = Event.prototype; + +ObjectDefineProperties(Event.prototype, { + NONE: { + __proto__: null, + value: 0, + writable: false, + enumerable: true, + configurable: false, + }, + CAPTURING_PHASE: { + __proto__: null, + value: 1, + writable: false, + enumerable: true, + configurable: false, + }, + AT_TARGET: { + __proto__: null, + value: 2, + writable: false, + enumerable: true, + configurable: false, + }, + BUBBLING_PHASE: { + __proto__: null, + value: 3, + writable: false, + enumerable: true, + configurable: false, + }, + [SymbolFor("Deno.privateCustomInspect")]: { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(EventPrototype, this), + keys: EVENT_PROPS, + }), + inspectOptions, + ); + }, + }, +}); + const EVENT_PROPS = [ + "type", + "target", + "currentTarget", + "eventPhase", "bubbles", "cancelable", - "composed", - "currentTarget", "defaultPrevented", - "eventPhase", - "srcElement", - "target", - "returnValue", + "composed", "timeStamp", - "type", + "srcElement", + "returnValue", + "cancelBubble", + // Not spec compliant. The spec defines it as [LegacyUnforgeable] + // but doing so has a big performance hit + "isTrusted", ]; -defineEnumerableProps(Event, EVENT_PROPS); - -// This is currently the only node type we are using, so instead of implementing -// the whole of the Node interface at the moment, this just gives us the one -// value to power the standards based logic -const DOCUMENT_FRAGMENT_NODE = 11; - -// DOM Logic Helper functions and type guards - -/** Get the parent node, for event targets that have a parent. - * - * Ref: https://dom.spec.whatwg.org/#get-the-parent */ -function getParent(eventTarget) { - return isNode(eventTarget) ? eventTarget.parentNode : null; -} - -function getRoot(eventTarget) { - return isNode(eventTarget) - ? eventTarget.getRootNode({ composed: true }) - : null; -} - -function isNode( - eventTarget, -) { - return eventTarget?.nodeType !== undefined; -} - -// https://dom.spec.whatwg.org/#concept-shadow-including-inclusive-ancestor -function isShadowInclusiveAncestor( - ancestor, - node, -) { - while (isNode(node)) { - if (node === ancestor) { - return true; - } - - if (isShadowRoot(node)) { - node = node && getHost(node); - } else { - node = getParent(node); - } - } - - return false; -} - -function isShadowRoot(nodeImpl) { - return Boolean( - nodeImpl && - isNode(nodeImpl) && - nodeImpl.nodeType === DOCUMENT_FRAGMENT_NODE && - getHost(nodeImpl) != null, - ); -} - -function isSlottable( - /* nodeImpl, */ -) { - // TODO(marcosc90) currently there aren't any slottables nodes - // https://dom.spec.whatwg.org/#concept-slotable - // return isNode(nodeImpl) && ReflectHas(nodeImpl, "assignedSlot"); - return false; -} - -// DOM Logic functions - -/** Append a path item to an event's path. - * - * Ref: https://dom.spec.whatwg.org/#concept-event-path-append - */ -function appendToEventPath( - eventImpl, - target, - targetOverride, - relatedTarget, - touchTargets, - slotInClosedTree, -) { - const itemInShadowTree = isNode(target) && isShadowRoot(getRoot(target)); - const rootOfClosedTree = isShadowRoot(target) && - getMode(target) === "closed"; - - ArrayPrototypePush(getPath(eventImpl), { - item: target, - itemInShadowTree, - target: targetOverride, - relatedTarget, - touchTargetList: touchTargets, - rootOfClosedTree, - slotInClosedTree, - }); -} - -function dispatch( - targetImpl, - eventImpl, - targetOverride, -) { - let clearTargets = false; - let activationTarget = null; - - setDispatched(eventImpl, true); - - targetOverride = targetOverride ?? targetImpl; - const eventRelatedTarget = eventImpl.relatedTarget; - let relatedTarget = retarget(eventRelatedTarget, targetImpl); - - if (targetImpl !== relatedTarget || targetImpl === eventRelatedTarget) { - const touchTargets = []; - - appendToEventPath( - eventImpl, - targetImpl, - targetOverride, - relatedTarget, - touchTargets, - false, - ); - - const isActivationEvent = eventImpl.type === "click"; - - if (isActivationEvent && getHasActivationBehavior(targetImpl)) { - activationTarget = targetImpl; - } - - let slotInClosedTree = false; - let slottable = isSlottable(targetImpl) && getAssignedSlot(targetImpl) - ? targetImpl - : null; - let parent = getParent(targetImpl); - - // Populate event path - // https://dom.spec.whatwg.org/#event-path - while (parent !== null) { - if (slottable !== null) { - slottable = null; - - const parentRoot = getRoot(parent); - if ( - isShadowRoot(parentRoot) && - parentRoot && - getMode(parentRoot) === "closed" - ) { - slotInClosedTree = true; - } - } - - relatedTarget = retarget(eventRelatedTarget, parent); - - if ( - isNode(parent) && - isShadowInclusiveAncestor(getRoot(targetImpl), parent) - ) { - appendToEventPath( - eventImpl, - parent, - null, - relatedTarget, - touchTargets, - slotInClosedTree, - ); - } else if (parent === relatedTarget) { - parent = null; - } else { - targetImpl = parent; - - if ( - isActivationEvent && - activationTarget === null && - getHasActivationBehavior(targetImpl) - ) { - activationTarget = targetImpl; - } - - appendToEventPath( - eventImpl, - parent, - targetImpl, - relatedTarget, - touchTargets, - slotInClosedTree, - ); - } - - if (parent !== null) { - parent = getParent(parent); - } - - slotInClosedTree = false; - } - - let clearTargetsTupleIndex = -1; - const path = getPath(eventImpl); - for ( - let i = path.length - 1; - i >= 0 && clearTargetsTupleIndex === -1; - i-- - ) { - if (path[i].target !== null) { - clearTargetsTupleIndex = i; - } - } - const clearTargetsTuple = path[clearTargetsTupleIndex]; - - clearTargets = (isNode(clearTargetsTuple.target) && - isShadowRoot(getRoot(clearTargetsTuple.target))) || - (isNode(clearTargetsTuple.relatedTarget) && - isShadowRoot(getRoot(clearTargetsTuple.relatedTarget))); - - setEventPhase(eventImpl, Event.CAPTURING_PHASE); - - for (let i = path.length - 1; i >= 0; --i) { - const tuple = path[i]; - - if (tuple.target === null) { - invokeEventListeners(tuple, eventImpl); - } - } - - for (let i = 0; i < path.length; i++) { - const tuple = path[i]; - - if (tuple.target !== null) { - setEventPhase(eventImpl, Event.AT_TARGET); - } else { - setEventPhase(eventImpl, Event.BUBBLING_PHASE); - } - - if ( - (eventImpl.eventPhase === Event.BUBBLING_PHASE && - eventImpl.bubbles) || - eventImpl.eventPhase === Event.AT_TARGET - ) { - invokeEventListeners(tuple, eventImpl); - } - } - } - - setEventPhase(eventImpl, Event.NONE); - setCurrentTarget(eventImpl, null); - setPath(eventImpl, []); - setDispatched(eventImpl, false); - eventImpl.cancelBubble = false; - setStopImmediatePropagation(eventImpl, false); - - if (clearTargets) { - setTarget(eventImpl, null); - setRelatedTarget(eventImpl, null); - } - - // TODO(bartlomieju): invoke activation targets if HTML nodes will be implemented - // if (activationTarget !== null) { - // if (!eventImpl.defaultPrevented) { - // activationTarget._activationBehavior(); - // } - // } - - return !eventImpl.defaultPrevented; -} - -/** Inner invoking of the event listeners where the resolved listeners are - * called. - * - * Ref: https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke */ -function innerInvokeEventListeners( - eventImpl, - targetListeners, -) { - let found = false; - - const { type } = eventImpl; - - if (!targetListeners || !targetListeners[type]) { - return found; - } - - let handlers = targetListeners[type]; - const handlersLength = handlers.length; - - // Copy event listeners before iterating since the list can be modified during the iteration. - if (handlersLength > 1) { - handlers = ArrayPrototypeSlice(targetListeners[type]); - } - - for (let i = 0; i < handlersLength; i++) { - const listener = handlers[i]; - - if ( - getStopImmediatePropagation(eventImpl) && - !listener.options[kResistStopImmediatePropagation] - ) { - continue; - } - - let capture, once, passive; - if (typeof listener.options === "boolean") { - capture = listener.options; - once = false; - passive = false; - } else { - capture = listener.options.capture; - once = listener.options.once; - passive = listener.options.passive; - } - - // Check if the event listener has been removed since the listeners has been cloned. - if (!ArrayPrototypeIncludes(targetListeners[type], listener)) { - continue; - } - - found = true; - - if ( - (eventImpl.eventPhase === Event.CAPTURING_PHASE && !capture) || - (eventImpl.eventPhase === Event.BUBBLING_PHASE && capture) - ) { - continue; - } - - if (once) { - ArrayPrototypeSplice( - targetListeners[type], - ArrayPrototypeIndexOf(targetListeners[type], listener), - 1, - ); - } - - if (passive) { - setInPassiveListener(eventImpl, true); - } - - if (typeof listener.callback === "object") { - if (typeof listener.callback.handleEvent === "function") { - listener.callback.handleEvent(eventImpl); - } - } else { - FunctionPrototypeCall( - listener.callback, - eventImpl.currentTarget, - eventImpl, - ); - } - - setInPassiveListener(eventImpl, false); - } - - return found; -} - -/** Invokes the listeners on a given event path with the supplied event. - * - * Ref: https://dom.spec.whatwg.org/#concept-event-listener-invoke */ -function invokeEventListeners(tuple, eventImpl) { - const path = getPath(eventImpl); - if (path.length === 1) { - const t = path[0]; - if (t.target) { - setTarget(eventImpl, t.target); - } - } else { - const tupleIndex = ArrayPrototypeIndexOf(path, tuple); - for (let i = tupleIndex; i >= 0; i--) { - const t = path[i]; - if (t.target) { - setTarget(eventImpl, t.target); - break; - } - } - } - - setRelatedTarget(eventImpl, tuple.relatedTarget); - - if (eventImpl.cancelBubble) { - return; - } - - setCurrentTarget(eventImpl, tuple.item); - - try { - innerInvokeEventListeners(eventImpl, getListeners(tuple.item)); - } catch (error) { - reportException(error); - } -} - -function normalizeEventHandlerOptions( - options, -) { - if (typeof options === "boolean" || typeof options === "undefined") { - return { - capture: Boolean(options), - }; - } else { - return options; - } -} - -/** Retarget the target following the spec logic. - * - * Ref: https://dom.spec.whatwg.org/#retarget */ -function retarget(a, b) { - while (true) { - if (!isNode(a)) { - return a; - } - - const aRoot = a.getRootNode(); - - if (aRoot) { - if ( - !isShadowRoot(aRoot) || - (isNode(b) && isShadowInclusiveAncestor(aRoot, b)) - ) { - return a; - } - - a = getHost(aRoot); - } - } -} +defineEnumerableProps(Event.prototype, EVENT_PROPS); // Accessors for non-public data -export const eventTargetData = Symbol(); -export const kResistStopImmediatePropagation = Symbol( - "kResistStopImmediatePropagation", -); - +/** + * @param {object} target + */ function setEventTargetData(target) { - target[eventTargetData] = getDefaultTargetData(); + op_event_wrap_event_target(target); } -function getAssignedSlot(target) { - return Boolean(target?.[eventTargetData]?.assignedSlot); +/** + * @param {EventTarget} target + * @param {Event} event + * @param {object=} targetOverride + */ +function dispatch(target, event, targetOverride) { + op_event_dispatch(target, event, targetOverride); } -function getHasActivationBehavior(target) { - return Boolean(target?.[eventTargetData]?.hasActivationBehavior); +/** + * @param {EventTarget} target + * @param {string} type + * @return {EventListenerOrEventListenerObject[]} + */ +function getListeners(target, type) { + return op_event_get_target_listeners(target, type); } -function getHost(target) { - return target?.[eventTargetData]?.host ?? null; -} - -function getListeners(target) { - return target?.[eventTargetData]?.listeners ?? {}; -} - -function getMode(target) { - return target?.[eventTargetData]?.mode ?? null; -} - -function listenerCount(target, type) { - return getListeners(target)?.[type]?.length ?? 0; -} - -function getDefaultTargetData() { - return { - assignedSlot: false, - hasActivationBehavior: false, - host: null, - listeners: ObjectCreate(null), - mode: "", - }; -} - -function addEventListenerOptionsConverter(V, prefix) { - if (webidl.type(V) !== "Object") { - return { capture: !!V, once: false, passive: false }; - } - - const options = { - capture: !!V.capture, - once: !!V.once, - passive: !!V.passive, - // This field exists for simulating Node.js behavior, implemented in https://github.com/nodejs/node/commit/bcd35c334ec75402ee081f1c4da128c339f70c24 - // Some internal event listeners in Node.js can ignore `e.stopImmediatePropagation()` calls - // from the earlier event listeners. - [kResistStopImmediatePropagation]: !!V[kResistStopImmediatePropagation], - }; - - const signal = V.signal; - if (signal !== undefined) { - options.signal = webidl.converters.AbortSignal( - signal, - prefix, - "'signal' of 'AddEventListenerOptions' (Argument 3)", - ); - } - - return options; -} - -class EventTarget { - constructor() { - this[eventTargetData] = getDefaultTargetData(); - this[webidl.brand] = webidl.brand; - } - - addEventListener( - type, - callback, - options, - ) { - const self = this ?? globalThis_; - webidl.assertBranded(self, EventTargetPrototype); - const prefix = "Failed to execute 'addEventListener' on 'EventTarget'"; - - webidl.requiredArguments(arguments.length, 2, prefix); - - options = addEventListenerOptionsConverter(options, prefix); - - if (callback === null) { - return; - } - - const { listeners } = self[eventTargetData]; - - if (!listeners[type]) { - listeners[type] = []; - } - - const listenerList = listeners[type]; - for (let i = 0; i < listenerList.length; ++i) { - const listener = listenerList[i]; - if ( - ((typeof listener.options === "boolean" && - listener.options === options.capture) || - (typeof listener.options === "object" && - listener.options.capture === options.capture)) && - listener.callback === callback - ) { - return; - } - } - if (options?.signal) { - const signal = options?.signal; - if (signal.aborted) { - // If signal is not null and its aborted flag is set, then return. - return; - } else { - // If listener's signal is not null, then add the following abort - // abort steps to it: Remove an event listener. - signal.addEventListener("abort", () => { - self.removeEventListener(type, callback, options); - }); - } - } - - ArrayPrototypePush(listeners[type], { callback, options }); - } - - removeEventListener( - type, - callback, - options, - ) { - const self = this ?? globalThis_; - webidl.assertBranded(self, EventTargetPrototype); - webidl.requiredArguments( - arguments.length, - 2, - "Failed to execute 'removeEventListener' on 'EventTarget'", - ); - - const { listeners } = self[eventTargetData]; - if (callback === null || !listeners[type]) { - return; - } - - options = normalizeEventHandlerOptions(options); - - for (let i = 0; i < listeners[type].length; ++i) { - const listener = listeners[type][i]; - if ( - ((typeof listener.options === "boolean" && - listener.options === options.capture) || - (typeof listener.options === "object" && - listener.options.capture === options.capture)) && - listener.callback === callback - ) { - ArrayPrototypeSplice(listeners[type], i, 1); - break; - } - } - } - - dispatchEvent(event) { - // If `this` is not present, then fallback to global scope. We don't use - // `globalThis` directly here, because it could be deleted by user. - // Instead use saved reference to global scope when the script was - // executed. - const self = this ?? globalThis_; - webidl.assertBranded(self, EventTargetPrototype); - webidl.requiredArguments( - arguments.length, - 1, - "Failed to execute 'dispatchEvent' on 'EventTarget'", - ); - - // This is an optimization to avoid creating an event listener - // on each startup. - // Stores the flag for checking whether unload is dispatched or not. - // This prevents the recursive dispatches of unload events. - // See https://github.com/denoland/deno/issues/9201. - if (event.type === "unload" && self === globalThis_) { - globalThis_[SymbolFor("Deno.isUnloadDispatched")] = true; - } - - const { listeners } = self[eventTargetData]; - if (!listeners[event.type]) { - setTarget(event, this); - return true; - } - - if (getDispatched(event)) { - throw new DOMException("Invalid event state", "InvalidStateError"); - } - - if (event.eventPhase !== Event.NONE) { - throw new DOMException("Invalid event state", "InvalidStateError"); - } - - return dispatch(self, event); - } - - getParent(_event) { - return null; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return `${this.constructor.name} ${inspect({}, inspectOptions)}`; - } +/** + * @param {EventTarget} target + * @param {string} type + * @returns {number} + */ +function getListenerCount(target, type) { + return op_event_get_target_listener_count(target, type); } webidl.configureInterface(EventTarget); const EventTargetPrototype = EventTarget.prototype; -defineEnumerableProps(EventTarget, [ +ObjectDefineProperty( + EventTarget.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return `${this.constructor.name} ${inspect({}, inspectOptions)}`; + }, + }, +); + +defineEnumerableProps(EventTarget.prototype, [ "addEventListener", "removeEventListener", "dispatchEvent", ]); -class ErrorEvent extends Event { - #message = ""; - #filename = ""; - #lineno = ""; - #colno = ""; - #error = ""; - - get message() { - return this.#message; - } - get filename() { - return this.#filename; - } - get lineno() { - return this.#lineno; - } - get colno() { - return this.#colno; - } - get error() { - return this.#error; - } - - constructor( - type, - { - bubbles, - cancelable, - composed, - message = "", - filename = "", - lineno = 0, - colno = 0, - error, - } = { __proto__: null }, - ) { - super(type, { - bubbles: bubbles, - cancelable: cancelable, - composed: composed, - }); - - this.#message = message; - this.#filename = filename; - this.#lineno = lineno; - this.#colno = colno; - this.#error = error; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(ErrorEventPrototype, this), - keys: [ - ...new SafeArrayIterator(EVENT_PROPS), - "message", - "filename", - "lineno", - "colno", - "error", - ], - }), - inspectOptions, - ); - } - - // TODO(lucacasonato): remove when this interface is spec aligned - [SymbolToStringTag] = "ErrorEvent"; -} - +webidl.configureInterface(ErrorEvent); const ErrorEventPrototype = ErrorEvent.prototype; -defineEnumerableProps(ErrorEvent, [ +ObjectDefineProperty( + ErrorEvent.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(ErrorEventPrototype, this), + keys: ArrayPrototypeFlat([ + EVENT_PROPS, + ERROR_EVENT_PROPS, + ]), + }), + inspectOptions, + ); + }, + }, +); + +const ERROR_EVENT_PROPS = [ "message", "filename", "lineno", "colno", "error", -]); +]; -class CloseEvent extends Event { - #wasClean = ""; - #code = ""; - #reason = ""; - - get wasClean() { - return this.#wasClean; - } - get code() { - return this.#code; - } - get reason() { - return this.#reason; - } - - constructor(type, { - bubbles, - cancelable, - composed, - wasClean = false, - code = 0, - reason = "", - } = { __proto__: null }) { - super(type, { - bubbles: bubbles, - cancelable: cancelable, - composed: composed, - }); - - this.#wasClean = wasClean; - this.#code = code; - this.#reason = reason; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(CloseEventPrototype, this), - keys: [ - ...new SafeArrayIterator(EVENT_PROPS), - "wasClean", - "code", - "reason", - ], - }), - inspectOptions, - ); - } -} +defineEnumerableProps(ErrorEvent.prototype, ERROR_EVENT_PROPS); +webidl.configureInterface(CloseEvent); const CloseEventPrototype = CloseEvent.prototype; +ObjectDefineProperty( + CloseEvent.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(CloseEventPrototype, this), + keys: ArrayPrototypeFlat([ + EVENT_PROPS, + CLOSE_EVENT_PROPS, + ]), + }), + inspectOptions, + ); + }, + }, +); + +const CLOSE_EVENT_PROPS = [ + "wasClean", + "code", + "reason", +]; + +defineEnumerableProps(CloseEvent.prototype, CLOSE_EVENT_PROPS); + class MessageEvent extends Event { get source() { return null; @@ -1252,11 +327,9 @@ class MessageEvent extends Event { inspectOptions, ); } - - // TODO(lucacasonato): remove when this interface is spec aligned - [SymbolToStringTag] = "MessageEvent"; } +webidl.configureInterface(MessageEvent); const MessageEventPrototype = MessageEvent.prototype; class CustomEvent extends Event { @@ -1282,19 +355,14 @@ class CustomEvent extends Event { createFilteredInspectProxy({ object: this, evaluate: ObjectPrototypeIsPrototypeOf(CustomEventPrototype, this), - keys: [ - ...new SafeArrayIterator(EVENT_PROPS), - "detail", - ], + keys: [...new SafeArrayIterator(EVENT_PROPS), "detail"], }), inspectOptions, ); } - - // TODO(lucacasonato): remove when this interface is spec aligned - [SymbolToStringTag] = "CustomEvent"; } +webidl.configureInterface(CustomEvent); const CustomEventPrototype = CustomEvent.prototype; ReflectDefineProperty(CustomEvent.prototype, "detail", { @@ -1328,11 +396,9 @@ class ProgressEvent extends Event { inspectOptions, ); } - - // TODO(lucacasonato): remove when this interface is spec aligned - [SymbolToStringTag] = "ProgressEvent"; } +webidl.configureInterface(ProgressEvent); const ProgressEventPrototype = ProgressEvent.prototype; class PromiseRejectionEvent extends Event { @@ -1348,13 +414,7 @@ class PromiseRejectionEvent extends Event { constructor( type, - { - bubbles, - cancelable, - composed, - promise, - reason, - } = { __proto__: null }, + { bubbles, cancelable, composed, promise, reason } = { __proto__: null }, ) { super(type, { bubbles: bubbles, @@ -1374,26 +434,17 @@ class PromiseRejectionEvent extends Event { PromiseRejectionEventPrototype, this, ), - keys: [ - ...new SafeArrayIterator(EVENT_PROPS), - "promise", - "reason", - ], + keys: [...new SafeArrayIterator(EVENT_PROPS), "promise", "reason"], }), inspectOptions, ); } - - // TODO(lucacasonato): remove when this interface is spec aligned - [SymbolToStringTag] = "PromiseRejectionEvent"; } +webidl.configureInterface(PromiseRejectionEvent); const PromiseRejectionEventPrototype = PromiseRejectionEvent.prototype; -defineEnumerableProps(PromiseRejectionEvent, [ - "promise", - "reason", -]); +defineEnumerableProps(PromiseRejectionEvent.prototype, ["promise", "reason"]); const _eventHandlers = Symbol("eventHandlers"); @@ -1465,10 +516,7 @@ function defineEventHandler( if (handlerWrapper) { handlerWrapper.handler = value; } else if (value !== null) { - handlerWrapper = makeWrappedHandler( - value, - isSpecialErrorEventHandler, - ); + handlerWrapper = makeWrappedHandler(value, isSpecialErrorEventHandler); this.addEventListener(name, handlerWrapper); init?.(this); } @@ -1547,7 +595,8 @@ export { Event, EventTarget, EventTargetPrototype, - listenerCount, + getListenerCount, + getListeners, MessageEvent, ProgressEvent, PromiseRejectionEvent, diff --git a/ext/web/03_abort_signal.js b/ext/web/03_abort_signal.js index 1f9ce42e1e..42009ccf1b 100644 --- a/ext/web/03_abort_signal.js +++ b/ext/web/03_abort_signal.js @@ -30,7 +30,7 @@ import { defineEventHandler, Event, EventTarget, - listenerCount, + getListenerCount, setIsTrusted, } from "./02_event.js"; import { clearTimeout, refTimer, unrefTimer } from "./02_timers.js"; @@ -183,7 +183,7 @@ class AbortSignal extends EventTarget { } } - if (listenerCount(this, "abort") > 0) { + if (getListenerCount(this, "abort") > 0) { const event = new Event("abort"); setIsTrusted(event, true); super.dispatchEvent(event); @@ -225,7 +225,7 @@ class AbortSignal extends EventTarget { // ops which would block the event loop. addEventListener() { FunctionPrototypeApply(super.addEventListener, this, arguments); - if (listenerCount(this, "abort") > 0) { + if (getListenerCount(this, "abort") > 0) { if (this[timerId] !== null) { refTimer(this[timerId]); } else if (this[sourceSignals] !== null) { @@ -242,7 +242,7 @@ class AbortSignal extends EventTarget { removeEventListener() { FunctionPrototypeApply(super.removeEventListener, this, arguments); - if (listenerCount(this, "abort") === 0) { + if (getListenerCount(this, "abort") === 0) { if (this[timerId] !== null) { unrefTimer(this[timerId]); } else if (this[sourceSignals] !== null) { @@ -256,7 +256,7 @@ class AbortSignal extends EventTarget { sourceSignal[dependentSignals].toArray(), (dependentSignal) => dependentSignal === this || - listenerCount(dependentSignal, "abort") === 0, + getListenerCount(dependentSignal, "abort") === 0, ) ) { unrefTimer(sourceSignal[timerId]); diff --git a/ext/web/15_performance.js b/ext/web/15_performance.js index 967cdda470..aaac2ec575 100644 --- a/ext/web/15_performance.js +++ b/ext/web/15_performance.js @@ -7,6 +7,7 @@ const { ArrayPrototypePush, ObjectKeys, ObjectPrototypeIsPrototypeOf, + ObjectSetPrototypeOf, ReflectHas, Symbol, SymbolFor, @@ -298,6 +299,7 @@ class PerformanceMark extends PerformanceEntry { } webidl.configureInterface(PerformanceMark); const PerformanceMarkPrototype = PerformanceMark.prototype; + class PerformanceMeasure extends PerformanceEntry { [_detail] = null; @@ -360,13 +362,13 @@ class PerformanceMeasure extends PerformanceEntry { } webidl.configureInterface(PerformanceMeasure); const PerformanceMeasurePrototype = PerformanceMeasure.prototype; -class Performance extends EventTarget { + +class Performance { constructor(key = null) { if (key != illegalConstructorKey) { webidl.illegalConstructor(); } - super(); this[webidl.brand] = webidl.brand; } @@ -600,6 +602,12 @@ class Performance extends EventTarget { ); } } + +// Prevent the execution of the EventTarget constructor and make it possible +// to initialize during bootstrap. +ObjectSetPrototypeOf(Performance, EventTarget); +ObjectSetPrototypeOf(Performance.prototype, EventTarget.prototype); + webidl.configureInterface(Performance); const PerformancePrototype = Performance.prototype; diff --git a/ext/web/event.rs b/ext/web/event.rs new file mode 100644 index 0000000000..e919d9379c --- /dev/null +++ b/ext/web/event.rs @@ -0,0 +1,1188 @@ +// Copyright 2018-2025 the Deno authors. MIT license. + +use std::cell::Cell; +use std::cell::RefCell; +use std::collections::HashMap; +use std::collections::VecDeque; +use std::rc::Rc; + +use deno_core::GarbageCollected; +use deno_core::WebIDL; +use deno_core::cppgc; +use deno_core::op2; +use deno_core::v8; +use deno_core::v8::Global; +use deno_core::webidl::Nullable; +use deno_core::webidl::WebIdlConverter; +use deno_core::webidl::WebIdlError; + +#[derive(Debug, thiserror::Error, deno_error::JsError)] +pub enum EventError { + #[class(type)] + #[error("parameter 2 is not of type 'Object'")] + InvalidListenerType, + #[class(type)] + #[error("parameter 1 is expected Event")] + ExpectedEvent, + #[class("DOMExceptionInvalidStateError")] + #[error("Invalid event state")] + InvalidState, + #[class(generic)] + #[error(transparent)] + DataError(#[from] v8::DataError), + #[class(inherit)] + #[error(transparent)] + WebIDL(#[from] WebIdlError), +} + +#[derive(WebIDL, Debug)] +#[webidl(dictionary)] +pub struct EventInit { + #[webidl(default = false)] + bubbles: bool, + #[webidl(default = false)] + cancelable: bool, + #[webidl(default = false)] + composed: bool, +} + +#[derive(WebIDL, Clone, Debug)] +#[webidl(enum)] +pub enum EventPhase { + None, + CapturingPhase, + AtTarget, + BubblingPhase, +} + +#[derive(Debug)] +struct Path { + invocation_target: v8::Global, + root_of_closed_tree: bool, + slot_in_closed_tree: bool, + // item_in_shadow_tree: bool, + shadow_adjusted_target: Option>, + related_target: Option>, + // touch_target_list: Vec>, +} + +enum InvokePhase { + Capturing, + Bubbling, +} + +#[derive(Debug)] +pub struct Event { + typ: RefCell, + bubbles: Cell, + cancelable: Cell, + composed: bool, + + target: RefCell>>, + related_target: RefCell>>, + current_target: RefCell>>, + path: RefCell>, + event_phase: RefCell, + + // flags + stop_propagation_flag: Cell, + stop_immediate_propagation_flag: Cell, + canceled_flag: Cell, + in_passive_listener_flag: Cell, + // ShadowRoot is not implemented + // composed_flag: Cell, + // document.createEvent is not implemented + // initialized_flag: Cell, + dispatch_flag: Cell, + + is_trusted: Cell, + time_stamp: f64, +} + +impl GarbageCollected for Event { + fn get_name(&self) -> &'static std::ffi::CStr { + c"Event" + } +} + +impl Event { + #[inline] + fn new(typ: String, init: Option) -> Event { + let (bubbles, cancelable, composed) = if let Some(init) = init { + (init.bubbles, init.cancelable, init.composed) + } else { + (false, false, false) + }; + + Event { + typ: RefCell::new(typ), + bubbles: Cell::new(bubbles), + cancelable: Cell::new(cancelable), + composed, + + target: RefCell::new(None), + related_target: RefCell::new(None), + current_target: RefCell::new(None), + path: RefCell::new(Vec::new()), + event_phase: RefCell::new(EventPhase::None), + + // flags + stop_propagation_flag: Cell::new(false), + stop_immediate_propagation_flag: Cell::new(false), + canceled_flag: Cell::new(false), + in_passive_listener_flag: Cell::new(false), + dispatch_flag: Cell::new(false), + + is_trusted: Cell::new(false), + time_stamp: 0.0, + } + } + + // https://dom.spec.whatwg.org/#concept-event-dispatch + fn dispatch<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + event_object: v8::Local<'a, v8::Object>, + target: &EventTarget, + target_object: v8::Global, + target_override: Option>, + ) -> bool { + // NOTE: Omit unnecessary implementations for Node, MouseEvent, and Slottable + + // 1. + self.dispatch_flag.set(true); + + // 2. + let target_override = target_override.or(Some(target_object.clone())); + + // 4. + let related_target = self.related_target.borrow().clone(); + + // 6.3. + self.append_to_event_path( + target_object.clone(), + target_override, + related_target, + false, + ); + + // 6.13. + for (path_index, path) in self.path.borrow().iter().enumerate().rev() { + if path.shadow_adjusted_target.is_none() { + self.event_phase.replace(EventPhase::CapturingPhase); + self.invoke( + scope, + event_object, + target, + target_object.clone(), + path_index, + InvokePhase::Capturing, + ); + } + } + + // 6.14. + for (path_index, path) in self.path.borrow().iter().enumerate() { + if path.shadow_adjusted_target.is_some() { + self.event_phase.replace(EventPhase::AtTarget); + } else { + if !self.bubbles.get() { + continue; + } + self.event_phase.replace(EventPhase::BubblingPhase); + } + self.invoke( + scope, + event_object, + target, + target_object.clone(), + path_index, + InvokePhase::Bubbling, + ); + } + + // 7. + self.event_phase.replace(EventPhase::None); + + // 8. + self.current_target.replace(None); + + // 9. + self.path.borrow_mut().clear(); + + // 10. + self.dispatch_flag.set(false); + self.stop_propagation_flag.set(false); + self.stop_immediate_propagation_flag.set(false); + + !self.canceled_flag.get() + } + + // https://dom.spec.whatwg.org/#concept-event-path-append + #[inline] + fn append_to_event_path( + &self, + invocation_target: v8::Global, + shadow_adjusted_target: Option>, + related_target: Option>, + slot_in_closed_tree: bool, + ) { + let mut path = self.path.borrow_mut(); + path.push(Path { + invocation_target, + root_of_closed_tree: false, + slot_in_closed_tree, + shadow_adjusted_target, + related_target, + }); + } + + // https://dom.spec.whatwg.org/#concept-event-listener-invoke + #[inline] + fn invoke<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + event_object: v8::Local<'a, v8::Object>, + target: &EventTarget, + target_object: v8::Global, + path_index: usize, + phase: InvokePhase, + ) { + let path = self.path.borrow(); + + // 1. + for (index, current) in path.iter().enumerate().rev() { + if let Some(target) = ¤t.shadow_adjusted_target { + self.target.replace(Some(target.clone())); + break; + } + if index == path_index { + break; + } + } + + // 2. + let current = &path[path_index]; + self.related_target.replace(current.related_target.clone()); + + // 4. + if self.stop_propagation_flag.get() { + return; + } + + // 5. + self + .current_target + .replace(Some(current.invocation_target.clone())); + + // 6. + // Against the spec, clone event listeners in inner_invoke + let typ = self.typ.borrow(); + let mut listeners = target.listeners.borrow_mut(); + let Some(listeners) = listeners.get_mut(&*typ) else { + return; + }; + + // 8. + let _ = self.inner_invoke( + scope, + event_object, + target_object.clone(), + listeners, + phase, + ); + } + + // https://dom.spec.whatwg.org/#concept-event-listener-inner-invoke + #[inline] + fn inner_invoke<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + event_object: v8::Local<'a, v8::Object>, + target_object: v8::Global, + listeners: &mut Vec>, + phase: InvokePhase, + ) -> bool { + // NOTE: Omit implementations for window.event (current event) + + // 1. + let mut found = false; + + // 2. + // Clone event listeners before iterating since the list can be modified during the iteration. + for listener in listeners.clone().iter() { + // Check if the event listener has been removed since the listeners has been cloned. + if !listeners.iter().any(|l| Rc::ptr_eq(l, listener)) { + continue; + } + + // 2.2. + found = true; + + // 3. + // 4. + if (matches!(phase, InvokePhase::Capturing) && !listener.capture) + || (matches!(phase, InvokePhase::Bubbling) && listener.capture) + { + continue; + } + + // 5. + if listener.once { + listeners.remove( + listeners + .iter() + .position(|l| Rc::ptr_eq(l, listener)) + .unwrap(), + ); + } + + // 9. + if listener.passive { + self.in_passive_listener_flag.set(true); + } + + // 11. + let scope = &mut v8::TryCatch::new(scope); + + let callback = v8::Local::new(scope, listener.callback.clone()); + let key = v8::String::new(scope, "handleEvent").unwrap(); + if let Some(handle_event) = callback.get(scope, key.into()) + && let Ok(handle_event) = + v8::Local::::try_from(handle_event) + { + let recv = v8::Local::new(scope, &target_object); + handle_event.call(scope, recv.into(), &[event_object.into()]); + } else { + match v8::Local::::try_from(callback) { + Ok(callback) => { + let recv = v8::Local::new(scope, &target_object); + callback.call(scope, recv.into(), &[event_object.into()]); + } + // 11.1. + Err(error) => { + // TODO(petamoriken): report exception + } + } + } + + // 11.1. + if let Some(exception) = scope.exception() { + // TODO(petamoriken): report exception + } + + // 12. + self.in_passive_listener_flag.set(false); + + // 14. + if self.stop_immediate_propagation_flag.get() + && !listener.resist_stop_immediate_propagation + { + break; + } + } + + // 15. + found + } +} + +#[op2(base)] +impl Event { + #[constructor] + #[required(1)] + #[cppgc] + fn constructor( + #[webidl] typ: String, + #[webidl] init: Nullable, + ) -> Event { + Event::new(typ, init.into_option()) + } + + // legacy + #[required(1)] + fn init_event<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + #[webidl] bubbles: Option, + #[webidl] cancelable: Option, + ) -> v8::Local<'a, v8::Primitive> { + let undefined = v8::undefined(scope); + if self.dispatch_flag.get() { + return undefined; + } + + self.typ.replace(typ); + if let Some(bubbles) = bubbles { + self.bubbles.replace(bubbles); + } + if let Some(cancelable) = cancelable { + self.cancelable.replace(cancelable); + } + undefined + } + + #[getter] + #[rename("type")] + #[string] + fn typ(&self) -> String { + self.typ.borrow().clone() + } + + #[getter] + #[global] + fn target(&self) -> Option> { + self.target.borrow().clone() + } + + // deprecated: an alias of target + #[getter] + #[global] + fn src_element<'a>(&self) -> Option> { + self.target.borrow().clone() + } + + #[getter] + #[global] + fn current_target(&self) -> Option> { + self.current_target.borrow().clone() + } + + fn composed_path<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + ) -> v8::Local<'a, v8::Array> { + let path = self.path.borrow(); + if path.is_empty() { + return v8::Array::new(scope, 0); + } + + let current_target = self.current_target.borrow(); + let current_target = current_target.as_ref().unwrap(); + let mut composed_path: VecDeque> = VecDeque::new(); + composed_path.push_back(v8::Local::new(scope, current_target).into()); + + let mut current_target_index = 0; + let mut current_target_hidden_subtree_level = 0; + for ( + index, + Path { + invocation_target, + root_of_closed_tree, + slot_in_closed_tree, + .. + }, + ) in path.iter().enumerate().rev() + { + if *root_of_closed_tree { + current_target_hidden_subtree_level += 1; + } + + if *invocation_target == current_target { + current_target_index = index; + break; + } + + if *slot_in_closed_tree { + current_target_hidden_subtree_level -= 1; + } + } + + let mut current_hidden_level = current_target_hidden_subtree_level; + let mut max_hidden_level = current_target_hidden_subtree_level; + for Path { + invocation_target, + root_of_closed_tree, + slot_in_closed_tree, + .. + } in path[0..current_target_index - 1].iter().rev() + { + if *root_of_closed_tree { + current_hidden_level += 1; + } + + if current_hidden_level <= max_hidden_level { + composed_path + .push_front(v8::Local::new(scope, invocation_target).into()); + } + + if *slot_in_closed_tree { + current_hidden_level -= 1; + if current_hidden_level < max_hidden_level { + max_hidden_level = current_hidden_level; + } + } + } + + current_hidden_level = current_target_hidden_subtree_level; + max_hidden_level = current_target_hidden_subtree_level; + for Path { + invocation_target, + root_of_closed_tree, + slot_in_closed_tree, + .. + } in path[current_target_index + 1..].iter() + { + if *slot_in_closed_tree { + current_hidden_level += 1; + } + + if current_hidden_level <= max_hidden_level { + composed_path + .push_back(v8::Local::new(scope, invocation_target).into()); + } + + if *root_of_closed_tree { + current_hidden_level -= 1; + if current_hidden_level < max_hidden_level { + max_hidden_level = current_hidden_level; + } + } + } + + v8::Array::new_with_elements(scope, composed_path.make_contiguous()) + } + + #[fast] + #[getter] + fn bubbles(&self) -> bool { + self.bubbles.get() + } + + #[fast] + #[getter] + fn cancelable(&self) -> bool { + self.cancelable.get() + } + + #[fast] + #[getter] + fn composed(&self) -> bool { + self.composed + } + + #[fast] + #[getter] + fn event_phase(&self) -> i32 { + self.event_phase.borrow().clone() as i32 + } + + #[fast] + fn stop_propagation(&self) { + self.stop_propagation_flag.set(true); + } + + // legacy + #[fast] + #[getter] + fn cancel_bubble(&self) -> bool { + self.stop_propagation_flag.get() + } + + // legacy + #[fast] + #[setter] + fn cancel_bubble(&self, value: bool) { + self.stop_propagation_flag.set(value); + } + + #[fast] + fn stop_immediate_propagation(&self) { + self.stop_propagation_flag.set(true); + self.stop_immediate_propagation_flag.set(true); + } + + #[fast] + #[getter] + fn default_prevented(&self) -> bool { + self.canceled_flag.get() + } + + // legacy + #[fast] + #[getter] + fn return_value(&self) -> bool { + !self.canceled_flag.get() + } + + // legacy + #[fast] + #[setter] + fn return_value(&self, value: bool) { + if !value { + self.canceled_flag.set(true); + } + } + + #[fast] + fn prevent_default(&self) { + if self.cancelable.get() && !self.in_passive_listener_flag.get() { + self.canceled_flag.set(true); + } + } + + // document.createEvent is not implemented + #[fast] + #[getter] + fn initialized(&self) -> bool { + true + } + + // Not spec compliant. The spec defines it as [LegacyUnforgeable] + // but doing so has a big performance hit + #[fast] + #[getter] + fn is_trusted(&self) -> bool { + self.is_trusted.get() + } + + #[fast] + #[getter] + fn time_stamp(&self) -> f64 { + self.time_stamp + } +} + +#[op2(fast)] +pub fn op_event_set_is_trusted(#[cppgc] event: &Event, value: bool) { + event.is_trusted.set(value); +} + +#[op2] +pub fn op_event_set_target( + #[cppgc] event: &Event, + #[global] value: v8::Global, +) { + event.target.replace(Some(value)); +} + +#[op2(reentrant)] +pub fn op_event_dispatch<'a>( + scope: &mut v8::HandleScope<'a>, + #[global] target_object: v8::Global, + event_object: v8::Local<'a, v8::Object>, + #[global] target_override: Option>, +) -> bool { + let target = v8::Local::new(scope, &target_object); + let target = + cppgc::try_unwrap_cppgc_object::(scope, target.into()) + .unwrap(); + let event = + cppgc::try_unwrap_cppgc_proto_object::(scope, event_object.into()) + .unwrap(); + event.dispatch(scope, event_object, &target, target_object, target_override) +} + +#[derive(WebIDL, Debug)] +#[webidl(dictionary)] +pub struct ErrorEventInit { + #[webidl(default = false)] + bubbles: bool, + #[webidl(default = false)] + cancelable: bool, + #[webidl(default = false)] + composed: bool, + #[webidl(default = String::new())] + message: String, + #[webidl(default = String::new())] + filename: String, + #[webidl(default = 0)] + lineno: u32, + #[webidl(default = 0)] + colno: u32, + // #[webidl(default = None)] + // error: Option>, +} + +#[derive(Debug)] +pub struct ErrorEvent { + message: String, + filename: String, + lineno: u32, + colno: u32, + error: Option>, +} + +impl GarbageCollected for ErrorEvent { + fn get_name(&self) -> &'static std::ffi::CStr { + c"ErrorEvent" + } +} + +impl ErrorEvent { + #[inline] + fn new( + init: Option, + error: Option>, + ) -> ErrorEvent { + let Some(init) = init else { + return ErrorEvent { + message: String::new(), + filename: String::new(), + lineno: 0, + colno: 0, + error, + }; + }; + + ErrorEvent { + message: init.message, + filename: init.filename, + lineno: init.lineno, + colno: init.colno, + error, + } + } +} + +#[op2(inherit = Event)] +impl ErrorEvent { + #[constructor] + #[required(1)] + #[cppgc] + fn constructor<'a>( + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + init: v8::Local<'a, v8::Value>, + ) -> Result<(Event, ErrorEvent), EventError> { + if init.is_null_or_undefined() { + return Ok((Event::new(typ, None), ErrorEvent::new(None, None))); + } + + let error_event_init = Nullable::::convert( + scope, + init, + "Failed to construct 'ErrorEvent'".into(), + (|| "Argument 2".into()).into(), + &Default::default(), + )?; + let error_event_init = error_event_init.into_option(); + let event = if let Some(ref error_event_init) = error_event_init { + let event_init = EventInit { + bubbles: error_event_init.bubbles, + cancelable: error_event_init.cancelable, + composed: error_event_init.composed, + }; + Event::new(typ, Some(event_init)) + } else { + Event::new(typ, None) + }; + + let error = if init.is_object() + && let Some(init) = init.to_object(scope) + { + get_value(scope, init, "error").map(|error| v8::Global::new(scope, error)) + } else { + None + }; + let error_event = ErrorEvent::new(error_event_init, error); + Ok((event, error_event)) + } + + #[getter] + #[string] + fn message(&self) -> String { + self.message.clone() + } + + #[getter] + #[string] + fn filename(&self) -> String { + self.filename.clone() + } + + #[getter] + fn lineno(&self) -> u32 { + self.lineno.clone() + } + + #[getter] + fn colno(&self) -> u32 { + self.colno.clone() + } + + #[getter] + fn error<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + ) -> v8::Local<'a, v8::Value> { + if let Some(error) = &self.error { + v8::Local::new(scope, error) + } else { + v8::undefined(scope).into() + } + } +} + +#[derive(WebIDL, Debug)] +#[webidl(dictionary)] +pub struct CloseEventInit { + #[webidl(default = false)] + bubbles: bool, + #[webidl(default = false)] + cancelable: bool, + #[webidl(default = false)] + composed: bool, + #[webidl(default = false)] + was_clean: bool, + #[webidl(default = 0)] + code: u16, + #[webidl(default = String::new())] + reason: String, +} + +#[derive(Debug)] +pub struct CloseEvent { + was_clean: bool, + code: u16, + reason: String, +} + +impl GarbageCollected for CloseEvent { + fn get_name(&self) -> &'static std::ffi::CStr { + c"CloseEvent" + } +} + +impl CloseEvent { + fn new(init: Option) -> CloseEvent { + let (was_clean, code, reason) = if let Some(init) = init { + (init.was_clean, init.code, init.reason) + } else { + (false, 0, String::new()) + }; + CloseEvent { + was_clean, + code, + reason, + } + } +} + +#[op2(inherit = Event)] +impl CloseEvent { + #[constructor] + #[required(1)] + #[cppgc] + fn constructor( + #[webidl] typ: String, + #[webidl] init: Nullable, + ) -> (Event, CloseEvent) { + let init = init.into_option(); + let event = if let Some(ref init) = init { + let event_init = EventInit { + bubbles: init.bubbles, + cancelable: init.cancelable, + composed: init.composed, + }; + Event::new(typ, Some(event_init)) + } else { + Event::new(typ, None) + }; + let close_event = CloseEvent::new(init); + (event, close_event) + } + + #[getter] + fn was_clean(&self) -> bool { + self.was_clean + } + + #[getter] + fn code(&self) -> u16 { + self.code + } + + #[getter] + #[string] + fn reason(&self) -> String { + self.reason.clone() + } +} + +// TODO(petamorken): list +// report error +// MessageEvent +// PromiseRejectionEvent +// CustomEvent +// ProgressEvent + +#[inline] +fn get_value<'a>( + scope: &mut v8::HandleScope<'a>, + obj: v8::Local<'a, v8::Object>, + key: &str, +) -> Option> { + let key = v8::String::new(scope, key).unwrap(); + if let Some(value) = obj.get(scope, key.into()) + && !value.is_undefined() + { + Some(value) + } else { + None + } +} + +#[derive(Debug)] +struct EventListener { + callback: v8::Global, + capture: bool, + passive: bool, + once: bool, + signal: Option>, + // This field exists for simulating Node.js behavior, implemented in https://github.com/nodejs/node/commit/bcd35c334ec75402ee081f1c4da128c339f70c24 + // Some internal event listeners in Node.js can ignore `e.stopImmediatePropagation()` calls from the earlier event listeners. + resist_stop_immediate_propagation: bool, +} + +#[derive(Debug)] +pub struct EventTarget { + listeners: RefCell>>>, +} + +impl GarbageCollected for EventTarget { + fn get_name(&self) -> &'static std::ffi::CStr { + c"EventTarget" + } +} + +#[op2] +impl EventTarget { + #[constructor] + #[cppgc] + fn new(_: bool) -> EventTarget { + EventTarget { + listeners: RefCell::new(HashMap::new()), + } + } + + #[required(2)] + #[undefined] + fn add_event_listener<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + callback: Option>, + options: Option>, + ) -> Result<(), EventError> { + let ( + capture, + passive, + once, + resist_stop_immediate_propagation, + /* signal */ + ) = match options { + Some(options) => { + if options.is_object() + && let Some(options) = options.to_object(scope) + { + #[inline] + fn to_bool<'a>( + scope: &mut v8::HandleScope<'a>, + options: v8::Local<'a, v8::Object>, + str: &'static str, + is_symbol: bool, + ) -> bool { + let str = v8::String::new(scope, str).unwrap(); + let key: v8::Local = if is_symbol { + v8::Symbol::for_key(scope, str).into() + } else { + str.into() + }; + match options.get(scope, key) { + Some(value) => value.to_boolean(scope).is_true(), + None => false, + } + } + + let capture = to_bool(scope, options, "capture", false); + let passive = to_bool(scope, options, "passive", false); + let once = to_bool(scope, options, "once", false); + let resist_stop_immediate_propagation = + to_bool(scope, options, "Deno.stopImmediatePropagation", true); + (capture, passive, once, resist_stop_immediate_propagation) + } else { + (options.to_boolean(scope).is_true(), false, false, false) + } + } + None => (false, false, false, false), + }; + + // TODO(petamoriken): signal have already aborted + + let callback = match callback { + None => { + return Ok(()); + } + Some(callback) => { + if callback.is_null() { + return Ok(()); + } + if !callback.is_object() { + return Err(EventError::InvalidListenerType); + } + callback.to_object(scope).unwrap() + } + }; + + let mut listeners = self.listeners.borrow_mut(); + let listeners = listeners.entry(typ.clone()).or_default(); + for listener in listeners.iter() { + if listener.capture == capture && listener.callback == callback { + return Ok(()); + } + } + + // TODO(petamoriken): add signal listeners + + listeners.push(Rc::new(EventListener { + callback: Global::new(scope, callback), + capture, + passive, + once, + signal: None, + resist_stop_immediate_propagation, + })); + + Ok(()) + } + + #[required(2)] + #[undefined] + fn remove_event_listener<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + callback: Option>, + options: Option>, + ) { + let capture = match options { + Some(options) => { + if options.is_object() + && let Some(options) = options.to_object(scope) + { + let key = v8::String::new(scope, "capture").unwrap(); + match options.get(scope, key.into()) { + Some(value) => value.to_boolean(scope).is_true(), + None => false, + } + } else { + options.to_boolean(scope).is_true() + } + } + None => false, + }; + + let callback = match callback { + None => { + return; + } + Some(callback) => { + if callback.is_null() { + return; + } + callback + } + }; + + let mut listeners = self.listeners.borrow_mut(); + let Some(listeners) = listeners.get_mut(&typ) else { + return; + }; + if let Some(index) = listeners.iter().position(|listener| { + listener.capture == capture && listener.callback == callback + }) { + listeners.remove(index); + } + } + + #[fast] + #[reentrant] + #[required(1)] + fn dispatch_event<'a>( + &self, + #[this] this: v8::Global, + scope: &mut v8::HandleScope<'a>, + event_object: v8::Local<'a, v8::Object>, + ) -> Result { + let Some(event) = + cppgc::try_unwrap_cppgc_proto_object::(scope, event_object.into()) + else { + return Err(EventError::ExpectedEvent); + }; + + let typ = event.typ.borrow(); + + // This is an optimization to avoid creating an event listener on each startup. + // Stores the flag for checking whether unload is dispatched or not. + // This prevents the recursive dispatches of unload events. + // See https://github.com/denoland/deno/issues/9201. + let global = scope.get_current_context().global(scope); + if this == global && *typ == "unload" { + let key = v8::String::new(scope, "Deno.isUnloadDispatched").unwrap(); + let symbol = v8::Symbol::for_key(scope, key); + let value = v8::Boolean::new(scope, true); + global.set(scope, symbol.into(), value.into()); + } + + let listeners = self.listeners.borrow(); + if listeners.get(&*typ).is_none() { + event.target.replace(Some(this)); + return Ok(true); + }; + + if event.dispatch_flag.get() + || !matches!(*event.event_phase.borrow(), EventPhase::None) + { + return Err(EventError::InvalidState); + } + + Ok(event.dispatch(scope, event_object, self, this, None)) + } +} + +#[op2(fast)] +pub fn op_event_wrap_event_target<'a>( + scope: &mut v8::HandleScope<'a>, + obj: v8::Local<'a, v8::Object>, +) { + cppgc::wrap_object( + scope, + obj, + EventTarget { + listeners: RefCell::new(HashMap::new()), + }, + ); +} + +#[op2] +pub fn op_event_get_target_listeners<'a>( + scope: &mut v8::HandleScope<'a>, + #[cppgc] event_target: &EventTarget, + #[string] typ: String, +) -> v8::Local<'a, v8::Array> { + let listeners = event_target.listeners.borrow(); + match listeners.get(&typ) { + Some(listeners) => { + let elements: Vec> = listeners + .iter() + .map(|listener| v8::Local::new(scope, listener.callback.clone()).into()) + .collect(); + v8::Array::new_with_elements(scope, elements.as_slice()) + } + None => v8::Array::new(scope, 0), + } +} + +#[op2(fast)] +pub fn op_event_get_target_listener_count<'a>( + #[cppgc] event_target: &EventTarget, + #[string] typ: String, +) -> u32 { + let listeners = event_target.listeners.borrow(); + match listeners.get(&typ) { + Some(listeners) => listeners.len() as u32, + None => 0, + } +} diff --git a/ext/web/lib.rs b/ext/web/lib.rs index c9adaf8330..30c8a026b8 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -2,6 +2,7 @@ mod blob; mod compression; +mod event; mod message_port; mod stream_resource; mod timers; @@ -80,6 +81,12 @@ deno_core::extension!(deno_web, compression::op_compression_new, compression::op_compression_write, compression::op_compression_finish, + event::op_event_dispatch, + event::op_event_get_target_listener_count, + event::op_event_get_target_listeners, + event::op_event_set_is_trusted, + event::op_event_set_target, + event::op_event_wrap_event_target, op_now

, op_time_origin

, op_defer, @@ -92,6 +99,12 @@ deno_core::extension!(deno_web, stream_resource::op_readable_stream_resource_close, stream_resource::op_readable_stream_resource_await_close, ], + objects = [ + event::Event, + event::EventTarget, + event::ErrorEvent, + event::CloseEvent, + ], esm = [ "00_infra.js", "01_dom_exception.js", diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 4ff056e8cc..51cb03bfcd 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -898,6 +898,7 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) { removeImportedOps(); performance.setTimeOrigin(); + event.setEventTargetData(performance.performance); globalThis_ = globalThis; // Remove bootstrapping data from the global scope @@ -935,6 +936,7 @@ function bootstrapMainRuntime(runtimeOptions, warmup = false) { core.wrapConsole(globalThis.console, core.v8Console); } + event.setEventTargetData(globalThis); event.defineEventHandler(globalThis, "error"); event.defineEventHandler(globalThis, "load"); event.defineEventHandler(globalThis, "beforeunload"); @@ -1033,6 +1035,7 @@ function bootstrapWorkerRuntime( closeOnIdle = runtimeOptions[14]; performance.setTimeOrigin(); + event.setEventTargetData(performance.performance); globalThis_ = globalThis; // Remove bootstrapping data from the global scope @@ -1065,6 +1068,7 @@ function bootstrapWorkerRuntime( core.wrapConsole(globalThis.console, core.v8Console); + event.setEventTargetData(globalThis); event.defineEventHandler(globalThis, "message"); event.defineEventHandler(globalThis, "error", undefined, true); @@ -1152,13 +1156,9 @@ globalThis.bootstrap = { dispatchProcessBeforeExitEvent, }; -event.setEventTargetData(globalThis); event.saveGlobalThisReference(globalThis); event.defineEventHandler(globalThis, "unhandledrejection"); -// Nothing listens to this, but it warms up the code paths for event dispatch -(new event.EventTarget()).dispatchEvent(new Event("warmup")); - removeImportedOps(); // Run the warmup path through node and runtime/worker bootstrap functions From e705b9e06339ee576188a6e7dbee999aca7cbd47 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Sun, 10 Aug 2025 23:58:03 +0900 Subject: [PATCH 02/18] wrap CustomEvent and MessageEvent --- ext/web/02_event.js | 132 ++++++++--------- ext/web/event.rs | 349 +++++++++++++++++++++++++++++++++++++++++++- ext/web/lib.rs | 2 + 3 files changed, 409 insertions(+), 74 deletions(-) diff --git a/ext/web/02_event.js b/ext/web/02_event.js index 49f3a530fa..0e2ea43928 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -15,7 +15,6 @@ const { ObjectDefineProperty, ObjectDefineProperties, ObjectPrototypeIsPrototypeOf, - ReflectDefineProperty, SafeArrayIterator, SafeMap, StringPrototypeStartsWith, @@ -25,9 +24,11 @@ const { } = primordials; import { CloseEvent, + CustomEvent, ErrorEvent, Event, EventTarget, + MessageEvent, op_event_dispatch, op_event_get_target_listener_count, op_event_get_target_listeners, @@ -49,7 +50,7 @@ function saveGlobalThisReference(val) { function defineEnumerableProps(prototype, props) { for (let i = 0; i < props.length; ++i) { const prop = props[i]; - ReflectDefineProperty(prototype, prop, { + ObjectDefineProperty(prototype, prop, { __proto__: null, enumerable: true, }); @@ -228,6 +229,35 @@ defineEnumerableProps(EventTarget.prototype, [ "dispatchEvent", ]); +webidl.configureInterface(CustomEvent); +const CustomEventPrototype = CustomEvent.prototype; + +ObjectDefineProperty( + CustomEvent.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(CustomEventPrototype, this), + keys: ArrayPrototypeFlat([ + EVENT_PROPS, + "detail", + ]), + }), + inspectOptions, + ); + }, + }, +); + +ObjectDefineProperty(CustomEvent.prototype, "detail", { + __proto__: null, + enumerable: true, +}); + webidl.configureInterface(ErrorEvent); const ErrorEventPrototype = ErrorEvent.prototype; @@ -294,81 +324,39 @@ const CLOSE_EVENT_PROPS = [ defineEnumerableProps(CloseEvent.prototype, CLOSE_EVENT_PROPS); -class MessageEvent extends Event { - get source() { - return null; - } - - constructor(type, eventInitDict) { - super(type, { - bubbles: eventInitDict?.bubbles ?? false, - cancelable: eventInitDict?.cancelable ?? false, - composed: eventInitDict?.composed ?? false, - }); - - this.data = eventInitDict?.data ?? null; - this.ports = eventInitDict?.ports ?? []; - this.origin = eventInitDict?.origin ?? ""; - this.lastEventId = eventInitDict?.lastEventId ?? ""; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(MessageEventPrototype, this), - keys: [ - ...new SafeArrayIterator(EVENT_PROPS), - "data", - "origin", - "lastEventId", - ], - }), - inspectOptions, - ); - } -} - webidl.configureInterface(MessageEvent); const MessageEventPrototype = MessageEvent.prototype; -class CustomEvent extends Event { - #detail = null; +ObjectDefineProperty( + MessageEvent.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(MessageEventPrototype, this), + keys: ArrayPrototypeFlat([ + EVENT_PROPS, + MESSAGE_EVENT_PROPS, + ]), + }), + inspectOptions, + ); + }, + }, +); - constructor(type, eventInitDict = { __proto__: null }) { - super(type, eventInitDict); - webidl.requiredArguments( - arguments.length, - 1, - "Failed to construct 'CustomEvent'", - ); - const { detail } = eventInitDict; - this.#detail = detail; - } +const MESSAGE_EVENT_PROPS = [ + "data", + "origin", + "lastEventId", + "source", + "ports", +]; - get detail() { - return this.#detail; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(CustomEventPrototype, this), - keys: [...new SafeArrayIterator(EVENT_PROPS), "detail"], - }), - inspectOptions, - ); - } -} - -webidl.configureInterface(CustomEvent); -const CustomEventPrototype = CustomEvent.prototype; - -ReflectDefineProperty(CustomEvent.prototype, "detail", { - __proto__: null, - enumerable: true, -}); +defineEnumerableProps(MessageEvent.prototype, MESSAGE_EVENT_PROPS); // ProgressEvent could also be used in other DOM progress event emits. // Current use is for FileReader. diff --git a/ext/web/event.rs b/ext/web/event.rs index e919d9379c..1ef1860043 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -15,6 +15,7 @@ use deno_core::v8::Global; use deno_core::webidl::Nullable; use deno_core::webidl::WebIdlConverter; use deno_core::webidl::WebIdlError; +use deno_core::webidl::WebIdlErrorKind; #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum EventError { @@ -674,6 +675,97 @@ pub fn op_event_dispatch<'a>( event.dispatch(scope, event_object, &target, target_object, target_override) } +#[derive(Debug)] +pub struct CustomEvent { + detail: RefCell>>, +} + +impl GarbageCollected for CustomEvent { + fn get_name(&self) -> &'static std::ffi::CStr { + c"CustomEvent" + } +} + +impl CustomEvent { + #[inline] + fn new(detail: Option>) -> CustomEvent { + CustomEvent { + detail: RefCell::new(detail), + } + } +} + +#[op2(inherit = Event)] +impl CustomEvent { + #[constructor] + #[required(1)] + #[cppgc] + fn constructor<'a>( + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + init: v8::Local<'a, v8::Value>, + ) -> Result<(Event, CustomEvent), EventError> { + if init.is_null_or_undefined() { + return Ok((Event::new(typ, None), CustomEvent::new(None))); + } + + let event_init = Nullable::::convert( + scope, + init, + "Failed to construct 'CustomEvent'".into(), + (|| "Argument 2".into()).into(), + &Default::default(), + )?; + let event = Event::new(typ, event_init.into_option()); + + let detail = if init.is_object() + && let Some(init) = init.to_object(scope) + { + get_value(scope, init, "detail") + .map(|detail| v8::Global::new(scope, detail)) + } else { + None + }; + let custom_event = CustomEvent::new(detail); + Ok((event, custom_event)) + } + + // legacy + #[required(1)] + fn init_custom_event<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + #[webidl] bubbles: Option, + #[webidl] cancelable: Option, + #[global] detail: Option>, + #[proto] event: &Event, + ) -> v8::Local<'a, v8::Primitive> { + let undefined = v8::undefined(scope); + if event.dispatch_flag.get() { + return undefined; + } + + event.typ.replace(typ); + if let Some(bubbles) = bubbles { + event.bubbles.replace(bubbles); + } + if let Some(cancelable) = cancelable { + event.cancelable.replace(cancelable); + } + if detail.is_some() { + self.detail.replace(detail); + } + undefined + } + + #[getter] + #[global] + fn detail(&self) -> Option> { + self.detail.borrow().clone() + } +} + #[derive(WebIDL, Debug)] #[webidl(dictionary)] pub struct ErrorEventInit { @@ -901,11 +993,264 @@ impl CloseEvent { } } +#[derive(WebIDL, Debug)] +#[webidl(dictionary)] +pub struct MessageEventInit { + #[webidl(default = false)] + bubbles: bool, + #[webidl(default = false)] + cancelable: bool, + #[webidl(default = false)] + composed: bool, + #[webidl(default = String::new())] + origin: String, + #[webidl(default = String::new())] + last_event_id: String, + // #[webidl(default = None)] + // data: Option>, + // #[webidl(default = None)] + // source: Option>, + // #[webidl(default = None)] + // ports: Option>, +} + +#[derive(Debug)] +pub struct MessageEvent { + origin: RefCell, + last_event_id: RefCell, + data: RefCell>>, + source: RefCell>>, + ports: RefCell>, +} + +impl GarbageCollected for MessageEvent { + fn get_name(&self) -> &'static std::ffi::CStr { + c"MessageEvent" + } +} + +impl MessageEvent { + #[inline] + fn new( + init: Option, + data: Option>, + source: Option>, + ports: v8::Global, + ) -> MessageEvent { + let Some(init) = init else { + return MessageEvent { + origin: RefCell::new(String::new()), + last_event_id: RefCell::new(String::new()), + data: RefCell::new(data), + source: RefCell::new(source), + ports: RefCell::new(ports), + }; + }; + + MessageEvent { + origin: RefCell::new(init.origin), + last_event_id: RefCell::new(init.last_event_id), + data: RefCell::new(data), + source: RefCell::new(source), + ports: RefCell::new(ports), + } + } +} + +#[op2(inherit = Event)] +impl MessageEvent { + #[constructor] + #[required(1)] + #[cppgc] + fn constructor<'a>( + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + init: v8::Local<'a, v8::Value>, + ) -> Result<(Event, MessageEvent), EventError> { + if init.is_null_or_undefined() { + let ports = v8::Array::new(scope, 0); + return Ok(( + Event::new(typ, None), + MessageEvent::new(None, None, None, Global::new(scope, ports)), + )); + } + + let prefix = "Failed to construct 'MessageEvent'"; + let message_event_init = Nullable::::convert( + scope, + init, + prefix.into(), + (|| "Argument 2".into()).into(), + &Default::default(), + )?; + let message_event_init = message_event_init.into_option(); + let event = if let Some(ref message_event_init) = message_event_init { + let event_init = EventInit { + bubbles: message_event_init.bubbles, + cancelable: message_event_init.cancelable, + composed: message_event_init.composed, + }; + Event::new(typ, Some(event_init)) + } else { + Event::new(typ, None) + }; + + let (data, source, ports) = if init.is_object() + && let Some(init) = init.to_object(scope) + { + let data = get_value(scope, init, "data") + .map(|value| v8::Global::new(scope, value)); + // TODO(petamoriken): Validate Window or MessagePort + let source = if let Some(source) = get_value(scope, init, "source") { + if source.is_object() + && let Some(source) = source.to_object(scope) + { + Some(v8::Global::new(scope, source)) + } else { + return Err(EventError::WebIDL(WebIdlError::new( + prefix.into(), + (|| "'source' of 'MessageEventInit' (Argument 2)".into()).into(), + WebIdlErrorKind::ConvertToConverterType("object"), + ))); + } + } else { + None + }; + // TODO(petamoriken): Validate sequence + let ports = if let Some(ports) = get_value(scope, init, "ports") { + let context = || "'ports' of 'MessageEventInit' (Argument 2)".into(); + let elements = Vec::>::convert( + scope, + ports, + prefix.into(), + context.into(), + &Default::default(), + )?; + if elements.iter().any(|element| !element.is_object()) { + return Err(EventError::WebIDL(WebIdlError::new( + prefix.into(), + context.into(), + WebIdlErrorKind::ConvertToConverterType("sequence"), + ))); + } + v8::Array::new_with_elements(scope, &elements) + } else { + v8::Array::new(scope, 0) + }; + ports.set_integrity_level(scope, v8::IntegrityLevel::Frozen); + let ports = v8::Global::new(scope, ports); + (data, source, ports) + } else { + let ports = v8::Array::new(scope, 0); + ports.set_integrity_level(scope, v8::IntegrityLevel::Frozen); + let ports = v8::Global::new(scope, ports); + (None, None, ports) + }; + let message_event = + MessageEvent::new(message_event_init, data, source, ports); + Ok((event, message_event)) + } + + // legacy + #[required(1)] + fn init_message_event<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + #[webidl] bubbles: Option, + #[webidl] cancelable: Option, + #[global] data: Option>, + #[webidl] origin: Option, + #[webidl] last_event_id: Option, + #[global] source: Option>, + ports: Option>, + #[proto] event: &Event, + ) -> Result, EventError> { + let undefined = v8::undefined(scope); + if event.dispatch_flag.get() { + return Ok(undefined); + } + + event.typ.replace(typ); + if let Some(bubbles) = bubbles { + event.bubbles.replace(bubbles); + } + if let Some(cancelable) = cancelable { + event.cancelable.replace(cancelable); + } + if data.is_some() { + self.data.replace(data); + } + if let Some(origin) = origin { + self.origin.replace(origin); + } + if let Some(last_event_id) = last_event_id { + self.last_event_id.replace(last_event_id); + } + // TODO(petamoriken): Validate Window or MessagePort + if source.is_some() { + self.source.replace(source); + } + // TODO(petamoriken): Validate sequence + if let Some(ports) = ports { + let prefix = "Failed to execute 'initMessageEvent' on 'MessageEvent'"; + let context = || "Argument 8".into(); + let elements = Vec::>::convert( + scope, + ports, + prefix.into(), + context.into(), + &Default::default(), + )?; + if elements.iter().any(|element| !element.is_object()) { + return Err(EventError::WebIDL(WebIdlError::new( + prefix.into(), + context.into(), + WebIdlErrorKind::ConvertToConverterType("sequence"), + ))); + } + let ports = v8::Array::new_with_elements(scope, &elements); + ports.set_integrity_level(scope, v8::IntegrityLevel::Frozen); + let ports = v8::Global::new(scope, ports); + self.ports.replace(ports); + } + Ok(undefined) + } + + #[getter] + #[string] + fn origin(&self) -> String { + self.origin.borrow().clone() + } + + #[getter] + #[string] + fn last_event_id(&self) -> String { + self.last_event_id.borrow().clone() + } + + #[getter] + #[global] + fn data(&self) -> Option> { + self.data.borrow().clone() + } + + #[getter] + #[global] + fn source(&self) -> Option> { + self.source.borrow().clone() + } + + #[getter] + #[global] + fn ports(&self) -> v8::Global { + self.ports.borrow().clone() + } +} + // TODO(petamorken): list // report error -// MessageEvent // PromiseRejectionEvent -// CustomEvent // ProgressEvent #[inline] diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 30c8a026b8..4aa949cf1e 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -102,8 +102,10 @@ deno_core::extension!(deno_web, objects = [ event::Event, event::EventTarget, + event::CustomEvent, event::ErrorEvent, event::CloseEvent, + event::MessageEvent, ], esm = [ "00_infra.js", From c5d14f18ffa7967f7c865ccdd0b06f9ef6cc1dd2 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Mon, 11 Aug 2025 02:30:47 +0900 Subject: [PATCH 03/18] wrap PromiseRejectionError --- ext/web/02_event.js | 83 +++++++++++++++++++++------------------------ ext/web/event.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++- ext/web/lib.rs | 1 + 3 files changed, 121 insertions(+), 46 deletions(-) diff --git a/ext/web/02_event.js b/ext/web/02_event.js index 0e2ea43928..a1627b0c11 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -35,6 +35,7 @@ import { op_event_set_is_trusted, op_event_set_target, op_event_wrap_event_target, + PromiseRejectionEvent, } from "ext:core/ops"; import * as webidl from "ext:deno_webidl/00_webidl.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; @@ -292,6 +293,43 @@ const ERROR_EVENT_PROPS = [ defineEnumerableProps(ErrorEvent.prototype, ERROR_EVENT_PROPS); +webidl.configureInterface(PromiseRejectionEvent); +const PromiseRejectionEventPrototype = PromiseRejectionEvent.prototype; + +ObjectDefineProperty( + PromiseRejectionEvent.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf( + PromiseRejectionEventPrototype, + this, + ), + keys: ArrayPrototypeFlat([ + EVENT_PROPS, + PROMISE_REJECTION_EVENT_PROPS, + ]), + }), + inspectOptions, + ); + }, + }, +); + +const PROMISE_REJECTION_EVENT_PROPS = [ + "promise", + "reason", +]; + +defineEnumerableProps( + PromiseRejectionEvent.prototype, + PROMISE_REJECTION_EVENT_PROPS, +); + webidl.configureInterface(CloseEvent); const CloseEventPrototype = CloseEvent.prototype; @@ -389,51 +427,6 @@ class ProgressEvent extends Event { webidl.configureInterface(ProgressEvent); const ProgressEventPrototype = ProgressEvent.prototype; -class PromiseRejectionEvent extends Event { - #promise = null; - #reason = null; - - get promise() { - return this.#promise; - } - get reason() { - return this.#reason; - } - - constructor( - type, - { bubbles, cancelable, composed, promise, reason } = { __proto__: null }, - ) { - super(type, { - bubbles: bubbles, - cancelable: cancelable, - composed: composed, - }); - - this.#promise = promise; - this.#reason = reason; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf( - PromiseRejectionEventPrototype, - this, - ), - keys: [...new SafeArrayIterator(EVENT_PROPS), "promise", "reason"], - }), - inspectOptions, - ); - } -} - -webidl.configureInterface(PromiseRejectionEvent); -const PromiseRejectionEventPrototype = PromiseRejectionEvent.prototype; - -defineEnumerableProps(PromiseRejectionEvent.prototype, ["promise", "reason"]); - const _eventHandlers = Symbol("eventHandlers"); function makeWrappedHandler(handler, isSpecialErrorEventHandler) { diff --git a/ext/web/event.rs b/ext/web/event.rs index 1ef1860043..270f0e1497 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -907,6 +907,88 @@ impl ErrorEvent { } } +#[derive(Debug)] +pub struct PromiseRejectionEvent { + promise: v8::Global, + reason: Option>, +} + +impl GarbageCollected for PromiseRejectionEvent { + fn get_name(&self) -> &'static std::ffi::CStr { + c"PromiseRejectionEvent" + } +} + +impl PromiseRejectionEvent { + fn new( + promise: v8::Global, + reason: Option>, + ) -> PromiseRejectionEvent { + PromiseRejectionEvent { promise, reason } + } +} + +#[op2(inherit = Event)] +impl PromiseRejectionEvent { + #[constructor] + #[required(1)] + #[cppgc] + fn constructor<'a>( + scope: &mut v8::HandleScope<'a>, + #[webidl] typ: String, + init: v8::Local<'a, v8::Object>, + ) -> Result<(Event, PromiseRejectionEvent), EventError> { + let prefix = "Failed to construct 'PromiseRejectionEvent'"; + let event_init = EventInit::convert( + scope, + init.into(), + prefix.into(), + (|| "Argument 2".into()).into(), + &Default::default(), + )?; + let event = Event::new(typ, Some(event_init)); + + let promise = { + let promise = get_value(scope, init, "promise"); + if let Some(promise) = promise + && promise.is_object() + && let Some(promise) = promise.to_object(scope) + { + v8::Global::new(scope, promise) + } else { + return Err(EventError::WebIDL(WebIdlError::new( + prefix.into(), + (|| "'promise' of 'PromiseRejectionEventInit' (Argument 2)".into()) + .into(), + WebIdlErrorKind::ConvertToConverterType("object"), + ))); + } + }; + let reason = get_value(scope, init, "reason") + .map(|reason| v8::Global::new(scope, reason)); + let promise_rejection_event = PromiseRejectionEvent::new(promise, reason); + Ok((event, promise_rejection_event)) + } + + #[getter] + #[global] + fn promise(&self) -> v8::Global { + self.promise.clone() + } + + #[getter] + fn reason<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + ) -> v8::Local<'a, v8::Value> { + if let Some(reason) = &self.reason { + v8::Local::new(scope, reason) + } else { + v8::undefined(scope).into() + } + } +} + #[derive(WebIDL, Debug)] #[webidl(dictionary)] pub struct CloseEventInit { @@ -1250,7 +1332,6 @@ impl MessageEvent { // TODO(petamorken): list // report error -// PromiseRejectionEvent // ProgressEvent #[inline] diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 4aa949cf1e..d8de9c90fb 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -104,6 +104,7 @@ deno_core::extension!(deno_web, event::EventTarget, event::CustomEvent, event::ErrorEvent, + event::PromiseRejectionEvent, event::CloseEvent, event::MessageEvent, ], From 1f946837372615de3608d6e0419ccb3ea6bdda02 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Mon, 11 Aug 2025 03:13:34 +0900 Subject: [PATCH 04/18] wrap ProgressEvent --- ext/web/02_event.js | 59 ++++++++++++++--------------- ext/web/event.rs | 90 ++++++++++++++++++++++++++++++++++++++++++++- ext/web/lib.rs | 1 + 3 files changed, 120 insertions(+), 30 deletions(-) diff --git a/ext/web/02_event.js b/ext/web/02_event.js index a1627b0c11..dee93f783d 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -15,7 +15,6 @@ const { ObjectDefineProperty, ObjectDefineProperties, ObjectPrototypeIsPrototypeOf, - SafeArrayIterator, SafeMap, StringPrototypeStartsWith, Symbol, @@ -35,6 +34,7 @@ import { op_event_set_is_trusted, op_event_set_target, op_event_wrap_event_target, + ProgressEvent, PromiseRejectionEvent, } from "ext:core/ops"; import * as webidl from "ext:deno_webidl/00_webidl.js"; @@ -396,37 +396,38 @@ const MESSAGE_EVENT_PROPS = [ defineEnumerableProps(MessageEvent.prototype, MESSAGE_EVENT_PROPS); -// ProgressEvent could also be used in other DOM progress event emits. -// Current use is for FileReader. -class ProgressEvent extends Event { - constructor(type, eventInitDict = { __proto__: null }) { - super(type, eventInitDict); - - this.lengthComputable = eventInitDict?.lengthComputable ?? false; - this.loaded = eventInitDict?.loaded ?? 0; - this.total = eventInitDict?.total ?? 0; - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(ProgressEventPrototype, this), - keys: [ - ...new SafeArrayIterator(EVENT_PROPS), - "lengthComputable", - "loaded", - "total", - ], - }), - inspectOptions, - ); - } -} - webidl.configureInterface(ProgressEvent); const ProgressEventPrototype = ProgressEvent.prototype; +ObjectDefineProperty( + ProgressEvent.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(ProgressEventPrototype, this), + keys: ArrayPrototypeFlat([ + EVENT_PROPS, + PROGRESS_EVENT_PROPS, + ]), + }), + inspectOptions, + ); + }, + }, +); + +const PROGRESS_EVENT_PROPS = [ + "lengthComputable", + "loaded", + "total", +]; + +defineEnumerableProps(ProgressEvent.prototype, PROGRESS_EVENT_PROPS); + const _eventHandlers = Symbol("eventHandlers"); function makeWrappedHandler(handler, isSpecialErrorEventHandler) { diff --git a/ext/web/event.rs b/ext/web/event.rs index 270f0e1497..59548066ec 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -1330,9 +1330,97 @@ impl MessageEvent { } } +#[derive(WebIDL, Debug)] +#[webidl(dictionary)] +pub struct ProgressEventInit { + #[webidl(default = false)] + bubbles: bool, + #[webidl(default = false)] + cancelable: bool, + #[webidl(default = false)] + composed: bool, + #[webidl(default = false)] + length_computable: bool, + #[webidl(default = 0.0)] + loaded: f64, + #[webidl(default = 0.0)] + total: f64, +} + +#[derive(Debug)] +pub struct ProgressEvent { + length_computable: bool, + loaded: f64, + total: f64, +} + +impl GarbageCollected for ProgressEvent { + fn get_name(&self) -> &'static std::ffi::CStr { + c"ProgressEvent" + } +} + +impl ProgressEvent { + #[inline] + fn new(init: Option) -> ProgressEvent { + let Some(init) = init else { + return ProgressEvent { + length_computable: false, + loaded: 0.0, + total: 0.0, + }; + }; + + ProgressEvent { + length_computable: init.length_computable, + loaded: init.loaded, + total: init.total, + } + } +} + +#[op2(inherit = Event)] +impl ProgressEvent { + #[constructor] + #[required(1)] + #[cppgc] + fn constructor<'a>( + #[webidl] typ: String, + #[webidl] init: Nullable, + ) -> (Event, ProgressEvent) { + let init = init.into_option(); + let event = if let Some(ref init) = init { + let event_init = EventInit { + bubbles: init.bubbles, + cancelable: init.cancelable, + composed: init.composed, + }; + Event::new(typ, Some(event_init)) + } else { + Event::new(typ, None) + }; + let progress_event = ProgressEvent::new(init); + (event, progress_event) + } + + #[getter] + fn length_computable(&self) -> bool { + self.length_computable + } + + #[getter] + fn loaded(&self) -> f64 { + self.loaded + } + + #[getter] + fn total(&self) -> f64 { + self.total + } +} + // TODO(petamorken): list // report error -// ProgressEvent #[inline] fn get_value<'a>( diff --git a/ext/web/lib.rs b/ext/web/lib.rs index d8de9c90fb..75dd420ac4 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -107,6 +107,7 @@ deno_core::extension!(deno_web, event::PromiseRejectionEvent, event::CloseEvent, event::MessageEvent, + event::ProgressEvent, ], esm = [ "00_infra.js", From b082bb2883ad74097b5b986b6b3e875a61f35136 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Thu, 14 Aug 2025 00:45:49 +0900 Subject: [PATCH 05/18] impl op_event_report_exception --- Cargo.lock | 9 +- Cargo.toml | 2 +- ext/web/02_event.js | 69 +----------- ext/web/event.rs | 251 +++++++++++++++++++++++++++++++++--------- ext/web/lib.rs | 4 + runtime/js/99_main.js | 1 - 6 files changed, 215 insertions(+), 121 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45e873fdbc..4639cb799a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1845,8 +1845,7 @@ dependencies = [ [[package]] name = "deno_core" version = "0.355.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "775d2fde80a2ec3116d179703b38346a931bb9626f4a826148d5fe8631cab29f" +source = "git+https://github.com/petamoriken/deno_core?branch=feat%2Fexport-dispatch-exception#240abafcd2e8155abbed0c9d44d02f0035364f26" dependencies = [ "anyhow", "az", @@ -2580,8 +2579,7 @@ dependencies = [ [[package]] name = "deno_ops" version = "0.231.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ca530772bbcbc9ad389ad7bcd86623b2ec555f68a2d062d23cc008915cbe781" +source = "git+https://github.com/petamoriken/deno_core?branch=feat%2Fexport-dispatch-exception#240abafcd2e8155abbed0c9d44d02f0035364f26" dependencies = [ "indexmap 2.9.0", "proc-macro-rules", @@ -7960,8 +7958,7 @@ dependencies = [ [[package]] name = "serde_v8" version = "0.264.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c34707712f3815e73e1c8319bba06e5bc105bb65fe812ea2e7279ffb905f6312" +source = "git+https://github.com/petamoriken/deno_core?branch=feat%2Fexport-dispatch-exception#240abafcd2e8155abbed0c9d44d02f0035364f26" dependencies = [ "deno_error", "num-bigint", diff --git a/Cargo.toml b/Cargo.toml index 2f55c9ca3e..19ff0d796d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,7 +61,7 @@ repository = "https://github.com/denoland/deno" [workspace.dependencies] deno_ast = { version = "=0.49", features = ["transpiling"] } -deno_core = { version = "0.355.0" } +deno_core = { git = "https://github.com/petamoriken/deno_core", branch = "feat/export-dispatch-exception" } deno_cache_dir = "=0.25.0" deno_doc = "=0.182.0" diff --git a/ext/web/02_event.js b/ext/web/02_event.js index dee93f783d..1d199edc01 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -5,10 +5,9 @@ // parts still exists. This means you will observe a lot of strange structures // and impossible logic branches based on what Deno currently supports. -import { core, primordials } from "ext:core/mod.js"; +import { primordials } from "ext:core/mod.js"; const { ArrayPrototypeFlat, - Error, FunctionPrototypeCall, MapPrototypeGet, MapPrototypeSet, @@ -16,10 +15,8 @@ const { ObjectDefineProperties, ObjectPrototypeIsPrototypeOf, SafeMap, - StringPrototypeStartsWith, Symbol, SymbolFor, - TypeError, } = primordials; import { CloseEvent, @@ -31,6 +28,8 @@ import { op_event_dispatch, op_event_get_target_listener_count, op_event_get_target_listeners, + op_event_report_error, + op_event_report_exception, op_event_set_is_trusted, op_event_set_target, op_event_wrap_event_target, @@ -40,14 +39,6 @@ import { import * as webidl from "ext:deno_webidl/00_webidl.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; -// This should be set via setGlobalThis this is required so that if even -// user deletes globalThis it is still usable -let globalThis_; - -function saveGlobalThisReference(val) { - globalThis_ = val; -} - function defineEnumerableProps(prototype, props) { for (let i = 0; i < props.length; ++i) { const prop = props[i]; @@ -509,63 +500,14 @@ function defineEventHandler( }); } -let reportExceptionStackedCalls = 0; - // https://html.spec.whatwg.org/#report-the-exception function reportException(error) { - reportExceptionStackedCalls++; - const jsError = core.destructureError(error); - const message = jsError.exceptionMessage; - let filename = ""; - let lineno = 0; - let colno = 0; - if (jsError.frames.length > 0) { - filename = jsError.frames[0].fileName; - lineno = jsError.frames[0].lineNumber; - colno = jsError.frames[0].columnNumber; - } else { - const jsError = core.destructureError(new Error()); - const frames = jsError.frames; - for (let i = 0; i < frames.length; ++i) { - const frame = frames[i]; - if ( - typeof frame.fileName == "string" && - !StringPrototypeStartsWith(frame.fileName, "ext:") - ) { - filename = frame.fileName; - lineno = frame.lineNumber; - colno = frame.columnNumber; - break; - } - } - } - const event = new ErrorEvent("error", { - cancelable: true, - message, - filename, - lineno, - colno, - error, - }); - // Avoid recursing `reportException()` via error handlers more than once. - if (reportExceptionStackedCalls > 1 || globalThis_.dispatchEvent(event)) { - core.reportUnhandledException(error); - } - reportExceptionStackedCalls--; -} - -function checkThis(thisArg) { - if (thisArg !== null && thisArg !== undefined && thisArg !== globalThis_) { - throw new TypeError("Illegal invocation"); - } + op_event_report_exception(error); } // https://html.spec.whatwg.org/#dom-reporterror function reportError(error) { - checkThis(this); - const prefix = "Failed to execute 'reportError'"; - webidl.requiredArguments(arguments.length, 1, prefix); - reportException(error); + op_event_report_error(error); } export { @@ -584,7 +526,6 @@ export { PromiseRejectionEvent, reportError, reportException, - saveGlobalThisReference, setEventTargetData, setIsTrusted, setTarget, diff --git a/ext/web/event.rs b/ext/web/event.rs index 59548066ec..15d3c65a8c 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -7,8 +7,11 @@ use std::collections::VecDeque; use std::rc::Rc; use deno_core::GarbageCollected; +use deno_core::OpState; use deno_core::WebIDL; use deno_core::cppgc; +use deno_core::error::JsError; +use deno_core::error::dispatch_exception; use deno_core::op2; use deno_core::v8; use deno_core::v8::Global; @@ -25,6 +28,9 @@ pub enum EventError { #[class(type)] #[error("parameter 1 is expected Event")] ExpectedEvent, + #[class(type)] + #[error("Illegal invocation")] + Illegal, #[class("DOMExceptionInvalidStateError")] #[error("Invalid event state")] InvalidState, @@ -143,6 +149,7 @@ impl Event { fn dispatch<'a>( &self, scope: &mut v8::HandleScope<'a>, + state: &mut OpState, event_object: v8::Local<'a, v8::Object>, target: &EventTarget, target_object: v8::Global, @@ -168,40 +175,48 @@ impl Event { ); // 6.13. - for (path_index, path) in self.path.borrow().iter().enumerate().rev() { - if path.shadow_adjusted_target.is_none() { - self.event_phase.replace(EventPhase::CapturingPhase); + { + let event_path = self.path.borrow(); + for (path_index, path) in event_path.iter().enumerate().rev() { + if path.shadow_adjusted_target.is_none() { + self.event_phase.replace(EventPhase::CapturingPhase); + self.invoke( + scope, + state, + event_object, + target, + target_object.clone(), + &event_path, + path_index, + InvokePhase::Capturing, + ); + } + } + + // 6.14. + for (path_index, path) in event_path.iter().enumerate() { + if path.shadow_adjusted_target.is_some() { + self.event_phase.replace(EventPhase::AtTarget); + } else { + if !self.bubbles.get() { + continue; + } + self.event_phase.replace(EventPhase::BubblingPhase); + } + self.invoke( scope, + state, event_object, target, target_object.clone(), + &event_path, path_index, - InvokePhase::Capturing, + InvokePhase::Bubbling, ); } } - // 6.14. - for (path_index, path) in self.path.borrow().iter().enumerate() { - if path.shadow_adjusted_target.is_some() { - self.event_phase.replace(EventPhase::AtTarget); - } else { - if !self.bubbles.get() { - continue; - } - self.event_phase.replace(EventPhase::BubblingPhase); - } - self.invoke( - scope, - event_object, - target, - target_object.clone(), - path_index, - InvokePhase::Bubbling, - ); - } - // 7. self.event_phase.replace(EventPhase::None); @@ -243,14 +258,14 @@ impl Event { fn invoke<'a>( &self, scope: &mut v8::HandleScope<'a>, + state: &mut OpState, event_object: v8::Local<'a, v8::Object>, target: &EventTarget, target_object: v8::Global, + path: &Vec, path_index: usize, phase: InvokePhase, ) { - let path = self.path.borrow(); - // 1. for (index, current) in path.iter().enumerate().rev() { if let Some(target) = ¤t.shadow_adjusted_target { @@ -277,18 +292,23 @@ impl Event { .replace(Some(current.invocation_target.clone())); // 6. - // Against the spec, clone event listeners in inner_invoke let typ = self.typ.borrow(); - let mut listeners = target.listeners.borrow_mut(); - let Some(listeners) = listeners.get_mut(&*typ) else { - return; + let listeners = { + let listeners = target.listeners.borrow(); + let Some(listeners) = listeners.get(&*typ) else { + return; + }; + listeners.clone() }; // 8. let _ = self.inner_invoke( scope, + state, event_object, target_object.clone(), + &typ, + target, listeners, phase, ); @@ -299,9 +319,12 @@ impl Event { fn inner_invoke<'a>( &self, scope: &mut v8::HandleScope<'a>, + state: &mut OpState, event_object: v8::Local<'a, v8::Object>, target_object: v8::Global, - listeners: &mut Vec>, + typ: &String, + target: &EventTarget, + listeners: Vec>, phase: InvokePhase, ) -> bool { // NOTE: Omit implementations for window.event (current event) @@ -310,10 +333,8 @@ impl Event { let mut found = false; // 2. - // Clone event listeners before iterating since the list can be modified during the iteration. - for listener in listeners.clone().iter() { - // Check if the event listener has been removed since the listeners has been cloned. - if !listeners.iter().any(|l| Rc::ptr_eq(l, listener)) { + for listener in listeners.iter() { + if listener.removed.get() { continue; } @@ -330,6 +351,9 @@ impl Event { // 5. if listener.once { + let mut listeners = target.listeners.borrow_mut(); + let listeners = listeners.get_mut(typ).unwrap(); + listener.removed.set(true); listeners.remove( listeners .iter() @@ -362,14 +386,16 @@ impl Event { } // 11.1. Err(error) => { - // TODO(petamoriken): report exception + let message = v8::String::new(scope, &error.to_string()).unwrap(); + let exception = v8::Exception::type_error(scope, message); + report_exception(scope, state, exception); } } } // 11.1. if let Some(exception) = scope.exception() { - // TODO(petamoriken): report exception + report_exception(scope, state, exception); } // 12. @@ -661,6 +687,7 @@ pub fn op_event_set_target( #[op2(reentrant)] pub fn op_event_dispatch<'a>( scope: &mut v8::HandleScope<'a>, + state: &mut OpState, #[global] target_object: v8::Global, event_object: v8::Local<'a, v8::Object>, #[global] target_override: Option>, @@ -672,7 +699,14 @@ pub fn op_event_dispatch<'a>( let event = cppgc::try_unwrap_cppgc_proto_object::(scope, event_object.into()) .unwrap(); - event.dispatch(scope, event_object, &target, target_object, target_override) + event.dispatch( + scope, + state, + event_object, + &target, + target_object, + target_override, + ) } #[derive(Debug)] @@ -1419,8 +1453,119 @@ impl ProgressEvent { } } -// TODO(petamorken): list -// report error +pub(crate) struct ReportExceptionStackedCalls(u32); + +impl Default for ReportExceptionStackedCalls { + fn default() -> Self { + ReportExceptionStackedCalls(0) + } +} + +// https://html.spec.whatwg.org/#report-the-exception +fn report_exception<'a>( + scope: &mut v8::HandleScope<'a>, + state: &mut OpState, + exception: v8::Local<'a, v8::Value>, +) { + // Avoid recursing `reportException()` via error handlers more than once. + let callable = { + let stacked_calls = state.borrow_mut::(); + stacked_calls.0 += 1; + stacked_calls.0 == 1 + }; + + let allow_default = if callable { + let js_error = JsError::from_v8_exception(scope, exception); + let message = js_error.message; + let (file_name, line_number, column_number) = + if let Some(frame) = js_error.frames.first() { + ( + frame.file_name.clone(), + frame.line_number, + frame.column_number, + ) + } else { + let message = v8::String::empty(scope); + let exception = v8::Exception::error(scope, message); + let js_error = JsError::from_v8_exception(scope, exception); + if let Some(frame) = js_error.frames.iter().find(|frame| { + frame + .file_name + .as_ref() + .is_some_and(|file_name| !file_name.starts_with("ext:")) + }) { + ( + frame.file_name.clone(), + frame.line_number, + frame.column_number, + ) + } else { + (None, None, None) + } + }; + let event_object = { + let event = Event::new( + "error".into(), + Some(EventInit { + bubbles: false, + cancelable: true, + composed: false, + }), + ); + let error_event = ErrorEvent { + message: message.unwrap_or(String::new()), + filename: file_name.unwrap_or(String::new()), + lineno: line_number.unwrap_or(0) as u32, + colno: column_number.unwrap_or(0) as u32, + error: Some(v8::Global::new(scope, exception)), + }; + let event_object = cppgc::make_cppgc_empty_object::(scope); + cppgc::wrap_object2(scope, event_object, (event, error_event)) + }; + let event = + cppgc::try_unwrap_cppgc_proto_object::(scope, event_object.into()) + .unwrap(); + let global = scope.get_current_context().global(scope); + let target = + cppgc::try_unwrap_cppgc_object::(scope, global.into()) + .unwrap(); + let global = v8::Global::new(scope, global); + event.dispatch(scope, state, event_object, &target, global, None) + } else { + true + }; + + if allow_default { + dispatch_exception(scope, exception, false); + } + + let stacked_calls = state.borrow_mut::(); + stacked_calls.0 -= 1; +} + +#[op2(fast, reentrant, required(1))] +pub fn op_event_report_exception<'a>( + scope: &mut v8::HandleScope<'a>, + state: &mut OpState, + exception: v8::Local<'a, v8::Value>, +) { + report_exception(scope, state, exception); +} + +#[op2(fast, reentrant, required(1))] +pub fn op_event_report_error<'a>( + #[this] this: v8::Global, + scope: &mut v8::HandleScope<'a>, + state: &mut OpState, + exception: v8::Local<'a, v8::Value>, +) -> Result<(), EventError> { + let global = scope.get_current_context().global(scope); + if global != this { + return Err(EventError::Illegal); + } + report_exception(scope, state, exception); + Ok(()) +} #[inline] fn get_value<'a>( @@ -1445,6 +1590,7 @@ struct EventListener { passive: bool, once: bool, signal: Option>, + removed: Cell, // This field exists for simulating Node.js behavior, implemented in https://github.com/nodejs/node/commit/bcd35c334ec75402ee081f1c4da128c339f70c24 // Some internal event listeners in Node.js can ignore `e.stopImmediatePropagation()` calls from the earlier event listeners. resist_stop_immediate_propagation: bool, @@ -1556,6 +1702,7 @@ impl EventTarget { passive, once, signal: None, + removed: Cell::new(false), resist_stop_immediate_propagation, })); @@ -1604,9 +1751,12 @@ impl EventTarget { let Some(listeners) = listeners.get_mut(&typ) else { return; }; - if let Some(index) = listeners.iter().position(|listener| { - listener.capture == capture && listener.callback == callback - }) { + if let Some((index, listener)) = + listeners.iter().enumerate().find(|(_, listener)| { + listener.capture == capture && listener.callback == callback + }) + { + listener.removed.set(true); listeners.remove(index); } } @@ -1618,6 +1768,7 @@ impl EventTarget { &self, #[this] this: v8::Global, scope: &mut v8::HandleScope<'a>, + state: &mut OpState, event_object: v8::Local<'a, v8::Object>, ) -> Result { let Some(event) = @@ -1640,11 +1791,13 @@ impl EventTarget { global.set(scope, symbol.into(), value.into()); } - let listeners = self.listeners.borrow(); - if listeners.get(&*typ).is_none() { - event.target.replace(Some(this)); - return Ok(true); - }; + { + let listeners = self.listeners.borrow(); + if listeners.get(&*typ).is_none() { + event.target.replace(Some(this)); + return Ok(true); + }; + } if event.dispatch_flag.get() || !matches!(*event.event_phase.borrow(), EventPhase::None) @@ -1652,7 +1805,7 @@ impl EventTarget { return Err(EventError::InvalidState); } - Ok(event.dispatch(scope, event_object, self, this, None)) + Ok(event.dispatch(scope, state, event_object, self, this, None)) } } diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 75dd420ac4..40d3d240fe 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -37,6 +37,7 @@ use crate::blob::op_blob_read_part; use crate::blob::op_blob_remove_part; use crate::blob::op_blob_revoke_object_url; use crate::blob::op_blob_slice_part; +use crate::event::ReportExceptionStackedCalls; pub use crate::message_port::JsMessageData; pub use crate::message_port::MessagePort; pub use crate::message_port::Transferable; @@ -87,6 +88,8 @@ deno_core::extension!(deno_web, event::op_event_set_is_trusted, event::op_event_set_target, event::op_event_wrap_event_target, + event::op_event_report_error, + event::op_event_report_exception, op_now

, op_time_origin

, op_defer, @@ -139,6 +142,7 @@ deno_core::extension!(deno_web, if let Some(location) = options.maybe_location { state.put(Location(location)); } + state.put(ReportExceptionStackedCalls::default()); state.put(StartTime::default()); } ); diff --git a/runtime/js/99_main.js b/runtime/js/99_main.js index 51cb03bfcd..8c52e84752 100644 --- a/runtime/js/99_main.js +++ b/runtime/js/99_main.js @@ -1156,7 +1156,6 @@ globalThis.bootstrap = { dispatchProcessBeforeExitEvent, }; -event.saveGlobalThisReference(globalThis); event.defineEventHandler(globalThis, "unhandledrejection"); removeImportedOps(); From 30dfba893dfd38ea1fed95f3033490e011f1f823 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Sun, 17 Aug 2025 13:36:48 +0900 Subject: [PATCH 06/18] fix OpState RefCell borrowing error --- ext/web/event.rs | 26 ++++++++++++++------------ runtime/ops/bootstrap.rs | 6 +++--- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/ext/web/event.rs b/ext/web/event.rs index 15d3c65a8c..a98e6cd0ba 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -149,7 +149,7 @@ impl Event { fn dispatch<'a>( &self, scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: &Rc>, event_object: v8::Local<'a, v8::Object>, target: &EventTarget, target_object: v8::Global, @@ -258,7 +258,7 @@ impl Event { fn invoke<'a>( &self, scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: &Rc>, event_object: v8::Local<'a, v8::Object>, target: &EventTarget, target_object: v8::Global, @@ -319,7 +319,7 @@ impl Event { fn inner_invoke<'a>( &self, scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: &Rc>, event_object: v8::Local<'a, v8::Object>, target_object: v8::Global, typ: &String, @@ -687,7 +687,7 @@ pub fn op_event_set_target( #[op2(reentrant)] pub fn op_event_dispatch<'a>( scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: Rc>, #[global] target_object: v8::Global, event_object: v8::Local<'a, v8::Object>, #[global] target_override: Option>, @@ -701,7 +701,7 @@ pub fn op_event_dispatch<'a>( .unwrap(); event.dispatch( scope, - state, + &state, event_object, &target, target_object, @@ -1464,11 +1464,12 @@ impl Default for ReportExceptionStackedCalls { // https://html.spec.whatwg.org/#report-the-exception fn report_exception<'a>( scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: &Rc>, exception: v8::Local<'a, v8::Value>, ) { // Avoid recursing `reportException()` via error handlers more than once. let callable = { + let mut state = state.borrow_mut(); let stacked_calls = state.borrow_mut::(); stacked_calls.0 += 1; stacked_calls.0 == 1 @@ -1539,6 +1540,7 @@ fn report_exception<'a>( dispatch_exception(scope, exception, false); } + let mut state = state.borrow_mut(); let stacked_calls = state.borrow_mut::(); stacked_calls.0 -= 1; } @@ -1546,24 +1548,24 @@ fn report_exception<'a>( #[op2(fast, reentrant, required(1))] pub fn op_event_report_exception<'a>( scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: Rc>, exception: v8::Local<'a, v8::Value>, ) { - report_exception(scope, state, exception); + report_exception(scope, &state, exception); } #[op2(fast, reentrant, required(1))] pub fn op_event_report_error<'a>( #[this] this: v8::Global, scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: Rc>, exception: v8::Local<'a, v8::Value>, ) -> Result<(), EventError> { let global = scope.get_current_context().global(scope); if global != this { return Err(EventError::Illegal); } - report_exception(scope, state, exception); + report_exception(scope, &state, exception); Ok(()) } @@ -1768,7 +1770,7 @@ impl EventTarget { &self, #[this] this: v8::Global, scope: &mut v8::HandleScope<'a>, - state: &mut OpState, + state: Rc>, event_object: v8::Local<'a, v8::Object>, ) -> Result { let Some(event) = @@ -1805,7 +1807,7 @@ impl EventTarget { return Err(EventError::InvalidState); } - Ok(event.dispatch(scope, state, event_object, self, this, None)) + Ok(event.dispatch(scope, &state, event_object, self, this, None)) } } diff --git a/runtime/ops/bootstrap.rs b/runtime/ops/bootstrap.rs index ff01d37b66..330c47a0b8 100644 --- a/runtime/ops/bootstrap.rs +++ b/runtime/ops/bootstrap.rs @@ -142,12 +142,12 @@ pub fn op_bootstrap_color_depth(state: &mut OpState) -> i32 { } #[op2(fast)] -pub fn op_bootstrap_no_color(_state: &mut OpState) -> bool { +pub fn op_bootstrap_no_color() -> bool { !deno_terminal::colors::use_color() } #[op2(fast)] -pub fn op_bootstrap_stdout_no_color(_state: &mut OpState) -> bool { +pub fn op_bootstrap_stdout_no_color() -> bool { if deno_terminal::colors::force_color() { return false; } @@ -156,7 +156,7 @@ pub fn op_bootstrap_stdout_no_color(_state: &mut OpState) -> bool { } #[op2(fast)] -pub fn op_bootstrap_stderr_no_color(_state: &mut OpState) -> bool { +pub fn op_bootstrap_stderr_no_color() -> bool { if deno_terminal::colors::force_color() { return false; } From 3c174f9ed113a7add9a7575cab77f5413a94bbe4 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Tue, 19 Aug 2025 04:40:28 +0900 Subject: [PATCH 07/18] refactor --- ext/web/event.rs | 97 +++++++++++++++++++++++++++--------------------- 1 file changed, 55 insertions(+), 42 deletions(-) diff --git a/ext/web/event.rs b/ext/web/event.rs index a98e6cd0ba..91b6dd15fe 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -427,6 +427,7 @@ impl Event { } // legacy + // TODO(petamoriken): #[undefined] macro does not work properly #[required(1)] fn init_event<'a>( &self, @@ -765,6 +766,7 @@ impl CustomEvent { } // legacy + // TODO(petamoriken): #[undefined] macro does not work properly #[required(1)] fn init_custom_event<'a>( &self, @@ -1269,6 +1271,7 @@ impl MessageEvent { // legacy #[required(1)] + #[undefined] fn init_message_event<'a>( &self, scope: &mut v8::HandleScope<'a>, @@ -1281,10 +1284,9 @@ impl MessageEvent { #[global] source: Option>, ports: Option>, #[proto] event: &Event, - ) -> Result, EventError> { - let undefined = v8::undefined(scope); + ) -> Result<(), EventError> { if event.dispatch_flag.get() { - return Ok(undefined); + return Ok(()); } event.typ.replace(typ); @@ -1330,7 +1332,7 @@ impl MessageEvent { let ports = v8::Global::new(scope, ports); self.ports.replace(ports); } - Ok(undefined) + Ok(()) } #[getter] @@ -1585,6 +1587,27 @@ fn get_value<'a>( } } +#[derive(WebIDL, Debug)] +#[webidl(dictionary)] +pub struct EventListenerOptions { + #[webidl(default = false)] + capture: bool, +} + +#[derive(WebIDL, Debug)] +#[webidl(dictionary)] +pub struct AddEventListenerOptions { + #[webidl(default = false)] + capture: bool, + #[webidl(default = false)] + passive: bool, + #[webidl(default = false)] + once: bool, + // #[webidl(default = false)] + // resist_stop_immediate_propagation: bool, + // signal: v8::Global +} + #[derive(Debug)] struct EventListener { callback: v8::Global, @@ -1639,31 +1662,20 @@ impl EventTarget { if options.is_object() && let Some(options) = options.to_object(scope) { - #[inline] - fn to_bool<'a>( - scope: &mut v8::HandleScope<'a>, - options: v8::Local<'a, v8::Object>, - str: &'static str, - is_symbol: bool, - ) -> bool { - let str = v8::String::new(scope, str).unwrap(); - let key: v8::Local = if is_symbol { - v8::Symbol::for_key(scope, str).into() - } else { - str.into() - }; - match options.get(scope, key) { - Some(value) => value.to_boolean(scope).is_true(), - None => false, - } - } - - let capture = to_bool(scope, options, "capture", false); - let passive = to_bool(scope, options, "passive", false); - let once = to_bool(scope, options, "once", false); - let resist_stop_immediate_propagation = - to_bool(scope, options, "Deno.stopImmediatePropagation", true); - (capture, passive, once, resist_stop_immediate_propagation) + let key = v8::String::new(scope, "Deno.stopImmediatePropagation").unwrap(); + let symbol = v8::Symbol::for_key(scope, key); + let resist_stop_immediate_propagation = match options.get(scope, symbol.into()) { + Some(value) => value.to_boolean(scope).is_true(), + None => false, + }; + let options = AddEventListenerOptions::convert( + scope, + options.into(), + "Failed to execute 'addEventListener' on 'EventTarget'".into(), + (|| "Argument 3)".into()).into(), + &Default::default(), + )?; + (options.capture, options.passive, options.once, resist_stop_immediate_propagation) } else { (options.to_boolean(scope).is_true(), false, false, false) } @@ -1707,7 +1719,6 @@ impl EventTarget { removed: Cell::new(false), resist_stop_immediate_propagation, })); - Ok(()) } @@ -1719,17 +1730,18 @@ impl EventTarget { #[webidl] typ: String, callback: Option>, options: Option>, - ) { + ) -> Result<(), EventError> { let capture = match options { Some(options) => { - if options.is_object() - && let Some(options) = options.to_object(scope) - { - let key = v8::String::new(scope, "capture").unwrap(); - match options.get(scope, key.into()) { - Some(value) => value.to_boolean(scope).is_true(), - None => false, - } + if options.is_object() { + let options = EventListenerOptions::convert( + scope, + options, + "Failed to execute 'removeEventListener' on 'EventTarget'".into(), + (|| "Argument 3".into()).into(), + &Default::default(), + )?; + options.capture } else { options.to_boolean(scope).is_true() } @@ -1739,11 +1751,11 @@ impl EventTarget { let callback = match callback { None => { - return; + return Ok(()); } Some(callback) => { if callback.is_null() { - return; + return Ok(()); } callback } @@ -1751,7 +1763,7 @@ impl EventTarget { let mut listeners = self.listeners.borrow_mut(); let Some(listeners) = listeners.get_mut(&typ) else { - return; + return Ok(()); }; if let Some((index, listener)) = listeners.iter().enumerate().find(|(_, listener)| { @@ -1761,6 +1773,7 @@ impl EventTarget { listener.removed.set(true); listeners.remove(index); } + Ok(()) } #[fast] From 83386524015ea652d5ddffb8366556b80640d729 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Thu, 21 Aug 2025 21:00:18 +0900 Subject: [PATCH 08/18] wrap AbortSignal --- ext/fetch/23_request.js | 9 +- ext/fetch/26_fetch.js | 23 +- ext/fs/30_fs.js | 17 +- ext/net/01_net.js | 13 +- ext/node/polyfills/util.ts | 4 +- ext/process/40_process.js | 13 +- ext/web/03_abort_signal.js | 407 +++++++-------------- ext/web/06_streams.js | 10 +- ext/web/event.rs | 533 +++++++++++++++++++++++++--- ext/web/lib.rs | 10 +- ext/websocket/02_websocketstream.js | 13 +- 11 files changed, 692 insertions(+), 360 deletions(-) diff --git a/ext/fetch/23_request.js b/ext/fetch/23_request.js index e9f2efb98f..876c3cda68 100644 --- a/ext/fetch/23_request.js +++ b/ext/fetch/23_request.js @@ -272,7 +272,8 @@ class Request { if (signal === false) { const signal = newSignal(); this[_signalCache] = signal; - signal[signalAbort]( + signalAbort( + signal, new DOMException(MESSAGE_REQUEST_CANCELLED, "AbortError"), ); return signal; @@ -283,7 +284,8 @@ class Request { const signal = newSignal(); this[_signalCache] = signal; this[_request].onCancel?.(() => { - signal[signalAbort]( + signalAbort( + signal, new DOMException(MESSAGE_REQUEST_CANCELLED, "AbortError"), ); }); @@ -609,7 +611,8 @@ const MESSAGE_REQUEST_CANCELLED = "The request has been cancelled."; function abortRequest(request) { if (request[_signalCache] !== undefined) { - request[_signal][signalAbort]( + signalAbort( + request[_signal], new DOMException(MESSAGE_REQUEST_CANCELLED, "AbortError"), ); } else { diff --git a/ext/fetch/26_fetch.js b/ext/fetch/26_fetch.js index 9341448d07..92bd3af50d 100644 --- a/ext/fetch/26_fetch.js +++ b/ext/fetch/26_fetch.js @@ -57,7 +57,10 @@ import { redirectStatus, toInnerResponse, } from "ext:deno_fetch/23_response.js"; -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; +import { + addSignalAlgorithm, + removeSignalAlgorithm, +} from "ext:deno_web/03_abort_signal.js"; import { builtinTracer, ContextManager, @@ -107,7 +110,7 @@ function createResponseBodyStream(responseBodyRid, terminator) { } // TODO(lucacasonato): clean up registration - terminator[abortSignal.add](onAbort); + addSignalAlgorithm(terminator, onAbort); return readable; } @@ -125,7 +128,7 @@ async function mainFetch(req, recursive, terminator) { } const body = new InnerBody(req.blobUrlEntry.stream()); - terminator[abortSignal.add](() => body.error(terminator.reason)); + addSignalAlgorithm(terminator, () => body.error(terminator.reason)); processUrlList(req.urlList, req.urlListProcessed); return { @@ -186,7 +189,7 @@ async function mainFetch(req, recursive, terminator) { core.tryClose(cancelHandleRid); } } - terminator[abortSignal.add](onAbort); + addSignalAlgorithm(terminator, onAbort); let resp; try { resp = await opFetchSend(requestRid); @@ -403,13 +406,13 @@ function fetch(input, init = { __proto__: null }) { // 9. let locallyAborted = false; // 10. - function onabort() { + function onAbort() { locallyAborted = true; reject( abortFetch(request, responseObject, requestObject.signal.reason), ); } - requestObject.signal[abortSignal.add](onabort); + addSignalAlgorithm(requestObject, onAbort); if (!requestObject.headers.has("Accept")) { ArrayPrototypePush(request.headerList, ["Accept", "*/*"]); @@ -435,7 +438,7 @@ function fetch(input, init = { __proto__: null }) { requestObject.signal.reason, ), ); - requestObject.signal[abortSignal.remove](onabort); + removeSignalAlgorithm(requestObject, onAbort); return; } // 12.3. @@ -444,7 +447,7 @@ function fetch(input, init = { __proto__: null }) { "Fetch failed: " + (response.error ?? "unknown error"), ); reject(err); - requestObject.signal[abortSignal.remove](onabort); + removeSignalAlgorithm(requestObject, onAbort); return; } responseObject = fromInnerResponse(response, "immutable"); @@ -454,12 +457,12 @@ function fetch(input, init = { __proto__: null }) { } resolve(responseObject); - requestObject.signal[abortSignal.remove](onabort); + removeSignalAlgorithm(requestObject, onAbort); }, ), (err) => { reject(err); - requestObject.signal[abortSignal.remove](onabort); + removeSignalAlgorithm(requestObject, onAbort); }, ); }); diff --git a/ext/fs/30_fs.js b/ext/fs/30_fs.js index 74e3c87c17..5eded7049a 100644 --- a/ext/fs/30_fs.js +++ b/ext/fs/30_fs.js @@ -92,7 +92,10 @@ const { } = primordials; import { read, readSync, write, writeSync } from "ext:deno_io/12_io.js"; -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; +import { + addSignalAlgorithm, + removeSignalAlgorithm, +} from "ext:deno_web/03_abort_signal.js"; import { readableStreamForRid, ReadableStreamPrototype, @@ -747,7 +750,7 @@ async function readFile(path, options) { options.signal.throwIfAborted(); cancelRid = createCancelHandle(); abortHandler = () => core.tryClose(cancelRid); - options.signal[abortSignal.add](abortHandler); + addSignalAlgorithm(options.signal, abortHandler); } try { @@ -758,7 +761,7 @@ async function readFile(path, options) { return read; } finally { if (options?.signal) { - options.signal[abortSignal.remove](abortHandler); + removeSignalAlgorithm(options.signal, abortHandler); // always throw the abort error when aborted options.signal.throwIfAborted(); @@ -777,7 +780,7 @@ async function readTextFile(path, options) { options.signal.throwIfAborted(); cancelRid = createCancelHandle(); abortHandler = () => core.tryClose(cancelRid); - options.signal[abortSignal.add](abortHandler); + addSignalAlgorithm(options.signal, abortHandler); } try { @@ -788,7 +791,7 @@ async function readTextFile(path, options) { return read; } finally { if (options?.signal) { - options.signal[abortSignal.remove](abortHandler); + removeSignalAlgorithm(options.signal, abortHandler); // always throw the abort error when aborted options.signal.throwIfAborted(); @@ -823,7 +826,7 @@ async function writeFile( options.signal.throwIfAborted(); cancelRid = createCancelHandle(); abortHandler = () => core.tryClose(cancelRid); - options.signal[abortSignal.add](abortHandler); + addSignalAlgorithm(options.signal, abortHandler); } try { if (ObjectPrototypeIsPrototypeOf(ReadableStreamPrototype, data)) { @@ -851,7 +854,7 @@ async function writeFile( } } finally { if (options.signal) { - options.signal[abortSignal.remove](abortHandler); + removeSignalAlgorithm(options.signal, abortHandler); // always throw the abort error when aborted options.signal.throwIfAborted(); diff --git a/ext/net/01_net.js b/ext/net/01_net.js index a01112d3c8..42377bd87d 100644 --- a/ext/net/01_net.js +++ b/ext/net/01_net.js @@ -64,7 +64,10 @@ import { readableStreamForRidUnrefableUnref, writableStreamForRid, } from "ext:deno_web/06_streams.js"; -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; +import { + addSignalAlgorithm, + removeSignalAlgorithm, +} from "ext:deno_web/03_abort_signal.js"; import { SymbolDispose } from "ext:deno_web/00_infra.js"; async function write(rid, data) { @@ -78,7 +81,7 @@ async function resolveDns(query, recordType, options) { options.signal.throwIfAborted(); cancelRid = createCancelHandle(); abortHandler = () => core.tryClose(cancelRid); - options.signal[abortSignal.add](abortHandler); + addSignalAlgorithm(options.signal, abortHandler); } try { @@ -91,7 +94,7 @@ async function resolveDns(query, recordType, options) { return ArrayPrototypeMap(res, (recordWithTtl) => recordWithTtl.data); } finally { if (options?.signal) { - options.signal[abortSignal.remove](abortHandler); + removeSignalAlgorithm(options.signal, abortHandler); // always throw the abort error when aborted options.signal.throwIfAborted(); @@ -691,7 +694,7 @@ async function connect(args) { args.signal.throwIfAborted(); cancelRid = createCancelHandle(); abortHandler = () => core.tryClose(cancelRid); - args.signal[abortSignal.add](abortHandler); + addSignalAlgorithm(args.signal, abortHandler); } const port = validatePort(args.port); @@ -711,7 +714,7 @@ async function connect(args) { return new TcpConn(rid, remoteAddr, localAddr); } finally { if (args?.signal) { - args.signal[abortSignal.remove](abortHandler); + removeSignalAlgorithm(args.signal, abortHandler); args.signal.throwIfAborted(); } } diff --git a/ext/node/polyfills/util.ts b/ext/node/polyfills/util.ts index beab4df67a..8d5e6909a5 100644 --- a/ext/node/polyfills/util.ts +++ b/ext/node/polyfills/util.ts @@ -50,7 +50,7 @@ import { validateString, } from "ext:deno_node/internal/validators.mjs"; import { parseArgs } from "ext:deno_node/internal/util/parse_args/parse_args.js"; -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; +import { addSignalAlgorithm } from "ext:deno_web/03_abort_signal.js"; import { ERR_INVALID_ARG_TYPE } from "ext:deno_node/internal/errors.ts"; export { @@ -246,7 +246,7 @@ export async function aborted( return PromiseResolve(); } const abortPromise = PromiseWithResolvers(); - signal[abortSignal.add](abortPromise.resolve); + addSignalAlgorithm(signal, abortPromise.resolve); return abortPromise.promise; } diff --git a/ext/process/40_process.js b/ext/process/40_process.js index 87fcbae879..a3f5845b60 100644 --- a/ext/process/40_process.js +++ b/ext/process/40_process.js @@ -31,7 +31,10 @@ import { pathFromURL, SymbolAsyncDispose, } from "ext:deno_web/00_infra.js"; -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; +import { + addSignalAlgorithm, + removeSignalAlgorithm, +} from "ext:deno_web/03_abort_signal.js"; import { readableStreamCollectIntoUint8Array, readableStreamForRidUnrefable, @@ -306,11 +309,15 @@ class ChildProcess { // Ignore the error for https://github.com/denoland/deno/issues/27112 } }; - signal?.[abortSignal.add](onAbort); + if (signal != null) { + addSignalAlgorithm(signal, onAbort); + } const waitPromise = op_spawn_wait(this.#rid); this.#waitPromise = waitPromise; this.#status = PromisePrototypeThen(waitPromise, (res) => { - signal?.[abortSignal.remove](onAbort); + if (signal != null) { + removeSignalAlgorithm(signal, onAbort); + } this.#waitComplete = true; return res; }); diff --git a/ext/web/03_abort_signal.js b/ext/web/03_abort_signal.js index 42009ccf1b..41e069982d 100644 --- a/ext/web/03_abort_signal.js +++ b/ext/web/03_abort_signal.js @@ -6,107 +6,39 @@ import { core, primordials } from "ext:core/mod.js"; const { ArrayPrototypeEvery, - ArrayPrototypePush, FunctionPrototypeApply, ObjectPrototypeIsPrototypeOf, - SafeSet, - SafeSetIterator, - SafeWeakRef, - SafeWeakSet, - SetPrototypeAdd, - SetPrototypeDelete, + ObjectDefineProperties, + ObjectDefineProperty, Symbol, SymbolFor, - TypeError, - WeakRefPrototypeDeref, - WeakSetPrototypeAdd, - WeakSetPrototypeHas, } = primordials; +import { + AbortSignal, + op_event_add_abort_algorithm, + op_event_create_abort_signal, + op_event_create_dependent_abort_signal, + op_event_get_dependent_signals, + op_event_get_source_signals, + op_event_remove_abort_algorithm, + op_event_signal_abort, +} from "ext:core/ops"; import * as webidl from "ext:deno_webidl/00_webidl.js"; -import { assert } from "./00_infra.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; +import { DOMException } from "./01_dom_exception.js"; import { defineEventHandler, - Event, EventTarget, getListenerCount, - setIsTrusted, } from "./02_event.js"; import { clearTimeout, refTimer, unrefTimer } from "./02_timers.js"; -// Since WeakSet is not a iterable, WeakRefSet class is provided to store and -// iterate objects. -// To create an AsyncIterable using GeneratorFunction in the internal code, -// there are many primordial considerations, so we simply implement the -// toArray method. -class WeakRefSet { - #weakSet = new SafeWeakSet(); - #refs = []; - - add(value) { - if (WeakSetPrototypeHas(this.#weakSet, value)) { - return; - } - WeakSetPrototypeAdd(this.#weakSet, value); - ArrayPrototypePush(this.#refs, new SafeWeakRef(value)); - } - - has(value) { - return WeakSetPrototypeHas(this.#weakSet, value); - } - - toArray() { - const ret = []; - for (let i = 0; i < this.#refs.length; ++i) { - const value = WeakRefPrototypeDeref(this.#refs[i]); - if (value !== undefined) { - ArrayPrototypePush(ret, value); - } - } - return ret; - } -} - -const add = Symbol("[[add]]"); -const signalAbort = Symbol("[[signalAbort]]"); -const remove = Symbol("[[remove]]"); -const runAbortSteps = Symbol("[[runAbortSteps]]"); -const abortReason = Symbol("[[abortReason]]"); -const abortAlgos = Symbol("[[abortAlgos]]"); -const dependent = Symbol("[[dependent]]"); -const sourceSignals = Symbol("[[sourceSignals]]"); -const dependentSignals = Symbol("[[dependentSignals]]"); -const signal = Symbol("[[signal]]"); const timerId = Symbol("[[timerId]]"); -const illegalConstructorKey = Symbol("illegalConstructorKey"); - -class AbortSignal extends EventTarget { - [abortReason] = undefined; - [abortAlgos] = null; - [dependent] = false; - [sourceSignals] = null; - [dependentSignals] = null; - [timerId] = null; - [webidl.brand] = webidl.brand; - - static any(signals) { - const prefix = "Failed to execute 'AbortSignal.any'"; - webidl.requiredArguments(arguments.length, 1, prefix); - return createDependentAbortSignal(signals, prefix); - } - - static abort(reason = undefined) { - if (reason !== undefined) { - reason = webidl.converters.any(reason); - } - const signal = new AbortSignal(illegalConstructorKey); - signal[signalAbort](reason); - return signal; - } - - static timeout(millis) { +ObjectDefineProperty(AbortSignal, "timeout", { + __proto__: null, + value: function timeout(millis) { const prefix = "Failed to execute 'AbortSignal.timeout'"; webidl.requiredArguments(arguments.length, 1, prefix); millis = webidl.converters["unsigned long long"]( @@ -118,7 +50,7 @@ class AbortSignal extends EventTarget { }, ); - const signal = new AbortSignal(illegalConstructorKey); + const signal = op_event_create_abort_signal(); signal[timerId] = core.queueSystemTimer( undefined, false, @@ -126,169 +58,111 @@ class AbortSignal extends EventTarget { () => { clearTimeout(signal[timerId]); signal[timerId] = null; - signal[signalAbort]( + op_event_signal_abort( + signal, new DOMException("Signal timed out.", "TimeoutError"), ); }, ); unrefTimer(signal[timerId]); return signal; - } + }, + configurable: true, + enumerable: true, + writable: true, +}); - [add](algorithm) { - if (this.aborted) { - return; - } - this[abortAlgos] ??= new SafeSet(); - SetPrototypeAdd(this[abortAlgos], algorithm); - } +const addEventListener_ = EventTarget.prototype.addEventListener; +const removeEventListener_ = EventTarget.prototype.removeEventListener; - [signalAbort]( - reason = new DOMException("The signal has been aborted", "AbortError"), - ) { - if (this.aborted) { - return; - } - this[abortReason] = reason; - - const dependentSignalsToAbort = []; - if (this[dependentSignals] !== null) { - const dependentSignalArray = this[dependentSignals].toArray(); - for (let i = 0; i < dependentSignalArray.length; ++i) { - const dependentSignal = dependentSignalArray[i]; - if (dependentSignal[abortReason] === undefined) { - dependentSignal[abortReason] = this[abortReason]; - ArrayPrototypePush(dependentSignalsToAbort, dependentSignal); - } - } - } - - this[runAbortSteps](); - - if (dependentSignalsToAbort.length !== 0) { - for (let i = 0; i < dependentSignalsToAbort.length; ++i) { - const dependentSignal = dependentSignalsToAbort[i]; - dependentSignal[runAbortSteps](); - } - } - } - - [runAbortSteps]() { - const algos = this[abortAlgos]; - this[abortAlgos] = null; - - if (algos !== null) { - for (const algorithm of new SafeSetIterator(algos)) { - algorithm(); - } - } - - if (getListenerCount(this, "abort") > 0) { - const event = new Event("abort"); - setIsTrusted(event, true); - super.dispatchEvent(event); - } - } - - [remove](algorithm) { - this[abortAlgos] && SetPrototypeDelete(this[abortAlgos], algorithm); - } - - constructor(key = null) { - if (key !== illegalConstructorKey) { - throw new TypeError("Illegal constructor"); - } - super(); - } - - get aborted() { - webidl.assertBranded(this, AbortSignalPrototype); - return this[abortReason] !== undefined; - } - - get reason() { - webidl.assertBranded(this, AbortSignalPrototype); - return this[abortReason]; - } - - throwIfAborted() { - webidl.assertBranded(this, AbortSignalPrototype); - if (this[abortReason] !== undefined) { - throw this[abortReason]; - } - } - - // `addEventListener` and `removeEventListener` have to be overridden in - // order to have the timer block the event loop while there are listeners. - // `[add]` and `[remove]` don't ref and unref the timer because they can - // only be used by Deno internals, which use it to essentially cancel async - // ops which would block the event loop. - addEventListener() { - FunctionPrototypeApply(super.addEventListener, this, arguments); - if (getListenerCount(this, "abort") > 0) { - if (this[timerId] !== null) { - refTimer(this[timerId]); - } else if (this[sourceSignals] !== null) { - const sourceSignalArray = this[sourceSignals].toArray(); - for (let i = 0; i < sourceSignalArray.length; ++i) { - const sourceSignal = sourceSignalArray[i]; - if (sourceSignal[timerId] !== null) { - refTimer(sourceSignal[timerId]); - } - } - } - } - } - - removeEventListener() { - FunctionPrototypeApply(super.removeEventListener, this, arguments); - if (getListenerCount(this, "abort") === 0) { - if (this[timerId] !== null) { - unrefTimer(this[timerId]); - } else if (this[sourceSignals] !== null) { - const sourceSignalArray = this[sourceSignals].toArray(); - for (let i = 0; i < sourceSignalArray.length; ++i) { - const sourceSignal = sourceSignalArray[i]; - if (sourceSignal[timerId] !== null) { - // Check that all dependent signals of the timer signal do not have listeners - if ( - ArrayPrototypeEvery( - sourceSignal[dependentSignals].toArray(), - (dependentSignal) => - dependentSignal === this || - getListenerCount(dependentSignal, "abort") === 0, - ) - ) { - unrefTimer(sourceSignal[timerId]); +// `addEventListener` and `removeEventListener` have to be overridden in +// order to have the timer block the event loop while there are listeners. +// `[add]` and `[remove]` don't ref and unref the timer because they can +// only be used by Deno internals, which use it to essentially cancel async +// ops which would block the event loop. +ObjectDefineProperties(AbortSignal.prototype, { + addEventListener: { + __proto__: null, + value: function addEventListener() { + FunctionPrototypeApply(addEventListener_, this, arguments); + if (getListenerCount(this, "abort") > 0) { + if (this[timerId] !== null) { + refTimer(this[timerId]); + } else { + const sourceSignals = op_event_get_source_signals(this); + for (let i = 0; i < sourceSignals.length; ++i) { + const sourceSignal = sourceSignals[i]; + if (sourceSignal[timerId] !== null) { + refTimer(sourceSignal[timerId]); } } } } - } - } + }, + configurable: true, + enumerable: true, + writable: true, + }, + removeEventListener: { + __proto__: null, + value: function removeEventListener() { + FunctionPrototypeApply(removeEventListener_, this, arguments); + if (getListenerCount(this, "abort") === 0) { + if (this[timerId] !== null) { + unrefTimer(this[timerId]); + } else { + const sourceSignals = op_event_get_source_signals(this); + for (let i = 0; i < sourceSignals.length; ++i) { + const sourceSignal = sourceSignals[i]; + if (sourceSignal[timerId] !== null) { + // Check that all dependent signals of the timer signal do not have listeners + if ( + ArrayPrototypeEvery( + op_event_get_dependent_signals(sourceSignal), + (dependentSignal) => + dependentSignal === this || + getListenerCount(dependentSignal, "abort") === 0, + ) + ) { + unrefTimer(sourceSignal[timerId]); + } + } + } + } + } + }, + configurable: true, + enumerable: true, + writable: true, + }, + [SymbolFor("Deno.privateCustomInspect")]: { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf(AbortSignalPrototype, this), + keys: [ + "aborted", + "reason", + "onabort", + ], + }), + inspectOptions, + ); + }, + }, +}); - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(AbortSignalPrototype, this), - keys: [ - "aborted", - "reason", - "onabort", - ], - }), - inspectOptions, - ); - } -} defineEventHandler(AbortSignal.prototype, "abort"); webidl.configureInterface(AbortSignal); const AbortSignalPrototype = AbortSignal.prototype; +const signal = Symbol("[[signal]]"); + class AbortController { - [signal] = new AbortSignal(illegalConstructorKey); + [signal] = op_event_create_abort_signal(); constructor() { this[webidl.brand] = webidl.brand; @@ -301,7 +175,7 @@ class AbortController { abort(reason) { webidl.assertBranded(this, AbortControllerPrototype); - this[signal][signalAbort](reason); + op_event_signal_abort(this[signal], reason); } [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { @@ -329,61 +203,54 @@ webidl.converters["sequence"] = webidl.createSequenceConverter( webidl.converters.AbortSignal, ); +/** + * @returns {AbortSignal} + */ function newSignal() { - return new AbortSignal(illegalConstructorKey); + return op_event_create_abort_signal(); } +/** + * @param {AbortSignal[]} signals + * @param {string} prefix + * @returns {AbortSignal} + */ function createDependentAbortSignal(signals, prefix) { - signals = webidl.converters["sequence"]( - signals, - prefix, - "Argument 1", - ); + return op_event_create_dependent_abort_signal(signals, prefix); +} - const resultSignal = new AbortSignal(illegalConstructorKey); - for (let i = 0; i < signals.length; ++i) { - const signal = signals[i]; - if (signal[abortReason] !== undefined) { - resultSignal[abortReason] = signal[abortReason]; - return resultSignal; - } - } +/** + * @param {AbortSignal} signal + * @param {() => void} algorithm + */ +function addSignalAlgorithm(signal, algorithm) { + op_event_add_abort_algorithm(signal, algorithm); +} - resultSignal[dependent] = true; - resultSignal[sourceSignals] = new WeakRefSet(); - for (let i = 0; i < signals.length; ++i) { - const signal = signals[i]; - if (!signal[dependent]) { - signal[dependentSignals] ??= new WeakRefSet(); - resultSignal[sourceSignals].add(signal); - signal[dependentSignals].add(resultSignal); - } else { - const sourceSignalArray = signal[sourceSignals].toArray(); - for (let j = 0; j < sourceSignalArray.length; ++j) { - const sourceSignal = sourceSignalArray[j]; - assert(sourceSignal[abortReason] === undefined); - assert(!sourceSignal[dependent]); +/** + * @param {AbortSignal} signal + * @param {() => void} algorithm + */ +function removeSignalAlgorithm(signal, algorithm) { + op_event_remove_abort_algorithm(signal, algorithm); +} - if (resultSignal[sourceSignals].has(sourceSignal)) { - continue; - } - resultSignal[sourceSignals].add(sourceSignal); - sourceSignal[dependentSignals].add(resultSignal); - } - } - } - - return resultSignal; +/** + * @param {AbortSignal} signal + * @param {any} reason + */ +function signalAbort(signal, reason) { + op_event_signal_abort(signal, reason); } export { AbortController, AbortSignal, AbortSignalPrototype, - add, + addSignalAlgorithm, createDependentAbortSignal, newSignal, - remove, + removeSignalAlgorithm, signalAbort, timerId, }; diff --git a/ext/web/06_streams.js b/ext/web/06_streams.js index 1a8ef1ba7c..e7e1594e8d 100644 --- a/ext/web/06_streams.js +++ b/ext/web/06_streams.js @@ -93,9 +93,9 @@ import * as webidl from "ext:deno_webidl/00_webidl.js"; import { structuredClone } from "./02_structured_clone.js"; import { AbortSignalPrototype, - add, + addSignalAlgorithm, newSignal, - remove, + removeSignalAlgorithm, signalAbort, } from "./03_abort_signal.js"; @@ -2746,7 +2746,7 @@ function readableStreamPipeTo( abortAlgorithm(); return promise.promise; } - signal[add](abortAlgorithm); + addSignalAlgorithm(signal, abortAlgorithm); } function pipeLoop() { @@ -2948,7 +2948,7 @@ function readableStreamPipeTo( readableStreamDefaultReaderRelease(reader); if (signal !== undefined) { - signal[remove](abortAlgorithm); + removeSignalAlgorithm(signal, abortAlgorithm); } if (isError) { promise.reject(error); @@ -4302,7 +4302,7 @@ function writableStreamAbort(stream, reason) { if (state === "closed" || state === "errored") { return PromiseResolve(undefined); } - stream[_controller][_signal][signalAbort](reason); + signalAbort(stream[_controller][_signal], reason); if (state === "closed" || state === "errored") { return PromiseResolve(undefined); } diff --git a/ext/web/event.rs b/ext/web/event.rs index 91b6dd15fe..5dff99b428 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -1,8 +1,13 @@ // Copyright 2018-2025 the Deno authors. MIT license. +#![allow(clippy::too_many_arguments)] +#![allow(clippy::extra_unused_lifetimes)] + +use std::borrow::Cow; use std::cell::Cell; use std::cell::RefCell; use std::collections::HashMap; +use std::collections::HashSet; use std::collections::VecDeque; use std::rc::Rc; @@ -10,23 +15,27 @@ use deno_core::GarbageCollected; use deno_core::OpState; use deno_core::WebIDL; use deno_core::cppgc; +use deno_core::cppgc::Ptr; use deno_core::error::JsError; use deno_core::error::dispatch_exception; +use deno_core::error::to_v8_error; use deno_core::op2; use deno_core::v8; use deno_core::v8::Global; +use deno_core::webidl::ContextFn; use deno_core::webidl::Nullable; use deno_core::webidl::WebIdlConverter; use deno_core::webidl::WebIdlError; use deno_core::webidl::WebIdlErrorKind; +use deno_error::JsErrorBox; #[derive(Debug, thiserror::Error, deno_error::JsError)] pub enum EventError { #[class(type)] - #[error("parameter 2 is not of type 'Object'")] + #[error("Argument 2 is not of type 'Object'")] InvalidListenerType, #[class(type)] - #[error("parameter 1 is expected Event")] + #[error("Argument 1 is expected Event")] ExpectedEvent, #[class(type)] #[error("Illegal invocation")] @@ -695,7 +704,7 @@ pub fn op_event_dispatch<'a>( ) -> bool { let target = v8::Local::new(scope, &target_object); let target = - cppgc::try_unwrap_cppgc_object::(scope, target.into()) + cppgc::try_unwrap_cppgc_proto_object::(scope, target.into()) .unwrap(); let event = cppgc::try_unwrap_cppgc_proto_object::(scope, event_object.into()) @@ -1614,7 +1623,6 @@ struct EventListener { capture: bool, passive: bool, once: bool, - signal: Option>, removed: Cell, // This field exists for simulating Node.js behavior, implemented in https://github.com/nodejs/node/commit/bcd35c334ec75402ee081f1c4da128c339f70c24 // Some internal event listeners in Node.js can ignore `e.stopImmediatePropagation()` calls from the earlier event listeners. @@ -1623,7 +1631,7 @@ struct EventListener { #[derive(Debug)] pub struct EventTarget { - listeners: RefCell>>>, + listeners: Rc>>>>, } impl GarbageCollected for EventTarget { @@ -1632,14 +1640,21 @@ impl GarbageCollected for EventTarget { } } -#[op2] +impl EventTarget { + #[inline] + fn new() -> EventTarget { + EventTarget { + listeners: Rc::new(RefCell::new(HashMap::new())), + } + } +} + +#[op2(base)] impl EventTarget { #[constructor] #[cppgc] - fn new(_: bool) -> EventTarget { - EventTarget { - listeners: RefCell::new(HashMap::new()), - } + fn constructor(_: bool) -> EventTarget { + EventTarget::new() } #[required(2)] @@ -1651,39 +1666,87 @@ impl EventTarget { callback: Option>, options: Option>, ) -> Result<(), EventError> { - let ( - capture, - passive, - once, - resist_stop_immediate_propagation, - /* signal */ - ) = match options { - Some(options) => { - if options.is_object() - && let Some(options) = options.to_object(scope) - { - let key = v8::String::new(scope, "Deno.stopImmediatePropagation").unwrap(); - let symbol = v8::Symbol::for_key(scope, key); - let resist_stop_immediate_propagation = match options.get(scope, symbol.into()) { - Some(value) => value.to_boolean(scope).is_true(), - None => false, - }; - let options = AddEventListenerOptions::convert( - scope, - options.into(), - "Failed to execute 'addEventListener' on 'EventTarget'".into(), - (|| "Argument 3)".into()).into(), - &Default::default(), - )?; - (options.capture, options.passive, options.once, resist_stop_immediate_propagation) - } else { - (options.to_boolean(scope).is_true(), false, false, false) - } - } - None => (false, false, false, false), - }; + let prefix = "Failed to execute 'addEventListener' on 'EventTarget'"; - // TODO(petamoriken): signal have already aborted + let (capture, passive, once, resist_stop_immediate_propagation, signal) = + match options { + Some(options) => { + if let Ok(options) = options.try_cast::() { + let key = + v8::String::new(scope, "Deno.stopImmediatePropagation").unwrap(); + let symbol = v8::Symbol::for_key(scope, key); + let resist_stop_immediate_propagation = + match options.get(scope, symbol.into()) { + Some(value) => value.to_boolean(scope).is_true(), + None => false, + }; + + // TODO(petamoriken): Validate AbortSignal + let key = v8::String::new(scope, "signal").unwrap(); + let signal = match options.get(scope, key.into()) { + Some(value) => { + if value.is_undefined() { + None + } else { + if !value.is_object() { + return Err(EventError::WebIDL(WebIdlError::new( + prefix.into(), + (|| { + "'signal' of 'AddEventListenerOptions' (Argument 3)" + .into() + }) + .into(), + WebIdlErrorKind::ConvertToConverterType("object"), + ))); + } + Some(value.cast::()) + } + } + None => None, + }; + + let options = AddEventListenerOptions::convert( + scope, + options.into(), + prefix.into(), + (|| "Argument 3".into()).into(), + &Default::default(), + )?; + + ( + options.capture, + options.passive, + options.once, + resist_stop_immediate_propagation, + signal, + ) + } else { + ( + options.to_boolean(scope).is_true(), + false, + false, + false, + None, + ) + } + } + None => (false, false, false, false, None), + }; + + let aborted = match signal { + Some(signal) => { + let key = v8::String::new(scope, "aborted").unwrap(); + signal + .get(scope, key.into()) + .unwrap() + .to_boolean(scope) + .is_true() + } + None => false, + }; + if aborted { + return Ok(()); + } let callback = match callback { None => { @@ -1696,7 +1759,7 @@ impl EventTarget { if !callback.is_object() { return Err(EventError::InvalidListenerType); } - callback.to_object(scope).unwrap() + callback.cast::() } }; @@ -1715,7 +1778,6 @@ impl EventTarget { capture, passive, once, - signal: None, removed: Cell::new(false), resist_stop_immediate_propagation, })); @@ -1829,13 +1891,7 @@ pub fn op_event_wrap_event_target<'a>( scope: &mut v8::HandleScope<'a>, obj: v8::Local<'a, v8::Object>, ) { - cppgc::wrap_object( - scope, - obj, - EventTarget { - listeners: RefCell::new(HashMap::new()), - }, - ); + cppgc::wrap_object1(scope, obj, EventTarget::new()); } #[op2] @@ -1868,3 +1924,378 @@ pub fn op_event_get_target_listener_count<'a>( None => 0, } } + +pub struct AbortSignal { + reason: RefCell>>, + algorithms: RefCell>>, + dependent: Cell, + source_signals: RefCell>>, + dependent_signals: RefCell>>, +} + +impl GarbageCollected for AbortSignal { + fn get_name(&self) -> &'static std::ffi::CStr { + c"AbortSignal" + } +} + +impl AbortSignal { + #[inline] + fn new() -> AbortSignal { + AbortSignal { + reason: RefCell::new(None), + algorithms: RefCell::new(HashSet::new()), + dependent: Cell::new(false), + source_signals: RefCell::new(Vec::new()), + dependent_signals: RefCell::new(Vec::new()), + } + } + + // https://dom.spec.whatwg.org/#create-a-dependent-abort-signal + fn new_with_dependent<'a>( + scope: &mut v8::HandleScope<'a>, + result_signal_object: v8::Local<'a, v8::Object>, + signal_values: Vec>, + prefix: Cow<'static, str>, + context: ContextFn<'_>, + ) -> Result { + let result_signal = AbortSignal::new(); + result_signal.dependent.set(true); + + { + let result_signal_weak = + v8::Weak::new(scope, result_signal_object.cast::()); + let mut result_source_signal = result_signal.source_signals.borrow_mut(); + for signal_value in signal_values { + let Some(signal) = cppgc::try_unwrap_cppgc_proto_object::( + scope, + signal_value, + ) else { + return Err(EventError::WebIDL(WebIdlError::new( + prefix, + context, + WebIdlErrorKind::ConvertToConverterType("AbortSignal"), + ))); + }; + if !signal.dependent.get() { + let signal_weak = v8::Weak::new(scope, signal_value); + result_source_signal.push(signal_weak); + signal + .dependent_signals + .borrow_mut() + .push(result_signal_weak.clone()); + } else { + for source_signal_weak in signal.source_signals.borrow().iter() { + if let Some(source_signal_value) = + source_signal_weak.to_local(scope) + { + let source_signal = cppgc::try_unwrap_cppgc_proto_object::< + AbortSignal, + >(scope, source_signal_value) + .unwrap(); + result_source_signal.push(source_signal_weak.clone()); + source_signal + .dependent_signals + .borrow_mut() + .push(result_signal_weak.clone()); + } + } + } + } + } + + Ok(result_signal) + } + + #[inline] + fn aborted_inner(&self) -> bool { + self.reason.borrow().is_some() + } + + // https://dom.spec.whatwg.org/#abortsignal-add + #[inline] + fn add(&self, algorithm: v8::Global) { + if self.aborted_inner() { + return; + } + self.algorithms.borrow_mut().insert(algorithm); + } + + // https://dom.spec.whatwg.org/#abortsignal-remove + #[inline] + fn remove(&self, algorithm: v8::Global) { + self.algorithms.borrow_mut().remove(&algorithm); + } + + // https://dom.spec.whatwg.org/#abortsignal-signal-abort + fn signal_abort<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + state: &Rc>, + signal_object: v8::Local<'a, v8::Object>, + reason: Option>, + ) { + if self.aborted_inner() { + return; + } + + let reason = if let Some(reason) = reason + && !reason.is_undefined() + { + reason + } else { + let error = JsErrorBox::new( + "DOMExceptionAbortError", + "The signal has been aborted", + ); + to_v8_error(scope, &error) + }; + let reason = v8::Global::new(scope, reason); + self.reason.replace(Some(reason.clone())); + + let mut dependent_signals_to_abort = Vec::new(); + { + let dependent_signals = self.dependent_signals.borrow(); + for dependent_signal_weak in &*dependent_signals { + if let Some(dependent_signal_value) = + dependent_signal_weak.to_local(scope) + { + let dependent_signal = cppgc::try_unwrap_cppgc_proto_object::< + AbortSignal, + >(scope, dependent_signal_value) + .unwrap(); + if !dependent_signal.aborted_inner() { + dependent_signal.reason.replace(Some(reason.clone())); + dependent_signals_to_abort + .push((dependent_signal, dependent_signal_value)); + } + } + } + } + + self.run_abort_steps(scope, state, signal_object); + + for (dependent_signal, dependent_signal_value) in dependent_signals_to_abort + { + dependent_signal.run_abort_steps( + scope, + state, + dependent_signal_value.cast(), + ); + } + } + + // https://dom.spec.whatwg.org/#run-the-abort-steps + #[inline] + fn run_abort_steps<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + state: &Rc>, + signal_object: v8::Local<'a, v8::Object>, + ) { + { + let algorithms = self.algorithms.borrow(); + for algorithm in algorithms.iter() { + let func = v8::Local::new(scope, algorithm); + func.call(scope, signal_object.into(), &[]); + } + } + + self.algorithms.borrow_mut().clear(); + + let target: Ptr = cppgc::try_unwrap_cppgc_proto_object::< + EventTarget, + >(scope, signal_object.into()) + .unwrap(); + let event_object = { + let event = Event::new("abort".to_string(), None); + event.is_trusted.set(true); + cppgc::make_cppgc_proto_object(scope, event) + }; + let event = + cppgc::try_unwrap_cppgc_proto_object::(scope, event_object.into()) + .unwrap(); + let signal_object = v8::Global::new(scope, signal_object); + event.dispatch(scope, state, event_object, &target, signal_object, None); + } +} + +#[op2(inherit = EventTarget)] +impl AbortSignal { + #[static_method] + fn abort<'a>( + scope: &mut v8::HandleScope<'a>, + reason: Option>, + ) -> v8::Local<'a, v8::Object> { + let event_target = EventTarget::new(); + let abort_signal = AbortSignal::new(); + + let reason = if let Some(reason) = reason + && !reason.is_undefined() + { + reason + } else { + let error = JsErrorBox::new( + "DOMExceptionAbortError", + "The signal has been aborted", + ); + to_v8_error(scope, &error) + }; + let reason = v8::Global::new(scope, reason); + abort_signal.reason.replace(Some(reason)); + + let obj = cppgc::make_cppgc_empty_object::(scope); + cppgc::wrap_object2(scope, obj, (event_target, abort_signal)) + } + + #[static_method] + fn any<'a>( + scope: &mut v8::HandleScope<'a>, + signals: v8::Local<'a, v8::Array>, + ) -> Result, EventError> { + let prefix = "Failed to execute 'AbortSignal.any'"; + let context = || "Argument 1".into(); + let signals = Vec::>::convert( + scope, + signals.into(), + prefix.into(), + context.into(), + &Default::default(), + )?; + let event_target = EventTarget::new(); + let obj = cppgc::make_cppgc_empty_object::(scope); + let abort_signal = AbortSignal::new_with_dependent( + scope, + obj, + signals, + prefix.into(), + context.into(), + )?; + + Ok(cppgc::wrap_object2( + scope, + obj, + (event_target, abort_signal), + )) + } + + #[getter] + fn aborted(&self) -> bool { + self.aborted_inner() + } + + #[getter] + fn reason<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + ) -> v8::Local<'a, v8::Value> { + if let Some(reason) = &*self.reason.borrow() { + v8::Local::new(scope, reason) + } else { + v8::undefined(scope).into() + } + } +} + +#[op2] +pub fn op_event_create_abort_signal<'a>( + scope: &mut v8::HandleScope<'a>, +) -> v8::Local<'a, v8::Object> { + let event_target = EventTarget::new(); + let abort_signal = AbortSignal::new(); + + let obj = cppgc::make_cppgc_empty_object::(scope); + cppgc::wrap_object2(scope, obj, (event_target, abort_signal)) +} + +#[op2] +pub fn op_event_create_dependent_abort_signal<'a>( + scope: &mut v8::HandleScope<'a>, + signals: v8::Local<'a, v8::Array>, + #[string] prefix: String, +) -> Result, EventError> { + let context = || "Argument 1".into(); + let signals = Vec::>::convert( + scope, + signals.into(), + prefix.clone().into(), + context.into(), + &Default::default(), + )?; + let event_target = EventTarget::new(); + let obj = cppgc::make_cppgc_empty_object::(scope); + let abort_signal = AbortSignal::new_with_dependent( + scope, + obj, + signals, + prefix.into(), + context.into(), + )?; + + Ok(cppgc::wrap_object2( + scope, + obj, + (event_target, abort_signal), + )) +} + +#[op2] +pub fn op_event_add_abort_algorithm( + #[cppgc] signal: &AbortSignal, + #[global] algorithm: v8::Global, +) { + signal.add(algorithm); +} + +#[op2] +pub fn op_event_remove_abort_algorithm( + #[cppgc] signal: &AbortSignal, + #[global] algorithm: v8::Global, +) { + signal.remove(algorithm); +} + +#[op2(fast)] +pub fn op_event_signal_abort<'a>( + scope: &mut v8::HandleScope<'a>, + state: Rc>, + signal_object: v8::Local<'a, v8::Object>, + reason: Option>, +) { + let signal = cppgc::try_unwrap_cppgc_proto_object::( + scope, + signal_object.into(), + ) + .unwrap(); + signal.signal_abort(scope, &state, signal_object, reason); +} + +#[op2] +pub fn op_event_get_source_signals<'a>( + scope: &mut v8::HandleScope<'a>, + #[cppgc] signal: &AbortSignal, +) -> v8::Local<'a, v8::Array> { + let mut elements = Vec::new(); + let source_signals = signal.source_signals.borrow(); + for source_signal in source_signals.iter() { + if let Some(source_signal) = source_signal.to_local(scope) { + elements.push(source_signal); + } + } + v8::Array::new_with_elements(scope, &elements) +} + +#[op2] +pub fn op_event_get_dependent_signals<'a>( + scope: &mut v8::HandleScope<'a>, + #[cppgc] signal: &AbortSignal, +) -> v8::Local<'a, v8::Array> { + let mut elements = Vec::new(); + let dependent_signals = signal.dependent_signals.borrow(); + for dependent_signal in dependent_signals.iter() { + if let Some(source_signal) = dependent_signal.to_local(scope) { + elements.push(source_signal); + } + } + v8::Array::new_with_elements(scope, &elements) +} diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 40d3d240fe..22c325458c 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -90,6 +90,13 @@ deno_core::extension!(deno_web, event::op_event_wrap_event_target, event::op_event_report_error, event::op_event_report_exception, + event::op_event_create_abort_signal, + event::op_event_create_dependent_abort_signal, + event::op_event_add_abort_algorithm, + event::op_event_remove_abort_algorithm, + event::op_event_signal_abort, + event::op_event_get_source_signals, + event::op_event_get_dependent_signals, op_now

, op_time_origin

, op_defer, @@ -104,13 +111,14 @@ deno_core::extension!(deno_web, ], objects = [ event::Event, - event::EventTarget, event::CustomEvent, event::ErrorEvent, event::PromiseRejectionEvent, event::CloseEvent, event::MessageEvent, event::ProgressEvent, + event::EventTarget, + event::AbortSignal, ], esm = [ "00_infra.js", diff --git a/ext/websocket/02_websocketstream.js b/ext/websocket/02_websocketstream.js index a1b76fb6c0..5c46599166 100644 --- a/ext/websocket/02_websocketstream.js +++ b/ext/websocket/02_websocketstream.js @@ -36,7 +36,10 @@ import * as webidl from "ext:deno_webidl/00_webidl.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; import { Deferred, writableStreamClose } from "ext:deno_web/06_streams.js"; import { DOMException } from "ext:deno_web/01_dom_exception.js"; -import { add, remove } from "ext:deno_web/03_abort_signal.js"; +import { + addSignalAlgorithm, + removeSignalAlgorithm, +} from "ext:deno_web/03_abort_signal.js"; import { fillHeaders, headerListFromHeaders, @@ -165,7 +168,9 @@ class WebSocketStream { const abort = () => { core.close(cancelRid); }; - options.signal?.[add](abort); + if (options.signal != null) { + addSignalAlgorithm(options.signal, abort); + } PromisePrototypeThen( op_ws_create( "new WebSocketStream()", @@ -175,7 +180,9 @@ class WebSocketStream { headerListFromHeaders(headers), ), (create) => { - options.signal?.[remove](abort); + if (options.signal != null) { + removeSignalAlgorithm(options.signal, abort); + } if (this[_earlyClose]) { PromisePrototypeThen( op_ws_close(create.rid), From 0c906254e32b898e1bf0f5bfac3aa0dc98d380df Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Thu, 21 Aug 2025 21:00:24 +0900 Subject: [PATCH 09/18] refactor --- ext/web/event.rs | 39 ++++++++++++++------------------------- 1 file changed, 14 insertions(+), 25 deletions(-) diff --git a/ext/web/event.rs b/ext/web/event.rs index 5dff99b428..c5e404c6e6 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -271,7 +271,7 @@ impl Event { event_object: v8::Local<'a, v8::Object>, target: &EventTarget, target_object: v8::Global, - path: &Vec, + path: &[Path], path_index: usize, phase: InvokePhase, ) { @@ -762,9 +762,7 @@ impl CustomEvent { )?; let event = Event::new(typ, event_init.into_option()); - let detail = if init.is_object() - && let Some(init) = init.to_object(scope) - { + let detail = if let Ok(init) = init.try_cast::() { get_value(scope, init, "detail") .map(|detail| v8::Global::new(scope, detail)) } else { @@ -906,9 +904,7 @@ impl ErrorEvent { Event::new(typ, None) }; - let error = if init.is_object() - && let Some(init) = init.to_object(scope) - { + let error = if let Ok(init) = init.try_cast::() { get_value(scope, init, "error").map(|error| v8::Global::new(scope, error)) } else { None @@ -931,12 +927,12 @@ impl ErrorEvent { #[getter] fn lineno(&self) -> u32 { - self.lineno.clone() + self.lineno } #[getter] fn colno(&self) -> u32 { - self.colno.clone() + self.colno } #[getter] @@ -965,6 +961,7 @@ impl GarbageCollected for PromiseRejectionEvent { } impl PromiseRejectionEvent { + #[inline] fn new( promise: v8::Global, reason: Option>, @@ -996,8 +993,7 @@ impl PromiseRejectionEvent { let promise = { let promise = get_value(scope, init, "promise"); if let Some(promise) = promise - && promise.is_object() - && let Some(promise) = promise.to_object(scope) + && let Ok(promise) = promise.try_cast::() { v8::Global::new(scope, promise) } else { @@ -1065,6 +1061,7 @@ impl GarbageCollected for CloseEvent { } impl CloseEvent { + #[inline] fn new(init: Option) -> CloseEvent { let (was_clean, code, reason) = if let Some(init) = init { (init.was_clean, init.code, init.reason) @@ -1222,16 +1219,13 @@ impl MessageEvent { Event::new(typ, None) }; - let (data, source, ports) = if init.is_object() - && let Some(init) = init.to_object(scope) + let (data, source, ports) = if let Ok(init) = init.try_cast::() { let data = get_value(scope, init, "data") .map(|value| v8::Global::new(scope, value)); // TODO(petamoriken): Validate Window or MessagePort let source = if let Some(source) = get_value(scope, init, "source") { - if source.is_object() - && let Some(source) = source.to_object(scope) - { + if let Ok(source) = source.try_cast::() { Some(v8::Global::new(scope, source)) } else { return Err(EventError::WebIDL(WebIdlError::new( @@ -1464,14 +1458,9 @@ impl ProgressEvent { } } +#[derive(Default)] pub(crate) struct ReportExceptionStackedCalls(u32); -impl Default for ReportExceptionStackedCalls { - fn default() -> Self { - ReportExceptionStackedCalls(0) - } -} - // https://html.spec.whatwg.org/#report-the-exception fn report_exception<'a>( scope: &mut v8::HandleScope<'a>, @@ -1525,8 +1514,8 @@ fn report_exception<'a>( }), ); let error_event = ErrorEvent { - message: message.unwrap_or(String::new()), - filename: file_name.unwrap_or(String::new()), + message: message.unwrap_or_default(), + filename: file_name.unwrap_or_default(), lineno: line_number.unwrap_or(0) as u32, colno: column_number.unwrap_or(0) as u32, error: Some(v8::Global::new(scope, exception)), @@ -1539,7 +1528,7 @@ fn report_exception<'a>( .unwrap(); let global = scope.get_current_context().global(scope); let target = - cppgc::try_unwrap_cppgc_object::(scope, global.into()) + cppgc::try_unwrap_cppgc_proto_object::(scope, global.into()) .unwrap(); let global = v8::Global::new(scope, global); event.dispatch(scope, state, event_object, &target, global, None) From c84d7c0c62dc373069cda45d7e8b8fe0496bf8cc Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Thu, 21 Aug 2025 22:00:04 +0900 Subject: [PATCH 10/18] fix --- ext/node/polyfills/internal/events/abort_listener.mjs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/ext/node/polyfills/internal/events/abort_listener.mjs b/ext/node/polyfills/internal/events/abort_listener.mjs index c1a5ab5d5c..b5b0c2a3e6 100644 --- a/ext/node/polyfills/internal/events/abort_listener.mjs +++ b/ext/node/polyfills/internal/events/abort_listener.mjs @@ -4,7 +4,10 @@ import { primordials } from "ext:core/mod.js"; const { queueMicrotask } = primordials; import { SymbolDispose } from "ext:deno_web/00_infra.js"; -import * as abortSignal from "ext:deno_web/03_abort_signal.js"; +import { + addSignalAlgorithm, + removeSignalAlgorithm, +} from "ext:deno_web/03_abort_signal.js"; import { validateAbortSignal, validateFunction } from "../validators.mjs"; import { codes } from "../errors.ts"; const { ERR_INVALID_ARG_TYPE } = codes; @@ -29,9 +32,9 @@ function addAbortListener(signal, listener) { removeEventListener?.(); listener({ target: signal }); }; - signal[abortSignal.add](handler); + addSignalAlgorithm(signal, handler); removeEventListener = () => { - signal[abortSignal.remove](handler); + removeSignalAlgorithm(signal, handler); }; } return { From 08bbcb065be2d8f7e49ee5aef214fcc73611a46d Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Thu, 21 Aug 2025 23:00:00 +0900 Subject: [PATCH 11/18] fix --- ext/web/event.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/ext/web/event.rs b/ext/web/event.rs index c5e404c6e6..e4bc26bcf6 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -2137,6 +2137,7 @@ impl AbortSignal { cppgc::wrap_object2(scope, obj, (event_target, abort_signal)) } + #[required(1)] #[static_method] fn any<'a>( scope: &mut v8::HandleScope<'a>, @@ -2168,6 +2169,17 @@ impl AbortSignal { )) } + fn throw_if_aborted<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + ) -> v8::Local<'a, v8::Value> { + if let Some(reason) = &*self.reason.borrow() { + let reason = v8::Local::new(scope, reason); + scope.throw_exception(reason); + } + v8::undefined(scope).into() + } + #[getter] fn aborted(&self) -> bool { self.aborted_inner() From 4ec9311e1de9616dca7369023ef1be4576100b56 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Fri, 5 Sep 2025 02:36:24 +0900 Subject: [PATCH 12/18] fix --- ext/web/02_event.js | 2 +- ext/web/event.rs | 21 +++++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/ext/web/02_event.js b/ext/web/02_event.js index 1d199edc01..2aecdd2e50 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -507,7 +507,7 @@ function reportException(error) { // https://html.spec.whatwg.org/#dom-reporterror function reportError(error) { - op_event_report_error(error); + FunctionPrototypeCall(op_event_report_error, this, error); } export { diff --git a/ext/web/event.rs b/ext/web/event.rs index e4bc26bcf6..b732192669 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -15,7 +15,6 @@ use deno_core::GarbageCollected; use deno_core::OpState; use deno_core::WebIDL; use deno_core::cppgc; -use deno_core::cppgc::Ptr; use deno_core::error::JsError; use deno_core::error::dispatch_exception; use deno_core::error::to_v8_error; @@ -39,7 +38,10 @@ pub enum EventError { ExpectedEvent, #[class(type)] #[error("Illegal invocation")] - Illegal, + InvalidCall, + #[class(type)] + #[error("Illegal constructor")] + InvalidConstructor, #[class("DOMExceptionInvalidStateError")] #[error("Invalid event state")] InvalidState, @@ -1563,7 +1565,7 @@ pub fn op_event_report_error<'a>( ) -> Result<(), EventError> { let global = scope.get_current_context().global(scope); if global != this { - return Err(EventError::Illegal); + return Err(EventError::InvalidCall); } report_exception(scope, &state, exception); Ok(()) @@ -2092,9 +2094,10 @@ impl AbortSignal { self.algorithms.borrow_mut().clear(); - let target: Ptr = cppgc::try_unwrap_cppgc_proto_object::< - EventTarget, - >(scope, signal_object.into()) + let target = cppgc::try_unwrap_cppgc_proto_object::( + scope, + signal_object.into(), + ) .unwrap(); let event_object = { let event = Event::new("abort".to_string(), None); @@ -2111,6 +2114,12 @@ impl AbortSignal { #[op2(inherit = EventTarget)] impl AbortSignal { + #[constructor] + #[cppgc] + fn constructor(_: bool) -> Result { + Err(EventError::InvalidConstructor) + } + #[static_method] fn abort<'a>( scope: &mut v8::HandleScope<'a>, From f217ecf8a80c0cee4b9d1f417f88c2213429dbe9 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Fri, 5 Sep 2025 03:11:01 +0900 Subject: [PATCH 13/18] wrap AbortController --- ext/web/03_abort_signal.js | 55 ++++++++++++++++---------------------- ext/web/event.rs | 52 ++++++++++++++++++++++++++++++++--- ext/web/lib.rs | 1 + 3 files changed, 72 insertions(+), 36 deletions(-) diff --git a/ext/web/03_abort_signal.js b/ext/web/03_abort_signal.js index 41e069982d..9a97a1e005 100644 --- a/ext/web/03_abort_signal.js +++ b/ext/web/03_abort_signal.js @@ -14,6 +14,7 @@ const { SymbolFor, } = primordials; import { + AbortController, AbortSignal, op_event_add_abort_algorithm, op_event_create_abort_signal, @@ -159,38 +160,28 @@ defineEventHandler(AbortSignal.prototype, "abort"); webidl.configureInterface(AbortSignal); const AbortSignalPrototype = AbortSignal.prototype; -const signal = Symbol("[[signal]]"); - -class AbortController { - [signal] = op_event_create_abort_signal(); - - constructor() { - this[webidl.brand] = webidl.brand; - } - - get signal() { - webidl.assertBranded(this, AbortControllerPrototype); - return this[signal]; - } - - abort(reason) { - webidl.assertBranded(this, AbortControllerPrototype); - op_event_signal_abort(this[signal], reason); - } - - [SymbolFor("Deno.privateCustomInspect")](inspect, inspectOptions) { - return inspect( - createFilteredInspectProxy({ - object: this, - evaluate: ObjectPrototypeIsPrototypeOf(AbortControllerPrototype, this), - keys: [ - "signal", - ], - }), - inspectOptions, - ); - } -} +ObjectDefineProperty( + AbortController.prototype, + SymbolFor("Deno.privateCustomInspect"), + { + __proto__: null, + value(inspect, inspectOptions) { + return inspect( + createFilteredInspectProxy({ + object: this, + evaluate: ObjectPrototypeIsPrototypeOf( + AbortControllerPrototype, + this, + ), + keys: [ + "signal", + ], + }), + inspectOptions, + ); + }, + }, +); webidl.configureInterface(AbortController); const AbortControllerPrototype = AbortController.prototype; diff --git a/ext/web/event.rs b/ext/web/event.rs index b732192669..3989167a8d 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -438,7 +438,6 @@ impl Event { } // legacy - // TODO(petamoriken): #[undefined] macro does not work properly #[required(1)] fn init_event<'a>( &self, @@ -775,7 +774,6 @@ impl CustomEvent { } // legacy - // TODO(petamoriken): #[undefined] macro does not work properly #[required(1)] fn init_custom_event<'a>( &self, @@ -2207,13 +2205,60 @@ impl AbortSignal { } } +pub struct AbortController { + signal: v8::Global, +} + +impl GarbageCollected for AbortController { + fn get_name(&self) -> &'static std::ffi::CStr { + c"AbortController" + } +} + +#[op2] +impl AbortController { + #[constructor] + #[cppgc] + fn constructor<'a>(scope: &mut v8::HandleScope<'a>) -> AbortController { + let event_target = EventTarget::new(); + let abort_signal = AbortSignal::new(); + let signal = cppgc::make_cppgc_empty_object::(scope); + cppgc::wrap_object2(scope, signal, (event_target, abort_signal)); + AbortController { + signal: v8::Global::new(scope, signal), + } + } + + #[getter] + #[global] + fn signal(&self) -> v8::Global { + self.signal.clone() + } + + fn abort<'a>( + &self, + scope: &mut v8::HandleScope<'a>, + state: Rc>, + reason: Option>, + ) -> v8::Local<'a, v8::Primitive> { + let undefined = v8::undefined(scope); + let signal_object = v8::Local::new(scope, self.signal.clone()); + let signal = cppgc::try_unwrap_cppgc_proto_object::( + scope, + signal_object.into(), + ) + .unwrap(); + signal.signal_abort(scope, &state, signal_object, reason); + undefined + } +} + #[op2] pub fn op_event_create_abort_signal<'a>( scope: &mut v8::HandleScope<'a>, ) -> v8::Local<'a, v8::Object> { let event_target = EventTarget::new(); let abort_signal = AbortSignal::new(); - let obj = cppgc::make_cppgc_empty_object::(scope); cppgc::wrap_object2(scope, obj, (event_target, abort_signal)) } @@ -2241,7 +2286,6 @@ pub fn op_event_create_dependent_abort_signal<'a>( prefix.into(), context.into(), )?; - Ok(cppgc::wrap_object2( scope, obj, diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 22c325458c..12bfc8faf4 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -119,6 +119,7 @@ deno_core::extension!(deno_web, event::ProgressEvent, event::EventTarget, event::AbortSignal, + event::AbortController, ], esm = [ "00_infra.js", From 647d50e86f50d04542f99e6f3169ec4dedb95d75 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Fri, 5 Sep 2025 05:42:02 +0900 Subject: [PATCH 14/18] update EventTarget#addEventListener --- ext/web/event.rs | 75 +++++++++++++++++++++++++++++++----------------- 1 file changed, 49 insertions(+), 26 deletions(-) diff --git a/ext/web/event.rs b/ext/web/event.rs index 3989167a8d..31ed1b804b 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -10,6 +10,7 @@ use std::collections::HashMap; use std::collections::HashSet; use std::collections::VecDeque; use std::rc::Rc; +use std::rc::Weak; use deno_core::GarbageCollected; use deno_core::OpState; @@ -1670,25 +1671,28 @@ impl EventTarget { None => false, }; - // TODO(petamoriken): Validate AbortSignal let key = v8::String::new(scope, "signal").unwrap(); let signal = match options.get(scope, key.into()) { Some(value) => { if value.is_undefined() { None } else { - if !value.is_object() { - return Err(EventError::WebIDL(WebIdlError::new( - prefix.into(), - (|| { - "'signal' of 'AddEventListenerOptions' (Argument 3)" - .into() - }) - .into(), - WebIdlErrorKind::ConvertToConverterType("object"), - ))); + match cppgc::try_unwrap_cppgc_proto_object::( + scope, value, + ) { + Some(signal) => Some(signal), + None => { + return Err(EventError::WebIDL(WebIdlError::new( + prefix.into(), + (|| { + "'signal' of 'AddEventListenerOptions' (Argument 3)" + .into() + }) + .into(), + WebIdlErrorKind::ConvertToConverterType("AbortSignal"), + ))); + } } - Some(value.cast::()) } } None => None, @@ -1723,14 +1727,7 @@ impl EventTarget { }; let aborted = match signal { - Some(signal) => { - let key = v8::String::new(scope, "aborted").unwrap(); - signal - .get(scope, key.into()) - .unwrap() - .to_boolean(scope) - .is_true() - } + Some(ref signal) => signal.aborted_inner(), None => false, }; if aborted { @@ -1751,6 +1748,7 @@ impl EventTarget { callback.cast::() } }; + let callback = v8::Global::new(scope, callback); let mut listeners = self.listeners.borrow_mut(); let listeners = listeners.entry(typ.clone()).or_default(); @@ -1760,16 +1758,41 @@ impl EventTarget { } } - // TODO(petamoriken): add signal listeners - - listeners.push(Rc::new(EventListener { - callback: Global::new(scope, callback), + let listener = Rc::new(EventListener { + callback, capture, passive, once, removed: Cell::new(false), resist_stop_immediate_propagation, - })); + }); + + if let Some(ref signal) = signal { + let abort_callback = |_scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue| { + let context = v8::Local::::try_from(args.data()) + .expect("Abort algorithm expected external data"); + // SAFETY: `context` is a valid pointer to a EventListener instance + let listener = + unsafe { Weak::from_raw(context.value() as *const EventListener) }; + let Some(listener) = listener.upgrade() else { + return; + }; + // TODO(petamoriken): remove listener from listeners + listener.removed.set(true); + }; + let listener = Rc::downgrade(&listener); + let external = v8::External::new(scope, Weak::into_raw(listener) as _); + let abort_algorithm = v8::Function::builder(abort_callback) + .data(external.into()) + .build(scope) + .expect("Failed to create abort algorithm"); + let abort_algorithm = v8::Global::new(scope, abort_algorithm); + signal.add(abort_algorithm); + } + + listeners.push(listener); Ok(()) } @@ -1835,7 +1858,7 @@ impl EventTarget { #[this] this: v8::Global, scope: &mut v8::HandleScope<'a>, state: Rc>, - event_object: v8::Local<'a, v8::Object>, + event_object: v8::Local<'a, v8::Value>, ) -> Result { let Some(event) = cppgc::try_unwrap_cppgc_proto_object::(scope, event_object.into()) From 09ed0a2dc2966eedf88c4b305ca0e588d745f936 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Fri, 5 Sep 2025 05:51:22 +0900 Subject: [PATCH 15/18] fix --- ext/web/event.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/ext/web/event.rs b/ext/web/event.rs index 31ed1b804b..a6d3bd24af 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -1865,6 +1865,7 @@ impl EventTarget { else { return Err(EventError::ExpectedEvent); }; + let event_object = event_object.cast::(); let typ = event.typ.borrow(); From 97c3ffbbe3a370a36e261d6350c96843f79d7d21 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Fri, 5 Sep 2025 06:07:02 +0900 Subject: [PATCH 16/18] fix --- ext/web/event.rs | 48 +++++++++++++++++++++++++++++++++++++----------- 1 file changed, 37 insertions(+), 11 deletions(-) diff --git a/ext/web/event.rs b/ext/web/event.rs index a6d3bd24af..97a28a653f 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -684,15 +684,24 @@ impl Event { } #[op2(fast)] -pub fn op_event_set_is_trusted(#[cppgc] event: &Event, value: bool) { +pub fn op_event_set_is_trusted<'a>( + scope: &mut v8::HandleScope<'a>, + event: v8::Local<'a, v8::Value>, + value: bool, +) { + let event = + cppgc::try_unwrap_cppgc_proto_object::(scope, event).unwrap(); event.is_trusted.set(value); } #[op2] -pub fn op_event_set_target( - #[cppgc] event: &Event, +pub fn op_event_set_target<'a>( + scope: &mut v8::HandleScope<'a>, + event: v8::Local<'a, v8::Value>, #[global] value: v8::Global, ) { + let event = + cppgc::try_unwrap_cppgc_proto_object::(scope, event).unwrap(); event.target.replace(Some(value)); } @@ -1910,9 +1919,12 @@ pub fn op_event_wrap_event_target<'a>( #[op2] pub fn op_event_get_target_listeners<'a>( scope: &mut v8::HandleScope<'a>, - #[cppgc] event_target: &EventTarget, + event_target: v8::Local<'a, v8::Value>, #[string] typ: String, ) -> v8::Local<'a, v8::Array> { + let event_target = + cppgc::try_unwrap_cppgc_proto_object::(scope, event_target) + .unwrap(); let listeners = event_target.listeners.borrow(); match listeners.get(&typ) { Some(listeners) => { @@ -1928,9 +1940,13 @@ pub fn op_event_get_target_listeners<'a>( #[op2(fast)] pub fn op_event_get_target_listener_count<'a>( - #[cppgc] event_target: &EventTarget, + scope: &mut v8::HandleScope<'a>, + event_target: v8::Local<'a, v8::Value>, #[string] typ: String, ) -> u32 { + let event_target = + cppgc::try_unwrap_cppgc_proto_object::(scope, event_target) + .unwrap(); let listeners = event_target.listeners.borrow(); match listeners.get(&typ) { Some(listeners) => listeners.len() as u32, @@ -2318,18 +2334,24 @@ pub fn op_event_create_dependent_abort_signal<'a>( } #[op2] -pub fn op_event_add_abort_algorithm( - #[cppgc] signal: &AbortSignal, +pub fn op_event_add_abort_algorithm<'a>( + scope: &mut v8::HandleScope<'a>, + signal: v8::Local<'a, v8::Value>, #[global] algorithm: v8::Global, ) { + let signal = + cppgc::try_unwrap_cppgc_proto_object::(scope, signal).unwrap(); signal.add(algorithm); } #[op2] -pub fn op_event_remove_abort_algorithm( - #[cppgc] signal: &AbortSignal, +pub fn op_event_remove_abort_algorithm<'a>( + scope: &mut v8::HandleScope<'a>, + signal: v8::Local<'a, v8::Value>, #[global] algorithm: v8::Global, ) { + let signal = + cppgc::try_unwrap_cppgc_proto_object::(scope, signal).unwrap(); signal.remove(algorithm); } @@ -2351,8 +2373,10 @@ pub fn op_event_signal_abort<'a>( #[op2] pub fn op_event_get_source_signals<'a>( scope: &mut v8::HandleScope<'a>, - #[cppgc] signal: &AbortSignal, + signal: v8::Local<'a, v8::Value>, ) -> v8::Local<'a, v8::Array> { + let signal = + cppgc::try_unwrap_cppgc_proto_object::(scope, signal).unwrap(); let mut elements = Vec::new(); let source_signals = signal.source_signals.borrow(); for source_signal in source_signals.iter() { @@ -2366,8 +2390,10 @@ pub fn op_event_get_source_signals<'a>( #[op2] pub fn op_event_get_dependent_signals<'a>( scope: &mut v8::HandleScope<'a>, - #[cppgc] signal: &AbortSignal, + signal: v8::Local<'a, v8::Value>, ) -> v8::Local<'a, v8::Array> { + let signal = + cppgc::try_unwrap_cppgc_proto_object::(scope, signal).unwrap(); let mut elements = Vec::new(); let dependent_signals = signal.dependent_signals.borrow(); for dependent_signal in dependent_signals.iter() { From 52037ecc7a5bfe241aced384c575d5a4bcb3cc40 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Fri, 5 Sep 2025 12:53:58 +0900 Subject: [PATCH 17/18] update --- ext/web/02_event.js | 15 +++++++++++++++ ext/web/13_message_port.js | 3 ++- ext/web/15_performance.js | 10 +++------- ext/web/event.rs | 17 ++++++++++++++++- ext/web/lib.rs | 1 + ext/websocket/01_websocket.js | 3 ++- 6 files changed, 39 insertions(+), 10 deletions(-) diff --git a/ext/web/02_event.js b/ext/web/02_event.js index 2aecdd2e50..d364aa90ac 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -14,6 +14,7 @@ const { ObjectDefineProperty, ObjectDefineProperties, ObjectPrototypeIsPrototypeOf, + ObjectSetPrototypeOf, SafeMap, Symbol, SymbolFor, @@ -25,6 +26,7 @@ import { Event, EventTarget, MessageEvent, + op_event_create_empty_event_target, op_event_dispatch, op_event_get_target_listener_count, op_event_get_target_listeners, @@ -167,6 +169,18 @@ defineEnumerableProps(Event.prototype, EVENT_PROPS); // Accessors for non-public data +/** + * NOTE: It is necessary to call setEventTargetData at runtime, not at the snapshot timing. + * @param {object} prototype + * @returns {object} + */ +function createEventTargetBranded(prototype) { + let t = op_event_create_empty_event_target(); + t[webidl.brand] = webidl.brand; + ObjectSetPrototypeOf(t, prototype); + return t; +} + /** * @param {object} target */ @@ -512,6 +526,7 @@ function reportError(error) { export { CloseEvent, + createEventTargetBranded, CustomEvent, defineEventHandler, dispatch, diff --git a/ext/web/13_message_port.js b/ext/web/13_message_port.js index c9fd7dba50..fb08783a88 100644 --- a/ext/web/13_message_port.js +++ b/ext/web/13_message_port.js @@ -34,6 +34,7 @@ const { import * as webidl from "ext:deno_webidl/00_webidl.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; import { + createEventTargetBranded, defineEventHandler, EventTarget, MessageEvent, @@ -111,7 +112,7 @@ export const unrefParentPort = Symbol("unrefParentPort"); * @returns {MessagePort} */ function createMessagePort(id) { - const port = webidl.createBranded(MessagePort); + const port = createEventTargetBranded(MessagePortPrototype); port[core.hostObjectBrand] = core.hostObjectBrand; setEventTargetData(port); port[_id] = id; diff --git a/ext/web/15_performance.js b/ext/web/15_performance.js index aaac2ec575..8231d2b100 100644 --- a/ext/web/15_performance.js +++ b/ext/web/15_performance.js @@ -20,7 +20,7 @@ const { import * as webidl from "ext:deno_webidl/00_webidl.js"; import { structuredClone } from "./02_structured_clone.js"; import { createFilteredInspectProxy } from "ext:deno_console/01_console.js"; -import { EventTarget } from "./02_event.js"; +import { EventTarget, createEventTargetBranded } from "./02_event.js"; import { DOMException } from "./01_dom_exception.js"; const illegalConstructorKey = Symbol("illegalConstructorKey"); @@ -365,11 +365,7 @@ const PerformanceMeasurePrototype = PerformanceMeasure.prototype; class Performance { constructor(key = null) { - if (key != illegalConstructorKey) { - webidl.illegalConstructor(); - } - - this[webidl.brand] = webidl.brand; + webidl.illegalConstructor(); } get timeOrigin() { @@ -616,7 +612,7 @@ webidl.converters["Performance"] = webidl.createInterfaceConverter( PerformancePrototype, ); -const performance = new Performance(illegalConstructorKey); +const performance = createEventTargetBranded(Performance.prototype); export { Performance, diff --git a/ext/web/event.rs b/ext/web/event.rs index 97a28a653f..f8f4446fae 100644 --- a/ext/web/event.rs +++ b/ext/web/event.rs @@ -1908,12 +1908,27 @@ impl EventTarget { } } +#[op2] +pub fn op_event_create_empty_event_target<'a>( + scope: &mut v8::HandleScope<'a>, +) -> v8::Local<'a, v8::Object> { + cppgc::make_cppgc_empty_object::(scope) +} + +#[inline] +pub fn set_event_target_data<'a>( + scope: &mut v8::HandleScope<'a>, + obj: v8::Local<'a, v8::Object>, +) { + cppgc::wrap_object1(scope, obj, EventTarget::new()); +} + #[op2(fast)] pub fn op_event_wrap_event_target<'a>( scope: &mut v8::HandleScope<'a>, obj: v8::Local<'a, v8::Object>, ) { - cppgc::wrap_object1(scope, obj, EventTarget::new()); + set_event_target_data(scope, obj); } #[op2] diff --git a/ext/web/lib.rs b/ext/web/lib.rs index 12bfc8faf4..932c56a659 100644 --- a/ext/web/lib.rs +++ b/ext/web/lib.rs @@ -87,6 +87,7 @@ deno_core::extension!(deno_web, event::op_event_get_target_listeners, event::op_event_set_is_trusted, event::op_event_set_target, + event::op_event_create_empty_event_target, event::op_event_wrap_event_target, event::op_event_report_error, event::op_event_report_exception, diff --git a/ext/websocket/01_websocket.js b/ext/websocket/01_websocket.js index c6ae46920b..0c19f004d6 100644 --- a/ext/websocket/01_websocket.js +++ b/ext/websocket/01_websocket.js @@ -54,6 +54,7 @@ import { HTTP_TOKEN_CODE_POINT_RE } from "ext:deno_web/00_infra.js"; import { DOMException } from "ext:deno_web/01_dom_exception.js"; import { clearTimeout, setTimeout } from "ext:deno_web/02_timers.js"; import { + createEventTargetBranded, CloseEvent, defineEventHandler, dispatch, @@ -717,7 +718,7 @@ webidl.configureInterface(WebSocket); const WebSocketPrototype = WebSocket.prototype; function createWebSocketBranded() { - const socket = webidl.createBranded(WebSocket); + const socket = createEventTargetBranded(WebSocketPrototype); socket[_rid] = undefined; socket[_role] = undefined; socket[_readyState] = CONNECTING; From 0c6deae6c5e01114d0ff0fd7492700d1a8caa5f2 Mon Sep 17 00:00:00 2001 From: Kenta Moriuchi Date: Fri, 5 Sep 2025 13:11:29 +0900 Subject: [PATCH 18/18] tweak --- ext/web/02_event.js | 2 +- ext/web/15_performance.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/ext/web/02_event.js b/ext/web/02_event.js index d364aa90ac..d003da8aa1 100644 --- a/ext/web/02_event.js +++ b/ext/web/02_event.js @@ -175,7 +175,7 @@ defineEnumerableProps(Event.prototype, EVENT_PROPS); * @returns {object} */ function createEventTargetBranded(prototype) { - let t = op_event_create_empty_event_target(); + const t = op_event_create_empty_event_target(); t[webidl.brand] = webidl.brand; ObjectSetPrototypeOf(t, prototype); return t; diff --git a/ext/web/15_performance.js b/ext/web/15_performance.js index 8231d2b100..9476eebcac 100644 --- a/ext/web/15_performance.js +++ b/ext/web/15_performance.js @@ -364,7 +364,7 @@ webidl.configureInterface(PerformanceMeasure); const PerformanceMeasurePrototype = PerformanceMeasure.prototype; class Performance { - constructor(key = null) { + constructor() { webidl.illegalConstructor(); }