add custom scheduler

This commit is contained in:
Karl Seguin
2025-07-31 21:08:05 +08:00
parent 254d22e2cc
commit 94e8964f69
10 changed files with 314 additions and 142 deletions

173
src/browser/Scheduler.zig Normal file
View File

@@ -0,0 +1,173 @@
// Copyright (C) 2023-2025 Lightpanda (Selecy SAS)
//
// Francis Bouvier <francis@lightpanda.io>
// Pierre Tachoire <pierre@lightpanda.io>
//
// 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, {}),
};
}
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,
});
}
// fn debug(self: *Scheduler) void {
// var it = self.primary.iterator();
// while (it.next()) |task| {
// std.debug.print("- {s}\n", .{task.name});
// }
// }
pub fn run(self: *Scheduler, force_secondary: bool) !?u32 {
if (self.primary.count() == 0 and force_secondary == false) {
return null;
}
const now = std.time.milliTimestamp();
const time_to_next_primary = try self.runQueue(&self.primary, now);
const time_to_next_secondary = try self.runQueue(&self.secondary, now);
if (time_to_next_primary == null) {
return time_to_next_secondary;
}
if (time_to_next_secondary == null) {
return time_to_next_primary;
}
return @min(time_to_next_primary.?, time_to_next_secondary.?);
}
fn runQueue(self: *Scheduler, queue: *Queue, now: i64) !?u32 {
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.run(false));
try testing.expectEqual(0, task.calls.items.len);
try s.add(&task, TestTask.run1, 3, .{});
try testing.expectDelta(3, try s.run(false), 1);
try testing.expectEqual(0, task.calls.items.len);
std.time.sleep(std.time.ns_per_ms * 5);
try testing.expectEqual(null, s.run(false));
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(2, try s.run(false), 1);
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
std.time.sleep(std.time.ns_per_ms * 5);
// only secondary won't be run unless forced
try testing.expectEqual(null, try s.run(false));
try testing.expectEqualSlices(u32, &.{ 1, 1, 2 }, task.calls.items);
// only secondary will be run when forced
try testing.expectDelta(2, try s.run(true), 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;
}
};

View File

@@ -18,7 +18,6 @@
const std = @import("std");
const log = @import("../log.zig");
const parser = @import("netsurf.zig");
const http = @import("../http/client.zig");
@@ -250,11 +249,10 @@ fn evaluate(self: *ScriptManager) void {
fn asyncDone(self: *ScriptManager) void {
self.async_count -= 1;
if (
self.async_count == 0 and // there are no more async scripts
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
if (self.async_count == 0 and // there are no more async scripts
self.static_scripts_done and // and we've finished parsing the HTML to queue all <scripts>
self.scripts.first == null and // and there are no more <script src=> to wait for
self.deferred.first == null // and there are no more <script defer src=> to wait for
self.deferred.first == null // and there are no more <script defer src=> to wait for
) {
// then the document is considered complete
self.page.documentIsComplete();
@@ -380,7 +378,6 @@ const PendingScript = struct {
log.warn(.http, "script fetch error", .{ .req = transfer, .err = err });
self.deinit();
}
};
const Script = struct {

View File

@@ -22,7 +22,6 @@ const Allocator = std.mem.Allocator;
const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Page = @import("../page.zig").Page;
const Loop = @import("../../runtime/loop.zig").Loop;
const Env = @import("../env.zig").Env;
const NodeList = @import("nodelist.zig").NodeList;
@@ -36,12 +35,10 @@ const Walker = @import("../dom/walker.zig").WalkerChildren;
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
pub const MutationObserver = struct {
loop: *Loop,
page: *Page,
cbk: Env.Function,
arena: Allocator,
connected: bool,
scheduled: bool,
loop_node: Loop.CallbackNode,
// List of records which were observed. When the call scope ends, we need to
// execute our callback with it.
@@ -50,17 +47,15 @@ pub const MutationObserver = struct {
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
return .{
.cbk = cbk,
.loop = page.loop,
.page = page,
.observed = .{},
.connected = true,
.scheduled = false,
.arena = page.arena,
.loop_node = .{ .func = callback },
};
}
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
const arena = self.arena;
const arena = self.page.arena;
var options = options_ orelse Options{};
if (options.attributeFilter.len > 0) {
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
@@ -115,17 +110,17 @@ pub const MutationObserver = struct {
}
}
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
fn callback(ctx: *anyopaque) ?u32 {
const self: *MutationObserver = @alignCast(@ptrCast(ctx));
if (self.connected == false) {
self.scheduled = true;
return;
return null;
}
self.scheduled = false;
const records = self.observed.items;
if (records.len == 0) {
return;
return null;
}
defer self.observed.clearRetainingCapacity();
@@ -138,6 +133,7 @@ pub const MutationObserver = struct {
.source = "mutation observer",
});
};
return null;
}
// TODO
@@ -301,7 +297,7 @@ const Observer = struct {
.type = event_type.recordType(),
};
const arena = mutation_observer.arena;
const arena = mutation_observer.page.arena;
switch (event_type) {
.DOMAttrModified => {
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
@@ -330,7 +326,12 @@ const Observer = struct {
if (mutation_observer.scheduled == false) {
mutation_observer.scheduled = true;
_ = try mutation_observer.loop.timeout(0, &mutation_observer.loop_node);
try mutation_observer.page.scheduler.add(
mutation_observer,
MutationObserver.callback,
0,
.{ .name = "mutation_observer" },
);
}
}
};

View File

@@ -21,7 +21,6 @@ const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Loop = @import("../../runtime/loop.zig").Loop;
const EventTarget = @import("../dom/event_target.zig").EventTarget;
pub const Interfaces = .{
@@ -77,11 +76,9 @@ pub const AbortSignal = struct {
const callback = try page.arena.create(TimeoutCallback);
callback.* = .{
.signal = .init,
.node = .{ .func = TimeoutCallback.run },
};
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
_ = try page.loop.timeout(delay_ms, &callback.node);
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
return &callback.signal;
}
@@ -131,15 +128,12 @@ pub const AbortSignal = struct {
const TimeoutCallback = struct {
signal: AbortSignal,
// This is the internal data that the event loop tracks. We'll get this
// back in run and, from it, can get our TimeoutCallback instance
node: Loop.CallbackNode = undefined,
fn run(node: *Loop.CallbackNode, _: *?u63) void {
const self: *TimeoutCallback = @fieldParentPtr("node", node);
fn run(ctx: *anyopaque) ?u32 {
const self: *TimeoutCallback = @alignCast(@ptrCast(ctx));
self.signal.abort("TimeoutError") catch |err| {
log.warn(.app, "abort signal timeout", .{ .err = err });
};
return null;
}
};

View File

@@ -22,7 +22,6 @@ const log = @import("../../log.zig");
const parser = @import("../netsurf.zig");
const Env = @import("../env.zig").Env;
const Page = @import("../page.zig").Page;
const Loop = @import("../../runtime/loop.zig").Loop;
const Navigator = @import("navigator.zig").Navigator;
const History = @import("history.zig").History;
@@ -57,7 +56,7 @@ pub const Window = struct {
// counter for having unique timer ids
timer_id: u30 = 0,
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
timers: std.AutoHashMapUnmanaged(u32, void) = .{},
crypto: Crypto = .{},
console: Console = .{},
@@ -179,34 +178,31 @@ pub const Window = struct {
}
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true });
return self.createTimeout(cbk, 5, page, .{ .animation_frame = true, .name = "animationFrame" });
}
pub fn _cancelAnimationFrame(self: *Window, id: u32, page: *Page) !void {
const kv = self.timers.fetchRemove(id) orelse return;
return page.loop.cancel(kv.value.loop_id);
pub fn _cancelAnimationFrame(self: *Window, id: u32) !void {
_ = self.timers.remove(id);
}
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .args = params });
return self.createTimeout(cbk, delay, page, .{ .args = params, .name = "setTimeout" });
}
pub fn _setInterval(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params });
return self.createTimeout(cbk, delay, page, .{ .repeat = true, .args = params, .name = "setInterval" });
}
pub fn _clearTimeout(self: *Window, id: u32, page: *Page) !void {
const kv = self.timers.fetchRemove(id) orelse return;
return page.loop.cancel(kv.value.loop_id);
pub fn _clearTimeout(self: *Window, id: u32) !void {
_ = self.timers.remove(id);
}
pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void {
const kv = self.timers.fetchRemove(id) orelse return;
return page.loop.cancel(kv.value.loop_id);
pub fn _clearInterval(self: *Window, id: u32) !void {
_ = self.timers.remove(id);
}
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
return self.createTimeout(cbk, 0, page, .{});
return self.createTimeout(cbk, 0, page, .{ .name = "queueMicrotask" });
}
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
@@ -232,6 +228,7 @@ pub const Window = struct {
}
const CreateTimeoutOpts = struct {
name: []const u8,
args: []Env.JsObject = &.{},
repeat: bool = false,
animation_frame: bool = false,
@@ -258,6 +255,8 @@ pub const Window = struct {
if (gop.found_existing) {
// this can only happen if we've created 2^31 timeouts.
return error.TooManyTimeout;
} else {
gop.value_ptr.* = {};
}
errdefer _ = self.timers.remove(timer_id);
@@ -270,22 +269,19 @@ pub const Window = struct {
}
}
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
const callback = try arena.create(TimerCallback);
callback.* = .{
.cbk = cbk,
.loop_id = 0, // we're going to set this to a real value shortly
.window = self,
.timer_id = timer_id,
.args = persisted_args,
.node = .{ .func = TimerCallback.run },
.repeat = if (opts.repeat) delay_ms else null,
.animation_frame = opts.animation_frame,
// setting a repeat time of 0 is illegal, doing + 1 is a simple way to avoid that
.repeat = if (opts.repeat) delay + 1 else null,
};
callback.loop_id = try page.loop.timeout(delay_ms, &callback.node);
gop.value_ptr.* = callback;
try page.scheduler.add(callback, TimerCallback.run, delay, .{ .name = opts.name });
return timer_id;
}
@@ -354,30 +350,32 @@ pub const Window = struct {
};
const TimerCallback = struct {
// the internal loop id, need it when cancelling
loop_id: usize,
// the id of our timer (windows.timers key)
timer_id: u31,
// if false, we'll remove the timer_id from the window.timers lookup on run
repeat: ?u32,
// The JavaScript callback to execute
cbk: Function,
// This is the internal data that the event loop tracks. We'll get this
// back in run and, from it, can get our TimerCallback instance
node: Loop.CallbackNode = undefined,
// if the event should be repeated
repeat: ?u63 = null,
animation_frame: bool = false,
window: *Window,
args: []Env.JsObject = &.{},
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
const self: *TimerCallback = @fieldParentPtr("node", node);
fn run(ctx: *anyopaque) ?u32 {
const self: *TimerCallback = @alignCast(@ptrCast(ctx));
if (self.repeat != null) {
if (self.window.timers.contains(self.timer_id) == false) {
// it was called
return null;
}
} else if (self.window.timers.remove(self.timer_id) == false) {
// it was cancelled
return null;
}
var result: Function.Result = undefined;
@@ -396,14 +394,7 @@ const TimerCallback = struct {
});
};
if (self.repeat) |r| {
// setInterval
repeat_delay.* = r;
return;
}
// setTimeout
_ = self.window.timers.remove(self.timer_id);
return self.repeat;
}
};
@@ -412,13 +403,11 @@ test "Browser.HTML.Window" {
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
defer runner.deinit();
try runner.testCases(&.{
.{ "window.parent === window", "true" },
.{ "window.top === window", "true" },
}, .{});
// try runner.testCases(&.{
// .{ "window.parent === window", "true" },
// .{ "window.top === window", "true" },
// }, .{});
// requestAnimationFrame should be able to wait by recursively calling itself
// Note however that we in this test do not wait as the request is just send to the browser
try runner.testCases(&.{
.{
\\ let start = 0;

View File

@@ -31,6 +31,7 @@ const Renderer = @import("renderer.zig").Renderer;
const Window = @import("html/window.zig").Window;
const Walker = @import("dom/walker.zig").WalkerDepthFirst;
const Loop = @import("../runtime/loop.zig").Loop;
const Scheduler = @import("Scheduler.zig");
const ScriptManager = @import("ScriptManager.zig");
const HTMLDocument = @import("html/document.zig").HTMLDocument;
@@ -75,11 +76,6 @@ pub const Page = struct {
renderer: Renderer,
// run v8 micro tasks
microtask_node: Loop.CallbackNode,
// run v8 pump message loop and idle tasks
messageloop_node: Loop.CallbackNode,
keydown_event_node: parser.EventNode,
window_clicked_event_node: parser.EventNode,
@@ -94,9 +90,9 @@ pub const Page = struct {
polyfill_loader: polyfill.Loader = .{},
scheduler: Scheduler,
http_client: *http.Client,
script_manager: ScriptManager,
mode: Mode,
loaded: bool = false,
@@ -127,8 +123,7 @@ pub const Page = struct {
.cookie_jar = &session.cookie_jar,
.script_manager = script_manager,
.http_client = browser.http_client,
.microtask_node = .{ .func = microtaskCallback },
.messageloop_node = .{ .func = messageLoopCallback },
.scheduler = Scheduler.init(arena),
.keydown_event_node = .{ .func = keydownCallback },
.window_clicked_event_node = .{ .func = windowClicked },
// @newhttp
@@ -140,10 +135,10 @@ pub const Page = struct {
self.main_context = try session.executor.createJsContext(&self.window, self, self, true, Env.GlobalMissingCallback.init(&self.polyfill_loader));
try polyfill.preload(self.arena, self.main_context);
try self.scheduler.add(self, runMicrotasks, 5, .{ .name = "page.microtasks" });
// message loop must run only non-test env
if (comptime !builtin.is_test) {
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
_ = try session.browser.app.loop.timeout(100 * std.time.ns_per_ms, &self.messageloop_node);
try self.scheduler.add(self, runMessageLoop, 5, .{ .name = "page.messageLoop" });
}
}
@@ -151,16 +146,16 @@ pub const Page = struct {
self.script_manager.deinit();
}
fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
const self: *Page = @fieldParentPtr("microtask_node", node);
fn runMicrotasks(ctx: *anyopaque) ?u32 {
const self: *Page = @alignCast(@ptrCast(ctx));
self.session.browser.runMicrotasks();
repeat_delay.* = 1 * std.time.ns_per_ms;
return 5;
}
fn messageLoopCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
const self: *Page = @fieldParentPtr("messageloop_node", node);
fn runMessageLoop(ctx: *anyopaque) ?u32 {
const self: *Page = @alignCast(@ptrCast(ctx));
self.session.browser.runMessageLoop();
repeat_delay.* = 100 * std.time.ns_per_ms;
return 100;
}
pub const DumpOpts = struct {
@@ -237,39 +232,66 @@ pub const Page = struct {
// The HTML page was parsed. We now either have JS scripts to
// download, or timeouts to execute, or both.
const cutoff = timestamp() + wait_sec;
var try_catch: Env.TryCatch = undefined;
try_catch.init(self.main_context);
defer try_catch.deinit();
var scheduler = &self.scheduler;
var http_client = self.http_client;
var loop = self.session.browser.app.loop;
// @newhttp Not sure about the timing / the order / any of this.
// I think I want to remove the loop. Implement our own timeouts
// and switch the CDP server to blocking. For now, just try this.`
while (timestamp() < cutoff) {
const has_pending_timeouts = loop.hasPendingTimeout();
if (http_client.active > 0) {
try http_client.tick(10); // 10ms
} else if (self.loaded and self.loaded and !has_pending_timeouts) {
// we have no active HTTP requests, and no timeouts pending
return;
}
var ms_remaining = wait_sec * 1000;
var timer = try std.time.Timer.start();
if (!has_pending_timeouts) {
continue;
}
while (true) {
const has_active_http = http_client.active > 0;
// 10ms
try loop.run(std.time.ns_per_ms * 10);
const ms_to_next_task = try scheduler.run(has_active_http);
if (try_catch.hasCaught()) {
const msg = (try try_catch.err(self.arena)) orelse "unknown";
log.err(.browser, "page wait error", .{ .err = msg });
return error.JsError;
}
if (has_active_http == false) {
if (ms_to_next_task) |ms| {
// There are no HTTP transfers, so there's no point calling
// http_client.tick.
// TODO: should we just force-run the scheduler??
if (ms > ms_remaining) {
// we'd wait to long, might as well exit early.
return;
}
std.time.sleep(std.time.ns_per_ms * ms);
ms_remaining -= ms;
continue;
}
// We have no active http transfer and no pending
// schedule tasks. We're done
return;
}
// We'll block here, waiting for network IO. We know
// when the next timeout is scheduled, and we know how long
// the caller wants to wait for, so we can pick a good wait
// duration
const ms_to_wait = @min(ms_remaining, ms_to_next_task orelse 1000);
try http_client.tick(ms_to_wait);
if (try_catch.hasCaught()) {
const msg = (try try_catch.err(self.arena)) orelse "unknown";
log.err(.browser, "page wait error", .{ .err = msg });
return error.JsError;
}
const ms_elapsed = timer.lap() / 100_000;
if (ms_elapsed > ms_remaining) {
return;
}
ms_remaining -= ms_elapsed;
}
},
.err => |err| return err,

View File

@@ -354,7 +354,7 @@ pub const XMLHttpRequest = struct {
return self.headers.append(
self.arena,
try std.fmt.allocPrintZ(self.arena, "{s}: {s}", .{name, value}),
try std.fmt.allocPrintZ(self.arena, "{s}: {s}", .{ name, value }),
);
}
@@ -424,9 +424,9 @@ pub const XMLHttpRequest = struct {
const header = &transfer.response_header.?;
log.debug(.http, "request header", .{
.source = "xhr",
.url = self.url,
.status = header.status,
.source = "xhr",
.url = self.url,
.status = header.status,
});
if (header.contentType()) |ct| {
@@ -731,7 +731,7 @@ pub const XMLHttpRequest = struct {
if (entry[name.len] != ':') {
continue;
}
return std.mem.trimLeft(u8, entry[name.len + 1..], " ");
return std.mem.trimLeft(u8, entry[name.len + 1 ..], " ");
}
return null;
}
@@ -793,12 +793,9 @@ test "Browser.XHR.XMLHttpRequest" {
.{ "req.status", "200" },
.{ "req.statusText", "OK" },
.{ "req.getResponseHeader('Content-Type')", "text/html; charset=utf-8" },
.{
"req.getAllResponseHeaders()",
"content-length: 100\r\n" ++
.{ "req.getAllResponseHeaders()", "content-length: 100\r\n" ++
"Content-Type: text/html; charset=utf-8\r\n" ++
"Connection: Close\r\n"
},
"Connection: Close\r\n" },
.{ "req.responseText.length", "100" },
.{ "req.response.length == req.responseText.length", "true" },
.{ "req.responseXML instanceof Document", "true" },

View File

@@ -38,8 +38,8 @@ pub fn load(allocator: Allocator, arena: Allocator) !c.curl_blob {
const encoded_size = encoder.calcSize(bytes.len);
const buffer_size = encoded_size +
(bundle.map.count() * 75) + // start / end per certificate + extra, just in case
(encoded_size / 64) // newline per 64 characters
(bundle.map.count() * 75) + // start / end per certificate + extra, just in case
(encoded_size / 64) // newline per 64 characters
;
try arr.ensureTotalCapacity(arena, buffer_size);
var writer = arr.writer(arena);
@@ -48,7 +48,7 @@ pub fn load(allocator: Allocator, arena: Allocator) !c.curl_blob {
const cert = try std.crypto.Certificate.der.Element.parse(bytes, index.*);
try writer.writeAll("-----BEGIN CERTIFICATE-----\n");
var line_writer = LineWriter{.inner = writer};
var line_writer = LineWriter{ .inner = writer };
try encoder.encodeWriter(&line_writer, bytes[index.*..cert.slice.end]);
try writer.writeAll("\n-----END CERTIFICATE-----\n");
}

View File

@@ -249,13 +249,13 @@ const Handles = struct {
node.data = &handles[i];
available.append(node);
}
}
return .{
return .{
.handles = handles,
.available = available,
.cert_arena = cert_arena,
};
};
}
fn deinit(self: *Handles, allocator: Allocator) void {
@@ -335,12 +335,12 @@ pub const Request = struct {
// arbitrary data that can be associated with this request
ctx: *anyopaque = undefined,
start_callback: ?*const fn(req: *Transfer) anyerror!void = null,
header_callback: ?*const fn (req: *Transfer, header: []const u8) anyerror!void = null ,
start_callback: ?*const fn (req: *Transfer) anyerror!void = null,
header_callback: ?*const fn (req: *Transfer, header: []const u8) anyerror!void = null,
header_done_callback: *const fn (req: *Transfer) anyerror!void,
data_callback: *const fn(req: *Transfer, data: []const u8) anyerror!void,
done_callback: *const fn(req: *Transfer) anyerror!void,
error_callback: *const fn(req: *Transfer, err: anyerror) void,
data_callback: *const fn (req: *Transfer, data: []const u8) anyerror!void,
done_callback: *const fn (req: *Transfer) anyerror!void,
error_callback: *const fn (req: *Transfer, err: anyerror) void,
};
pub const Transfer = struct {
@@ -365,7 +365,7 @@ pub const Transfer = struct {
pub fn format(self: *const Transfer, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
const req = self.req;
return writer.print("[{d}] {s} {s}", .{self.id, @tagName(req.method), req.url});
return writer.print("[{d}] {s} {s}", .{ self.id, @tagName(req.method), req.url });
}
fn onError(self: *Transfer, err: anyerror) void {
@@ -385,7 +385,7 @@ pub const Transfer = struct {
pub fn abort(self: *Transfer) void {
var client = self.handle.client;
errorMCheck(c.curl_multi_remove_handle(client.multi, self.handle.easy)) catch |err| {
log.err(.http, "Failed to abort", .{.err = err});
log.err(.http, "Failed to abort", .{ .err = err });
};
client.active -= 1;
self.deinit();
@@ -397,13 +397,13 @@ pub const Transfer = struct {
const handle: *Handle = @alignCast(@ptrCast(data));
var transfer = fromEasy(handle.easy) catch |err| {
log.err(.http, "retrive private info", .{.err = err});
log.err(.http, "retrive private info", .{ .err = err });
return 0;
};
std.debug.assert(std.mem.endsWith(u8, buffer[0..buf_len], "\r\n"));
const header = buffer[0..buf_len - 2];
const header = buffer[0 .. buf_len - 2];
if (transfer.response_header == null) {
if (buf_len < 13 or std.mem.startsWith(u8, header, "HTTP/") == false) {
@@ -477,7 +477,7 @@ pub const Transfer = struct {
const handle: *Handle = @alignCast(@ptrCast(data));
var transfer = fromEasy(handle.easy) catch |err| {
log.err(.http, "retrive private info", .{.err = err});
log.err(.http, "retrive private info", .{ .err = err });
return c.CURL_WRITEFUNC_ERROR;
};
@@ -549,4 +549,3 @@ pub const ProxyAuth = union(enum) {
basic: struct { user_pass: []const u8 },
bearer: struct { token: []const u8 },
};

View File

@@ -19,7 +19,7 @@
const std = @import("std");
const c = @import("client.zig").c;
pub const Error = error {
pub const Error = error{
UnsupportedProtocol,
FailedInit,
UrlMalformat,
@@ -201,7 +201,7 @@ pub fn fromCode(code: c.CURLcode) Error {
};
}
pub const Multi = error {
pub const Multi = error{
BadHandle,
BadEasyHandle,
OutOfMemory,