mirror of
https://github.com/lightpanda-io/browser.git
synced 2025-10-29 23:23:28 +00:00
add custom scheduler
This commit is contained in:
173
src/browser/Scheduler.zig
Normal file
173
src/browser/Scheduler.zig
Normal 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;
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -18,7 +18,6 @@
|
|||||||
|
|
||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
|
|
||||||
|
|
||||||
const log = @import("../log.zig");
|
const log = @import("../log.zig");
|
||||||
const parser = @import("netsurf.zig");
|
const parser = @import("netsurf.zig");
|
||||||
const http = @import("../http/client.zig");
|
const http = @import("../http/client.zig");
|
||||||
@@ -250,8 +249,7 @@ fn evaluate(self: *ScriptManager) void {
|
|||||||
|
|
||||||
fn asyncDone(self: *ScriptManager) void {
|
fn asyncDone(self: *ScriptManager) void {
|
||||||
self.async_count -= 1;
|
self.async_count -= 1;
|
||||||
if (
|
if (self.async_count == 0 and // there are no more async scripts
|
||||||
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.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.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
|
||||||
@@ -380,7 +378,6 @@ const PendingScript = struct {
|
|||||||
log.warn(.http, "script fetch error", .{ .req = transfer, .err = err });
|
log.warn(.http, "script fetch error", .{ .req = transfer, .err = err });
|
||||||
self.deinit();
|
self.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const Script = struct {
|
const Script = struct {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const Allocator = std.mem.Allocator;
|
|||||||
const log = @import("../../log.zig");
|
const log = @import("../../log.zig");
|
||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
|
||||||
|
|
||||||
const Env = @import("../env.zig").Env;
|
const Env = @import("../env.zig").Env;
|
||||||
const NodeList = @import("nodelist.zig").NodeList;
|
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
|
// WEB IDL https://dom.spec.whatwg.org/#interface-mutationobserver
|
||||||
pub const MutationObserver = struct {
|
pub const MutationObserver = struct {
|
||||||
loop: *Loop,
|
page: *Page,
|
||||||
cbk: Env.Function,
|
cbk: Env.Function,
|
||||||
arena: Allocator,
|
|
||||||
connected: bool,
|
connected: bool,
|
||||||
scheduled: bool,
|
scheduled: bool,
|
||||||
loop_node: Loop.CallbackNode,
|
|
||||||
|
|
||||||
// List of records which were observed. When the call scope ends, we need to
|
// List of records which were observed. When the call scope ends, we need to
|
||||||
// execute our callback with it.
|
// execute our callback with it.
|
||||||
@@ -50,17 +47,15 @@ pub const MutationObserver = struct {
|
|||||||
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
pub fn constructor(cbk: Env.Function, page: *Page) !MutationObserver {
|
||||||
return .{
|
return .{
|
||||||
.cbk = cbk,
|
.cbk = cbk,
|
||||||
.loop = page.loop,
|
.page = page,
|
||||||
.observed = .{},
|
.observed = .{},
|
||||||
.connected = true,
|
.connected = true,
|
||||||
.scheduled = false,
|
.scheduled = false,
|
||||||
.arena = page.arena,
|
|
||||||
.loop_node = .{ .func = callback },
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _observe(self: *MutationObserver, node: *parser.Node, options_: ?Options) !void {
|
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{};
|
var options = options_ orelse Options{};
|
||||||
if (options.attributeFilter.len > 0) {
|
if (options.attributeFilter.len > 0) {
|
||||||
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
options.attributeFilter = try arena.dupe([]const u8, options.attributeFilter);
|
||||||
@@ -115,17 +110,17 @@ pub const MutationObserver = struct {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn callback(node: *Loop.CallbackNode, _: *?u63) void {
|
fn callback(ctx: *anyopaque) ?u32 {
|
||||||
const self: *MutationObserver = @fieldParentPtr("loop_node", node);
|
const self: *MutationObserver = @alignCast(@ptrCast(ctx));
|
||||||
if (self.connected == false) {
|
if (self.connected == false) {
|
||||||
self.scheduled = true;
|
self.scheduled = true;
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
self.scheduled = false;
|
self.scheduled = false;
|
||||||
|
|
||||||
const records = self.observed.items;
|
const records = self.observed.items;
|
||||||
if (records.len == 0) {
|
if (records.len == 0) {
|
||||||
return;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
defer self.observed.clearRetainingCapacity();
|
defer self.observed.clearRetainingCapacity();
|
||||||
@@ -138,6 +133,7 @@ pub const MutationObserver = struct {
|
|||||||
.source = "mutation observer",
|
.source = "mutation observer",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
// TODO
|
||||||
@@ -301,7 +297,7 @@ const Observer = struct {
|
|||||||
.type = event_type.recordType(),
|
.type = event_type.recordType(),
|
||||||
};
|
};
|
||||||
|
|
||||||
const arena = mutation_observer.arena;
|
const arena = mutation_observer.page.arena;
|
||||||
switch (event_type) {
|
switch (event_type) {
|
||||||
.DOMAttrModified => {
|
.DOMAttrModified => {
|
||||||
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
record.attribute_name = parser.mutationEventAttributeName(mutation_event) catch null;
|
||||||
@@ -330,7 +326,12 @@ const Observer = struct {
|
|||||||
|
|
||||||
if (mutation_observer.scheduled == false) {
|
if (mutation_observer.scheduled == false) {
|
||||||
mutation_observer.scheduled = true;
|
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" },
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ const log = @import("../../log.zig");
|
|||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
const Env = @import("../env.zig").Env;
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
|
||||||
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
const EventTarget = @import("../dom/event_target.zig").EventTarget;
|
||||||
|
|
||||||
pub const Interfaces = .{
|
pub const Interfaces = .{
|
||||||
@@ -77,11 +76,9 @@ pub const AbortSignal = struct {
|
|||||||
const callback = try page.arena.create(TimeoutCallback);
|
const callback = try page.arena.create(TimeoutCallback);
|
||||||
callback.* = .{
|
callback.* = .{
|
||||||
.signal = .init,
|
.signal = .init,
|
||||||
.node = .{ .func = TimeoutCallback.run },
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const delay_ms: u63 = @as(u63, delay) * std.time.ns_per_ms;
|
try page.scheduler.add(callback, TimeoutCallback.run, delay, .{ .name = "abort_signal" });
|
||||||
_ = try page.loop.timeout(delay_ms, &callback.node);
|
|
||||||
return &callback.signal;
|
return &callback.signal;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,15 +128,12 @@ pub const AbortSignal = struct {
|
|||||||
const TimeoutCallback = struct {
|
const TimeoutCallback = struct {
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
|
||||||
// This is the internal data that the event loop tracks. We'll get this
|
fn run(ctx: *anyopaque) ?u32 {
|
||||||
// back in run and, from it, can get our TimeoutCallback instance
|
const self: *TimeoutCallback = @alignCast(@ptrCast(ctx));
|
||||||
node: Loop.CallbackNode = undefined,
|
|
||||||
|
|
||||||
fn run(node: *Loop.CallbackNode, _: *?u63) void {
|
|
||||||
const self: *TimeoutCallback = @fieldParentPtr("node", node);
|
|
||||||
self.signal.abort("TimeoutError") catch |err| {
|
self.signal.abort("TimeoutError") catch |err| {
|
||||||
log.warn(.app, "abort signal timeout", .{ .err = err });
|
log.warn(.app, "abort signal timeout", .{ .err = err });
|
||||||
};
|
};
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ const log = @import("../../log.zig");
|
|||||||
const parser = @import("../netsurf.zig");
|
const parser = @import("../netsurf.zig");
|
||||||
const Env = @import("../env.zig").Env;
|
const Env = @import("../env.zig").Env;
|
||||||
const Page = @import("../page.zig").Page;
|
const Page = @import("../page.zig").Page;
|
||||||
const Loop = @import("../../runtime/loop.zig").Loop;
|
|
||||||
|
|
||||||
const Navigator = @import("navigator.zig").Navigator;
|
const Navigator = @import("navigator.zig").Navigator;
|
||||||
const History = @import("history.zig").History;
|
const History = @import("history.zig").History;
|
||||||
@@ -57,7 +56,7 @@ pub const Window = struct {
|
|||||||
|
|
||||||
// counter for having unique timer ids
|
// counter for having unique timer ids
|
||||||
timer_id: u30 = 0,
|
timer_id: u30 = 0,
|
||||||
timers: std.AutoHashMapUnmanaged(u32, *TimerCallback) = .{},
|
timers: std.AutoHashMapUnmanaged(u32, void) = .{},
|
||||||
|
|
||||||
crypto: Crypto = .{},
|
crypto: Crypto = .{},
|
||||||
console: Console = .{},
|
console: Console = .{},
|
||||||
@@ -179,34 +178,31 @@ pub const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub fn _requestAnimationFrame(self: *Window, cbk: Function, page: *Page) !u32 {
|
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 {
|
pub fn _cancelAnimationFrame(self: *Window, id: u32) !void {
|
||||||
const kv = self.timers.fetchRemove(id) orelse return;
|
_ = self.timers.remove(id);
|
||||||
return page.loop.cancel(kv.value.loop_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _setTimeout(self: *Window, cbk: Function, delay: ?u32, params: []Env.JsObject, page: *Page) !u32 {
|
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 {
|
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 {
|
pub fn _clearTimeout(self: *Window, id: u32) !void {
|
||||||
const kv = self.timers.fetchRemove(id) orelse return;
|
_ = self.timers.remove(id);
|
||||||
return page.loop.cancel(kv.value.loop_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _clearInterval(self: *Window, id: u32, page: *Page) !void {
|
pub fn _clearInterval(self: *Window, id: u32) !void {
|
||||||
const kv = self.timers.fetchRemove(id) orelse return;
|
_ = self.timers.remove(id);
|
||||||
return page.loop.cancel(kv.value.loop_id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn _queueMicrotask(self: *Window, cbk: Function, page: *Page) !u32 {
|
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 {
|
pub fn _matchMedia(_: *const Window, media: []const u8, page: *Page) !MediaQueryList {
|
||||||
@@ -232,6 +228,7 @@ pub const Window = struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const CreateTimeoutOpts = struct {
|
const CreateTimeoutOpts = struct {
|
||||||
|
name: []const u8,
|
||||||
args: []Env.JsObject = &.{},
|
args: []Env.JsObject = &.{},
|
||||||
repeat: bool = false,
|
repeat: bool = false,
|
||||||
animation_frame: bool = false,
|
animation_frame: bool = false,
|
||||||
@@ -258,6 +255,8 @@ pub const Window = struct {
|
|||||||
if (gop.found_existing) {
|
if (gop.found_existing) {
|
||||||
// this can only happen if we've created 2^31 timeouts.
|
// this can only happen if we've created 2^31 timeouts.
|
||||||
return error.TooManyTimeout;
|
return error.TooManyTimeout;
|
||||||
|
} else {
|
||||||
|
gop.value_ptr.* = {};
|
||||||
}
|
}
|
||||||
errdefer _ = self.timers.remove(timer_id);
|
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);
|
const callback = try arena.create(TimerCallback);
|
||||||
|
|
||||||
callback.* = .{
|
callback.* = .{
|
||||||
.cbk = cbk,
|
.cbk = cbk,
|
||||||
.loop_id = 0, // we're going to set this to a real value shortly
|
|
||||||
.window = self,
|
.window = self,
|
||||||
.timer_id = timer_id,
|
.timer_id = timer_id,
|
||||||
.args = persisted_args,
|
.args = persisted_args,
|
||||||
.node = .{ .func = TimerCallback.run },
|
|
||||||
.repeat = if (opts.repeat) delay_ms else null,
|
|
||||||
.animation_frame = opts.animation_frame,
|
.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;
|
return timer_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,30 +350,32 @@ pub const Window = struct {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const TimerCallback = struct {
|
const TimerCallback = struct {
|
||||||
// the internal loop id, need it when cancelling
|
|
||||||
loop_id: usize,
|
|
||||||
|
|
||||||
// the id of our timer (windows.timers key)
|
// the id of our timer (windows.timers key)
|
||||||
timer_id: u31,
|
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
|
// The JavaScript callback to execute
|
||||||
cbk: Function,
|
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,
|
animation_frame: bool = false,
|
||||||
|
|
||||||
window: *Window,
|
window: *Window,
|
||||||
|
|
||||||
args: []Env.JsObject = &.{},
|
args: []Env.JsObject = &.{},
|
||||||
|
|
||||||
fn run(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
fn run(ctx: *anyopaque) ?u32 {
|
||||||
const self: *TimerCallback = @fieldParentPtr("node", node);
|
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;
|
var result: Function.Result = undefined;
|
||||||
|
|
||||||
@@ -396,14 +394,7 @@ const TimerCallback = struct {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
if (self.repeat) |r| {
|
return self.repeat;
|
||||||
// setInterval
|
|
||||||
repeat_delay.* = r;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// setTimeout
|
|
||||||
_ = self.window.timers.remove(self.timer_id);
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -412,13 +403,11 @@ test "Browser.HTML.Window" {
|
|||||||
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
var runner = try testing.jsRunner(testing.tracking_allocator, .{});
|
||||||
defer runner.deinit();
|
defer runner.deinit();
|
||||||
|
|
||||||
try runner.testCases(&.{
|
// try runner.testCases(&.{
|
||||||
.{ "window.parent === window", "true" },
|
// .{ "window.parent === window", "true" },
|
||||||
.{ "window.top === 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(&.{
|
try runner.testCases(&.{
|
||||||
.{
|
.{
|
||||||
\\ let start = 0;
|
\\ let start = 0;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ const Renderer = @import("renderer.zig").Renderer;
|
|||||||
const Window = @import("html/window.zig").Window;
|
const Window = @import("html/window.zig").Window;
|
||||||
const Walker = @import("dom/walker.zig").WalkerDepthFirst;
|
const Walker = @import("dom/walker.zig").WalkerDepthFirst;
|
||||||
const Loop = @import("../runtime/loop.zig").Loop;
|
const Loop = @import("../runtime/loop.zig").Loop;
|
||||||
|
const Scheduler = @import("Scheduler.zig");
|
||||||
const ScriptManager = @import("ScriptManager.zig");
|
const ScriptManager = @import("ScriptManager.zig");
|
||||||
const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
const HTMLDocument = @import("html/document.zig").HTMLDocument;
|
||||||
|
|
||||||
@@ -75,11 +76,6 @@ pub const Page = struct {
|
|||||||
|
|
||||||
renderer: Renderer,
|
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,
|
keydown_event_node: parser.EventNode,
|
||||||
window_clicked_event_node: parser.EventNode,
|
window_clicked_event_node: parser.EventNode,
|
||||||
|
|
||||||
@@ -94,9 +90,9 @@ pub const Page = struct {
|
|||||||
|
|
||||||
polyfill_loader: polyfill.Loader = .{},
|
polyfill_loader: polyfill.Loader = .{},
|
||||||
|
|
||||||
|
scheduler: Scheduler,
|
||||||
http_client: *http.Client,
|
http_client: *http.Client,
|
||||||
script_manager: ScriptManager,
|
script_manager: ScriptManager,
|
||||||
|
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
|
|
||||||
loaded: bool = false,
|
loaded: bool = false,
|
||||||
@@ -127,8 +123,7 @@ pub const Page = struct {
|
|||||||
.cookie_jar = &session.cookie_jar,
|
.cookie_jar = &session.cookie_jar,
|
||||||
.script_manager = script_manager,
|
.script_manager = script_manager,
|
||||||
.http_client = browser.http_client,
|
.http_client = browser.http_client,
|
||||||
.microtask_node = .{ .func = microtaskCallback },
|
.scheduler = Scheduler.init(arena),
|
||||||
.messageloop_node = .{ .func = messageLoopCallback },
|
|
||||||
.keydown_event_node = .{ .func = keydownCallback },
|
.keydown_event_node = .{ .func = keydownCallback },
|
||||||
.window_clicked_event_node = .{ .func = windowClicked },
|
.window_clicked_event_node = .{ .func = windowClicked },
|
||||||
// @newhttp
|
// @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));
|
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 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
|
// message loop must run only non-test env
|
||||||
if (comptime !builtin.is_test) {
|
if (comptime !builtin.is_test) {
|
||||||
_ = try session.browser.app.loop.timeout(1 * std.time.ns_per_ms, &self.microtask_node);
|
try self.scheduler.add(self, runMessageLoop, 5, .{ .name = "page.messageLoop" });
|
||||||
_ = try session.browser.app.loop.timeout(100 * std.time.ns_per_ms, &self.messageloop_node);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,16 +146,16 @@ pub const Page = struct {
|
|||||||
self.script_manager.deinit();
|
self.script_manager.deinit();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn microtaskCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
fn runMicrotasks(ctx: *anyopaque) ?u32 {
|
||||||
const self: *Page = @fieldParentPtr("microtask_node", node);
|
const self: *Page = @alignCast(@ptrCast(ctx));
|
||||||
self.session.browser.runMicrotasks();
|
self.session.browser.runMicrotasks();
|
||||||
repeat_delay.* = 1 * std.time.ns_per_ms;
|
return 5;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn messageLoopCallback(node: *Loop.CallbackNode, repeat_delay: *?u63) void {
|
fn runMessageLoop(ctx: *anyopaque) ?u32 {
|
||||||
const self: *Page = @fieldParentPtr("messageloop_node", node);
|
const self: *Page = @alignCast(@ptrCast(ctx));
|
||||||
self.session.browser.runMessageLoop();
|
self.session.browser.runMessageLoop();
|
||||||
repeat_delay.* = 100 * std.time.ns_per_ms;
|
return 100;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const DumpOpts = struct {
|
pub const DumpOpts = struct {
|
||||||
@@ -237,39 +232,66 @@ pub const Page = struct {
|
|||||||
// The HTML page was parsed. We now either have JS scripts to
|
// The HTML page was parsed. We now either have JS scripts to
|
||||||
// download, or timeouts to execute, or both.
|
// download, or timeouts to execute, or both.
|
||||||
|
|
||||||
const cutoff = timestamp() + wait_sec;
|
|
||||||
|
|
||||||
var try_catch: Env.TryCatch = undefined;
|
var try_catch: Env.TryCatch = undefined;
|
||||||
try_catch.init(self.main_context);
|
try_catch.init(self.main_context);
|
||||||
defer try_catch.deinit();
|
defer try_catch.deinit();
|
||||||
|
|
||||||
|
var scheduler = &self.scheduler;
|
||||||
var http_client = self.http_client;
|
var http_client = self.http_client;
|
||||||
var loop = self.session.browser.app.loop;
|
|
||||||
|
|
||||||
// @newhttp Not sure about the timing / the order / any of this.
|
var ms_remaining = wait_sec * 1000;
|
||||||
// I think I want to remove the loop. Implement our own timeouts
|
var timer = try std.time.Timer.start();
|
||||||
// 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!has_pending_timeouts) {
|
while (true) {
|
||||||
continue;
|
const has_active_http = http_client.active > 0;
|
||||||
}
|
|
||||||
|
|
||||||
// 10ms
|
const ms_to_next_task = try scheduler.run(has_active_http);
|
||||||
try loop.run(std.time.ns_per_ms * 10);
|
|
||||||
|
|
||||||
if (try_catch.hasCaught()) {
|
if (try_catch.hasCaught()) {
|
||||||
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
const msg = (try try_catch.err(self.arena)) orelse "unknown";
|
||||||
log.err(.browser, "page wait error", .{ .err = msg });
|
log.err(.browser, "page wait error", .{ .err = msg });
|
||||||
return error.JsError;
|
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,
|
.err => |err| return err,
|
||||||
|
|||||||
@@ -354,7 +354,7 @@ pub const XMLHttpRequest = struct {
|
|||||||
|
|
||||||
return self.headers.append(
|
return self.headers.append(
|
||||||
self.arena,
|
self.arena,
|
||||||
try std.fmt.allocPrintZ(self.arena, "{s}: {s}", .{name, value}),
|
try std.fmt.allocPrintZ(self.arena, "{s}: {s}", .{ name, value }),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -731,7 +731,7 @@ pub const XMLHttpRequest = struct {
|
|||||||
if (entry[name.len] != ':') {
|
if (entry[name.len] != ':') {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
return std.mem.trimLeft(u8, entry[name.len + 1..], " ");
|
return std.mem.trimLeft(u8, entry[name.len + 1 ..], " ");
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
@@ -793,12 +793,9 @@ test "Browser.XHR.XMLHttpRequest" {
|
|||||||
.{ "req.status", "200" },
|
.{ "req.status", "200" },
|
||||||
.{ "req.statusText", "OK" },
|
.{ "req.statusText", "OK" },
|
||||||
.{ "req.getResponseHeader('Content-Type')", "text/html; charset=utf-8" },
|
.{ "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" ++
|
"Content-Type: text/html; charset=utf-8\r\n" ++
|
||||||
"Connection: Close\r\n"
|
"Connection: Close\r\n" },
|
||||||
},
|
|
||||||
.{ "req.responseText.length", "100" },
|
.{ "req.responseText.length", "100" },
|
||||||
.{ "req.response.length == req.responseText.length", "true" },
|
.{ "req.response.length == req.responseText.length", "true" },
|
||||||
.{ "req.responseXML instanceof Document", "true" },
|
.{ "req.responseXML instanceof Document", "true" },
|
||||||
|
|||||||
@@ -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.*);
|
const cert = try std.crypto.Certificate.der.Element.parse(bytes, index.*);
|
||||||
|
|
||||||
try writer.writeAll("-----BEGIN CERTIFICATE-----\n");
|
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 encoder.encodeWriter(&line_writer, bytes[index.*..cert.slice.end]);
|
||||||
try writer.writeAll("\n-----END CERTIFICATE-----\n");
|
try writer.writeAll("\n-----END CERTIFICATE-----\n");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -335,12 +335,12 @@ pub const Request = struct {
|
|||||||
// arbitrary data that can be associated with this request
|
// arbitrary data that can be associated with this request
|
||||||
ctx: *anyopaque = undefined,
|
ctx: *anyopaque = undefined,
|
||||||
|
|
||||||
start_callback: ?*const fn(req: *Transfer) 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_callback: ?*const fn (req: *Transfer, header: []const u8) anyerror!void = null,
|
||||||
header_done_callback: *const fn (req: *Transfer) anyerror!void,
|
header_done_callback: *const fn (req: *Transfer) anyerror!void,
|
||||||
data_callback: *const fn(req: *Transfer, data: []const u8) anyerror!void,
|
data_callback: *const fn (req: *Transfer, data: []const u8) anyerror!void,
|
||||||
done_callback: *const fn(req: *Transfer) anyerror!void,
|
done_callback: *const fn (req: *Transfer) anyerror!void,
|
||||||
error_callback: *const fn(req: *Transfer, err: anyerror) void,
|
error_callback: *const fn (req: *Transfer, err: anyerror) void,
|
||||||
};
|
};
|
||||||
|
|
||||||
pub const Transfer = struct {
|
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 {
|
pub fn format(self: *const Transfer, comptime _: []const u8, _: std.fmt.FormatOptions, writer: anytype) !void {
|
||||||
const req = self.req;
|
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 {
|
fn onError(self: *Transfer, err: anyerror) void {
|
||||||
@@ -385,7 +385,7 @@ pub const Transfer = struct {
|
|||||||
pub fn abort(self: *Transfer) void {
|
pub fn abort(self: *Transfer) void {
|
||||||
var client = self.handle.client;
|
var client = self.handle.client;
|
||||||
errorMCheck(c.curl_multi_remove_handle(client.multi, self.handle.easy)) catch |err| {
|
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;
|
client.active -= 1;
|
||||||
self.deinit();
|
self.deinit();
|
||||||
@@ -397,13 +397,13 @@ pub const Transfer = struct {
|
|||||||
|
|
||||||
const handle: *Handle = @alignCast(@ptrCast(data));
|
const handle: *Handle = @alignCast(@ptrCast(data));
|
||||||
var transfer = fromEasy(handle.easy) catch |err| {
|
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;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
std.debug.assert(std.mem.endsWith(u8, buffer[0..buf_len], "\r\n"));
|
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 (transfer.response_header == null) {
|
||||||
if (buf_len < 13 or std.mem.startsWith(u8, header, "HTTP/") == false) {
|
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));
|
const handle: *Handle = @alignCast(@ptrCast(data));
|
||||||
var transfer = fromEasy(handle.easy) catch |err| {
|
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;
|
return c.CURL_WRITEFUNC_ERROR;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -549,4 +549,3 @@ pub const ProxyAuth = union(enum) {
|
|||||||
basic: struct { user_pass: []const u8 },
|
basic: struct { user_pass: []const u8 },
|
||||||
bearer: struct { token: []const u8 },
|
bearer: struct { token: []const u8 },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@
|
|||||||
const std = @import("std");
|
const std = @import("std");
|
||||||
const c = @import("client.zig").c;
|
const c = @import("client.zig").c;
|
||||||
|
|
||||||
pub const Error = error {
|
pub const Error = error{
|
||||||
UnsupportedProtocol,
|
UnsupportedProtocol,
|
||||||
FailedInit,
|
FailedInit,
|
||||||
UrlMalformat,
|
UrlMalformat,
|
||||||
@@ -201,7 +201,7 @@ pub fn fromCode(code: c.CURLcode) Error {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub const Multi = error {
|
pub const Multi = error{
|
||||||
BadHandle,
|
BadHandle,
|
||||||
BadEasyHandle,
|
BadEasyHandle,
|
||||||
OutOfMemory,
|
OutOfMemory,
|
||||||
|
|||||||
Reference in New Issue
Block a user