From 550fb58f3ffc60819c12c818115c4e3d78ea5055 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Wed, 22 Apr 2026 20:38:50 +0800 Subject: [PATCH] Introduce Page (container) Follow up to https://github.com/lightpanda-io/browser/pull/2200 This change is actually pretty mundane, but a bunch of files that used to take a *Session (e.g. every WebAPI releaseRef and deinit) now take a *Page. This aims to separate the 2 lifetimes currently managed by Session by moving the "Page" lifetime to a dedicated container: Page. Ultimately, the goal is to remove the 1-page-per-session limit of the current design. Not to explicitly support multiple pages per session (though, that's more possible now), but in order to better emulate Chrome where, during a navigation event, the old and new page both exist. --- src/SemanticTree.zig | 4 +- src/browser/EventManager.zig | 6 +- src/browser/EventManagerBase.zig | 6 +- src/browser/Frame.zig | 64 ++-- src/browser/Page.zig | 232 +++++++++++++ src/browser/Runner.zig | 20 +- src/browser/ScriptManager.zig | 2 +- src/browser/Session.zig | 323 ++++++------------ src/browser/actions.zig | 6 +- src/browser/forms.zig | 24 +- src/browser/interactive.zig | 4 +- src/browser/js/Caller.zig | 33 +- src/browser/js/Context.zig | 22 +- src/browser/js/Env.zig | 16 +- src/browser/js/Function.zig | 2 +- src/browser/js/Local.zig | 54 +-- src/browser/js/Promise.zig | 2 +- src/browser/js/String.zig | 2 +- src/browser/js/Value.zig | 2 +- src/browser/links.zig | 2 +- src/browser/markdown.zig | 8 +- src/browser/structured_data.zig | 4 +- src/browser/webapi/AbortSignal.zig | 2 +- src/browser/webapi/AbstractRange.zig | 13 +- src/browser/webapi/Blob.zig | 23 +- src/browser/webapi/Document.zig | 6 +- src/browser/webapi/Event.zig | 23 +- src/browser/webapi/EventTarget.zig | 8 +- src/browser/webapi/File.zig | 7 +- src/browser/webapi/FileReader.zig | 10 +- src/browser/webapi/IntersectionObserver.zig | 24 +- src/browser/webapi/MessagePort.zig | 2 +- src/browser/webapi/MutationObserver.zig | 18 +- src/browser/webapi/Permissions.zig | 11 +- src/browser/webapi/Selection.zig | 14 +- src/browser/webapi/TreeWalker.zig | 4 +- src/browser/webapi/URL.zig | 2 +- src/browser/webapi/Window.zig | 24 +- src/browser/webapi/Worker.zig | 10 +- src/browser/webapi/WorkerGlobalScope.zig | 25 +- src/browser/webapi/animation/Animation.zig | 12 +- src/browser/webapi/canvas/OffscreenCanvas.zig | 2 +- src/browser/webapi/collections/ChildNodes.zig | 9 +- src/browser/webapi/collections/NodeList.zig | 21 +- src/browser/webapi/collections/iterator.zig | 14 +- src/browser/webapi/css/FontFace.zig | 11 +- src/browser/webapi/css/FontFaceSet.zig | 21 +- src/browser/webapi/element/html/Input.zig | 4 +- src/browser/webapi/element/html/Media.zig | 2 +- src/browser/webapi/element/html/TextArea.zig | 4 +- src/browser/webapi/encoding/TextDecoder.zig | 18 +- src/browser/webapi/event/CustomEvent.zig | 22 +- src/browser/webapi/event/ErrorEvent.zig | 34 +- src/browser/webapi/event/MessageEvent.zig | 32 +- .../webapi/event/PromiseRejectionEvent.zig | 22 +- src/browser/webapi/net/Fetch.zig | 6 +- src/browser/webapi/net/Request.zig | 2 +- src/browser/webapi/net/Response.zig | 16 +- src/browser/webapi/net/WebSocket.zig | 43 +-- src/browser/webapi/net/XMLHttpRequest.zig | 14 +- src/browser/webapi/selector/List.zig | 6 +- src/cdp/AXNode.zig | 4 +- src/cdp/CDP.zig | 2 +- src/cdp/Node.zig | 12 +- src/cdp/domains/dom.zig | 4 +- src/cdp/domains/lp.zig | 10 +- src/cdp/domains/network.zig | 4 +- src/cdp/domains/page.zig | 10 +- src/cdp/domains/target.zig | 16 +- src/cdp/testing.zig | 4 +- src/lightpanda.zig | 16 +- src/main_legacy_test.zig | 4 +- src/mcp/tools.zig | 10 +- src/testing.zig | 8 +- 74 files changed, 819 insertions(+), 664 deletions(-) create mode 100644 src/browser/Page.zig diff --git a/src/SemanticTree.zig b/src/SemanticTree.zig index 6ed57d25..47220745 100644 --- a/src/SemanticTree.zig +++ b/src/SemanticTree.zig @@ -726,7 +726,7 @@ test "SemanticTree backendDOMNodeId" { var frame = try testing.pageTest("cdp/registry1.html", .{}); defer testing.reset(); - defer frame._session.removeFrame(); + defer frame._session.removePage(); const st: Self = .{ .dom_node = frame.window._document.asNode(), @@ -750,7 +750,7 @@ test "SemanticTree max_depth" { var frame = try testing.pageTest("cdp/registry1.html", .{}); defer testing.reset(); - defer frame._session.removeFrame(); + defer frame._session.removePage(); const st: Self = .{ .dom_node = frame.window._document.asNode(), diff --git a/src/browser/EventManager.zig b/src/browser/EventManager.zig index 994550e9..cabc56a3 100644 --- a/src/browser/EventManager.zig +++ b/src/browser/EventManager.zig @@ -104,7 +104,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.releaseRef(self.frame._session); + defer _ = event.releaseRef(self.frame._page); // Increment event count for Event Timing API self.frame.window._performance._event_counts.increment(event._type_string.str()); @@ -138,7 +138,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event, window._current_event = event; defer window._current_event = prev_event; - try self.base.dispatchDirect(frame.call_arena, frame.js, target, event, handler, frame._session, opts); + try self.base.dispatchDirect(frame.call_arena, frame.js, target, event, handler, frame._page, opts); } /// Check if there are any listeners for a direct dispatch (non-DOM target). @@ -617,7 +617,7 @@ const ActivationState = struct { const event = try Event.initTrusted(comptime .wrap(typ), .{ .bubbles = true, .cancelable = false, - }, frame._session); + }, frame._page); const target = input.asElement().asEventTarget(); try frame._event_manager.dispatch(target, event); diff --git a/src/browser/EventManagerBase.zig b/src/browser/EventManagerBase.zig index 055ea213..8e13ecd5 100644 --- a/src/browser/EventManagerBase.zig +++ b/src/browser/EventManagerBase.zig @@ -21,7 +21,7 @@ const lp = @import("lightpanda"); const builtin = @import("builtin"); const js = @import("js/js.zig"); -const Session = @import("Session.zig"); +const Page = @import("Page.zig"); const Event = @import("webapi/Event.zig"); const EventTarget = @import("webapi/EventTarget.zig"); @@ -216,7 +216,7 @@ pub fn dispatchDirect( target: *EventTarget, event: *Event, handler: anytype, - session: *Session, + page: *Page, comptime opts: DispatchDirectOptions, ) DispatchError!void { if (comptime IS_DEBUG) { @@ -224,7 +224,7 @@ pub fn dispatchDirect( } event.acquireRef(); - defer _ = event.releaseRef(session); + defer _ = event.releaseRef(page); if (comptime opts.inject_target) { event._target = target; diff --git a/src/browser/Frame.zig b/src/browser/Frame.zig index 8f1b120b..7994abb4 100644 --- a/src/browser/Frame.zig +++ b/src/browser/Frame.zig @@ -22,6 +22,7 @@ const builtin = @import("builtin"); const JS = @import("js/js.zig"); const Mime = @import("Mime.zig"); +const Page = @import("Page.zig"); const Factory = @import("Factory.zig"); const Session = @import("Session.zig"); const EventManager = @import("EventManager.zig"); @@ -87,6 +88,8 @@ _frame_id: u32, // navigate. _loader_id: u32, +_page: *Page, + _session: *Session, _event_manager: EventManager, @@ -255,35 +258,39 @@ _type: enum { root, frame }, // only used for logs right now _req_id: u32 = 0, _navigated_options: ?NavigatedOpts = null, -pub fn init(self: *Frame, frame_id: u32, session: *Session, parent: ?*Frame) !void { +pub fn init(self: *Frame, frame_id: u32, page: *Page, parent: ?*Frame) !void { if (comptime IS_DEBUG) { log.debug(.frame, "frame.init", .{}); } + const session = page.session; const call_arena = try session.getArena(.medium, "call_arena"); errdefer session.releaseArena(call_arena); - const factory = &session.factory; + const factory = &page.factory; const document = (try factory.document(Node.Document.HTMLDocument{ ._proto = undefined, })).asDocument(); + const arena = page.frame_arena; + self.* = .{ .js = undefined, + .arena = arena, .parent = parent, - .arena = session.frame_arena, .document = document, .window = undefined, .call_arena = call_arena, ._frame_id = frame_id, - ._loader_id = session.nextLoaderId(), + ._page = page, ._session = session, + ._loader_id = session.nextLoaderId(), ._factory = factory, ._pending_loads = 1, // always 1 for the ScriptManager ._type = if (parent == null) .root else .frame, ._style_manager = undefined, ._script_manager = undefined, - ._event_manager = EventManager.init(session.frame_arena, self), + ._event_manager = EventManager.init(arena, self), }; self._to_load = &self._to_load_1; @@ -322,8 +329,8 @@ pub fn init(self: *Frame, frame_id: u32, session: *Session, parent: ?*Frame) !vo errdefer self._script_manager.deinit(); self.js = try browser.env.createContext(self, .{ - .identity = &session.identity, - .identity_arena = session.frame_arena, + .identity = &page.identity, + .identity_arena = arena, .call_arena = self.call_arena, }); errdefer browser.env.destroyContext(self.js); @@ -363,10 +370,10 @@ pub fn deinit(self: *Frame, abort_http: bool) void { // stats.print(&stream) catch unreachable; } - const session = self._session; + const page = self._page; if (self._queued_navigation) |qn| { - session.releaseArena(qn.arena); + page.releaseArena(qn.arena); } { @@ -374,7 +381,7 @@ pub fn deinit(self: *Frame, abort_http: bool) void { { var it = self._blob_urls.valueIterator(); while (it.next()) |blob| { - blob.*.releaseRef(session); + blob.*.releaseRef(page); } } @@ -383,39 +390,40 @@ pub fn deinit(self: *Frame, abort_http: bool) void { while (node) |n| { node = n.next; // capture before we potentially delete observer const observer: *MutationObserver = @fieldParentPtr("node", n); - observer.releaseRef(session); + observer.releaseRef(page); } } for (self._intersection_observers.items) |observer| { - observer.releaseRef(session); + observer.releaseRef(page); } var document = self.window._document; - document._selection.releaseRef(session); + document._selection.releaseRef(page); if (document._fonts) |f| { - f.releaseRef(session); + f.releaseRef(page); } } - session.browser.env.destroyContext(self.js); + const browser = page.session.browser; + browser.env.destroyContext(self.js); self._script_manager.shutdown = true; if (self.parent == null) { - session.browser.http_client.abort(); + browser.http_client.abort(); } else if (abort_http) { // a small optimization, it's faster to abort _everything_ on the root // frame, so we prefer that. But if it's just the frame that's going // away (a frame navigation) then we'll abort the frame-related requests - session.browser.http_client.abortFrame(self._frame_id); + browser.http_client.abortFrame(self._frame_id); } self._script_manager.deinit(); self._style_manager.deinit(); - session.releaseArena(self.call_arena); + page.releaseArena(self.call_arena); } pub fn trackWorker(self: *Frame, worker: *Worker) !void { @@ -807,7 +815,7 @@ pub fn documentIsLoaded(self: *Frame) void { } pub fn _documentIsLoaded(self: *Frame) !void { - const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self._session); + const event = try Event.initTrusted(.wrap("DOMContentLoaded"), .{ .bubbles = true }, self._page); try self._event_manager.dispatch( self.document.asEventTarget(), event, @@ -834,7 +842,7 @@ pub fn iframeCompletedLoading(self: *Frame, iframe: *IFrame) void { defer entered.exit(); blk: { - const event = Event.initTrusted(comptime .wrap("load"), .{}, self._session) catch |err| { + const event = Event.initTrusted(comptime .wrap("load"), .{}, self._page) catch |err| { log.err(.frame, "iframe event init", .{ .err = err, .url = iframe._src }); break :blk; }; @@ -888,7 +896,7 @@ fn _documentIsComplete(self: *Frame) !void { // Dispatch window.load event. const window_target = self.window.asEventTarget(); if (self._event_manager.hasDirectListeners(window_target, "load", self.window._on_load)) { - const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._session); + const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._page); // This event is weird, it's dispatched directly on the window, but // with the document as the target. event._target = self.document.asEventTarget(); @@ -1213,7 +1221,7 @@ pub fn iframeAddedCallback(self: *Frame, iframe: *IFrame) !void { const new_frame = try self.arena.create(Frame); const frame_id = session.nextFrameId(); - try Frame.init(new_frame, frame_id, session, self); + try Frame.init(new_frame, frame_id, self._page, self); errdefer new_frame.deinit(true); self._pending_loads += 1; @@ -1431,7 +1439,7 @@ pub fn registerMutationObserver(self: *Frame, observer: *MutationObserver) !void } pub fn unregisterMutationObserver(self: *Frame, observer: *MutationObserver) void { - observer.releaseRef(self._session); + observer.releaseRef(self._page); self._mutation_observers.remove(&observer.node); } @@ -1443,7 +1451,7 @@ pub fn registerIntersectionObserver(self: *Frame, observer: *IntersectionObserve pub fn unregisterIntersectionObserver(self: *Frame, observer: *IntersectionObserver) void { for (self._intersection_observers.items, 0..) |obs, i| { if (obs == observer) { - observer.releaseRef(self._session); + observer.releaseRef(self._page); _ = self._intersection_observers.swapRemove(i); return; } @@ -1468,7 +1476,7 @@ pub fn dispatchLoad(self: *Frame) !void { for (to_process.items) |html_element| { if (has_dom_load_listener or html_element.hasAttributeFunction(.onload, self)) { - const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._session); + const event = try Event.initTrusted(comptime .wrap("load"), .{}, self._page); try self._event_manager.dispatch(html_element.asEventTarget(), event); } } @@ -1579,7 +1587,7 @@ pub fn deliverSlotchangeEvents(self: *Frame) void { self._slots_pending_slotchange.clearRetainingCapacity(); for (slots) |slot| { - const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self._session) catch |err| { + const event = Event.initTrusted(comptime .wrap("slotchange"), .{ .bubbles = true }, self._page) catch |err| { log.err(.frame, "deliverSlotchange.init", .{ .err = err, .type = self._type, .url = self.url }); continue; }; @@ -3520,7 +3528,7 @@ pub fn handleClick(self: *Frame, target: *Node) !void { pub fn triggerKeyboard(self: *Frame, keyboard_event: *KeyboardEvent) !void { const event = keyboard_event.asEvent(); const element = self.window._document._active_element orelse { - event.deinit(self._session); + event.deinit(self._page); return; }; @@ -3616,7 +3624,7 @@ pub fn submitForm(self: *Frame, submitter_: ?*Element, form_: ?*Element.Html.For // so submit_event is still valid when we check _prevent_default submit_event.acquireRef(); - defer _ = submit_event.releaseRef(self._session); + defer _ = submit_event.releaseRef(self._page); try self._event_manager.dispatch(form_element.asEventTarget(), submit_event); // If the submit event was prevented, don't submit the form diff --git a/src/browser/Page.zig b/src/browser/Page.zig new file mode 100644 index 00000000..af58f34a --- /dev/null +++ b/src/browser/Page.zig @@ -0,0 +1,232 @@ +// Copyright (C) 2023-2026 Lightpanda (Selecy SAS) +// +// Francis Bouvier +// Pierre Tachoire +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program. If not, see . + +const std = @import("std"); +const lp = @import("lightpanda"); +const builtin = @import("builtin"); + +const App = @import("../App.zig"); + +const js = @import("js/js.zig"); +const v8 = js.v8; + +const Frame = @import("Frame.zig"); +const Session = @import("Session.zig"); +const Factory = @import("Factory.zig"); + +const log = lp.log; +const Allocator = std.mem.Allocator; +const IS_DEBUG = builtin.mode == .Debug; + +// A Page is the container for a root Frame and all of its descendants +// (nested iframes). It owns the resources that share the lifetime of the root +// document: the DOM factory, the per-page arena, the JS identity map, shared +// origins, v8 global handles, and queued navigation buffers. +// +// In the future, a Session may hold multiple Pages at once (e.g. during a +// navigation, while the old Page is retiring and the new one is provisional). +// For now, Session still holds a single Page. +const Page = @This(); + +session: *Session, + +// DOM object factory scoped to this Page's documents. +factory: Factory, + +// The arena for this Page's lifetime. Document / Frame / Factory / DOM +// objects allocate out of this. +frame_arena: Allocator, + +// Origin map for same-origin context sharing. Entries live for the Page's +// lifetime. +origins: std.StringHashMapUnmanaged(*js.Origin) = .empty, + +// Identity tracking for the main world. All main-world contexts in this Page +// share this, ensuring object identity works across same-origin frames. +identity: js.Identity = .{}, + +// Finalizer callbacks for Zig instances exposed to v8 in this Page. Keyed by +// Zig instance ptr. The backing FinalizerCallback.Identity structs come from +// Session.fc_identity_pool so they outlive the Page for v8 weak-callback +// safety. +finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty, + +// Tracked global v8 objects that need to be released when the Page tears down. +globals: std.ArrayList(v8.Global) = .empty, + +// Temporary v8 globals that can be released early. Key is global.data_ptr. +temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, + +// Double buffered so that, as we process one list of queued navigations, new +// entries are added to the separate buffer. Prevents endless navigation loops +// and invalidation of the list during iteration. +queued_navigation_1: std.ArrayList(*Frame) = .empty, +queued_navigation_2: std.ArrayList(*Frame) = .empty, +// pointer to either queued_navigation_1 or queued_navigation_2 +queued_navigation: *std.ArrayList(*Frame) = undefined, + +// Temporary buffer for about:blank navigations during processing. +// We process async navigations first (safe from re-entrance), then sync +// about:blank navigations (which may add to queued_navigation). +queued_queued_navigation: std.ArrayList(*Frame) = .empty, + +// The root Frame of this Page. Non-optional — a Page always has a root frame. +frame: Frame, + +// Initialize a Page and its root Frame. +pub fn init(self: *Page, session: *Session, frame_id: u32) !void { + const frame_arena = try session.arena_pool.acquire(.large, "Page.frame_arena"); + errdefer session.arena_pool.release(frame_arena); + + self.* = .{ + .session = session, + .frame = undefined, + .frame_arena = frame_arena, + .factory = Factory.init(frame_arena), + }; + self.queued_navigation = &self.queued_navigation_1; + + try Frame.init(&self.frame, frame_id, self, null); +} + +// Tear down the Page and its root Frame. Equivalent to the old +// Session.removePage + Session.resetFrameResources. +pub fn deinit(self: *Page, abort_http: bool) void { + self.frame.deinit(abort_http); + + const session = self.session; + defer session.browser.env.memoryPressureNotification(.moderate); + + self.identity.deinit(); + self.identity = .{}; + + // Force cleanup all remaining finalized objects. + { + var it = self.finalizer_callbacks.valueIterator(); + while (it.next()) |fc| { + fc.*.deinit(self); + } + self.finalizer_callbacks = .empty; + } + + { + for (self.globals.items) |*global| { + v8.v8__Global__Reset(global); + } + self.globals = .empty; + } + + { + var it = self.temps.valueIterator(); + while (it.next()) |global| { + v8.v8__Global__Reset(global); + } + self.temps = .empty; + } + + if (comptime IS_DEBUG) { + std.debug.assert(self.origins.count() == 0); + } + // Defensive cleanup in case origins leaked. + { + const app = session.browser.app; + var it = self.origins.valueIterator(); + while (it.next()) |value| { + value.*.deinit(app); + } + self.origins = .empty; + } + + session.arena_pool.release(self.frame_arena); +} + +pub fn getArena(self: *Page, size_or_bucket: anytype, debug: []const u8) !Allocator { + return self.session.getArena(size_or_bucket, debug); +} + +pub fn releaseArena(self: *Page, allocator: Allocator) void { + return self.session.releaseArena(allocator); +} + +pub fn getOrCreateOrigin(self: *Page, key_: ?[]const u8) !*js.Origin { + const session = self.session; + 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 Page.releaseOrigin which will free it. + return js.Origin.init(session.browser.app, session.browser.env.isolate, &opaque_origin); + }; + + const gop = try self.origins.getOrPut(session.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(session.browser.app, session.browser.env.isolate, key); + gop.key_ptr.* = origin.key; + gop.value_ptr.* = origin; + return origin; +} + +pub fn releaseOrigin(self: *Page, origin: *js.Origin) void { + const rc = origin.rc; + if (rc == 1) { + _ = self.origins.remove(origin.key); + origin.deinit(self.session.browser.app); + } else { + origin.rc = rc - 1; + } +} + +pub fn scheduleNavigation(self: *Page, frame: *Frame) !void { + const list = self.queued_navigation; + + // Check if frame is already queued + for (list.items) |existing| { + if (existing == frame) { + // Already queued + return; + } + } + + return list.append(self.session.arena, frame); +} + +pub fn findFrameByFrameId(self: *Page, frame_id: u32) ?*Frame { + return findFrameBy(&self.frame, "_frame_id", frame_id); +} + +pub fn findFrameByLoaderId(self: *Page, loader_id: u32) ?*Frame { + return findFrameBy(&self.frame, "_loader_id", loader_id); +} + +fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame { + if (@field(frame, field) == id) return frame; + for (frame.child_frames.items) |f| { + if (findFrameBy(f, field, id)) |found| { + return found; + } + } + return null; +} diff --git a/src/browser/Runner.zig b/src/browser/Runner.zig index 69b66a9f..b6274eb0 100644 --- a/src/browser/Runner.zig +++ b/src/browser/Runner.zig @@ -40,7 +40,7 @@ http_client: *HttpClient, pub const Opts = struct {}; pub fn init(session: *Session, _: Opts) !Runner { - const frame = &(session.frame orelse return error.NoPage); + const frame = session.currentFrame() orelse return error.NoPage; return .{ .frame = frame, @@ -150,10 +150,12 @@ fn _tick(self: *Runner, comptime is_cdp: bool, opts: TickOpts) !CDPTickResult { }, .html, .complete => { const session = self.session; - if (session.queued_navigation.items.len != 0) { - try session.processQueuedNavigation(); - self.frame = &session.frame.?; // might have changed - return .{ .ok = 0 }; + if (session.currentPage()) |page| { + if (page.queued_navigation.items.len != 0) { + try session.processQueuedNavigation(); + self.frame = session.currentFrame().?; // might have changed + return .{ .ok = 0 }; + } } const browser = session.browser; @@ -323,7 +325,7 @@ test "Runner: no page" { test "Runner: waitForSelector timeout" { const frame = try testing.pageTest("runner/runner1.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var runner = try frame._session.runner(.{}); try testing.expectError(error.Timeout, runner.waitForSelector("#nope", 10)); @@ -332,7 +334,7 @@ test "Runner: waitForSelector timeout" { test "Runner: waitForSelector" { defer testing.reset(); const frame = try testing.pageTest("runner/runner1.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var runner = try frame._session.runner(.{}); const el = try runner.waitForSelector("#sel1", 10); @@ -341,7 +343,7 @@ test "Runner: waitForSelector" { test "Runner: waitForScript timeout" { const frame = try testing.pageTest("runner/runner1.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var runner = try frame._session.runner(.{}); try testing.expectError(error.Timeout, runner.waitForScript("document.querySelector('#nope')", 10)); @@ -349,7 +351,7 @@ test "Runner: waitForScript timeout" { test "Runner: waitForScript" { const frame = try testing.pageTest("runner/runner1.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var runner = try frame._session.runner(.{}); try runner.waitForScript("document.querySelector('#sel1')", 10); diff --git a/src/browser/ScriptManager.zig b/src/browser/ScriptManager.zig index 327ce69b..5f82b88e 100644 --- a/src/browser/ScriptManager.zig +++ b/src/browser/ScriptManager.zig @@ -949,7 +949,7 @@ pub const Script = struct { fn executeCallback(self: *const Script, typ: String, frame: *Frame) void { const Event = @import("webapi/Event.zig"); - const event = Event.initTrusted(typ, .{}, frame._session) catch |err| { + const event = Event.initTrusted(typ, .{}, frame._page) catch |err| { log.warn(.js, "script internal callback", .{ .url = self.url, .type = typ, diff --git a/src/browser/Session.zig b/src/browser/Session.zig index d3d3d74f..cc6ad3a9 100644 --- a/src/browser/Session.zig +++ b/src/browser/Session.zig @@ -29,9 +29,9 @@ const Navigation = @import("webapi/navigation/Navigation.zig"); const History = @import("webapi/History.zig"); const Frame = @import("Frame.zig"); +const Page = @import("Page.zig"); pub const Runner = @import("Runner.zig"); const Browser = @import("Browser.zig"); -const Factory = @import("Factory.zig"); const Notification = @import("../Notification.zig"); const QueuedNavigation = Frame.QueuedNavigation; @@ -40,17 +40,16 @@ const ArenaPool = App.ArenaPool; const Allocator = std.mem.Allocator; const IS_DEBUG = builtin.mode == .Debug; -// You can create successively multiple frames for a session, but you must -// deinit a frame before running another one. It manages two distinct lifetimes. +// A Session represents a browsing context group (cookie jar, session storage, +// navigation history) within a Browser. It hosts one Page at a time — the +// root Frame and all of its descendants — and is responsible for Page +// lifecycle (create, remove, replace on root navigation). // -// The first is the lifetime of the Session itself, where frames 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 frame hierarchy, i.e. \ -// the root frame and all of its frames (and all of their frames.) +// Multiple concurrent Pages (e.g. an old Page retiring while a new provisional +// Page is loading) are not yet supported; see Page.zig for the intended +// direction. const Session = @This(); -// These are the fields that remain intact for the duration of the Session browser: *Browser, arena: Allocator, history: History, @@ -59,54 +58,18 @@ storage_shed: storage.Shed, notification: *Notification, cookie_jar: storage.Cookie.Jar, -// These are the fields that get reset whenever the Session's frame (the root) is reset. -factory: Factory, - -frame_arena: Allocator, - -// Origin map for same-origin context sharing. Scoped to the root frame lifetime. -origins: std.StringHashMapUnmanaged(*js.Origin) = .empty, - -// Identity tracking for the main world. All main world contexts share this, -// ensuring object identity works across same-origin frames. -identity: js.Identity = .{}, - -// Shared finalizer callbacks across all Identities. Keyed by Zig instance ptr. -// This ensures objects are only freed when ALL v8 wrappers are gone. -finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *FinalizerCallback) = .empty, - -// Pool for FinalizerCallback.Identity structs. These must survive frame resets -// so V8 weak callbacks can validate the FC before dereferencing it. -fc_identity_pool: std.heap.MemoryPool(FinalizerCallback.Identity), - -// Tracked global v8 objects that need to be released on cleanup. -// Lives at Session level so objects can outlive individual Identities. -globals: std.ArrayList(v8.Global) = .empty, - -// Temporary v8 globals that can be released early. Key is global.data_ptr. -// Lives at Session level so objects holding Temps can outlive individual Identities. -temps: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, - -// Shared resources for all frames in this session. -// These live for the duration of the frame tree (root + frames). +// Shared allocator. Used by Session itself and borrowed by Pages. arena_pool: *ArenaPool, -frame: ?Frame, +// Pool for FinalizerCallback.Identity structs. These must survive Page +// teardowns so V8 weak callbacks can validate the FC before dereferencing it. +fc_identity_pool: std.heap.MemoryPool(FinalizerCallback.Identity), -// Double buffer so that, as we process one list of queued navigations, new entries -// are added to the separate buffer. This ensures that we don't end up with -// endless navigation loops AND that we don't invalidate the list while iterating -// if a new entry gets appended -queued_navigation_1: std.ArrayList(*Frame), -queued_navigation_2: std.ArrayList(*Frame), -// pointer to either queued_navigation_1 or queued_navigation_2 -queued_navigation: *std.ArrayList(*Frame), - -// Temporary buffer for about:blank navigations during processing. -// We process async navigations first (safe from re-entrance), then sync -// about:blank navigations (which may add to queued_navigation). -queued_queued_navigation: std.ArrayList(*Frame), +// The currently-active Page. Null when no Page exists (between removePage +// and createPage, or at startup). +page: ?Page, +// IDs. Kept at Session level so IDs can remain unique across Page replacements. frame_id_gen: u32 = 0, loader_id_gen: u32 = 0, @@ -117,57 +80,47 @@ pub fn init(self: *Session, browser: *Browser, notification: *Notification) !voi const arena = try arena_pool.acquire(.small, "Session"); errdefer arena_pool.release(arena); - const frame_arena = try arena_pool.acquire(.large, "Session.frame_arena"); - errdefer arena_pool.release(frame_arena); - self.* = .{ - .frame = null, + .page = null, .arena = arena, .arena_pool = arena_pool, - .frame_arena = frame_arena, - .factory = Factory.init(frame_arena), .history = .{}, // The prototype (EventTarget) for Navigation is created when a Frame is created. .navigation = .{ ._proto = undefined }, .storage_shed = .{}, .browser = browser, - .queued_navigation = undefined, - .queued_navigation_1 = .{}, - .queued_navigation_2 = .{}, - .queued_queued_navigation = .{}, .notification = notification, - .cookie_jar = storage.Cookie.Jar.init(allocator), .fc_identity_pool = .init(allocator), + .cookie_jar = storage.Cookie.Jar.init(allocator), }; - self.queued_navigation = &self.queued_navigation_1; } pub fn deinit(self: *Session) void { - if (self.frame != null) { - self.removeFrame(); + if (self.page != null) { + self.removePage(); } self.cookie_jar.deinit(); self.fc_identity_pool.deinit(); self.storage_shed.deinit(self.browser.app.allocator); - self.arena_pool.release(self.frame_arena); self.arena_pool.release(self.arena); } // NOTE: the caller is not the owner of the returned value, // the pointer on Frame is just returned as a convenience -pub fn createFrame(self: *Session) !*Frame { - lp.assert(self.frame == null, "Session.createFrame - frame not null", .{}); +pub fn createPage(self: *Session) !*Frame { + lp.assert(self.page == null, "Session.createPage - page not null", .{}); - self.frame = @as(Frame, undefined); - const frame = &self.frame.?; - try Frame.init(frame, self.nextFrameId(), self, null); + self.page = @as(Page, undefined); + const page = &self.page.?; + try Page.init(page, self, self.nextFrameId()); + const frame = &page.frame; // Creates a new NavigationEventTarget for this frame. try self.navigation.onNewFrame(frame); if (comptime IS_DEBUG) { - log.debug(.browser, "create frame", .{}); + log.debug(.browser, "create page", .{}); } // start JS env // Inform CDP the main frame has been created such that additional context for other Worlds can be created as well @@ -176,19 +129,22 @@ pub fn createFrame(self: *Session) !*Frame { return frame; } -pub fn removeFrame(self: *Session) void { +pub fn removePage(self: *Session) void { // Inform CDP the frame is going to be removed, allowing other worlds to remove themselves before the main one self.notification.dispatch(.frame_remove, .{}); - lp.assert(self.frame != null, "Session.removeFrame - frame is null", .{}); + lp.assert(self.page != null, "Session.removePage - page is null", .{}); - self.frame.?.deinit(false); - self.frame = null; + self.page.?.deinit(false); + self.page = null; self.navigation.onRemoveFrame(); - self.resetFrameResources(); + + // resetting frame_id_gen preserves previous behavior where removing the + // root page returned us to a clean-slate state. + self.frame_id_gen = 0; if (comptime IS_DEBUG) { - log.debug(.browser, "remove frame", .{}); + log.debug(.browser, "remove page", .{}); } } @@ -201,132 +157,49 @@ pub fn releaseArena(self: *Session, allocator: Allocator) void { } 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; + return self.page.?.getOrCreateOrigin(key_); } 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; - } + return self.page.?.releaseOrigin(origin); } -/// Reset frame_arena and factory for a clean slate. -/// Called when root frame is removed. -fn resetFrameResources(self: *Session) void { - defer self.browser.env.memoryPressureNotification(.moderate); - - self.identity.deinit(); - self.identity = .{}; - - // Force cleanup all remaining finalized objects - { - var it = self.finalizer_callbacks.valueIterator(); - while (it.next()) |fc| { - fc.*.deinit(self); - } - self.finalizer_callbacks = .empty; - } - - { - for (self.globals.items) |*global| { - v8.v8__Global__Reset(global); - } - self.globals = .empty; - } - - { - var it = self.temps.valueIterator(); - while (it.next()) |global| { - v8.v8__Global__Reset(global); - } - self.temps = .empty; - } - +pub fn replacePage(self: *Session) !*Frame { 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 = .empty; + log.debug(.browser, "replace page", .{}); } - self.frame_id_gen = 0; - self.arena_pool.reset(self.frame_arena, 64 * 1024); - self.factory = Factory.init(self.frame_arena); -} + lp.assert(self.page != null, "Session.replacePage null page", .{}); + const current = &self.page.?; + lp.assert(current.frame.parent == null, "Session.replacePage with parent", .{}); -pub fn replaceFrame(self: *Session) !*Frame { - if (comptime IS_DEBUG) { - log.debug(.browser, "replace frame", .{}); - } - - lp.assert(self.frame != null, "Session.replaceFrame null frame", .{}); - lp.assert(self.frame.?.parent == null, "Session.replaceFrame with parent", .{}); - - var current = self.frame.?; - const frame_id = current._frame_id; + const frame_id = current.frame._frame_id; current.deinit(true); + self.page = null; - self.resetFrameResources(); + // Preserve prior behavior: frame_id_gen reset on root replacement so a + // subsequent createPage starts from id 1. The captured frame_id is + // passed into Page.init explicitly, so it isn't affected. + self.frame_id_gen = 0; - self.frame = @as(Frame, undefined); - const frame = &self.frame.?; - try Frame.init(frame, frame_id, self, null); - return frame; + self.page = @as(Page, undefined); + const page = &self.page.?; + try Page.init(page, self, frame_id); + return &page.frame; +} + +pub fn currentPage(self: *Session) ?*Page { + return &(self.page orelse return null); } pub fn currentFrame(self: *Session) ?*Frame { - return &(self.frame orelse return null); + const page = self.currentPage() orelse return null; + return &page.frame; } pub fn findFrameByFrameId(self: *Session, frame_id: u32) ?*Frame { - const frame = self.currentFrame() orelse return null; - return findFrameBy(frame, "_frame_id", frame_id); -} - -pub fn findFrameByLoaderId(self: *Session, loader_id: u32) ?*Frame { - const frame = self.currentFrame() orelse return null; - return findFrameBy(frame, "_loader_id", loader_id); -} - -fn findFrameBy(frame: *Frame, comptime field: []const u8, id: u32) ?*Frame { - if (@field(frame, field) == id) return frame; - for (frame.child_frames.items) |f| { - if (findFrameBy(f, field, id)) |found| { - return found; - } - } - return null; + const page = self.currentPage() orelse return null; + return page.findFrameByFrameId(frame_id); } pub fn runner(self: *Session, opts: Runner.Opts) !Runner { @@ -334,28 +207,19 @@ pub fn runner(self: *Session, opts: Runner.Opts) !Runner { } pub fn scheduleNavigation(self: *Session, frame: *Frame) !void { - const list = self.queued_navigation; - - // Check if frame is already queued - for (list.items) |existing| { - if (existing == frame) { - // Already queued - return; - } - } - - return list.append(self.arena, frame); + return self.page.?.scheduleNavigation(frame); } pub fn processQueuedNavigation(self: *Session) !void { - const navigations = self.queued_navigation; - if (self.queued_navigation == &self.queued_navigation_1) { - self.queued_navigation = &self.queued_navigation_2; + const page = self.currentPage() orelse return; + const navigations = page.queued_navigation; + if (page.queued_navigation == &page.queued_navigation_1) { + page.queued_navigation = &page.queued_navigation_2; } else { - self.queued_navigation = &self.queued_navigation_1; + page.queued_navigation = &page.queued_navigation_1; } - if (self.frame.?._queued_navigation != null) { + if (page.frame._queued_navigation != null) { // This is both an optimization and a simplification of sorts. If the // root frame is navigating, then we don't need to process any other // navigation. Also, the navigation for the root frame and for a frame @@ -365,7 +229,7 @@ pub fn processQueuedNavigation(self: *Session) !void { return self.processRootQueuedNavigation(); } - const about_blank_queue = &self.queued_queued_navigation; + const about_blank_queue = &page.queued_queued_navigation; defer about_blank_queue.clearRetainingCapacity(); // First pass: process async navigations (non-about:blank) @@ -395,14 +259,14 @@ pub fn processQueuedNavigation(self: *Session) !void { // Safety: Remove any about:blank navigations that were queued during // processing to prevent infinite loops. New navigations have been queued // in the other buffer. - const new_navigations = self.queued_navigation; + const new_navigations = page.queued_navigation; var i: usize = 0; while (i < new_navigations.items.len) { const frame = new_navigations.items[i]; if (frame._queued_navigation) |qn| { if (qn.is_about_blank) { log.warn(.frame, "recursive about blank", .{}); - _ = self.queued_navigation.swapRemove(i); + _ = page.queued_navigation.swapRemove(i); continue; } } @@ -428,10 +292,11 @@ fn processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) } const frame_id = frame._frame_id; + const page = self.currentPage().?; frame.deinit(true); frame.* = undefined; - try Frame.init(frame, frame_id, self, parent); + try Frame.init(frame, frame_id, page, parent); errdefer { for (parent.child_frames.items, 0..) |f, i| { if (f == frame) { @@ -456,7 +321,7 @@ fn processFrameNavigation(self: *Session, frame: *Frame, qn: *QueuedNavigation) } fn processRootQueuedNavigation(self: *Session) !void { - const current_frame = &self.frame.?; + const current_frame = &self.page.?.frame; const frame_id = current_frame._frame_id; // create a copy before the frame is cleared @@ -465,11 +330,21 @@ fn processRootQueuedNavigation(self: *Session) !void { defer self.arena_pool.release(qn.arena); - self.removeFrame(); + // Dispatch frame_remove (same as removePage) then replace the Page + // in-place, keeping the frame_id stable. + self.notification.dispatch(.frame_remove, .{}); + self.page.?.deinit(true); + self.page = null; - self.frame = @as(Frame, undefined); - const new_frame = &self.frame.?; - try Frame.init(new_frame, frame_id, self, null); + self.navigation.onRemoveFrame(); + + // Preserve prior behavior: the old resetFrameResources reset frame_id_gen. + self.frame_id_gen = 0; + + self.page = @as(Page, undefined); + const page = &self.page.?; + try Page.init(page, self, frame_id); + const new_frame = &page.frame; // Creates a new NavigationEventTarget for this frame. try self.navigation.onNewFrame(new_frame); @@ -497,14 +372,14 @@ pub fn nextLoaderId(self: *Session) u32 { } // Every finalizable instance of Zig gets 1 FinalizerCallback registered in the -// session. This is to ensure that, if v8 doesn't finalize the value, we can -// release on frame reset. +// Page. This is to ensure that, if v8 doesn't finalize the value, we can +// release on Page teardown. pub const FinalizerCallback = struct { + page: *Page, arena: Allocator, - session: *Session, resolved_ptr_id: usize, finalizer_ptr_id: usize, - release_ref: *const fn (ptr_id: usize, session: *Session) void, + release_ref: *const fn (ptr_id: usize, page: *Page) void, // Linked list of Identities referencing this FC. identities: ?*Identity = null, @@ -513,10 +388,14 @@ pub const FinalizerCallback = struct { // For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one // for every identity that gets the instance. In most cases, that'll be 1. - // Allocated from Session.fc_identity_pool so it survives frame resets and + // Allocated from Session.fc_identity_pool so it survives Page teardowns and // allows the weak callback to safely check the done flag. pub const Identity = struct { session: *Session, + // The Page that owns the FinalizerCallback this Identity references. + // Only safe to dereference when `done == false`. When done is true, + // the Page may have been torn down and this pointer is stale. + page: *Page, identity: *js.Identity, finalizer_ptr_id: usize, resolved_ptr_id: usize, @@ -524,8 +403,8 @@ pub const FinalizerCallback = struct { done: bool = false, }; - // Called during frame reset to force cleanup regardless of identities. - fn deinit(self: *FinalizerCallback, session: *Session) void { + // Called during Page teardown to force cleanup regardless of identities. + pub fn deinit(self: *FinalizerCallback, page: *Page) void { // Mark all identities as done so stale V8 weak callbacks // won't find the wrong FC if resolved_ptr_id is reused. var id = self.identities; @@ -533,7 +412,7 @@ pub const FinalizerCallback = struct { identity.done = true; id = identity.next; } - self.release_ref(self.finalizer_ptr_id, session); - session.releaseArena(self.arena); + self.release_ref(self.finalizer_ptr_id, page); + page.releaseArena(self.arena); } }; diff --git a/src/browser/actions.zig b/src/browser/actions.zig index d09cf57b..9380de04 100644 --- a/src/browser/actions.zig +++ b/src/browser/actions.zig @@ -28,12 +28,12 @@ const Session = @import("Session.zig"); const Selector = @import("webapi/selector/Selector.zig"); fn dispatchInputAndChangeEvents(el: *Element, frame: *Frame) !void { - const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame._session); + const input_evt: *Event = try .initTrusted(comptime .wrap("input"), .{ .bubbles = true }, frame._page); frame._event_manager.dispatch(el.asEventTarget(), input_evt) catch |err| { lp.log.err(.app, "dispatch input event failed", .{ .err = err }); }; - const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame._session); + const change_evt: *Event = try .initTrusted(comptime .wrap("change"), .{ .bubbles = true }, frame._page); frame._event_manager.dispatch(el.asEventTarget(), change_evt) catch |err| { lp.log.err(.app, "dispatch change event failed", .{ .err = err }); }; @@ -196,7 +196,7 @@ pub fn scroll(node: ?*DOMNode, x: ?i32, y: ?i32, frame: *Frame) !void { }; } - const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame._session); + const scroll_evt: *Event = try .initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, frame._page); frame._event_manager.dispatch(el.asEventTarget(), scroll_evt) catch |err| { lp.log.err(.app, "dispatch scroll event failed", .{ .err = err }); }; diff --git a/src/browser/forms.zig b/src/browser/forms.zig index 34a931d3..51366627 100644 --- a/src/browser/forms.zig +++ b/src/browser/forms.zig @@ -278,7 +278,7 @@ fn collectSelectOptions( const testing = @import("../testing.zig"); fn testForms(html: []const u8) ![]FormInfo { - const frame = try testing.test_session.createFrame(); + const frame = try testing.test_session.createPage(); const doc = frame.window._document; const div = try doc.createElement("div", null, frame); @@ -289,7 +289,7 @@ fn testForms(html: []const u8) ![]FormInfo { test "browser.forms: login form" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\
\\ @@ -310,7 +310,7 @@ test "browser.forms: login form" { test "browser.forms: form with select" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\ @@ -343,7 +343,7 @@ test "browser.forms: form with textarea" { test "browser.forms: empty form skipped" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\

No fields here

@@ -354,7 +354,7 @@ test "browser.forms: empty form skipped" { test "browser.forms: hidden inputs excluded" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\ @@ -368,7 +368,7 @@ test "browser.forms: hidden inputs excluded" { test "browser.forms: multiple forms" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\ @@ -385,7 +385,7 @@ test "browser.forms: multiple forms" { test "browser.forms: disabled fields flagged" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\ @@ -400,7 +400,7 @@ test "browser.forms: disabled fields flagged" { test "browser.forms: disabled fieldset" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\
@@ -417,7 +417,7 @@ test "browser.forms: disabled fieldset" { test "browser.forms: external field via form attribute" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\ @@ -430,7 +430,7 @@ test "browser.forms: external field via form attribute" { test "browser.forms: checkbox and radio return value attribute" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\ @@ -447,7 +447,7 @@ test "browser.forms: checkbox and radio return value attribute" { test "browser.forms: form without action or method" { defer testing.reset(); - defer testing.test_session.removeFrame(); + defer testing.test_session.removePage(); const forms = try testForms( \\ \\ diff --git a/src/browser/interactive.zig b/src/browser/interactive.zig index 6d6a4be8..313e7c73 100644 --- a/src/browser/interactive.zig +++ b/src/browser/interactive.zig @@ -451,8 +451,8 @@ fn getListenerTypes(target: *EventTarget, listener_targets: ListenerTargetMap) [ const testing = @import("../testing.zig"); fn testInteractive(html: []const u8) ![]InteractiveElement { - const frame = try testing.test_session.createFrame(); - defer testing.test_session.removeFrame(); + const frame = try testing.test_session.createPage(); + defer testing.test_session.removePage(); const doc = frame.window._document; const div = try doc.createElement("div", null, frame); diff --git a/src/browser/js/Caller.zig b/src/browser/js/Caller.zig index 3e20c134..4df8813a 100644 --- a/src/browser/js/Caller.zig +++ b/src/browser/js/Caller.zig @@ -21,6 +21,7 @@ const lp = @import("lightpanda"); const string = @import("../../string.zig"); const Frame = @import("../Frame.zig"); +const Page = @import("../Page.zig"); const Session = @import("../Session.zig"); const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig"); @@ -447,10 +448,14 @@ fn tupleFieldName(comptime i: usize) [:0]const u8 { }; } -fn isPage(comptime T: type) bool { +fn isFrame(comptime T: type) bool { return T == *Frame or T == *const Frame; } +fn isPage(comptime T: type) bool { + return T == *Page or T == *const Page; +} + fn isSession(comptime T: type) bool { return T == *Session or T == *const Session; } @@ -460,19 +465,19 @@ fn isExecution(comptime T: type) bool { } fn getGlobalArg(comptime T: type, ctx: *Context) T { - if (comptime isPage(T)) { + if (comptime isFrame(T)) { return switch (ctx.global) { .frame => |frame| frame, .worker => unreachable, }; } - if (comptime isExecution(T)) { - return &ctx.execution; + if (comptime isPage(T)) { + return ctx.page; } - if (comptime isSession(T)) { - return ctx.session; + if (comptime isExecution(T)) { + return &ctx.execution; } @compileError("Unsupported global arg type: " ++ @typeName(T)); @@ -761,17 +766,17 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info: return args; } - // If the last parameter is the Page or Worker, set it, and exclude it - // from our params slice, because we don't want to bind it to - // a JS argument + // If the last parameter is Frame/Page/Session/Execution, set it from + // context and exclude it from our params slice, because we don't want + // to bind it to a JS argument. const LastParamType = params[params.len - 1].type.?; - if (comptime isPage(LastParamType) or isExecution(LastParamType) or isSession(LastParamType)) { + if (comptime isFrame(LastParamType) or isPage(LastParamType) or isExecution(LastParamType) or isSession(LastParamType)) { @field(args, tupleFieldName(params.len - 1 + offset)) = getGlobalArg(LastParamType, local.ctx); break :blk params[0 .. params.len - 1]; } - // we have neither a Page, Execution, nor a JsObject. All params must be - // bound to a JavaScript value. + // we have neither a Frame/Page/Session/Execution nor a JsObject. + // All params must be bound to a JavaScript value. break :blk params; }; @@ -818,7 +823,9 @@ fn getArgs(comptime F: type, comptime offset: usize, local: *const Local, info: } } - if (comptime isPage(param.type.?)) { + if (comptime isFrame(param.type.?)) { + @compileError("Frame must be the last parameter: " ++ @typeName(F)); + } else if (comptime isPage(param.type.?)) { @compileError("Page must be the last parameter: " ++ @typeName(F)); } else if (comptime isExecution(param.type.?)) { @compileError("Execution must be the last parameter: " ++ @typeName(F)); diff --git a/src/browser/js/Context.zig b/src/browser/js/Context.zig index 7087130c..b880ce6e 100644 --- a/src/browser/js/Context.zig +++ b/src/browser/js/Context.zig @@ -27,6 +27,7 @@ const Scheduler = @import("Scheduler.zig"); const Execution = @import("Execution.zig"); const Frame = @import("../Frame.zig"); +const Page = @import("../Page.zig"); const Session = @import("../Session.zig"); const ScriptManager = @import("../ScriptManager.zig"); const WorkerGlobalScope = @import("../webapi/WorkerGlobalScope.zig"); @@ -69,7 +70,14 @@ pub const GlobalScope = union(enum) { id: usize, env: *Env, global: GlobalScope, -session: *Session, + +// The Page this Context belongs to. For main-world frame contexts, this is +// the Page of the frame. For worker contexts, this is the Page of the +// worker's parent frame — a worker's v8 globals and identity tracking live +// on the same Page as its owning frame (worker dies with its page). The +// Session is always reachable via `page.session`. +page: *Page, + isolate: js.Isolate, // Per-context microtask queue for isolation between contexts @@ -114,7 +122,7 @@ origin: *Origin, identity: *js.Identity, // Allocator to use for identity map operations. For main world contexts this is -// session.frame_arena, for isolated worlds it's the isolated world's arena. +// page.frame_arena, for isolated worlds it's the isolated world's arena. identity_arena: Allocator, // Unlike other v8 types, like functions or objects, modules are not shared @@ -207,7 +215,7 @@ pub fn deinit(self: *Context) void { v8.v8__Global__Reset(global); } - self.session.releaseOrigin(self.origin); + self.page.releaseOrigin(self.origin); // Clear the embedder data so that if V8 keeps this context alive // (because objects created in it are still referenced), we don't @@ -234,9 +242,9 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void { lp.assert(self.origin.rc == 1, "Ref opaque origin", .{ .rc = self.origin.rc }); } - const origin = try self.session.getOrCreateOrigin(key); + const origin = try self.page.getOrCreateOrigin(key); - self.session.releaseOrigin(self.origin); + self.page.releaseOrigin(self.origin); self.origin = origin; { @@ -252,11 +260,11 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void { } pub fn trackGlobal(self: *Context, global: v8.Global) !void { - return self.session.globals.append(self.session.frame_arena, global); + return self.page.globals.append(self.page.frame_arena, global); } pub fn trackTemp(self: *Context, global: v8.Global) !void { - return self.session.temps.put(self.session.frame_arena, global.data_ptr, global); + return self.page.temps.put(self.page.frame_arena, global.data_ptr, global); } pub const IdentityResult = struct { diff --git a/src/browser/js/Env.zig b/src/browser/js/Env.zig index 6572a3ca..2d4a7b15 100644 --- a/src/browser/js/Env.zig +++ b/src/browser/js/Env.zig @@ -275,9 +275,9 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context const context_id = self.context_id; self.context_id = context_id + 1; - const session = global._session; - const origin = try session.getOrCreateOrigin(null); - errdefer session.releaseOrigin(origin); + const page = global._page; + const origin = try page.getOrCreateOrigin(null); + errdefer page.releaseOrigin(origin); const context = try context_arena.create(Context); context.* = .{ @@ -285,7 +285,7 @@ fn _createContext(self: *Env, global: anytype, params: ContextParams) !*Context .global = if (comptime is_frame) .{ .frame = global } else .{ .worker = global }, .origin = origin, .id = context_id, - .session = session, + .page = page, .isolate = isolate, .arena = context_arena, .handle = context_global, @@ -558,8 +558,8 @@ const PrivateSymbols = struct { const testing = @import("../../testing.zig"); test "Env: Worker context " { const session = testing.test_session; - const frame = try session.createFrame(); - defer session.removeFrame(); + const frame = try session.createPage(); + defer session.removePage(); const worker = try @import("../webapi/Worker.zig").init("http://localhost:9582/src/browser/tests/testing.js", &frame.js.execution); @@ -573,8 +573,8 @@ test "Env: Worker context " { test "Env: Frame context" { const session = testing.test_session; - const frame = try session.createFrame(); - defer session.removeFrame(); + const frame = try session.createPage(); + defer session.removePage(); // Frame already has a context created, use it directly const ctx = frame.js; diff --git a/src/browser/js/Function.zig b/src/browser/js/Function.zig index e8713a8d..218c7bcd 100644 --- a/src/browser/js/Function.zig +++ b/src/browser/js/Function.zig @@ -215,7 +215,7 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .temps = &ctx.session.temps }; + return .{ .handle = global, .temps = &ctx.page.temps }; } pub fn tempWithThis(self: *const Function, value: anytype) !Temp { diff --git a/src/browser/js/Local.zig b/src/browser/js/Local.zig index d386a9a5..3e810f31 100644 --- a/src/browser/js/Local.zig +++ b/src/browser/js/Local.zig @@ -20,7 +20,8 @@ const std = @import("std"); const lp = @import("lightpanda"); const string = @import("../../string.zig"); -const Session = @import("../Session.zig"); +const Page = @import("../Page.zig"); +const FinalizerCallback = @import("../Session.zig").FinalizerCallback; const js = @import("js.zig"); const bridge = @import("bridge.zig"); @@ -270,19 +271,21 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object, if (resolved.finalizer) |finalizer| { const finalizer_ptr_id = finalizer.ptr_id; - const session = ctx.session; - const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.frame_arena, finalizer_ptr_id); + const page = ctx.page; + const session = page.session; + const finalizer_gop = try page.finalizer_callbacks.getOrPut(page.frame_arena, finalizer_ptr_id); if (finalizer_gop.found_existing == false) { // This is the first context (and very likely only one) to // see this Zig instance. We need to create the FinalizerCallback - // so that we can cleanup on frame reset if v8 doesn't finalize. - errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id); + // so that we can cleanup on Page teardown if v8 doesn't finalize. + errdefer _ = page.finalizer_callbacks.remove(finalizer_ptr_id); finalizer.acquire_ref(finalizer_ptr_id); finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.release_ref_from_zig); } const fc = finalizer_gop.value_ptr.*; const identity_finalizer = try session.fc_identity_pool.create(); identity_finalizer.* = .{ + .page = page, .session = session, .identity = ctx.identity, .finalizer_ptr_id = finalizer_ptr_id, @@ -1178,7 +1181,7 @@ const Resolved = struct { ptr_id: usize, acquire_ref: *const fn (ptr_id: usize) void, release_ref: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void, - release_ref_from_zig: *const fn (ptr_id: usize, session: *Session) void, + release_ref_from_zig: *const fn (ptr_id: usize, page: *Page) void, }; }; pub fn resolveValue(value: anytype) Resolved { @@ -1224,12 +1227,12 @@ fn resolveT(comptime T: type, value: *T) Resolved { fn releaseRef(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void { const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?; - const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr)); + const identity_finalizer: *FinalizerCallback.Identity = @ptrCast(@alignCast(ptr)); // Identity is allocated from pool, so it's valid even after frame reset. - const session = identity_finalizer.session; + const page = identity_finalizer.page; const resolved_ptr_id = identity_finalizer.resolved_ptr_id; - defer session.fc_identity_pool.destroy(identity_finalizer); + defer page.session.fc_identity_pool.destroy(identity_finalizer); // Always clean up the identity map entry if (identity_finalizer.identity.identity_map.fetchRemove(resolved_ptr_id)) |kv| { @@ -1237,28 +1240,29 @@ fn resolveT(comptime T: type, value: *T) Resolved { v8.v8__Global__Reset(&global); } - // If done, FC was already cleaned up during frame reset. The - // finalizer_ptr_id may have been reused for a new object, so - // we must not look it up in the map. + // If done, FC was already cleaned up during Page teardown. + // The finalizer_ptr_id may have been reused for a new object, + // so we must not look it up in the map. It's also unsafe to + // dereference identity_finalizer.page after done is true. if (identity_finalizer.done) return; const finalizer_ptr_id = identity_finalizer.finalizer_ptr_id; - const fc = session.finalizer_callbacks.get(finalizer_ptr_id) orelse return; + const fc = page.finalizer_callbacks.get(finalizer_ptr_id) orelse return; const identity_count = fc.identity_count; if (identity_count == 1) { // Last identity - clean up the FC. // Remove from map before releaseRef to prevent address reuse issues. - _ = session.finalizer_callbacks.remove(finalizer_ptr_id); - FT.releaseRef(@ptrFromInt(finalizer_ptr_id), session); - session.releaseArena(fc.arena); + _ = page.finalizer_callbacks.remove(finalizer_ptr_id); + FT.releaseRef(@ptrFromInt(finalizer_ptr_id), page); + page.releaseArena(fc.arena); } else { fc.identity_count = identity_count - 1; } } - fn releaseRefFromZig(ptr_id: usize, session: *Session) void { - FT.releaseRef(@ptrFromInt(ptr_id), session); + fn releaseRefFromZig(ptr_id: usize, page: *Page) void { + FT.releaseRef(@ptrFromInt(ptr_id), page); } }; break :blk .{ @@ -1524,17 +1528,17 @@ fn createFinalizerCallback( // The most specific value where finalizers are defined // What actually gets acquired / released / deinit finalizer_ptr_id: usize, - release_ref: *const fn (ptr_id: usize, session: *Session) void, -) !*Session.FinalizerCallback { - const session = self.ctx.session; + release_ref: *const fn (ptr_id: usize, page: *Page) void, +) !*FinalizerCallback { + const page = self.ctx.page; - const arena = try session.getArena(.tiny, "FinalizerCallback"); - errdefer session.releaseArena(arena); + const arena = try page.getArena(.tiny, "FinalizerCallback"); + errdefer page.releaseArena(arena); - const fc = try arena.create(Session.FinalizerCallback); + const fc = try arena.create(FinalizerCallback); fc.* = .{ + .page = page, .arena = arena, - .session = session, .release_ref = release_ref, .resolved_ptr_id = resolved_ptr_id, .finalizer_ptr_id = finalizer_ptr_id, diff --git a/src/browser/js/Promise.zig b/src/browser/js/Promise.zig index 8418c408..0118532c 100644 --- a/src/browser/js/Promise.zig +++ b/src/browser/js/Promise.zig @@ -67,7 +67,7 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .temps = &ctx.session.temps }; + return .{ .handle = global, .temps = &ctx.page.temps }; } pub const Temp = G(.temp); diff --git a/src/browser/js/String.zig b/src/browser/js/String.zig index 3558c178..5c51863f 100644 --- a/src/browser/js/String.zig +++ b/src/browser/js/String.zig @@ -57,7 +57,7 @@ fn _toSlice(self: String, comptime null_terminate: bool, allocator: Allocator) ! pub fn toSSO(self: String, comptime global: bool) !(if (global) lp.String.Global else lp.String) { if (comptime global) { - return .{ .str = try self.toSSOWithAlloc(self.local.ctx.session.frame_arena) }; + return .{ .str = try self.toSSOWithAlloc(self.local.ctx.page.frame_arena) }; } return self.toSSOWithAlloc(self.local.call_arena); } diff --git a/src/browser/js/Value.zig b/src/browser/js/Value.zig index a59701d7..b73a9fb2 100644 --- a/src/browser/js/Value.zig +++ b/src/browser/js/Value.zig @@ -398,7 +398,7 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa return .{ .handle = global, .temps = {} }; } try ctx.trackTemp(global); - return .{ .handle = global, .temps = &ctx.session.temps }; + return .{ .handle = global, .temps = &ctx.page.temps }; } pub fn toZig(self: Value, comptime T: type) !T { diff --git a/src/browser/links.zig b/src/browser/links.zig index b3f5f192..158bca61 100644 --- a/src/browser/links.zig +++ b/src/browser/links.zig @@ -31,7 +31,7 @@ pub fn collectLinks(arena: Allocator, root: *Node, frame: *Frame) ![]const []con var links: std.ArrayList([]const u8) = .empty; if (Selector.querySelectorAll(root, "a[href]", frame)) |list| { - defer list.deinit(frame._session); + defer list.deinit(frame._page); for (list._nodes) |node| { if (node.is(Element.Html.Anchor)) |anchor| { diff --git a/src/browser/markdown.zig b/src/browser/markdown.zig index 69ae5e8d..52f792dc 100644 --- a/src/browser/markdown.zig +++ b/src/browser/markdown.zig @@ -474,8 +474,8 @@ pub fn dump(node: *Node, opts: Opts, writer: *std.Io.Writer, frame: *Frame) !voi fn testMarkdownHTML(html: []const u8, expected: []const u8) !void { const testing = @import("../testing.zig"); - const frame = try testing.test_session.createFrame(); - defer testing.test_session.removeFrame(); + const frame = try testing.test_session.createPage(); + defer testing.test_session.removePage(); frame.url = "http://localhost/"; const doc = frame.window._document; @@ -677,8 +677,8 @@ test "browser.markdown: skip empty links" { test "browser.markdown: resolve links" { const testing = @import("../testing.zig"); - const frame = try testing.test_session.createFrame(); - defer testing.test_session.removeFrame(); + const frame = try testing.test_session.createPage(); + defer testing.test_session.removePage(); frame.url = "https://example.com/a/index.html"; const doc = frame.window._document; diff --git a/src/browser/structured_data.zig b/src/browser/structured_data.zig index 9fe1f7b3..8b03add6 100644 --- a/src/browser/structured_data.zig +++ b/src/browser/structured_data.zig @@ -318,8 +318,8 @@ fn collectLink( const testing = @import("../testing.zig"); fn testStructuredData(html: []const u8) !StructuredData { - const frame = try testing.test_session.createFrame(); - defer testing.test_session.removeFrame(); + const frame = try testing.test_session.createPage(); + defer testing.test_session.removePage(); const doc = frame.window._document; const div = try doc.createElement("div", null, frame); diff --git a/src/browser/webapi/AbortSignal.zig b/src/browser/webapi/AbortSignal.zig index 8e1241e0..a1b5cd00 100644 --- a/src/browser/webapi/AbortSignal.zig +++ b/src/browser/webapi/AbortSignal.zig @@ -83,7 +83,7 @@ pub fn abort(self: *AbortSignal, reason_: ?Reason, exec: *const Execution) !void switch (exec.context.global) { inline else => |g| { if (g._event_manager.hasDirectListeners(target, "abort", on_abort)) { - const event = try Event.initTrusted(comptime .wrap("abort"), .{}, g._session); + const event = try Event.initTrusted(comptime .wrap("abort"), .{}, g._page); try g.dispatch(target, event, on_abort, .{ .context = "abort signal" }); } }, diff --git a/src/browser/webapi/AbstractRange.zig b/src/browser/webapi/AbstractRange.zig index 8e934849..46af8b3f 100644 --- a/src/browser/webapi/AbstractRange.zig +++ b/src/browser/webapi/AbstractRange.zig @@ -20,8 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); - -const Session = @import("../Session.zig"); +const Page = @import("../Page.zig"); const Node = @import("Node.zig"); const Range = @import("Range.zig"); @@ -49,15 +48,15 @@ pub fn acquireRef(self: *AbstractRange) void { self._rc.acquire(); } -pub fn deinit(self: *AbstractRange, session: *Session) void { - if (session.findFrameByLoaderId(self._frame_loader_id)) |frame| { +pub fn deinit(self: *AbstractRange, page: *Page) void { + if (page.findFrameByLoaderId(self._frame_loader_id)) |frame| { frame._live_ranges.remove(&self._range_link); } - session.releaseArena(self._arena); + page.releaseArena(self._arena); } -pub fn releaseRef(self: *AbstractRange, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *AbstractRange, page: *Page) void { + self._rc.release(self, page); } pub const Type = union(enum) { diff --git a/src/browser/webapi/Blob.zig b/src/browser/webapi/Blob.zig index 5de433ff..2bead21d 100644 --- a/src/browser/webapi/Blob.zig +++ b/src/browser/webapi/Blob.zig @@ -20,7 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); -const Session = @import("../Session.zig"); +const Page = @import("../Page.zig"); const Mime = @import("../Mime.zig"); @@ -61,7 +61,8 @@ const InitOptions = struct { /// Creates a new Blob from JS values with optional MIME validation. /// This is the JS Constructor -pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, session: *Session) !*Blob { +pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, page: *Page) !*Blob { + const session = page.session; const arena = try session.getArena(.large, "Blob"); errdefer session.releaseArena(arena); @@ -94,9 +95,9 @@ pub fn init(parts_: ?[]const js.Value, opts_: ?InitOptions, session: *Session) ! } /// Creates a new Blob from raw byte slices (for internal Zig use). -pub fn initFromBytes(data: []const u8, content_type: []const u8, validate_mime: bool, session: *Session) !*Blob { - const arena = try session.getArena(.large, "Blob"); - errdefer session.releaseArena(arena); +pub fn initFromBytes(data: []const u8, content_type: []const u8, validate_mime: bool, page: *Page) !*Blob { + const arena = try page.getArena(.large, "Blob"); + errdefer page.releaseArena(arena); const mime = try validateMimeType(arena, content_type, validate_mime); @@ -137,12 +138,12 @@ fn validateMimeType(arena: Allocator, mime_type: []const u8, full_validation: bo return buf; } -pub fn deinit(self: *Blob, session: *Session) void { - session.releaseArena(self._arena); +pub fn deinit(self: *Blob, page: *Page) void { + page.releaseArena(self._arena); } -pub fn releaseRef(self: *Blob, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *Blob, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *Blob) void { @@ -291,7 +292,7 @@ pub fn slice( start_: ?i32, end_: ?i32, content_type_: ?[]const u8, - session: *Session, + page: *Page, ) !*Blob { const data = self._slice; @@ -312,7 +313,7 @@ pub fn slice( break :blk @min(data.len, @max(start, @as(u31, @intCast(requested_end)))); }; - return Blob.initFromBytes(data[start..end], content_type_ orelse "", false, session); + return Blob.initFromBytes(data[start..end], content_type_ orelse "", false, page); } /// Returns the size of the Blob in bytes. diff --git a/src/browser/webapi/Document.zig b/src/browser/webapi/Document.zig index b8524d97..a00d301b 100644 --- a/src/browser/webapi/Document.zig +++ b/src/browser/webapi/Document.zig @@ -353,12 +353,12 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !* const normalized = std.ascii.lowerString(&frame.buf, event_type); if (std.mem.eql(u8, normalized, "event") or std.mem.eql(u8, normalized, "events") or std.mem.eql(u8, normalized, "htmlevents")) { - return Event.init("", null, frame); + return Event.init("", null, frame._page); } if (std.mem.eql(u8, normalized, "customevent") or std.mem.eql(u8, normalized, "customevents")) { const CustomEvent = @import("event/CustomEvent.zig"); - return (try CustomEvent.init("", null, frame)).asEvent(); + return (try CustomEvent.init("", null, frame._page)).asEvent(); } if (std.mem.eql(u8, normalized, "keyboardevent")) { @@ -378,7 +378,7 @@ pub fn createEvent(_: *const Document, event_type: []const u8, frame: *Frame) !* if (std.mem.eql(u8, normalized, "messageevent")) { const MessageEvent = @import("event/MessageEvent.zig"); - return (try MessageEvent.init("", null, frame._session)).asEvent(); + return (try MessageEvent.init("", null, frame._page)).asEvent(); } if (std.mem.eql(u8, normalized, "uievent") or std.mem.eql(u8, normalized, "uievents")) { diff --git a/src/browser/webapi/Event.zig b/src/browser/webapi/Event.zig index b3053b8e..5692efe6 100644 --- a/src/browser/webapi/Event.zig +++ b/src/browser/webapi/Event.zig @@ -20,8 +20,8 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Frame = @import("../Frame.zig"); -const Session = @import("../Session.zig"); const Node = @import("Node.zig"); const EventTarget = @import("EventTarget.zig"); @@ -89,16 +89,16 @@ pub const Options = struct { composed: bool = false, }; -pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*Event { - const arena = try frame.getArena(.tiny, "Event"); - errdefer frame.releaseArena(arena); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*Event { + const arena = try page.getArena(.tiny, "Event"); + errdefer page.releaseArena(arena); const str = try String.init(arena, typ, .{}); return initWithTrusted(arena, str, opts_, false); } -pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*Event { - const arena = try session.getArena(.tiny, "Event.trusted"); - errdefer session.releaseArena(arena); +pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*Event { + const arena = try page.getArena(.tiny, "Event.trusted"); + errdefer page.releaseArena(arena); return initWithTrusted(arena, typ, opts_, true); } @@ -145,13 +145,12 @@ pub fn acquireRef(self: *Event) void { self._rc.acquire(); } -/// Force cleanup on Session shutdown. -pub fn deinit(self: *Event, session: *Session) void { - session.releaseArena(self._arena); +pub fn deinit(self: *Event, page: *Page) void { + page.releaseArena(self._arena); } -pub fn releaseRef(self: *Event, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *Event, page: *Page) void { + self._rc.release(self, page); } pub fn as(self: *Event, comptime T: type) *T { diff --git a/src/browser/webapi/EventTarget.zig b/src/browser/webapi/EventTarget.zig index 2b52c3ec..1e390900 100644 --- a/src/browser/webapi/EventTarget.zig +++ b/src/browser/webapi/EventTarget.zig @@ -19,7 +19,7 @@ const std = @import("std"); const js = @import("../js/js.zig"); -const Session = @import("../Session.zig"); +const Page = @import("../Page.zig"); const EventManager = @import("../EventManager.zig"); const Event = @import("Event.zig"); @@ -52,8 +52,8 @@ pub const Type = union(enum) { websocket: *@import("net/WebSocket.zig"), }; -pub fn init(session: *Session) !*EventTarget { - return session.factory.create(EventTarget{ +pub fn init(page: *Page) !*EventTarget { + return page.factory.create(EventTarget{ ._type = .generic, }); } @@ -67,7 +67,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, exec: *js.Execution) !bo switch (exec.context.global) { .frame => |frame| { event.acquireRef(); - defer _ = event.releaseRef(frame._session); + defer _ = event.releaseRef(frame._page); try frame._event_manager.dispatch(self, event); }, .worker => |wgs| try wgs.dispatch(self, event, null, .{}), diff --git a/src/browser/webapi/File.zig b/src/browser/webapi/File.zig index 9f4cfb46..a0dce126 100644 --- a/src/browser/webapi/File.zig +++ b/src/browser/webapi/File.zig @@ -20,7 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); -const Session = @import("../Session.zig"); +const Page = @import("../Page.zig"); const Blob = @import("Blob.zig"); @@ -29,10 +29,11 @@ const File = @This(); _proto: *Blob, // TODO: Implement File API. -pub fn init(session: *Session) !*File { +pub fn init(page: *Page) !*File { + const session = page.session; const arena = try session.getArena(.tiny, "File"); errdefer session.releaseArena(arena); - return session.factory.blob(arena, File{ ._proto = undefined }); + return page.factory.blob(arena, File{ ._proto = undefined }); } pub const JsApi = struct { diff --git a/src/browser/webapi/FileReader.zig b/src/browser/webapi/FileReader.zig index fcec74c2..ead58366 100644 --- a/src/browser/webapi/FileReader.zig +++ b/src/browser/webapi/FileReader.zig @@ -21,8 +21,8 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Frame = @import("../Frame.zig"); -const Session = @import("../Session.zig"); const EventTarget = @import("EventTarget.zig"); const ProgressEvent = @import("event/ProgressEvent.zig"); const Blob = @import("Blob.zig"); @@ -73,7 +73,7 @@ pub fn init(frame: *Frame) !*FileReader { }); } -pub fn deinit(self: *FileReader, session: *Session) void { +pub fn deinit(self: *FileReader, page: *Page) void { if (self._on_abort) |func| func.release(); if (self._on_error) |func| func.release(); if (self._on_load) |func| func.release(); @@ -81,11 +81,11 @@ pub fn deinit(self: *FileReader, session: *Session) void { if (self._on_load_start) |func| func.release(); if (self._on_progress) |func| func.release(); - session.releaseArena(self._arena); + page.releaseArena(self._arena); } -pub fn releaseRef(self: *FileReader, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *FileReader, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *FileReader) void { diff --git a/src/browser/webapi/IntersectionObserver.zig b/src/browser/webapi/IntersectionObserver.zig index 2a3f3d56..64e4e594 100644 --- a/src/browser/webapi/IntersectionObserver.zig +++ b/src/browser/webapi/IntersectionObserver.zig @@ -20,8 +20,8 @@ const lp = @import("lightpanda"); const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Frame = @import("../Frame.zig"); -const Session = @import("../Session.zig"); const Node = @import("Node.zig"); const Element = @import("Element.zig"); @@ -110,18 +110,18 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, frame: *Frame) ! return self; } -pub fn deinit(self: *IntersectionObserver, session: *Session) void { +pub fn deinit(self: *IntersectionObserver, page: *Page) void { self._callback.release(); for (self._pending_entries.items) |entry| { // These were never handed to v8, they do not have a corresponding // FinalizerCallback. We 100% own them. - entry.deinit(session); + entry.deinit(page); } - session.releaseArena(self._arena); + page.releaseArena(self._arena); } -pub fn releaseRef(self: *IntersectionObserver, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *IntersectionObserver, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *IntersectionObserver) void { @@ -164,7 +164,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) v while (j < self._pending_entries.items.len) { if (self._pending_entries.items[j]._target == target) { const entry = self._pending_entries.swapRemove(j); - entry.deinit(frame._session); + entry.deinit(frame._page); } else { j += 1; } @@ -180,7 +180,7 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, frame: *Frame) v pub fn disconnect(self: *IntersectionObserver, frame: *Frame) void { for (self._pending_entries.items) |entry| { - entry.deinit(frame._session); + entry.deinit(frame._page); } self._pending_entries.clearRetainingCapacity(); self._previous_states.clearRetainingCapacity(); @@ -330,12 +330,12 @@ pub const IntersectionObserverEntry = struct { _intersection_ratio: f64, _is_intersecting: bool, - pub fn deinit(self: *IntersectionObserverEntry, session: *Session) void { - session.releaseArena(self._arena); + pub fn deinit(self: *IntersectionObserverEntry, page: *Page) void { + page.releaseArena(self._arena); } - pub fn releaseRef(self: *IntersectionObserverEntry, session: *Session) void { - self._rc.release(self, session); + pub fn releaseRef(self: *IntersectionObserverEntry, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *IntersectionObserverEntry) void { diff --git a/src/browser/webapi/MessagePort.zig b/src/browser/webapi/MessagePort.zig index 58b48674..42831378 100644 --- a/src/browser/webapi/MessagePort.zig +++ b/src/browser/webapi/MessagePort.zig @@ -131,7 +131,7 @@ const PostMessageCallback = struct { .data = .{ .value = self.message }, .origin = "", .source = null, - }, frame._session) catch |err| { + }, frame._page) catch |err| { log.err(.dom, "MessagePort.postMessage", .{ .err = err }); return null; }).asEvent(); diff --git a/src/browser/webapi/MutationObserver.zig b/src/browser/webapi/MutationObserver.zig index a60e25a1..2e538207 100644 --- a/src/browser/webapi/MutationObserver.zig +++ b/src/browser/webapi/MutationObserver.zig @@ -20,8 +20,8 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Frame = @import("../Frame.zig"); -const Session = @import("../Session.zig"); const Node = @import("Node.zig"); const Element = @import("Element.zig"); @@ -86,18 +86,18 @@ pub fn init(callback: js.Function.Temp, frame: *Frame) !*MutationObserver { return self; } -pub fn deinit(self: *MutationObserver, session: *Session) void { +pub fn deinit(self: *MutationObserver, page: *Page) void { for (self._pending_records.items) |record| { // These were never handed to v8, they do not have a corresponding // FinalizerCallback. We 100% own them. - record.deinit(session); + record.deinit(page); } self._callback.release(); - session.releaseArena(self._arena); + page.releaseArena(self._arena); } -pub fn releaseRef(self: *MutationObserver, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *MutationObserver, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *MutationObserver) void { @@ -178,7 +178,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, pub fn disconnect(self: *MutationObserver, frame: *Frame) void { for (self._pending_records.items) |record| { - record.deinit(frame._session); + record.deinit(frame._page); } self._pending_records.clearRetainingCapacity(); @@ -375,11 +375,11 @@ pub const MutationRecord = struct { characterData, }; - pub fn deinit(self: *MutationRecord, session: *Session) void { + pub fn deinit(self: *MutationRecord, session: *Page) void { session.releaseArena(self._arena); } - pub fn releaseRef(self: *MutationRecord, session: *Session) void { + pub fn releaseRef(self: *MutationRecord, session: *Page) void { self._rc.release(self, session); } diff --git a/src/browser/webapi/Permissions.zig b/src/browser/webapi/Permissions.zig index e530a8a6..3485e11a 100644 --- a/src/browser/webapi/Permissions.zig +++ b/src/browser/webapi/Permissions.zig @@ -18,9 +18,10 @@ const std = @import("std"); const lp = @import("lightpanda"); + const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Frame = @import("../Frame.zig"); -const Session = @import("../Session.zig"); const Allocator = std.mem.Allocator; @@ -56,12 +57,12 @@ const PermissionStatus = struct { _name: []const u8, _state: []const u8, - pub fn deinit(self: *PermissionStatus, session: *Session) void { - session.releaseArena(self._arena); + pub fn deinit(self: *PermissionStatus, page: *Page) void { + page.releaseArena(self._arena); } - pub fn releaseRef(self: *PermissionStatus, session: *Session) void { - self._rc.release(self, session); + pub fn releaseRef(self: *PermissionStatus, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *PermissionStatus) void { diff --git a/src/browser/webapi/Selection.zig b/src/browser/webapi/Selection.zig index f6fa694b..a9724152 100644 --- a/src/browser/webapi/Selection.zig +++ b/src/browser/webapi/Selection.zig @@ -20,8 +20,8 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Frame = @import("../Frame.zig"); -const Session = @import("../Session.zig"); const Range = @import("Range.zig"); const AbstractRange = @import("AbstractRange.zig"); @@ -39,15 +39,15 @@ _direction: SelectionDirection = .none, pub const init: Selection = .{}; -pub fn deinit(self: *Selection, session: *Session) void { +pub fn deinit(self: *Selection, page: *Page) void { if (self._range) |r| { - r.asAbstractRange().releaseRef(session); + r.asAbstractRange().releaseRef(page); self._range = null; } } -pub fn releaseRef(self: *Selection, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *Selection, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *Selection) void { @@ -55,7 +55,7 @@ pub fn acquireRef(self: *Selection) void { } fn dispatchSelectionChangeEvent(frame: *Frame) !void { - const event = try Event.init("selectionchange", .{}, frame); + const event = try Event.init("selectionchange", .{}, frame._page); try frame._event_manager.dispatch(frame.document.asEventTarget(), event); } @@ -703,7 +703,7 @@ pub fn toString(self: *const Selection, frame: *Frame) ![]const u8 { fn setRange(self: *Selection, new_range: ?*Range, frame: *Frame) void { if (self._range) |existing| { - _ = existing.asAbstractRange().releaseRef(frame._session); + _ = existing.asAbstractRange().releaseRef(frame._page); } if (new_range) |nr| { nr.asAbstractRange().acquireRef(); diff --git a/src/browser/webapi/TreeWalker.zig b/src/browser/webapi/TreeWalker.zig index 77946c9e..f21c304b 100644 --- a/src/browser/webapi/TreeWalker.zig +++ b/src/browser/webapi/TreeWalker.zig @@ -160,8 +160,8 @@ pub fn TreeWalker(comptime mode: Mode) type { test "TreeWalker: skipChildren" { const testing = @import("../../testing.zig"); - const frame = try testing.test_session.createFrame(); - defer testing.test_session.removeFrame(); + const frame = try testing.test_session.createPage(); + defer testing.test_session.removePage(); const doc = frame.window._document; //
diff --git a/src/browser/webapi/URL.zig b/src/browser/webapi/URL.zig index 76f12142..ba8a6083 100644 --- a/src/browser/webapi/URL.zig +++ b/src/browser/webapi/URL.zig @@ -274,7 +274,7 @@ pub fn revokeObjectURL(url: []const u8, exec: *const Execution) void { switch (exec.context.global) { inline else => |g| { if (g._blob_urls.fetchRemove(url)) |entry| { - entry.value.releaseRef(g._session); + entry.value.releaseRef(g._page); } }, } diff --git a/src/browser/webapi/Window.zig b/src/browser/webapi/Window.zig index 96df3ea7..83a90df9 100644 --- a/src/browser/webapi/Window.zig +++ b/src/browser/webapi/Window.zig @@ -362,7 +362,7 @@ pub fn reportError(self: *Window, err: js.Value, frame: *Frame) !void { .message = err.toStringSlice() catch "Unknown error", .bubbles = false, .cancelable = true, - }, frame._session); + }, frame._page); // Invoke window.onerror callback if set (per WHATWG spec, this is called // with 5 arguments: message, source, lineno, colno, error) @@ -530,17 +530,16 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void frame, struct { fn dispatch(_frame: *anyopaque) anyerror!?u32 { - const p: *Frame = @ptrCast(@alignCast(_frame)); - const pos = &p.window._scroll_pos; + const f: *Frame = @ptrCast(@alignCast(_frame)); + const pos = &f.window._scroll_pos; // If the state isn't scroll, we can ignore safely to throttle // the events. if (pos.state != .scroll) { return null; } - const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, p._session); - try p._event_manager.dispatch(p.document.asEventTarget(), event); - + const event = try Event.initTrusted(comptime .wrap("scroll"), .{ .bubbles = true }, f._page); + try f._event_manager.dispatch(f.document.asEventTarget(), event); pos.state = .end; return null; @@ -554,8 +553,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void frame, struct { fn dispatch(_frame: *anyopaque) anyerror!?u32 { - const p: *Frame = @ptrCast(@alignCast(_frame)); - const pos = &p.window._scroll_pos; + const f: *Frame = @ptrCast(@alignCast(_frame)); + const pos = &f.window._scroll_pos; // Dispatch only if the state is .end. // If a scroll is pending, retry in 10ms. // If the state is .end, the event has been dispatched, so @@ -565,9 +564,8 @@ pub fn scrollTo(self: *Window, opts: ScrollToOpts, y: ?i32, frame: *Frame) !void .end => {}, .done => return null, } - const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, p._session); - try p._event_manager.dispatch(p.document.asEventTarget(), event); - + const event = try Event.initTrusted(comptime .wrap("scrollend"), .{ .bubbles = true }, f._page); + try f._event_manager.dispatch(f.document.asEventTarget(), event); pos.state = .done; return null; @@ -622,7 +620,7 @@ pub fn unhandledPromiseRejection(self: *Window, no_handler: bool, rejection: js. const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{ .reason = if (rejection.reason()) |r| try r.temp() else null, .promise = try rejection.promise().temp(), - }, frame._session)).asEvent(); + }, frame._page)).asEvent(); try frame._event_manager.dispatchDirect(target, event, attribute_callback, .{ .context = "window.unhandledrejection" }); } } @@ -811,7 +809,7 @@ const PostMessageCallback = struct { .source = self.source, .bubbles = false, .cancelable = false, - }, frame._session)).asEvent(); + }, frame._page)).asEvent(); try frame._event_manager.dispatchDirect(event_target, event, window._on_message, .{ .context = "window.postMessage" }); } diff --git a/src/browser/webapi/Worker.zig b/src/browser/webapi/Worker.zig index 3d0bacaf..86eb985f 100644 --- a/src/browser/webapi/Worker.zig +++ b/src/browser/webapi/Worker.zig @@ -24,7 +24,6 @@ const http = @import("../../network/http.zig"); const URL = @import("../URL.zig"); const Frame = @import("../Frame.zig"); -const Session = @import("../Session.zig"); const HttpClient = @import("../HttpClient.zig"); const Blob = @import("Blob.zig"); @@ -71,7 +70,7 @@ pub fn init(url: []const u8, exec: *Execution) !*Worker { errdefer session.releaseArena(arena); const resolved_url = try URL.resolve(arena, exec.url.*, url, .{}); - const self = try session.factory.eventTargetWithAllocator(arena, Worker{ + const self = try frame._page.factory.eventTargetWithAllocator(arena, Worker{ ._arena = arena, ._proto = undefined, ._frame = frame, @@ -216,7 +215,6 @@ fn fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Tem fn _fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Temp) !void { const frame = self._frame; - const session = frame._session; const target = self.asEventTarget(); const on_error = self._on_error; @@ -232,7 +230,7 @@ fn _fireErrorEvent(self: *Worker, message: []const u8, error_value: ?js.Value.Te .filename = self._url, .bubbles = false, .cancelable = true, - }, session); + }, frame._page); try frame._event_manager.dispatchDirect(target, error_event.asEvent(), on_error, .{ .context = "Worker.onerror", @@ -354,7 +352,7 @@ const ReceiveMessageCallback = struct { .data = .{ .string = @errorName(err) }, .bubbles = false, .cancelable = false, - }, frame._session)).asEvent(); + }, frame._page)).asEvent(); try frame._event_manager.dispatchDirect(target, event, on_messageerror, .{ .context = "Worker.messageerror" }); return null; }; @@ -371,7 +369,7 @@ const ReceiveMessageCallback = struct { .data = .{ .value = data }, .bubbles = false, .cancelable = false, - }, frame._session)).asEvent(); + }, frame._page)).asEvent(); try frame._event_manager.dispatchDirect(target, event, on_message, .{ .context = "Worker.receiveMessage" }); diff --git a/src/browser/webapi/WorkerGlobalScope.zig b/src/browser/webapi/WorkerGlobalScope.zig index e6c35b05..42280fe7 100644 --- a/src/browser/webapi/WorkerGlobalScope.zig +++ b/src/browser/webapi/WorkerGlobalScope.zig @@ -24,6 +24,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const JS = @import("../js/js.zig"); +const Page = @import("../Page.zig"); const Factory = @import("../Factory.zig"); const Session = @import("../Session.zig"); const EventManagerBase = @import("../EventManagerBase.zig"); @@ -48,6 +49,7 @@ const WorkerGlobalScope = @This(); // can access these the same for a Page of a WGS. // These fields represent the "Page"-like component of the WGS _session: *Session, +_page: *Page, _factory: *Factory, _identity: JS.Identity = .{}, arena: Allocator, @@ -83,12 +85,12 @@ _on_messageerror: ?JS.Function.Global = null, pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { const arena = worker._arena; const parent = worker._frame; - const session = parent._session; - const factory = &session.factory; + const session = worker._frame._session; const call_arena = try session.getArena(.small, "WorkerGlobalScope.call_arena"); errdefer session.releaseArena(call_arena); + const factory = parent._factory; const self = try factory.eventTargetWithAllocator(arena, WorkerGlobalScope{ .url = url, .arena = arena, @@ -96,6 +98,7 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { .js = undefined, .call_arena = call_arena, ._session = session, + ._page = parent._page, ._identity = .{}, ._proto = undefined, ._factory = factory, @@ -115,13 +118,13 @@ pub fn init(worker: *Worker, url: [:0]const u8) !*WorkerGlobalScope { pub fn deinit(self: *WorkerGlobalScope) void { self._identity.deinit(); - const session = self._session; + const page = self._page; var it = self._blob_urls.valueIterator(); while (it.next()) |blob| { - blob.*.releaseRef(session); + blob.*.releaseRef(page); } - session.browser.env.destroyContext(self.js); - session.releaseArena(self.call_arena); + page.session.browser.env.destroyContext(self.js); + page.releaseArena(self.call_arena); } pub fn base(self: *const WorkerGlobalScope) [:0]const u8 { @@ -148,7 +151,7 @@ pub fn dispatch( target, event, handler, - self._session, + self._page, opts, ); } @@ -282,7 +285,7 @@ pub fn unhandledPromiseRejection(self: *WorkerGlobalScope, no_handler: bool, rej const event = (try @import("event/PromiseRejectionEvent.zig").init(event_name, .{ .reason = if (rejection.reason()) |r| try r.temp() else null, .promise = try rejection.promise().temp(), - }, self._session)).asEvent(); + }, self._page)).asEvent(); try self.dispatch(target, event, attribute_callback, .{}); } } @@ -299,7 +302,7 @@ pub fn reportError(self: *WorkerGlobalScope, err: JS.Value) !void { .message = err.toStringSlice() catch "Unknown error", .bubbles = false, .cancelable = true, - }, self._session); + }, self._page); // Invoke onerror callback if set (per WHATWG spec, this is called // with 5 arguments: message, source, lineno, colno, error) @@ -392,7 +395,7 @@ const ReceiveMessageCallback = struct { const event = (try MessageEvent.initTrusted(comptime .wrap("messageerror"), .{ .bubbles = false, .cancelable = false, - }, worker_scope._session)).asEvent(); + }, worker_scope._page)).asEvent(); try worker_scope.dispatch(target, event, on_messageerror, .{}); return null; } @@ -409,7 +412,7 @@ const ReceiveMessageCallback = struct { .data = .{ .value = self.data.? }, .bubbles = false, .cancelable = false, - }, worker_scope._session)).asEvent(); + }, worker_scope._page)).asEvent(); try worker_scope.dispatch(target, event, on_message, .{}); return null; } diff --git a/src/browser/webapi/animation/Animation.zig b/src/browser/webapi/animation/Animation.zig index 02cf8236..91ddff41 100644 --- a/src/browser/webapi/animation/Animation.zig +++ b/src/browser/webapi/animation/Animation.zig @@ -19,8 +19,8 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); const log = lp.log; const Allocator = std.mem.Allocator; @@ -64,12 +64,12 @@ pub fn init(frame: *Frame) !*Animation { return self; } -pub fn deinit(self: *Animation, session: *Session) void { - session.releaseArena(self._arena); +pub fn deinit(self: *Animation, page: *Page) void { + page.releaseArena(self._arena); } -pub fn releaseRef(self: *Animation, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *Animation, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *Animation) void { @@ -211,7 +211,7 @@ fn update(ctx: *anyopaque) !?u32 { } // No future change scheduled, set the object weak for garbage collection. - self.releaseRef(self._frame._session); + self.releaseRef(self._frame._page); return null; } diff --git a/src/browser/webapi/canvas/OffscreenCanvas.zig b/src/browser/webapi/canvas/OffscreenCanvas.zig index e43de01c..ce83697f 100644 --- a/src/browser/webapi/canvas/OffscreenCanvas.zig +++ b/src/browser/webapi/canvas/OffscreenCanvas.zig @@ -72,7 +72,7 @@ pub fn getContext(_: *OffscreenCanvas, context_type: []const u8, frame: *Frame) /// Returns a Promise that resolves to a Blob containing the image. /// Since we have no actual rendering, this returns an empty blob. pub fn convertToBlob(_: *OffscreenCanvas, frame: *Frame) !js.Promise { - const blob = try Blob.init(null, null, frame._session); + const blob = try Blob.init(null, null, frame._page); return frame.js.local.?.resolvePromise(blob); } diff --git a/src/browser/webapi/collections/ChildNodes.zig b/src/browser/webapi/collections/ChildNodes.zig index ecce1baf..0bda6560 100644 --- a/src/browser/webapi/collections/ChildNodes.zig +++ b/src/browser/webapi/collections/ChildNodes.zig @@ -19,9 +19,10 @@ const std = @import("std"); const js = @import("../../js/js.zig"); -const Node = @import("../Node.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); + +const Node = @import("../Node.zig"); const GenericIterator = @import("iterator.zig").Entry; // Optimized for node.childNodes, which has to be a live list. @@ -55,8 +56,8 @@ pub fn init(node: *Node, frame: *Frame) !*ChildNodes { return self; } -pub fn deinit(self: *const ChildNodes, session: *Session) void { - session.releaseArena(self._arena); +pub fn deinit(self: *const ChildNodes, page: *Page) void { + page.releaseArena(self._arena); } pub fn length(self: *ChildNodes, frame: *Frame) !u32 { diff --git a/src/browser/webapi/collections/NodeList.zig b/src/browser/webapi/collections/NodeList.zig index 440b3c79..38d0525f 100644 --- a/src/browser/webapi/collections/NodeList.zig +++ b/src/browser/webapi/collections/NodeList.zig @@ -20,8 +20,9 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); + const Node = @import("../Node.zig"); const ChildNodes = @import("ChildNodes.zig"); @@ -41,16 +42,16 @@ _data: union(enum) { }, _rc: lp.RC(u32) = .{}, -pub fn deinit(self: *NodeList, session: *Session) void { +pub fn deinit(self: *NodeList, page: *Page) void { switch (self._data) { - .child_nodes => |cn| cn.deinit(session), - .selector_list => |list| list.deinit(session), + .child_nodes => |cn| cn.deinit(page), + .selector_list => |list| list.deinit(page), else => {}, } } -pub fn releaseRef(self: *NodeList, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *NodeList, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *NodeList) void { @@ -115,12 +116,12 @@ const Iterator = struct { const Entry = struct { u32, *Node }; - pub fn deinit(self: *Iterator, session: *Session) void { - self.list.deinit(session); + pub fn deinit(self: *Iterator, page: *Page) void { + self.list.deinit(page); } - pub fn releaseRef(self: *Iterator, session: *Session) void { - self.list.releaseRef(session); + pub fn releaseRef(self: *Iterator, page: *Page) void { + self.list.releaseRef(page); } pub fn acquireRef(self: *Iterator) void { diff --git a/src/browser/webapi/collections/iterator.zig b/src/browser/webapi/collections/iterator.zig index 24d1d52e..ab45d708 100644 --- a/src/browser/webapi/collections/iterator.zig +++ b/src/browser/webapi/collections/iterator.zig @@ -18,9 +18,11 @@ const std = @import("std"); const lp = @import("lightpanda"); + const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); + const Execution = js.Execution; pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type { @@ -48,15 +50,15 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type { return self; } - pub fn deinit(self: *Self, session: *Session) void { + pub fn deinit(self: *Self, page: *Page) void { if (@hasDecl(Inner, "releaseRef")) { - self._inner.releaseRef(session); + self._inner.releaseRef(page); } - session.factory.destroy(self); + page.factory.destroy(self); } - pub fn releaseRef(self: *Self, session: *Session) void { - self._rc.release(self, session); + pub fn releaseRef(self: *Self, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *Self) void { diff --git a/src/browser/webapi/css/FontFace.zig b/src/browser/webapi/css/FontFace.zig index 41bb6743..9b6cd636 100644 --- a/src/browser/webapi/css/FontFace.zig +++ b/src/browser/webapi/css/FontFace.zig @@ -18,9 +18,10 @@ const std = @import("std"); const lp = @import("lightpanda"); + const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); const Allocator = std.mem.Allocator; @@ -44,12 +45,12 @@ pub fn init(family: []const u8, source: []const u8, frame: *Frame) !*FontFace { return self; } -pub fn deinit(self: *FontFace, session: *Session) void { - session.releaseArena(self._arena); +pub fn deinit(self: *FontFace, page: *Page) void { + page.releaseArena(self._arena); } -pub fn releaseRef(self: *FontFace, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *FontFace, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *FontFace) void { diff --git a/src/browser/webapi/css/FontFaceSet.zig b/src/browser/webapi/css/FontFaceSet.zig index 6d4695da..f0bd994f 100644 --- a/src/browser/webapi/css/FontFaceSet.zig +++ b/src/browser/webapi/css/FontFaceSet.zig @@ -18,12 +18,15 @@ const std = @import("std"); const lp = @import("lightpanda"); + const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); -const FontFace = @import("FontFace.zig"); -const EventTarget = @import("../EventTarget.zig"); + const Event = @import("../Event.zig"); +const EventTarget = @import("../EventTarget.zig"); + +const FontFace = @import("FontFace.zig"); const Allocator = std.mem.Allocator; @@ -43,12 +46,12 @@ pub fn init(frame: *Frame) !*FontFaceSet { }); } -pub fn deinit(self: *FontFaceSet, session: *Session) void { - session.releaseArena(self._arena); +pub fn deinit(self: *FontFaceSet, page: *Page) void { + page.releaseArena(self._arena); } -pub fn releaseRef(self: *FontFaceSet, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *FontFaceSet, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *FontFaceSet) void { @@ -80,13 +83,13 @@ pub fn load(self: *FontFaceSet, font: []const u8, frame: *Frame) !js.Promise { // Dispatch loading event const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "loading", null)) { - const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame._session); + const event = try Event.initTrusted(comptime .wrap("loading"), .{}, frame._page); try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" }); } // Dispatch loadingdone event if (frame._event_manager.hasDirectListeners(target, "loadingdone", null)) { - const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame._session); + const event = try Event.initTrusted(comptime .wrap("loadingdone"), .{}, frame._page); try frame._event_manager.dispatchDirect(target, event, null, .{ .context = "load font face set" }); } diff --git a/src/browser/webapi/element/html/Input.zig b/src/browser/webapi/element/html/Input.zig index 8d503723..be82f254 100644 --- a/src/browser/webapi/element/html/Input.zig +++ b/src/browser/webapi/element/html/Input.zig @@ -102,7 +102,7 @@ pub fn setOnSelectionChange(self: *Input, listener: ?js.Function) !void { } fn dispatchSelectionChangeEvent(self: *Input, frame: *Frame) !void { - const event = try Event.init("selectionchange", .{ .bubbles = true }, frame); + const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page); try frame._event_manager.dispatch(self.asElement().asEventTarget(), event); } @@ -372,7 +372,7 @@ pub fn setAutocomplete(self: *Input, autocomplete: []const u8, frame: *Frame) !v pub fn select(self: *Input, frame: *Frame) !void { const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0; try self.setSelectionRange(0, len, null, frame); - const event = try Event.init("select", .{ .bubbles = true }, frame); + const event = try Event.init("select", .{ .bubbles = true }, frame._page); try frame._event_manager.dispatch(self.asElement().asEventTarget(), event); } diff --git a/src/browser/webapi/element/html/Media.zig b/src/browser/webapi/element/html/Media.zig index 9fc5d65a..27be59d9 100644 --- a/src/browser/webapi/element/html/Media.zig +++ b/src/browser/webapi/element/html/Media.zig @@ -166,7 +166,7 @@ pub fn load(self: *Media, frame: *Frame) !void { } fn dispatchEvent(self: *Media, name: []const u8, frame: *Frame) !void { - const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, frame); + const event = try Event.init(name, .{ .bubbles = false, .cancelable = false }, frame._page); try frame._event_manager.dispatch(self.asElement().asEventTarget(), event); } diff --git a/src/browser/webapi/element/html/TextArea.zig b/src/browser/webapi/element/html/TextArea.zig index c81f165e..89910e92 100644 --- a/src/browser/webapi/element/html/TextArea.zig +++ b/src/browser/webapi/element/html/TextArea.zig @@ -52,7 +52,7 @@ pub fn setOnSelectionChange(self: *TextArea, listener: ?js.Function) !void { } fn dispatchSelectionChangeEvent(self: *TextArea, frame: *Frame) !void { - const event = try Event.init("selectionchange", .{ .bubbles = true }, frame); + const event = try Event.init("selectionchange", .{ .bubbles = true }, frame._page); try frame._event_manager.dispatch(self.asElement().asEventTarget(), event); } @@ -142,7 +142,7 @@ pub fn setRequired(self: *TextArea, required: bool, frame: *Frame) !void { pub fn select(self: *TextArea, frame: *Frame) !void { const len = if (self._value) |v| @as(u32, @intCast(v.len)) else 0; try self.setSelectionRange(0, len, null, frame); - const event = try Event.init("select", .{ .bubbles = true }, frame); + const event = try Event.init("select", .{ .bubbles = true }, frame._page); try frame._event_manager.dispatch(self.asElement().asEventTarget(), event); } diff --git a/src/browser/webapi/encoding/TextDecoder.zig b/src/browser/webapi/encoding/TextDecoder.zig index 6c7cf6c5..e32ac3a5 100644 --- a/src/browser/webapi/encoding/TextDecoder.zig +++ b/src/browser/webapi/encoding/TextDecoder.zig @@ -18,10 +18,12 @@ const std = @import("std"); const lp = @import("lightpanda"); + const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); + const html5ever = @import("../../parser/html5ever.zig"); -const Session = @import("../../Session.zig"); const Allocator = std.mem.Allocator; const TextDecoder = @This(); @@ -41,7 +43,7 @@ const InitOpts = struct { ignoreBOM: bool = false, }; -pub fn init(label_: ?[]const u8, opts_: ?InitOpts, session: *Session) !*TextDecoder { +pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder { const label = label_ orelse "utf-8"; const info = html5ever.encoding_for_label(label.ptr, label.len); @@ -55,8 +57,8 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, session: *Session) !*TextDeco return error.RangeError; } - const arena = try session.getArena(.large, "TextDecoder"); - errdefer session.releaseArena(arena); + const arena = try page.getArena(.large, "TextDecoder"); + errdefer page.releaseArena(arena); const opts = opts_ orelse InitOpts{}; const self = try arena.create(TextDecoder); @@ -73,15 +75,15 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, session: *Session) !*TextDeco return self; } -pub fn deinit(self: *TextDecoder, session: *Session) void { +pub fn deinit(self: *TextDecoder, page: *Page) void { if (self._decoder) |decoder| { html5ever.encoding_decoder_free(decoder); } - session.releaseArena(self._arena); + page.releaseArena(self._arena); } -pub fn releaseRef(self: *TextDecoder, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *TextDecoder, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *TextDecoder) void { diff --git a/src/browser/webapi/event/CustomEvent.zig b/src/browser/webapi/event/CustomEvent.zig index 55455d33..986d3242 100644 --- a/src/browser/webapi/event/CustomEvent.zig +++ b/src/browser/webapi/event/CustomEvent.zig @@ -20,8 +20,9 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); + const Event = @import("../Event.zig"); const String = lp.String; @@ -39,13 +40,13 @@ const CustomEventOptions = struct { const Options = Event.inheritOptions(CustomEvent, CustomEventOptions); -pub fn init(typ: []const u8, opts_: ?Options, frame: *Frame) !*CustomEvent { - const arena = try frame.getArena(.tiny, "CustomEvent"); - errdefer frame.releaseArena(arena); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CustomEvent { + const arena = try page.getArena(.tiny, "CustomEvent"); + errdefer page.releaseArena(arena); const type_string = try String.init(arena, typ, .{}); const opts = opts_ orelse Options{}; - const event = try frame._factory.event( + const event = try page.factory.event( arena, type_string, CustomEvent{ @@ -75,20 +76,21 @@ pub fn initCustomEvent( self._detail = detail_; } -pub fn deinit(self: *CustomEvent, session: *Session) void { +pub fn deinit(self: *CustomEvent, page: *Page) void { if (self._detail) |d| { d.release(); } - self._proto.deinit(session); + self._proto.deinit(page); +} + +pub fn releaseRef(self: *CustomEvent, page: *Page) void { + self._proto._rc.release(self, page); } pub fn acquireRef(self: *CustomEvent) void { self._proto.acquireRef(); } -pub fn releaseRef(self: *CustomEvent, session: *Session) void { - self._proto._rc.release(self, session); -} pub fn asEvent(self: *CustomEvent) *Event { return self._proto; diff --git a/src/browser/webapi/event/ErrorEvent.zig b/src/browser/webapi/event/ErrorEvent.zig index d3d60bf7..de7c1d92 100644 --- a/src/browser/webapi/event/ErrorEvent.zig +++ b/src/browser/webapi/event/ErrorEvent.zig @@ -20,7 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); -const Session = @import("../../Session.zig"); +const Page = @import("../../Page.zig"); const Event = @import("../Event.zig"); @@ -47,23 +47,23 @@ pub const ErrorEventOptions = struct { const Options = Event.inheritOptions(ErrorEvent, ErrorEventOptions); -pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*ErrorEvent { - const arena = try session.getArena(.small, "ErrorEvent"); - errdefer session.releaseArena(arena); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*ErrorEvent { + const arena = try page.getArena(.small, "ErrorEvent"); + errdefer page.releaseArena(arena); const type_string = try String.init(arena, typ, .{}); - return initWithTrusted(arena, type_string, opts_, false, session); + return initWithTrusted(arena, type_string, opts_, false, page); } -pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*ErrorEvent { - const arena = try session.getArena(.small, "ErrorEvent.trusted"); - errdefer session.releaseArena(arena); - return initWithTrusted(arena, typ, opts_, true, session); +pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*ErrorEvent { + const arena = try page.getArena(.small, "ErrorEvent.trusted"); + errdefer page.releaseArena(arena); + return initWithTrusted(arena, typ, opts_, true, page); } -fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, session: *Session) !*ErrorEvent { +fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*ErrorEvent { const opts = opts_ orelse Options{}; - const event = try session.factory.event( + const event = try page.factory.event( arena, typ, ErrorEvent{ @@ -81,21 +81,21 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool return event; } -pub fn deinit(self: *ErrorEvent, session: *Session) void { +pub fn deinit(self: *ErrorEvent, page: *Page) void { if (self._error) |e| { e.release(); } - self._proto.deinit(session); + self._proto.deinit(page); +} + +pub fn releaseRef(self: *ErrorEvent, page: *Page) void { + self._proto._rc.release(self, page); } pub fn acquireRef(self: *ErrorEvent) void { self._proto.acquireRef(); } -pub fn releaseRef(self: *ErrorEvent, session: *Session) void { - self._proto._rc.release(self, session); -} - pub fn asEvent(self: *ErrorEvent) *Event { return self._proto; } diff --git a/src/browser/webapi/event/MessageEvent.zig b/src/browser/webapi/event/MessageEvent.zig index 40f0f699..6f2c761e 100644 --- a/src/browser/webapi/event/MessageEvent.zig +++ b/src/browser/webapi/event/MessageEvent.zig @@ -20,7 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); -const Session = @import("../../Session.zig"); +const Page = @import("../../Page.zig"); const Event = @import("../Event.zig"); const Window = @import("../Window.zig"); @@ -50,23 +50,23 @@ pub const Data = union(enum) { const Options = Event.inheritOptions(MessageEvent, MessageEventOptions); -pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*MessageEvent { - const arena = try session.getArena(.small, "MessageEvent"); - errdefer session.releaseArena(arena); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*MessageEvent { + const arena = try page.getArena(.small, "MessageEvent"); + errdefer page.releaseArena(arena); const type_string = try String.init(arena, typ, .{}); - return initWithTrusted(arena, type_string, opts_, false, session); + return initWithTrusted(arena, type_string, opts_, false, page); } -pub fn initTrusted(typ: String, opts_: ?Options, session: *Session) !*MessageEvent { - const arena = try session.getArena(.small, "MessageEvent.trusted"); - errdefer session.releaseArena(arena); - return initWithTrusted(arena, typ, opts_, true, session); +pub fn initTrusted(typ: String, opts_: ?Options, page: *Page) !*MessageEvent { + const arena = try page.getArena(.small, "MessageEvent.trusted"); + errdefer page.releaseArena(arena); + return initWithTrusted(arena, typ, opts_, true, page); } -fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, session: *Session) !*MessageEvent { +fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool, page: *Page) !*MessageEvent { const opts = opts_ orelse Options{}; - const event = try session.factory.event( + const event = try page.factory.event( arena, typ, MessageEvent{ @@ -81,23 +81,23 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool return event; } -pub fn deinit(self: *MessageEvent, session: *Session) void { +pub fn deinit(self: *MessageEvent, page: *Page) void { if (self._data) |d| { switch (d) { .value => |js_val| js_val.release(), - .blob => |blob| blob.releaseRef(session), + .blob => |blob| blob.releaseRef(page), .string, .arraybuffer => {}, } } - self._proto.deinit(session); + self._proto.deinit(page); } pub fn acquireRef(self: *MessageEvent) void { self._proto.acquireRef(); } -pub fn releaseRef(self: *MessageEvent, session: *Session) void { - self._proto._rc.release(self, session); +pub fn releaseRef(self: *MessageEvent, page: *Page) void { + self._proto._rc.release(self, page); } pub fn asEvent(self: *MessageEvent) *Event { diff --git a/src/browser/webapi/event/PromiseRejectionEvent.zig b/src/browser/webapi/event/PromiseRejectionEvent.zig index 7de0feeb..0ef2aeb4 100644 --- a/src/browser/webapi/event/PromiseRejectionEvent.zig +++ b/src/browser/webapi/event/PromiseRejectionEvent.zig @@ -19,7 +19,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); -const Session = @import("../../Session.zig"); +const Page = @import("../../Page.zig"); const Event = @import("../Event.zig"); @@ -38,13 +38,13 @@ const PromiseRejectionEventOptions = struct { const Options = Event.inheritOptions(PromiseRejectionEvent, PromiseRejectionEventOptions); -pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*PromiseRejectionEvent { - const arena = try session.getArena(.tiny, "PromiseRejectionEvent"); - errdefer session.releaseArena(arena); +pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEvent { + const arena = try page.getArena(.tiny, "PromiseRejectionEvent"); + errdefer page.releaseArena(arena); const type_string = try String.init(arena, typ, .{}); const opts = opts_ orelse Options{}; - const event = try session.factory.event( + const event = try page.factory.event( arena, type_string, PromiseRejectionEvent{ @@ -58,24 +58,24 @@ pub fn init(typ: []const u8, opts_: ?Options, session: *Session) !*PromiseReject return event; } -pub fn deinit(self: *PromiseRejectionEvent, session: *Session) void { +pub fn deinit(self: *PromiseRejectionEvent, page: *Page) void { if (self._reason) |r| { r.release(); } if (self._promise) |p| { p.release(); } - self._proto.deinit(session); + self._proto.deinit(page); +} + +pub fn releaseRef(self: *PromiseRejectionEvent, page: *Page) void { + self._proto._rc.release(self, page); } pub fn acquireRef(self: *PromiseRejectionEvent) void { self._proto.acquireRef(); } -pub fn releaseRef(self: *PromiseRejectionEvent, session: *Session) void { - self._proto._rc.release(self, session); -} - pub fn asEvent(self: *PromiseRejectionEvent) *Event { return self._proto; } diff --git a/src/browser/webapi/net/Fetch.zig b/src/browser/webapi/net/Fetch.zig index f37cf19b..623037d6 100644 --- a/src/browser/webapi/net/Fetch.zig +++ b/src/browser/webapi/net/Fetch.zig @@ -62,7 +62,7 @@ pub fn init(input: Input, options: ?InitOpts, frame: *Frame) !js.Promise { } const response = try Response.init(null, .{ .status = 0 }, &frame.js.execution); - errdefer response.deinit(frame._session); + errdefer response.deinit(frame._page); const fetch = try response._arena.create(Fetch); fetch.* = .{ @@ -248,7 +248,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(self._frame._session); + response.deinit(self._frame._page); }; var ls: js.Local.Scope = undefined; @@ -269,7 +269,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void { if (self._owns_response) { var response = self._response; response._http_response = null; - response.deinit(self._frame._session); + response.deinit(self._frame._page); // 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/Request.zig b/src/browser/webapi/net/Request.zig index 9b20d973..5d749e6d 100644 --- a/src/browser/webapi/net/Request.zig +++ b/src/browser/webapi/net/Request.zig @@ -175,7 +175,7 @@ pub fn blob(self: *Request, exec: *const Execution) !js.Promise { const headers = try self.getHeaders(exec); const content_type = try headers.get("content-type", exec) orelse ""; - const b = try Blob.initFromBytes(body, content_type, true, exec.context.session); + const b = try Blob.initFromBytes(body, content_type, true, exec.context.page); return exec.context.local.?.resolvePromise(b); } diff --git a/src/browser/webapi/net/Response.zig b/src/browser/webapi/net/Response.zig index 881513e6..a7ff5aa4 100644 --- a/src/browser/webapi/net/Response.zig +++ b/src/browser/webapi/net/Response.zig @@ -20,7 +20,7 @@ const std = @import("std"); const lp = @import("lightpanda"); const js = @import("../../js/js.zig"); -const Session = @import("../../Session.zig"); +const Page = @import("../../Page.zig"); const HttpClient = @import("../../HttpClient.zig"); const Blob = @import("../Blob.zig"); @@ -72,7 +72,7 @@ pub const BodyInit = union(enum) { }; pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Response { - const session = exec.context.session; + const session = exec.context.page.session; const arena = try session.getArena(.large, "Response"); errdefer session.releaseArena(arena); @@ -109,16 +109,16 @@ pub fn init(body_: ?BodyInit, opts_: ?InitOpts, exec: *const Execution) !*Respon return self; } -pub fn deinit(self: *Response, session: *Session) void { +pub fn deinit(self: *Response, page: *Page) void { if (self._http_response) |resp| { resp.abort(error.Abort); self._http_response = null; } - session.releaseArena(self._arena); + page.releaseArena(self._arena); } -pub fn releaseRef(self: *Response, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *Response, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *Response) void { @@ -321,7 +321,7 @@ pub fn blob(self: *const Response, exec: *const Execution) !js.Promise { .stream => return local.rejectPromise(.{ .type_error = "Cannot read blob from stream body" }), }; const content_type = try self._headers.get("content-type", exec) orelse ""; - const b = try Blob.initFromBytes(body, content_type, true, exec.context.session); + const b = try Blob.initFromBytes(body, content_type, true, exec.context.page); return local.resolvePromise(b); } @@ -336,7 +336,7 @@ pub fn bytes(self: *const Response, exec: *const Execution) !js.Promise { } pub fn clone(self: *const Response, exec: *const Execution) !*Response { - const session = exec.context.session; + const session = exec.context.page.session; const body_len = switch (self._body) { .bytes => |b| b.len, .empty => 0, diff --git a/src/browser/webapi/net/WebSocket.zig b/src/browser/webapi/net/WebSocket.zig index 7338234f..0b49b693 100644 --- a/src/browser/webapi/net/WebSocket.zig +++ b/src/browser/webapi/net/WebSocket.zig @@ -24,14 +24,15 @@ const http = @import("../../../network/http.zig"); const js = @import("../../js/js.zig"); const Blob = @import("../Blob.zig"); const URL = @import("../../URL.zig"); + +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); const HttpClient = @import("../../HttpClient.zig"); const Event = @import("../Event.zig"); const EventTarget = @import("../EventTarget.zig"); -const MessageEvent = @import("../event/MessageEvent.zig"); const CloseEvent = @import("../event/CloseEvent.zig"); +const MessageEvent = @import("../event/MessageEvent.zig"); const log = lp.log; const Allocator = std.mem.Allocator; @@ -160,7 +161,7 @@ pub fn init(url: []const u8, protocols: [][]const u8, frame: *Frame) !*WebSocket return self; } -pub fn deinit(self: *WebSocket, session: *Session) void { +pub fn deinit(self: *WebSocket, page: *Page) void { self.cleanup(); if (self._on_open) |func| { @@ -177,14 +178,14 @@ pub fn deinit(self: *WebSocket, session: *Session) void { } for (self._send_queue.items) |msg| { - msg.deinit(session); + msg.deinit(page); } - session.releaseArena(self._arena); + page.releaseArena(self._arena); } -pub fn releaseRef(self: *WebSocket, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *WebSocket, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *WebSocket) void { @@ -235,7 +236,7 @@ fn cleanup(self: *WebSocket) void { self._http_client.removeConn(conn); self._req_headers.deinit(); self._conn = null; - self.releaseRef(self._frame._session); + self.releaseRef(self._frame._page); self._send_queue.clearRetainingCapacity(); } } @@ -303,8 +304,8 @@ pub fn send(self: *WebSocket, data: SendData) !void { switch (data) { .blob => |blob| { - const arena = try self._frame._session.getArena(blob._slice.len, "WebSocket.message"); - errdefer self._frame._session.releaseArena(arena); + const arena = try self._frame.getArena(blob._slice.len, "WebSocket.message"); + errdefer self._frame.releaseArena(arena); try self.queueMessage(.{ .binary = .{ .arena = arena, .data = try arena.dupe(u8, blob._slice), @@ -312,8 +313,8 @@ pub fn send(self: *WebSocket, data: SendData) !void { }, .js_val => |js_val| { if (js_val.isString()) |str| { - const arena = try self._frame._session.getArena(str.len(), "WebSocket.message"); - errdefer self._frame._session.releaseArena(arena); + const arena = try self._frame.getArena(str.len(), "WebSocket.message"); + errdefer self._frame.releaseArena(arena); try self.queueMessage(.{ .text = .{ .arena = arena, .data = try str.toSliceWithAlloc(arena), @@ -322,8 +323,8 @@ pub fn send(self: *WebSocket, data: SendData) !void { const binary = try js_val.toZig(BinaryData); const buffer = binary.asBuffer(); - const arena = try self._frame._session.getArena(buffer.len, "WebSocket.message"); - errdefer self._frame._session.releaseArena(arena); + const arena = try self._frame.getArena(buffer.len, "WebSocket.message"); + errdefer self._frame.releaseArena(arena); try self.queueMessage(.{ .binary = .{ .arena = arena, .data = try arena.dupe(u8, buffer), @@ -452,7 +453,7 @@ fn dispatchOpenEvent(self: *WebSocket) !void { const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "open", self._on_open)) { - const event = try Event.initTrusted(comptime .wrap("open"), .{}, frame._session); + const event = try Event.initTrusted(comptime .wrap("open"), .{}, frame._page); try frame._event_manager.dispatchDirect(target, event, self._on_open, .{ .context = "WebSocket open" }); } } @@ -466,7 +467,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF switch (self._binary_type) { .arraybuffer => .{ .arraybuffer = .{ .values = data } }, .blob => blk: { - const blob = try Blob.initFromBytes(data, "", false, frame._session); + const blob = try Blob.initFromBytes(data, "", false, frame._page); blob.acquireRef(); break :blk .{ .blob = blob }; }, @@ -477,7 +478,7 @@ fn dispatchMessageEvent(self: *WebSocket, data: []const u8, frame_type: http.WsF const event = try MessageEvent.initTrusted(comptime .wrap("message"), .{ .data = msg_data, .origin = "", - }, frame._session); + }, frame._page); try frame._event_manager.dispatchDirect(target, event.asEvent(), self._on_message, .{ .context = "WebSocket message" }); } } @@ -487,7 +488,7 @@ fn dispatchErrorEvent(self: *WebSocket) !void { const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "error", self._on_error)) { - const event = try Event.initTrusted(comptime .wrap("error"), .{}, frame._session); + const event = try Event.initTrusted(comptime .wrap("error"), .{}, frame._page); try frame._event_manager.dispatchDirect(target, event, self._on_error, .{ .context = "WebSocket error" }); } } @@ -575,7 +576,7 @@ fn writeContent(self: *WebSocket, conn: *http.Connection, buf: []u8, byte_msg: M if (self._send_offset >= byte_msg.data.len) { const removed = self._send_queue.orderedRemove(0); - removed.deinit(self._frame._session); + removed.deinit(self._frame._page); if (comptime IS_DEBUG) { log.debug(.websocket, "send complete", .{ .url = self._url, .len = byte_msg.data.len, .queue = self._send_queue.items.len }); } @@ -718,9 +719,9 @@ const Message = union(enum) { arena: Allocator, data: []const u8, }; - fn deinit(self: Message, session: *Session) void { + fn deinit(self: Message, page: *Page) void { switch (self) { - .text, .binary => |msg| session.releaseArena(msg.arena), + .text, .binary => |msg| page.releaseArena(msg.arena), .close => {}, } } diff --git a/src/browser/webapi/net/XMLHttpRequest.zig b/src/browser/webapi/net/XMLHttpRequest.zig index 10dc62f4..35f7f7fa 100644 --- a/src/browser/webapi/net/XMLHttpRequest.zig +++ b/src/browser/webapi/net/XMLHttpRequest.zig @@ -25,8 +25,8 @@ const http = @import("../../../network/http.zig"); const URL = @import("../../URL.zig"); const Mime = @import("../../Mime.zig"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); const Node = @import("../Node.zig"); const Event = @import("../Event.zig"); @@ -100,7 +100,7 @@ pub fn init(frame: *Frame) !*XMLHttpRequest { return self; } -pub fn deinit(self: *XMLHttpRequest, session: *Session) void { +pub fn deinit(self: *XMLHttpRequest, page: *Page) void { if (self._http_response) |resp| { resp.abort(error.Abort); self._http_response = null; @@ -135,19 +135,19 @@ pub fn deinit(self: *XMLHttpRequest, session: *Session) void { } } - session.releaseArena(self._arena); + page.releaseArena(self._arena); } fn releaseSelfRef(self: *XMLHttpRequest) void { if (self._active_request == false) { return; } - self.releaseRef(self._frame._session); + self.releaseRef(self._frame._page); self._active_request = false; } -pub fn releaseRef(self: *XMLHttpRequest, session: *Session) void { - self._rc.release(self, session); +pub fn releaseRef(self: *XMLHttpRequest, page: *Page) void { + self._rc.release(self, page); } pub fn acquireRef(self: *XMLHttpRequest) void { @@ -588,7 +588,7 @@ fn stateChanged(self: *XMLHttpRequest, state: ReadyState, frame: *Frame) !void { const target = self.asEventTarget(); if (frame._event_manager.hasDirectListeners(target, "readystatechange", self._on_ready_state_change)) { - const event = try Event.initTrusted(.wrap("readystatechange"), .{}, frame._session); + const event = try Event.initTrusted(.wrap("readystatechange"), .{}, frame._page); try frame._event_manager.dispatchDirect(target, event, self._on_ready_state_change, .{ .context = "XHR state change" }); } } diff --git a/src/browser/webapi/selector/List.zig b/src/browser/webapi/selector/List.zig index 416adc8e..fd3a4204 100644 --- a/src/browser/webapi/selector/List.zig +++ b/src/browser/webapi/selector/List.zig @@ -18,8 +18,8 @@ const std = @import("std"); +const Page = @import("../../Page.zig"); const Frame = @import("../../Frame.zig"); -const Session = @import("../../Session.zig"); const Node = @import("../Node.zig"); const Part = @import("Selector.zig").Part; @@ -41,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, session: *Session) void { - session.releaseArena(self._arena); +pub fn deinit(self: *const List, page: *Page) void { + page.releaseArena(self._arena); } pub fn collect( diff --git a/src/cdp/AXNode.zig b/src/cdp/AXNode.zig index 4a9008eb..8f145e5a 100644 --- a/src/cdp/AXNode.zig +++ b/src/cdp/AXNode.zig @@ -1366,7 +1366,7 @@ test "AXNode: writer" { defer registry.deinit(); var frame = try testing.pageTest("cdp/dom3.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var doc = frame.window._document; const node = try registry.register(doc.asNode()); @@ -1440,7 +1440,7 @@ test "AXNode: writer prunes hidden and resolves labels" { defer registry.deinit(); var frame = try testing.pageTest("cdp/ax_tree.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var doc = frame.window._document; const node = try registry.register(doc.asNode()); diff --git a/src/cdp/CDP.zig b/src/cdp/CDP.zig index 3ec21c63..16a08cc9 100644 --- a/src/cdp/CDP.zig +++ b/src/cdp/CDP.zig @@ -837,7 +837,7 @@ const IsolatedWorld = struct { // The isolate world must share at least some of the state with the related frame, specifically the DocumentHTML // (assuming grantUniversalAccess will be set to True!). // We just created the world and the frame. The frame's state lives in the session, but is update on navigation. - // This also means this pointer becomes invalid after removeFrame until a new frame is created. + // This also means this pointer becomes invalid after removePage until a new frame is created. // Currently we have only 1 frame and thus also only 1 state in the isolate world. pub fn createContext(self: *IsolatedWorld, frame: *Frame) !*js.Context { if (self.context == null) { diff --git a/src/cdp/Node.zig b/src/cdp/Node.zig index f854d2ce..2fbbde95 100644 --- a/src/cdp/Node.zig +++ b/src/cdp/Node.zig @@ -346,7 +346,7 @@ test "cdp Node: Registry register" { try testing.expectEqual(0, registry.lookup_by_node.count()); var frame = try testing.pageTest("cdp/registry1.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var doc = frame.window._document; { @@ -403,12 +403,12 @@ test "cdp Node: search list" { { var frame = try testing.pageTest("cdp/registry2.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var doc = frame.window._document; { const l1 = try doc.querySelectorAll(.wrap("a"), frame); - defer l1.deinit(frame._session); + defer l1.deinit(frame._page); const s1 = try search_list.create(l1._nodes); try testing.expectEqual("1", s1.name); try testing.expectEqualSlices(u32, &.{ 1, 2 }, s1.node_ids); @@ -419,7 +419,7 @@ test "cdp Node: search list" { { const l2 = try doc.querySelectorAll(.wrap("#a1"), frame); - defer l2.deinit(frame._session); + defer l2.deinit(frame._page); const s2 = try search_list.create(l2._nodes); try testing.expectEqual("2", s2.name); try testing.expectEqualSlices(u32, &.{1}, s2.node_ids); @@ -427,7 +427,7 @@ test "cdp Node: search list" { { const l3 = try doc.querySelectorAll(.wrap("#a2"), frame); - defer l3.deinit(frame._session); + defer l3.deinit(frame._page); const s3 = try search_list.create(l3._nodes); try testing.expectEqual("3", s3.name); try testing.expectEqualSlices(u32, &.{2}, s3.node_ids); @@ -443,7 +443,7 @@ test "cdp Node: Writer" { defer registry.deinit(); var frame = try testing.pageTest("cdp/registry3.html", .{}); - defer frame._session.removeFrame(); + defer frame._session.removePage(); var doc = frame.window._document; { diff --git a/src/cdp/domains/dom.zig b/src/cdp/domains/dom.zig index daf321eb..a322f5cd 100644 --- a/src/cdp/domains/dom.zig +++ b/src/cdp/domains/dom.zig @@ -101,7 +101,7 @@ fn performSearch(cmd: *CDP.Command) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const frame = bc.session.currentFrame() orelse return error.FrameNotLoaded; const list = try Selector.querySelectorAll(frame.window._document.asNode(), params.query, frame); - defer list.deinit(frame._session); + defer list.deinit(frame._page); const search = try bc.node_search_list.create(list._nodes); @@ -252,7 +252,7 @@ fn querySelectorAll(cmd: *CDP.Command) !void { }; const selected_nodes = try Selector.querySelectorAll(node.dom, params.selector, frame); - defer selected_nodes.deinit(frame._session); + defer selected_nodes.deinit(frame._page); const nodes = selected_nodes._nodes; diff --git a/src/cdp/domains/lp.zig b/src/cdp/domains/lp.zig index cc0b74a6..7f5defbd 100644 --- a/src/cdp/domains/lp.zig +++ b/src/cdp/domains/lp.zig @@ -299,7 +299,7 @@ test "cdp.lp: getMarkdown" { defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); - _ = try bc.session.createFrame(); + _ = try bc.session.createPage(); try ctx.processMessage(.{ .id = 1, @@ -315,7 +315,7 @@ test "cdp.lp: getInteractiveElements" { defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); - _ = try bc.session.createFrame(); + _ = try bc.session.createPage(); try ctx.processMessage(.{ .id = 1, @@ -331,7 +331,7 @@ test "cdp.lp: getStructuredData" { defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); - _ = try bc.session.createFrame(); + _ = try bc.session.createPage(); try ctx.processMessage(.{ .id = 1, @@ -347,7 +347,7 @@ test "cdp.lp: action tools" { defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); - const frame = try bc.session.createFrame(); + const frame = try bc.session.createPage(); const url = "http://localhost:9582/src/browser/tests/mcp_actions.html"; try frame.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } }); var runner = try bc.session.runner(.{}); @@ -408,7 +408,7 @@ test "cdp.lp: waitForSelector" { defer ctx.deinit(); const bc = try ctx.loadBrowserContext(.{}); - const frame = try bc.session.createFrame(); + const frame = try bc.session.createPage(); const url = "http://localhost:9582/src/browser/tests/mcp_wait_for_selector.html"; try frame.navigate(url, .{ .reason = .address_bar, .kind = .{ .push = null } }); var runner = try bc.session.runner(.{}); diff --git a/src/cdp/domains/network.zig b/src/cdp/domains/network.zig index 1e11e88c..c60aa7a7 100644 --- a/src/cdp/domains/network.zig +++ b/src/cdp/domains/network.zig @@ -187,7 +187,7 @@ fn getCookies(cmd: *CDP.Command) !void { const params = (try cmd.params(GetCookiesParam)) orelse GetCookiesParam{}; // If not specified, use the URLs of the page and all of its subframes. TODO subframes - const frame_url = if (bc.session.frame) |frame| frame.url else null; + const frame_url = if (bc.session.currentFrame()) |frame| frame.url else null; const param_urls = params.urls orelse &[_][:0]const u8{frame_url orelse return error.InvalidParams}; var urls = try std.ArrayList(CdpStorage.PreparedUri).initCapacity(cmd.arena, param_urls.len); @@ -239,7 +239,7 @@ pub fn httpRequestFail(bc: *CDP.BrowserContext, msg: *const Notification.Request // Isn't possible to do a network request within a Browser (which our // notification is tied to), without a frame. - lp.assert(bc.session.frame != null, "CDP.network.httpRequestFail null frame", .{}); + lp.assert(bc.session.page != null, "CDP.network.httpRequestFail null frame", .{}); // We're missing a bunch of fields, but, for now, this seems like enough try bc.cdp.sendEvent("Network.loadingFailed", .{ diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 02209eea..b0aae79e 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -212,7 +212,7 @@ fn close(cmd: *CDP.Command) !void { const target_id = bc.target_id orelse return error.TargetNotLoaded; // can't be null if we have a target_id - lp.assert(bc.session.frame != null, "CDP.frame.close null frame", .{}); + lp.assert(bc.session.page != null, "CDP.frame.close null frame", .{}); try cmd.sendResult(.{}, .{}); @@ -235,7 +235,7 @@ fn close(cmd: *CDP.Command) !void { bc.session_id = null; } - bc.session.removeFrame(); + bc.session.removePage(); for (bc.isolated_worlds.items) |world| { world.deinit(); } @@ -307,7 +307,7 @@ fn navigate(cmd: *CDP.Command) !void { isolated_world.identity.deinit(); isolated_world.identity = .{}; } - frame = try session.replaceFrame(); + frame = try session.replacePage(); } const encoded_url = try URL.ensureEncoded(frame.call_arena, params.url, "UTF-8"); @@ -333,7 +333,7 @@ fn doReload(cmd: *CDP.Command) !void { const session = bc.session; var frame = session.currentFrame() orelse return error.FrameNotLoaded; - // Dupe URL before replaceFrame() frees the old frame's arena. + // Dupe URL before replacePage() frees the old frame's arena. const reload_url = try cmd.arena.dupeZ(u8, frame.url); if (frame._load_state != .waiting) { @@ -343,7 +343,7 @@ fn doReload(cmd: *CDP.Command) !void { isolated_world.identity.deinit(); isolated_world.identity = .{}; } - frame = try session.replaceFrame(); + frame = try session.replacePage(); } try frame.navigate(reload_url, .{ diff --git a/src/cdp/domains/target.zig b/src/cdp/domains/target.zig index fccf0dd3..e6cfe1ff 100644 --- a/src/cdp/domains/target.zig +++ b/src/cdp/domains/target.zig @@ -171,12 +171,12 @@ fn createTarget(cmd: *CDP.Command) !void { } // if target_id is null, we should never have a blank frame - lp.assert(bc.session.frame == null, "CDP.target.createTarget not null page", .{}); + lp.assert(bc.session.page == null, "CDP.target.createTarget not null page", .{}); // if target_id is null, we should never have a session_id lp.assert(bc.session_id == null, "CDP.target.createTarget not null session_id", .{}); - const frame = try bc.session.createFrame(); + const frame = try bc.session.createPage(); // the target_id == the frame_id of the "root" frame const frame_id = id.toFrameId(frame._frame_id); @@ -284,7 +284,7 @@ fn closeTarget(cmd: *CDP.Command) !void { } // can't be null if we have a target_id - lp.assert(bc.session.frame != null, "CDP.target.closeTarget null frame", .{}); + lp.assert(bc.session.page != null, "CDP.target.closeTarget null frame", .{}); try cmd.sendResult(.{ .success = true }, .{ .include_session_id = false }); @@ -305,7 +305,7 @@ fn closeTarget(cmd: *CDP.Command) !void { bc.session_id = null; } - bc.session.removeFrame(); + bc.session.removePage(); for (bc.isolated_worlds.items) |world| { world.deinit(); } @@ -626,7 +626,7 @@ test "cdp.target: closeTarget" { } // pretend we createdTarget first - _ = try bc.session.createFrame(); + _ = try bc.session.createPage(); bc.target_id = "TID-000000000A".*; { try ctx.processMessage(.{ .id = 10, .method = "Target.closeTarget", .params = .{ .targetId = "TID-8" } }); @@ -636,7 +636,7 @@ test "cdp.target: closeTarget" { { try ctx.processMessage(.{ .id = 11, .method = "Target.closeTarget", .params = .{ .targetId = "TID-000000000A" } }); try ctx.expectSentResult(.{ .success = true }, .{ .id = 11 }); - try testing.expectEqual(null, bc.session.frame); + try testing.expectEqual(null, bc.session.page); try testing.expectEqual(null, bc.target_id); } } @@ -657,7 +657,7 @@ test "cdp.target: attachToTarget" { } // pretend we createdTarget first - _ = try bc.session.createFrame(); + _ = try bc.session.createPage(); bc.target_id = "TID-000000000B".*; { try ctx.processMessage(.{ .id = 10, .method = "Target.attachToTarget", .params = .{ .targetId = "TID-8" } }); @@ -701,7 +701,7 @@ test "cdp.target: getTargetInfo" { } // pretend we createdTarget first - _ = try bc.session.createFrame(); + _ = try bc.session.createPage(); bc.target_id = "TID-000000000C".*; { try ctx.processMessage(.{ .id = 10, .method = "Target.getTargetInfo", .params = .{ .targetId = "TID-8" } }); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 488b5bd1..8be66b43 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -93,7 +93,7 @@ const TestContext = struct { if (bc.target_id == null) { bc.target_id = "TID-000000000Z".*; } - const frame = try bc.session.createFrame(); + const frame = try bc.session.createPage(); const full_url = try std.fmt.allocPrintSentinel( base.arena_allocator, "http://127.0.0.1:9582/src/browser/tests/{s}", @@ -204,7 +204,7 @@ const TestContext = struct { if (self.cdp_) |*cdp__| { if (cdp__.browser_context) |*bc| { - if (bc.session.frame != null) { + if (bc.session.page != null) { var runner = try bc.session.runner(.{}); _ = try runner.tick(.{ .ms = 1000 }); } diff --git a/src/lightpanda.zig b/src/lightpanda.zig index a4e9fcd3..992388a1 100644 --- a/src/lightpanda.zig +++ b/src/lightpanda.zig @@ -23,12 +23,14 @@ pub const App = @import("App.zig"); pub const Network = @import("network/Network.zig"); pub const Server = @import("Server.zig"); pub const Config = @import("Config.zig"); -pub const URL = @import("browser/URL.zig"); pub const String = @import("string.zig").String; +pub const Notification = @import("Notification.zig"); + +pub const URL = @import("browser/URL.zig"); +pub const Page = @import("browser/Page.zig"); pub const Frame = @import("browser/Frame.zig"); pub const Browser = @import("browser/Browser.zig"); pub const Session = @import("browser/Session.zig"); -pub const Notification = @import("Notification.zig"); pub const js = @import("browser/js/js.zig"); pub const dump = @import("browser/dump.zig"); @@ -40,13 +42,13 @@ pub const links = @import("browser/links.zig"); pub const forms = @import("browser/forms.zig"); pub const actions = @import("browser/actions.zig"); pub const structured_data = @import("browser/structured_data.zig"); +pub const HttpClient = @import("browser/HttpClient.zig"); + pub const mcp = @import("mcp.zig"); pub const cookies = @import("cookies.zig"); pub const build_config = @import("build_config"); pub const crash_handler = @import("crash_handler.zig"); -pub const HttpClient = @import("browser/HttpClient.zig"); - const IS_DEBUG = @import("builtin").mode == .Debug; pub const FetchOpts = struct { @@ -80,7 +82,7 @@ pub fn fetch(app: *App, url: [:0]const u8, opts: FetchOpts) !void { } } - const frame = try session.createFrame(); + const frame = try session.createPage(); // // Comment this out to get a profile of the JS code in v8/profile.json. // // You can open this in Chrome's profiler. @@ -262,7 +264,7 @@ pub fn RC(comptime T: type) type { self._refs += 1; } - pub fn release(self: *@This(), value: anytype, session: *Session) void { + pub fn release(self: *@This(), value: anytype, page: *Page) void { assert(self._refs > 0, "release overflow", .{ .type = @typeName(@TypeOf(value)) }); const refs = self._refs - 1; @@ -270,7 +272,7 @@ pub fn RC(comptime T: type) type { if (refs > 0) { return; } - value.deinit(session); + value.deinit(page); } pub fn format(self: @This(), writer: *std.Io.Writer) !void { diff --git a/src/main_legacy_test.zig b/src/main_legacy_test.zig index baa0275b..438af9cb 100644 --- a/src/main_legacy_test.zig +++ b/src/main_legacy_test.zig @@ -94,8 +94,8 @@ pub fn main() !void { pub fn run(allocator: Allocator, file: []const u8, session: *lp.Session) !void { const url = try std.fmt.allocPrintSentinel(allocator, "http://localhost:9589/{s}", .{file}, 0); - const frame = try session.createFrame(); - defer session.removeFrame(); + const frame = try session.createPage(); + defer session.removePage(); var ls: lp.js.Local.Scope = undefined; frame.js.localScope(&ls); diff --git a/src/mcp/tools.zig b/src/mcp/tools.zig index 0f74a019..b1fb7c38 100644 --- a/src/mcp/tools.zig +++ b/src/mcp/tools.zig @@ -920,10 +920,10 @@ fn parseArgs(comptime T: type, arena: std.mem.Allocator, arguments: ?std.json.Va fn performGoto(server: *Server, url: [:0]const u8, id: std.json.Value, timeout: ?u32, waitUntil: ?lp.Config.WaitUntil) !void { const session = server.session; - if (session.frame != null) { - session.removeFrame(); + if (session.page != null) { + session.removePage(); } - const frame = session.createFrame() catch { + const frame = session.createPage() catch { try server.sendError(id, .InternalError, "Failed to create page"); return error.NavigationFailed; }; @@ -988,7 +988,7 @@ test "MCP - Actions: click, fill, scroll, hover, press, selectOption, setChecked const server = try testLoadPage("http://localhost:9582/src/browser/tests/mcp_actions.html", &out.writer); defer server.deinit(); - const frame = &server.session.frame.?; + const frame = server.session.currentFrame().?; { // Test Click @@ -1233,7 +1233,7 @@ fn testLoadPage(url: [:0]const u8, writer: *std.Io.Writer) !*Server { var server = try Server.init(testing.allocator, testing.test_app, writer); errdefer server.deinit(); - const frame = try server.session.createFrame(); + const frame = try server.session.createPage(); try frame.navigate(url, .{}); var runner = try server.session.runner(.{}); diff --git a/src/testing.zig b/src/testing.zig index bba6bcdd..47d7e01f 100644 --- a/src/testing.zig +++ b/src/testing.zig @@ -394,8 +394,8 @@ pub fn htmlRunner(comptime path: []const u8, opts: HtmlRunnerOpts) !void { } fn runWebApiTest(test_file: [:0]const u8) !void { - const frame = try test_session.createFrame(); - defer test_session.removeFrame(); + const frame = try test_session.createPage(); + defer test_session.removePage(); const url = try std.fmt.allocPrintSentinel( arena_allocator, @@ -453,8 +453,8 @@ const PageTestOpts = struct { wait_until_done: bool = true, }; pub fn pageTest(comptime test_file: []const u8, opts: PageTestOpts) !*Frame { - const frame = try test_session.createFrame(); - errdefer test_session.removeFrame(); + const frame = try test_session.createPage(); + errdefer test_session.removePage(); const url = try std.fmt.allocPrintSentinel( arena_allocator,