Attempt to improve non-DOM EventTarget dispatching

There are two main ways to dispatch events, both via the EventManager: dispatch
and dispatchWithFunction. dispatchWithFunction came about from having to
dispatch to function callbacks in addition to event listeners. Specifically,
firing the window.onload callback.

Since that original design, much has changed. Most significantly, with
https://github.com/lightpanda-io/browser/pull/1524 callbacks defined via
attributes became properly (I hope) integrated with the event dispatching.
Furthermore, the number of non-tree event targets (e.g. AbortSignal) has grown
significantly. Finally, dispatching an event is DOM-based event is pretty
complex, involving multiple phases and capturing the path.

The current design is largely correct, but non-obvious. This commit attempts to
improve the ergonomics of event dispatching.

`dispatchWithFunction` has been renamed to `dispatchDirect`. This function is
meant to be used with non-DOM event targets. It is optimized for having an event
path with a single target andh no bubbling/capture phase. In addition to being
a little more streamlined, `dispatchDirect` will internally turn a
`js.Function.Global` or `js.Function.Temp` into a local. This makes the callsite
simpler, but also provides optimization opportunity - not having to create
a new scope for the common case of having no callback/listener. This lays the
groundwork for having a `hasDirect` guard clause at the callsite to avoid
unnecessary event creation (todo in a follow up commit).

`dispatch` remains unchanged. While `dispatch` is primarily meant to handle the
DOM-based EventTarget, it will forward non-DOM EventTargets to `dispatchDirect`.
This is necessary since JS code can call `signal.dispatchEvent(....)`.

Two notes:
1 - The flow of dispatchDirect is an optimization. The spec makes no distinction
    between DOM and non-DOM based EventTargets.
2 - While the window (as an EventTarget) should probably be thought of as a
    DOM-based EventTarget, we use `dispatchDirect with it. This is because it
    sits at the root and thus can safely go through the faster `dispatchDirect`.
This commit is contained in:
Karl Seguin
2026-03-02 15:11:02 +08:00
parent 516335e0ed
commit ce73f7ac5a
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 },
);
}