Skip to content

Commit f034065

Browse files
Merge pull request #520 from lightpanda-io/navigate_notifications
Communicate page navigation state via notifications
2 parents 64bd4de + 3fc7ffa commit f034065

File tree

6 files changed

+173
-101
lines changed

6 files changed

+173
-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+
notify_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,
145162
.env = undefined,
146163
.browser = browser,
164+
.notify_ctx = any_ctx,
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.notify_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, .timestamp = 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, .timestamp = 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)