chore: Move ownership of the Inbox from HttpClient to WebSocker Driver

Currently, the HttpClient owns the inbox and its borrowed by the Link. This is
a bit backwards, but it also means that we can't eagerly create a Link: the
Link needs the inbox, so it needs the HttpClient, which is created by the
Browser (which creates an Isolate).

Remember, the Inbox is one of the few things shared between the main thread
and the worker, so either end can own it and the other can borrow it.

This switches the ownership so that the HttpClient now borrows the Inbox from
the Server's side of the Link (the WebSocket).

The main goal of this change is to prepare for more advanced HTTP WebDriver
flows. The more we can create _without_ a Browser, the fewer edge cases we have
to deal with (Browser because it's expensive and has to be created on the
Worker thread due to how V8::Isolate works).
This commit is contained in:
Karl Seguin committed 2026-09-03 16:51:12 +08:00
1 parent cb26e05533
commit 87320a506d
16 files changed
+111 -96

No files matched your search

+8 -12
View File
@@ -30,6 +30,7 @@ const BiDi = @import("bidi/BiDi.zig");
const WS = @import("WS.zig");
const http = @import("http.zig");
const Driver = @import("Driver.zig");
const Inbox = @import("../Inbox.zig");
const log = lp.log;
const posix = std.posix;
@@ -60,6 +61,8 @@ const WebSocket = struct {
// threads `websockets` while live, the pool's free list otherwise
node: DoublyLinkedList.Node,
protocol: Driver.Protocol,
// The worker's mailbox, which is how the main loop communicates with the worker.
inbox: Inbox = .{},
// null until the worker attaches
driver: ?Driver = null,
// whether or not socket is in the poll set. Makes sure we don't double-remove
@@ -582,6 +585,7 @@ fn releaseWorker(self: *Server, ws: *WebSocket, notify: *std.Io.Event) void {
// or upgradeConnection when there never was a worker.
fn releaseWebSocket(self: *Server, ws: *WebSocket) void {
self.websockets.remove(&ws.node);
ws.inbox.deinit();
lp.metrics.serve_active_connections.decr(ws.protocol);
self.websocket_pool.release(ws);
self.slotFreed();
@@ -672,31 +676,23 @@ const Worker = struct {
.cdp => {
const cdp = try allocator.create(CDP);
defer allocator.destroy(cdp);
try cdp.init(server.app, ws.socket);
try cdp.init(server.app, ws.socket, &ws.inbox);
defer cdp.deinit();
Worker.run(server, ws, .init(.{ .cdp = cdp }));
Worker.run(server, ws, .init(.{ .cdp = cdp }, &ws.inbox));
},
.bidi => {
const bidi = try allocator.create(BiDi);
defer allocator.destroy(bidi);
try bidi.init(server.app, ws.socket, session_id);
try bidi.init(server.app, ws.socket, &ws.inbox, session_id);
defer bidi.deinit();
Worker.run(server, ws, .init(.{ .bidi = bidi }));
Worker.run(server, ws, .init(.{ .bidi = bidi }, &ws.inbox));
},
}
}
fn run(server: *Server, ws: *WebSocket, driver: Driver) void {
// Gates HttpClient's block in curl_multi_poll: false (tests, before
// the attach) means "nobody will wake us, don't sleep on it". From
// here the loop is about to feed our inbox and wake us, so the
// worker parks in poll instead of spinning through tick().
driver.browser.http_client.driver_link_active = true;
Worker.notifyLoopOfChange(server, .{ .ws = ws, .op = .{ .attach = driver } });
driver.run();
// The loop is done with us once releaseConnection returns; the
// driver's deinit may still tick the client, without a producer.
defer driver.browser.http_client.driver_link_active = false;
// Release first: until the loop has let go of this websocket it can
// still drop the link, and onLinkDisconnect requests a terminate. Doing
// it the other way round left that request landing after the cancel,