Skip to content

Commit 71c3d48

Browse files
committed
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.
1 parent 66bac32 commit 71c3d48

File tree

6 files changed

+165
-101
lines changed

6 files changed

+165
-101
lines changed

src/browser/browser.zig

Lines changed: 51 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ const Walker = @import("../dom/walker.zig").WalkerDepthFirst;
4040

4141
const URL = @import("../url.zig").URL;
4242
const storage = @import("../storage/storage.zig");
43+
const Notification = @import("../notification.zig").Notification;
4344

4445
const http = @import("../http/client.zig");
4546
const UserContext = @import("../user_context.zig").UserContext;
@@ -137,14 +138,32 @@ pub const Session = struct {
137138

138139
jstypes: [Types.len]usize = undefined,
139140

141+
// recipient of notification, passed as the first parameter to notify
142+
ctx: *anyopaque,
143+
notify_func: *const fn (ctx: *anyopaque, notification: *const Notification) anyerror!void,
144+
140145
fn init(self: *Session, browser: *Browser, ctx: anytype) !void {
146+
const ContextT = @TypeOf(ctx);
147+
const ContextStruct = switch (@typeInfo(ContextT)) {
148+
.@"struct" => ContextT,
149+
.pointer => |ptr| ptr.child,
150+
.void => NoopContext,
151+
else => @compileError("invalid context type"),
152+
};
153+
154+
// ctx can be void, to be able to store it in our *anyopaque field, we
155+
// need to play a little game.
156+
const any_ctx: *anyopaque = if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx;
157+
141158
const app = browser.app;
142159
const allocator = app.allocator;
143160
self.* = .{
144161
.app = app,
162+
.ctx = any_ctx,
145163
.env = undefined,
146164
.browser = browser,
147165
.inspector = undefined,
166+
.notify_func = ContextStruct.notify,
148167
.http_client = browser.http_client,
149168
.storage_shed = storage.Shed.init(allocator),
150169
.arena = std.heap.ArenaAllocator.init(allocator),
@@ -157,24 +176,15 @@ pub const Session = struct {
157176
errdefer self.env.deinit();
158177
try self.env.load(&self.jstypes);
159178

160-
const ContextT = @TypeOf(ctx);
161-
const InspectorContainer = switch (@typeInfo(ContextT)) {
162-
.@"struct" => ContextT,
163-
.pointer => |ptr| ptr.child,
164-
.void => NoopInspector,
165-
else => @compileError("invalid context type"),
166-
};
167-
168179
// const ctx_opaque = @as(*anyopaque, @ptrCast(ctx));
169180
self.inspector = try jsruntime.Inspector.init(
170181
arena,
171182
&self.env,
172-
if (@TypeOf(ctx) == void) @constCast(@ptrCast(&{})) else ctx,
173-
InspectorContainer.onInspectorResponse,
174-
InspectorContainer.onInspectorEvent,
183+
any_ctx,
184+
ContextStruct.onInspectorResponse,
185+
ContextStruct.onInspectorEvent,
175186
);
176187
self.env.setInspector(self.inspector);
177-
178188
try self.env.setModuleLoadFn(self, Session.fetchModule);
179189
}
180190

@@ -269,6 +279,12 @@ pub const Session = struct {
269279
log.debug("inspector context created", .{});
270280
self.inspector.contextCreated(&self.env, "", (page.origin() catch "://") orelse "://", aux_data);
271281
}
282+
283+
fn notify(self: *const Session, notification: *const Notification) void {
284+
self.notify_func(self.ctx, notification) catch |err| {
285+
log.err("notify {}: {}", .{ std.meta.activeTag(notification.*), err });
286+
};
287+
}
272288
};
273289

274290
// Page navigates to an url.
@@ -344,37 +360,41 @@ pub const Page = struct {
344360
// spec reference: https://html.spec.whatwg.org/#document-lifecycle
345361
// - aux_data: extra data forwarded to the Inspector
346362
// see Inspector.contextCreated
347-
pub fn navigate(self: *Page, url_string: []const u8, aux_data: ?[]const u8) !void {
363+
pub fn navigate(self: *Page, request_url: URL, aux_data: ?[]const u8) !void {
348364
const arena = self.arena;
365+
const session = self.session;
349366

350-
log.debug("starting GET {s}", .{url_string});
367+
log.debug("starting GET {s}", .{request_url});
351368

352369
// if the url is about:blank, nothing to do.
353-
if (std.mem.eql(u8, "about:blank", url_string)) {
370+
if (std.mem.eql(u8, "about:blank", request_url.raw)) {
354371
return;
355372
}
356373

357-
// we don't clone url_string, because we're going to replace self.url
374+
// we don't clone url, because we're going to replace self.url
358375
// later in this function, with the final request url (since we might
359376
// redirect)
360-
self.url = try URL.parse(url_string, "https");
361-
self.session.app.telemetry.record(.{ .navigate = .{
377+
self.url = request_url;
378+
var url = &self.url.?;
379+
380+
session.app.telemetry.record(.{ .navigate = .{
362381
.proxy = false,
363-
.tls = std.ascii.eqlIgnoreCase(self.url.?.scheme(), "https"),
382+
.tls = std.ascii.eqlIgnoreCase(url.scheme(), "https"),
364383
} });
365384

366385
// load the data
367-
var request = try self.newHTTPRequest(.GET, &self.url.?, .{ .navigation = true });
386+
var request = try self.newHTTPRequest(.GET, url, .{ .navigation = true });
368387
defer request.deinit();
369388

389+
session.notify(&.{ .page_navigate = .{ .url = url, .ts = timestamp() } });
370390
var response = try request.sendSync(.{});
371391

372392
// would be different than self.url in the case of a redirect
373393
self.url = try URL.fromURI(arena, request.uri);
394+
url = &self.url.?;
374395

375-
const url = &self.url.?;
376396
const header = response.header;
377-
try self.session.cookie_jar.populateFromResponse(&url.uri, &header);
397+
try session.cookie_jar.populateFromResponse(&url.uri, &header);
378398

379399
// TODO handle fragment in url.
380400
try self.session.window.replaceLocation(.{ .url = try url.toWebApi(arena) });
@@ -407,6 +427,8 @@ pub const Page = struct {
407427
// save the body into the page.
408428
self.raw_data = arr.items;
409429
}
430+
431+
session.notify(&.{ .page_navigated = .{ .url = url, .ts = timestamp() } });
410432
}
411433

412434
pub const ClickResult = union(enum) {
@@ -835,7 +857,13 @@ const FlatRenderer = struct {
835857
}
836858
};
837859

838-
const NoopInspector = struct {
860+
const NoopContext = struct {
839861
pub fn onInspectorResponse(_: *anyopaque, _: u32, _: []const u8) void {}
840862
pub fn onInspectorEvent(_: *anyopaque, _: []const u8) void {}
863+
pub fn notify(_: *anyopaque, _: *const Notification) !void {}
841864
};
865+
866+
fn timestamp() u32 {
867+
const ts = std.posix.clock_gettime(std.posix.CLOCK.MONOTONIC) catch unreachable;
868+
return @intCast(ts.sec);
869+
}

src/cdp/cdp.zig

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ const json = std.json;
2323
const App = @import("../app.zig").App;
2424
const asUint = @import("../str/parser.zig").asUint;
2525
const Incrementing = @import("../id.zig").Incrementing;
26+
const Notification = @import("../notification.zig").Notification;
2627

2728
const log = std.log.scoped(.cdp);
2829

@@ -248,6 +249,17 @@ pub fn CDPT(comptime TypeProvider: type) type {
248249
return true;
249250
}
250251

252+
const SendEventOpts = struct {
253+
session_id: ?[]const u8 = null,
254+
};
255+
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void {
256+
return self.sendJSON(.{
257+
.method = method,
258+
.params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p,
259+
.sessionId = opts.session_id,
260+
});
261+
}
262+
251263
fn sendJSON(self: *Self, message: anytype) !void {
252264
return self.client.sendJSON(message, .{
253265
.emit_null_optional_fields = false,
@@ -338,6 +350,15 @@ pub fn BrowserContext(comptime CDP_T: type) type {
338350
return if (page.url) |*url| url.raw else null;
339351
}
340352

353+
pub fn notify(ctx: *anyopaque, notification: *const Notification) !void {
354+
const self: *Self = @alignCast(@ptrCast(ctx));
355+
356+
switch (notification.*) {
357+
.page_navigate => |*pn| return @import("domains/page.zig").pageNavigate(self, pn),
358+
.page_navigated => |*pn| return @import("domains/page.zig").pageNavigated(self, pn),
359+
}
360+
}
361+
341362
pub fn onInspectorResponse(ctx: *anyopaque, _: u32, msg: []const u8) void {
342363
if (std.log.defaultLogEnabled(.debug)) {
343364
// msg should be {"id":<id>,...
@@ -472,13 +493,9 @@ pub fn Command(comptime CDP_T: type, comptime Sender: type) type {
472493
const SendEventOpts = struct {
473494
session_id: ?[]const u8 = null,
474495
};
475-
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: SendEventOpts) !void {
496+
pub fn sendEvent(self: *Self, method: []const u8, p: anytype, opts: CDP_T.SendEventOpts) !void {
476497
// Events ALWAYS go to the client. self.sender should not be used
477-
return self.cdp.sendJSON(.{
478-
.method = method,
479-
.params = if (comptime @typeInfo(@TypeOf(p)) == .null) struct {}{} else p,
480-
.sessionId = opts.session_id,
481-
});
498+
return self.cdp.sendEvent(method, p, opts);
482499
}
483500

484501
pub fn sendError(self: *Self, code: i32, message: []const u8) !void {

0 commit comments

Comments
 (0)