From ded203b1c167312c1aae0af004291c4d5f2766c8 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Tue, 10 Feb 2026 16:50:11 +0800 Subject: [PATCH] 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. --- src/browser/js/Context.zig | 9 ++ src/browser/js/Env.zig | 18 ++-- src/browser/js/Local.zig | 13 ++- src/browser/js/Promise.zig | 50 ++++++--- src/browser/js/PromiseRejection.zig | 41 +++++++ src/browser/js/bridge.zig | 1 + src/browser/js/js.zig | 1 + .../tests/event/promise_rejection.html | 22 ++++ src/browser/tests/window/window.html | 27 +++++ src/browser/webapi/Event.zig | 2 + src/browser/webapi/MessagePort.zig | 4 +- src/browser/webapi/Window.zig | 30 +++++- src/browser/webapi/event/CompositionEvent.zig | 2 +- src/browser/webapi/event/CustomEvent.zig | 16 +-- src/browser/webapi/event/ErrorEvent.zig | 12 ++- src/browser/webapi/event/MessageEvent.zig | 12 ++- .../webapi/event/PromiseRejectionEvent.zig | 102 ++++++++++++++++++ src/browser/webapi/net/XMLHttpRequest.zig | 17 +-- 18 files changed, 323 insertions(+), 56 deletions(-) create mode 100644 src/browser/js/PromiseRejection.zig create mode 100644 src/browser/tests/event/promise_rejection.html create mode 100644 src/browser/webapi/event/PromiseRejectionEvent.zig diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 7fc9073f..34d8d654 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -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)), }; diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 057c2c87..c73450a8 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -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 { diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 578617bd..524dfd54 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -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; diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index 44be00c7..afadbe82 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -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)), + }; + } + }; +} diff --git a/src/browser/js/PromiseRejection.zig b/src/browser/js/PromiseRejection.zig new file mode 100644 index 00000000..5e6c67d5 --- /dev/null +++ b/src/browser/js/PromiseRejection.zig @@ -0,0 +1,41 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . + +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, + }; +} diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index fedd4f4d..1b0748e2 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -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"), diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 336688ce..49842fa7 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -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; diff --git a/src/browser/tests/event/promise_rejection.html b/src/browser/tests/event/promise_rejection.html new file mode 100644 index 00000000..54804d4b --- /dev/null +++ b/src/browser/tests/event/promise_rejection.html @@ -0,0 +1,22 @@ + + + + diff --git a/src/browser/tests/window/window.html b/src/browser/tests/window/window.html index 46d32940..5d36e74d 100644 --- a/src/browser/tests/window/window.html +++ b/src/browser/tests/window/window.html @@ -114,3 +114,30 @@ testing.expectEqual(24, screen.pixelDepth); testing.expectEqual(screen, window.screen); + + diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 721a032d..ffb3edc5 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -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; diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig index 448df566..def38da0 100644 --- a/src/browser/webapi/MessagePort.zig +++ b/src/browser/webapi/MessagePort.zig @@ -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 { diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index a1939b8a..c84346c9 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -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); diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index d2543d73..0362cb83 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -16,7 +16,7 @@ // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . 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"); diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index 029827b0..579afb81 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -17,7 +17,7 @@ // along with this program. If not, see . 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; } diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index ce8360d1..4983fe90 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -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; } diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index 45d24b9e..d285e0e7 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -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; } diff --git a/src/browser/webapi/event/PromiseRejectionEvent.zig b/src/browser/webapi/event/PromiseRejectionEvent.zig new file mode 100644 index 00000000..db498a24 --- /dev/null +++ b/src/browser/webapi/event/PromiseRejectionEvent.zig @@ -0,0 +1,102 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// 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 . +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", .{}); +} diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 73e35309..cbe4464e 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -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); } }