From 71c3d484a9765ba3e57db0b5b7f25bcd109a53dc Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 10 Apr 2025 14:21:05 +0800 Subject: [PATCH 1/3] Communicate page navigation state via notifications In order to support click handling on anchors from JavaScript, we need some hook from the page/session to the CDP instance. This first phase adds notifications in page.navigate, as well as a primitive notification hook to the session. CDP's existing Page.navigate uses this new notifiation system. --- src/browser/browser.zig | 74 +++++++++++++------ src/cdp/cdp.zig | 29 ++++++-- src/cdp/domains/page.zig | 149 ++++++++++++++++++++------------------- src/cdp/testing.zig | 2 +- src/main.zig | 7 +- src/notification.zig | 11 +++ 6 files changed, 168 insertions(+), 104 deletions(-) create mode 100644 src/notification.zig diff --git a/src/browser/browser.zig b/src/browser/browser.zig index 718a360c..fed991bc 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -40,6 +40,7 @@ const Walker = @import("../dom/walker.zig").WalkerDepthFirst; const URL = @import("../url.zig").URL; const storage = @import("../storage/storage.zig"); +const Notification = @import("../notification.zig").Notification; const http = @import("../http/client.zig"); const UserContext = @import("../user_context.zig").UserContext; @@ -137,14 +138,32 @@ pub const Session = struct { jstypes: [Types.len]usize = undefined, + // recipient of notification, passed as the first parameter to notify + ctx: *anyopaque, + notify_func: *const fn (ctx: *anyopaque, notification: *const Notification) anyerror!void, + fn init(self: *Session, browser: *Browser, ctx: anytype) !void { + const ContextT = @TypeOf(ctx); + const ContextStruct = switch (@typeInfo(ContextT)) { + .@"struct" => ContextT, + .pointer => |ptr| ptr.child, + .void => NoopContext, + else => @compileError("invalid context type"), + }; + + // ctx can be void, to be able to store it in our *anyopaque field, we + // need to play a little game. + const any_ctx: *anyopaque = if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx; + const app = browser.app; const allocator = app.allocator; self.* = .{ .app = app, + .ctx = any_ctx, .env = undefined, .browser = browser, .inspector = undefined, + .notify_func = ContextStruct.notify, .http_client = browser.http_client, .storage_shed = storage.Shed.init(allocator), .arena = std.heap.ArenaAllocator.init(allocator), @@ -157,24 +176,15 @@ pub const Session = struct { errdefer self.env.deinit(); try self.env.load(&self.jstypes); - const ContextT = @TypeOf(ctx); - const InspectorContainer = switch (@typeInfo(ContextT)) { - .@"struct" => ContextT, - .pointer => |ptr| ptr.child, - .void => NoopInspector, - else => @compileError("invalid context type"), - }; - // const ctx_opaque = @as(*anyopaque, @ptrCast(ctx)); self.inspector = try jsruntime.Inspector.init( arena, &self.env, - if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx, - InspectorContainer.onInspectorResponse, - InspectorContainer.onInspectorEvent, + any_ctx, + ContextStruct.onInspectorResponse, + ContextStruct.onInspectorEvent, ); self.env.setInspector(self.inspector); - try self.env.setModuleLoadFn(self, Session.fetchModule); } @@ -269,6 +279,12 @@ pub const Session = struct { log.debug("inspector context created", .{}); self.inspector.contextCreated(&self.env, "", (page.origin() catch "://") orelse "://", aux_data); } + + fn notify(self: *const Session, notification: *const Notification) void { + self.notify_func(self.ctx, notification) catch |err| { + log.err("notify {}: {}", .{ std.meta.activeTag(notification.*), err }); + }; + } }; // Page navigates to an url. @@ -344,37 +360,41 @@ pub const Page = struct { // spec reference: https://html.spec.whatwg.org/#document-lifecycle // - aux_data: extra data forwarded to the Inspector // see Inspector.contextCreated - pub fn navigate(self: *Page, url_string: []const u8, aux_data: ?[]const u8) !void { + pub fn navigate(self: *Page, request_url: URL, aux_data: ?[]const u8) !void { const arena = self.arena; + const session = self.session; - log.debug("starting GET {s}", .{url_string}); + log.debug("starting GET {s}", .{request_url}); // if the url is about:blank, nothing to do. - if (std.mem.eql(u8, "about:blank", url_string)) { + if (std.mem.eql(u8, "about:blank", request_url.raw)) { return; } - // we don't clone url_string, because we're going to replace self.url + // we don't clone url, because we're going to replace self.url // later in this function, with the final request url (since we might // redirect) - self.url = try URL.parse(url_string, "https"); - self.session.app.telemetry.record(.{ .navigate = .{ + self.url = request_url; + var url = &self.url.?; + + session.app.telemetry.record(.{ .navigate = .{ .proxy = false, - .tls = std.ascii.eqlIgnoreCase(self.url.?.scheme(), "https"), + .tls = std.ascii.eqlIgnoreCase(url.scheme(), "https"), } }); // load the data - var request = try self.newHTTPRequest(.GET, &self.url.?, .{ .navigation = true }); + var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true }); defer request.deinit(); + session.notify(&.{ .page_navigate = .{ .url = url, .ts = timestamp() } }); var response = try request.sendSync(.{}); // would be different than self.url in the case of a redirect self.url = try URL.fromURI(arena, request.uri); + url = &self.url.?; - const url = &self.url.?; const header = response.header; - try self.session.cookie_jar.populateFromResponse(&url.uri, &header); + try session.cookie_jar.populateFromResponse(&url.uri, &header); // TODO handle fragment in url. try self.session.window.replaceLocation(.{ .url = try url.toWebApi(arena) }); @@ -407,6 +427,8 @@ pub const Page = struct { // save the body into the page. self.raw_data = arr.items; } + + session.notify(&.{ .page_navigated = .{ .url = url, .ts = timestamp() } }); } pub const ClickResult = union(enum) { @@ -835,7 +857,13 @@ const FlatRenderer = struct { } }; -const NoopInspector = struct { +const NoopContext = struct { pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {} pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {} + pub fn notify(_: *anyopaque, _: *const Notification) !void {} }; + +fn timestamp() u32 { + const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable; + return @intCast(ts.sec); +} diff --git a/src/cdp/cdp.zig b/src/cdp/cdp.zig index 2e609075..ff00b5e9 100644 --- a/src/cdp/cdp.zig +++ b/src/cdp/cdp.zig @@ -23,6 +23,7 @@ const json = std.json; const App = @import("../app.zig").App; const asUint = @import("../str/parser.zig").asUint; const Incrementing = @import("../id.zig").Incrementing; +const Notification = @import("../notification.zig").Notification; const log = std.log.scoped(.cdp); @@ -248,6 +249,17 @@ pub fn CDPT(comptime TypeProvider: type) type { return true; } + const SendEventOpts = struct { + session_id: ?[]const u8 = null, + }; + pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void { + return self.sendJSON(.{ + .method = method, + .params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p, + .sessionId = opts.session_id, + }); + } + fn sendJSON(self: *Self, message: anytype) !void { return self.client.sendJSON(message, .{ .emit_null_optional_fields = false, @@ -338,6 +350,15 @@ pub fn BrowserContext(comptime CDP_T: type) type { return if (page.url) |*url| url.raw else null; } + pub fn notify(ctx: *anyopaque, notification: *const Notification) !void { + const self: *Self = @alignCast(@ptrCast(ctx)); + + switch (notification.*) { + .page_navigate => |*pn| return @import("domains/page.zig").pageNavigate(self, pn), + .page_navigated => |*pn| return @import("domains/page.zig").pageNavigated(self, pn), + } + } + pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void { if (std.log.defaultLogEnabled(.debug)) { // msg should be {"id":,... @@ -472,13 +493,9 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type { const SendEventOpts = struct { session_id: ?[]const u8 = null, }; - pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void { + pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: CDP_T.SendEventOpts) !void { // Events ALWAYS go to the client. self.sender should not be used - return self.cdp.sendJSON(.{ - .method = method, - .params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p, - .sessionId = opts.session_id, - }); + return self.cdp.sendEvent(method, p, opts); } pub fn sendError(self: *Self, code: i32, message: []const u8) !void { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 02134cd2..cd092437 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -18,6 +18,8 @@ const std = @import("std"); const runtime = @import("runtime.zig"); +const URL = @import("../../url.zig").URL; +const Notification = @import("../../notification.zig").Notification; pub fn processMessage(cmd: anytype) !void { const action = std.meta.stringToEnum(enum { @@ -137,62 +139,17 @@ fn navigate(cmd: anytype) !void { // referrerPolicy: ?[]const u8 = null, // TODO: enum })) orelse return error.InvalidParams; - return navigateToUrl(cmd, params.url, true); -} - -pub fn navigateToUrl(cmd: anytype, url: []const u8, send_result: bool) !void { const bc = cmd.browser_context orelse return error.BrowserContextNotLoaded; // didn't create? const target_id = bc.target_id orelse return error.TargetIdNotLoaded; // didn't attach? - const session_id = bc.session_id orelse return error.SessionIdNotLoaded; - - // if we have a target_id we have to have a page; - std.debug.assert(bc.session.page != null); - - // change state - bc.reset(); - bc.loader_id = cmd.cdp.loader_id_gen.next(); - - const LifecycleEvent = struct { - frameId: []const u8, - loaderId: ?[]const u8, - name: []const u8, - timestamp: f32, - }; - - var life_event = LifecycleEvent{ - .frameId = target_id, - .loaderId = bc.loader_id, - .name = "init", - .timestamp = 343721.796037, - }; - - // frameStartedLoading event - // TODO: event partially hard coded - try cmd.sendEvent("Page.frameStartedLoading", .{ - .frameId = target_id, - }, .{ .session_id = session_id }); - - if (bc.page_life_cycle_events) { - try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); + if (bc.session_id == null) { + return error.SessionIdNotLoaded; } - // output - if (send_result) { - try cmd.sendResult(.{ - .frameId = target_id, - .loaderId = bc.loader_id, - }, .{}); - } - - // TODO: at this point do we need async the following actions to be async? - - // Send Runtime.executionContextsCleared event - // TODO: noop event, we have no env context at this point, is it necesarry? - try cmd.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id }); + const url = try URL.parse(params.url, "https"); const aux_data = try std.fmt.allocPrint( cmd.arena, @@ -204,68 +161,118 @@ pub fn navigateToUrl(cmd: anytype, url: []const u8, send_result: bool) !void { var page = bc.session.currentPage().?; try page.navigate(url, aux_data); - // Events + bc.loader_id = bc.cdp.loader_id_gen.next(); + try cmd.sendResult(.{ + .frameId = target_id, + .loaderId = bc.loader_id, + }, .{}); +} + +pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void { + // I don't think it's possible that we get these notifications and don't + // have these things setup. + std.debug.assert(bc.session.page != null); + + var cdp = bc.cdp; + const loader_id = bc.loader_id; + const target_id = bc.target_id orelse unreachable; + const session_id = bc.session_id orelse unreachable; + + bc.reset(); + + // frameStartedLoading event + try cdp.sendEvent("Page.frameStartedLoading", .{ + .frameId = target_id, + }, .{ .session_id = session_id }); - // lifecycle init event - // TODO: partially hard coded if (bc.page_life_cycle_events) { - life_event.name = "init"; - life_event.timestamp = 343721.796037; - try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); + try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ + .name = "init", + .frameId = target_id, + .loaderId = loader_id, + .timestamp = event.ts, + }, .{ .session_id = session_id }); } - try cmd.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id }); + // Send Runtime.executionContextsCleared event + // TODO: noop event, we have no env context at this point, is it necesarry? + try cdp.sendEvent("Runtime.executionContextsCleared", null, .{ .session_id = session_id }); +} + +pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void { + // I don't think it's possible that we get these notifications and don't + // have these things setup. + std.debug.assert(bc.session.page != null); + + var cdp = bc.cdp; + const ts = event.ts; + const loader_id = bc.loader_id; + const target_id = bc.target_id orelse unreachable; + const session_id = bc.session_id orelse unreachable; + + try cdp.sendEvent("DOM.documentUpdated", null, .{ .session_id = session_id }); // frameNavigated event - try cmd.sendEvent("Page.frameNavigated", .{ + try cdp.sendEvent("Page.frameNavigated", .{ .type = "Navigation", .frame = Frame{ .id = target_id, - .url = url, + .url = event.url.raw, + .loaderId = bc.loader_id, .securityOrigin = bc.security_origin, .secureContextType = bc.secure_context_type, - .loaderId = bc.loader_id, }, }, .{ .session_id = session_id }); // domContentEventFired event // TODO: partially hard coded - try cmd.sendEvent( + try cdp.sendEvent( "Page.domContentEventFired", - .{ .timestamp = 343721.803338 }, + .{ .timestamp = ts }, .{ .session_id = session_id }, ); // lifecycle DOMContentLoaded event // TODO: partially hard coded if (bc.page_life_cycle_events) { - life_event.name = "DOMContentLoaded"; - life_event.timestamp = 343721.803338; - try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); + try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ + .timestamp = ts, + .name = "DOMContentLoaded", + .frameId = target_id, + .loaderId = loader_id, + }, .{ .session_id = session_id }); } // loadEventFired event - // TODO: partially hard coded - try cmd.sendEvent( + try cdp.sendEvent( "Page.loadEventFired", - .{ .timestamp = 343721.824655 }, + .{ .timestamp = ts }, .{ .session_id = session_id }, ); // lifecycle DOMContentLoaded event - // TODO: partially hard coded if (bc.page_life_cycle_events) { - life_event.name = "load"; - life_event.timestamp = 343721.824655; - try cmd.sendEvent("Page.lifecycleEvent", life_event, .{ .session_id = session_id }); + try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ + .timestamp = ts, + .name = "load", + .frameId = target_id, + .loaderId = loader_id, + }, .{ .session_id = session_id }); } // frameStoppedLoading - return cmd.sendEvent("Page.frameStoppedLoading", .{ + return cdp.sendEvent("Page.frameStoppedLoading", .{ .frameId = target_id, }, .{ .session_id = session_id }); } +const LifecycleEvent = struct { + frameId: []const u8, + loaderId: ?[]const u8, + name: []const u8, + timestamp: u32, +}; + const testing = @import("../testing.zig"); test "cdp.page: getFrameTree" { var ctx = testing.context(); diff --git a/src/cdp/testing.zig b/src/cdp/testing.zig index 3ab30c92..c2c833e3 100644 --- a/src/cdp/testing.zig +++ b/src/cdp/testing.zig @@ -161,7 +161,7 @@ const Page = struct { aux_data: []const u8 = "", doc: ?*parser.Document = null, - pub fn navigate(_: *Page, url: []const u8, aux_data: []const u8) !void { + pub fn navigate(_: *Page, url: URL, aux_data: []const u8) !void { _ = url; _ = aux_data; } diff --git a/src/main.zig b/src/main.zig index a1dd2a67..80b5a8f6 100644 --- a/src/main.zig +++ b/src/main.zig @@ -86,6 +86,7 @@ pub fn main() !void { }, .fetch => |opts| { log.debug("Fetch mode: url {s}, dump {any}", .{ opts.url, opts.dump }); + const url = try @import("url.zig").URL.parse(opts.url, null); var app = try App.init(alloc, .{ .run_mode = args.mode, @@ -107,13 +108,13 @@ pub fn main() !void { // page const page = try session.createPage(null); - _ = page.navigate(opts.url, null) catch |err| switch (err) { + _ = page.navigate(url, null) catch |err| switch (err) { error.UnsupportedUriScheme, error.UriMissingHost => { - log.err("'{s}' is not a valid URL ({any})\n", .{ opts.url, err }); + log.err("'{s}' is not a valid URL ({any})\n", .{ url, err }); return args.printUsageAndExit(false); }, else => { - log.err("'{s}' fetching error ({any})\n", .{ opts.url, err }); + log.err("'{s}' fetching error ({any})\n", .{ url, err }); return err; }, }; diff --git a/src/notification.zig b/src/notification.zig new file mode 100644 index 00000000..8f25ca62 --- /dev/null +++ b/src/notification.zig @@ -0,0 +1,11 @@ +const URL = @import("url.zig").URL; + +pub const Notification = union(enum) { + page_navigate: PageEvent, + page_navigated: PageEvent, + + pub const PageEvent = struct { + ts: u32, + url: *const URL, + }; +}; From 30fd35828669684fd8ff4857980b2c563dee6ad1 Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 10 Apr 2025 16:07:31 +0800 Subject: [PATCH 2/3] improve playwright pafe lifecycle message compatibility --- src/cdp/domains/page.zig | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index cd092437..0d928cb4 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -159,13 +159,13 @@ fn navigate(cmd: anytype) !void { ); var page = bc.session.currentPage().?; - try page.navigate(url, aux_data); - bc.loader_id = bc.cdp.loader_id_gen.next(); try cmd.sendResult(.{ .frameId = target_id, .loaderId = bc.loader_id, }, .{}); + + try page.navigate(url, aux_data); } pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void { @@ -180,6 +180,14 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void { bc.reset(); + // frameStartedNavigating event + try cdp.sendEvent("Page.frameStartedNavigating", .{ + .frameId = target_id, + .url = event.url.raw, + .loaderId = loader_id, + .navigationType = "differentDocument", + }, .{ .session_id = session_id }); + // frameStartedLoading event try cdp.sendEvent("Page.frameStartedLoading", .{ .frameId = target_id, From 3fc7ffadbf542817cff523517dab5f8e4c84d76e Mon Sep 17 00:00:00 2001 From: Karl Seguin Date: Thu, 10 Apr 2025 18:27:14 +0800 Subject: [PATCH 3/3] rename ts => timestamp, ctx => notify_ctx --- src/browser/browser.zig | 10 +++++----- src/cdp/domains/page.zig | 12 ++++++------ src/notification.zig | 2 +- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/browser/browser.zig b/src/browser/browser.zig index fed991bc..8181e350 100644 --- a/src/browser/browser.zig +++ b/src/browser/browser.zig @@ -139,7 +139,7 @@ pub const Session = struct { jstypes: [Types.len]usize = undefined, // recipient of notification, passed as the first parameter to notify - ctx: *anyopaque, + notify_ctx: *anyopaque, notify_func: *const fn (ctx: *anyopaque, notification: *const Notification) anyerror!void, fn init(self: *Session, browser: *Browser, ctx: anytype) !void { @@ -159,9 +159,9 @@ pub const Session = struct { const allocator = app.allocator; self.* = .{ .app = app, - .ctx = any_ctx, .env = undefined, .browser = browser, + .notify_ctx = any_ctx, .inspector = undefined, .notify_func = ContextStruct.notify, .http_client = browser.http_client, @@ -281,7 +281,7 @@ pub const Session = struct { } fn notify(self: *const Session, notification: *const Notification) void { - self.notify_func(self.ctx, notification) catch |err| { + self.notify_func(self.notify_ctx, notification) catch |err| { log.err("notify {}: {}", .{ std.meta.activeTag(notification.*), err }); }; } @@ -386,7 +386,7 @@ pub const Page = struct { var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true }); defer request.deinit(); - session.notify(&.{ .page_navigate = .{ .url = url, .ts = timestamp() } }); + session.notify(&.{ .page_navigate = .{ .url = url, .timestamp = timestamp() } }); var response = try request.sendSync(.{}); // would be different than self.url in the case of a redirect @@ -428,7 +428,7 @@ pub const Page = struct { self.raw_data = arr.items; } - session.notify(&.{ .page_navigated = .{ .url = url, .ts = timestamp() } }); + session.notify(&.{ .page_navigated = .{ .url = url, .timestamp = timestamp() } }); } pub const ClickResult = union(enum) { diff --git a/src/cdp/domains/page.zig b/src/cdp/domains/page.zig index 0d928cb4..1a7bc9bd 100644 --- a/src/cdp/domains/page.zig +++ b/src/cdp/domains/page.zig @@ -198,7 +198,7 @@ pub fn pageNavigate(bc: anytype, event: *const Notification.PageEvent) !void { .name = "init", .frameId = target_id, .loaderId = loader_id, - .timestamp = event.ts, + .timestamp = event.timestamp, }, .{ .session_id = session_id }); } @@ -213,7 +213,7 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void { std.debug.assert(bc.session.page != null); var cdp = bc.cdp; - const ts = event.ts; + const timestamp = event.timestamp; const loader_id = bc.loader_id; const target_id = bc.target_id orelse unreachable; const session_id = bc.session_id orelse unreachable; @@ -236,7 +236,7 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void { // TODO: partially hard coded try cdp.sendEvent( "Page.domContentEventFired", - .{ .timestamp = ts }, + .{ .timestamp = timestamp }, .{ .session_id = session_id }, ); @@ -244,7 +244,7 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void { // TODO: partially hard coded if (bc.page_life_cycle_events) { try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ - .timestamp = ts, + .timestamp = timestamp, .name = "DOMContentLoaded", .frameId = target_id, .loaderId = loader_id, @@ -254,14 +254,14 @@ pub fn pageNavigated(bc: anytype, event: *const Notification.PageEvent) !void { // loadEventFired event try cdp.sendEvent( "Page.loadEventFired", - .{ .timestamp = ts }, + .{ .timestamp = timestamp }, .{ .session_id = session_id }, ); // lifecycle DOMContentLoaded event if (bc.page_life_cycle_events) { try cdp.sendEvent("Page.lifecycleEvent", LifecycleEvent{ - .timestamp = ts, + .timestamp = timestamp, .name = "load", .frameId = target_id, .loaderId = loader_id, diff --git a/src/notification.zig b/src/notification.zig index 8f25ca62..28c5162e 100644 --- a/src/notification.zig +++ b/src/notification.zig @@ -5,7 +5,7 @@ pub const Notification = union(enum) { page_navigated: PageEvent, pub const PageEvent = struct { - ts: u32, + timestamp: u32, url: *const URL, }; };