diff --git a/.github/actions/install/action.yml b/.github/actions/install/action.yml index 6c98f2e5..d9d7b803 100644 --- a/.github/actions/install/action.yml +++ b/.github/actions/install/action.yml @@ -13,7 +13,7 @@ inputs: zig-v8: description: 'zig v8 version to install' required: false - default: 'v0.3.1' + default: 'v0.3.2' v8: description: 'v8 version to install' required: false diff --git a/Dockerfile b/Dockerfile index 8729f992..e30fbdbe 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,7 +3,7 @@ FROM debian:stable-slim ARG MINISIG=0.12 ARG ZIG_MINISIG=RWSGOq2NVecA2UPNdBUZykf1CCb147pkmdtYxgb3Ti+JO/wCYvhbAb/U ARG V8=14.0.365.4 -ARG ZIG_V8=v0.3.1 +ARG ZIG_V8=v0.3.2 ARG TARGETPLATFORM RUN apt-get update -yq && \ diff --git a/build.zig.zon b/build.zig.zon index eb3812a8..b7f9cf3b 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -5,10 +5,10 @@ .minimum_zig_version = "0.15.2", .dependencies = .{ .v8 = .{ - .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.1.tar.gz", - .hash = "v8-0.0.0-xddH64J7BAC81mkf6G9RbEJxS-W3TIRl5iFnShwbqCqy", + .url = "https://github.com/lightpanda-io/zig-v8-fork/archive/refs/tags/v0.3.2.tar.gz", + .hash = "v8-0.0.0-xddH6wx-BABNgL7YIDgbnFgKZuXZ68yZNngNSrV6OjrY", }, - //.v8 = .{ .path = "../zig-v8-fork" }, + // .v8 = .{ .path = "../zig-v8-fork" }, .brotli = .{ // v1.2.0 .url = "https://github.com/google/brotli/archive/028fb5a23661f123017c060daa546b55cf4bde29.tar.gz", diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 573aa4f9..5588b704 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -205,7 +205,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void { event.acquireRef(); - defer event.deinit(false, self.page); + defer event.deinit(false, self.page._session); if (comptime IS_DEBUG) { log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles }); @@ -234,7 +234,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, const page = self.page; event.acquireRef(); - defer event.deinit(false, page); + defer event.deinit(false, page._session); if (comptime IS_DEBUG) { log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context }); diff --git a/src/browser/Factory.zig b/src/browser/Factory.zig index cbc2170d..c5ede22b 100644 --- a/src/browser/Factory.zig +++ b/src/browser/Factory.zig @@ -48,13 +48,11 @@ const Factory = @This(); _arena: Allocator, _slab: SlabAllocator, -pub fn init(arena: Allocator) !*Factory { - const self = try arena.create(Factory); - self.* = .{ +pub fn init(arena: Allocator) Factory { + return .{ ._arena = arena, ._slab = SlabAllocator.init(arena, 128), }; - return self; } // this is a root object diff --git a/src/browser/Page.zig b/src/browser/Page.zig index 47c63904..811e7e5c 100644 --- a/src/browser/Page.zig +++ b/src/browser/Page.zig @@ -190,6 +190,8 @@ _queued_navigation: ?*QueuedNavigation = null, // The URL of the current page url: [:0]const u8 = "about:blank", +origin: ?[]const u8 = null, + // The base url specifies the base URL used to resolve the relative urls. // It is set by a tag. // If null the url must be used. @@ -212,14 +214,6 @@ arena: Allocator, // from JS. Best arena to use, when possible. 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, struct { - owner: []const u8, - count: usize, -}) else void) = if (IS_DEBUG) .empty else {}, - parent: ?*Page, window: *Window, document: *Document, @@ -246,17 +240,11 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void if (comptime IS_DEBUG) { log.debug(.page, "page.init", .{}); } - const browser = session.browser; - const arena_pool = browser.arena_pool; - const page_arena = if (parent) |p| p.arena else try arena_pool.acquire(); - errdefer if (parent == null) arena_pool.release(page_arena); - - var factory = if (parent) |p| p._factory else try Factory.init(page_arena); - - const call_arena = try arena_pool.acquire(); - errdefer arena_pool.release(call_arena); + const call_arena = try session.getArena(.{ .debug = "call_arena" }); + errdefer session.releaseArena(call_arena); + const factory = &session.factory; const document = (try factory.document(Node.Document.HTMLDocument{ ._proto = undefined, })).asDocument(); @@ -264,10 +252,9 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void self.* = .{ .js = undefined, .parent = parent, - .arena = page_arena, + .arena = session.page_arena, .document = document, .window = undefined, - .arena_pool = arena_pool, .call_arena = call_arena, ._frame_id = frame_id, ._session = session, @@ -275,7 +262,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void ._pending_loads = 1, // always 1 for the ScriptManager ._type = if (parent == null) .root else .frame, ._script_manager = undefined, - ._event_manager = EventManager.init(page_arena, self), + ._event_manager = EventManager.init(session.page_arena, self), }; var screen: *Screen = undefined; @@ -303,6 +290,7 @@ pub fn init(self: *Page, frame_id: u32, session: *Session, parent: ?*Page) !void ._visual_viewport = visual_viewport, }); + const browser = session.browser; self._script_manager = ScriptManager.init(browser.allocator, browser.http_client, self); errdefer self._script_manager.deinit(); @@ -338,11 +326,12 @@ pub fn deinit(self: *Page, abort_http: bool) void { // stats.print(&stream) catch unreachable; } + const session = self._session; + if (self._queued_navigation) |qn| { - self.arena_pool.release(qn.arena); + session.releaseArena(qn.arena); } - const session = self._session; session.browser.env.destroyContext(self.js); self._script_manager.shutdown = true; @@ -358,23 +347,7 @@ pub fn deinit(self: *Page, abort_http: bool) void { self._script_manager.deinit(); - if (comptime IS_DEBUG) { - var it = self._arena_pool_leak_track.valueIterator(); - while (it.next()) |value_ptr| { - if (value_ptr.count > 0) { - log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner, .type = self._type, .url = self.url }); - if (comptime builtin.is_test) { - @panic("ArenaPool Leak"); - } - } - } - } - - self.arena_pool.release(self.call_arena); - - if (self.parent == null) { - self.arena_pool.release(self.arena); - } + session.releaseArena(self.call_arena); } pub fn base(self: *const Page) [:0]const u8 { @@ -388,10 +361,6 @@ pub fn getTitle(self: *Page) !?[]const u8 { return null; } -pub fn getOrigin(self: *Page, allocator: Allocator) !?[]const u8 { - return try URL.getOrigin(allocator, self.url); -} - // Add comon headers for a request: // * cookies // * referer @@ -418,38 +387,16 @@ pub fn headersForRequest(self: *Page, temp: Allocator, url: [:0]const u8, header } } -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) { - const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr)); - if (gop.found_existing) { - std.debug.assert(gop.value_ptr.count == 0); - } - gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 }; - } - return allocator; +pub fn getArena(self: *Page, comptime opts: Session.GetArenaOpts) !Allocator { + return self._session.getArena(opts); } pub fn releaseArena(self: *Page, allocator: Allocator) void { - if (comptime IS_DEBUG) { - const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?; - if (found.count != 1) { - log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count, .type = self._type, .url = self.url }); - if (comptime builtin.is_test) { - @panic("ArenaPool Double Free"); - } - return; - } - found.count = 0; - } - return self.arena_pool.release(allocator); + return self._session.releaseArena(allocator); } 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 = self.origin orelse return false; return std.mem.startsWith(u8, url, current_origin); } @@ -472,6 +419,14 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi // page and dispatch the events. if (std.mem.eql(u8, "about:blank", request_url)) { self.url = "about:blank"; + + if (self.parent) |parent| { + self.origin = parent.origin; + } else { + self.origin = null; + } + try self.js.setOrigin(self.origin); + // Assume we parsed the document. // It's important to force a reset during the following navigation. self._parse_state = .complete; @@ -518,6 +473,7 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi var http_client = session.browser.http_client; self.url = try self.arena.dupeZ(u8, request_url); + self.origin = try URL.getOrigin(self.arena, self.url); self._req_id = req_id; self._navigated_options = .{ @@ -578,8 +534,8 @@ pub fn scheduleNavigation(self: *Page, request_url: []const u8, opts: NavigateOp if (self.canScheduleNavigation(std.meta.activeTag(nt)) == false) { return; } - const arena = try self.arena_pool.acquire(); - errdefer self.arena_pool.release(arena); + const arena = try self._session.getArena(.{ .debug = "scheduleNavigation" }); + errdefer self._session.releaseArena(arena); return self.scheduleNavigationWithArena(arena, request_url, opts, nt); } @@ -618,9 +574,8 @@ fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url: if (target.parent == null) { try session.navigation.updateEntries(target.url, opts.kind, target, true); } - // doin't defer this, the caller, the caller is responsible for freeing - // it on error - target.arena_pool.release(arena); + // don't defer this, the caller is responsible for freeing it on error + session.releaseArena(arena); return; } @@ -652,7 +607,7 @@ fn scheduleNavigationWithArena(originator: *Page, arena: Allocator, request_url: }; if (target._queued_navigation) |existing| { - target.arena_pool.release(existing.arena); + session.releaseArena(existing.arena); } target._queued_navigation = qn; @@ -825,9 +780,15 @@ fn notifyParentLoadComplete(self: *Page) void { fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool { var self: *Page = @ptrCast(@alignCast(transfer.ctx)); - // would be different than self.url in the case of a redirect const header = &transfer.response_header.?; - self.url = try self.arena.dupeZ(u8, std.mem.span(header.url)); + + const response_url = std.mem.span(header.url); + if (std.mem.eql(u8, response_url, self.url) == false) { + // would be different than self.url in the case of a redirect + self.url = try self.arena.dupeZ(u8, response_url); + self.origin = try URL.getOrigin(self.arena, self.url); + } + try self.js.setOrigin(self.origin); self.window._location = try Location.init(self.url, self); self.document._location = self.window._location; @@ -3163,7 +3124,7 @@ pub fn handleClick(self: *Page, target: *Node) !void { pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void { const event = keyboard_event.asEvent(); const element = self.window._document._active_element orelse { - keyboard_event.deinit(false, self); + keyboard_event.deinit(false, self._session); return; }; @@ -3239,7 +3200,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form // so submit_event is still valid when we check _prevent_default submit_event.acquireRef(); - defer submit_event.deinit(false, self); + defer submit_event.deinit(false, self._session); try self._event_manager.dispatch(form_element.asEventTarget(), submit_event); // If the submit event was prevented, don't submit the form @@ -3253,8 +3214,8 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form // I don't think this is technically correct, but FormData handles it ok const form_data = try FormData.init(form, submitter_, self); - const arena = try self.arena_pool.acquire(); - errdefer self.arena_pool.release(arena); + const arena = try self._session.getArena(.{ .debug = "submitForm" }); + errdefer self._session.releaseArena(arena); const encoding = form_element.getAttributeSafe(comptime .wrap("enctype")); diff --git a/src/browser/Session.zig b/src/browser/Session.zig index d8a85fa2..529f0847 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -21,6 +21,7 @@ const lp = @import("lightpanda"); const builtin = @import("builtin"); const log = @import("../log.zig"); +const App = @import("../App.zig"); const js = @import("js/js.zig"); const storage = @import("webapi/storage/storage.zig"); @@ -29,20 +30,53 @@ const History = @import("webapi/History.zig"); const Page = @import("Page.zig"); const Browser = @import("Browser.zig"); +const Factory = @import("Factory.zig"); const Notification = @import("../Notification.zig"); const QueuedNavigation = Page.QueuedNavigation; const Allocator = std.mem.Allocator; +const ArenaPool = App.ArenaPool; const IS_DEBUG = builtin.mode == .Debug; -// Session is like a browser's tab. -// It owns the js env and the loader for all the pages of the session. // You can create successively multiple pages for a session, but you must -// deinit a page before running another one. +// deinit a page before running another one. It manages two distinct lifetimes. +// +// The first is the lifetime of the Session itself, where pages are created and +// removed, but share the same cookie jar and navigation history (etc...) +// +// The second is as a container the data needed by the full page hierarchy, i.e. \ +// the root page and all of its frames (and all of their frames.) const Session = @This(); +// These are the fields that remain intact for the duration of the Session browser: *Browser, +arena: Allocator, +history: History, +navigation: Navigation, +storage_shed: storage.Shed, notification: *Notification, +cookie_jar: storage.Cookie.Jar, + +// These are the fields that get reset whenever the Session's page (the root) is reset. +factory: Factory, + +page_arena: Allocator, + +// Origin map for same-origin context sharing. Scoped to the root page lifetime. +origins: std.StringHashMapUnmanaged(*js.Origin) = .empty, + +// Shared resources for all pages in this session. +// These live for the duration of the page tree (root + frames). +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, struct { + owner: []const u8, + count: usize, +}) else void = if (IS_DEBUG) .empty else {}, + +page: ?Page, queued_navigation: std.ArrayList(*Page), // Temporary buffer for about:blank navigations during processing. @@ -50,27 +84,24 @@ queued_navigation: std.ArrayList(*Page), // about:blank navigations (which may add to queued_navigation). queued_queued_navigation: std.ArrayList(*Page), -// Used to create our Inspector and in the BrowserContext. -arena: Allocator, - -cookie_jar: storage.Cookie.Jar, -storage_shed: storage.Shed, - -history: History, -navigation: Navigation, - -page: ?Page, - frame_id_gen: u32, pub fn init(self: *Session, browser: *Browser, notification: *Notification) !void { const allocator = browser.app.allocator; - const arena = try browser.arena_pool.acquire(); - errdefer browser.arena_pool.release(arena); + const arena_pool = browser.arena_pool; + + const arena = try arena_pool.acquire(); + errdefer arena_pool.release(arena); + + const page_arena = try arena_pool.acquire(); + errdefer arena_pool.release(page_arena); self.* = .{ .page = null, .arena = arena, + .arena_pool = arena_pool, + .page_arena = page_arena, + .factory = Factory.init(page_arena), .history = .{}, .frame_id_gen = 0, // The prototype (EventTarget) for Navigation is created when a Page is created. @@ -90,9 +121,9 @@ pub fn deinit(self: *Session) void { } self.cookie_jar.deinit(); - const browser = self.browser; - self.storage_shed.deinit(browser.app.allocator); - browser.arena_pool.release(self.arena); + self.storage_shed.deinit(self.browser.app.allocator); + self.arena_pool.release(self.page_arena); + self.arena_pool.release(self.arena); } // NOTE: the caller is not the owner of the returned value, @@ -126,29 +157,133 @@ pub fn removePage(self: *Session) void { self.page = null; self.navigation.onRemovePage(); + self.resetPageResources(); if (comptime IS_DEBUG) { log.debug(.browser, "remove page", .{}); } } +pub const GetArenaOpts = struct { + debug: []const u8, +}; + +pub fn getArena(self: *Session, opts: GetArenaOpts) !Allocator { + const allocator = try self.arena_pool.acquire(); + if (comptime IS_DEBUG) { + // Use session's arena (not page_arena) since page_arena gets reset between pages + const gop = try self._arena_pool_leak_track.getOrPut(self.arena, @intFromPtr(allocator.ptr)); + if (gop.found_existing and gop.value_ptr.count != 0) { + log.err(.bug, "ArenaPool Double Use", .{ .owner = gop.value_ptr.*.owner }); + @panic("ArenaPool Double Use"); + } + gop.value_ptr.* = .{ .owner = opts.debug, .count = 1 }; + } + return allocator; +} + +pub fn releaseArena(self: *Session, allocator: Allocator) void { + if (comptime IS_DEBUG) { + const found = self._arena_pool_leak_track.getPtr(@intFromPtr(allocator.ptr)).?; + if (found.count != 1) { + log.err(.bug, "ArenaPool Double Free", .{ .owner = found.owner, .count = found.count }); + if (comptime builtin.is_test) { + @panic("ArenaPool Double Free"); + } + return; + } + found.count = 0; + } + return self.arena_pool.release(allocator); +} + +pub fn getOrCreateOrigin(self: *Session, key_: ?[]const u8) !*js.Origin { + const key = key_ orelse { + var opaque_origin: [36]u8 = undefined; + @import("../id.zig").uuidv4(&opaque_origin); + // Origin.init will dupe opaque_origin. It's fine that this doesn't + // get added to self.origins. In fact, it further isolates it. When the + // context is freed, it'll call session.releaseOrigin which will free it. + return js.Origin.init(self.browser.app, self.browser.env.isolate, &opaque_origin); + }; + + const gop = try self.origins.getOrPut(self.arena, key); + if (gop.found_existing) { + const origin = gop.value_ptr.*; + origin.rc += 1; + return origin; + } + + errdefer _ = self.origins.remove(key); + + const origin = try js.Origin.init(self.browser.app, self.browser.env.isolate, key); + gop.key_ptr.* = origin.key; + gop.value_ptr.* = origin; + return origin; +} + +pub fn releaseOrigin(self: *Session, origin: *js.Origin) void { + const rc = origin.rc; + if (rc == 1) { + _ = self.origins.remove(origin.key); + origin.deinit(self.browser.app); + } else { + origin.rc = rc - 1; + } +} + +/// Reset page_arena and factory for a clean slate. +/// Called when root page is removed. +fn resetPageResources(self: *Session) void { + // Check for arena leaks before releasing + if (comptime IS_DEBUG) { + var it = self._arena_pool_leak_track.valueIterator(); + while (it.next()) |value_ptr| { + if (value_ptr.count > 0) { + log.err(.bug, "ArenaPool Leak", .{ .owner = value_ptr.owner }); + } + } + self._arena_pool_leak_track.clearRetainingCapacity(); + } + + // All origins should have been released when contexts were destroyed + if (comptime IS_DEBUG) { + std.debug.assert(self.origins.count() == 0); + } + // Defensive cleanup in case origins leaked + { + const app = self.browser.app; + var it = self.origins.valueIterator(); + while (it.next()) |value| { + value.*.deinit(app); + } + self.origins.clearRetainingCapacity(); + } + + // Release old page_arena and acquire fresh one + self.frame_id_gen = 0; + self.arena_pool.reset(self.page_arena, 64 * 1024); + self.factory = Factory.init(self.page_arena); +} + pub fn replacePage(self: *Session) !*Page { if (comptime IS_DEBUG) { log.debug(.browser, "replace page", .{}); } lp.assert(self.page != null, "Session.replacePage null page", .{}); + lp.assert(self.page.?.parent == null, "Session.replacePage with parent", .{}); var current = self.page.?; const frame_id = current._frame_id; - const parent = current.parent; - current.deinit(false); + current.deinit(true); + self.resetPageResources(); self.browser.env.memoryPressureNotification(.moderate); self.page = @as(Page, undefined); const page = &self.page.?; - try Page.init(page, frame_id, self, parent); + try Page.init(page, frame_id, self, null); return page; } @@ -428,12 +563,11 @@ fn processQueuedNavigation(self: *Session) !void { fn processFrameNavigation(self: *Session, page: *Page, qn: *QueuedNavigation) !void { lp.assert(page.parent != null, "root queued navigation", .{}); - const browser = self.browser; const iframe = page.iframe.?; const parent = page.parent.?; page._queued_navigation = null; - defer browser.arena_pool.release(qn.arena); + defer self.releaseArena(qn.arena); errdefer iframe._window = null; @@ -465,9 +599,21 @@ fn processRootQueuedNavigation(self: *Session) !void { // create a copy before the page is cleared const qn = current_page._queued_navigation.?; current_page._queued_navigation = null; - defer self.browser.arena_pool.release(qn.arena); + + defer self.arena_pool.release(qn.arena); + + // HACK + // Mark as released in tracking BEFORE removePage clears the map. + // We can't call releaseArena() because that would also return the arena + // to the pool, making the memory invalid before we use qn.url/qn.opts. + if (comptime IS_DEBUG) { + if (self._arena_pool_leak_track.getPtr(@intFromPtr(qn.arena.ptr))) |found| { + found.count = 0; + } + } self.removePage(); + self.page = @as(Page, undefined); const new_page = &self.page.?; try Page.init(new_page, frame_id, self, null); diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 9180223c..ac6ed3c1 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -23,9 +23,11 @@ const log = @import("../../log.zig"); const js = @import("js.zig"); const Env = @import("Env.zig"); const bridge = @import("bridge.zig"); +const Origin = @import("Origin.zig"); const Scheduler = @import("Scheduler.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const ScriptManager = @import("../ScriptManager.zig"); const v8 = js.v8; @@ -41,6 +43,7 @@ const Context = @This(); id: usize, env: *Env, page: *Page, +session: *Session, isolate: js.Isolate, // Per-context microtask queue for isolation between contexts @@ -74,39 +77,11 @@ call_depth: usize = 0, // context.localScope local: ?*const js.Local = null, -// Serves two purposes. Like `global_objects`, this is used to free -// every Global(Object) we've created during the lifetime of the context. -// More importantly, it serves as an identity map - for a given Zig -// instance, we map it to the same Global(Object). -// The key is the @intFromPtr of the Zig value -identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, +origin: *Origin, -// 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, -finalizer_callback_pool: std.heap.MemoryPool(FinalizerCallback), - -// Some web APIs have to manage opaque values. Ideally, they use an -// js.Object, but the js.Object has no lifetime guarantee beyond the -// current call. They can call .persist() on their js.Object to get -// a `Global(Object)`. We need to track these to free them. -// This used to be a map and acted like identity_map; the key was -// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without -// a reliable way to know if an object has already been persisted, -// we now simply persist every time persist() is called. -global_values: std.ArrayList(v8.Global) = .empty, -global_objects: std.ArrayList(v8.Global) = .empty, +// Unlike other v8 types, like functions or objects, modules are not shared +// across origins. global_modules: std.ArrayList(v8.Global) = .empty, -global_promises: std.ArrayList(v8.Global) = .empty, -global_functions: std.ArrayList(v8.Global) = .empty, -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. module_cache: std.StringHashMapUnmanaged(ModuleEntry) = .empty, @@ -174,64 +149,11 @@ pub fn deinit(self: *Context) void { // this can release objects self.scheduler.deinit(); - { - var it = self.identity_map.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } - { - var it = self.finalizer_callbacks.valueIterator(); - while (it.next()) |finalizer| { - finalizer.*.deinit(); - } - self.finalizer_callback_pool.deinit(); - } - - for (self.global_values.items) |*global| { - v8.v8__Global__Reset(global); - } - - for (self.global_objects.items) |*global| { - v8.v8__Global__Reset(global); - } - for (self.global_modules.items) |*global| { v8.v8__Global__Reset(global); } - for (self.global_functions.items) |*global| { - v8.v8__Global__Reset(global); - } - - for (self.global_promises.items) |*global| { - v8.v8__Global__Reset(global); - } - - for (self.global_promise_resolvers.items) |*global| { - v8.v8__Global__Reset(global); - } - - { - var it = self.global_values_temp.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - } - - { - 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| { - v8.v8__Global__Reset(global); - } - } + self.session.releaseOrigin(self.origin); v8.v8__Global__Reset(&self.handle); env.isolate.notifyContextDisposed(); @@ -241,8 +163,40 @@ pub fn deinit(self: *Context) void { v8.v8__MicrotaskQueue__DELETE(self.microtask_queue); } +pub fn setOrigin(self: *Context, key: ?[]const u8) !void { + const env = self.env; + const isolate = env.isolate; + + const origin = try self.session.getOrCreateOrigin(key); + errdefer self.session.releaseOrigin(origin); + + try self.origin.transferTo(origin); + self.origin.deinit(env.app); + + self.origin = origin; + + { + var ls: js.Local.Scope = undefined; + self.localScope(&ls); + defer ls.deinit(); + + // Set the V8::Context SecurityToken, which is a big part of what allows + // one context to access another. + const token_local = v8.v8__Global__Get(&origin.security_token, isolate.handle); + v8.v8__Context__SetSecurityToken(ls.local.handle, token_local); + } +} + +pub fn trackGlobal(self: *Context, global: v8.Global) !void { + return self.origin.trackGlobal(global); +} + +pub fn trackTemp(self: *Context, global: v8.Global) !void { + return self.origin.trackTemp(global); +} + pub fn weakRef(self: *Context, obj: anytype) void { - const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse { + const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -253,7 +207,7 @@ pub fn weakRef(self: *Context, obj: anytype) void { } pub fn safeWeakRef(self: *Context, obj: anytype) void { - const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse { + const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -265,7 +219,7 @@ pub fn safeWeakRef(self: *Context, obj: anytype) void { } pub fn strongRef(self: *Context, obj: anytype) void { - const fc = self.finalizer_callbacks.get(@intFromPtr(obj)) orelse { + const fc = self.origin.finalizer_callbacks.get(@intFromPtr(obj)) orelse { if (comptime IS_DEBUG) { // should not be possible std.debug.assert(false); @@ -275,45 +229,6 @@ pub fn strongRef(self: *Context, obj: anytype) void { v8.v8__Global__ClearWeak(&fc.global); } -pub fn release(self: *Context, item: anytype) void { - if (@TypeOf(item) == *anyopaque) { - // Existing *anyopaque path for identity_map. Called internally from - // finalizers - var global = self.identity_map.fetchRemove(@intFromPtr(item)) 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. - const fc = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse { - if (comptime IS_DEBUG) { - // should not be possible - std.debug.assert(false); - } - return; - }; - self.finalizer_callback_pool.destroy(fc.value); - return; - } - - 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)), - }; - - if (map.fetchRemove(item.handle.data_ptr)) |kv| { - var global = kv.value; - v8.v8__Global__Reset(&global); - } -} - // Any operation on the context have to be made from a local. pub fn localScope(self: *Context, ls: *js.Local.Scope) void { const isolate = self.isolate; @@ -1039,34 +954,6 @@ pub fn queueMicrotaskFunc(self: *Context, cb: js.Function) void { v8.v8__MicrotaskQueue__EnqueueMicrotaskFunc(self.microtask_queue, self.isolate.handle, cb.handle); } -pub fn createFinalizerCallback(self: *Context, global: v8.Global, ptr: *anyopaque, finalizerFn: *const fn (ptr: *anyopaque, page: *Page) void) !*FinalizerCallback { - const fc = try self.finalizer_callback_pool.create(); - fc.* = .{ - .ctx = self, - .ptr = ptr, - .global = global, - .finalizerFn = finalizerFn, - }; - return fc; -} - -// == Misc == -// A type that has a finalizer can have its finalizer called one of two ways. -// The first is from V8 via the WeakCallback we give to weakRef. But that isn't -// guaranteed to fire, so we track this in ctx._finalizers and call them on -// context shutdown. -pub const FinalizerCallback = struct { - ctx: *Context, - ptr: *anyopaque, - global: v8.Global, - finalizerFn: *const fn (ptr: *anyopaque, page: *Page) void, - - pub fn deinit(self: *FinalizerCallback) void { - self.finalizerFn(self.ptr, self.ctx.page); - self.ctx.finalizer_callback_pool.destroy(self); - } -}; - // == Profiler == pub fn startCpuProfiler(self: *Context) void { if (comptime !IS_DEBUG) { diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 1a86ffd5..e8488541 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -26,6 +26,7 @@ const App = @import("../../App.zig"); const log = @import("../../log.zig"); const bridge = @import("bridge.zig"); +const Origin = @import("Origin.zig"); const Context = @import("Context.zig"); const Isolate = @import("Isolate.zig"); const Platform = @import("Platform.zig"); @@ -57,6 +58,8 @@ const Env = @This(); app: *App, +allocator: Allocator, + platform: *const Platform, // the global isolate @@ -70,6 +73,11 @@ isolate_params: *v8.CreateParams, context_id: usize, +// Maps origin -> shared Origin contains, for v8 values shared across +// same-origin Contexts. There's a mismatch here between our JS model and our +// Browser model. Origins only live as long as the root page of a session exists. +// It would be wrong/dangerous to re-use an Origin across root page navigations. + // Global handles that need to be freed on deinit eternal_function_templates: []v8.Eternal, @@ -206,6 +214,7 @@ pub fn init(app: *App, opts: InitOpts) !Env { return .{ .app = app, .context_id = 0, + .allocator = allocator, .contexts = undefined, .context_count = 0, .isolate = isolate, @@ -228,7 +237,9 @@ pub fn deinit(self: *Env) void { ctx.deinit(); } - const allocator = self.app.allocator; + const app = self.app; + const allocator = app.allocator; + if (self.inspector) |i| { i.deinit(allocator); } @@ -272,6 +283,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context { // get the global object for the context, this maps to our Window const global_obj = v8.v8__Context__Global(v8_context).?; + { // Store our TAO inside the internal field of the global object. This // maps the v8::Object -> Zig instance. Almost all objects have this, and @@ -287,6 +299,7 @@ pub fn createContext(self: *Env, page: *Page) !*Context { }; v8.v8__Object__SetAlignedPointerInInternalField(global_obj, 0, tao); } + // our window wrapped in a v8::Global var global_global: v8.Global = undefined; v8.v8__Global__New(isolate.handle, global_obj, &global_global); @@ -294,10 +307,15 @@ pub fn createContext(self: *Env, page: *Page) !*Context { const context_id = self.context_id; self.context_id = context_id + 1; + const origin = try page._session.getOrCreateOrigin(null); + errdefer page._session.releaseOrigin(origin); + const context = try context_arena.create(Context); context.* = .{ .env = self, .page = page, + .session = page._session, + .origin = origin, .id = context_id, .isolate = isolate, .arena = context_arena, @@ -307,9 +325,8 @@ pub fn createContext(self: *Env, page: *Page) !*Context { .microtask_queue = microtask_queue, .script_manager = &page._script_manager, .scheduler = .init(context_arena), - .finalizer_callback_pool = std.heap.MemoryPool(Context.FinalizerCallback).init(self.app.allocator), }; - try context.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global); + try context.origin.identity_map.putNoClobber(context_arena, @intFromPtr(page.window), global_global); // Store a pointer to our context inside the v8 context so that, given // a v8 context, we can get our context out diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index 01243d35..bfb5e53d 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -209,11 +209,11 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { - try ctx.global_functions.append(ctx.arena, global); - } else { - try ctx.global_functions_temp.put(ctx.arena, global.data_ptr, global); + try ctx.trackGlobal(global); + return .{ .handle = global, .origin = {} }; } - return .{ .handle = global }; + try ctx.trackTemp(global); + return .{ .handle = global, .origin = ctx.origin }; } pub fn tempWithThis(self: *const Function, value: anytype) !Temp { @@ -226,15 +226,18 @@ pub fn persistWithThis(self: *const Function, value: anytype) !Global { return with_this.persist(); } -pub const Temp = G(0); -pub const Global = G(1); +pub const Temp = G(.temp); +pub const Global = G(.global); -fn G(comptime discriminator: u8) type { +const GlobalType = enum(u8) { + temp, + global, +}; + +fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - - // makes the types different (G(0) != G(1)), without taking up space - comptime _: u8 = discriminator, + origin: if (global_type == .temp) *js.Origin else void, const Self = @This(); @@ -252,5 +255,9 @@ fn G(comptime discriminator: u8) type { pub fn isEqual(self: *const Self, other: Function) bool { return v8.v8__Global__IsEqual(&self.handle, other.handle); } + + pub fn release(self: *const Self) void { + self.origin.releaseTemp(self.handle); + } }; } diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index 6a68b332..78ab9d15 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -18,6 +18,7 @@ const std = @import("std"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const log = @import("../../log.zig"); const string = @import("../../string.zig"); @@ -171,7 +172,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, .pointer => |ptr| { const resolved = resolveValue(value); - const gop = try ctx.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr)); + const gop = try ctx.origin.identity_map.getOrPut(arena, @intFromPtr(resolved.ptr)); if (gop.found_existing) { // we've seen this instance before, return the same object return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); @@ -225,16 +226,17 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, // can't figure out how to make that work, since it depends on // the [runtime] `value`. // We need the resolved finalizer, which we have in resolved. + // // The above if statement would be more clear as: // if (resolved.finalizer_from_v8) |finalizer| { // But that's a runtime check. // Instead, we check if the base has finalizer. The assumption // here is that if a resolve type has a finalizer, then the base // should have a finalizer too. - const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?); + const fc = try ctx.origin.createFinalizerCallback(ctx.session, gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?); { errdefer fc.deinit(); - try ctx.finalizer_callbacks.put(ctx.arena, @intFromPtr(resolved.ptr), fc); + try ctx.origin.finalizer_callbacks.put(ctx.origin.arena, @intFromPtr(resolved.ptr), fc); } conditionallyReference(value); @@ -1083,7 +1085,7 @@ const Resolved = struct { class_id: u16, prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry, finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null, - finalizer_from_zig: ?*const fn (ptr: *anyopaque, page: *Page) void = null, + finalizer_from_zig: ?*const fn (ptr: *anyopaque, session: *Session) void = null, }; pub fn resolveValue(value: anytype) Resolved { const T = bridge.Struct(@TypeOf(value)); diff --git a/src/browser/js/Object.zig b/src/browser/js/Object.zig index 981f4a2b..fbf036e4 100644 --- a/src/browser/js/Object.zig +++ b/src/browser/js/Object.zig @@ -97,7 +97,7 @@ pub fn persist(self: Object) !Global { var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); - try ctx.global_objects.append(ctx.arena, global); + try ctx.trackGlobal(global); return .{ .handle = global }; } diff --git a/src/browser/js/Origin.zig b/src/browser/js/Origin.zig new file mode 100644 index 00000000..486888f1 --- /dev/null +++ b/src/browser/js/Origin.zig @@ -0,0 +1,240 @@ +// Copyright (C) 2023-2025 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 . + +// Origin represents the shared Zig<->JS bridge state for all contexts within +// the same origin. Multiple contexts (frames) from the same origin share a +// single Origin, ensuring that JS objects maintain their identity across frames. + +const std = @import("std"); +const js = @import("js.zig"); + +const App = @import("../../App.zig"); +const Session = @import("../Session.zig"); + +const v8 = js.v8; +const Allocator = std.mem.Allocator; +const IS_DEBUG = @import("builtin").mode == .Debug; + +const Origin = @This(); + +rc: usize = 1, +arena: Allocator, + +// The key, e.g. lightpanda.io:443 +key: []const u8, + +// Security token - all contexts in this realm must use the same v8::Value instance +// as their security token for V8 to allow cross-context access +security_token: v8.Global, + +// Serves two purposes. Like `global_objects`, this is used to free +// every Global(Object) we've created during the lifetime of the realm. +// More importantly, it serves as an identity map - for a given Zig +// instance, we map it to the same Global(Object). +// The key is the @intFromPtr of the Zig value +identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, + +// Some web APIs have to manage opaque values. Ideally, they use an +// js.Object, but the js.Object has no lifetime guarantee beyond the +// current call. They can call .persist() on their js.Object to get +// a `Global(Object)`. We need to track these to free them. +// This used to be a map and acted like identity_map; the key was +// the @intFromPtr(js_obj.handle). But v8 can re-use address. Without +// a reliable way to know if an object has already been persisted, +// we now simply persist every time persist() is called. +globals: std.ArrayList(v8.Global) = .empty, + +// Temp variants stored in HashMaps for O(1) early cleanup. +// Key is global.data_ptr. +temps: 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, + +pub fn init(app: *App, isolate: js.Isolate, key: []const u8) !*Origin { + const arena = try app.arena_pool.acquire(); + errdefer app.arena_pool.release(arena); + + var hs: js.HandleScope = undefined; + hs.init(isolate); + defer hs.deinit(); + + const owned_key = try arena.dupe(u8, key); + const token_local = isolate.initStringHandle(owned_key); + var token_global: v8.Global = undefined; + v8.v8__Global__New(isolate.handle, token_local, &token_global); + + const self = try arena.create(Origin); + self.* = .{ + .rc = 1, + .arena = arena, + .key = owned_key, + .globals = .empty, + .temps = .empty, + .security_token = token_global, + }; + return self; +} + +pub fn deinit(self: *Origin, app: *App) void { + // Call finalizers before releasing anything + { + var it = self.finalizer_callbacks.valueIterator(); + while (it.next()) |finalizer| { + finalizer.*.deinit(); + } + } + + v8.v8__Global__Reset(&self.security_token); + + { + var it = self.identity_map.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + } + + for (self.globals.items) |*global| { + v8.v8__Global__Reset(global); + } + + { + var it = self.temps.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + } + + app.arena_pool.release(self.arena); +} + +pub fn trackGlobal(self: *Origin, global: v8.Global) !void { + return self.globals.append(self.arena, global); +} + +pub fn trackTemp(self: *Origin, global: v8.Global) !void { + return self.temps.put(self.arena, global.data_ptr, global); +} + +pub fn releaseTemp(self: *Origin, global: v8.Global) void { + if (self.temps.fetchRemove(global.data_ptr)) |kv| { + var g = kv.value; + v8.v8__Global__Reset(&g); + } +} + +/// Release an item from the identity_map (called after finalizer runs from V8) +pub fn release(self: *Origin, item: *anyopaque) void { + var global = self.identity_map.fetchRemove(@intFromPtr(item)) orelse { + if (comptime IS_DEBUG) { + std.debug.assert(false); + } + return; + }; + v8.v8__Global__Reset(&global.value); + + // The item has been finalized, remove it from the finalizer callback so that + // we don't try to call it again on shutdown. + const kv = self.finalizer_callbacks.fetchRemove(@intFromPtr(item)) orelse { + if (comptime IS_DEBUG) { + std.debug.assert(false); + } + return; + }; + const fc = kv.value; + fc.session.releaseArena(fc.arena); +} + +pub fn createFinalizerCallback( + self: *Origin, + session: *Session, + global: v8.Global, + ptr: *anyopaque, + zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, +) !*FinalizerCallback { + const arena = try session.getArena(.{ .debug = "FinalizerCallback" }); + errdefer session.releaseArena(arena); + const fc = try arena.create(FinalizerCallback); + fc.* = .{ + .arena = arena, + .origin = self, + .session = session, + .ptr = ptr, + .global = global, + .zig_finalizer = zig_finalizer, + }; + return fc; +} + +pub fn transferTo(self: *Origin, dest: *Origin) !void { + const arena = dest.arena; + + try dest.globals.ensureUnusedCapacity(arena, self.globals.items.len); + for (self.globals.items) |obj| { + dest.globals.appendAssumeCapacity(obj); + } + self.globals.clearRetainingCapacity(); + + { + try dest.temps.ensureUnusedCapacity(arena, self.temps.count()); + var it = self.temps.iterator(); + while (it.next()) |kv| { + try dest.temps.put(arena, kv.key_ptr.*, kv.value_ptr.*); + } + self.temps.clearRetainingCapacity(); + } + + { + try dest.finalizer_callbacks.ensureUnusedCapacity(arena, self.finalizer_callbacks.count()); + var it = self.finalizer_callbacks.iterator(); + while (it.next()) |kv| { + kv.value_ptr.*.origin = dest; + try dest.finalizer_callbacks.put(arena, kv.key_ptr.*, kv.value_ptr.*); + } + self.finalizer_callbacks.clearRetainingCapacity(); + } + + { + try dest.identity_map.ensureUnusedCapacity(arena, self.identity_map.count()); + var it = self.identity_map.iterator(); + while (it.next()) |kv| { + try dest.identity_map.put(arena, kv.key_ptr.*, kv.value_ptr.*); + } + self.identity_map.clearRetainingCapacity(); + } +} + +// A type that has a finalizer can have its finalizer called one of two ways. +// The first is from V8 via the WeakCallback we give to weakRef. But that isn't +// guaranteed to fire, so we track this in finalizer_callbacks and call them on +// origin shutdown. +pub const FinalizerCallback = struct { + arena: Allocator, + origin: *Origin, + session: *Session, + ptr: *anyopaque, + global: v8.Global, + zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void, + + pub fn deinit(self: *FinalizerCallback) void { + self.zig_finalizer(self.ptr, self.session); + self.session.releaseArena(self.arena); + } +}; diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index afadbe82..372d2578 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -62,22 +62,25 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &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); + try ctx.trackGlobal(global); + return .{ .handle = global, .origin = {} }; } - return .{ .handle = global }; + try ctx.trackTemp(global); + return .{ .handle = global, .origin = ctx.origin }; } -pub const Temp = G(0); -pub const Global = G(1); +pub const Temp = G(.temp); +pub const Global = G(.global); -fn G(comptime discriminator: u8) type { +const GlobalType = enum(u8) { + temp, + global, +}; + +fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - - // makes the types different (G(0) != G(1)), without taking up space - comptime _: u8 = discriminator, + origin: if (global_type == .temp) *js.Origin else void, const Self = @This(); @@ -91,5 +94,9 @@ fn G(comptime discriminator: u8) type { .handle = @ptrCast(v8.v8__Global__Get(&self.handle, l.isolate.handle)), }; } + + pub fn release(self: *const Self) void { + self.origin.releaseTemp(self.handle); + } }; } diff --git a/src/browser/js/PromiseResolver.zig b/src/browser/js/PromiseResolver.zig index 183effee..f2aac0e0 100644 --- a/src/browser/js/PromiseResolver.zig +++ b/src/browser/js/PromiseResolver.zig @@ -79,7 +79,7 @@ pub fn persist(self: PromiseResolver) !Global { var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); - try ctx.global_promise_resolvers.append(ctx.arena, global); + try ctx.trackGlobal(global); return .{ .handle = global }; } diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index 7963ae7c..309bdb6b 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -259,11 +259,11 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); if (comptime is_global) { - try ctx.global_values.append(ctx.arena, global); - } else { - try ctx.global_values_temp.put(ctx.arena, global.data_ptr, global); + try ctx.trackGlobal(global); + return .{ .handle = global, .origin = {} }; } - return .{ .handle = global }; + try ctx.trackTemp(global); + return .{ .handle = global, .origin = ctx.origin }; } pub fn toZig(self: Value, comptime T: type) !T { @@ -310,15 +310,18 @@ pub fn format(self: Value, writer: *std.Io.Writer) !void { return js_str.format(writer); } -pub const Temp = G(0); -pub const Global = G(1); +pub const Temp = G(.temp); +pub const Global = G(.global); -fn G(comptime discriminator: u8) type { +const GlobalType = enum(u8) { + temp, + global, +}; + +fn G(comptime global_type: GlobalType) type { return struct { handle: v8.Global, - - // makes the types different (G(0) != G(1)), without taking up space - comptime _: u8 = discriminator, + origin: if (global_type == .temp) *js.Origin else void, const Self = @This(); @@ -336,5 +339,9 @@ fn G(comptime discriminator: u8) type { pub fn isEqual(self: *const Self, other: Value) bool { return v8.v8__Global__IsEqual(&self.handle, other.handle); } + + pub fn release(self: *const Self) void { + self.origin.releaseTemp(self.handle); + } }; } diff --git a/src/browser/js/bridge.zig b/src/browser/js/bridge.zig index fcbbea21..f95329dc 100644 --- a/src/browser/js/bridge.zig +++ b/src/browser/js/bridge.zig @@ -21,11 +21,13 @@ const js = @import("js.zig"); const lp = @import("lightpanda"); const log = @import("../../log.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const v8 = js.v8; const Caller = @import("Caller.zig"); const Context = @import("Context.zig"); +const Origin = @import("Origin.zig"); const IS_DEBUG = @import("builtin").mode == .Debug; @@ -104,24 +106,24 @@ pub fn Builder(comptime T: type) type { return entries; } - pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, page: *Page) void) Finalizer { + pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer { return .{ .from_zig = struct { - fn wrap(ptr: *anyopaque, page: *Page) void { - func(@ptrCast(@alignCast(ptr)), true, page); + fn wrap(ptr: *anyopaque, session: *Session) void { + func(@ptrCast(@alignCast(ptr)), true, session); } }.wrap, .from_v8 = struct { fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?; - const fc: *Context.FinalizerCallback = @ptrCast(@alignCast(ptr)); + const fc: *Origin.FinalizerCallback = @ptrCast(@alignCast(ptr)); - const ctx = fc.ctx; + const origin = fc.origin; const value_ptr = fc.ptr; - if (ctx.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { - func(@ptrCast(@alignCast(value_ptr)), false, ctx.page); - ctx.release(value_ptr); + if (origin.finalizer_callbacks.contains(@intFromPtr(value_ptr))) { + func(@ptrCast(@alignCast(value_ptr)), false, fc.session); + origin.release(value_ptr); } else { // A bit weird, but v8 _requires_ that we release it // If we don't. We'll 100% crash. @@ -413,12 +415,12 @@ pub const Property = struct { }; const Finalizer = struct { - // The finalizer wrapper when called fro Zig. This is only called on - // Context.deinit - from_zig: *const fn (ctx: *anyopaque, page: *Page) void, + // The finalizer wrapper when called from Zig. This is only called on + // Origin.deinit + from_zig: *const fn (ctx: *anyopaque, session: *Session) 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, + // (hence why we fallback to calling in Origin.deinit). 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, }; diff --git a/src/browser/js/js.zig b/src/browser/js/js.zig index 9415b717..0c196e5b 100644 --- a/src/browser/js/js.zig +++ b/src/browser/js/js.zig @@ -24,6 +24,7 @@ const string = @import("../../string.zig"); pub const Env = @import("Env.zig"); pub const bridge = @import("bridge.zig"); pub const Caller = @import("Caller.zig"); +pub const Origin = @import("Origin.zig"); pub const Context = @import("Context.zig"); pub const Local = @import("Local.zig"); pub const Inspector = @import("Inspector.zig"); @@ -161,7 +162,7 @@ pub fn ArrayBufferRef(comptime kind: ArrayType) type { var ctx = self.local.ctx; var global: v8.Global = undefined; v8.v8__Global__New(ctx.isolate.handle, self.handle, &global); - try ctx.global_values.append(ctx.arena, global); + try ctx.trackGlobal(global); return .{ .handle = global }; } diff --git a/src/browser/tests/frames/frames.html b/src/browser/tests/frames/frames.html index 4e614de9..97bed281 100644 --- a/src/browser/tests/frames/frames.html +++ b/src/browser/tests/frames/frames.html @@ -64,11 +64,12 @@ // child frame's top.parent is itself (root has no parent) testing.expectEqual(window, window[0].top.parent); - // Todo: Context security tokens - // testing.expectEqual(true, window.sub1_loaded); - // testing.expectEqual(true, window.sub2_loaded); - // testing.expectEqual(1, window.sub1_count); - // testing.expectEqual(2, window.sub2_count); + // Cross-frame property access + testing.expectEqual(true, window.sub1_loaded); + testing.expectEqual(true, window.sub2_loaded); + testing.expectEqual(1, window.sub1_count); + // depends on how far the initial load got before it was cancelled. + testing.expectEqual(true, window.sub2_count == 1 || window.sub2_count == 2); }); diff --git a/src/browser/tests/testing.js b/src/browser/tests/testing.js index 90434f0f..987ba042 100644 --- a/src/browser/tests/testing.js +++ b/src/browser/tests/testing.js @@ -118,7 +118,7 @@ BASE_URL: 'http://127.0.0.1:9582/src/browser/tests/', }; - if (!IS_TEST_RUNNER) { + if (window.navigator.userAgent.startsWith("Lightpanda/") == false) { // The page is running in a different browser. Probably a developer making sure // a test is correct. There are a few tweaks we need to do to make this a // seemless, namely around adapting paths/urls. diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index 2821e9f1..d70042f8 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const EventTarget = @import("EventTarget.zig"); const Node = @import("Node.zig"); const String = @import("../../string.zig").String; @@ -139,9 +140,9 @@ pub fn acquireRef(self: *Event) void { self._rc += 1; } -pub fn deinit(self: *Event, shutdown: bool, page: *Page) void { +pub fn deinit(self: *Event, shutdown: bool, session: *Session) void { if (shutdown) { - page.releaseArena(self._arena); + session.releaseArena(self._arena); return; } @@ -151,7 +152,7 @@ pub fn deinit(self: *Event, shutdown: bool, page: *Page) void { } if (rc == 1) { - page.releaseArena(self._arena); + session.releaseArena(self._arena); } else { self._rc = rc - 1; } diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 31f472c3..715c209a 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -59,7 +59,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool { event._is_trusted = false; event.acquireRef(); - defer event.deinit(false, page); + defer event.deinit(false, page._session); try page._event_manager.dispatch(self, event); return !event._cancelable or !event._prevent_default; } diff --git a/src/browser/webapi/FileReader.zig b/src/browser/webapi/FileReader.zig index 90e26aa0..da7ec71c 100644 --- a/src/browser/webapi/FileReader.zig +++ b/src/browser/webapi/FileReader.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const EventTarget = @import("EventTarget.zig"); const ProgressEvent = @import("event/ProgressEvent.zig"); const Blob = @import("Blob.zig"); @@ -69,17 +70,15 @@ pub fn init(page: *Page) !*FileReader { }); } -pub fn deinit(self: *FileReader, _: bool, page: *Page) void { - const js_ctx = page.js; +pub fn deinit(self: *FileReader, _: bool, session: *Session) void { + if (self._on_abort) |func| func.release(); + if (self._on_error) |func| func.release(); + if (self._on_load) |func| func.release(); + if (self._on_load_end) |func| func.release(); + if (self._on_load_start) |func| func.release(); + if (self._on_progress) |func| func.release(); - if (self._on_abort) |func| js_ctx.release(func); - if (self._on_error) |func| js_ctx.release(func); - if (self._on_load) |func| js_ctx.release(func); - if (self._on_load_end) |func| js_ctx.release(func); - if (self._on_load_start) |func| js_ctx.release(func); - if (self._on_progress) |func| js_ctx.release(func); - - page.releaseArena(self._arena); + session.releaseArena(self._arena); } fn asEventTarget(self: *FileReader) *EventTarget { diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 9d64de3c..74a5d79e 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -24,6 +24,7 @@ const IS_DEBUG = @import("builtin").mode == .Debug; const Allocator = std.mem.Allocator; const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const Element = @import("Element.zig"); const DOMRect = @import("DOMRect.zig"); @@ -91,13 +92,13 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I return self; } -pub fn deinit(self: *IntersectionObserver, shutdown: bool, page: *Page) void { - page.js.release(self._callback); +pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void { + self._callback.release(); if ((comptime IS_DEBUG) and !shutdown) { std.debug.assert(self._observing.items.len == 0); } - page.releaseArena(self._arena); + session.releaseArena(self._arena); } pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { @@ -137,7 +138,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi while (j < self._pending_entries.items.len) { if (self._pending_entries.items[j]._target == target) { const entry = self._pending_entries.swapRemove(j); - entry.deinit(false, page); + entry.deinit(false, page._session); } else { j += 1; } @@ -157,7 +158,7 @@ pub fn disconnect(self: *IntersectionObserver, page: *Page) void { self._previous_states.clearRetainingCapacity(); for (self._pending_entries.items) |entry| { - entry.deinit(false, page); + entry.deinit(false, page._session); } self._pending_entries.clearRetainingCapacity(); page.js.safeWeakRef(self); @@ -302,8 +303,8 @@ pub const IntersectionObserverEntry = struct { _intersection_ratio: f64, _is_intersecting: bool, - pub fn deinit(self: *const IntersectionObserverEntry, _: bool, page: *Page) void { - page.releaseArena(self._arena); + pub fn deinit(self: *IntersectionObserverEntry, _: bool, session: *Session) void { + session.releaseArena(self._arena); } pub fn getTarget(self: *const IntersectionObserverEntry) *Element { diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index a5dd15dd..b8608381 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -21,6 +21,7 @@ const String = @import("../../string.zig").String; const js = @import("../js/js.zig"); const Page = @import("../Page.zig"); +const Session = @import("../Session.zig"); const Node = @import("Node.zig"); const Element = @import("Element.zig"); const log = @import("../../log.zig"); @@ -84,13 +85,13 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver { return self; } -pub fn deinit(self: *MutationObserver, shutdown: bool, page: *Page) void { - page.js.release(self._callback); +pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void { + self._callback.release(); if ((comptime IS_DEBUG) and !shutdown) { std.debug.assert(self._observing.items.len == 0); } - page.releaseArena(self._arena); + session.releaseArena(self._arena); } pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { @@ -171,7 +172,7 @@ pub fn disconnect(self: *MutationObserver, page: *Page) void { page.unregisterMutationObserver(self); self._observing.clearRetainingCapacity(); for (self._pending_records.items) |record| { - record.deinit(false, page); + record.deinit(false, page._session); } self._pending_records.clearRetainingCapacity(); page.js.safeWeakRef(self); @@ -363,8 +364,8 @@ pub const MutationRecord = struct { characterData, }; - pub fn deinit(self: *const MutationRecord, _: bool, page: *Page) void { - page.releaseArena(self._arena); + pub fn deinit(self: *MutationRecord, _: bool, session: *Session) void { + session.releaseArena(self._arena); } pub fn getType(self: *const MutationRecord) []const u8 { diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 3bc6f586..fda7d2a5 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -243,11 +243,10 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 { var uuid_buf: [36]u8 = undefined; @import("../../id.zig").uuidv4(&uuid_buf); - const origin = (try page.getOrigin(page.call_arena)) orelse "null"; const blob_url = try std.fmt.allocPrint( page.arena, "blob:{s}/{s}", - .{ origin, uuid_buf }, + .{ page.origin orelse "null", uuid_buf }, ); try page._blob_urls.put(page.arena, blob_url, blob); return blob_url; diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 40265bb5..2bc9fbb0 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -646,9 +646,9 @@ const ScheduleCallback = struct { } fn deinit(self: *ScheduleCallback) void { - self.page.js.release(self.cb); + self.cb.release(); for (self.params) |param| { - self.page.js.release(param); + param.release(); } self.page.releaseArena(self.arena); } diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index 96143952..8d445733 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -20,6 +20,7 @@ const std = @import("std"); const log = @import("../../../log.zig"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const Allocator = std.mem.Allocator; @@ -61,8 +62,8 @@ pub fn init(page: *Page) !*Animation { return self; } -pub fn deinit(self: *Animation, _: bool, page: *Page) void { - page.releaseArena(self._arena); +pub fn deinit(self: *Animation, _: bool, session: *Session) void { + session.releaseArena(self._arena); } pub fn play(self: *Animation, page: *Page) !void { diff --git a/src/browser/webapi/collections/ChildNodes.zig b/src/browser/webapi/collections/ChildNodes.zig index 5ba6256a..9c2bde91 100644 --- a/src/browser/webapi/collections/ChildNodes.zig +++ b/src/browser/webapi/collections/ChildNodes.zig @@ -20,6 +20,7 @@ const std = @import("std"); const Node = @import("../Node.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const GenericIterator = @import("iterator.zig").Entry; // Optimized for node.childNodes, which has to be a live list. @@ -53,8 +54,8 @@ pub fn init(node: *Node, page: *Page) !*ChildNodes { return self; } -pub fn deinit(self: *const ChildNodes, page: *Page) void { - page.releaseArena(self._arena); +pub fn deinit(self: *const ChildNodes, session: *Session) void { + session.releaseArena(self._arena); } pub fn length(self: *ChildNodes, page: *Page) !u32 { diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index 0237a76c..a61cc598 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -21,6 +21,7 @@ const std = @import("std"); const log = @import("../../../log.zig"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const Node = @import("../Node.zig"); const ChildNodes = @import("ChildNodes.zig"); @@ -38,7 +39,7 @@ _data: union(enum) { }, _rc: usize = 0, -pub fn deinit(self: *NodeList, _: bool, page: *Page) void { +pub fn deinit(self: *NodeList, _: bool, session: *Session) void { const rc = self._rc; if (rc > 1) { self._rc = rc - 1; @@ -46,8 +47,8 @@ pub fn deinit(self: *NodeList, _: bool, page: *Page) void { } switch (self._data) { - .selector_list => |list| list.deinit(page), - .child_nodes => |cn| cn.deinit(page), + .selector_list => |list| list.deinit(session), + .child_nodes => |cn| cn.deinit(session), else => {}, } } @@ -118,8 +119,8 @@ const Iterator = struct { const Entry = struct { u32, *Node }; - pub fn deinit(self: *Iterator, shutdown: bool, page: *Page) void { - self.list.deinit(shutdown, page); + pub fn deinit(self: *Iterator, shutdown: bool, session: *Session) void { + self.list.deinit(shutdown, session); } pub fn acquireRef(self: *Iterator) void { diff --git a/src/browser/webapi/collections/iterator.zig b/src/browser/webapi/collections/iterator.zig index 3c43f3f8..9fe3354d 100644 --- a/src/browser/webapi/collections/iterator.zig +++ b/src/browser/webapi/collections/iterator.zig @@ -19,6 +19,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type { const R = reflect(Inner, field); @@ -39,9 +40,9 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type { return page._factory.create(Self{ .inner = inner }); } - pub fn deinit(self: *Self, shutdown: bool, page: *Page) void { + pub fn deinit(self: *Self, shutdown: bool, session: *Session) void { if (@hasDecl(Inner, "deinit")) { - self.inner.deinit(shutdown, page); + self.inner.deinit(shutdown, session); } } diff --git a/src/browser/webapi/css/FontFace.zig b/src/browser/webapi/css/FontFace.zig index f824259a..f3c4059d 100644 --- a/src/browser/webapi/css/FontFace.zig +++ b/src/browser/webapi/css/FontFace.zig @@ -19,6 +19,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const Allocator = std.mem.Allocator; @@ -41,8 +42,8 @@ pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace { return self; } -pub fn deinit(self: *FontFace, _: bool, page: *Page) void { - page.releaseArena(self._arena); +pub fn deinit(self: *FontFace, _: bool, session: *Session) void { + session.releaseArena(self._arena); } pub fn getFamily(self: *const FontFace) []const u8 { diff --git a/src/browser/webapi/css/FontFaceSet.zig b/src/browser/webapi/css/FontFaceSet.zig index 6e5cd941..2a4a000d 100644 --- a/src/browser/webapi/css/FontFaceSet.zig +++ b/src/browser/webapi/css/FontFaceSet.zig @@ -19,6 +19,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const FontFace = @import("FontFace.zig"); const Allocator = std.mem.Allocator; @@ -38,8 +39,8 @@ pub fn init(page: *Page) !*FontFaceSet { return self; } -pub fn deinit(self: *FontFaceSet, _: bool, page: *Page) void { - page.releaseArena(self._arena); +pub fn deinit(self: *FontFaceSet, _: bool, session: *Session) void { + session.releaseArena(self._arena); } // FontFaceSet.ready - returns an already-resolved Promise. diff --git a/src/browser/webapi/encoding/TextDecoder.zig b/src/browser/webapi/encoding/TextDecoder.zig index 4f23cfcc..736a4008 100644 --- a/src/browser/webapi/encoding/TextDecoder.zig +++ b/src/browser/webapi/encoding/TextDecoder.zig @@ -20,6 +20,7 @@ const std = @import("std"); const js = @import("../../js/js.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const Allocator = std.mem.Allocator; const TextDecoder = @This(); @@ -59,8 +60,8 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder { return self; } -pub fn deinit(self: *TextDecoder, _: bool, page: *Page) void { - page.releaseArena(self._arena); +pub fn deinit(self: *TextDecoder, _: bool, session: *Session) void { + session.releaseArena(self._arena); } pub fn getIgnoreBOM(self: *const TextDecoder) bool { diff --git a/src/browser/webapi/event/CompositionEvent.zig b/src/browser/webapi/event/CompositionEvent.zig index b98fdd6f..a761e3ba 100644 --- a/src/browser/webapi/event/CompositionEvent.zig +++ b/src/browser/webapi/event/CompositionEvent.zig @@ -20,6 +20,7 @@ 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"); const Allocator = std.mem.Allocator; @@ -53,8 +54,8 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent { return event; } -pub fn deinit(self: *CompositionEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *CompositionEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *CompositionEvent) *Event { diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index e303d901..eaba2e7d 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -21,6 +21,7 @@ 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"); const Allocator = std.mem.Allocator; @@ -72,11 +73,11 @@ pub fn initCustomEvent( self._detail = detail_; } -pub fn deinit(self: *CustomEvent, shutdown: bool, page: *Page) void { +pub fn deinit(self: *CustomEvent, shutdown: bool, session: *Session) void { if (self._detail) |d| { - page.js.release(d); + d.release(); } - self._proto.deinit(shutdown, page); + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *CustomEvent) *Event { diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index 5dd12a26..2d3b857f 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -21,6 +21,7 @@ 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"); const Allocator = std.mem.Allocator; @@ -79,11 +80,11 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool return event; } -pub fn deinit(self: *ErrorEvent, shutdown: bool, page: *Page) void { +pub fn deinit(self: *ErrorEvent, shutdown: bool, session: *Session) void { if (self._error) |e| { - page.js.release(e); + e.release(); } - self._proto.deinit(shutdown, page); + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *ErrorEvent) *Event { diff --git a/src/browser/webapi/event/FocusEvent.zig b/src/browser/webapi/event/FocusEvent.zig index 37065936..f6823c23 100644 --- a/src/browser/webapi/event/FocusEvent.zig +++ b/src/browser/webapi/event/FocusEvent.zig @@ -20,6 +20,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const String = @import("../../../string.zig").String; const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const js = @import("../../js/js.zig"); const Event = @import("../Event.zig"); @@ -69,8 +70,8 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *FocusEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *FocusEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *FocusEvent) *Event { diff --git a/src/browser/webapi/event/KeyboardEvent.zig b/src/browser/webapi/event/KeyboardEvent.zig index 6e391812..1d4494b3 100644 --- a/src/browser/webapi/event/KeyboardEvent.zig +++ b/src/browser/webapi/event/KeyboardEvent.zig @@ -21,6 +21,7 @@ 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"); const UIEvent = @import("UIEvent.zig"); @@ -221,8 +222,8 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *KeyboardEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *KeyboardEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *KeyboardEvent) *Event { diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index 34e04518..32ced1d8 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -22,6 +22,7 @@ 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"); const Window = @import("../Window.zig"); const Allocator = std.mem.Allocator; @@ -72,11 +73,11 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool return event; } -pub fn deinit(self: *MessageEvent, shutdown: bool, page: *Page) void { +pub fn deinit(self: *MessageEvent, shutdown: bool, session: *Session) void { if (self._data) |d| { - page.js.release(d); + d.release(); } - self._proto.deinit(shutdown, page); + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *MessageEvent) *Event { diff --git a/src/browser/webapi/event/MouseEvent.zig b/src/browser/webapi/event/MouseEvent.zig index c94a37d1..6b032433 100644 --- a/src/browser/webapi/event/MouseEvent.zig +++ b/src/browser/webapi/event/MouseEvent.zig @@ -19,6 +19,7 @@ const std = @import("std"); const String = @import("../../../string.zig").String; const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const js = @import("../../js/js.zig"); const Event = @import("../Event.zig"); @@ -109,8 +110,8 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*MouseEvent { return event; } -pub fn deinit(self: *MouseEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *MouseEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *MouseEvent) *Event { diff --git a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig index 59e32b06..98cba330 100644 --- a/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig +++ b/src/browser/webapi/event/NavigationCurrentEntryChangeEvent.zig @@ -21,6 +21,7 @@ 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"); const NavigationHistoryEntry = @import("../navigation/NavigationHistoryEntry.zig"); @@ -82,8 +83,8 @@ fn initWithTrusted( return event; } -pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event { diff --git a/src/browser/webapi/event/PageTransitionEvent.zig b/src/browser/webapi/event/PageTransitionEvent.zig index eceab4f2..bf747c9a 100644 --- a/src/browser/webapi/event/PageTransitionEvent.zig +++ b/src/browser/webapi/event/PageTransitionEvent.zig @@ -21,6 +21,7 @@ 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"); const Allocator = std.mem.Allocator; @@ -65,8 +66,8 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *PageTransitionEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *PageTransitionEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *PageTransitionEvent) *Event { diff --git a/src/browser/webapi/event/PointerEvent.zig b/src/browser/webapi/event/PointerEvent.zig index 82f4874f..c5178faf 100644 --- a/src/browser/webapi/event/PointerEvent.zig +++ b/src/browser/webapi/event/PointerEvent.zig @@ -21,6 +21,7 @@ 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"); const MouseEvent = @import("MouseEvent.zig"); @@ -127,8 +128,8 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent { return event; } -pub fn deinit(self: *PointerEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *PointerEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *PointerEvent) *Event { diff --git a/src/browser/webapi/event/PopStateEvent.zig b/src/browser/webapi/event/PopStateEvent.zig index 45774998..f26c17b6 100644 --- a/src/browser/webapi/event/PopStateEvent.zig +++ b/src/browser/webapi/event/PopStateEvent.zig @@ -21,6 +21,7 @@ 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"); const Allocator = std.mem.Allocator; @@ -66,8 +67,8 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *PopStateEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *PopStateEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *PopStateEvent) *Event { diff --git a/src/browser/webapi/event/ProgressEvent.zig b/src/browser/webapi/event/ProgressEvent.zig index a78982a1..b257f12b 100644 --- a/src/browser/webapi/event/ProgressEvent.zig +++ b/src/browser/webapi/event/ProgressEvent.zig @@ -20,6 +20,7 @@ const std = @import("std"); const String = @import("../../../string.zig").String; const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const Event = @import("../Event.zig"); const Allocator = std.mem.Allocator; @@ -67,8 +68,8 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool return event; } -pub fn deinit(self: *ProgressEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *ProgressEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *ProgressEvent) *Event { diff --git a/src/browser/webapi/event/PromiseRejectionEvent.zig b/src/browser/webapi/event/PromiseRejectionEvent.zig index 957228df..f0a195b9 100644 --- a/src/browser/webapi/event/PromiseRejectionEvent.zig +++ b/src/browser/webapi/event/PromiseRejectionEvent.zig @@ -20,6 +20,7 @@ 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"); const Allocator = std.mem.Allocator; @@ -56,14 +57,14 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEve return event; } -pub fn deinit(self: *PromiseRejectionEvent, shutdown: bool, page: *Page) void { +pub fn deinit(self: *PromiseRejectionEvent, shutdown: bool, session: *Session) void { if (self._reason) |r| { - page.js.release(r); + r.release(); } if (self._promise) |p| { - page.js.release(p); + p.release(); } - self._proto.deinit(shutdown, page); + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *PromiseRejectionEvent) *Event { diff --git a/src/browser/webapi/event/TextEvent.zig b/src/browser/webapi/event/TextEvent.zig index 54789c13..fd5e32fb 100644 --- a/src/browser/webapi/event/TextEvent.zig +++ b/src/browser/webapi/event/TextEvent.zig @@ -19,6 +19,7 @@ const std = @import("std"); const String = @import("../../../string.zig").String; const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const js = @import("../../js/js.zig"); const Event = @import("../Event.zig"); @@ -58,8 +59,8 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*TextEvent { return event; } -pub fn deinit(self: *TextEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *TextEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *TextEvent) *Event { diff --git a/src/browser/webapi/event/UIEvent.zig b/src/browser/webapi/event/UIEvent.zig index 6d329221..0aa2943b 100644 --- a/src/browser/webapi/event/UIEvent.zig +++ b/src/browser/webapi/event/UIEvent.zig @@ -18,6 +18,7 @@ const String = @import("../../../string.zig").String; const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const js = @import("../../js/js.zig"); const Event = @import("../Event.zig"); @@ -69,8 +70,8 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent { return event; } -pub fn deinit(self: *UIEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *UIEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn as(self: *UIEvent, comptime T: type) *T { diff --git a/src/browser/webapi/event/WheelEvent.zig b/src/browser/webapi/event/WheelEvent.zig index ee725941..831c4e02 100644 --- a/src/browser/webapi/event/WheelEvent.zig +++ b/src/browser/webapi/event/WheelEvent.zig @@ -19,6 +19,7 @@ const std = @import("std"); const String = @import("../../../string.zig").String; const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const js = @import("../../js/js.zig"); const Event = @import("../Event.zig"); @@ -86,8 +87,8 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*WheelEvent { return event; } -pub fn deinit(self: *WheelEvent, shutdown: bool, page: *Page) void { - self._proto.deinit(shutdown, page); +pub fn deinit(self: *WheelEvent, shutdown: bool, session: *Session) void { + self._proto.deinit(shutdown, session); } pub fn asEvent(self: *WheelEvent) *Event { diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index 699cc9c4..9b0f2f98 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -45,7 +45,7 @@ pub const InitOpts = Request.InitOpts; pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise { const request = try Request.init(input, options, page); const response = try Response.init(null, .{ .status = 0 }, page); - errdefer response.deinit(true, page); + errdefer response.deinit(true, page._session); const resolver = page.js.local.?.createPromiseResolver(); @@ -184,7 +184,7 @@ fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { // clear this. (defer since `self is in the response's arena). defer if (self._owns_response) { - response.deinit(err == error.Abort, self._page); + response.deinit(err == error.Abort, self._page._session); self._owns_response = false; }; @@ -205,7 +205,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void { if (self._owns_response) { var response = self._response; response._transfer = null; - response.deinit(true, self._page); + response.deinit(true, self._page._session); // Do not access `self` after this point: the Fetch struct was // allocated from response._arena which has been released. } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 13048f33..6a926369 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -21,6 +21,7 @@ const js = @import("../../js/js.zig"); const HttpClient = @import("../../HttpClient.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const Headers = @import("Headers.zig"); const ReadableStream = @import("../streams/ReadableStream.zig"); const Blob = @import("../Blob.zig"); @@ -77,7 +78,7 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response { return self; } -pub fn deinit(self: *Response, shutdown: bool, page: *Page) void { +pub fn deinit(self: *Response, shutdown: bool, session: *Session) void { if (self._transfer) |transfer| { if (shutdown) { transfer.terminate(); @@ -86,7 +87,7 @@ pub fn deinit(self: *Response, shutdown: bool, page: *Page) void { } self._transfer = null; } - page.releaseArena(self._arena); + session.releaseArena(self._arena); } pub fn getStatus(self: *const Response) u16 { diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index b9f053a8..d8d5e369 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -26,6 +26,8 @@ const net_http = @import("../../../network/http.zig"); const URL = @import("../../URL.zig"); const Mime = @import("../../Mime.zig"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); + const Node = @import("../Node.zig"); const Event = @import("../Event.zig"); const Headers = @import("Headers.zig"); @@ -93,7 +95,7 @@ pub fn init(page: *Page) !*XMLHttpRequest { }); } -pub fn deinit(self: *XMLHttpRequest, shutdown: bool, page: *Page) void { +pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void { if (self._transfer) |transfer| { if (shutdown) { transfer.terminate(); @@ -103,37 +105,36 @@ pub fn deinit(self: *XMLHttpRequest, shutdown: bool, page: *Page) void { self._transfer = null; } - const js_ctx = page.js; if (self._on_ready_state_change) |func| { - js_ctx.release(func); + func.release(); } { const proto = self._proto; if (proto._on_abort) |func| { - js_ctx.release(func); + func.release(); } if (proto._on_error) |func| { - js_ctx.release(func); + func.release(); } if (proto._on_load) |func| { - js_ctx.release(func); + func.release(); } if (proto._on_load_end) |func| { - js_ctx.release(func); + func.release(); } if (proto._on_load_start) |func| { - js_ctx.release(func); + func.release(); } if (proto._on_progress) |func| { - js_ctx.release(func); + func.release(); } if (proto._on_timeout) |func| { - js_ctx.release(func); + func.release(); } } - page.releaseArena(self._arena); + session.releaseArena(self._arena); } fn asEventTarget(self: *XMLHttpRequest) *EventTarget { diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 04055910..1061320d 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -19,6 +19,7 @@ const std = @import("std"); const Page = @import("../../Page.zig"); +const Session = @import("../../Session.zig"); const Node = @import("../Node.zig"); const Part = @import("Selector.zig").Part; @@ -40,8 +41,8 @@ pub const EntryIterator = GenericIterator(Iterator, null); pub const KeyIterator = GenericIterator(Iterator, "0"); pub const ValueIterator = GenericIterator(Iterator, "1"); -pub fn deinit(self: *const List, page: *Page) void { - page.releaseArena(self._arena); +pub fn deinit(self: *const List, session: *Session) void { + session.releaseArena(self._arena); } pub fn collect( diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index 34c58e3a..1260217b 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -406,7 +406,7 @@ test "cdp Node: search list" { { const l1 = try doc.querySelectorAll(.wrap("a"), page); - defer l1.deinit(page); + defer l1.deinit(page._session); const s1 = try search_list.create(l1._nodes); try testing.expectEqual("1", s1.name); try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); @@ -417,7 +417,7 @@ test "cdp Node: search list" { { const l2 = try doc.querySelectorAll(.wrap("#a1"), page); - defer l2.deinit(page); + defer l2.deinit(page._session); const s2 = try search_list.create(l2._nodes); try testing.expectEqual("2", s2.name); try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); @@ -425,7 +425,7 @@ test "cdp Node: search list" { { const l3 = try doc.querySelectorAll(.wrap("#a2"), page); - defer l3.deinit(page); + defer l3.deinit(page._session); const s3 = try search_list.create(l3._nodes); try testing.expectEqual("3", s3.name); try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index 2f2befa5..df1d37c2 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -98,7 +98,7 @@ fn performSearch(cmd: anytype) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded; const list = try Selector.querySelectorAll(page.window._document.asNode(), params.query, page); - defer list.deinit(page); + defer list.deinit(page._session); const search = try bc.node_search_list.create(list._nodes); @@ -249,7 +249,7 @@ fn querySelectorAll(cmd: anytype) !void { }; const selected_nodes = try Selector.querySelectorAll(node.dom, params.selector, page); - defer selected_nodes.deinit(page); + defer selected_nodes.deinit(page._session); const nodes = selected_nodes._nodes; diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index a96ac2b6..6e406c05 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -414,7 +414,7 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P bc.inspector_session.inspector.contextCreated( &ls.local, "", - try page.getOrigin(arena) orelse "", + page.origin orelse "", aux_data, true, ); diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 1ca9d120..53d29757 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -92,7 +92,8 @@ const ToolStreamingText = struct { }, .links => { if (Selector.querySelectorAll(self.page.document.asNode(), "a[href]", self.page)) |list| { - defer list.deinit(self.page); + defer list.deinit(self.page._session); + var first = true; for (list._nodes) |node| { if (node.is(Element.Html.Anchor)) |anchor| { diff --git a/src/testing.zig b/src/testing.zig index a398f824..774f76e4 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -414,15 +414,6 @@ fn runWebApiTest(test_file: [:0]const u8) !void { try_catch.init(&ls.local); defer try_catch.deinit(); - // by default, on load, testing.js will call testing.assertOk(). This makes our - // tests work well in a browser. But, for our test runner, we disable that - // and call it explicitly. This gives us better error messages. - ls.local.eval("window._lightpanda_skip_auto_assert = true;", "auto_assert") catch |err| { - const caught = try_catch.caughtOrError(arena_allocator, err); - std.debug.print("disable auto assert failure\nError: {f}\n", .{caught}); - return err; - }; - try page.navigate(url, .{}); _ = test_session.wait(2000);