Merge pull request #1383 from lightpanda-io/xhr_finalizer

Add XHR finalizer and ArenaPool
This commit is contained in:
Karl Seguin
2026-01-26 07:34:23 +08:00
committed by GitHub
11 changed files with 311 additions and 90 deletions

View File

@@ -21,13 +21,14 @@ const std = @import("std");
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
const log = @import("log.zig"); const log = @import("log.zig");
const Http = @import("http/Http.zig");
const Snapshot = @import("browser/js/Snapshot.zig"); const Snapshot = @import("browser/js/Snapshot.zig");
const Platform = @import("browser/js/Platform.zig"); const Platform = @import("browser/js/Platform.zig");
const Notification = @import("Notification.zig");
const Telemetry = @import("telemetry/telemetry.zig").Telemetry; const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
pub const Http = @import("http/Http.zig");
pub const ArenaPool = @import("ArenaPool.zig");
pub const Notification = @import("Notification.zig");
// Container for global state / objects that various parts of the system // Container for global state / objects that various parts of the system
// might need. // might need.
const App = @This(); const App = @This();
@@ -38,6 +39,7 @@ platform: Platform,
snapshot: Snapshot, snapshot: Snapshot,
telemetry: Telemetry, telemetry: Telemetry,
allocator: Allocator, allocator: Allocator,
arena_pool: ArenaPool,
app_dir_path: ?[]const u8, app_dir_path: ?[]const u8,
notification: *Notification, notification: *Notification,
shutdown: bool = false, shutdown: bool = false,
@@ -96,6 +98,9 @@ pub fn init(allocator: Allocator, config: Config) !*App {
try app.telemetry.register(app.notification); try app.telemetry.register(app.notification);
app.arena_pool = ArenaPool.init(allocator);
errdefer app.arena_pool.deinit();
return app; return app;
} }
@@ -114,6 +119,7 @@ pub fn deinit(self: *App) void {
self.http.deinit(); self.http.deinit();
self.snapshot.deinit(); self.snapshot.deinit();
self.platform.deinit(); self.platform.deinit();
self.arena_pool.deinit();
allocator.destroy(self); allocator.destroy(self);
} }

84
src/ArenaPool.zig Normal file
View File

@@ -0,0 +1,84 @@
// 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 Allocator = std.mem.Allocator;
const ArenaAllocator = std.heap.ArenaAllocator;
const ArenaPool = @This();
allocator: Allocator,
retain_bytes: usize,
free_list_len: u16 = 0,
free_list: ?*Entry = null,
free_list_max: u16,
entry_pool: std.heap.MemoryPool(Entry),
const Entry = struct {
next: ?*Entry,
arena: ArenaAllocator,
};
pub fn init(allocator: Allocator) ArenaPool {
return .{
.allocator = allocator,
.free_list_max = 512, // TODO make configurable
.retain_bytes = 1024 * 16, // TODO make configurable
.entry_pool = std.heap.MemoryPool(Entry).init(allocator),
};
}
pub fn deinit(self: *ArenaPool) void {
var entry = self.free_list;
while (entry) |e| {
entry = e.next;
e.arena.deinit();
}
self.entry_pool.deinit();
}
pub fn acquire(self: *ArenaPool) !Allocator {
if (self.free_list) |entry| {
self.free_list = entry.next;
return entry.arena.allocator();
}
const entry = try self.entry_pool.create();
entry.* = .{
.next = null,
.arena = ArenaAllocator.init(self.allocator),
};
return entry.arena.allocator();
}
pub fn release(self: *ArenaPool, allocator: Allocator) void {
const arena: *std.heap.ArenaAllocator = @ptrCast(@alignCast(allocator.ptr));
const entry: *Entry = @fieldParentPtr("arena", arena);
if (self.free_list_len == self.free_list_max) {
arena.deinit();
self.entry_pool.destroy(entry);
return;
}
_ = arena.reset(.{ .retain_with_limit = self.retain_bytes });
entry.next = self.free_list;
self.free_list = entry;
}

View File

@@ -24,8 +24,10 @@ const ArenaAllocator = std.heap.ArenaAllocator;
const js = @import("js/js.zig"); const js = @import("js/js.zig");
const log = @import("../log.zig"); const log = @import("../log.zig");
const App = @import("../App.zig"); const App = @import("../App.zig");
const HttpClient = @import("../http/Client.zig");
const Notification = @import("../Notification.zig"); const ArenaPool = App.ArenaPool;
const HttpClient = App.Http.Client;
const Notification = App.Notification;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
@@ -40,6 +42,7 @@ env: js.Env,
app: *App, app: *App,
session: ?Session, session: ?Session,
allocator: Allocator, allocator: Allocator,
arena_pool: *ArenaPool,
http_client: *HttpClient, http_client: *HttpClient,
call_arena: ArenaAllocator, call_arena: ArenaAllocator,
page_arena: ArenaAllocator, page_arena: ArenaAllocator,
@@ -64,6 +67,7 @@ pub fn init(app: *App) !Browser {
.session = null, .session = null,
.allocator = allocator, .allocator = allocator,
.notification = notification, .notification = notification,
.arena_pool = &app.arena_pool,
.http_client = app.http.client, .http_client = app.http.client,
.call_arena = ArenaAllocator.init(allocator), .call_arena = ArenaAllocator.init(allocator),
.page_arena = ArenaAllocator.init(allocator), .page_arena = ArenaAllocator.init(allocator),

View File

@@ -361,32 +361,6 @@ pub fn textTrackCue(self: *Factory, child: anytype) !*@TypeOf(child) {
).create(allocator, child); ).create(allocator, child);
} }
fn hasChainRoot(comptime T: type) bool {
// Check if this is a root
if (@hasDecl(T, "_prototype_root")) {
return true;
}
// If no _proto field, we're at the top but not a recognized root
if (!@hasField(T, "_proto")) return false;
// Get the _proto field's type and recurse
const fields = @typeInfo(T).@"struct".fields;
inline for (fields) |field| {
if (std.mem.eql(u8, field.name, "_proto")) {
const ProtoType = reflect.Struct(field.type);
return hasChainRoot(ProtoType);
}
}
return false;
}
fn isChainType(comptime T: type) bool {
if (@hasField(T, "_proto")) return false;
return comptime hasChainRoot(T);
}
pub fn destroy(self: *Factory, value: anytype) void { pub fn destroy(self: *Factory, value: anytype) void {
const S = reflect.Struct(@TypeOf(value)); const S = reflect.Struct(@TypeOf(value));
@@ -403,7 +377,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
} }
} }
if (comptime isChainType(S)) { if (comptime @hasField(S, "_proto")) {
self.destroyChain(value, true, 0, std.mem.Alignment.@"1"); self.destroyChain(value, true, 0, std.mem.Alignment.@"1");
} else { } else {
self.destroyStandalone(value); self.destroyStandalone(value);
@@ -411,20 +385,7 @@ pub fn destroy(self: *Factory, value: anytype) void {
} }
pub fn destroyStandalone(self: *Factory, value: anytype) void { pub fn destroyStandalone(self: *Factory, value: anytype) void {
const S = reflect.Struct(@TypeOf(value));
assert(!@hasDecl(S, "_prototype_root"));
const allocator = self._slab.allocator(); const allocator = self._slab.allocator();
if (@hasDecl(S, "deinit")) {
// And it has a deinit, we'll call it
switch (@typeInfo(@TypeOf(S.deinit)).@"fn".params.len) {
1 => value.deinit(),
2 => value.deinit(self._page),
else => @compileLog(@typeName(S) ++ " has an invalid deinit function"),
}
}
allocator.destroy(value); allocator.destroy(value);
} }
@@ -440,10 +401,8 @@ fn destroyChain(
// aligns the old size to the alignment of this element // aligns the old size to the alignment of this element
const current_size = std.mem.alignForward(usize, old_size, @alignOf(S)); const current_size = std.mem.alignForward(usize, old_size, @alignOf(S));
const alignment = std.mem.Alignment.fromByteUnits(@alignOf(S));
const new_align = std.mem.Alignment.max(old_align, alignment);
const new_size = current_size + @sizeOf(S); const new_size = current_size + @sizeOf(S);
const new_align = std.mem.Alignment.max(old_align, std.mem.Alignment.of(S));
// This is initially called from a deinit. We don't want to call that // This is initially called from a deinit. We don't want to call that
// same deinit. So when this is the first time destroyChain is called // same deinit. So when this is the first time destroyChain is called
@@ -462,20 +421,15 @@ fn destroyChain(
if (@hasField(S, "_proto")) { if (@hasField(S, "_proto")) {
self.destroyChain(value._proto, false, new_size, new_align); self.destroyChain(value._proto, false, new_size, new_align);
} else if (@hasDecl(S, "JsApi")) {
// Doesn't have a _proto, but has a JsApi.
if (self._page.js.removeTaggedMapping(@intFromPtr(value))) |tagged| {
allocator.destroy(tagged);
}
} else { } else {
// no proto so this is the head of the chain. // no proto so this is the head of the chain.
// we use this as the ptr to the start of the chain. // we use this as the ptr to the start of the chain.
// and we have summed up the length. // and we have summed up the length.
assert(@hasDecl(S, "_prototype_root")); assert(@hasDecl(S, "_prototype_root"));
const memory_ptr: [*]const u8 = @ptrCast(value); const memory_ptr: [*]u8 = @ptrCast(@constCast(value));
const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits()); const len = std.mem.alignForward(usize, new_size, new_align.toByteUnits());
allocator.free(memory_ptr[0..len]); allocator.rawFree(memory_ptr[0..len], new_align, @returnAddress());
} }
} }

View File

@@ -27,7 +27,7 @@ const IS_DEBUG = builtin.mode == .Debug;
const log = @import("../log.zig"); const log = @import("../log.zig");
const Http = @import("../http/Http.zig"); const App = @import("../App.zig");
const String = @import("../string.zig").String; const String = @import("../string.zig").String;
const Mime = @import("Mime.zig"); const Mime = @import("Mime.zig");
@@ -59,6 +59,9 @@ const PageTransitionEvent = @import("webapi/event/PageTransitionEvent.zig");
const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind; const NavigationKind = @import("webapi/navigation/root.zig").NavigationKind;
const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig"); const KeyboardEvent = @import("webapi/event/KeyboardEvent.zig");
const Http = App.Http;
const ArenaPool = App.ArenaPool;
const timestamp = @import("../datetime.zig").timestamp; const timestamp = @import("../datetime.zig").timestamp;
const milliTimestamp = @import("../datetime.zig").milliTimestamp; const milliTimestamp = @import("../datetime.zig").milliTimestamp;
@@ -168,6 +171,11 @@ arena: Allocator,
// from JS. Best arena to use, when possible. // from JS. Best arena to use, when possible.
call_arena: Allocator, call_arena: Allocator,
arena_pool: *ArenaPool,
// In Debug, we use this to see if anything fails to release an arena back to
// the pool.
_arena_pool_leak_track: (if (IS_DEBUG) std.AutoHashMapUnmanaged(usize, []const u8) else void),
window: *Window, window: *Window,
document: *Document, document: *Document,
@@ -185,10 +193,14 @@ pub fn init(arena: Allocator, call_arena: Allocator, session: *Session) !*Page {
} }
const page = try session.browser.allocator.create(Page); const page = try session.browser.allocator.create(Page);
page._session = session;
page.arena = arena; page.arena = arena;
page.call_arena = call_arena; page.call_arena = call_arena;
page._session = session; page.arena_pool = session.browser.arena_pool;
if (comptime IS_DEBUG) {
page._arena_pool_leak_track = .empty;
}
try page.reset(true); try page.reset(true);
return page; return page;
@@ -220,6 +232,14 @@ pub fn deinit(self: *Page) void {
self._script_manager.shutdown = true; self._script_manager.shutdown = true;
session.browser.http_client.abort(); session.browser.http_client.abort();
self._script_manager.deinit(); self._script_manager.deinit();
if (comptime IS_DEBUG) {
var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* });
}
}
session.browser.allocator.destroy(self); session.browser.allocator.destroy(self);
} }
@@ -306,6 +326,14 @@ fn reset(self: *Page, comptime initializing: bool) !void {
self._undefined_custom_elements = .{}; self._undefined_custom_elements = .{};
try self.registerBackgroundTasks(); try self.registerBackgroundTasks();
if (comptime IS_DEBUG) {
var it = self._arena_pool_leak_track.valueIterator();
while (it.next()) |value_ptr| {
log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.* });
}
self._arena_pool_leak_track.clearRetainingCapacity();
}
} }
pub fn base(self: *const Page) [:0]const u8 { pub fn base(self: *const Page) [:0]const u8 {
@@ -340,6 +368,24 @@ pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 {
return try URL.getOrigin(allocator, self.url); return try URL.getOrigin(allocator, self.url);
} }
const GetArenaOpts = struct {
debug: []const u8,
};
pub fn getArena(self: *Page, comptime opts: GetArenaOpts) !Allocator {
const allocator = try self.arena_pool.acquire();
if (comptime IS_DEBUG) {
try self._arena_pool_leak_track.put(self.arena, @intFromPtr(allocator.ptr), opts.debug);
}
return allocator;
}
pub fn releaseArena(self: *Page, allocator: Allocator) void {
if (comptime IS_DEBUG) {
_ = self._arena_pool_leak_track.remove(@intFromPtr(allocator.ptr));
}
return self.arena_pool.release(allocator);
}
pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool { pub fn isSameOrigin(self: *const Page, url: [:0]const u8) !bool {
const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false; const current_origin = (try URL.getOrigin(self.call_arena, self.url)) orelse return false;
return std.mem.startsWith(u8, url, current_origin); return std.mem.startsWith(u8, url, current_origin);

View File

@@ -79,6 +79,11 @@ local: ?*const js.Local = null,
// The key is the @intFromPtr of the Zig value // The key is the @intFromPtr of the Zig value
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Any type that is stored in the identity_map which has a finalizer declared
// will have its finalizer stored here. This is only used when shutting down
// if v8 hasn't called the finalizer directly itself.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, FinalizerCallback) = .empty,
// Some web APIs have to manage opaque values. Ideally, they use an // Some web APIs have to manage opaque values. Ideally, they use an
// js.Object, but the js.Object has no lifetime guarantee beyond the // js.Object, but the js.Object has no lifetime guarantee beyond the
// current call. They can call .persist() on their js.Object to get // current call. They can call .persist() on their js.Object to get
@@ -145,6 +150,12 @@ pub fn deinit(self: *Context) void {
v8.v8__Global__Reset(global); v8.v8__Global__Reset(global);
} }
} }
{
var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.deinit();
}
}
for (self.global_values.items) |*global| { for (self.global_values.items) |*global| {
v8.v8__Global__Reset(global); v8.v8__Global__Reset(global);
@@ -179,6 +190,48 @@ pub fn deinit(self: *Context) void {
v8.v8__Global__Reset(&self.handle); v8.v8__Global__Reset(&self.handle);
} }
pub fn weakRef(self: *Context, obj: anytype) void {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__SetWeakFinalizer(global, obj, bridge.Struct(@TypeOf(obj)).JsApi.Meta.finalizer.from_v8, v8.kParameter);
}
pub fn strongRef(self: *Context, obj: anytype) void {
const global = self.identity_map.getPtr(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(global);
}
pub fn release(self: *Context, obj: *anyopaque) void {
var global = self.identity_map.fetchRemove(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__Reset(&global.value);
// The item has been fianalized, remove it for the finalizer callback so that
// we don't try to call it again on shutdown.
_ = self.finalizer_callbacks.fetchRemove(@intFromPtr(obj)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
};
}
// Any operation on the context have to be made from a local. // Any operation on the context have to be made from a local.
pub fn localScope(self: *Context, ls: *js.Local.Scope) void { pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
const isolate = self.isolate; const isolate = self.isolate;
@@ -793,31 +846,28 @@ pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void {
} }
// == Misc == // == Misc ==
// An interface for types that want to have their jsDeinit function to be // A type that has a finalizer can have its finalizer called one of two ways.
// called when the call context ends // The first is from V8 via the WeakCallback we give to weakRef. But that isn't
const DestructorCallback = struct { // guaranteed to fire, so we track this in ctx._finalizers and call them on
// context shutdown.
const FinalizerCallback = struct {
ptr: *anyopaque, ptr: *anyopaque,
destructorFn: *const fn (ptr: *anyopaque) void, finalizerFn: *const fn (ptr: *anyopaque) void,
fn init(ptr: anytype) DestructorCallback {
const T = @TypeOf(ptr);
const ptr_info = @typeInfo(T);
const gen = struct {
pub fn destructor(pointer: *anyopaque) void {
const self: T = @ptrCast(@alignCast(pointer));
return ptr_info.pointer.child.destructor(self);
}
};
pub fn init(ptr: anytype) FinalizerCallback {
const T = bridge.Struct(@TypeOf(ptr));
return .{ return .{
.ptr = ptr, .ptr = ptr,
.destructorFn = gen.destructor, .finalizerFn = struct {
pub fn wrap(self: *anyopaque) void {
T.JsApi.Meta.finalizer.from_zig(self);
}
}.wrap,
}; };
} }
pub fn destructor(self: DestructorCallback) void { pub fn deinit(self: FinalizerCallback) void {
self.destructorFn(self.ptr); self.finalizerFn(self.ptr);
} }
}; };

View File

@@ -26,6 +26,8 @@ const Context = @import("Context.zig");
const Isolate = @import("Isolate.zig"); const Isolate = @import("Isolate.zig");
const TaggedOpaque = @import("TaggedOpaque.zig"); const TaggedOpaque = @import("TaggedOpaque.zig");
const IS_DEBUG = @import("builtin").mode == .Debug;
const v8 = js.v8; const v8 = js.v8;
const CallOpts = Caller.CallOpts; const CallOpts = Caller.CallOpts;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
@@ -194,6 +196,21 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// dont' use js_obj.persist(), because we don't want to track this in // dont' use js_obj.persist(), because we don't want to track this in
// context.global_objects, we want to track it in context.identity_map. // context.global_objects, we want to track it in context.identity_map.
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr); v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
if (@hasDecl(JsApi.Meta, "finalizer")) {
if (comptime IS_DEBUG) {
// You can normally return a "*Node" and we'll correctly
// handle it as what it really is, e.g. an HTMLScriptElement.
// But for finalizers, we can't do that. I think this
// limitation will be OK - this auto-resolution is largely
// limited to Node -> HtmlElement, none of which has finalizers
std.debug.assert(resolved.class_id == JsApi.Meta.class_id);
}
try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), .init(value));
if (@hasDecl(JsApi.Meta, "finalizer")) {
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, resolved.ptr, JsApi.Meta.finalizer.from_v8, v8.kParameter);
}
}
return js_obj; return js_obj;
}, },
else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"), else => @compileError("Expected a struct or pointer, got " ++ @typeName(T) ++ " (constructors must return struct or pointers)"),

View File

@@ -91,6 +91,36 @@ pub fn Builder(comptime T: type) type {
} }
return entries; return entries;
} }
pub fn finalizer(comptime func: *const fn (self: *T, comptime shutdown: bool) void) Finalizer {
return .{
.from_zig = struct {
fn wrap(ptr: *anyopaque) void {
func(@ptrCast(@alignCast(ptr)), true);
}
}.wrap,
.from_v8 = struct {
fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const self: *T = @ptrCast(@alignCast(ptr));
// This is simply a requirement of any type that Finalizes:
// It must have a _page: *Page field. We need it because
// we need to check the item has already been cleared
// (There are all types of weird timing issues that seem
// to be possible between finalization and context shutdown,
// we need to be defensive).
// There _ARE_ alternatives to this. But this is simple.
const ctx = self._page.js;
if (!ctx.identity_map.contains(@intFromPtr(ptr))) {
return;
}
func(self, false);
ctx.release(ptr);
}
}.wrap,
};
}
}; };
} }
@@ -369,6 +399,17 @@ pub const Property = union(enum) {
int: i64, int: i64,
}; };
const Finalizer = struct {
// The finalizer wrapper when called fro Zig. This is only called on
// Context.deinit
from_zig: *const fn (ctx: *anyopaque) void,
// The finalizer wrapper when called from V8. This may never be called
// (hence why we fallback to calling in Context.denit). If it is called,
// it is only ever called after we SetWeak on the Global.
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
};
pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { pub fn unknownPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller: Caller = undefined; var caller: Caller = undefined;

View File

@@ -79,19 +79,28 @@ const ResponseType = enum {
}; };
pub fn init(page: *Page) !*XMLHttpRequest { pub fn init(page: *Page) !*XMLHttpRequest {
return page._factory.xhrEventTarget(XMLHttpRequest{ const arena = try page.getArena(.{.debug = "XMLHttpRequest"});
errdefer page.releaseArena(arena);
return try page._factory.xhrEventTarget(XMLHttpRequest{
._page = page, ._page = page,
._arena = arena,
._proto = undefined, ._proto = undefined,
._arena = page.arena,
._request_headers = try Headers.init(null, page), ._request_headers = try Headers.init(null, page),
}); });
} }
pub fn deinit(self: *XMLHttpRequest) void { pub fn deinit(self: *XMLHttpRequest, comptime shutdown: bool) void {
if (self.transfer) |transfer| { if (self._transfer) |transfer| {
if (shutdown) {
transfer.terminate();
} else {
transfer.abort(error.Abort); transfer.abort(error.Abort);
self.transfer = null;
} }
self._transfer = null;
}
self._page.releaseArena(self._arena);
self._page._factory.destroy(self);
} }
fn asEventTarget(self: *XMLHttpRequest) *EventTarget { fn asEventTarget(self: *XMLHttpRequest) *EventTarget {
@@ -110,7 +119,7 @@ pub fn setOnReadyStateChange(self: *XMLHttpRequest, cb_: ?js.Function) !void {
} }
} }
// TODO: this takes an opitonal 3 more parameters // TODO: this takes an optional 3 more parameters
// TODO: url should be a union, as it can be multiple things // TODO: url should be a union, as it can be multiple things
pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void { pub fn open(self: *XMLHttpRequest, method_: []const u8, url: [:0]const u8) !void {
// Abort any in-progress request // Abort any in-progress request
@@ -477,6 +486,7 @@ pub const JsApi = struct {
pub const name = "XMLHttpRequest"; pub const name = "XMLHttpRequest";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const finalizer = bridge.finalizer(XMLHttpRequest.deinit);
}; };
pub const constructor = bridge.constructor(XMLHttpRequest.init, .{}); pub const constructor = bridge.constructor(XMLHttpRequest.init, .{});

View File

@@ -371,7 +371,7 @@ fn makeTransfer(self: *Client, req: Request) !*Transfer {
return transfer; return transfer;
} }
fn requestFailed(self: *Client, transfer: *Transfer, err: anyerror) void { fn requestFailed(self: *Client, transfer: *Transfer, err: anyerror, comptime execute_callback: bool) void {
// this shouldn't happen, we'll crash in debug mode. But in release, we'll // this shouldn't happen, we'll crash in debug mode. But in release, we'll
// just noop this state. // just noop this state.
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
@@ -390,8 +390,10 @@ fn requestFailed(self: *Client, transfer: *Transfer, err: anyerror) void {
}); });
} }
if (execute_callback) {
transfer.req.error_callback(transfer.ctx, err); transfer.req.error_callback(transfer.ctx, err);
} }
}
// Restrictive since it'll only work if there are no inflight requests. In some // Restrictive since it'll only work if there are no inflight requests. In some
// cases, the libcurl documentation is clear that changing settings while a // cases, the libcurl documentation is clear that changing settings while a
@@ -600,18 +602,18 @@ fn processMessages(self: *Client) !bool {
if (!transfer._header_done_called) { if (!transfer._header_done_called) {
const proceed = transfer.headerDoneCallback(easy) catch |err| { const proceed = transfer.headerDoneCallback(easy) catch |err| {
log.err(.http, "header_done_callback", .{ .err = err }); log.err(.http, "header_done_callback", .{ .err = err });
self.requestFailed(transfer, err); self.requestFailed(transfer, err, true);
continue; continue;
}; };
if (!proceed) { if (!proceed) {
self.requestFailed(transfer, error.Abort); self.requestFailed(transfer, error.Abort, true);
break :blk; break :blk;
} }
} }
transfer.req.done_callback(transfer.ctx) catch |err| { transfer.req.done_callback(transfer.ctx) catch |err| {
// transfer isn't valid at this point, don't use it. // transfer isn't valid at this point, don't use it.
log.err(.http, "done_callback", .{ .err = err }); log.err(.http, "done_callback", .{ .err = err });
self.requestFailed(transfer, err); self.requestFailed(transfer, err, true);
continue; continue;
}; };
@@ -622,7 +624,7 @@ fn processMessages(self: *Client) !bool {
} }
processed = true; processed = true;
} else |err| { } else |err| {
self.requestFailed(transfer, err); self.requestFailed(transfer, err, true);
} }
} }
return processed; return processed;
@@ -972,7 +974,15 @@ pub const Transfer = struct {
} }
pub fn abort(self: *Transfer, err: anyerror) void { pub fn abort(self: *Transfer, err: anyerror) void {
self.client.requestFailed(self, err); self.client.requestFailed(self, err, true);
if (self._handle != null) {
self.client.endTransfer(self);
}
self.deinit();
}
pub fn terminate(self: *Transfer) void {
self.client.requestFailed(self, error.Shutdown, false);
if (self._handle != null) { if (self._handle != null) {
self.client.endTransfer(self); self.client.endTransfer(self);
} }

View File

@@ -404,7 +404,6 @@ pub const SlabAllocator = struct {
const ptr = memory.ptr; const ptr = memory.ptr;
const len = memory.len; const len = memory.len;
const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits()); const aligned_len = std.mem.alignForward(usize, len, alignment.toByteUnits());
const list = self.slabs.getPtr(.{ .size = aligned_len, .alignment = alignment }).?; const list = self.slabs.getPtr(.{ .size = aligned_len, .alignment = alignment }).?;
list.free(ptr); list.free(ptr);
} }