diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 27a5c38a..9e716e91 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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) diff --git a/src/browser/webapi/Worker.zig b/src/browser/webapi/Worker.zig index 6c7a0d5a..a56f62d5 100644 --- a/src/browser/webapi/Worker.zig +++ b/src/browser/webapi/Worker.zig @@ -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(); diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index 06f66c01..2512baf5 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -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 }); diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index 4bb68573..56659d20 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -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{