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.
This commit is contained in:
Karl Seguin
2026-04-22 20:38:50 +08:00
parent 553b32b3b8
commit 550fb58f3f
74 changed files with 819 additions and 664 deletions

View File

@@ -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(),

View File

@@ -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);

View File

@@ -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;

View File

@@ -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

232
src/browser/Page.zig Normal file
View File

@@ -0,0 +1,232 @@
// Copyright (C) 2023-2026 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std");
const 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;
}

View File

@@ -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);

View File

@@ -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,

View File

@@ -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);
}
};

View File

@@ -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 });
};

View File

@@ -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(
\\<form action="/login" method="POST">
\\ <input type="email" name="email" required placeholder="Email">
@@ -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(
\\<form>
\\ <select name="color">
@@ -329,7 +329,7 @@ test "browser.forms: form with select" {
test "browser.forms: form with textarea" {
defer testing.reset();
defer testing.test_session.removeFrame();
defer testing.test_session.removePage();
const forms = try testForms(
\\<form method="POST">
\\ <textarea name="message" placeholder="Your message"></textarea>
@@ -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(
\\<form action="/empty">
\\ <p>No fields here</p>
@@ -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(
\\<form>
\\ <input type="hidden" name="csrf" value="token123">
@@ -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(
\\<form action="/search" method="GET">
\\ <input type="text" name="q" placeholder="Search">
@@ -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(
\\<form>
\\ <input type="text" name="enabled_field">
@@ -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(
\\<form>
\\ <fieldset disabled>
@@ -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(
\\<input type="text" name="external" form="myform">
\\<form id="myform" action="/submit">
@@ -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(
\\<form>
\\ <input type="checkbox" name="agree" value="yes" checked>
@@ -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(
\\<form>
\\ <input type="text" name="q">

View File

@@ -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);

View File

@@ -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));

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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 {

View File

@@ -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,

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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| {

View File

@@ -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;

View File

@@ -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);

View File

@@ -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" });
}
},

View File

@@ -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) {

View File

@@ -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.

View File

@@ -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")) {

View File

@@ -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 {

View File

@@ -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, .{}),

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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();

View File

@@ -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;
// <div>

View File

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

View File

@@ -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" });
}

View File

@@ -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" });

View File

@@ -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;
}

View File

@@ -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;
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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" });
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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 {

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -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 {

View File

@@ -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;
}

View File

@@ -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.
}

View File

@@ -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);
}

View File

@@ -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,

View File

@@ -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 => {},
}
}

View File

@@ -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" });
}
}

View File

@@ -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(

View File

@@ -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());

View File

@@ -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) {

View File

@@ -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;
{

View File

@@ -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;

View File

@@ -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(.{});

View File

@@ -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", .{

View File

@@ -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, .{

View File

@@ -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" } });

View File

@@ -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 });
}

View File

@@ -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 {

View File

@@ -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);

View File

@@ -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(.{});

View File

@@ -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,