add error callbacks for workers

This commit is contained in:
Karl Seguin
2026-04-07 19:50:30 +08:00
parent 5e7f891546
commit b3a8a7454e
4 changed files with 155 additions and 31 deletions

View File

@@ -335,7 +335,7 @@ pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, page);
}, page._session);
// Invoke window.onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)

View File

@@ -29,6 +29,7 @@ const HttpClient = @import("../HttpClient.zig");
const EventTarget = @import("EventTarget.zig");
const MessageEvent = @import("event/MessageEvent.zig");
const ErrorEvent = @import("event/ErrorEvent.zig");
const WorkerGlobalScope = @import("WorkerGlobalScope.zig");
const Execution = js.Execution;
@@ -161,7 +162,9 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
_ = ls.local.eval(script, url) catch |err| {
log.err(.browser, "worker script error", .{ .url = url, .err = err });
// TODO: Fire error event on Worker
self.fireErrorEvent(@errorName(err), null) catch |e| {
log.warn(.browser, "worker error event failed", .{ .err = e });
};
return;
};
@@ -177,7 +180,35 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
.err = err,
});
// TODO: Fire error event on Worker
self.fireErrorEvent(@errorName(err), null) catch |e| {
log.warn(.browser, "worker error event failed", .{ .err = e });
};
}
// Fire an error event on the Worker object (parent context)
fn fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Temp) !void {
const page = self._page;
const session = page._session;
const target = self.asEventTarget();
const on_error = self._on_error;
// Check if there are any listeners
if (!page._event_manager.hasDirectListeners(target, "error", on_error)) {
if (error_value) |ev| ev.release();
return;
}
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = error_value,
.message = message,
.filename = self._url,
.bubbles = false,
.cancelable = true,
}, session);
try page._event_manager.dispatchDirect(target, error_event.asEvent(), on_error, .{
.context = "Worker.onerror",
});
}
pub fn terminate(self: *Worker) void {
@@ -195,7 +226,7 @@ pub fn postMessage(self: *Worker, data: js.Value) !void {
try self._worker_scope.receiveMessage(data);
}
// Called internally by WorkerGlobalScope when it wants to post a message to use
// Called internally by WorkerGlobalScope when it wants to post a message to us
pub fn receiveMessage(self: *Worker, data: js.Value) !void {
const page = self._page;
const cloned_data = blk: {
@@ -204,10 +235,9 @@ pub fn receiveMessage(self: *Worker, data: js.Value) !void {
defer ls.deinit();
// clones from where it currently is (the Worker context) to our Page's context
const cloned = try data.structuredCloneTo(&ls.local);
break :blk try cloned.temp();
const cloned = data.structuredCloneTo(&ls.local) catch |err| break :blk err;
break :blk cloned.temp();
};
errdefer cloned_data.release();
const message_arena = try page.getArena(.{ .debug = "Worker.receiveMessage" });
errdefer page.releaseArena(message_arena);
@@ -264,13 +294,15 @@ fn getFunctionFromSetter(setter_: ?FunctionSetter) ?js.Function.Global {
}
const ReceiveMessageCallback = struct {
data: js.Value.Temp,
data: anyerror!js.Value.Temp,
arena: Allocator,
worker: *Worker,
fn cancelled(ctx: *anyopaque) void {
const self: *ReceiveMessageCallback = @ptrCast(@alignCast(ctx));
self.data.release();
if (self.data) |d| {
d.release();
} else |_| {}
self.deinit();
}
@@ -285,16 +317,32 @@ const ReceiveMessageCallback = struct {
const worker = self.worker;
const page = worker._page;
const target = worker.asEventTarget();
// If data is null, structured clone failed - fire messageerror
const data = self.data catch |err| {
const on_messageerror = worker._on_messageerror;
if (!page._event_manager.hasDirectListeners(target, "messageerror", on_messageerror)) {
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{
.data = .{ .string = @errorName(err) },
.bubbles = false,
.cancelable = false,
}, page._session)).asEvent();
try page._event_manager.dispatchDirect(target, event, on_messageerror, .{ .context = "Worker.messageerror" });
return null;
};
const on_message = worker._on_message;
// Check if there are any listeners before creating the event
if (!page._event_manager.hasDirectListeners(target, "message", on_message)) {
self.data.release();
data.release();
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = .{ .value = self.data },
.data = .{ .value = data },
.bubbles = false,
.cancelable = false,
}, page._session)).asEvent();

View File

@@ -35,8 +35,10 @@ const Console = @import("Console.zig");
const EventTarget = @import("EventTarget.zig");
const Performance = @import("Performance.zig");
const MessageEvent = @import("event/MessageEvent.zig");
const ErrorEvent = @import("event/ErrorEvent.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const builtin = @import("builtin");
const IS_DEBUG = builtin.mode == .Debug;
const Allocator = std.mem.Allocator;
@@ -69,6 +71,7 @@ _on_error: ?JS.Function.Global = null,
_on_rejection_handled: ?JS.Function.Global = null,
_on_unhandled_rejection: ?JS.Function.Global = null,
_on_message: ?JS.Function.Global = null,
_on_messageerror: ?JS.Function.Global = null,
pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope {
const arena = worker._arena;
@@ -180,6 +183,14 @@ pub fn setOnMessage(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
self._on_message = getFunctionFromSetter(setter);
}
pub fn getOnMessageError(self: *const WorkerGlobalScope) ?JS.Function.Global {
return self._on_messageerror;
}
pub fn setOnMessageError(self: *WorkerGlobalScope, setter: ?FunctionSetter) void {
self._on_messageerror = getFunctionFromSetter(setter);
}
// Posts a message from the worker back to the page.
// The message is cloned via structured clone and dispatched on the Worker object.
pub fn postMessage(self: *WorkerGlobalScope, data: JS.Value) !void {
@@ -188,17 +199,16 @@ pub fn postMessage(self: *WorkerGlobalScope, data: JS.Value) !void {
// Called internally by Worker when it wants to post a message to us
pub fn receiveMessage(self: *WorkerGlobalScope, data: JS.Value) !void {
const cloned_data = blk: {
const cloned_data: ?JS.Value.Temp = blk: {
// Enter our context to clone the message
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
// clones from where it currently is (the Worker's Page context) to our Context
const cloned = try data.structuredCloneTo(&ls.local);
break :blk try cloned.temp();
const cloned = data.structuredCloneTo(&ls.local) catch break :blk null;
break :blk cloned.temp() catch break :blk null;
};
errdefer cloned_data.release();
const session = self._session;
@@ -259,6 +269,56 @@ pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rej
}
}
pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void {
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = try err.temp(),
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
}, self._session);
// Invoke onerror callback if set (per WHATWG spec, this is called
// with 5 arguments: message, source, lineno, colno, error)
// If it returns true, the event is cancelled.
var prevent_default = false;
if (self._on_error) |on_error| {
var ls: JS.Local.Scope = undefined;
self.js.localScope(&ls);
defer ls.deinit();
const local_func = ls.toLocal(on_error);
const result = local_func.call(JS.Value, .{
error_event._message,
error_event._filename,
error_event._line_number,
error_event._column_number,
err,
}) catch null;
// Per spec: returning true from onerror cancels the event
if (result) |r| {
prevent_default = r.isTrue();
}
}
const event = error_event.asEvent();
event._prevent_default = prevent_default;
// Pass null as handler: onerror was already called above with 5 args.
// We still dispatch so that addEventListener('error', ...) listeners fire.
try self.dispatch(self.asEventTarget(), event, null);
if (comptime builtin.is_test == false) {
if (!event._prevent_default) {
log.warn(.js, "worker.reportError", .{
.message = error_event._message,
.filename = error_event._filename,
.line_number = error_event._line_number,
.column_number = error_event._column_number,
});
}
}
}
// TODO: importScripts - needs script loading infrastructure
// TODO: location - needs WorkerLocation
// TODO: navigator - needs WorkerNavigator
@@ -278,13 +338,13 @@ fn getFunctionFromSetter(setter_: ?FunctionSetter) ?JS.Function.Global {
}
const ReceiveMessageCallback = struct {
data: JS.Value.Temp,
data: ?JS.Value.Temp,
arena: Allocator,
worker_scope: *WorkerGlobalScope,
fn cancelled(ctx: *anyopaque) void {
const self: *ReceiveMessageCallback = @ptrCast(@alignCast(ctx));
self.data.release();
if (self.data) |d| d.release();
self.deinit();
}
@@ -298,16 +358,31 @@ const ReceiveMessageCallback = struct {
const worker_scope = self.worker_scope;
const target = worker_scope.asEventTarget();
// If data is null, structured clone failed - fire messageerror
if (self.data == null) {
const on_messageerror = worker_scope._on_messageerror;
if (!worker_scope._event_manager.hasDirectListeners(target, "messageerror", on_messageerror)) {
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{
.bubbles = false,
.cancelable = false,
}, worker_scope._session)).asEvent();
try worker_scope.dispatch(target, event, on_messageerror);
return null;
}
const on_message = worker_scope._on_message;
// Check if there are any listeners before creating the event
if (!worker_scope._event_manager.hasDirectListeners(target, "message", on_message)) {
self.data.release();
self.data.?.release();
return null;
}
const event = (try MessageEvent.initTrusted(comptime .wrap("message"), .{
.data = .{ .value = self.data },
.data = .{ .value = self.data.? },
.bubbles = false,
.cancelable = false,
}, worker_scope._session)).asEvent();
@@ -338,8 +413,10 @@ pub const JsApi = struct {
pub const atob = bridge.function(WorkerGlobalScope.atob, .{ .dom_exception = true });
pub const structuredClone = bridge.function(WorkerGlobalScope.structuredClone, .{});
pub const postMessage = bridge.function(WorkerGlobalScope.postMessage, .{});
pub const reportError = bridge.function(WorkerGlobalScope.reportError, .{});
pub const onmessage = bridge.accessor(WorkerGlobalScope.getOnMessage, WorkerGlobalScope.setOnMessage, .{});
pub const onmessageerror = bridge.accessor(WorkerGlobalScope.getOnMessageError, WorkerGlobalScope.setOnMessageError, .{});
// Return false since workers don't have secure-context-only APIs
pub const isSecureContext = bridge.property(false, .{ .template = false });

View File

@@ -20,7 +20,6 @@ const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig");
const Event = @import("../Event.zig");
@@ -46,23 +45,23 @@ pub const ErrorEventOptions = struct {
const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent {
const arena = try page.getArena(.small, "ErrorEvent");
errdefer page.releaseArena(arena);
pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*ErrorEvent {
const arena = try session.getArena(.small, "ErrorEvent");
errdefer session.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
return initWithTrusted(arena, type_string, opts_, false, page);
return initWithTrusted(arena, type_string, opts_, false, session);
}
pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*ErrorEvent {
const arena = try page.getArena(.small, "ErrorEvent.trusted");
errdefer page.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, page);
pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*ErrorEvent {
const arena = try session.getArena(.small, "ErrorEvent.trusted");
errdefer session.releaseArena(arena);
return initWithTrusted(arena, typ, opts_, true, session);
}
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent {
fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, session: *Session) !*ErrorEvent {
const opts = opts_ orelse Options{};
const event = try page._factory.event(
const event = try session.factory.event(
arena,
typ,
ErrorEvent{