Merge pull request #1516 from lightpanda-io/unhandle_rejection_callback

Adds PromiseRejectionCallback
This commit is contained in:
Karl Seguin
2026-02-11 07:14:26 +08:00
committed by GitHub
18 changed files with 323 additions and 56 deletions

View File

@@ -105,6 +105,7 @@ global_promise_resolvers: std.ArrayList(v8.Global) = .empty,
// Temp variants stored in HashMaps for O(1) early cleanup.
// Key is global.data_ptr.
global_values_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
global_promises_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
global_functions_temp: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Our module cache: normalized module specifier => module.
@@ -233,6 +234,13 @@ pub fn deinit(self: *Context) void {
}
}
{
var it = self.global_promises_temp.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
{
var it = self.global_functions_temp.valueIterator();
while (it.next()) |global| {
@@ -309,6 +317,7 @@ pub fn release(self: *Context, item: anytype) void {
var map = switch (@TypeOf(item)) {
js.Value.Temp => &self.global_values_temp,
js.Promise.Temp => &self.global_promises_temp,
js.Function.Temp => &self.global_functions_temp,
else => |T| @compileError("Context.release cannot be called with a " ++ @typeName(T)),
};

View File

@@ -383,17 +383,13 @@ fn promiseRejectCallback(message_handle: v8.PromiseRejectMessage) callconv(.c) v
.call_arena = ctx.call_arena,
};
const value =
if (v8.v8__PromiseRejectMessage__GetValue(&message_handle)) |v8_value|
js.Value.toStringSlice(.{ .local = &local, .handle = v8_value }) catch |err| @errorName(err)
else
"no value";
log.debug(.js, "unhandled rejection", .{
.value = value,
.stack = local.stackTrace() catch |err| @errorName(err) orelse "???",
.note = "This should be updated to call window.unhandledrejection",
});
const page = ctx.page;
page.window.unhandledPromiseRejection(.{
.local = &local,
.handle = &message_handle,
}, page) catch |err| {
log.warn(.browser, "unhandled rejection handler", .{ .err = err });
};
}
fn fatalCallback(c_location: [*c]const u8, c_message: [*c]const u8) callconv(.c) void {

View File

@@ -323,6 +323,7 @@ pub fn zigValueToJs(self: *const Local, value: anytype, comptime opts: CallOpts)
js.Value.Temp,
js.Object.Global,
js.Promise.Global,
js.Promise.Temp,
js.PromiseResolver.Global,
js.Module.Global => return .{ .local = self, .handle = @ptrCast(value.local(self).handle) },
else => {}
@@ -619,15 +620,19 @@ fn jsValueToStruct(self: *const Local, comptime T: type, js_val: js.Value) !?T {
return try obj.persist();
},
js.Promise.Global => {
js.Promise.Global, js.Promise.Temp => {
if (!js_val.isPromise()) {
return null;
}
const promise = js.Promise{
.ctx = self,
const js_promise = js.Promise{
.local = self,
.handle = @ptrCast(js_val.handle),
};
return try promise.persist();
return switch (T) {
js.Promise.Temp => try js_promise.temp(),
js.Promise.Global => try js_promise.persist(),
else => unreachable,
};
},
string.String => {
const js_str = js_val.isString() orelse return null;

View File

@@ -47,25 +47,49 @@ pub fn thenAndCatch(self: Promise, on_fulfilled: js.Function, on_rejected: js.Fu
}
return error.PromiseChainFailed;
}
pub fn persist(self: Promise) !Global {
return self._persist(true);
}
pub fn temp(self: Promise) !Temp {
return self._persist(false);
}
fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Global else Temp) {
var ctx = self.local.ctx;
var global: v8.Global = undefined;
v8.v8__Global__New(ctx.isolate.handle, self.handle, &global);
try ctx.global_promises.append(ctx.arena, global);
if (comptime is_global) {
try ctx.global_promises.append(ctx.arena, global);
} else {
try ctx.global_promises_temp.put(ctx.arena, global.data_ptr, global);
}
return .{ .handle = global };
}
pub const Global = struct {
handle: v8.Global,
pub const Temp = G(0);
pub const Global = G(1);
pub fn deinit(self: *Global) void {
v8.v8__Global__Reset(&self.handle);
}
fn G(comptime discriminator: u8) type {
return struct {
handle: v8.Global,
pub fn local(self: *const Global, l: *const js.Local) Promise {
return .{
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
};
// makes the types different (G(0) != G(1)), without taking up space
comptime _: u8 = discriminator,
const Self = @This();
pub fn deinit(self: *Self) void {
v8.v8__Global__Reset(&self.handle);
}
pub fn local(self: *const Self, l: *const js.Local) Promise {
return .{
.local = l,
.handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)),
};
}
};
}

View File

@@ -0,0 +1,41 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const js = @import("js.zig");
const v8 = js.v8;
const PromiseRejection = @This();
local: *const js.Local,
handle: *const v8.PromiseRejectMessage,
pub fn promise(self: PromiseRejection) js.Promise {
return .{
.local = self.local,
.handle = v8.v8__PromiseRejectMessage__GetPromise(self.handle).?,
};
}
pub fn reason(self: PromiseRejection) ?js.Value {
const value_handle = v8.v8__PromiseRejectMessage__GetValue(self.handle) orelse return null;
return .{
.local = self.local,
.handle = value_handle,
};
}

View File

@@ -879,6 +879,7 @@ pub const JsApis = flattenTypes(&.{
@import("../webapi/event/MouseEvent.zig"),
@import("../webapi/event/PointerEvent.zig"),
@import("../webapi/event/KeyboardEvent.zig"),
@import("../webapi/event/PromiseRejectionEvent.zig"),
@import("../webapi/MessageChannel.zig"),
@import("../webapi/MessagePort.zig"),
@import("../webapi/media/MediaError.zig"),

View File

@@ -44,6 +44,7 @@ pub const BigInt = @import("BigInt.zig");
pub const Number = @import("Number.zig");
pub const Integer = @import("Integer.zig");
pub const PromiseResolver = @import("PromiseResolver.zig");
pub const PromiseRejection = @import("PromiseRejection.zig");
const Allocator = std.mem.Allocator;

View File

@@ -0,0 +1,22 @@
<!DOCTYPE html>
<script src="../testing.js"></script>
<script id=project_rejection>
{
let e1 = new PromiseRejectionEvent("rejectionhandled");
testing.expectEqual(true, e1 instanceof PromiseRejectionEvent);
testing.expectEqual(true, e1 instanceof Event);
testing.expectEqual("rejectionhandled", e1.type);
testing.expectEqual(null, e1.reason);
testing.expectEqual(null, e1.promise);
let e2 = new PromiseRejectionEvent("rejectionhandled", {reason: ['tea']});
testing.expectEqual(true, e2 instanceof PromiseRejectionEvent);
testing.expectEqual(true, e2 instanceof Event);
testing.expectEqual("rejectionhandled", e2.type);
testing.expectEqual(['tea'], e2.reason);
testing.expectEqual(null, e2.promise);
}
</script>

View File

@@ -114,3 +114,30 @@
testing.expectEqual(24, screen.pixelDepth);
testing.expectEqual(screen, window.screen);
</script>
<script id=unhandled_rejection>
{
let unhandledCalled = 0;
window.onunhandledrejection = function(e) {
testing.expectEqual(true, e instanceof PromiseRejectionEvent);
testing.expectEqual({x: 'Fail'}, e.reason);
testing.expectEqual('unhandledrejection', e.type);
testing.expectEqual(window, e.target);
testing.expectEqual(window, e.srcElement);
testing.expectEqual(window, e.currentTarget);
unhandledCalled += 1;
}
window.addEventListener('unhandledrejection', function(e) {
testing.expectEqual(true, e instanceof PromiseRejectionEvent);
testing.expectEqual({x: 'Fail'}, e.reason);
testing.expectEqual('unhandledrejection', e.type);
testing.expectEqual(window, e.target);
testing.expectEqual(window, e.srcElement);
testing.expectEqual(window, e.currentTarget);
unhandledCalled += 1;
});
Promise.reject({x: 'Fail'});
testing.eventually(() => testing.expectEqual(2, unhandledCalled));
}
</script>

View File

@@ -71,6 +71,7 @@ pub const Type = union(enum) {
page_transition_event: *@import("event/PageTransitionEvent.zig"),
pop_state_event: *@import("event/PopStateEvent.zig"),
ui_event: *@import("event/UIEvent.zig"),
promise_rejection_event: *@import("event/PromiseRejectionEvent.zig"),
};
pub const Options = struct {
@@ -150,6 +151,7 @@ pub fn is(self: *Event, comptime T: type) ?*T {
.navigation_current_entry_change_event => |e| return if (T == @import("event/NavigationCurrentEntryChangeEvent.zig")) e else null,
.page_transition_event => |e| return if (T == @import("event/PageTransitionEvent.zig")) e else null,
.pop_state_event => |e| return if (T == @import("event/PopStateEvent.zig")) e else null,
.promise_rejection_event => |e| return if (T == @import("event/PromiseRejectionEvent.zig")) e else null,
.ui_event => |e| {
if (T == @import("event/UIEvent.zig")) {
return e;

View File

@@ -48,7 +48,7 @@ pub fn entangle(port1: *MessagePort, port2: *MessagePort) void {
port2._entangled_port = port1;
}
pub fn postMessage(self: *MessagePort, message: js.Value.Global, page: *Page) !void {
pub fn postMessage(self: *MessagePort, message: js.Value.Temp, page: *Page) !void {
if (self._closed) {
return;
}
@@ -106,7 +106,7 @@ pub fn setOnMessageError(self: *MessagePort, cb: ?js.Function.Global) !void {
const PostMessageCallback = struct {
port: *MessagePort,
message: js.Value.Global,
message: js.Value.Temp,
page: *Page,
fn deinit(self: *PostMessageCallback) void {

View File

@@ -44,6 +44,8 @@ const CSSStyleProperties = @import("css/CSSStyleProperties.zig");
const CustomElementRegistry = @import("CustomElementRegistry.zig");
const Selection = @import("Selection.zig");
const IS_DEBUG = builtin.mode == .Debug;
const Allocator = std.mem.Allocator;
const Window = @This();
@@ -278,7 +280,7 @@ pub fn cancelIdleCallback(self: *Window, id: u32) void {
pub fn reportError(self: *Window, err: js.Value, page: *Page) !void {
const error_event = try ErrorEvent.initTrusted(comptime .wrap("error"), .{
.@"error" = try err.persist(),
.@"error" = try err.temp(),
.message = err.toStringSlice() catch "Unknown error",
.bubbles = false,
.cancelable = true,
@@ -342,7 +344,7 @@ pub fn getComputedStyle(_: *const Window, element: *Element, pseudo_element: ?[]
return CSSStyleProperties.init(element, true, page);
}
pub fn postMessage(self: *Window, message: js.Value.Global, target_origin: ?[]const u8, page: *Page) !void {
pub fn postMessage(self: *Window, message: js.Value.Temp, target_origin: ?[]const u8, page: *Page) !void {
// For now, we ignore targetOrigin checking and just dispatch the message
// In a full implementation, we would validate the origin
_ = target_origin;
@@ -487,6 +489,28 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, page: *Page) !void {
);
}
pub fn unhandledPromiseRejection(self: *Window, rejection: js.PromiseRejection, page: *Page) !void {
if (comptime IS_DEBUG) {
log.debug(.js, "unhandled rejection", .{
.value = rejection.reason(),
.stack = rejection.local.stackTrace() catch |err| @errorName(err) orelse "???",
});
}
var event = (try @import("event/PromiseRejectionEvent.zig").init("unhandledrejection", .{
.reason = if (rejection.reason()) |r| try r.temp() else null,
.promise = try rejection.promise().temp(),
}, page)).asEvent();
defer if (!event._v8_handoff) event.deinit(false);
try page._event_manager.dispatchWithFunction(
self.asEventTarget(),
event,
rejection.local.toLocal(self._on_unhandled_rejection),
.{ .inject_target = true, .context = "window.unhandledrejection" },
);
}
const ScheduleOpts = struct {
repeat: bool,
params: []js.Value.Temp,
@@ -626,7 +650,7 @@ const PostMessageCallback = struct {
page: *Page,
arena: Allocator,
origin: []const u8,
message: js.Value.Global,
message: js.Value.Temp,
fn deinit(self: *PostMessageCallback) void {
self.page.releaseArena(self.arena);

View File

@@ -16,7 +16,7 @@
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../..//string.zig").String;
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../..//string.zig").String;
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
@@ -27,11 +27,11 @@ const Allocator = std.mem.Allocator;
const CustomEvent = @This();
_proto: *Event,
_detail: ?js.Value.Global = null,
_detail: ?js.Value.Temp = null,
_arena: Allocator,
const CustomEventOptions = struct {
detail: ?js.Value.Global = null,
detail: ?js.Value.Temp = null,
};
const Options = Event.inheritOptions(CustomEvent, CustomEventOptions);
@@ -61,7 +61,7 @@ pub fn initCustomEvent(
event_string: []const u8,
bubbles: ?bool,
cancelable: ?bool,
detail_: ?js.Value.Global,
detail_: ?js.Value.Temp,
) !void {
// This function can only be called after the constructor has called.
// So we assume proto is initialized already by constructor.
@@ -73,14 +73,18 @@ pub fn initCustomEvent(
}
pub fn deinit(self: *CustomEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
const proto = self._proto;
if (self._detail) |d| {
proto._page.js.release(d);
}
proto.deinit(shutdown);
}
pub fn asEvent(self: *CustomEvent) *Event {
return self._proto;
}
pub fn getDetail(self: *const CustomEvent) ?js.Value.Global {
pub fn getDetail(self: *const CustomEvent) ?js.Value.Temp {
return self._detail;
}

View File

@@ -32,7 +32,7 @@ _message: []const u8 = "",
_filename: []const u8 = "",
_line_number: u32 = 0,
_column_number: u32 = 0,
_error: ?js.Value.Global = null,
_error: ?js.Value.Temp = null,
_arena: Allocator,
pub const ErrorEventOptions = struct {
@@ -40,7 +40,7 @@ pub const ErrorEventOptions = struct {
filename: ?[]const u8 = null,
lineno: u32 = 0,
colno: u32 = 0,
@"error": ?js.Value.Global = null,
@"error": ?js.Value.Temp = null,
};
const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions);
@@ -80,7 +80,11 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
}
pub fn deinit(self: *ErrorEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
const proto = self._proto;
if (self._error) |e| {
proto._page.js.release(e);
}
proto.deinit(shutdown);
}
pub fn asEvent(self: *ErrorEvent) *Event {
@@ -103,7 +107,7 @@ pub fn getColumnNumber(self: *const ErrorEvent) u32 {
return self._column_number;
}
pub fn getError(self: *const ErrorEvent) ?js.Value.Global {
pub fn getError(self: *const ErrorEvent) ?js.Value.Temp {
return self._error;
}

View File

@@ -29,12 +29,12 @@ const Allocator = std.mem.Allocator;
const MessageEvent = @This();
_proto: *Event,
_data: ?js.Value.Global = null,
_data: ?js.Value.Temp = null,
_origin: []const u8 = "",
_source: ?*Window = null,
const MessageEventOptions = struct {
data: ?js.Value.Global = null,
data: ?js.Value.Temp = null,
origin: ?[]const u8 = null,
source: ?*Window = null,
};
@@ -73,14 +73,18 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
}
pub fn deinit(self: *MessageEvent, shutdown: bool) void {
self._proto.deinit(shutdown);
const proto = self._proto;
if (self._data) |d| {
proto._page.js.release(d);
}
proto.deinit(shutdown);
}
pub fn asEvent(self: *MessageEvent) *Event {
return self._proto;
}
pub fn getData(self: *const MessageEvent) ?js.Value.Global {
pub fn getData(self: *const MessageEvent) ?js.Value.Temp {
return self._data;
}

View File

@@ -0,0 +1,102 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const String = @import("../../../string.zig").String;
const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig");
const Event = @import("../Event.zig");
const Allocator = std.mem.Allocator;
const PromiseRejectionEvent = @This();
_proto: *Event,
_reason: ?js.Value.Temp = null,
_promise: ?js.Promise.Temp = null,
const PromiseRejectionEventOptions = struct {
reason: ?js.Value.Temp = null,
promise: ?js.Promise.Temp = null,
};
const Options = Event.inheritOptions(PromiseRejectionEvent, PromiseRejectionEventOptions);
pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEvent {
const arena = try page.getArena(.{ .debug = "PromiseRejectionEvent" });
errdefer page.releaseArena(arena);
const type_string = try String.init(arena, typ, .{});
const opts = opts_ orelse Options{};
const event = try page._factory.event(
arena,
type_string,
PromiseRejectionEvent{
._proto = undefined,
._reason = opts.reason,
._promise = opts.promise,
},
);
Event.populatePrototypes(event, opts, false);
return event;
}
pub fn deinit(self: *PromiseRejectionEvent, shutdown: bool) void {
const proto = self._proto;
const js_ctx = proto._page.js;
if (self._reason) |r| {
js_ctx.release(r);
}
if (self._promise) |p| {
js_ctx.release(p);
}
proto.deinit(shutdown);
}
pub fn asEvent(self: *PromiseRejectionEvent) *Event {
return self._proto;
}
pub fn getReason(self: *const PromiseRejectionEvent) ?js.Value.Temp {
return self._reason;
}
pub fn getPromise(self: *const PromiseRejectionEvent) ?js.Promise.Temp {
return self._promise;
}
pub const JsApi = struct {
pub const bridge = js.Bridge(PromiseRejectionEvent);
pub const Meta = struct {
pub const name = "PromiseRejectionEvent";
pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PromiseRejectionEvent.deinit);
};
pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{});
pub const reason = bridge.accessor(PromiseRejectionEvent.getReason, null, .{});
pub const promise = bridge.accessor(PromiseRejectionEvent.getPromise, null, .{});
};
const testing = @import("../../../testing.zig");
test "WebApi: PromiseRejectionEvent" {
try testing.htmlRunner("event/promise_rejection.html", .{});
}

View File

@@ -103,32 +103,33 @@ pub fn deinit(self: *XMLHttpRequest, shutdown: bool) void {
}
const page = self._page;
const js_ctx = page.js;
if (self._on_ready_state_change) |func| {
page.js.release(func);
js_ctx.release(func);
}
{
const proto = self._proto;
if (proto._on_abort) |func| {
page.js.release(func);
js_ctx.release(func);
}
if (proto._on_error) |func| {
page.js.release(func);
js_ctx.release(func);
}
if (proto._on_load) |func| {
page.js.release(func);
js_ctx.release(func);
}
if (proto._on_load_end) |func| {
page.js.release(func);
js_ctx.release(func);
}
if (proto._on_load_start) |func| {
page.js.release(func);
js_ctx.release(func);
}
if (proto._on_progress) |func| {
page.js.release(func);
js_ctx.release(func);
}
if (proto._on_timeout) |func| {
page.js.release(func);
js_ctx.release(func);
}
}