Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
47 commits
Select commit Hold shift + click to select a range
b0fe5d6
Initial work on integrating libcurl and making all http nonblocking
karlseguin Jul 29, 2025
54ab132
Switch XHR to new http client
karlseguin Jul 30, 2025
254d22e
don't poll libcurl if we have no running transfers
karlseguin Jul 30, 2025
94e8964
add custom scheduler
karlseguin Jul 31, 2025
f65a39a
Re-enable telemetry
karlseguin Aug 1, 2025
3555680
Working navigation events (clicks, form submission)
karlseguin Aug 2, 2025
77475ca
Re-enable --insecure_disable_tls_host_verification
karlseguin Aug 2, 2025
4244b57
Improve page.wait
karlseguin Aug 2, 2025
dc83765
fix build
karlseguin Aug 2, 2025
3c0d027
dynamic script support
karlseguin Aug 2, 2025
f45726d
ScriptManager & HttpClient support for JS modules
karlseguin Aug 3, 2025
74b40b9
fix ScriptManager wrong order execution
karlseguin Aug 4, 2025
7831aab
connect proxy
karlseguin Aug 4, 2025
7f9e309
Shutdown clean async scripts
karlseguin Aug 4, 2025
32566cc
Set window location on load
karlseguin Aug 4, 2025
9876d79
Add Accept-Encoding
karlseguin Aug 5, 2025
c7484c6
Increase max concurrent request to 10
karlseguin Aug 5, 2025
ddb549c
cookie support
karlseguin Aug 5, 2025
cabd4fa
re-enable datauris
karlseguin Aug 5, 2025
06984ac
fix overflow and debug units
karlseguin Aug 5, 2025
1e612e4
Add command line options to control HTTP client
karlseguin Aug 6, 2025
c96fb3c
support CDP proxy override
karlseguin Aug 6, 2025
3554634
cleanup optional request headers
karlseguin Aug 6, 2025
332e264
remove unimportant todos
karlseguin Aug 6, 2025
ff742c0
don't allow concurrent blocking calls
karlseguin Aug 7, 2025
079ce5e
whitelist application/ld+json
karlseguin Aug 8, 2025
05192b6
update flake
mookums Aug 11, 2025
19c9080
Treat pending requests as active
karlseguin Aug 12, 2025
ea0bbaf
Revert "Treat pending requests as active"
karlseguin Aug 12, 2025
971524f
finalize document loading with non-HTML pages
krichprollsch Aug 12, 2025
bed3202
Merge pull request #939 from lightpanda-io/raw-done
karlseguin Aug 12, 2025
03694b5
3# This is a combination of 3 commits.
sjorsdonkers Aug 12, 2025
77eee7f
Cookies
sjorsdonkers Aug 12, 2025
a49154a
http_request_fail
sjorsdonkers Aug 12, 2025
2dc09c7
Merge pull request #930 from lightpanda-io/request_interception
karlseguin Aug 13, 2025
ca9e850
Create Client.Transfer earlier.
karlseguin Aug 13, 2025
5a3d5f5
improve elapsed display for larger numbers
karlseguin Aug 13, 2025
9bd8b2f
fix wpt runner
karlseguin Aug 13, 2025
3c8065f
fix fmt
karlseguin Aug 13, 2025
f6c68e4
fix release build (constness via telemetry, not seen in debug)
karlseguin Aug 13, 2025
c0106a2
http_headers_done_receiving
sjorsdonkers Aug 13, 2025
7d05712
setExtraHTTPHeaders
sjorsdonkers Aug 13, 2025
8d2d4ff
fix integer overflow for sleeping delay
krichprollsch Aug 13, 2025
35e2fa5
Merge pull request #943 from lightpanda-io/integer-overflow
karlseguin Aug 13, 2025
5100e06
fix header done callback
karlseguin Aug 14, 2025
96b10f4
Optimize Network.responseReceived
karlseguin Aug 14, 2025
1e095fe
zig fmt build.zig
krichprollsch Aug 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .gitmodules
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,15 @@
[submodule "vendor/mimalloc"]
path = vendor/mimalloc
url = https://github.com/microsoft/mimalloc.git/
[submodule "vendor/nghttp2"]
path = vendor/nghttp2
url = https://github.com/nghttp2/nghttp2.git
[submodule "vendor/mbedtls"]
path = vendor/mbedtls
url = https://github.com/Mbed-TLS/mbedtls.git
[submodule "vendor/zlib"]
path = vendor/zlib
url = https://github.com/madler/zlib.git
[submodule "vendor/curl"]
path = vendor/curl
url = https://github.com/curl/curl.git
597 changes: 580 additions & 17 deletions build.zig

Large diffs are not rendered by default.

4 changes: 0 additions & 4 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,6 @@
.version = "0.0.0",
.fingerprint = 0xda130f3af836cea0,
.dependencies = .{
.tls = .{
.url = "https://github.com/ianic/tls.zig/archive/55845f755d9e2e821458ea55693f85c737cd0c7a.tar.gz",
.hash = "tls-0.1.0-ER2e0m43BQAshi8ixj1qf3w2u2lqKtXtkrxUJ4AGZDcl",
},
.tigerbeetle_io = .{
.url = "https://github.com/lightpanda-io/tigerbeetle-io/archive/61d9652f1a957b7f4db723ea6aa0ce9635e840ce.tar.gz",
.hash = "tigerbeetle_io-0.0.0-ViLgxpyRBAB5BMfIcj3KMXfbJzwARs9uSl8aRy2OXULd",
Expand Down
6 changes: 3 additions & 3 deletions flake.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions flake.nix
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
glib.dev
glibc.dev
zlib
zlib.dev
];
};
in
Expand Down
40 changes: 26 additions & 14 deletions src/app.zig
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
const std = @import("std");

const Allocator = std.mem.Allocator;

const log = @import("log.zig");
const Http = @import("http/Http.zig");
const Loop = @import("runtime/loop.zig").Loop;
const http = @import("http/client.zig");
const Platform = @import("runtime/js.zig").Platform;

const Telemetry = @import("telemetry/telemetry.zig").Telemetry;
Expand All @@ -12,12 +13,12 @@ const Notification = @import("notification.zig").Notification;
// Container for global state / objects that various parts of the system
// might need.
pub const App = struct {
http: Http,
loop: *Loop,
config: Config,
platform: ?*const Platform,
allocator: Allocator,
telemetry: Telemetry,
http_client: http.Client,
app_dir_path: ?[]const u8,
notification: *Notification,

Expand All @@ -32,9 +33,12 @@ pub const App = struct {
run_mode: RunMode,
platform: ?*const Platform = null,
tls_verify_host: bool = true,
http_proxy: ?std.Uri = null,
proxy_type: ?http.ProxyType = null,
proxy_auth: ?http.ProxyAuth = null,
http_proxy: ?[:0]const u8 = null,
proxy_bearer_token: ?[:0]const u8 = null,
http_timeout_ms: ?u31 = null,
http_connect_timeout_ms: ?u31 = null,
http_max_host_open: ?u8 = null,
http_max_concurrent: ?u8 = null,
};

pub fn init(allocator: Allocator, config: Config) !*App {
Expand All @@ -50,25 +54,33 @@ pub const App = struct {
const notification = try Notification.init(allocator, null);
errdefer notification.deinit();

var http = try Http.init(allocator, .{
.max_host_open = config.http_max_host_open orelse 4,
.max_concurrent = config.http_max_concurrent orelse 10,
.timeout_ms = config.http_timeout_ms orelse 5000,
.connect_timeout_ms = config.http_connect_timeout_ms orelse 0,
.http_proxy = config.http_proxy,
.tls_verify_host = config.tls_verify_host,
.proxy_bearer_token = config.proxy_bearer_token,
});
errdefer http.deinit();

const app_dir_path = getAndMakeAppDir(allocator);

app.* = .{
.loop = loop,
.http = http,
.allocator = allocator,
.telemetry = undefined,
.platform = config.platform,
.app_dir_path = app_dir_path,
.notification = notification,
.http_client = try http.Client.init(allocator, loop, .{
.max_concurrent = 3,
.http_proxy = config.http_proxy,
.proxy_type = config.proxy_type,
.proxy_auth = config.proxy_auth,
.tls_verify_host = config.tls_verify_host,
}),
.config = config,
};
app.telemetry = Telemetry.init(app, config.run_mode);

app.telemetry = try Telemetry.init(app, config.run_mode);
errdefer app.telemetry.deinit();

try app.telemetry.register(app.notification);

return app;
Expand All @@ -82,8 +94,8 @@ pub const App = struct {
self.telemetry.deinit();
self.loop.deinit();
allocator.destroy(self.loop);
self.http_client.deinit();
self.notification.deinit();
self.http.deinit();
allocator.destroy(self);
}
};
Expand Down
52 changes: 52 additions & 0 deletions src/browser/DataURI.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
const std = @import("std");
const Allocator = std.mem.Allocator;

// Parses data:[<media-type>][;base64],<data>
pub fn parse(allocator: Allocator, src: []const u8) !?[]const u8 {
if (!std.mem.startsWith(u8, src, "data:")) {
return null;
}

const uri = src[5..];
const data_starts = std.mem.indexOfScalar(u8, uri, ',') orelse return null;

var data = uri[data_starts + 1 ..];

// Extract the encoding.
const metadata = uri[0..data_starts];
if (std.mem.endsWith(u8, metadata, ";base64")) {
const decoder = std.base64.standard.Decoder;
const decoded_size = try decoder.calcSizeForSlice(data);

const buffer = try allocator.alloc(u8, decoded_size);
errdefer allocator.free(buffer);

try decoder.decode(buffer, data);
data = buffer;
}

return data;
}

const testing = @import("../testing.zig");
test "DataURI: parse valid" {
try test_valid("data:text/javascript; charset=utf-8;base64,Zm9v", "foo");
try test_valid("data:text/javascript; charset=utf-8;,foo", "foo");
try test_valid("data:,foo", "foo");
}

test "DataURI: parse invalid" {
try test_cannot_parse("atad:,foo");
try test_cannot_parse("data:foo");
try test_cannot_parse("data:");
}

fn test_valid(uri: []const u8, expected: []const u8) !void {
defer testing.reset();
const data_uri = try parse(testing.arena_allocator, uri) orelse return error.TestFailed;
try testing.expectEqual(expected, data_uri);
}

fn test_cannot_parse(uri: []const u8) !void {
try testing.expectEqual(null, parse(undefined, uri));
}
168 changes: 168 additions & 0 deletions src/browser/Scheduler.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <[email protected]>
// Pierre Tachoire <[email protected]>
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// 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/>.

const std = @import("std");
const log = @import("../log.zig");
const Allocator = std.mem.Allocator;

const Scheduler = @This();

primary: Queue,

// For repeating tasks. We only want to run these if there are other things to
// do. We don't, for example, want a window.setInterval or the page.runMicrotasks
// to block the page.wait.
secondary: Queue,

// we expect allocator to be the page arena, hence we never call primary.deinit
pub fn init(allocator: Allocator) Scheduler {
return .{
.primary = Queue.init(allocator, {}),
.secondary = Queue.init(allocator, {}),
};
}

pub fn reset(self: *Scheduler) void {
self.primary.clearRetainingCapacity();
self.secondary.clearRetainingCapacity();
}

const AddOpts = struct {
name: []const u8 = "",
};
pub fn add(self: *Scheduler, ctx: *anyopaque, func: Task.Func, ms: u32, opts: AddOpts) !void {
if (ms > 5_000) {
log.warn(.user_script, "long timeout ignored", .{ .delay = ms });
// ignore any task that we're almost certainly never going to run
return;
}
return self.primary.add(.{
.ms = std.time.milliTimestamp() + ms,
.ctx = ctx,
.func = func,
.name = opts.name,
});
}

pub fn runHighPriority(self: *Scheduler) !?u32 {
return self.runQueue(&self.primary);
}

pub fn runLowPriority(self: *Scheduler) !?u32 {
return self.runQueue(&self.secondary);
}

fn runQueue(self: *Scheduler, queue: *Queue) !?u32 {
// this is O(1)
if (queue.count() == 0) {
return null;
}

const now = std.time.milliTimestamp();

var next = queue.peek();
while (next) |task| {
const time_to_next = task.ms - now;
if (time_to_next > 0) {
// @intCast is petty safe since we limit tasks to just 5 seconds
// in the future
return @intCast(time_to_next);
}

if (task.func(task.ctx)) |repeat_delay| {
// if we do (now + 0) then our WHILE loop will run endlessly.
// no task should ever return 0
std.debug.assert(repeat_delay != 0);

var copy = task;
copy.ms = now + repeat_delay;
try self.secondary.add(copy);
}
_ = queue.remove();
next = queue.peek();
}
return null;
}

const Task = struct {
ms: i64,
func: Func,
ctx: *anyopaque,
name: []const u8,

const Func = *const fn (ctx: *anyopaque) ?u32;
};

const Queue = std.PriorityQueue(Task, void, struct {
fn compare(_: void, a: Task, b: Task) std.math.Order {
return std.math.order(a.ms, b.ms);
}
}.compare);

const testing = @import("../testing.zig");
test "Scheduler" {
defer testing.reset();

var task = TestTask{ .allocator = testing.arena_allocator };

var s = Scheduler.init(testing.arena_allocator);
try testing.expectEqual(null, s.runHighPriority());
try testing.expectEqual(0, task.calls.items.len);

try s.add(&task, TestTask.run1, 3, .{});

try testing.expectDelta(3, try s.runHighPriority(), 1);
try testing.expectEqual(0, task.calls.items.len);

std.time.sleep(std.time.ns_per_ms * 5);
try testing.expectEqual(null, s.runHighPriority());
try testing.expectEqualSlices(u32, &.{1}, task.calls.items);

try s.add(&task, TestTask.run2, 3, .{});
try s.add(&task, TestTask.run1, 2, .{});

std.time.sleep(std.time.ns_per_ms * 5);
try testing.expectDelta(null, try s.runHighPriority(), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);

std.time.sleep(std.time.ns_per_ms * 5);
// wont' run secondary
try testing.expectEqual(null, try s.runHighPriority());
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);

//runs secondary
try testing.expectDelta(2, try s.runLowPriority(), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2, 2 }, task.calls.items);
}

const TestTask = struct {
allocator: Allocator,
calls: std.ArrayListUnmanaged(u32) = .{},

fn run1(ctx: *anyopaque) ?u32 {
var self: *TestTask = @alignCast(@ptrCast(ctx));
self.calls.append(self.allocator, 1) catch unreachable;
return null;
}

fn run2(ctx: *anyopaque) ?u32 {
var self: *TestTask = @alignCast(@ptrCast(ctx));
self.calls.append(self.allocator, 2) catch unreachable;
return 2;
}
};
Loading