Adds PromiseRectionCallback

Fires the window.onunhandledrejection. This API is a bit different than
everything else, because it's entered from the Isolate/Env. So there's a bit
more js -> webapi awareness baked into Env now to handle it.

Also touched up existing Events that have .Global data and changed it to .Temp
and cleaned it up in their deinit.
This commit is contained in:
Karl Seguin
2026-02-10 16:50:11 +08:00
parent 60d8f2323e
commit ded203b1c1
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

@@ -882,6 +882,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);
}
}