Merge pull request #1690 from lightpanda-io/event_dispatch_cleanup

Attempt to improve non-DOM EventTarget dispatching
This commit is contained in:
Karl Seguin
2026-03-02 17:40:29 +08:00
committed by GitHub
12 changed files with 189 additions and 155 deletions

View File

@@ -211,38 +211,9 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
}
event._target = target;
event._dispatch_target = target; // Store original target for composedPath()
var was_handled = false;
defer if (was_handled) {
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
ls.local.runMicrotasks();
};
switch (target._type) {
.node => |node| try self.dispatchNode(node, event, &was_handled, opts),
.xhr,
.window,
.abort_signal,
.media_query_list,
.message_port,
.text_track_cue,
.navigation,
.screen,
.screen_orientation,
.visual_viewport,
.file_reader,
.generic,
=> {
const list = self.lookup.get(.{
.event_target = @intFromPtr(target),
.type_string = event._type_string,
}) orelse return;
try self.dispatchAll(list, target, event, &was_handled, opts);
},
.node => |node| try self.dispatchNode(node, event, opts),
else => try self.dispatchDirect(target, event, null, .{ .context = "dispatch" }),
}
}
@@ -251,16 +222,22 @@ pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, co
// property is just a shortcut for calling addEventListener, but they are distinct.
// An event set via property cannot be removed by removeEventListener. If you
// set both the property and add a listener, they both execute.
const DispatchWithFunctionOptions = struct {
const DispatchDirectOptions = struct {
context: []const u8,
inject_target: bool = true,
};
pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *Event, function_: ?js.Function, comptime opts: DispatchWithFunctionOptions) !void {
// Direct dispatch for non-DOM targets (Window, XHR, AbortSignal) or DOM nodes with
// property handlers. No propagation - just calls the handler and registered listeners.
// Handler can be: null, ?js.Function.Global, ?js.Function.Temp, or js.Function
pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, handler: anytype, comptime opts: DispatchDirectOptions) !void {
const page = self.page;
event.acquireRef();
defer event.deinit(false, self.page);
defer event.deinit(false, page);
if (comptime IS_DEBUG) {
log.debug(.event, "dispatchWithFunction", .{ .type = event._type_string.str(), .context = opts.context, .has_function = function_ != null });
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });
}
if (comptime opts.inject_target) {
@@ -269,14 +246,15 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
}
var was_dispatched = false;
defer if (was_dispatched) {
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
ls.local.runMicrotasks();
};
if (function_) |func| {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer {
ls.local.runMicrotasks();
ls.deinit();
}
if (getFunction(handler, &ls.local)) |func| {
event._current_target = target;
if (func.callWithThis(void, target, .{event})) {
was_dispatched = true;
@@ -286,17 +264,126 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
}
}
// listeners reigstered via addEventListener
const list = self.lookup.get(.{
.event_target = @intFromPtr(target),
.type_string = event._type_string,
}) orelse return;
try self.dispatchAll(list, target, event, &was_dispatched, .{});
// This is a slightly simplified version of what you'll find in dispatchPhase
// It is simpler because, for direct dispatching, we know there's no ancestors
// and only the single target phase.
// Track dispatch depth for deferred removal
self.dispatch_depth += 1;
defer {
const dispatch_depth = self.dispatch_depth;
// Only destroy deferred listeners when we exit the outermost dispatch
if (dispatch_depth == 1) {
for (self.deferred_removals.items) |removal| {
removal.list.remove(&removal.listener.node);
self.listener_pool.destroy(removal.listener);
}
self.deferred_removals.clearRetainingCapacity();
} else {
self.dispatch_depth = dispatch_depth - 1;
}
}
// Use the last listener in the list as sentinel - listeners added during dispatch will be after it
const last_node = list.last orelse return;
const last_listener: *Listener = @alignCast(@fieldParentPtr("node", last_node));
// Iterate through the list, stopping after we've encountered the last_listener
var node = list.first;
var is_done = false;
while (node) |n| {
if (is_done) {
break;
}
const listener: *Listener = @alignCast(@fieldParentPtr("node", n));
is_done = (listener == last_listener);
node = n.next;
// Skip removed listeners
if (listener.removed) {
continue;
}
// If the listener has an aborted signal, remove it and skip
if (listener.signal) |signal| {
if (signal.getAborted()) {
self.removeListener(list, listener);
continue;
}
}
// Remove "once" listeners BEFORE calling them so nested dispatches don't see them
if (listener.once) {
self.removeListener(list, listener);
}
was_dispatched = true;
event._current_target = target;
switch (listener.function) {
.value => |value| try ls.toLocal(value).callWithThis(void, target, .{event}),
.string => |string| {
const str = try page.call_arena.dupeZ(u8, string.str());
try ls.local.eval(str, null);
},
.object => |obj_global| {
const obj = ls.toLocal(obj_global);
if (try obj.getFunction("handleEvent")) |handleEvent| {
try handleEvent.callWithThis(void, obj, .{event});
}
},
}
if (event._stop_immediate_propagation) {
return;
}
}
}
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
fn getFunction(handler: anytype, local: *const js.Local) ?js.Function {
const T = @TypeOf(handler);
const ti = @typeInfo(T);
if (ti == .null) {
return null;
}
if (ti == .optional) {
return getFunction(handler orelse return null, local);
}
return switch (T) {
js.Function => handler,
js.Function.Temp => local.toLocal(handler),
js.Function.Global => local.toLocal(handler),
else => @compileError("handler must be null or \\??js.Function(\\.(Temp|Global))?"),
};
}
fn dispatchNode(self: *EventManager, target: *Node, event: *Event, comptime opts: DispatchOpts) !void {
const ShadowRoot = @import("webapi/ShadowRoot.zig");
{
const et = target.asEventTarget();
event._target = et;
event._dispatch_target = et; // Store original target for composedPath()
}
const page = self.page;
var was_handled = false;
defer if (was_handled) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
ls.local.runMicrotasks();
};
const activation_state = ActivationState.create(event, target, page);
// Defer runs even on early return - ensures event phase is reset
@@ -374,7 +461,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
.event_target = @intFromPtr(current_target),
.type_string = event._type_string,
})) |list| {
try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(true, opts));
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(true, opts));
}
}
@@ -386,7 +473,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
blk: {
// Get inline handler (e.g., onclick property) for this target
if (self.getInlineHandler(target_et, event)) |inline_handler| {
was_handled.* = true;
was_handled = true;
event._current_target = target_et;
var ls: js.Local.Scope = undefined;
@@ -408,7 +495,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
.type_string = event._type_string,
.event_target = @intFromPtr(target_et),
})) |list| {
try self.dispatchPhase(list, target_et, event, was_handled, comptime .init(null, opts));
try self.dispatchPhase(list, target_et, event, &was_handled, comptime .init(null, opts));
if (event._stop_propagation) {
return;
}
@@ -425,7 +512,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled:
.type_string = event._type_string,
.event_target = @intFromPtr(current_target),
})) |list| {
try self.dispatchPhase(list, current_target, event, was_handled, comptime .init(false, opts));
try self.dispatchPhase(list, current_target, event, &was_handled, comptime .init(false, opts));
}
}
}
@@ -549,11 +636,6 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
}
}
// Non-Node dispatching (XHR, Window without propagation)
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime opts: DispatchOpts) !void {
return self.dispatchPhase(list, current_target, event, was_handled, comptime .init(null, opts));
}
fn getInlineHandler(self: *EventManager, target: *EventTarget, event: *Event) ?js.Function.Global {
const global_event_handlers = @import("webapi/global_event_handlers.zig");
const handler_type = global_event_handlers.fromEventType(event._type_string.str()) orelse return null;

View File

@@ -131,7 +131,7 @@ _element_namespace_uris: Element.NamespaceUriLookup = .empty,
/// ```js
/// img.setAttribute("onload", "(() => { ... })()");
/// ```
_element_attr_listeners: GlobalEventHandlersLookup = .empty,
_event_target_attr_listeners: GlobalEventHandlersLookup = .empty,
// Blob URL registry for URL.createObjectURL/revokeObjectURL
_blob_urls: std.StringHashMapUnmanaged(*Blob) = .{},
@@ -726,27 +726,23 @@ fn _documentIsComplete(self: *Page) !void {
// Run load events before window.load.
try self.dispatchLoad();
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
// Dispatch window.load event.
const event = try Event.initTrusted(comptime .wrap("load"), .{}, self);
// This event is weird, it's dispatched directly on the window, but
// with the document as the target.
event._target = self.document.asEventTarget();
try self._event_manager.dispatchWithFunction(
try self._event_manager.dispatchDirect(
self.window.asEventTarget(),
event,
ls.toLocal(self.window._on_load),
self.window._on_load,
.{ .inject_target = false, .context = "page load" },
);
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
try self._event_manager.dispatchWithFunction(
try self._event_manager.dispatchDirect(
self.window.asEventTarget(),
pageshow_event,
ls.toLocal(self.window._on_pageshow),
self.window._on_pageshow,
.{ .context = "page show" },
);
@@ -3127,20 +3123,13 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
const form_element = form.asElement();
if (submit_opts.fire_event) {
const onsubmit_handler = try form.asHtmlElement().getOnSubmit(self);
const submit_event = try Event.initTrusted(comptime .wrap("submit"), .{ .bubbles = true, .cancelable = true }, self);
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
try self._event_manager.dispatchWithFunction(
form_element.asEventTarget(),
submit_event,
ls.toLocal(onsubmit_handler),
.{ .context = "form submit" },
);
// so submit_event is still valid when we check _prevent_default
submit_event.acquireRef();
defer submit_event.deinit(false, self);
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
// If the submit event was prevented, don't submit the form
if (submit_event._prevent_default) {
return;

View File

@@ -38,7 +38,7 @@ pub fn getSignal(self: *const AbortController) *AbortSignal {
}
pub fn abort(self: *AbortController, reason_: ?js.Value.Global, page: *Page) !void {
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page);
try self._signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
}
pub const JsApi = struct {

View File

@@ -57,7 +57,7 @@ pub fn asEventTarget(self: *AbortSignal) *EventTarget {
return self._proto;
}
pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page: *Page) !void {
pub fn abort(self: *AbortSignal, reason_: ?Reason, page: *Page) !void {
if (self._aborted) {
return;
}
@@ -77,10 +77,10 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page:
// Dispatch abort event
const event = try Event.initTrusted(comptime .wrap("abort"), .{}, page);
try page._event_manager.dispatchWithFunction(
try page._event_manager.dispatchDirect(
self.asEventTarget(),
event,
local.toLocal(self._on_abort),
self._on_abort,
.{ .context = "abort signal" },
);
}
@@ -88,7 +88,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, local: *const js.Local, page:
// Static method to create an already-aborted signal
pub fn createAborted(reason_: ?js.Value.Global, page: *Page) !*AbortSignal {
const signal = try init(page);
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page.js.local.?, page);
try signal.abort(if (reason_) |r| .{ .js_val = r } else null, page);
return signal;
}
@@ -136,11 +136,7 @@ const TimeoutCallback = struct {
fn run(ctx: *anyopaque) !?u32 {
const self: *TimeoutCallback = @ptrCast(@alignCast(ctx));
var ls: js.Local.Scope = undefined;
self.page.js.localScope(&ls);
defer ls.deinit();
self.signal.abort(.{ .string = "TimeoutError" }, &ls.local, self.page) catch |err| {
self.signal.abort(.{ .string = "TimeoutError" }, self.page) catch |err| {
log.warn(.app, "abort signal timeout", .{ .err = err });
};
return null;

View File

@@ -183,12 +183,7 @@ fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
const page = self._page;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local = &ls.local;
try self.dispatch(.load_start, .{ .loaded = 0, .total = blob.getSize() }, local, page);
try self.dispatch(.load_start, .{ .loaded = 0, .total = blob.getSize() }, page);
if (self._aborted) {
return;
}
@@ -196,7 +191,7 @@ fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
// Perform the read (synchronous since data is in memory)
const data = blob._slice;
const size = data.len;
try self.dispatch(.progress, .{ .loaded = size, .total = size }, local, page);
try self.dispatch(.progress, .{ .loaded = size, .total = size }, page);
if (self._aborted) {
return;
}
@@ -216,8 +211,8 @@ fn readInternal(self: *FileReader, blob: *Blob, read_type: ReadType) !void {
self._ready_state = .done;
try self.dispatch(.load, .{ .loaded = size, .total = size }, local, page);
try self.dispatch(.load_end, .{ .loaded = size, .total = size }, local, page);
try self.dispatch(.load, .{ .loaded = size, .total = size }, page);
try self.dispatch(.load_end, .{ .loaded = size, .total = size }, page);
}
pub fn abort(self: *FileReader) !void {
@@ -231,17 +226,12 @@ pub fn abort(self: *FileReader) !void {
const page = self._page;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local = &ls.local;
try self.dispatch(.abort, null, page);
try self.dispatch(.abort, null, local, page);
try self.dispatch(.load_end, null, local, page);
try self.dispatch(.load_end, null, page);
}
fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Progress, local: *const js.Local, page: *Page) !void {
fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Progress, page: *Page) !void {
const field, const typ = comptime blk: {
break :blk switch (event_type) {
.abort => .{ "_on_abort", "abort" },
@@ -260,10 +250,10 @@ fn dispatch(self: *FileReader, comptime event_type: DispatchType, progress_: ?Pr
page,
)).asEvent();
return page._event_manager.dispatchWithFunction(
return page._event_manager.dispatchDirect(
self.asEventTarget(),
event,
local.toLocal(@field(self, field)),
@field(self, field),
.{ .context = "FileReader " ++ typ },
);
}

View File

@@ -80,10 +80,10 @@ fn goInner(delta: i32, page: *Page) !void {
if (entry._url) |url| {
if (try page.isSameOrigin(url)) {
const event = (try PopStateEvent.initTrusted(comptime .wrap("popstate"), .{ .state = entry._state.value }, page)).asEvent();
try page._event_manager.dispatchWithFunction(
try page._event_manager.dispatchDirect(
page.window.asEventTarget(),
event,
page.js.toLocal(page.window._on_popstate),
page.window._on_popstate,
.{ .context = "Pop State" },
);
}

View File

@@ -131,14 +131,10 @@ const PostMessageCallback = struct {
return null;
}).asEvent();
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
page._event_manager.dispatchWithFunction(
page._event_manager.dispatchDirect(
self.port.asEventTarget(),
event,
ls.toLocal(self.port._on_message),
self.port._on_message,
.{ .context = "MessagePort message" },
) catch |err| {
log.err(.dom, "MessagePort.postMessage", .{ .err = err });

View File

@@ -556,10 +556,10 @@ pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection,
.promise = try rejection.promise().temp(),
}, page)).asEvent();
try page._event_manager.dispatchWithFunction(
try page._event_manager.dispatchDirect(
self.asEventTarget(),
event,
rejection.local.toLocal(self._on_unhandled_rejection),
self._on_unhandled_rejection,
.{ .inject_target = true, .context = "window.unhandledrejection" },
);
}

View File

@@ -380,7 +380,7 @@ pub fn getAttributeFunction(
page: *Page,
) !?js.Function.Global {
const element = self.asElement();
if (page._element_attr_listeners.get(.{ .target = element.asEventTarget(), .handler = listener_type })) |cached| {
if (page._event_target_attr_listeners.get(.{ .target = element.asEventTarget(), .handler = listener_type })) |cached| {
return cached;
}
@@ -404,7 +404,7 @@ pub fn getAttributeFunction(
}
pub fn hasAttributeFunction(self: *HtmlElement, listener_type: GlobalEventHandler, page: *const Page) bool {
return page._element_attr_listeners.contains(.{ .target = self.asEventTarget(), .handler = listener_type });
return page._event_target_attr_listeners.contains(.{ .target = self.asEventTarget(), .handler = listener_type });
}
fn setAttributeListener(
@@ -421,7 +421,7 @@ fn setAttributeListener(
}
if (listener_callback) |cb| {
try page._element_attr_listeners.put(page.arena, .{
try page._event_target_attr_listeners.put(page.arena, .{
.target = self.asEventTarget(),
.handler = listener_type,
}, cb);
@@ -429,7 +429,7 @@ fn setAttributeListener(
}
// The listener is null, remove existing listener.
_ = page._element_attr_listeners.remove(.{
_ = page._event_target_attr_listeners.remove(.{
.target = self.asEventTarget(),
.handler = listener_type,
});

View File

@@ -440,14 +440,10 @@ pub fn updateCurrentEntry(self: *Navigation, options: UpdateCurrentEntryOptions,
}
pub fn dispatch(self: *Navigation, func: js.Function.Global, event: *Event, page: *Page) !void {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
return page._event_manager.dispatchWithFunction(
return page._event_manager.dispatchDirect(
self.asEventTarget(),
event,
ls.toLocal(func),
func,
.{ .context = "Navigation" },
);
}

View File

@@ -184,7 +184,7 @@ pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void
const page = self._page;
self._method = try parseMethod(method_);
self._url = try URL.resolve(self._arena, page.base(), url, .{ .always_dupe = true, .encode = true });
try self.stateChanged(.opened, page.js.local.?, page);
try self.stateChanged(.opened, page);
}
pub fn setRequestHeader(self: *XMLHttpRequest, name: []const u8, value: []const u8, page: *Page) !void {
@@ -397,11 +397,10 @@ fn httpHeaderDoneCallback(transfer: *Http.Transfer) !bool {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local = &ls.local;
try self.stateChanged(.headers_received, local, page);
try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, local, page);
try self.stateChanged(.loading, local, page);
try self.stateChanged(.headers_received, page);
try self._proto.dispatch(.load_start, .{ .loaded = 0, .total = self._response_len orelse 0 }, page);
try self.stateChanged(.loading, page);
return true;
}
@@ -412,14 +411,10 @@ fn httpDataCallback(transfer: *Http.Transfer, data: []const u8) !void {
const page = self._page;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
try self._proto.dispatch(.progress, .{
.total = self._response_len orelse 0,
.loaded = self._response_data.items.len,
}, &ls.local, page);
}, page);
}
fn httpDoneCallback(ctx: *anyopaque) !void {
@@ -438,22 +433,17 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
const page = self._page;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local = &ls.local;
try self.stateChanged(.done, local, page);
try self.stateChanged(.done, page);
const loaded = self._response_data.items.len;
try self._proto.dispatch(.load, .{
.total = loaded,
.loaded = loaded,
}, local, page);
}, page);
try self._proto.dispatch(.load_end, .{
.total = loaded,
.loaded = loaded,
}, local, page);
}, page);
page.js.weakRef(self);
}
@@ -495,17 +485,12 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
if (new_state != self._ready_state) {
const page = self._page;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const local = &ls.local;
try self.stateChanged(new_state, local, page);
try self.stateChanged(new_state, page);
if (is_abort) {
try self._proto.dispatch(.abort, null, local, page);
try self._proto.dispatch(.abort, null, page);
}
try self._proto.dispatch(.err, null, local, page);
try self._proto.dispatch(.load_end, null, local, page);
try self._proto.dispatch(.err, null, page);
try self._proto.dispatch(.load_end, null, page);
}
const level: log.Level = if (err == error.Abort) .debug else .err;
@@ -516,7 +501,7 @@ fn _handleError(self: *XMLHttpRequest, err: anyerror) !void {
});
}
fn stateChanged(self: *XMLHttpRequest, state: ReadyState, local: *const js.Local, page: *Page) !void {
fn stateChanged(self: *XMLHttpRequest, state: ReadyState, page: *Page) !void {
if (state == self._ready_state) {
return;
}
@@ -524,10 +509,10 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, local: *const js.Local
self._ready_state = state;
const event = try Event.initTrusted(.wrap("readystatechange"), .{}, page);
try page._event_manager.dispatchWithFunction(
try page._event_manager.dispatchDirect(
self.asEventTarget(),
event,
local.toLocal(self._on_ready_state_change),
self._on_ready_state_change,
.{ .context = "XHR state change" },
);
}

View File

@@ -43,7 +43,7 @@ pub fn asEventTarget(self: *XMLHttpRequestEventTarget) *EventTarget {
return self._proto;
}
pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchType, progress_: ?Progress, local: *const js.Local, page: *Page) !void {
pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchType, progress_: ?Progress, page: *Page) !void {
const field, const typ = comptime blk: {
break :blk switch (event_type) {
.abort => .{ "_on_abort", "abort" },
@@ -63,10 +63,10 @@ pub fn dispatch(self: *XMLHttpRequestEventTarget, comptime event_type: DispatchT
page,
)).asEvent();
return page._event_manager.dispatchWithFunction(
return page._event_manager.dispatchDirect(
self.asEventTarget(),
event,
local.toLocal(@field(self, field)),
@field(self, field),
.{ .context = "XHR " ++ typ },
);
}