Compare commits

..

2 Commits

Author SHA1 Message Date
Karl Seguin
078e13d8a4 Remove DOMContentLoaded and Loaded events from page_navigated
These were moved to their own distinct events, and should have been removed from
here.
2026-03-30 11:39:38 +08:00
Karl Seguin
cabb029bed Improve/Fix CDP navigation event order
These changes all better align with chrome's event ordering/timing.

There are two big changes. The first is that our internal page_navigated event,
which is kind of our heavy hitter, is sent once the header is received as
opposed to (much later) on document load. The main goal of this internal event
is to trigger the "Page.frameNavigated" CDP event which is meant to happen
once the URL is committed, which _is_ on header response.

To accommodate this earlier trigger, new explicit events for DOMContentLoaded
and load have be added.

This drastically changes the flow of events as things go from:
Start Page Navigation
Response Received
  Start Frame Navigation
  Response Received
  End Frame Navigation
End Page Navigation
context clear + reset
DOMContentLoaded
Loaded

TO:
Start Page Navigation
Response Received
End Page Navigation
context clear + reset
Start Frame Navigation
Response Received
End Frame Navigation
DOMContentLoaded
Loaded

So not only does it remove the nesting, but it ensures that the context are
cleared and reset once the main page's navigation is locked in, and before any
frame is created.
2026-03-30 11:02:53 +08:00
69 changed files with 623 additions and 845 deletions

View File

@@ -24,7 +24,6 @@ const log = @import("log.zig");
const dump = @import("browser/dump.zig"); const dump = @import("browser/dump.zig");
const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config; const WebBotAuthConfig = @import("network/WebBotAuth.zig").Config;
const mcp = @import("mcp.zig");
pub const RunMode = enum { pub const RunMode = enum {
help, help,
@@ -223,7 +222,6 @@ pub const Serve = struct {
pub const Mcp = struct { pub const Mcp = struct {
common: Common = .{}, common: Common = .{},
version: mcp.Version = .default,
}; };
pub const DumpFormat = enum { pub const DumpFormat = enum {
@@ -455,12 +453,6 @@ pub fn printUsageAndExit(self: *const Config, success: bool) void {
\\Starts an MCP (Model Context Protocol) server over stdio \\Starts an MCP (Model Context Protocol) server over stdio
\\Example: {s} mcp \\Example: {s} mcp
\\ \\
\\Options:
\\--version
\\ Override the reported MCP version.
\\ Valid: 2024-11-05, 2025-03-26, 2025-06-18, 2025-11-25.
\\ Defaults to "2024-11-05".
\\
++ common_options ++ ++ common_options ++
\\ \\
\\version command \\version command
@@ -648,22 +640,10 @@ fn parseMcpArgs(
allocator: Allocator, allocator: Allocator,
args: *std.process.ArgIterator, args: *std.process.ArgIterator,
) !Mcp { ) !Mcp {
var result: Mcp = .{}; var mcp: Mcp = .{};
while (args.next()) |opt| { while (args.next()) |opt| {
if (std.mem.eql(u8, "--version", opt)) { if (try parseCommonArg(allocator, opt, args, &mcp.common)) {
const str = args.next() orelse {
log.fatal(.mcp, "missing argument value", .{ .arg = opt });
return error.InvalidArgument;
};
result.version = std.meta.stringToEnum(mcp.Version, str) orelse {
log.fatal(.mcp, "invalid protocol version", .{ .value = str });
return error.InvalidArgument;
};
continue;
}
if (try parseCommonArg(allocator, opt, args, &result.common)) {
continue; continue;
} }
@@ -671,7 +651,7 @@ fn parseMcpArgs(
return error.UnkownOption; return error.UnkownOption;
} }
return result; return mcp;
} }
fn parseFetchArgs( fn parseFetchArgs(

View File

@@ -74,6 +74,8 @@ const EventListeners = struct {
page_network_idle: List = .{}, page_network_idle: List = .{},
page_network_almost_idle: List = .{}, page_network_almost_idle: List = .{},
page_frame_created: List = .{}, page_frame_created: List = .{},
page_dom_content_loaded: List = .{},
page_loaded: List = .{},
http_request_fail: List = .{}, http_request_fail: List = .{},
http_request_start: List = .{}, http_request_start: List = .{},
http_request_intercept: List = .{}, http_request_intercept: List = .{},
@@ -91,6 +93,8 @@ const Events = union(enum) {
page_network_idle: *const PageNetworkIdle, page_network_idle: *const PageNetworkIdle,
page_network_almost_idle: *const PageNetworkAlmostIdle, page_network_almost_idle: *const PageNetworkAlmostIdle,
page_frame_created: *const PageFrameCreated, page_frame_created: *const PageFrameCreated,
page_dom_content_loaded: *const PageDOMContentLoaded,
page_loaded: *const PageLoaded,
http_request_fail: *const RequestFail, http_request_fail: *const RequestFail,
http_request_start: *const RequestStart, http_request_start: *const RequestStart,
http_request_intercept: *const RequestIntercept, http_request_intercept: *const RequestIntercept,
@@ -137,6 +141,18 @@ pub const PageFrameCreated = struct {
timestamp: u64, timestamp: u64,
}; };
pub const PageDOMContentLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const PageLoaded = struct {
req_id: u32,
frame_id: u32,
timestamp: u64,
};
pub const RequestStart = struct { pub const RequestStart = struct {
transfer: *Transfer, transfer: *Transfer,
}; };

View File

@@ -205,7 +205,7 @@ pub fn dispatch(self: *EventManager, target: *EventTarget, event: *Event) Dispat
pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void { pub fn dispatchOpts(self: *EventManager, target: *EventTarget, event: *Event, comptime opts: DispatchOpts) DispatchError!void {
event.acquireRef(); event.acquireRef();
defer _ = event.releaseRef(self.page._session); defer event.deinit(false, self.page._session);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles }); log.debug(.event, "eventManager.dispatch", .{ .type = event._type_string.str(), .bubbles = event._bubbles });
@@ -240,7 +240,7 @@ pub fn dispatchDirect(self: *EventManager, target: *EventTarget, event: *Event,
defer window._current_event = prev_event; defer window._current_event = prev_event;
event.acquireRef(); event.acquireRef();
defer _ = event.releaseRef(page._session); defer event.deinit(false, page._session);
if (comptime IS_DEBUG) { if (comptime IS_DEBUG) {
log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context }); log.debug(.event, "dispatchDirect", .{ .type = event._type_string, .context = opts.context });

View File

@@ -239,7 +239,7 @@ fn eventInit(arena: Allocator, typ: String, value: anytype) !Event {
const time_stamp = (raw_timestamp / 2) * 2; const time_stamp = (raw_timestamp / 2) * 2;
return .{ return .{
._rc = .{}, ._rc = 0,
._arena = arena, ._arena = arena,
._type = unionInit(Event.Type, value), ._type = unionInit(Event.Type, value),
._type_string = typ, ._type_string = typ,
@@ -255,7 +255,6 @@ pub fn blob(_: *const Factory, arena: Allocator, child: anytype) !*@TypeOf(child
const blob_ptr = chain.get(0); const blob_ptr = chain.get(0);
blob_ptr.* = .{ blob_ptr.* = .{
._rc = .{},
._arena = arena, ._arena = arena,
._type = unionInit(Blob.Type, chain.get(1)), ._type = unionInit(Blob.Type, chain.get(1)),
._slice = "", ._slice = "",
@@ -272,7 +271,7 @@ pub fn abstractRange(_: *const Factory, arena: Allocator, child: anytype, page:
const doc = page.document.asNode(); const doc = page.document.asNode();
const abstract_range = chain.get(0); const abstract_range = chain.get(0);
abstract_range.* = AbstractRange{ abstract_range.* = AbstractRange{
._rc = .{}, ._rc = 0,
._arena = arena, ._arena = arena,
._page_id = page.id, ._page_id = page.id,
._type = unionInit(AbstractRange.Type, chain.get(1)), ._type = unionInit(AbstractRange.Type, chain.get(1)),

View File

@@ -821,16 +821,16 @@ fn processOneMessage(self: *Client, msg: http.Handles.MultiMessage, transfer: *T
break :blk std.ascii.eqlIgnoreCase(hdr.value, "close"); break :blk std.ascii.eqlIgnoreCase(hdr.value, "close");
}; };
// make sure the transfer can't be immediately aborted from a callback
// since we still need it here.
transfer._performing = true;
defer transfer._performing = false;
if (msg.err != null and !is_conn_close_recv) { if (msg.err != null and !is_conn_close_recv) {
transfer.requestFailed(transfer._callback_error orelse msg.err.?, true); transfer.requestFailed(transfer._callback_error orelse msg.err.?, true);
return true; return true;
} }
// make sure the transfer can't be immediately aborted from a callback
// since we still need it here.
transfer._performing = true;
defer transfer._performing = false;
if (!transfer._header_done_called) { if (!transfer._header_done_called) {
// In case of request w/o data, we need to call the header done // In case of request w/o data, we need to call the header done
// callback now. // callback now.
@@ -873,6 +873,7 @@ fn processMessages(self: *Client) !bool {
var processed = false; var processed = false;
while (self.handles.readMessage()) |msg| { while (self.handles.readMessage()) |msg| {
const transfer = try Transfer.fromConnection(&msg.conn); const transfer = try Transfer.fromConnection(&msg.conn);
const done = self.processOneMessage(msg, transfer) catch |err| blk: { const done = self.processOneMessage(msg, transfer) catch |err| blk: {
log.err(.http, "process_messages", .{ .err = err, .req = transfer }); log.err(.http, "process_messages", .{ .err = err, .req = transfer });
transfer.requestFailed(err, true); transfer.requestFailed(err, true);
@@ -1067,24 +1068,6 @@ pub const Transfer = struct {
if (self.req.shutdown_callback) |cb| { if (self.req.shutdown_callback) |cb| {
cb(self.ctx); cb(self.ctx);
} }
if (self._performing or self.client.performing) {
// We're currently inside of a callback. This client, and libcurl
// generally don't expect a transfer to become deinitialized during
// a callback. We can flag the transfer as aborted (which is what
// we do when transfer.abort() is called in this condition) AND,
// since this "kill()"should prevent any future callbacks, the best
// we can do is null/noop them.
self.aborted = true;
self.req.start_callback = null;
self.req.shutdown_callback = null;
self.req.header_callback = Noop.headerCallback;
self.req.data_callback = Noop.dataCallback;
self.req.done_callback = Noop.doneCallback;
self.req.error_callback = Noop.errorCallback;
return;
}
self.deinit(); self.deinit();
} }
@@ -1342,15 +1325,15 @@ pub const Transfer = struct {
} }
} }
transfer.req.notification.dispatch(.http_response_header_done, &.{
.transfer = transfer,
});
const proceed = transfer.req.header_callback(transfer) catch |err| { const proceed = transfer.req.header_callback(transfer) catch |err| {
log.err(.http, "header_callback", .{ .err = err, .req = transfer }); log.err(.http, "header_callback", .{ .err = err, .req = transfer });
return err; return err;
}; };
transfer.req.notification.dispatch(.http_response_header_done, &.{
.transfer = transfer,
});
return proceed and transfer.aborted == false; return proceed and transfer.aborted == false;
} }
@@ -1509,12 +1492,3 @@ pub const Transfer = struct {
return null; return null;
} }
}; };
const Noop = struct {
fn headerCallback(_: *Transfer) !bool {
return true;
}
fn dataCallback(_: *Transfer, _: []const u8) !void {}
fn doneCallback(_: *anyopaque) !void {}
fn errorCallback(_: *anyopaque, _: anyerror) void {}
};

View File

@@ -487,7 +487,6 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
return error.InjectBlankFailed; return error.InjectBlankFailed;
}; };
} }
self.documentIsComplete();
session.notification.dispatch(.page_navigate, &.{ session.notification.dispatch(.page_navigate, &.{
.frame_id = self._frame_id, .frame_id = self._frame_id,
@@ -519,6 +518,8 @@ pub fn navigate(self: *Page, request_url: [:0]const u8, opts: NavigateOpts) !voi
// force next request id manually b/c we won't create a real req. // force next request id manually b/c we won't create a real req.
_ = session.browser.http_client.incrReqId(); _ = session.browser.http_client.incrReqId();
self.documentIsComplete();
return; return;
} }
@@ -738,6 +739,12 @@ pub fn _documentIsLoaded(self: *Page) !void {
self.document.asEventTarget(), self.document.asEventTarget(),
event, event,
); );
self._session.notification.dispatch(.page_dom_content_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
} }
pub fn scriptsCompletedLoading(self: *Page) void { pub fn scriptsCompletedLoading(self: *Page) void {
@@ -796,19 +803,6 @@ pub fn documentIsComplete(self: *Page) void {
self._documentIsComplete() catch |err| { self._documentIsComplete() catch |err| {
log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url }); log.err(.page, "document is complete", .{ .err = err, .type = self._type, .url = self.url });
}; };
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
} }
fn _documentIsComplete(self: *Page) !void { fn _documentIsComplete(self: *Page) !void {
@@ -827,6 +821,12 @@ fn _documentIsComplete(self: *Page) !void {
try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" }); try self._event_manager.dispatchDirect(window_target, event, self.window._on_load, .{ .inject_target = false, .context = "page load" });
} }
self._session.notification.dispatch(.page_loaded, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.timestamp = timestamp(.monotonic),
});
if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) { if (self._event_manager.hasDirectListeners(window_target, "pageshow", self.window._on_pageshow)) {
const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent(); const pageshow_event = (try PageTransitionEvent.initTrusted(comptime .wrap("pageshow"), .{}, self)).asEvent();
try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" }); try self._event_manager.dispatchDirect(window_target, pageshow_event, self.window._on_pageshow, .{ .context = "page show" });
@@ -879,6 +879,19 @@ fn pageHeaderDoneCallback(transfer: *HttpClient.Transfer) !bool {
}); });
} }
if (self._navigated_options) |no| {
// _navigated_options will be null in special short-circuit cases, like
// "navigating" to about:blank, in which case this notification has
// already been sent
self._session.notification.dispatch(.page_navigated, &.{
.frame_id = self._frame_id,
.req_id = self._req_id,
.opts = no,
.url = self.url,
.timestamp = timestamp(.monotonic),
});
}
return true; return true;
} }
@@ -3392,7 +3405,7 @@ pub fn handleClick(self: *Page, target: *Node) !void {
pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void { pub fn triggerKeyboard(self: *Page, keyboard_event: *KeyboardEvent) !void {
const event = keyboard_event.asEvent(); const event = keyboard_event.asEvent();
const element = self.window._document._active_element orelse { const element = self.window._document._active_element orelse {
_ = event.releaseRef(self._session); keyboard_event.deinit(false, self._session);
return; return;
}; };
@@ -3488,7 +3501,7 @@ pub fn submitForm(self: *Page, submitter_: ?*Element, form_: ?*Element.Html.Form
// so submit_event is still valid when we check _prevent_default // so submit_event is still valid when we check _prevent_default
submit_event.acquireRef(); submit_event.acquireRef();
defer _ = submit_event.releaseRef(self._session); defer submit_event.deinit(false, self._session);
try self._event_manager.dispatch(form_element.asEventTarget(), submit_event); try self._event_manager.dispatch(form_element.asEventTarget(), submit_event);
// If the submit event was prevented, don't submit the form // If the submit event was prevented, don't submit the form

View File

@@ -71,18 +71,6 @@ origins: std.StringHashMapUnmanaged(*js.Origin) = .empty,
// ensuring object identity works across same-origin frames. // ensuring object identity works across same-origin frames.
identity: js.Identity = .{}, 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,
// 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 pages in this session. // Shared resources for all pages in this session.
// These live for the duration of the page tree (root + frames). // These live for the duration of the page tree (root + frames).
arena_pool: *ArenaPool, arena_pool: *ArenaPool,
@@ -236,30 +224,6 @@ pub fn releaseOrigin(self: *Session, origin: *js.Origin) void {
/// Reset page_arena and factory for a clean slate. /// Reset page_arena and factory for a clean slate.
/// Called when root page is removed. /// Called when root page is removed.
fn resetPageResources(self: *Session) void { fn resetPageResources(self: *Session) void {
// 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;
}
self.identity.deinit(); self.identity.deinit();
self.identity = .{}; self.identity = .{};
@@ -493,25 +457,35 @@ pub fn nextPageId(self: *Session) u32 {
return id; return id;
} }
// Every finalizable instance of Zig gets 1 FinalizerCallback registered in the // A type that has a finalizer can have its finalizer called one of two ways.
// session. This is to ensure that, if v8 doesn't finalize the value, we can // The first is from V8 via the WeakCallback we give to weakRef. But that isn't
// release on page reset. // guaranteed to fire, so we track this in finalizer_callbacks and call them on
// page reset.
pub const FinalizerCallback = struct { pub const FinalizerCallback = struct {
arena: Allocator, arena: Allocator,
session: *Session, session: *Session,
resolved_ptr_id: usize, ptr: *anyopaque,
finalizer_ptr_id: usize, global: v8.Global,
_deinit: *const fn (ptr_id: usize, session: *Session) void, identity: *js.Identity,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
// For every FinalizerCallback we'll have 1+ FinalizerCallback.Identity: one pub fn deinit(self: *FinalizerCallback) void {
// for every identity that gets the instance. In most cases, that'l be 1. self.zig_finalizer(self.ptr, self.session);
pub const Identity = struct { self.session.releaseArena(self.arena);
identity: *js.Identity, }
fc: *Session.FinalizerCallback,
}; /// Release this item from the identity tracking maps (called after finalizer runs from V8)
pub fn releaseIdentity(self: *FinalizerCallback) void {
const session = self.session;
const id = @intFromPtr(self.ptr);
if (self.identity.identity_map.fetchRemove(id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
_ = self.identity.finalizer_callbacks.remove(id);
fn deinit(self: *FinalizerCallback, session: *Session) void {
self._deinit(self.finalizer_ptr_id, session);
session.releaseArena(self.arena); session.releaseArena(self.arena);
} }
}; };

View File

@@ -21,7 +21,6 @@ const lp = @import("lightpanda");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const bridge = @import("bridge.zig");
const Env = @import("Env.zig"); const Env = @import("Env.zig");
const Origin = @import("Origin.zig"); const Origin = @import("Origin.zig");
const Scheduler = @import("Scheduler.zig"); const Scheduler = @import("Scheduler.zig");
@@ -214,11 +213,48 @@ pub fn setOrigin(self: *Context, key: ?[]const u8) !void {
} }
pub fn trackGlobal(self: *Context, global: v8.Global) !void { pub fn trackGlobal(self: *Context, global: v8.Global) !void {
return self.session.globals.append(self.session.page_arena, global); return self.identity.globals.append(self.identity_arena, global);
} }
pub fn trackTemp(self: *Context, global: v8.Global) !void { pub fn trackTemp(self: *Context, global: v8.Global) !void {
return self.session.temps.put(self.session.page_arena, global.data_ptr, global); return self.identity.temps.put(self.identity_arena, global.data_ptr, global);
}
pub fn weakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn safeWeakRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(&fc.global);
v8.v8__Global__SetWeakFinalizer(&fc.global, fc, resolved.finalizer_from_v8, v8.kParameter);
}
pub fn strongRef(self: *Context, obj: anytype) void {
const resolved = js.Local.resolveValue(obj);
const fc = self.identity.finalizer_callbacks.get(@intFromPtr(resolved.ptr)) orelse {
if (comptime IS_DEBUG) {
// should not be possible
std.debug.assert(false);
}
return;
};
v8.v8__Global__ClearWeak(&fc.global);
} }
pub const IdentityResult = struct { pub const IdentityResult = struct {
@@ -234,6 +270,35 @@ pub fn addIdentity(self: *Context, ptr: usize) !IdentityResult {
}; };
} }
pub fn releaseTemp(self: *Context, global: v8.Global) void {
if (self.identity.temps.fetchRemove(global.data_ptr)) |kv| {
var g = kv.value;
v8.v8__Global__Reset(&g);
}
}
pub fn createFinalizerCallback(
self: *Context,
global: v8.Global,
ptr: *anyopaque,
zig_finalizer: *const fn (ptr: *anyopaque, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
.ptr = ptr,
.global = global,
.zig_finalizer = zig_finalizer,
// Store identity pointer for cleanup when V8 GCs the object
.identity = self.identity,
};
return fc;
}
// Any operation on the context have to be made from a local. // Any operation on the context have to be made from a local.
pub fn localScope(self: *Context, ls: *js.Local.Scope) void { pub fn localScope(self: *Context, ls: *js.Local.Scope) void {
const isolate = self.isolate; const isolate = self.isolate;

View File

@@ -213,7 +213,7 @@ fn _persist(self: *const Function, comptime is_global: bool) !(if (is_global) Gl
return .{ .handle = global, .temps = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.session.temps }; return .{ .handle = global, .temps = &ctx.identity.temps };
} }
pub fn tempWithThis(self: *const Function, value: anytype) !Temp { pub fn tempWithThis(self: *const Function, value: anytype) !Temp {

View File

@@ -38,9 +38,38 @@ const Identity = @This();
// Maps Zig instance pointers to their v8::Global(Object) wrappers. // Maps Zig instance pointers to their v8::Global(Object) wrappers.
identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty, identity_map: std.AutoHashMapUnmanaged(usize, v8.Global) = .empty,
// Tracked global v8 objects that need to be released on cleanup.
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,
// Finalizer callbacks for weak references. Key is @intFromPtr of the Zig instance.
finalizer_callbacks: std.AutoHashMapUnmanaged(usize, *Session.FinalizerCallback) = .empty,
pub fn deinit(self: *Identity) void { pub fn deinit(self: *Identity) void {
var it = self.identity_map.valueIterator(); {
while (it.next()) |global| { var it = self.finalizer_callbacks.valueIterator();
while (it.next()) |finalizer| {
finalizer.*.deinit();
}
}
{
var it = self.identity_map.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
for (self.globals.items) |*global| {
v8.v8__Global__Reset(global); v8.v8__Global__Reset(global);
} }
{
var it = self.temps.valueIterator();
while (it.next()) |global| {
v8.v8__Global__Reset(global);
}
}
} }

View File

@@ -17,11 +17,10 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const Session = @import("../Session.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
const string = @import("../../string.zig"); const string = @import("../../string.zig");
const Session = @import("../Session.zig");
const js = @import("js.zig"); const js = @import("js.zig");
const bridge = @import("bridge.zig"); const bridge = @import("bridge.zig");
const Caller = @import("Caller.zig"); const Caller = @import("Caller.zig");
@@ -214,8 +213,7 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
.pointer => |ptr| { .pointer => |ptr| {
const resolved = resolveValue(value); const resolved = resolveValue(value);
const resolved_ptr_id = @intFromPtr(resolved.ptr); const gop = try ctx.addIdentity(@intFromPtr(resolved.ptr));
const gop = try ctx.addIdentity(resolved_ptr_id);
if (gop.found_existing) { if (gop.found_existing) {
// we've seen this instance before, return the same object // we've seen this instance before, return the same object
return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self); return (js.Object.Global{ .handle = gop.value_ptr.* }).local(self);
@@ -264,27 +262,31 @@ pub fn mapZigInstanceToJs(self: *const Local, js_obj_handle: ?*const v8.Object,
// dont' use js_obj.persist(), because we don't want to track this in // dont' use js_obj.persist(), because we don't want to track this in
// context.global_objects, we want to track it in context.identity_map. // context.global_objects, we want to track it in context.identity_map.
v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr); v8.v8__Global__New(isolate.handle, js_obj.handle, gop.value_ptr);
if (resolved.finalizer) |finalizer| { if (@hasDecl(JsApi.Meta, "finalizer")) {
const finalizer_ptr_id = finalizer.ptr_id; // It would be great if resolved knew the resolved type, but I
finalizer.acquireRef(finalizer_ptr_id); // can't figure out how to make that work, since it depends on
// the [runtime] `value`.
const session = ctx.session; // We need the resolved finalizer, which we have in resolved.
const finalizer_gop = try session.finalizer_callbacks.getOrPut(session.page_arena, finalizer_ptr_id); //
if (finalizer_gop.found_existing == false) { // The above if statement would be more clear as:
// This is the first context (and very likely only one) to // if (resolved.finalizer_from_v8) |finalizer| {
// see this Zig instance. We need to create the FinalizerCallback // But that's a runtime check.
// so that we can cleanup on page reset if v8 doesn't finalize. // Instead, we check if the base has finalizer. The assumption
errdefer _ = session.finalizer_callbacks.remove(finalizer_ptr_id); // here is that if a resolve type has a finalizer, then the base
finalizer_gop.value_ptr.* = try self.createFinalizerCallback(resolved_ptr_id, finalizer_ptr_id, finalizer.deinit); // should have a finalizer too.
const fc = try ctx.createFinalizerCallback(gop.value_ptr.*, resolved.ptr, resolved.finalizer_from_zig.?);
{
errdefer fc.deinit();
try ctx.identity.finalizer_callbacks.put(ctx.identity_arena, @intFromPtr(resolved.ptr), fc);
} }
const fc = finalizer_gop.value_ptr.*;
const identity_finalizer = try fc.arena.create(Session.FinalizerCallback.Identity);
identity_finalizer.* = .{
.fc = fc,
.identity = ctx.identity,
};
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, identity_finalizer, finalizer.release, v8.kParameter); conditionallyReference(value);
if (@hasDecl(JsApi.Meta, "weak")) {
if (comptime IS_DEBUG) {
std.debug.assert(JsApi.Meta.weak == true);
}
v8.v8__Global__SetWeakFinalizer(gop.value_ptr, fc, resolved.finalizer_from_v8, v8.kParameter);
}
} }
return js_obj; return js_obj;
}, },
@@ -1119,19 +1121,12 @@ fn jsUnsignedIntToZig(comptime T: type, max: comptime_int, maybe: u32) !T {
// This function recursively walks the _type union field (if there is one) to // This function recursively walks the _type union field (if there is one) to
// get the most specific class_id possible. // get the most specific class_id possible.
const Resolved = struct { const Resolved = struct {
weak: bool,
ptr: *anyopaque, ptr: *anyopaque,
class_id: u16, class_id: u16,
prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry, prototype_chain: []const @import("TaggedOpaque.zig").PrototypeChainEntry,
finalizer: ?Finalizer, finalizer_from_v8: ?*const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void = null,
finalizer_from_zig: ?*const fn (ptr: *anyopaque, session: *Session) void = null,
const Finalizer = struct {
// Resolved.ptr is the most specific value in a chain (e.g. IFrame, not EventTarget, Node, ...)
// Finalizer.ptr_id is the most specific value in a chain that defines an acquireRef
ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
acquireRef: *const fn (ptr_id: usize) void,
release: *const fn (handle: ?*const v8.WeakCallbackInfo) callconv(.c) void,
};
}; };
pub fn resolveValue(value: anytype) Resolved { pub fn resolveValue(value: anytype) Resolved {
const T = bridge.Struct(@TypeOf(value)); const T = bridge.Struct(@TypeOf(value));
@@ -1158,85 +1153,27 @@ pub fn resolveValue(value: anytype) Resolved {
unreachable; unreachable;
} }
fn resolveT(comptime T: type, value: *T) Resolved { fn resolveT(comptime T: type, value: *anyopaque) Resolved {
const Meta = T.JsApi.Meta; const Meta = T.JsApi.Meta;
return .{ return .{
.ptr = value, .ptr = value,
.class_id = Meta.class_id, .class_id = Meta.class_id,
.prototype_chain = &Meta.prototype_chain, .prototype_chain = &Meta.prototype_chain,
.finalizer = blk: { .weak = if (@hasDecl(Meta, "weak")) Meta.weak else false,
const FT = (comptime findFinalizerType(T)) orelse break :blk null; .finalizer_from_v8 = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_v8 else null,
const getFinalizerPtr = comptime finalizerPtrGetter(T, FT); .finalizer_from_zig = if (@hasDecl(Meta, "finalizer")) Meta.finalizer.from_zig else null,
const finalizer_ptr = getFinalizerPtr(value);
const Wrap = struct {
fn deinit(ptr_id: usize, session: *Session) void {
FT.deinit(@ptrFromInt(ptr_id), session);
}
fn acquireRef(ptr_id: usize) void {
FT.acquireRef(@ptrFromInt(ptr_id));
}
fn release(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const identity_finalizer: *Session.FinalizerCallback.Identity = @ptrCast(@alignCast(ptr));
const fc = identity_finalizer.fc;
if (identity_finalizer.identity.identity_map.fetchRemove(fc.resolved_ptr_id)) |kv| {
var global = kv.value;
v8.v8__Global__Reset(&global);
}
FT.releaseRef(@ptrFromInt(fc.finalizer_ptr_id), fc.session);
}
};
break :blk .{
.ptr_id = @intFromPtr(finalizer_ptr),
.deinit = Wrap.deinit,
.acquireRef = Wrap.acquireRef,
.release = Wrap.release,
};
},
}; };
} }
// Start at the "resolved" type (the most specific) and work our way up the fn conditionallyReference(value: anytype) void {
// prototype chain looking for the type that defines acquireRef const T = bridge.Struct(@TypeOf(value));
fn findFinalizerType(comptime T: type) ?type { if (@hasDecl(T, "acquireRef")) {
const S = bridge.Struct(T); value.acquireRef();
if (@hasDecl(S, "acquireRef")) { return;
return S;
} }
if (@hasField(S, "_proto")) { if (@hasField(T, "_proto")) {
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type; conditionallyReference(value._proto);
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
return findFinalizerType(ProtoChild);
} }
return null;
}
// Generate a function that follows the _proto pointer chain to get to the finalizer type
fn finalizerPtrGetter(comptime T: type, comptime FT: type) *const fn (*T) *FT {
const S = bridge.Struct(T);
if (S == FT) {
return struct {
fn get(v: *T) *FT {
return v;
}
}.get;
}
if (@hasField(S, "_proto")) {
const ProtoPtr = std.meta.fieldInfo(S, ._proto).type;
const ProtoChild = @typeInfo(ProtoPtr).pointer.child;
const childGetter = comptime finalizerPtrGetter(ProtoChild, FT);
return struct {
fn get(v: *T) *FT {
return childGetter(v._proto);
}
}.get;
}
@compileError("Cannot find path from " ++ @typeName(T) ++ " to " ++ @typeName(FT));
} }
pub fn stackTrace(self: *const Local) !?[]const u8 { pub fn stackTrace(self: *const Local) !?[]const u8 {
@@ -1444,34 +1381,6 @@ pub fn debugContextId(self: *const Local) i32 {
return v8.v8__Context__DebugContextId(self.handle); return v8.v8__Context__DebugContextId(self.handle);
} }
fn createFinalizerCallback(
self: *const Local,
// Key in identity map
// The most specific value (KeyboardEvent, not Event)
resolved_ptr_id: usize,
// The most specific value where finalizers are defined
// What actually gets acquired / released / deinit
finalizer_ptr_id: usize,
deinit: *const fn (ptr_id: usize, session: *Session) void,
) !*Session.FinalizerCallback {
const session = self.ctx.session;
const arena = try session.getArena(.{ .debug = "FinalizerCallback" });
errdefer session.releaseArena(arena);
const fc = try arena.create(Session.FinalizerCallback);
fc.* = .{
.arena = arena,
.session = session,
._deinit = deinit,
.resolved_ptr_id = resolved_ptr_id,
.finalizer_ptr_id = finalizer_ptr_id,
};
return fc;
}
// Encapsulates a Local and a HandleScope. When we're going from V8->Zig // Encapsulates a Local and a HandleScope. When we're going from V8->Zig
// we easily get both a Local and a HandleScope via Caller.init. // we easily get both a Local and a HandleScope via Caller.init.
// But when we're going from Zig -> V8, things are more complicated. // But when we're going from Zig -> V8, things are more complicated.

View File

@@ -67,7 +67,7 @@ fn _persist(self: *const Promise, comptime is_global: bool) !(if (is_global) Glo
return .{ .handle = global, .temps = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.session.temps }; return .{ .handle = global, .temps = &ctx.identity.temps };
} }
pub const Temp = G(.temp); pub const Temp = G(.temp);

View File

@@ -303,7 +303,7 @@ fn _persist(self: *const Value, comptime is_global: bool) !(if (is_global) Globa
return .{ .handle = global, .temps = {} }; return .{ .handle = global, .temps = {} };
} }
try ctx.trackTemp(global); try ctx.trackTemp(global);
return .{ .handle = global, .temps = &ctx.session.temps }; return .{ .handle = global, .temps = &ctx.identity.temps };
} }
pub fn toZig(self: Value, comptime T: type) !T { pub fn toZig(self: Value, comptime T: type) !T {

View File

@@ -101,19 +101,34 @@ pub fn Builder(comptime T: type) type {
} }
return entries; return entries;
} }
};
}
fn releaseRef(comptime T: type, ptr_id: usize, session: *Session) void { pub fn finalizer(comptime func: *const fn (self: *T, shutdown: bool, session: *Session) void) Finalizer {
if (@hasDecl(T, "releaseRef")) { return .{
T.releaseRef(@ptrFromInt(ptr_id), session); .from_zig = struct {
return; fn wrap(ptr: *anyopaque, session: *Session) void {
} func(@ptrCast(@alignCast(ptr)), true, session);
if (@hasField(T, "_proto")) { }
releaseRef(Struct(std.meta.fieldInfo(T, ._proto).type), ptr_id, session); }.wrap,
return;
} .from_v8 = struct {
@compileError(@typeName(T) ++ " marked with finalizer without an acquireRef in its prototype chain"); fn wrap(handle: ?*const v8.WeakCallbackInfo) callconv(.c) void {
const ptr = v8.v8__WeakCallbackInfo__GetParameter(handle.?).?;
const fc: *Session.FinalizerCallback = @ptrCast(@alignCast(ptr));
const value_ptr = fc.ptr;
if (fc.identity.finalizer_callbacks.contains(@intFromPtr(value_ptr))) {
func(@ptrCast(@alignCast(value_ptr)), false, fc.session);
fc.releaseIdentity();
} else {
// A bit weird, but v8 _requires_ that we release it
// If we don't. We'll 100% crash.
v8.v8__Global__Reset(&fc.global);
}
}
}.wrap,
};
}
};
} }
pub const Constructor = struct { pub const Constructor = struct {
@@ -396,6 +411,17 @@ pub const Property = struct {
} }
}; };
const Finalizer = struct {
// The finalizer wrapper when called from Zig. This is only called on
// Origin.deinit
from_zig: *const fn (ctx: *anyopaque, session: *Session) void,
// The finalizer wrapper when called from V8. This may never be called
// (hence why we fallback to calling in Origin.deinit). If it is called,
// it is only ever called after we SetWeak on the Global.
from_v8: *const fn (?*const v8.WeakCallbackInfo) callconv(.c) void,
};
pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 { pub fn unknownWindowPropertyCallback(c_name: ?*const v8.Name, handle: ?*const v8.PropertyCallbackInfo) callconv(.c) u8 {
const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?; const v8_isolate = v8.v8__PropertyCallbackInfo__GetIsolate(handle).?;
var caller: Caller = undefined; var caller: Caller = undefined;

View File

@@ -148,13 +148,3 @@
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('2d');
testing.expectTrue(ctx === element.getContext('2d'));
testing.expectEqual(null, element.getContext('webgl'));
}
</script>

View File

@@ -85,13 +85,3 @@
loseContext.restoreContext(); loseContext.restoreContext();
} }
</script> </script>
<script id=identity>
{
const element = document.createElement('canvas');
const ctx = element.getContext('webgl');
testing.expectTrue(ctx === element.getContext('webgl'));
testing.expectEqual(null, element.getContext('2d'));
}
</script>

View File

@@ -15,9 +15,8 @@
testing.expectEqual(true, validPlatforms.includes(navigator.platform)); testing.expectEqual(true, validPlatforms.includes(navigator.platform));
testing.expectEqual('en-US', navigator.language); testing.expectEqual('en-US', navigator.language);
testing.expectEqual(true, Array.isArray(navigator.languages)); testing.expectEqual(true, Array.isArray(navigator.languages));
testing.expectEqual(2, navigator.languages.length); testing.expectEqual(1, navigator.languages.length);
testing.expectEqual('en-US', navigator.languages[0]); testing.expectEqual('en-US', navigator.languages[0]);
testing.expectEqual('en', navigator.languages[1]);
testing.expectEqual(true, navigator.onLine); testing.expectEqual(true, navigator.onLine);
testing.expectEqual(true, navigator.cookieEnabled); testing.expectEqual(true, navigator.cookieEnabled);
testing.expectEqual(true, navigator.hardwareConcurrency > 0); testing.expectEqual(true, navigator.hardwareConcurrency > 0);

View File

@@ -17,8 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
@@ -33,7 +31,7 @@ const AbstractRange = @This();
pub const _prototype_root = true; pub const _prototype_root = true;
_rc: lp.RC(u8) = .{}, _rc: u8,
_type: Type, _type: Type,
_page_id: u32, _page_id: u32,
_arena: Allocator, _arena: Allocator,
@@ -46,18 +44,24 @@ _start_container: *Node,
_range_link: std.DoublyLinkedList.Node = .{}, _range_link: std.DoublyLinkedList.Node = .{},
pub fn acquireRef(self: *AbstractRange) void { pub fn acquireRef(self: *AbstractRange) void {
self._rc.acquire(); self._rc += 1;
} }
pub fn deinit(self: *AbstractRange, session: *Session) void { pub fn deinit(self: *AbstractRange, shutdown: bool, session: *Session) void {
if (session.findPageById(self._page_id)) |page| { _ = shutdown;
page._live_ranges.remove(&self._range_link); const rc = self._rc;
if (comptime IS_DEBUG) {
std.debug.assert(rc != 0);
} }
session.releaseArena(self._arena);
}
pub fn releaseRef(self: *AbstractRange, session: *Session) void { if (rc == 1) {
self._rc.release(self, session); if (session.findPageById(self._page_id)) |page| {
page._live_ranges.remove(&self._range_link);
}
session.releaseArena(self._arena);
return;
}
self._rc = rc - 1;
} }
pub const Type = union(enum) { pub const Type = union(enum) {
@@ -334,6 +338,8 @@ pub const JsApi = struct {
pub const name = "AbstractRange"; pub const name = "AbstractRange";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(AbstractRange.deinit);
}; };
pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{}); pub const startContainer = bridge.accessor(AbstractRange.getStartContainer, null, .{});

View File

@@ -17,7 +17,7 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda"); const Writer = std.Io.Writer;
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -25,7 +25,6 @@ const Session = @import("../Session.zig");
const Mime = @import("../Mime.zig"); const Mime = @import("../Mime.zig");
const Writer = std.Io.Writer;
const Allocator = std.mem.Allocator; const Allocator = std.mem.Allocator;
/// https://w3c.github.io/FileAPI/#blob-section /// https://w3c.github.io/FileAPI/#blob-section
@@ -35,7 +34,6 @@ const Blob = @This();
pub const _prototype_root = true; pub const _prototype_root = true;
_type: Type, _type: Type,
_rc: lp.RC(u32),
_arena: Allocator, _arena: Allocator,
@@ -122,7 +120,6 @@ pub fn initWithMimeValidation(
const self = try arena.create(Blob); const self = try arena.create(Blob);
self.* = .{ self.* = .{
._rc = .{},
._arena = arena, ._arena = arena,
._type = .generic, ._type = .generic,
._slice = data, ._slice = data,
@@ -131,18 +128,11 @@ pub fn initWithMimeValidation(
return self; return self;
} }
pub fn deinit(self: *Blob, session: *Session) void { pub fn deinit(self: *Blob, shutdown: bool, session: *Session) void {
_ = shutdown;
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *Blob, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Blob) void {
self._rc.acquire();
}
const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8); const largest_vector = @max(std.simd.suggestVectorLength(u8) orelse 1, 8);
/// Array of possible vector sizes for the current arch in decrementing order. /// Array of possible vector sizes for the current arch in decrementing order.
/// We may move this to some file for SIMD helpers in the future. /// We may move this to some file for SIMD helpers in the future.
@@ -335,6 +325,8 @@ pub const JsApi = struct {
pub const name = "Blob"; pub const name = "Blob";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Blob.deinit);
}; };
pub const constructor = bridge.constructor(Blob.init, .{}); pub const constructor = bridge.constructor(Blob.init, .{});

View File

@@ -59,7 +59,6 @@ pub fn fromError(err: anyerror) ?DOMException {
error.TimeoutError => .{ ._code = .timeout_error }, error.TimeoutError => .{ ._code = .timeout_error },
error.InvalidNodeType => .{ ._code = .invalid_node_type_error }, error.InvalidNodeType => .{ ._code = .invalid_node_type_error },
error.DataClone => .{ ._code = .data_clone_error }, error.DataClone => .{ ._code = .data_clone_error },
error.InvalidAccessError => .{ ._code = .invalid_access_error },
else => null, else => null,
}; };
} }

View File

@@ -61,7 +61,7 @@ _fonts: ?*FontFaceSet = null,
_write_insertion_point: ?*Node = null, _write_insertion_point: ?*Node = null,
_script_created_parser: ?Parser.Streaming = null, _script_created_parser: ?Parser.Streaming = null,
_adopted_style_sheets: ?js.Object.Global = null, _adopted_style_sheets: ?js.Object.Global = null,
_selection: Selection = .{ ._rc = .init(1) }, _selection: Selection = .init,
// https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter // https://html.spec.whatwg.org/multipage/dynamic-markup-insertion.html#throw-on-dynamic-markup-insertion-counter
// Incremented during custom element reactions when parsing. When > 0, // Incremented during custom element reactions when parsing. When > 0,

View File

@@ -17,8 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -57,7 +55,7 @@ _is_trusted: bool = false,
// - 0: no reference, always a transient state going to either 1 or about to be deinit'd // - 0: no reference, always a transient state going to either 1 or about to be deinit'd
// - 1: either zig or v8 have a reference // - 1: either zig or v8 have a reference
// - 2: both zig and v8 have a reference // - 2: both zig and v8 have a reference
_rc: lp.RC(u8) = .{}, _rc: u8 = 0,
pub const EventPhase = enum(u8) { pub const EventPhase = enum(u8) {
none = 0, none = 0,
@@ -141,16 +139,25 @@ pub fn initEvent(
} }
pub fn acquireRef(self: *Event) void { pub fn acquireRef(self: *Event) void {
self._rc.acquire(); self._rc += 1;
} }
/// Force cleanup on Session shutdown. pub fn deinit(self: *Event, shutdown: bool, session: *Session) void {
pub fn deinit(self: *Event, session: *Session) void { if (shutdown) {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} return;
}
pub fn releaseRef(self: *Event, session: *Session) void { const rc = self._rc;
self._rc.release(self, session); if (comptime IS_DEBUG) {
std.debug.assert(rc != 0);
}
if (rc == 1) {
session.releaseArena(self._arena);
} else {
self._rc = rc - 1;
}
} }
pub fn as(self: *Event, comptime T: type) *T { pub fn as(self: *Event, comptime T: type) *T {
@@ -433,6 +440,8 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Event.deinit);
pub const enumerable = false; pub const enumerable = false;
}; };

View File

@@ -60,7 +60,7 @@ pub fn dispatchEvent(self: *EventTarget, event: *Event, page: *Page) !bool {
event._is_trusted = false; event._is_trusted = false;
event.acquireRef(); event.acquireRef();
defer _ = event.releaseRef(page._session); defer event.deinit(false, page._session);
try page._event_manager.dispatch(self, event); try page._event_manager.dispatch(self, event);
return !event._cancelable or !event._prevent_default; return !event._cancelable or !event._prevent_default;
} }

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -27,6 +26,7 @@ const Blob = @import("Blob.zig");
const File = @This(); const File = @This();
/// `File` inherits `Blob`.
_proto: *Blob, _proto: *Blob,
// TODO: Implement File API. // TODO: Implement File API.
@@ -36,6 +36,10 @@ pub fn init(page: *Page) !*File {
return page._factory.blob(arena, File{ ._proto = undefined }); return page._factory.blob(arena, File{ ._proto = undefined });
} }
pub fn deinit(self: *File, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub const JsApi = struct { pub const JsApi = struct {
pub const bridge = js.Bridge(File); pub const bridge = js.Bridge(File);
@@ -43,6 +47,8 @@ pub const JsApi = struct {
pub const name = "File"; pub const name = "File";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(File.deinit);
}; };
pub const constructor = bridge.constructor(File.init, .{}); pub const constructor = bridge.constructor(File.init, .{});

View File

@@ -17,8 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -33,7 +31,6 @@ const Allocator = std.mem.Allocator;
/// https://developer.mozilla.org/en-US/docs/Web/API/FileReader /// https://developer.mozilla.org/en-US/docs/Web/API/FileReader
const FileReader = @This(); const FileReader = @This();
_rc: lp.RC(u8) = .{},
_page: *Page, _page: *Page,
_proto: *EventTarget, _proto: *EventTarget,
_arena: Allocator, _arena: Allocator,
@@ -73,7 +70,7 @@ pub fn init(page: *Page) !*FileReader {
}); });
} }
pub fn deinit(self: *FileReader, session: *Session) void { pub fn deinit(self: *FileReader, _: bool, session: *Session) void {
if (self._on_abort) |func| func.release(); if (self._on_abort) |func| func.release();
if (self._on_error) |func| func.release(); if (self._on_error) |func| func.release();
if (self._on_load) |func| func.release(); if (self._on_load) |func| func.release();
@@ -84,14 +81,6 @@ pub fn deinit(self: *FileReader, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *FileReader, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FileReader) void {
self._rc.acquire();
}
fn asEventTarget(self: *FileReader) *EventTarget { fn asEventTarget(self: *FileReader) *EventTarget {
return self._proto; return self._proto;
} }
@@ -320,6 +309,8 @@ pub const JsApi = struct {
pub const name = "FileReader"; pub const name = "FileReader";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FileReader.deinit);
}; };
pub const constructor = bridge.constructor(FileReader.init, .{}); pub const constructor = bridge.constructor(FileReader.init, .{});

View File

@@ -16,8 +16,6 @@
// You should have received a copy of the GNU Affero General Public License // 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/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const log = @import("../../log.zig"); const log = @import("../../log.zig");
@@ -41,7 +39,6 @@ pub fn registerTypes() []const type {
const IntersectionObserver = @This(); const IntersectionObserver = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_callback: js.Function.Temp, _callback: js.Function.Temp,
_observing: std.ArrayList(*Element) = .{}, _observing: std.ArrayList(*Element) = .{},
@@ -111,22 +108,15 @@ pub fn init(callback: js.Function.Temp, options: ?ObserverInit, page: *Page) !*I
return self; return self;
} }
pub fn deinit(self: *IntersectionObserver, session: *Session) void { pub fn deinit(self: *IntersectionObserver, shutdown: bool, session: *Session) void {
self._callback.release(); self._callback.release();
for (self._pending_entries.items) |entry| { if ((comptime IS_DEBUG) and !shutdown) {
entry.deinitIfUnused(session); std.debug.assert(self._observing.items.len == 0);
} }
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *IntersectionObserver, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *IntersectionObserver) void {
self._rc.acquire();
}
pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void { pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void {
// Check if already observing this target // Check if already observing this target
for (self._observing.items) |elem| { for (self._observing.items) |elem| {
@@ -137,7 +127,7 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
self._rc._refs += 1; page.js.strongRef(self);
try page.registerIntersectionObserver(self); try page.registerIntersectionObserver(self);
} }
@@ -154,19 +144,17 @@ pub fn observe(self: *IntersectionObserver, target: *Element, page: *Page) !void
} }
pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void { pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) void {
const original_length = self._observing.items.len;
for (self._observing.items, 0..) |elem, i| { for (self._observing.items, 0..) |elem, i| {
if (elem == target) { if (elem == target) {
_ = self._observing.swapRemove(i); _ = self._observing.swapRemove(i);
_ = self._previous_states.remove(target); _ = self._previous_states.remove(target);
// Remove any pending entries for this target. // Remove any pending entries for this target
// Entries will be cleaned up by V8 GC via the finalizer.
var j: usize = 0; var j: usize = 0;
while (j < self._pending_entries.items.len) { while (j < self._pending_entries.items.len) {
if (self._pending_entries.items[j]._target == target) { if (self._pending_entries.items[j]._target == target) {
const entry = self._pending_entries.swapRemove(j); const entry = self._pending_entries.swapRemove(j);
entry.deinitIfUnused(page._session); entry.deinit(false, page._session);
} else { } else {
j += 1; j += 1;
} }
@@ -175,26 +163,21 @@ pub fn unobserve(self: *IntersectionObserver, target: *Element, page: *Page) voi
} }
} }
if (original_length > 0 and self._observing.items.len == 0) { if (self._observing.items.len == 0) {
self._rc._refs -= 1; page.js.safeWeakRef(self);
} }
} }
pub fn disconnect(self: *IntersectionObserver, page: *Page) void { pub fn disconnect(self: *IntersectionObserver, page: *Page) void {
for (self._pending_entries.items) |entry| { page.unregisterIntersectionObserver(self);
entry.deinitIfUnused(page._session); self._observing.clearRetainingCapacity();
}
self._pending_entries.clearRetainingCapacity();
self._previous_states.clearRetainingCapacity(); self._previous_states.clearRetainingCapacity();
const observing_count = self._observing.items.len; for (self._pending_entries.items) |entry| {
self._observing.clearRetainingCapacity(); entry.deinit(false, page._session);
page.unregisterIntersectionObserver(self);
if (observing_count > 0) {
_ = self.releaseRef(page._session);
} }
self._pending_entries.clearRetainingCapacity();
page.js.safeWeakRef(self);
} }
pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry { pub fn takeRecords(self: *IntersectionObserver, page: *Page) ![]*IntersectionObserverEntry {
@@ -285,6 +268,7 @@ fn checkIntersection(self: *IntersectionObserver, target: *Element, page: *Page)
._bounding_client_rect = try page._factory.create(data.bounding_client_rect), ._bounding_client_rect = try page._factory.create(data.bounding_client_rect),
._intersection_ratio = data.intersection_ratio, ._intersection_ratio = data.intersection_ratio,
}; };
try self._pending_entries.append(self._arena, entry); try self._pending_entries.append(self._arena, entry);
} }
@@ -326,7 +310,6 @@ pub fn deliverEntries(self: *IntersectionObserver, page: *Page) !void {
} }
pub const IntersectionObserverEntry = struct { pub const IntersectionObserverEntry = struct {
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_time: f64, _time: f64,
_target: *Element, _target: *Element,
@@ -336,25 +319,10 @@ pub const IntersectionObserverEntry = struct {
_intersection_ratio: f64, _intersection_ratio: f64,
_is_intersecting: bool, _is_intersecting: bool,
pub fn deinit(self: *IntersectionObserverEntry, session: *Session) void { pub fn deinit(self: *IntersectionObserverEntry, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
fn deinitIfUnused(self: *IntersectionObserverEntry, session: *Session) void {
if (self._rc._refs == 0) {
// hasn't been handed to JS yet.
self.deinit(session);
}
}
pub fn releaseRef(self: *IntersectionObserverEntry, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *IntersectionObserverEntry) void {
self._rc.acquire();
}
pub fn getTarget(self: *const IntersectionObserverEntry) *Element { pub fn getTarget(self: *const IntersectionObserverEntry) *Element {
return self._target; return self._target;
} }
@@ -390,6 +358,8 @@ pub const IntersectionObserverEntry = struct {
pub const name = "IntersectionObserverEntry"; pub const name = "IntersectionObserverEntry";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserverEntry.deinit);
}; };
pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{}); pub const target = bridge.accessor(IntersectionObserverEntry.getTarget, null, .{});
@@ -409,6 +379,8 @@ pub const JsApi = struct {
pub const name = "IntersectionObserver"; pub const name = "IntersectionObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(IntersectionObserver.deinit);
}; };
pub const constructor = bridge.constructor(init, .{}); pub const constructor = bridge.constructor(init, .{});

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const String = @import("../../string.zig").String; const String = @import("../../string.zig").String;
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
@@ -40,7 +39,6 @@ pub fn registerTypes() []const type {
const MutationObserver = @This(); const MutationObserver = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_callback: js.Function.Temp, _callback: js.Function.Temp,
_observing: std.ArrayList(Observing) = .{}, _observing: std.ArrayList(Observing) = .{},
@@ -87,20 +85,15 @@ pub fn init(callback: js.Function.Temp, page: *Page) !*MutationObserver {
return self; return self;
} }
/// Force cleanup on Session shutdown. pub fn deinit(self: *MutationObserver, shutdown: bool, session: *Session) void {
pub fn deinit(self: *MutationObserver, session: *Session) void {
self._callback.release(); self._callback.release();
if ((comptime IS_DEBUG) and !shutdown) {
std.debug.assert(self._observing.items.len == 0);
}
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *MutationObserver, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *MutationObserver) void {
self._rc.acquire();
}
pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void { pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions, page: *Page) !void {
const arena = self._arena; const arena = self._arena;
@@ -165,7 +158,7 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
// Register with page if this is our first observation // Register with page if this is our first observation
if (self._observing.items.len == 0) { if (self._observing.items.len == 0) {
self._rc._refs += 1; page.js.strongRef(self);
try page.registerMutationObserver(self); try page.registerMutationObserver(self);
} }
@@ -176,17 +169,13 @@ pub fn observe(self: *MutationObserver, target: *Node, options: ObserveOptions,
} }
pub fn disconnect(self: *MutationObserver, page: *Page) void { pub fn disconnect(self: *MutationObserver, page: *Page) void {
page.unregisterMutationObserver(self);
self._observing.clearRetainingCapacity();
for (self._pending_records.items) |record| { for (self._pending_records.items) |record| {
_ = record.releaseRef(page._session); record.deinit(false, page._session);
} }
self._pending_records.clearRetainingCapacity(); self._pending_records.clearRetainingCapacity();
const observing_count = self._observing.items.len; page.js.safeWeakRef(self);
self._observing.clearRetainingCapacity();
if (observing_count > 0) {
_ = self.releaseRef(page._session);
}
page.unregisterMutationObserver(self);
} }
pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord { pub fn takeRecords(self: *MutationObserver, page: *Page) ![]*MutationRecord {
@@ -359,7 +348,6 @@ pub fn deliverRecords(self: *MutationObserver, page: *Page) !void {
} }
pub const MutationRecord = struct { pub const MutationRecord = struct {
_rc: lp.RC(u8) = .{},
_type: Type, _type: Type,
_target: *Node, _target: *Node,
_arena: Allocator, _arena: Allocator,
@@ -376,18 +364,10 @@ pub const MutationRecord = struct {
characterData, characterData,
}; };
pub fn deinit(self: *MutationRecord, session: *Session) void { pub fn deinit(self: *MutationRecord, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *MutationRecord, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *MutationRecord) void {
self._rc.acquire();
}
pub fn getType(self: *const MutationRecord) []const u8 { pub fn getType(self: *const MutationRecord) []const u8 {
return switch (self._type) { return switch (self._type) {
.attributes => "attributes", .attributes => "attributes",
@@ -438,6 +418,8 @@ pub const MutationRecord = struct {
pub const name = "MutationRecord"; pub const name = "MutationRecord";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationRecord.deinit);
}; };
pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{}); pub const @"type" = bridge.accessor(MutationRecord.getType, null, .{});
@@ -459,6 +441,8 @@ pub const JsApi = struct {
pub const name = "MutationObserver"; pub const name = "MutationObserver";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MutationObserver.deinit);
}; };
pub const constructor = bridge.constructor(MutationObserver.init, .{}); pub const constructor = bridge.constructor(MutationObserver.init, .{});

View File

@@ -40,8 +40,8 @@ pub fn getUserAgent(_: *const Navigator, page: *Page) []const u8 {
return page._session.browser.app.config.http_headers.user_agent; return page._session.browser.app.config.http_headers.user_agent;
} }
pub fn getLanguages(_: *const Navigator) [2][]const u8 { pub fn getLanguages(_: *const Navigator) [1][]const u8 {
return .{ "en-US", "en" }; return .{"en-US"};
} }
pub fn getPlatform(_: *const Navigator) []const u8 { pub fn getPlatform(_: *const Navigator) []const u8 {

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
const Session = @import("../Session.zig"); const Session = @import("../Session.zig");
@@ -51,23 +50,14 @@ pub fn query(_: *const Permissions, qd: QueryDescriptor, page: *Page) !js.Promis
} }
const PermissionStatus = struct { const PermissionStatus = struct {
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_name: []const u8, _name: []const u8,
_state: []const u8, _state: []const u8,
pub fn deinit(self: *PermissionStatus, session: *Session) void { pub fn deinit(self: *PermissionStatus, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *PermissionStatus, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *PermissionStatus) void {
self._rc.acquire();
}
fn getName(self: *const PermissionStatus) []const u8 { fn getName(self: *const PermissionStatus) []const u8 {
return self._name; return self._name;
} }
@@ -82,6 +72,8 @@ const PermissionStatus = struct {
pub const name = "PermissionStatus"; pub const name = "PermissionStatus";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PermissionStatus.deinit);
}; };
pub const name = bridge.accessor(getName, null, .{}); pub const name = bridge.accessor(getName, null, .{});
pub const state = bridge.accessor(getState, null, .{}); pub const state = bridge.accessor(getState, null, .{});

View File

@@ -38,6 +38,10 @@ pub fn init(page: *Page) !*Range {
return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page); return page._factory.abstractRange(arena, Range{ ._proto = undefined }, page);
} }
pub fn deinit(self: *Range, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asAbstractRange(self: *Range) *AbstractRange { pub fn asAbstractRange(self: *Range) *AbstractRange {
return self._proto; return self._proto;
} }
@@ -693,6 +697,8 @@ pub const JsApi = struct {
pub const name = "Range"; pub const name = "Range";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Range.deinit);
}; };
// Constants for compareBoundaryPoints // Constants for compareBoundaryPoints

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../js/js.zig"); const js = @import("../js/js.zig");
const Page = @import("../Page.zig"); const Page = @import("../Page.zig");
@@ -33,27 +32,18 @@ const Selection = @This();
pub const SelectionDirection = enum { backward, forward, none }; pub const SelectionDirection = enum { backward, forward, none };
_rc: lp.RC(u8) = .{},
_range: ?*Range = null, _range: ?*Range = null,
_direction: SelectionDirection = .none, _direction: SelectionDirection = .none,
pub const init: Selection = .{}; pub const init: Selection = .{};
pub fn deinit(self: *Selection, session: *Session) void { pub fn deinit(self: *Selection, shutdown: bool, session: *Session) void {
if (self._range) |r| { if (self._range) |r| {
r.asAbstractRange().releaseRef(session); r.deinit(shutdown, session);
self._range = null; self._range = null;
} }
} }
pub fn releaseRef(self: *Selection, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Selection) void {
self._rc.acquire();
}
fn dispatchSelectionChangeEvent(page: *Page) !void { fn dispatchSelectionChangeEvent(page: *Page) !void {
const event = try Event.init("selectionchange", .{}, page); const event = try Event.init("selectionchange", .{}, page);
try page._event_manager.dispatch(page.document.asEventTarget(), event); try page._event_manager.dispatch(page.document.asEventTarget(), event);
@@ -703,7 +693,7 @@ pub fn toString(self: *const Selection, page: *Page) ![]const u8 {
fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void { fn setRange(self: *Selection, new_range: ?*Range, page: *Page) void {
if (self._range) |existing| { if (self._range) |existing| {
_ = existing.asAbstractRange().releaseRef(page._session); existing.deinit(false, page._session);
} }
if (new_range) |nr| { if (new_range) |nr| {
nr.asAbstractRange().acquireRef(); nr.asAbstractRange().acquireRef();
@@ -718,6 +708,7 @@ pub const JsApi = struct {
pub const name = "Selection"; pub const name = "Selection";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const finalizer = bridge.finalizer(Selection.deinit);
}; };
pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{}); pub const anchorNode = bridge.accessor(Selection.getAnchorNode, null, .{});

View File

@@ -256,7 +256,8 @@ pub fn createObjectURL(blob: *Blob, page: *Page) ![]const u8 {
.{ page.origin orelse "null", uuid_buf }, .{ page.origin orelse "null", uuid_buf },
); );
try page._blob_urls.put(page.arena, blob_url, blob); try page._blob_urls.put(page.arena, blob_url, blob);
blob.acquireRef(); // prevent GC from cleaning up the blob while it's in the registry
page.js.strongRef(blob);
return blob_url; return blob_url;
} }
@@ -266,8 +267,9 @@ pub fn revokeObjectURL(url: []const u8, page: *Page) void {
return; return;
} }
// Remove from registry and release strong ref (no-op if not found)
if (page._blob_urls.fetchRemove(url)) |entry| { if (page._blob_urls.fetchRemove(url)) |entry| {
entry.value.releaseRef(page._session); page.js.weakRef(entry.value);
} }
} }

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
@@ -34,7 +33,6 @@ const PlayState = enum {
finished, finished,
}; };
_rc: lp.RC(u32) = .{},
_page: *Page, _page: *Page,
_arena: Allocator, _arena: Allocator,
@@ -64,18 +62,10 @@ pub fn init(page: *Page) !*Animation {
return self; return self;
} }
pub fn deinit(self: *Animation, session: *Session) void { pub fn deinit(self: *Animation, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *Animation, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Animation) void {
self._rc.acquire();
}
pub fn play(self: *Animation, page: *Page) !void { pub fn play(self: *Animation, page: *Page) !void {
if (self._playState == .running) { if (self._playState == .running) {
return; return;
@@ -85,7 +75,7 @@ pub fn play(self: *Animation, page: *Page) !void {
self._playState = .running; self._playState = .running;
// Schedule the transition from .running => .finished in 10ms. // Schedule the transition from .running => .finished in 10ms.
self.acquireRef(); page.js.strongRef(self);
try page.js.scheduler.add( try page.js.scheduler.add(
self, self,
Animation.update, Animation.update,
@@ -211,7 +201,7 @@ fn update(ctx: *anyopaque) !?u32 {
} }
// No future change scheduled, set the object weak for garbage collection. // No future change scheduled, set the object weak for garbage collection.
self.releaseRef(self._page._session); self._page.js.weakRef(self);
return null; return null;
} }
@@ -230,6 +220,8 @@ pub const JsApi = struct {
pub const name = "Animation"; pub const name = "Animation";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Animation.deinit);
}; };
pub const play = bridge.function(Animation.play, .{}); pub const play = bridge.function(Animation.play, .{});

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
@@ -38,9 +37,15 @@ _data: union(enum) {
radio_node_list: *RadioNodeList, radio_node_list: *RadioNodeList,
name: NodeLive(.name), name: NodeLive(.name),
}, },
_rc: lp.RC(u32) = .{}, _rc: usize = 0,
pub fn deinit(self: *NodeList, _: bool, session: *Session) void {
const rc = self._rc;
if (rc > 1) {
self._rc = rc - 1;
return;
}
pub fn deinit(self: *NodeList, session: *Session) void {
switch (self._data) { switch (self._data) {
.selector_list => |list| list.deinit(session), .selector_list => |list| list.deinit(session),
.child_nodes => |cn| cn.deinit(session), .child_nodes => |cn| cn.deinit(session),
@@ -48,12 +53,8 @@ pub fn deinit(self: *NodeList, session: *Session) void {
} }
} }
pub fn releaseRef(self: *NodeList, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *NodeList) void { pub fn acquireRef(self: *NodeList) void {
self._rc.acquire(); self._rc += 1;
} }
pub fn length(self: *NodeList, page: *Page) !u32 { pub fn length(self: *NodeList, page: *Page) !u32 {
@@ -118,12 +119,8 @@ const Iterator = struct {
const Entry = struct { u32, *Node }; const Entry = struct { u32, *Node };
pub fn deinit(self: *Iterator, session: *Session) void { pub fn deinit(self: *Iterator, shutdown: bool, session: *Session) void {
self.list.deinit(session); self.list.deinit(shutdown, session);
}
pub fn releaseRef(self: *Iterator, session: *Session) void {
self.list.releaseRef(session);
} }
pub fn acquireRef(self: *Iterator) void { pub fn acquireRef(self: *Iterator) void {
@@ -146,6 +143,8 @@ pub const JsApi = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const enumerable = false; pub const enumerable = false;
pub const weak = true;
pub const finalizer = bridge.finalizer(NodeList.deinit);
}; };
pub const length = bridge.accessor(NodeList.length, null, .{}); pub const length = bridge.accessor(NodeList.length, null, .{});

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
@@ -41,15 +40,9 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
return page._factory.create(Self{ .inner = inner }); return page._factory.create(Self{ .inner = inner });
} }
pub fn deinit(self: *Self, session: *Session) void { pub fn deinit(self: *Self, shutdown: bool, session: *Session) void {
_ = self; if (@hasDecl(Inner, "deinit")) {
_ = session; self.inner.deinit(shutdown, session);
}
pub fn releaseRef(self: *Self, session: *Session) void {
// Release the reference to the inner type that we acquired
if (@hasDecl(Inner, "releaseRef")) {
self.inner.releaseRef(session);
} }
} }
@@ -80,6 +73,8 @@ pub fn Entry(comptime Inner: type, comptime field: ?[]const u8) type {
pub const Meta = struct { pub const Meta = struct {
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Self.deinit);
}; };
pub const next = bridge.function(Self.next, .{ .null_as_undefined = true }); pub const next = bridge.function(Self.next, .{ .null_as_undefined = true });

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
@@ -26,7 +25,6 @@ const Allocator = std.mem.Allocator;
const FontFace = @This(); const FontFace = @This();
_rc: lp.RC(u8) = .{},
_arena: Allocator, _arena: Allocator,
_family: []const u8, _family: []const u8,
@@ -44,18 +42,10 @@ pub fn init(family: []const u8, source: []const u8, page: *Page) !*FontFace {
return self; return self;
} }
pub fn deinit(self: *FontFace, session: *Session) void { pub fn deinit(self: *FontFace, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *FontFace, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FontFace) void {
self._rc.acquire();
}
pub fn getFamily(self: *const FontFace) []const u8 { pub fn getFamily(self: *const FontFace) []const u8 {
return self._family; return self._family;
} }
@@ -77,6 +67,8 @@ pub const JsApi = struct {
pub const name = "FontFace"; pub const name = "FontFace";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFace.deinit);
}; };
pub const constructor = bridge.constructor(FontFace.init, .{}); pub const constructor = bridge.constructor(FontFace.init, .{});

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
const Session = @import("../../Session.zig"); const Session = @import("../../Session.zig");
@@ -29,7 +28,6 @@ const Allocator = std.mem.Allocator;
const FontFaceSet = @This(); const FontFaceSet = @This();
_rc: lp.RC(u8) = .{},
_proto: *EventTarget, _proto: *EventTarget,
_arena: Allocator, _arena: Allocator,
@@ -43,18 +41,10 @@ pub fn init(page: *Page) !*FontFaceSet {
}); });
} }
pub fn deinit(self: *FontFaceSet, session: *Session) void { pub fn deinit(self: *FontFaceSet, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *FontFaceSet, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *FontFaceSet) void {
self._rc.acquire();
}
pub fn asEventTarget(self: *FontFaceSet) *EventTarget { pub fn asEventTarget(self: *FontFaceSet) *EventTarget {
return self._proto; return self._proto;
} }
@@ -105,6 +95,8 @@ pub const JsApi = struct {
pub const name = "FontFaceSet"; pub const name = "FontFaceSet";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FontFaceSet.deinit);
}; };
pub const size = bridge.property(0, .{ .template = false, .readonly = true }); pub const size = bridge.property(0, .{ .template = false, .readonly = true });

View File

@@ -29,9 +29,6 @@ const OffscreenCanvas = @import("../../canvas/OffscreenCanvas.zig");
const Canvas = @This(); const Canvas = @This();
_proto: *HtmlElement, _proto: *HtmlElement,
_cached: ?DrawingContext = null,
const ContextType = enum { none, @"2d", webgl };
pub fn asElement(self: *Canvas) *Element { pub fn asElement(self: *Canvas) *Element {
return self._proto._proto; return self._proto._proto;
@@ -71,28 +68,17 @@ const DrawingContext = union(enum) {
}; };
pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext { pub fn getContext(self: *Canvas, context_type: []const u8, page: *Page) !?DrawingContext {
if (self._cached) |cached| { if (std.mem.eql(u8, context_type, "2d")) {
const matches = switch (cached) { const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self });
.@"2d" => std.mem.eql(u8, context_type, "2d"), return .{ .@"2d" = ctx };
.webgl => std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl"),
};
return if (matches) cached else null;
} }
const drawing_context: DrawingContext = blk: { if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) {
if (std.mem.eql(u8, context_type, "2d")) { const ctx = try page._factory.create(WebGLRenderingContext{});
const ctx = try page._factory.create(CanvasRenderingContext2D{ ._canvas = self }); return .{ .webgl = ctx };
break :blk .{ .@"2d" = ctx }; }
}
if (std.mem.eql(u8, context_type, "webgl") or std.mem.eql(u8, context_type, "experimental-webgl")) { return null;
const ctx = try page._factory.create(WebGLRenderingContext{});
break :blk .{ .webgl = ctx };
}
return null;
};
self._cached = drawing_context;
return drawing_context;
} }
/// Transfers control of the canvas to an OffscreenCanvas. /// Transfers control of the canvas to an OffscreenCanvas.

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const Page = @import("../../Page.zig"); const Page = @import("../../Page.zig");
@@ -26,7 +25,6 @@ const Allocator = std.mem.Allocator;
const TextDecoder = @This(); const TextDecoder = @This();
_rc: lp.RC(u8) = .{},
_fatal: bool, _fatal: bool,
_arena: Allocator, _arena: Allocator,
_ignore_bom: bool, _ignore_bom: bool,
@@ -62,18 +60,10 @@ pub fn init(label_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*TextDecoder {
return self; return self;
} }
pub fn deinit(self: *TextDecoder, session: *Session) void { pub fn deinit(self: *TextDecoder, _: bool, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *TextDecoder, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *TextDecoder) void {
self._rc.acquire();
}
pub fn getIgnoreBOM(self: *const TextDecoder) bool { pub fn getIgnoreBOM(self: *const TextDecoder) bool {
return self._ignore_bom; return self._ignore_bom;
} }
@@ -119,6 +109,8 @@ pub const JsApi = struct {
pub const name = "TextDecoder"; pub const name = "TextDecoder";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextDecoder.deinit);
}; };
pub const constructor = bridge.constructor(TextDecoder.init, .{}); pub const constructor = bridge.constructor(TextDecoder.init, .{});

View File

@@ -53,6 +53,10 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*CompositionEvent {
return event; return event;
} }
pub fn deinit(self: *CompositionEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *CompositionEvent) *Event { pub fn asEvent(self: *CompositionEvent) *Event {
return self._proto; return self._proto;
} }
@@ -68,6 +72,8 @@ pub const JsApi = struct {
pub const name = "CompositionEvent"; pub const name = "CompositionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CompositionEvent.deinit);
}; };
pub const constructor = bridge.constructor(CompositionEvent.init, .{}); pub const constructor = bridge.constructor(CompositionEvent.init, .{});

View File

@@ -73,19 +73,11 @@ pub fn initCustomEvent(
self._detail = detail_; self._detail = detail_;
} }
pub fn deinit(self: *CustomEvent, session: *Session) void { pub fn deinit(self: *CustomEvent, shutdown: bool, session: *Session) void {
if (self._detail) |d| { if (self._detail) |d| {
d.release(); d.release();
} }
self._proto.deinit(session); self._proto.deinit(shutdown, session);
}
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 { pub fn asEvent(self: *CustomEvent) *Event {
@@ -103,6 +95,8 @@ pub const JsApi = struct {
pub const name = "CustomEvent"; pub const name = "CustomEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(CustomEvent.deinit);
pub const enumerable = false; pub const enumerable = false;
}; };

View File

@@ -80,19 +80,11 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *ErrorEvent, session: *Session) void { pub fn deinit(self: *ErrorEvent, shutdown: bool, session: *Session) void {
if (self._error) |e| { if (self._error) |e| {
e.release(); e.release();
} }
self._proto.deinit(session); self._proto.deinit(shutdown, session);
}
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 { pub fn asEvent(self: *ErrorEvent) *Event {

View File

@@ -70,6 +70,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *FocusEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *FocusEvent) *Event { pub fn asEvent(self: *FocusEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -85,6 +89,8 @@ pub const JsApi = struct {
pub const name = "FocusEvent"; pub const name = "FocusEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FocusEvent.deinit);
}; };
pub const constructor = bridge.constructor(FocusEvent.init, .{}); pub const constructor = bridge.constructor(FocusEvent.init, .{});

View File

@@ -66,6 +66,10 @@ fn initWithTrusted(arena: Allocator, typ: String, maybe_options: ?Options, trust
return event; return event;
} }
pub fn deinit(self: *FormDataEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *FormDataEvent) *Event { pub fn asEvent(self: *FormDataEvent) *Event {
return self._proto; return self._proto;
} }
@@ -81,6 +85,8 @@ pub const JsApi = struct {
pub const name = "FormDataEvent"; pub const name = "FormDataEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(FormDataEvent.deinit);
}; };
pub const constructor = bridge.constructor(FormDataEvent.init, .{}); pub const constructor = bridge.constructor(FormDataEvent.init, .{});

View File

@@ -83,6 +83,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *InputEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *InputEvent) *Event { pub fn asEvent(self: *InputEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -106,6 +110,8 @@ pub const JsApi = struct {
pub const name = "InputEvent"; pub const name = "InputEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(InputEvent.deinit);
}; };
pub const constructor = bridge.constructor(InputEvent.init, .{}); pub const constructor = bridge.constructor(InputEvent.init, .{});

View File

@@ -229,6 +229,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *KeyboardEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *KeyboardEvent) *Event { pub fn asEvent(self: *KeyboardEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -292,6 +296,8 @@ pub const JsApi = struct {
pub const name = "KeyboardEvent"; pub const name = "KeyboardEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(KeyboardEvent.deinit);
}; };
pub const constructor = bridge.constructor(KeyboardEvent.init, .{}); pub const constructor = bridge.constructor(KeyboardEvent.init, .{});

View File

@@ -73,19 +73,11 @@ fn initWithTrusted(arena: Allocator, typ: String, opts_: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *MessageEvent, session: *Session) void { pub fn deinit(self: *MessageEvent, shutdown: bool, session: *Session) void {
if (self._data) |d| { if (self._data) |d| {
d.release(); d.release();
} }
self._proto.deinit(session); self._proto.deinit(shutdown, session);
}
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 asEvent(self: *MessageEvent) *Event { pub fn asEvent(self: *MessageEvent) *Event {
@@ -111,6 +103,8 @@ pub const JsApi = struct {
pub const name = "MessageEvent"; pub const name = "MessageEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MessageEvent.deinit);
}; };
pub const constructor = bridge.constructor(MessageEvent.init, .{}); pub const constructor = bridge.constructor(MessageEvent.init, .{});

View File

@@ -121,6 +121,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *MouseEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *MouseEvent) *Event { pub fn asEvent(self: *MouseEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -199,6 +203,8 @@ pub const JsApi = struct {
pub const name = "MouseEvent"; pub const name = "MouseEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(MouseEvent.deinit);
}; };
pub const constructor = bridge.constructor(MouseEvent.init, .{}); pub const constructor = bridge.constructor(MouseEvent.init, .{});

View File

@@ -83,6 +83,10 @@ fn initWithTrusted(
return event; return event;
} }
pub fn deinit(self: *NavigationCurrentEntryChangeEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event { pub fn asEvent(self: *NavigationCurrentEntryChangeEvent) *Event {
return self._proto; return self._proto;
} }
@@ -102,6 +106,8 @@ pub const JsApi = struct {
pub const name = "NavigationCurrentEntryChangeEvent"; pub const name = "NavigationCurrentEntryChangeEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(NavigationCurrentEntryChangeEvent.deinit);
}; };
pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{}); pub const constructor = bridge.constructor(NavigationCurrentEntryChangeEvent.init, .{});

View File

@@ -66,6 +66,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *PageTransitionEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PageTransitionEvent) *Event { pub fn asEvent(self: *PageTransitionEvent) *Event {
return self._proto; return self._proto;
} }
@@ -81,6 +85,8 @@ pub const JsApi = struct {
pub const name = "PageTransitionEvent"; pub const name = "PageTransitionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PageTransitionEvent.deinit);
}; };
pub const constructor = bridge.constructor(PageTransitionEvent.init, .{}); pub const constructor = bridge.constructor(PageTransitionEvent.init, .{});

View File

@@ -128,6 +128,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*PointerEvent {
return event; return event;
} }
pub fn deinit(self: *PointerEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PointerEvent) *Event { pub fn asEvent(self: *PointerEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -187,6 +191,8 @@ pub const JsApi = struct {
pub const name = "PointerEvent"; pub const name = "PointerEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PointerEvent.deinit);
}; };
pub const constructor = bridge.constructor(PointerEvent.init, .{}); pub const constructor = bridge.constructor(PointerEvent.init, .{});

View File

@@ -67,6 +67,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *PopStateEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *PopStateEvent) *Event { pub fn asEvent(self: *PopStateEvent) *Event {
return self._proto; return self._proto;
} }
@@ -88,6 +92,8 @@ pub const JsApi = struct {
pub const name = "PopStateEvent"; pub const name = "PopStateEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PopStateEvent.deinit);
}; };
pub const constructor = bridge.constructor(PopStateEvent.init, .{}); pub const constructor = bridge.constructor(PopStateEvent.init, .{});

View File

@@ -68,6 +68,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *ProgressEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *ProgressEvent) *Event { pub fn asEvent(self: *ProgressEvent) *Event {
return self._proto; return self._proto;
} }
@@ -92,6 +96,8 @@ pub const JsApi = struct {
pub const name = "ProgressEvent"; pub const name = "ProgressEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(ProgressEvent.deinit);
}; };
pub const constructor = bridge.constructor(ProgressEvent.init, .{}); pub const constructor = bridge.constructor(ProgressEvent.init, .{});

View File

@@ -56,22 +56,14 @@ pub fn init(typ: []const u8, opts_: ?Options, page: *Page) !*PromiseRejectionEve
return event; return event;
} }
pub fn deinit(self: *PromiseRejectionEvent, session: *Session) void { pub fn deinit(self: *PromiseRejectionEvent, shutdown: bool, session: *Session) void {
if (self._reason) |r| { if (self._reason) |r| {
r.release(); r.release();
} }
if (self._promise) |p| { if (self._promise) |p| {
p.release(); p.release();
} }
self._proto.deinit(session); self._proto.deinit(shutdown, session);
}
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 { pub fn asEvent(self: *PromiseRejectionEvent) *Event {
@@ -93,6 +85,8 @@ pub const JsApi = struct {
pub const name = "PromiseRejectionEvent"; pub const name = "PromiseRejectionEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(PromiseRejectionEvent.deinit);
}; };
pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{}); pub const constructor = bridge.constructor(PromiseRejectionEvent.init, .{});

View File

@@ -67,6 +67,10 @@ fn initWithTrusted(arena: Allocator, typ: String, _opts: ?Options, trusted: bool
return event; return event;
} }
pub fn deinit(self: *SubmitEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *SubmitEvent) *Event { pub fn asEvent(self: *SubmitEvent) *Event {
return self._proto; return self._proto;
} }
@@ -82,6 +86,8 @@ pub const JsApi = struct {
pub const name = "SubmitEvent"; pub const name = "SubmitEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(SubmitEvent.deinit);
}; };
pub const constructor = bridge.constructor(SubmitEvent.init, .{}); pub const constructor = bridge.constructor(SubmitEvent.init, .{});

View File

@@ -59,6 +59,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*TextEvent {
return event; return event;
} }
pub fn deinit(self: *TextEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *TextEvent) *Event { pub fn asEvent(self: *TextEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -97,6 +101,8 @@ pub const JsApi = struct {
pub const name = "TextEvent"; pub const name = "TextEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(TextEvent.deinit);
}; };
// No constructor - TextEvent is created via document.createEvent('TextEvent') // No constructor - TextEvent is created via document.createEvent('TextEvent')

View File

@@ -71,6 +71,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*UIEvent {
return event; return event;
} }
pub fn deinit(self: *UIEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn as(self: *UIEvent, comptime T: type) *T { pub fn as(self: *UIEvent, comptime T: type) *T {
return self.is(T).?; return self.is(T).?;
} }
@@ -118,6 +122,8 @@ pub const JsApi = struct {
pub const name = "UIEvent"; pub const name = "UIEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(UIEvent.deinit);
}; };
pub const constructor = bridge.constructor(UIEvent.init, .{}); pub const constructor = bridge.constructor(UIEvent.init, .{});

View File

@@ -87,6 +87,10 @@ pub fn init(typ: []const u8, _opts: ?Options, page: *Page) !*WheelEvent {
return event; return event;
} }
pub fn deinit(self: *WheelEvent, shutdown: bool, session: *Session) void {
self._proto.deinit(shutdown, session);
}
pub fn asEvent(self: *WheelEvent) *Event { pub fn asEvent(self: *WheelEvent) *Event {
return self._proto.asEvent(); return self._proto.asEvent();
} }
@@ -114,6 +118,8 @@ pub const JsApi = struct {
pub const name = "WheelEvent"; pub const name = "WheelEvent";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(WheelEvent.deinit);
}; };
pub const constructor = bridge.constructor(WheelEvent.init, .{}); pub const constructor = bridge.constructor(WheelEvent.init, .{});

View File

@@ -62,7 +62,7 @@ pub fn init(input: Input, options: ?InitOpts, page: *Page) !js.Promise {
} }
const response = try Response.init(null, .{ .status = 0 }, page); const response = try Response.init(null, .{ .status = 0 }, page);
errdefer response.deinit(page._session); errdefer response.deinit(true, page._session);
const fetch = try response._arena.create(Fetch); const fetch = try response._arena.create(Fetch);
fetch.* = .{ fetch.* = .{
@@ -226,7 +226,7 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
return ls.toLocal(self._resolver).resolve("fetch done", js_val); return ls.toLocal(self._resolver).resolve("fetch done", js_val);
} }
fn httpErrorCallback(ctx: *anyopaque, _: anyerror) void { fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *Fetch = @ptrCast(@alignCast(ctx)); const self: *Fetch = @ptrCast(@alignCast(ctx));
var response = self._response; var response = self._response;
@@ -235,7 +235,7 @@ fn httpErrorCallback(ctx: *anyopaque, _: anyerror) void {
// clear this. (defer since `self is in the response's arena). // clear this. (defer since `self is in the response's arena).
defer if (self._owns_response) { defer if (self._owns_response) {
response.deinit(self._page._session); response.deinit(err == error.Abort, self._page._session);
self._owns_response = false; self._owns_response = false;
}; };
@@ -257,7 +257,7 @@ fn httpShutdownCallback(ctx: *anyopaque) void {
if (self._owns_response) { if (self._owns_response) {
var response = self._response; var response = self._response;
response._transfer = null; response._transfer = null;
response.deinit(self._page._session); response.deinit(true, self._page._session);
// Do not access `self` after this point: the Fetch struct was // Do not access `self` after this point: the Fetch struct was
// allocated from response._arena which has been released. // allocated from response._arena which has been released.
} }

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const HttpClient = @import("../../HttpClient.zig"); const HttpClient = @import("../../HttpClient.zig");
@@ -39,7 +38,6 @@ pub const Type = enum {
opaqueredirect, opaqueredirect,
}; };
_rc: lp.RC(u8) = .{},
_status: u16, _status: u16,
_arena: Allocator, _arena: Allocator,
_headers: *Headers, _headers: *Headers,
@@ -80,22 +78,18 @@ pub fn init(body_: ?[]const u8, opts_: ?InitOpts, page: *Page) !*Response {
return self; return self;
} }
pub fn deinit(self: *Response, session: *Session) void { pub fn deinit(self: *Response, shutdown: bool, session: *Session) void {
if (self._transfer) |transfer| { if (self._transfer) |transfer| {
transfer.abort(error.Abort); if (shutdown) {
transfer.terminate();
} else {
transfer.abort(error.Abort);
}
self._transfer = null; self._transfer = null;
} }
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *Response, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *Response) void {
self._rc.acquire();
}
pub fn getStatus(self: *const Response) u16 { pub fn getStatus(self: *const Response) u16 {
return self._status; return self._status;
} }
@@ -203,6 +197,8 @@ pub const JsApi = struct {
pub const name = "Response"; pub const name = "Response";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(Response.deinit);
}; };
pub const constructor = bridge.constructor(Response.init, .{}); pub const constructor = bridge.constructor(Response.init, .{});

View File

@@ -17,7 +17,6 @@
// along with this program. If not, see <https://www.gnu.org/licenses/>. // along with this program. If not, see <https://www.gnu.org/licenses/>.
const std = @import("std"); const std = @import("std");
const lp = @import("lightpanda");
const js = @import("../../js/js.zig"); const js = @import("../../js/js.zig");
const log = @import("../../../log.zig"); const log = @import("../../../log.zig");
@@ -39,7 +38,6 @@ const Allocator = std.mem.Allocator;
const IS_DEBUG = @import("builtin").mode == .Debug; const IS_DEBUG = @import("builtin").mode == .Debug;
const XMLHttpRequest = @This(); const XMLHttpRequest = @This();
_rc: lp.RC(u8) = .{},
_page: *Page, _page: *Page,
_proto: *XMLHttpRequestEventTarget, _proto: *XMLHttpRequestEventTarget,
_arena: Allocator, _arena: Allocator,
@@ -89,18 +87,21 @@ const ResponseType = enum {
pub fn init(page: *Page) !*XMLHttpRequest { pub fn init(page: *Page) !*XMLHttpRequest {
const arena = try page.getArena(.{ .debug = "XMLHttpRequest" }); const arena = try page.getArena(.{ .debug = "XMLHttpRequest" });
errdefer page.releaseArena(arena); errdefer page.releaseArena(arena);
const xhr = try page._factory.xhrEventTarget(arena, XMLHttpRequest{ return page._factory.xhrEventTarget(arena, XMLHttpRequest{
._page = page, ._page = page,
._arena = arena, ._arena = arena,
._proto = undefined, ._proto = undefined,
._request_headers = try Headers.init(null, page), ._request_headers = try Headers.init(null, page),
}); });
return xhr;
} }
pub fn deinit(self: *XMLHttpRequest, session: *Session) void { pub fn deinit(self: *XMLHttpRequest, shutdown: bool, session: *Session) void {
if (self._transfer) |transfer| { if (self._transfer) |transfer| {
transfer.abort(error.Abort); if (shutdown) {
transfer.terminate();
} else {
transfer.abort(error.Abort);
}
self._transfer = null; self._transfer = null;
} }
@@ -136,14 +137,6 @@ pub fn deinit(self: *XMLHttpRequest, session: *Session) void {
session.releaseArena(self._arena); session.releaseArena(self._arena);
} }
pub fn releaseRef(self: *XMLHttpRequest, session: *Session) void {
self._rc.release(self, session);
}
pub fn acquireRef(self: *XMLHttpRequest) void {
self._rc.acquire();
}
fn asEventTarget(self: *XMLHttpRequest) *EventTarget { fn asEventTarget(self: *XMLHttpRequest) *EventTarget {
return self._proto._proto; return self._proto._proto;
} }
@@ -252,6 +245,8 @@ pub fn send(self: *XMLHttpRequest, body_: ?[]const u8) !void {
.error_callback = httpErrorCallback, .error_callback = httpErrorCallback,
.shutdown_callback = httpShutdownCallback, .shutdown_callback = httpShutdownCallback,
}); });
page.js.strongRef(self);
} }
fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void { fn handleBlobUrl(self: *XMLHttpRequest, page: *Page) !void {
@@ -393,7 +388,6 @@ fn httpStartCallback(transfer: *HttpClient.Transfer) !void {
log.debug(.http, "request start", .{ .method = self._method, .url = self._url, .source = "xhr" }); log.debug(.http, "request start", .{ .method = self._method, .url = self._url, .source = "xhr" });
} }
self._transfer = transfer; self._transfer = transfer;
self.acquireRef();
} }
fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void { fn httpHeaderCallback(transfer: *HttpClient.Transfer, header: net_http.Header) !void {
@@ -492,17 +486,15 @@ fn httpDoneCallback(ctx: *anyopaque) !void {
.loaded = loaded, .loaded = loaded,
}, page); }, page);
self.releaseRef(page._session); page.js.weakRef(self);
} }
fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void { fn httpErrorCallback(ctx: *anyopaque, err: anyerror) void {
const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx)); const self: *XMLHttpRequest = @ptrCast(@alignCast(ctx));
// http client will close it after an error, it isn't safe to keep around // http client will close it after an error, it isn't safe to keep around
self._transfer = null;
self.handleError(err); self.handleError(err);
if (self._transfer != null) { self._page.js.weakRef(self);
self._transfer = null;
self.releaseRef(self._page._session);
}
} }
fn httpShutdownCallback(ctx: *anyopaque) void { fn httpShutdownCallback(ctx: *anyopaque) void {
@@ -513,10 +505,10 @@ fn httpShutdownCallback(ctx: *anyopaque) void {
pub fn abort(self: *XMLHttpRequest) void { pub fn abort(self: *XMLHttpRequest) void {
self.handleError(error.Abort); self.handleError(error.Abort);
if (self._transfer) |transfer| { if (self._transfer) |transfer| {
self._transfer = null;
transfer.abort(error.Abort); transfer.abort(error.Abort);
self.releaseRef(self._page._session); self._transfer = null;
} }
self._page.js.weakRef(self);
} }
fn handleError(self: *XMLHttpRequest, err: anyerror) void { fn handleError(self: *XMLHttpRequest, err: anyerror) void {
@@ -590,6 +582,8 @@ pub const JsApi = struct {
pub const name = "XMLHttpRequest"; pub const name = "XMLHttpRequest";
pub const prototype_chain = bridge.prototypeChain(); pub const prototype_chain = bridge.prototypeChain();
pub var class_id: bridge.ClassId = undefined; pub var class_id: bridge.ClassId = undefined;
pub const weak = true;
pub const finalizer = bridge.finalizer(XMLHttpRequest.deinit);
}; };
pub const constructor = bridge.constructor(XMLHttpRequest.init, .{}); pub const constructor = bridge.constructor(XMLHttpRequest.init, .{});

View File

@@ -363,11 +363,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
inspector_session: *js.Inspector.Session, inspector_session: *js.Inspector.Session,
isolated_worlds: std.ArrayList(*IsolatedWorld), isolated_worlds: std.ArrayList(*IsolatedWorld),
// Scripts registered via Page.addScriptToEvaluateOnNewDocument.
// Evaluated in each new document after navigation completes.
scripts_on_new_document: std.ArrayList(ScriptOnNewDocument) = .empty,
next_script_id: u32 = 1,
http_proxy_changed: bool = false, http_proxy_changed: bool = false,
// Extra headers to add to all requests. // Extra headers to add to all requests.
@@ -432,6 +427,8 @@ pub fn BrowserContext(comptime CDP_T: type) type {
try notification.register(.page_navigate, self, onPageNavigate); try notification.register(.page_navigate, self, onPageNavigate);
try notification.register(.page_navigated, self, onPageNavigated); try notification.register(.page_navigated, self, onPageNavigated);
try notification.register(.page_frame_created, self, onPageFrameCreated); try notification.register(.page_frame_created, self, onPageFrameCreated);
try notification.register(.page_dom_content_loaded, self, onPageDOMContentLoaded);
try notification.register(.page_loaded, self, onPageLoaded);
} }
pub fn deinit(self: *Self) void { pub fn deinit(self: *Self) void {
@@ -607,6 +604,16 @@ pub fn BrowserContext(comptime CDP_T: type) type {
return @import("domains/page.zig").pageFrameCreated(self, msg); return @import("domains/page.zig").pageFrameCreated(self, msg);
} }
pub fn onPageDOMContentLoaded(ctx: *anyopaque, msg: *const Notification.PageDOMContentLoaded) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageDOMContentLoaded(self, msg);
}
pub fn onPageLoaded(ctx: *anyopaque, msg: *const Notification.PageLoaded) !void {
const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageLoaded(self, msg);
}
pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void { pub fn onPageNetworkIdle(ctx: *anyopaque, msg: *const Notification.PageNetworkIdle) !void {
const self: *Self = @ptrCast(@alignCast(ctx)); const self: *Self = @ptrCast(@alignCast(ctx));
return @import("domains/page.zig").pageNetworkIdle(self, msg); return @import("domains/page.zig").pageNetworkIdle(self, msg);
@@ -767,11 +774,6 @@ pub fn BrowserContext(comptime CDP_T: type) type {
/// Clients create this to be able to create variables and run code without interfering with the /// Clients create this to be able to create variables and run code without interfering with the
/// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after /// normal namespace and values of the webpage. Similar to the main context we need to pretend to recreate it after
/// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed /// a executionContextsCleared event which happens when navigating to a new page. A client can have a command be executed
const ScriptOnNewDocument = struct {
identifier: u32,
source: []const u8,
};
/// in the isolated world by using its Context ID or the worldName. /// in the isolated world by using its Context ID or the worldName.
/// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects. /// grantUniveralAccess Indecated whether the isolated world can reference objects like the DOM or other JS Objects.
/// An isolated world has it's own instance of globals like Window. /// An isolated world has it's own instance of globals like Window.

View File

@@ -37,7 +37,6 @@ pub fn processMessage(cmd: anytype) !void {
getFrameTree, getFrameTree,
setLifecycleEventsEnabled, setLifecycleEventsEnabled,
addScriptToEvaluateOnNewDocument, addScriptToEvaluateOnNewDocument,
removeScriptToEvaluateOnNewDocument,
createIsolatedWorld, createIsolatedWorld,
navigate, navigate,
reload, reload,
@@ -52,7 +51,6 @@ pub fn processMessage(cmd: anytype) !void {
.getFrameTree => return getFrameTree(cmd), .getFrameTree => return getFrameTree(cmd),
.setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd), .setLifecycleEventsEnabled => return setLifecycleEventsEnabled(cmd),
.addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd), .addScriptToEvaluateOnNewDocument => return addScriptToEvaluateOnNewDocument(cmd),
.removeScriptToEvaluateOnNewDocument => return removeScriptToEvaluateOnNewDocument(cmd),
.createIsolatedWorld => return createIsolatedWorld(cmd), .createIsolatedWorld => return createIsolatedWorld(cmd),
.navigate => return navigate(cmd), .navigate => return navigate(cmd),
.reload => return doReload(cmd), .reload => return doReload(cmd),
@@ -149,55 +147,22 @@ fn setLifecycleEventsEnabled(cmd: anytype) !void {
return cmd.sendResult(null, .{}); return cmd.sendResult(null, .{});
} }
// TODO: hard coded method
// With the command we receive a script we need to store and run for each new document.
// Note that the worldName refers to the name given to the isolated world.
fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void { fn addScriptToEvaluateOnNewDocument(cmd: anytype) !void {
const params = (try cmd.params(struct { // const params = (try cmd.params(struct {
source: []const u8, // source: []const u8,
worldName: ?[]const u8 = null, // worldName: ?[]const u8 = null,
includeCommandLineAPI: bool = false, // includeCommandLineAPI: bool = false,
runImmediately: bool = false, // runImmediately: bool = false,
})) orelse return error.InvalidParams; // })) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
if (params.runImmediately) {
log.warn(.not_implemented, "addScriptOnNewDocument", .{ .param = "runImmediately" });
}
const script_id = bc.next_script_id;
bc.next_script_id += 1;
const source_dupe = try bc.arena.dupe(u8, params.source);
try bc.scripts_on_new_document.append(bc.arena, .{
.identifier = script_id,
.source = source_dupe,
});
var id_buf: [16]u8 = undefined;
const id_str = std.fmt.bufPrint(&id_buf, "{d}", .{script_id}) catch "1";
return cmd.sendResult(.{ return cmd.sendResult(.{
.identifier = id_str, .identifier = "1",
}, .{}); }, .{});
} }
fn removeScriptToEvaluateOnNewDocument(cmd: anytype) !void {
const params = (try cmd.params(struct {
identifier: []const u8,
})) orelse return error.InvalidParams;
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
const target_id = std.fmt.parseInt(u32, params.identifier, 10) catch
return cmd.sendResult(null, .{});
for (bc.scripts_on_new_document.items, 0..) |script, i| {
if (script.identifier == target_id) {
_ = bc.scripts_on_new_document.orderedRemove(i);
break;
}
}
return cmd.sendResult(null, .{});
}
fn close(cmd: anytype) !void { fn close(cmd: anytype) !void {
const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded;
@@ -420,7 +385,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
// things, but no session. // things, but no session.
const session_id = bc.session_id orelse return; const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
const frame_id = &id.toFrameId(event.frame_id); const frame_id = &id.toFrameId(event.frame_id);
const loader_id = &id.toLoaderId(event.req_id); const loader_id = &id.toLoaderId(event.req_id);
@@ -472,9 +436,9 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
const page = bc.session.currentPage() orelse return error.PageNotLoaded; const page = bc.session.currentPage() orelse return error.PageNotLoaded;
// When we actually recreated the context we should have the inspector send // When we actually recreated the context we should have the inspector send
// this event, see: resetContextGroup Sending this event will tell the // this event, see: resetContextGroup. Sending this event will tell the
// client that the context ids they had are invalid and the context shouls // client that the context ids they had are invalid and the context should
// be dropped The client will expect us to send new contextCreated events, // be dropped. The client will expect us to send new contextCreated events,
// such that the client has new id's for the active contexts. // such that the client has new id's for the active contexts.
// Only send executionContextsCleared for main frame navigations. For child // Only send executionContextsCleared for main frame navigations. For child
// frames (iframes), clearing all contexts would destroy the main frame's // frames (iframes), clearing all contexts would destroy the main frame's
@@ -484,6 +448,18 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id }); try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id });
} }
// frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation",
.frame = Frame{
.id = frame_id,
.url = event.url,
.loaderId = loader_id,
.securityOrigin = bc.security_origin,
.secureContextType = bc.secure_context_type,
},
}, .{ .session_id = session_id });
{ {
const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id }); const aux_data = try std.fmt.allocPrint(arena, "{{\"isDefault\":true,\"type\":\"default\",\"frameId\":\"{s}\",\"loaderId\":\"{s}\"}}", .{ frame_id, loader_id });
@@ -517,27 +493,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
); );
} }
// Evaluate scripts registered via Page.addScriptToEvaluateOnNewDocument.
// Must run after the execution context is created but before the client
// receives frameNavigated/loadEventFired so polyfills are available for
// subsequent CDP commands.
if (bc.scripts_on_new_document.items.len > 0) {
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
for (bc.scripts_on_new_document.items) |script| {
var try_catch: lp.js.TryCatch = undefined;
try_catch.init(&ls.local);
defer try_catch.deinit();
ls.local.eval(script.source, null) catch |err| {
const caught = try_catch.caughtOrError(arena, err);
log.warn(.cdp, "script on new doc", .{ .caught = caught });
};
}
}
// frameNavigated event // frameNavigated event
try cdp.sendEvent("Page.frameNavigated", .{ try cdp.sendEvent("Page.frameNavigated", .{
.type = "Navigation", .type = "Navigation",
@@ -554,18 +509,22 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
// chromedp client expects to receive the events is this order. // chromedp client expects to receive the events is this order.
// see https://github.com/chromedp/chromedp/issues/1558 // see https://github.com/chromedp/chromedp/issues/1558
try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id }); try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id });
}
pub fn pageDOMContentLoaded(bc: anytype, event: *const Notification.PageDOMContentLoaded) !void {
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
var cdp = bc.cdp;
// domContentEventFired event
// TODO: partially hard coded
try cdp.sendEvent( try cdp.sendEvent(
"Page.domContentEventFired", "Page.domContentEventFired",
.{ .timestamp = timestamp }, .{ .timestamp = timestamp },
.{ .session_id = session_id }, .{ .session_id = session_id },
); );
// lifecycle DOMContentLoaded event
// TODO: partially hard coded
if (bc.page_life_cycle_events) { if (bc.page_life_cycle_events) {
const frame_id = &id.toFrameId(event.frame_id);
const loader_id = &id.toLoaderId(event.req_id);
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.timestamp = timestamp, .timestamp = timestamp,
.name = "DOMContentLoaded", .name = "DOMContentLoaded",
@@ -573,16 +532,23 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
.loaderId = loader_id, .loaderId = loader_id,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
}
pub fn pageLoaded(bc: anytype, event: *const Notification.PageLoaded) !void {
const session_id = bc.session_id orelse return;
const timestamp = event.timestamp;
var cdp = bc.cdp;
const frame_id = &id.toFrameId(event.frame_id);
// loadEventFired event
try cdp.sendEvent( try cdp.sendEvent(
"Page.loadEventFired", "Page.loadEventFired",
.{ .timestamp = timestamp }, .{ .timestamp = timestamp },
.{ .session_id = session_id }, .{ .session_id = session_id },
); );
// lifecycle DOMContentLoaded event
if (bc.page_life_cycle_events) { if (bc.page_life_cycle_events) {
const loader_id = &id.toLoaderId(event.req_id);
try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{
.timestamp = timestamp, .timestamp = timestamp,
.name = "load", .name = "load",
@@ -591,7 +557,6 @@ pub fn pageNavigated(arena: Allocator, bc: anytype, event: *const Notification.P
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
} }
// frameStoppedLoading
return cdp.sendEvent("Page.frameStoppedLoading", .{ return cdp.sendEvent("Page.frameStoppedLoading", .{
.frameId = frame_id, .frameId = frame_id,
}, .{ .session_id = session_id }); }, .{ .session_id = session_id });
@@ -896,55 +861,3 @@ test "cdp.page: reload" {
try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } }); try ctx.processMessage(.{ .id = 32, .method = "Page.reload", .params = .{ .ignoreCache = true } });
} }
} }
test "cdp.page: addScriptToEvaluateOnNewDocument" {
var ctx = try testing.context();
defer ctx.deinit();
var bc = try ctx.loadBrowserContext(.{ .id = "BID-9", .url = "hi.html", .target_id = "FID-000000000X".* });
{
// Register a script — should return unique identifier "1"
try ctx.processMessage(.{ .id = 20, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test = 1" } });
try ctx.expectSentResult(.{
.identifier = "1",
}, .{ .id = 20 });
}
{
// Register another script — should return identifier "2"
try ctx.processMessage(.{ .id = 21, .method = "Page.addScriptToEvaluateOnNewDocument", .params = .{ .source = "window.__test2 = 2" } });
try ctx.expectSentResult(.{
.identifier = "2",
}, .{ .id = 21 });
}
{
// Remove the first script — should succeed
try ctx.processMessage(.{ .id = 22, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "1" } });
try ctx.expectSentResult(null, .{ .id = 22 });
}
{
// Remove a non-existent identifier — should succeed silently
try ctx.processMessage(.{ .id = 23, .method = "Page.removeScriptToEvaluateOnNewDocument", .params = .{ .identifier = "999" } });
try ctx.expectSentResult(null, .{ .id = 23 });
}
{
try ctx.processMessage(.{ .id = 34, .method = "Page.reload" });
// wait for this event, which is sent after we've run the registered scripts
try ctx.expectSentEvent("Page.frameNavigated", .{
.frame = .{ .loaderId = "LID-0000000002" },
}, .{});
const page = bc.session.currentPage() orelse unreachable;
var ls: js.Local.Scope = undefined;
page.js.localScope(&ls);
defer ls.deinit();
const test_val = try ls.local.exec("window.__test2", null);
try testing.expectEqual(2, try test_val.toI32());
}
}

View File

@@ -168,26 +168,13 @@ const TestContext = struct {
index: ?usize = null, index: ?usize = null,
}; };
pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void { pub fn expectSent(self: *TestContext, expected: anytype, opts: SentOpts) !void {
const expected_json = blk: { const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
// Zig makes this hard. When sendJSON is called, we're sending an anytype. .whitespace = .indent_2,
// We can't record that in an ArrayList(???), so we serialize it to JSON. .emit_null_optional_fields = false,
// Now, ideally, we could just take our expected structure, serialize it to });
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
const serialized = try json.Stringify.valueAlloc(base.arena_allocator, expected, .{
.whitespace = .indent_2,
.emit_null_optional_fields = false,
});
break :blk try std.json.parseFromSliceLeaky(json.Value, base.arena_allocator, serialized, .{});
};
for (0..5) |_| { for (0..5) |_| {
for (self.received.items, 0..) |received, i| { for (self.received.items, 0..) |received, i| {
if (try base.isEqualJson(expected_json, received) == false) { if (try compareExpectedToSent(serialized, received) == false) {
continue; continue;
} }
@@ -200,15 +187,6 @@ const TestContext = struct {
} }
return; return;
} }
if (self.cdp_) |*cdp__| {
if (cdp__.browser_context) |*bc| {
if (bc.session.page != null) {
var runner = try bc.session.runner(.{});
_ = try runner.tick(.{ .ms = 1000 });
}
}
}
std.Thread.sleep(5 * std.time.ns_per_ms); std.Thread.sleep(5 * std.time.ns_per_ms);
try self.read(); try self.read();
} }
@@ -321,3 +299,17 @@ pub fn context() !TestContext {
.socket = pair[0], .socket = pair[0],
}; };
} }
// Zig makes this hard. When sendJSON is called, we're sending an anytype.
// We can't record that in an ArrayList(???), so we serialize it to JSON.
// Now, ideally, we could just take our expected structure, serialize it to
// json and check if the two are equal.
// Except serializing to JSON isn't deterministic.
// So we serialize the JSON then we deserialize to json.Value. And then we can
// compare our anytype expectation with the json.Value that we captured
fn compareExpectedToSent(expected: []const u8, actual: json.Value) !bool {
const expected_value = try std.json.parseFromSlice(json.Value, std.testing.allocator, expected, .{});
defer expected_value.deinit();
return base.isEqualJson(expected_value.value, actual);
}

View File

@@ -209,37 +209,6 @@ noinline fn assertionFailure(comptime ctx: []const u8, args: anytype) noreturn {
@import("crash_handler.zig").crash(ctx, args, @returnAddress()); @import("crash_handler.zig").crash(ctx, args, @returnAddress());
} }
// Reference counting helper
pub fn RC(comptime T: type) type {
return struct {
_refs: T = 0,
pub fn init(refs: T) @This() {
return .{ ._refs = refs };
}
pub fn acquire(self: *@This()) void {
self._refs += 1;
}
pub fn release(self: *@This(), value: anytype, session: *Session) void {
if (comptime IS_DEBUG) {
std.debug.assert(self._refs > 0);
}
const refs = self._refs - 1;
self._refs = refs;
if (refs > 0) {
return;
}
value.deinit(session);
if (session.finalizer_callbacks.fetchRemove(@intFromPtr(value))) |kv| {
session.releaseArena(kv.value.arena);
}
}
};
}
test { test {
std.testing.refAllDecls(@This()); std.testing.refAllDecls(@This());
} }

View File

@@ -1,7 +1,6 @@
const std = @import("std"); const std = @import("std");
pub const protocol = @import("mcp/protocol.zig"); pub const protocol = @import("mcp/protocol.zig");
pub const Version = protocol.Version;
pub const router = @import("mcp/router.zig"); pub const router = @import("mcp/router.zig");
pub const Server = @import("mcp/Server.zig"); pub const Server = @import("mcp/Server.zig");

View File

@@ -114,7 +114,7 @@ test "MCP.Server - Integration: synchronous smoke test" {
try router.processRequests(server, &in_reader); try router.processRequests(server, &in_reader);
try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1, .result = .{ .protocolVersion = "2024-11-05" } }, out_alloc.writer.buffered()); try testing.expectJson(.{ .jsonrpc = "2.0", .id = 1 }, out_alloc.writer.buffered());
} }
test "MCP.Server - Integration: ping request returns an empty result" { test "MCP.Server - Integration: ping request returns an empty result" {

View File

@@ -1,14 +1,5 @@
const std = @import("std"); const std = @import("std");
pub const Version = enum {
@"2024-11-05",
@"2025-03-26",
@"2025-06-18",
@"2025-11-25",
pub const default: Version = .@"2024-11-05";
};
pub const Request = struct { pub const Request = struct {
jsonrpc: []const u8 = "2.0", jsonrpc: []const u8 = "2.0",
id: ?std.json.Value = null, id: ?std.json.Value = null,

View File

@@ -81,12 +81,8 @@ pub fn handleMessage(server: *Server, arena: std.mem.Allocator, msg: []const u8)
fn handleInitialize(server: *Server, req: protocol.Request) !void { fn handleInitialize(server: *Server, req: protocol.Request) !void {
const id = req.id orelse return; const id = req.id orelse return;
const version: protocol.Version = switch (server.app.config.mode) { const result = protocol.InitializeResult{
.mcp => |opts| opts.version, .protocolVersion = "2025-11-25",
else => .default,
};
const result: protocol.InitializeResult = .{
.protocolVersion = @tagName(version),
.capabilities = .{ .capabilities = .{
.resources = .{}, .resources = .{},
.tools = .{}, .tools = .{},
@@ -125,7 +121,7 @@ test "MCP.router - handleMessage - synchronous unit tests" {
\\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}} \\{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"test-client","version":"1.0.0"}}}
); );
try testing.expectJson( try testing.expectJson(
\\{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2024-11-05", "capabilities": { "tools": {} } } } \\{ "jsonrpc": "2.0", "id": 1, "result": { "capabilities": { "tools": {} } } }
, out_alloc.writer.buffered()); , out_alloc.writer.buffered());
out_alloc.writer.end = 0; out_alloc.writer.end = 0;