explicitly run microtasks

This commit is contained in:
Karl Seguin
2025-11-25 15:54:25 +08:00
parent 218d08b1f6
commit 35a728e69f
5 changed files with 45 additions and 27 deletions

View File

@@ -107,12 +107,19 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) !void
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles }); log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
} }
event._target = target; event._target = target;
var was_handled = false;
defer if (was_handled) {
self.page.js.runMicrotasks();
};
switch (target._type) { switch (target._type) {
.node => |node| try self.dispatchNode(node, event), .node => |node| try self.dispatchNode(node, event, &was_handled),
.xhr, .window, .abort_signal, .media_query_list => { .xhr, .window, .abort_signal, .media_query_list => {
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
try self.dispatchAll(list, target, event); try self.dispatchAll(list, target, event, &was_handled);
}, },
} }
} }
@@ -135,19 +142,26 @@ pub fn dispatchWithFunction(self: *EventManager, target: *EventTarget, event: *E
event._target = target; event._target = target;
} }
var was_dispatched = false;
defer if (was_dispatched) {
self.page.js.runMicrotasks();
};
if (function_) |func| { if (function_) |func| {
event._current_target = target; event._current_target = target;
func.call(void, .{event}) catch |err| { if (func.call(void, .{event})) {
was_dispatched = true;
} else |err| {
// a non-JS error // a non-JS error
log.warn(.event, opts.context, .{ .err = err }); log.warn(.event, opts.context, .{ .err = err });
}; }
} }
const list = self.lookup.getPtr(@intFromPtr(target)) orelse return; const list = self.lookup.getPtr(@intFromPtr(target)) orelse return;
try self.dispatchAll(list, target, event); try self.dispatchAll(list, target, event, &was_dispatched);
} }
fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void { fn dispatchNode(self: *EventManager, target: *Node, event: *Event, was_handled: *bool) !void {
var path_len: usize = 0; var path_len: usize = 0;
var path_buffer: [128]*EventTarget = undefined; var path_buffer: [128]*EventTarget = undefined;
@@ -175,7 +189,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
i -= 1; i -= 1;
const current_target = path[i]; const current_target = path[i];
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| { if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
try self.dispatchPhase(list, current_target, event, true); try self.dispatchPhase(list, current_target, event, was_handled, true);
if (event._stop_propagation) { if (event._stop_propagation) {
event._event_phase = .none; event._event_phase = .none;
return; return;
@@ -187,7 +201,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
event._event_phase = .at_target; event._event_phase = .at_target;
const target_et = target.asEventTarget(); const target_et = target.asEventTarget();
if (self.lookup.getPtr(@intFromPtr(target_et))) |list| { if (self.lookup.getPtr(@intFromPtr(target_et))) |list| {
try self.dispatchPhase(list, target_et, event, null); try self.dispatchPhase(list, target_et, event, was_handled, null);
if (event._stop_propagation) { if (event._stop_propagation) {
event._event_phase = .none; event._event_phase = .none;
return; return;
@@ -200,7 +214,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
event._event_phase = .bubbling_phase; event._event_phase = .bubbling_phase;
for (path[1..]) |current_target| { for (path[1..]) |current_target| {
if (self.lookup.getPtr(@intFromPtr(current_target))) |list| { if (self.lookup.getPtr(@intFromPtr(current_target))) |list| {
try self.dispatchPhase(list, current_target, event, false); try self.dispatchPhase(list, current_target, event, was_handled, false);
if (event._stop_propagation) { if (event._stop_propagation) {
break; break;
} }
@@ -211,7 +225,7 @@ fn dispatchNode(self: *EventManager, target: *Node, event: *Event) !void {
event._event_phase = .none; event._event_phase = .none;
} }
fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, comptime capture_only: ?bool) !void { fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool, comptime capture_only: ?bool) !void {
const page = self.page; const page = self.page;
const typ = event._type_string; const typ = event._type_string;
@@ -240,6 +254,7 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
} }
} }
was_handled.* = true;
event._current_target = current_target; event._current_target = current_target;
switch (listener.function) { switch (listener.function) {
@@ -261,8 +276,8 @@ fn dispatchPhase(self: *EventManager, list: *std.DoublyLinkedList, current_targe
} }
// Non-Node dispatching (XHR, Window without propagation) // Non-Node dispatching (XHR, Window without propagation)
fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event) !void { fn dispatchAll(self: *EventManager, list: *std.DoublyLinkedList, current_target: *EventTarget, event: *Event, was_handled: *bool) !void {
return self.dispatchPhase(list, current_target, event, null); return self.dispatchPhase(list, current_target, event, was_handled, null);
} }
fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void { fn removeListener(self: *EventManager, list: *std.DoublyLinkedList, listener: *Listener) void {

View File

@@ -242,21 +242,13 @@ fn registerBackgroundTasks(self: *Page) !void {
const Browser = @import("Browser.zig"); const Browser = @import("Browser.zig");
try self.scheduler.add(self._session.browser, struct {
fn runMicrotasks(ctx: *anyopaque) !?u32 {
const b: *Browser = @ptrCast(@alignCast(ctx));
b.runMicrotasks();
return 5;
}
}.runMicrotasks, 5, .{ .name = "page.microtasks" });
try self.scheduler.add(self._session.browser, struct { try self.scheduler.add(self._session.browser, struct {
fn runMessageLoop(ctx: *anyopaque) !?u32 { fn runMessageLoop(ctx: *anyopaque) !?u32 {
const b: *Browser = @ptrCast(@alignCast(ctx)); const b: *Browser = @ptrCast(@alignCast(ctx));
b.runMessageLoop(); b.runMessageLoop();
return 100; return 250;
} }
}.runMessageLoop, 5, .{ .name = "page.messageLoop" }); }.runMessageLoop, 250, .{ .name = "page.messageLoop" });
} }
pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void { pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !void {
@@ -705,10 +697,10 @@ fn _wait(self: *Page, wait_ms: u32) !Session.WaitResult {
} }
pub fn tick(self: *Page) void { pub fn tick(self: *Page) void {
self._session.browser.runMicrotasks();
_ = self.scheduler.run() catch |err| { _ = self.scheduler.run() catch |err| {
log.err(.page, "tick", .{ .err = err }); log.err(.page, "tick", .{ .err = err });
}; };
self.js.runMicrotasks();
} }
pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void { pub fn scriptAddedCallback(self: *Page, script: *HtmlScript) !void {

View File

@@ -1162,6 +1162,10 @@ pub fn resolvePromise(self: *Context, value: anytype) !js.Promise {
return resolver.getPromise(); return resolver.getPromise();
} }
pub fn runMicrotasks(self: *Context) void {
self.isolate.performMicrotasksCheckpoint();
}
// creates a PersistentPromiseResolver, taking in a lifetime parameter. // creates a PersistentPromiseResolver, taking in a lifetime parameter.
// If the lifetime is page, the page will clean up the PersistentPromiseResolver. // If the lifetime is page, the page will clean up the PersistentPromiseResolver.
// If the lifetime is self, you will be expected to deinitalize the PersistentPromiseResolver. // If the lifetime is self, you will be expected to deinitalize the PersistentPromiseResolver.
@@ -1444,6 +1448,7 @@ fn dynamicModuleSourceCallback(ctx: *anyopaque, fetch_result_: anyerror!ScriptMa
} }
fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, module_entry: ModuleEntry) void { fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, module_entry: ModuleEntry) void {
defer self.runMicrotasks();
const ctx = self.v8_context; const ctx = self.v8_context;
const isolate = self.isolate; const isolate = self.isolate;
const external = v8.External.init(self.isolate, @ptrCast(state)); const external = v8.External.init(self.isolate, @ptrCast(state));
@@ -1479,6 +1484,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
return; return;
} }
defer caller.context.runMicrotasks();
const namespace = s.module.?.getModuleNamespace(); const namespace = s.module.?.getModuleNamespace();
_ = s.resolver.castToPromiseResolver().resolve(caller.context.v8_context, namespace); _ = s.resolver.castToPromiseResolver().resolve(caller.context.v8_context, namespace);
} }
@@ -1494,6 +1500,7 @@ fn resolveDynamicModule(self: *Context, state: *DynamicModuleResolveState, modul
if (s.context_id != caller.context.id) { if (s.context_id != caller.context.id) {
return; return;
} }
defer caller.context.runMicrotasks();
_ = s.resolver.castToPromiseResolver().reject(caller.context.v8_context, info.getData()); _ = s.resolver.castToPromiseResolver().reject(caller.context.v8_context, info.getData());
} }
}.callback, external); }.callback, external);

View File

@@ -107,6 +107,7 @@ pub const PersistentPromiseResolver = struct {
pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void { pub fn resolve(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context; const context = self.context;
const js_value = try context.zigValueToJs(value, .{}); const js_value = try context.zigValueToJs(value, .{});
defer context.runMicrotasks();
// resolver.resolve will return null if the promise isn't pending // resolver.resolve will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return; const ok = self.resolver.castToPromiseResolver().resolve(context.v8_context, js_value) orelse return;
@@ -118,6 +119,7 @@ pub const PersistentPromiseResolver = struct {
pub fn reject(self: PersistentPromiseResolver, value: anytype) !void { pub fn reject(self: PersistentPromiseResolver, value: anytype) !void {
const context = self.context; const context = self.context;
const js_value = try context.zigValueToJs(value, .{}); const js_value = try context.zigValueToJs(value, .{});
defer context.runMicrotasks();
// resolver.reject will return null if the promise isn't pending // resolver.reject will return null if the promise isn't pending
const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return; const ok = self.resolver.castToPromiseResolver().reject(context.v8_context, js_value) orelse return;

View File

@@ -321,14 +321,15 @@ const ScheduleCallback = struct {
fn run(ctx: *anyopaque) !?u32 { fn run(ctx: *anyopaque) !?u32 {
const self: *ScheduleCallback = @ptrCast(@alignCast(ctx)); const self: *ScheduleCallback = @ptrCast(@alignCast(ctx));
const page = self.page;
if (self.removed) { if (self.removed) {
_ = self.page.window._timers.remove(self.timer_id); _ = page.window._timers.remove(self.timer_id);
self.deinit(); self.deinit();
return null; return null;
} }
if (self.animation_frame) { if (self.animation_frame) {
self.cb.call(void, .{self.page.window._performance.now()}) catch |err| { self.cb.call(void, .{page.window._performance.now()}) catch |err| {
// a non-JS error // a non-JS error
log.warn(.js, "window.RAF", .{ .name = self.name, .err = err }); log.warn(.js, "window.RAF", .{ .name = self.name, .err = err });
}; };
@@ -342,9 +343,10 @@ const ScheduleCallback = struct {
if (self.repeat_ms) |ms| { if (self.repeat_ms) |ms| {
return ms; return ms;
} }
defer self.deinit();
_ = self.page.window._timers.remove(self.timer_id); _ = page.window._timers.remove(self.timer_id);
self.deinit(); page.js.runMicrotasks();
return null; return null;
} }
}; };